opossum 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 072b3d832b3017a8625ed6bd5c16740566eaf4bac7b79347c55834d99b085d90
4
- data.tar.gz: 14faf920b01e184b49e434e7ea21e76f2eec76969cf44b6b44d25c37fe83010c
3
+ metadata.gz: fc7e39df727f7bb72b25088d9dccf4a0926dcaba41c624e27045f455c08650be
4
+ data.tar.gz: 32977c12dca6c24b2182e791cb3081705a08fd2b5c76f717ecceb0db03e5e14d
5
5
  SHA512:
6
- metadata.gz: 91125cac82c4d80569be8eb23ec46ebf66daf68b0abc3dfbfb4178ba4730c6889968cc5022578eb14267e58d66c27d50713b948258fea1139fe4065efc28f67e
7
- data.tar.gz: 98c0a00e4909966db372da82f7177dc8fdb0d139bbe924f4d5366e5623b7bf98f2540bf76f83d8b5fcd299c6e90942def270d22b991f8e543752d2b1c49fa7e9
6
+ metadata.gz: a34ab41b169655e35028138e18cb111810c2ecd4beef2e83fd4b9ecad8b1f3bdc59fb6c1a0cbb124c0bcc5675637095411443472d248fb722dc1e66067bc6865
7
+ data.tar.gz: 26319316d4a349ca1c3b0a6663d6e8e6198b714c60a151d800f6bff13a956cd0444e19b6f34a2a69efb29c3531ffed7d045c5cf0321403f931d9c048a54c5cb3
data/CHANGELOG.md CHANGED
@@ -1,7 +1,54 @@
1
- ## [Unreleased]
1
+ ## [0.2.1] - 2025-08-07
2
2
 
3
3
  ### Improved
4
- - **Code Quality** - Minor code style improvements for better readability
4
+ - **Instagram API Response Handling** - Enhanced error handling and response processing
5
+ - Improved JSON parsing with better error messages when API returns invalid JSON
6
+ - Added specific Instagram API error detection and messaging via `error_message` field
7
+ - Enhanced HTTP error handling with more descriptive error messages
8
+ - Better separation of concerns in ApiHelper with dedicated private methods for response processing
9
+ - More robust error handling chain: HTTP errors → JSON parsing errors → Instagram API errors
10
+
11
+ ## [0.2.0] - 2025-08-06
12
+
13
+ ### Changed
14
+ - **Publisher class initialization** - Improved API design for better usability
15
+ - Constructor now requires `ig_id` parameter: `Publisher.new(access_token: token, ig_id: account_id)`
16
+ - `publish_media` method no longer requires `ig_id` parameter (stored in instance)
17
+ - Simplified method calls: `publisher.publish_media(media_url: url, media_type: 'IMAGE')`
18
+ - Breaking change: existing code needs to be updated to new initialization pattern
19
+
20
+ ### Added
21
+ - **Authenticator#get_user_info_from_code** - New convenience method that combines authentication flow
22
+ - Exchanges authorization code for access token
23
+ - Gets long-lived access token (60-day validity)
24
+ - Optionally retrieves user information with specified fields
25
+ - Returns `{ access_token: token_hash }` or `{ access_token: token_hash, user_details: user_info }`
26
+ - Simplifies common authentication workflow into single method call
27
+
28
+ ### Improved
29
+ - **UserDetails API** - All methods now return hashes with symbol keys instead of string keys
30
+ - `get_user_info` returns `{ id: "...", username: "...", media_count: 42 }` (Ruby convention)
31
+ - `get_long_lived_access_token` returns `{ access_token: "...", token_type: "bearer", expires_in: 5184000 }`
32
+ - `refresh_access_token` returns `{ access_token: "...", token_type: "bearer", expires_in: 5184000 }`
33
+ - **Publisher class refactoring** - Improved code structure and maintainability
34
+ - Split large methods into smaller, focused functions for better readability
35
+ - Extracted media container body building logic into separate method
36
+ - Separated carousel media preparation logic for better organization
37
+ - Improved status handling with dedicated error management methods
38
+ - All methods now comply with linting standards (≤10 lines, ≤5 parameters)
39
+ - Enhanced code modularity while maintaining backward compatibility
40
+
41
+ ### Tests
42
+ - **Complete test suite** - Added comprehensive RSpec test coverage for all components
43
+ - **Authenticator tests** - Full coverage including new `get_user_info_from_code` method
44
+ - Tests for both scenarios: with and without fields parameter
45
+ - Tests for empty fields array handling
46
+ - Error handling tests for all possible failure points
47
+ - Mock-based testing with proper isolation
48
+ - **UserDetails tests** - Updated to work with symbol keys
49
+ - **Publisher tests** - Complete media publishing workflow coverage
50
+ - **ApiHelper tests** - HTTP request handling and error scenarios
51
+ - All 83 tests passing with comprehensive coverage
5
52
 
