opossum 0.1.0 → 0.2.0

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: 28f90ba90661894c077eb72004e0300f07e9c2d93059d8221985c396b467b74e
4
+ data.tar.gz: 29afd0da8dd9f9192caeea525f92365637b75d15816fca846a0a577049ea56d9
5
5
  SHA512:
6
- metadata.gz: 91125cac82c4d80569be8eb23ec46ebf66daf68b0abc3dfbfb4178ba4730c6889968cc5022578eb14267e58d66c27d50713b948258fea1139fe4065efc28f67e
7
- data.tar.gz: 98c0a00e4909966db372da82f7177dc8fdb0d139bbe924f4d5366e5623b7bf98f2540bf76f83d8b5fcd299c6e90942def270d22b991f8e543752d2b1c49fa7e9
6
+ metadata.gz: cf6f1c1f02604906beb4ea09fb3af644ede569c4ec9dcf962a6f2d851f2ee5fbfedd4a2ade0831b0a677b8ae78e51ced66a919eb12237820658b74014c150d5f
7
+ data.tar.gz: ccba27fcb4c858e825ca63f494ab97d6246e58078a4369274373170dab71cb11e20dcec00789e924dc52312c696f1073b740a27bd76e99c6e3659dc6e49227c8
data/CHANGELOG.md CHANGED
@@ -1,7 +1,44 @@
1
- ## [Unreleased]
1
+ ## [0.2.0] - 2025-08-06
2
+
3
+ ### Changed
4
+ - **Publisher class initialization** - Improved API design for better usability
5
+ - Constructor now requires `ig_id` parameter: `Publisher.new(access_token: token, ig_id: account_id)`
6
+ - `publish_media` method no longer requires `ig_id` parameter (stored in instance)
7
+ - Simplified method calls: `publisher.publish_media(media_url: url, media_type: 'IMAGE')`
8
+ - Breaking change: existing code needs to be updated to new initialization pattern
9
+
10
+ ### Added
11
+ - **Authenticator#get_user_info_from_code** - New convenience method that combines authentication flow
12
+ - Exchanges authorization code for access token
13
+ - Gets long-lived access token (60-day validity)
14
+ - Optionally retrieves user information with specified fields
15
+ - Returns `{ access_token: token_hash }` or `{ access_token: token_hash, user_details: user_info }`
16
+ - Simplifies common authentication workflow into single method call
2
17
 
3
18
  ### Improved
4
- - **Code Quality** - Minor code style improvements for better readability
19
+ - **UserDetails API** - All methods now return hashes with symbol keys instead of string keys
20
+ - `get_user_info` returns `{ id: "...", username: "...", media_count: 42 }` (Ruby convention)
21
+ - `get_long_lived_access_token` returns `{ access_token: "...", token_type: "bearer", expires_in: 5184000 }`
22
+ - `refresh_access_token` returns `{ access_token: "...", token_type: "bearer", expires_in: 5184000 }`
23
+ - **Publisher class refactoring** - Improved code structure and maintainability
24
+ - Split large methods into smaller, focused functions for better readability
25
+ - Extracted media container body building logic into separate method
26
+ - Separated carousel media preparation logic for better organization
27
+ - Improved status handling with dedicated error management methods
28
+ - All methods now comply with linting standards (≤10 lines, ≤5 parameters)
29
+ - Enhanced code modularity while maintaining backward compatibility
30
+
31
+ ### Tests
32
+ - **Complete test suite** - Added comprehensive RSpec test coverage for all components
33
+ - **Authenticator tests** - Full coverage including new `get_user_info_from_code` method
34
+ - Tests for both scenarios: with and without fields parameter
35
+ - Tests for empty fields array handling
36
+ - Error handling tests for all possible failure points
37
+ - Mock-based testing with proper isolation
38
+ - **UserDetails tests** - Updated to work with symbol keys
39
+ - **Publisher tests** - Complete media publishing workflow coverage
40
+ - **ApiHelper tests** - HTTP request handling and error scenarios
41
+ - All 83 tests passing with comprehensive coverage
5
42
 
6
43
  ## [0.1.0] - 2025-07-31
7
44
 
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'
@@ -122,9 +135,12 @@ The gem includes comprehensive error handling for API responses:
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
@@ -154,7 +170,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
154
170
 
155
171
  ## Contributing
156
172
 
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).
173
+ 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
174
 
159
175
  ## License
160
176
 
@@ -162,4 +178,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
162
178
 
163
179
  ## Code of Conduct
164
180
 
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).
181
+ 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).
@@ -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.0"
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.0
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