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 +4 -4
- data/CHANGELOG.md +39 -2
- data/README.md +44 -28
- data/lib/opossum/authenticator.rb +20 -3
- data/lib/opossum/publisher.rb +58 -42
- data/lib/opossum/user_details.rb +3 -3
- data/lib/opossum/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28f90ba90661894c077eb72004e0300f07e9c2d93059d8221985c396b467b74e
|
4
|
+
data.tar.gz: 29afd0da8dd9f9192caeea525f92365637b75d15816fca846a0a577049ea56d9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf6f1c1f02604906beb4ea09fb3af644ede569c4ec9dcf962a6f2d851f2ee5fbfedd4a2ade0831b0a677b8ae78e51ced66a919eb12237820658b74014c150d5f
|
7
|
+
data.tar.gz: ccba27fcb4c858e825ca63f494ab97d6246e58078a4369274373170dab71cb11e20dcec00789e924dc52312c696f1073b740a27bd76e99c6e3659dc6e49227c8
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,44 @@
|
|
1
|
-
## [
|
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
|
-
- **
|
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 '
|
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
|
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 '
|
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
|
-
#
|
51
|
-
|
52
|
-
|
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
|
80
|
+
### User Details (Optional)
|
56
81
|
|
57
82
|
```ruby
|
58
|
-
#
|
83
|
+
# If you need to refresh an existing access token
|
59
84
|
user_details = Opossum::UserDetails.new(
|
60
|
-
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
|
-
|
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/
|
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/
|
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:
|
33
|
-
client_secret:
|
49
|
+
client_id: client_id,
|
50
|
+
client_secret: client_secret,
|
34
51
|
grant_type: "authorization_code",
|
35
|
-
redirect_uri:
|
52
|
+
redirect_uri: redirect_uri,
|
36
53
|
code: code
|
37
54
|
}
|
38
55
|
end
|
data/lib/opossum/publisher.rb
CHANGED
@@ -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
|
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(
|
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
|
-
|
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
|
-
|
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
|
41
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
|
data/lib/opossum/user_details.rb
CHANGED
@@ -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
|
data/lib/opossum/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2025-08-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|