6
53
  ## [0.1.0] - 2025-07-31
7
54
 
data/README.md CHANGED
@@ -16,7 +16,7 @@ Before using this gem, you need to:
16
16
  Add this line to your application's Gemfile:
17
17
 
18
18
  ```ruby
19
- gem 'instagram_publish_api_via_instagram_login'
19
+ gem 'opossum'
20
20
  ```
21
21
 
22
22
  And then execute:
@@ -28,7 +28,7 @@ bundle install
28
28
  Or install it yourself as:
29
29
 
30
30
  ```bash
31
- gem install instagram_publish_api_via_instagram_login
31
+ gem install oposum
32
32
  ```
33
33
 
34
34
  ## Usage
@@ -38,7 +38,7 @@ This gem provides three independent classes for working with Instagram API:
38
38
  ### Authentication
39
39
 
40
40
  ```ruby
41
- require 'instagram_publish_api_via_instagram_login'
41
+ require 'opossum'
42
42
 
43
43
  # Create authenticator
44
44
  authenticator = Opossum::Authenticator.new(
@@ -47,27 +47,42 @@ authenticator = Opossum::Authenticator.new(
47
47
  redirect_uri: 'your_redirect_uri'
48
48
  )
49
49
 
50
- # Exchange code for token
51
- response = authenticator.exchange_code_for_token(authorization_code)
52
- access_token = response['access_token']
50
+ # Get user info and long-lived token from authorization code
51
+ result = authenticator.get_user_info_from_code(
52
+ authorization_code,
53
+ fields: 'id,user_id'
54
+ )
55
+ # Returns:
56
+ # {
57
+ # access_token: {
58
+ # access_token: "ACCESS_TOKEN",
59
+ # token_type: "TOKEN_TYPE",
60
+ # expires_in: 5183742
61
+ # },
62
+ # user_details: {
63
+ # id: "IG_ID",
64
+ # user_id: "IG_USER_ID"
65
+ # }
66
+ # }
67
+
68
+ # Or get only long-lived token without user details
69
+ result = authenticator.get_user_info_from_code(authorization_code)
70
+ # Returns:
71
+ # {
72
+ # access_token: {
73
+ # access_token: "ACCESS_TOKEN",
74
+ # token_type: "TOKEN_TYPE",
75
+ # expires_in: 5183742
76
+ # }
77
+ # }
53
78
  ```
54
79
 
55
- ### User Information
80
+ ### User Details (Optional)
56
81
 
57
82
  ```ruby
58
- # Create user details instance
83
+ # If you need to refresh an existing access token
59
84
  user_details = Opossum::UserDetails.new(
60
- access_token: access_token
61
- )
62
-
63
- # Get user info
64
- user_info = user_details.get_user_info(
65
- fields: 'id,username,account_type'
66
- )
67
-
68
- # Get long-lived access token (60 days)
69
- long_lived_token = user_details.get_long_lived_access_token(
70
- client_secret: 'your_app_secret'
85
+ access_token: existing_access_token
71
86
  )
72
87
 
73
88
  # Refresh access token (extends for another 60 days)
@@ -77,21 +92,20 @@ refreshed_token = user_details.refresh_access_token
77
92
  ### Publishing
78
93
 
79
94
  ```ruby
80
- # Create publisher with access token
95
+ # Create publisher with access token and Instagram business account ID
81
96
  publisher = Opossum::Publisher.new(
82
- access_token: access_token
97
+ access_token: access_token,
98
+ ig_id: instagram_business_account_id
83
99
  )
84
100
 
85
101
  # Publish single image
86
102
  result = publisher.publish_media(
87
- ig_id: instagram_business_account_id,
88
103
  media_url: 'https://example.com/image.jpg',
89
104
  media_type: 'IMAGE'
90
105
  )
91
106
 
92
107
  # Publish image with caption
93
108
  result = publisher.publish_media(
94
- ig_id: instagram_business_account_id,
95
109
  media_url: 'https://example.com/image.jpg',
96
110
  media_type: 'IMAGE',
97
111
  caption: 'Beautiful sunset! 🌅 #nature #photography'
@@ -99,7 +113,6 @@ result = publisher.publish_media(
99
113
 
100
114
  # Publish carousel with caption
101
115
  result = publisher.publish_media(
102
- ig_id: instagram_business_account_id,
103
116
  media_url: [
104
117
  'https://example.com/image1.jpg',
105
118
  'https://example.com/image2.jpg'
@@ -118,20 +131,33 @@ result = publisher.publish_media(
118
131
 
119
132
  ## Error Handling
120
133
 
121
- The gem includes comprehensive error handling for API responses:
134
+ The gem includes comprehensive error handling for API responses with enhanced Instagram API error detection:
122
135
 
123
136
  ```ruby
124
137
  begin
125
- result = client.publish_media(
126
- ig_id: instagram_business_account_id,
138
+ publisher = Opossum::Publisher.new(
127
139
  access_token: access_token,
140
+ ig_id: instagram_business_account_id
141
+ )
142
+
143
+ result = publisher.publish_media(
128
144
  media_url: 'https://example.com/image.jpg'
129
145
  )
130
146
  rescue Opossum::Error => e
131
147
  puts "Error: #{e.message}"
148
+ # Examples of error messages:
149
+ # "HTTP Error: Connection failed"
150
+ # "JSON Parse Error: Unexpected token"
151
+ # "Instagram API Error: Invalid media URL"
132
152
  end
133
153
  ```
134
154
 
155
+ **Error Handling Features:**
156
+ - **HTTP Error Detection** - Catches network and connection issues with descriptive messages
157
+ - **JSON Parsing** - Handles malformed API responses with clear error descriptions
158
+ - **Instagram API Errors** - Automatically detects and reports Instagram-specific errors via `error_message` field
159
+ - **Error Chain Processing** - Processes errors in logical order: HTTP → JSON → Instagram API
160
+
135
161
  ## Supported Media Types
136
162
 
137
163
  - **IMAGE** - Single images (JPEG, PNG) with optional caption
@@ -154,7 +180,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
154
180
 
155
181
  ## Contributing
156
182
 
157
- Bug reports and pull requests are welcome on GitHub at https://github.com/ninjarender/instagram_publish_api_via_instagram_login. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/instagram_publish_api_via_instagram_login/blob/main/CODE_OF_CONDUCT.md).
183
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ninjarender/opossum. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/opossum/blob/main/CODE_OF_CONDUCT.md).
158
184
 
159
185
  ## License
160
186
 
@@ -162,4 +188,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
162
188
 
163
189
  ## Code of Conduct
164
190
 
165
- Everyone interacting in the OpossumInstagramPublisher project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ninjarender/instagram_publish_api_via_instagram_login/blob/main/CODE_OF_CONDUCT.md).
191
+ Everyone interacting in the OpossumInstagramPublisher project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ninjarender/opossum/blob/main/CODE_OF_CONDUCT.md).
@@ -36,8 +36,6 @@ module Opossum
36
36
  end
37
37
 
38
38
  def handle_response(response)
39
- raise Opossum::Error, "HTTP #{response.status}: #{response.body}" unless response.success?
40
-
41
39
  parsed_response = parse_json(response.body)
42
40
  check_api_errors(parsed_response)
43
41
  parsed_response
@@ -52,11 +50,9 @@ module Opossum
52
50
  end
53
51
 
54
52
  def check_api_errors(parsed_response)
55
- return unless parsed_response["error"]
53
+ return unless parsed_response["error_message"]
56
54
 
57
- error_message = "Instagram API Error: #{parsed_response["error"]}"
58
- error_message += " - #{parsed_response["error_description"]}" if parsed_response["error_description"]
59
- raise Opossum::Error, error_message
55
+ raise Opossum::Error, "Instagram API Error: #{parsed_response["error_message"]}"
60
56
  end
61
57
  end
62
58
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "api_helper"
4
+ require_relative "user_details"
4
5
 
5
6
  module Opossum
6
7
  # Handles Instagram authentication flow
@@ -23,16 +24,32 @@ module Opossum
23
24
  )
24
25
  end
25
26
 
27
+ def get_user_info_from_code(code, fields: "")
28
+ access_token = exchange_code_for_token(code)
29
+
30
+ user_details_client = UserDetails.new(access_token: access_token["access_token"])
31
+ long_lived_access_token = user_details_client.get_long_lived_access_token(client_secret: client_secret)
32
+
33
+ return { access_token: long_lived_access_token } if !fields.is_a?(String) || fields.empty?
34
+
35
+ user_details = user_details_client.get_user_info(fields: fields)
36
+
37
+ {
38
+ access_token: long_lived_access_token,
39
+ user_details: user_details
40
+ }
41
+ end
42
+
26
43
  private
27
44
 
28
45
  attr_reader :client_id, :client_secret, :redirect_uri
29
46
 
30
47
  def token_request_params(code)
31
48
  {
32
- client_id: @client_id,
33
- client_secret: @client_secret,
49
+ client_id: client_id,
50
+ client_secret: client_secret,
34
51
  grant_type: "authorization_code",
35
- redirect_uri: @redirect_uri,
52
+ redirect_uri: redirect_uri,
36
53
  code: code
37
54
  }
38
55
  end
@@ -6,10 +6,15 @@ require_relative "base_client"
6
6
  module Opossum
7
7
  # Handles Instagram media publishing
8
8
  class Publisher < BaseClient
9
- def publish_media(ig_id:, media_url:, media_type: "IMAGE", caption: nil)
9
+ def initialize(access_token:, ig_id:)
10
+ super(access_token: access_token)
11
+
12
+ @ig_id = ig_id
13
+ end
14
+
15
+ def publish_media(media_url:, media_type: "IMAGE", caption: nil)
10
16
  path = "#{INSTAGRAM_GRAPH_API_ENDPOINT}/#{GRAPH_API_VERSION}/#{ig_id}/media_publish"
11
- media_container_id = prepare_media_container(ig_id: ig_id, media_url: media_url,
12
- media_type: media_type, caption: caption)
17
+ media_container_id = prepare_media_container(media_url: media_url, media_type: media_type, caption: caption)
13
18
 
14
19
  ApiHelper.post(
15
20
  path: path,
@@ -19,30 +24,45 @@ module Opossum
19
24
 
20
25
  private
21
26
 
22
- def prepare_media_container(ig_id:, media_url:, media_type:, caption:)
27
+ attr_reader :ig_id
28
+
29
+ def prepare_media_container(media_url:, media_type:, caption:)
23
30
  if media_url.is_a?(Array)
24
- children_ids = media_url.map do |url|
25
- create_media_container(
26
- ig_id: ig_id, media_url: url, is_carousel_item: true, caption: caption
27
- )
28
- end
29
-
30
- create_media_container(
31
- ig_id: ig_id, media_url: children_ids, media_type: media_type, caption: caption
32
- )
31
+ prepare_carousel_media(media_urls: media_url, media_type: media_type, caption: caption)
33
32
  else
34
- create_media_container(
35
- ig_id: ig_id, media_url: media_url, media_type: media_type, caption: caption
36
- )
33
+ create_media_container(media_url: media_url, media_type: media_type, caption: caption)
37
34
  end
38
35
  end
39
36
 
40
- def create_media_container(ig_id:, media_url:, media_type: "IMAGE", is_carousel_item: false, upload_type: nil,
41
- caption: nil)
37
+ def prepare_carousel_media(media_urls:, media_type:, caption:)
38
+ children_ids = media_urls.map do |url|
39
+ create_media_container(media_url: url, is_carousel_item: true, caption: caption)
40
+ end
41
+
42
+ create_media_container(media_url: children_ids, media_type: media_type, caption: caption)
43
+ end
44
+
45
+ def create_media_container(media_url:, media_type: "IMAGE", is_carousel_item: false, upload_type: nil, caption: nil)
42
46
  path = "#{INSTAGRAM_GRAPH_API_ENDPOINT}/#{GRAPH_API_VERSION}/#{ig_id}/media"
47
+ body = build_media_container_body(media_url: media_url, media_type: media_type, caption: caption,
48
+ is_carousel_item: is_carousel_item, upload_type: upload_type)
43
49
 
50
+ response = ApiHelper.post(path: path, body: body)
51
+ media_container_id = response["id"]
52
+
53
+ wait_for_media_container_status(media_container_id: media_container_id)
54
+ media_container_id
55
+ end
56
+
57
+ def build_media_container_body(media_url:, media_type:, caption:, is_carousel_item:, upload_type:)
44
58
  body = { access_token: access_token, media_type: media_type, caption: caption }
59
+ set_media_url_field(body, media_url, media_type)
60
+ body[:is_carousel_item] = is_carousel_item if is_carousel_item
61
+ body[:upload_type] = upload_type if upload_type
62
+ body
63
+ end
45
64
 
65
+ def set_media_url_field(body, media_url, media_type)
46
66
  case media_type
47
67
  when "IMAGE"
48
68
  body[:image_url] = media_url
@@ -51,39 +71,35 @@ module Opossum
51
71
  when "CAROUSEL"
52
72
  body[:children] = media_url
53
73
  end
54
-
55
- body[:is_carousel_item] = is_carousel_item if is_carousel_item
56
- body[:upload_type] = upload_type if upload_type
57
-
58
- response = ApiHelper.post(path: path, body: body)
59
- media_container_id = response["id"]
60
-
61
- wait_for_media_container_status(media_container_id: media_container_id)
62
-
63
- media_container_id
64
74
  end
65
75
 
66
76
  def wait_for_media_container_status(media_container_id:)
67
77
  loop do
68
78
  status = check_media_container_status(media_container_id: media_container_id)["status"]
79
+ break if handle_media_container_status(status)
80
+ end
81
+ end
69
82
 
70
- case status
71
- when "FINISHED"
72
- break
73
- when "IN_PROGRESS"
74
- sleep 30
75
- when "EXPIRED"
76
- raise "Media container has expired. The container was not published within 24 hours."
77
- when "ERROR"
78
- raise "Media container failed to complete the publishing process."
79
- when "PUBLISHED"
80
- raise "Media container has already been published."
81
- else
82
- raise "Unknown media container status: #{status}"
83
- end
83
+ def handle_media_container_status(status)
84
+ case status
85
+ when "FINISHED" then true
86
+ when "IN_PROGRESS"
87
+ sleep 30
88
+ false
89
+ when "EXPIRED", "ERROR", "PUBLISHED" then raise_status_error(status)
90
+ else raise "Unknown media container status: #{status}"
84
91
  end
85
92
  end
86
93
 
94
+ def raise_status_error(status)
95
+ messages = {
96
+ "EXPIRED" => "Media container has expired. The container was not published within 24 hours.",
97
+ "ERROR" => "Media container failed to complete the publishing process.",
98
+ "PUBLISHED" => "Media container has already been published."
99
+ }
100
+ raise messages[status]
101
+ end
102
+
87
103
  def check_media_container_status(media_container_id:)
88
104
  path = "#{INSTAGRAM_GRAPH_API_ENDPOINT}/#{GRAPH_API_VERSION}/#{media_container_id}?fields=status_code"
89
105
 
@@ -12,7 +12,7 @@ module Opossum
12
12
  ApiHelper.get(
13
13
  path: path,
14
14
  params: { access_token: access_token, fields: fields }
15
- )
15
+ ).transform_keys(&:to_sym)
16
16
  end
17
17
 
18
18
  def get_long_lived_access_token(client_secret:)
@@ -21,7 +21,7 @@ module Opossum
21
21
  ApiHelper.get(
22
22
  path: path,
23
23
  params: { access_token: access_token, client_secret: client_secret, grant_type: "ig_exchange_token" }
24
- )
24
+ ).transform_keys(&:to_sym)
25
25
  end
26
26
 
27
27
  def refresh_access_token
@@ -30,7 +30,7 @@ module Opossum
30
30
  ApiHelper.get(
31
31
  path: path,
32
32
  params: { access_token: access_token, grant_type: "ig_refresh_token" }
33
- )
33
+ ).transform_keys(&:to_sym)
34
34
  end
35
35
  end
36
36
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Opossum
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opossum
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vadym Kruchyna
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-01 00:00:00.000000000 Z
11
+ date: 2025-08-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday