feed2gram 1.2.3 → 1.3.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 +10 -0
- data/README.md +5 -2
- data/example/Gemfile +1 -0
- data/lib/feed2gram/publishes_posts.rb +93 -42
- data/lib/feed2gram/refreshes_token.rb +14 -6
- data/lib/feed2gram/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 73e7d2df466144a3e956cc38742d783cce241a65894beaa90e7b332ee1b99ea6
|
4
|
+
data.tar.gz: 9b58a6e07f42adcae0b86969f01d84df77480e2db7831a6b98dd48ab67159ac1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a46c7e7ca41c516c88ed3f12fee71506a5b24f280dd784d53c787cc910027bebb3b37fda1f364ad6b8dd84e9d5460ee569c4e7ff3a2eb480195b3ce6a6ddb8a4
|
7
|
+
data.tar.gz: 4a1c63c08f0ddf6693e587f2a2bd78c94e1638cd68471f54a50cd1d1df4c67ac8fbb876a402ac9b8d63e018f20ad5c415ce90b0d487bfc26b1950524c3f07054
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
## [1.3.0]
|
2
|
+
|
3
|
+
* Add undocumented methods for using the gem as an API. I leave figuring out its
|
4
|
+
usage as an exercise to the reader
|
5
|
+
|
6
|
+
## [1.2.4]
|
7
|
+
|
8
|
+
* When uploads fail, output a message that includes the error code
|
9
|
+
(and the URL to look them up)
|
10
|
+
|
1
11
|
## [1.2.3]
|
2
12
|
|
3
13
|
* Add a retry option after IG continues to fail to download videos correctly. See `RETRIES_AFTER_UPLOAD_TIMEOUT` (default 5 retries)
|
data/README.md
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
-
# feed2gram
|
1
|
+
# feed2gram - Syndicate your site's photos, videos, and carousels to Instagram
|
2
|
+
|
3
|
+
**feed2gram is for automating Instagram posts, for posting to Threads, see
|
4
|
+
[feed2thread](https://github.com/searls/feed2thread)**
|
2
5
|
|
3
6
|
I've joined the [POSSE](https://indieweb.org/POSSE) and publish as much as I can
|
4
7
|
to [justin.searls.co](https://justin.searls.co) and syndicate it elsewhere. I'm
|
@@ -169,7 +172,7 @@ We publish a Docker image [using GitHub
|
|
169
172
|
actions](https://github.com/searls/feed2gram/blob/main/.github/workflows/main.yml)
|
170
173
|
tagged as `latest` for every new commit to the `main` branch, as well as with a
|
171
174
|
release tag tracking every release of the gem on
|
172
|
-
[rubygems.org](https://rubygems.org). The images are hosted [here on GitHub's
|
175
|
+
[rubygems.org](https://rubygems.org/gems/feed2gram). The images are hosted [here on GitHub's
|
173
176
|
container
|
174
177
|
registry](https://github.com/searls/feed2gram/pkgs/container/feed2gram)
|
175
178
|
|
data/example/Gemfile
CHANGED
@@ -31,6 +31,79 @@ module Feed2Gram
|
|
31
31
|
}
|
32
32
|
end
|
33
33
|
|
34
|
+
# Pseudo-public "API mode"
|
35
|
+
def create_single_media_container(post, config)
|
36
|
+
media = post.medias.first
|
37
|
+
Http.post("/#{config.instagram_id}/media", {
|
38
|
+
:media_type => post.media_type,
|
39
|
+
:caption => post.caption,
|
40
|
+
:access_token => config.access_token,
|
41
|
+
:cover_url => media.cover_url,
|
42
|
+
media.video? ? :video_url : :image_url => media.url
|
43
|
+
}.compact)[:id]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Pseudo-public "API mode"
|
47
|
+
def create_carousel_media_container(media, config)
|
48
|
+
res = Http.post("/#{config.instagram_id}/media", {
|
49
|
+
:media_type => media.media_type,
|
50
|
+
:is_carousel_item => true,
|
51
|
+
:access_token => config.access_token,
|
52
|
+
media.video? ? :video_url : :image_url => media.url
|
53
|
+
}.compact)
|
54
|
+
res[:id]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Pseudo-public "API mode"
|
58
|
+
def create_carousel_container(post, media_containers, config)
|
59
|
+
Http.post("/#{config.instagram_id}/media", {
|
60
|
+
caption: post.caption,
|
61
|
+
media_type: post.media_type,
|
62
|
+
children: media_containers.join(","),
|
63
|
+
access_token: config.access_token
|
64
|
+
})[:id]
|
65
|
+
end
|
66
|
+
|
67
|
+
# Pseudo-public "API mode"
|
68
|
+
def upload_finished?(container_id, config)
|
69
|
+
res = Http.get("/#{container_id}", {
|
70
|
+
fields: "status_code,status",
|
71
|
+
access_token: config.access_token
|
72
|
+
})
|
73
|
+
|
74
|
+
if res[:status_code] == "FINISHED"
|
75
|
+
true
|
76
|
+
elsif res[:status_code] == "IN_PROGRESS"
|
77
|
+
false
|
78
|
+
else
|
79
|
+
raise <<~MSG
|
80
|
+
Unexpected status code (#{res[:status_code]}) uploading container: #{container_id}"
|
81
|
+
|
82
|
+
API sent back this: #{res[:status]}
|
83
|
+
|
84
|
+
Error codes can be looked up here: https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/error-codes/
|
85
|
+
MSG
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Pseudo-public "API mode"
|
90
|
+
def publish_container(container_id, config)
|
91
|
+
res = Http.post("/#{config.instagram_id}/media_publish", {
|
92
|
+
creation_id: container_id,
|
93
|
+
access_token: config.access_token
|
94
|
+
})
|
95
|
+
res[:id]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Pseudo-public "API mode"
|
99
|
+
def get_media_permalink(media_id, config)
|
100
|
+
res = Http.get("/#{media_id}", {
|
101
|
+
fields: "permalink",
|
102
|
+
access_token: config.access_token
|
103
|
+
})
|
104
|
+
res[:permalink]
|
105
|
+
end
|
106
|
+
|
34
107
|
private
|
35
108
|
|
36
109
|
def retry_if_upload_times_out(times_remaining, post, options, &blk)
|
@@ -49,55 +122,35 @@ module Feed2Gram
|
|
49
122
|
media = post.medias.first
|
50
123
|
|
51
124
|
puts "Creating media resource for URL - #{media.url}" if options.verbose
|
52
|
-
container_id =
|
53
|
-
:media_type => post.media_type,
|
54
|
-
:caption => post.caption,
|
55
|
-
:access_token => config.access_token,
|
56
|
-
:cover_url => media.cover_url,
|
57
|
-
media.video? ? :video_url : :image_url => media.url
|
58
|
-
}.compact)[:id]
|
125
|
+
container_id = create_single_media_container(post, config)
|
59
126
|
|
60
127
|
if media.video?
|
61
128
|
wait_for_media_to_upload!(media.url, container_id, config, options)
|
62
129
|
end
|
63
130
|
|
64
131
|
puts "Publishing media for URL - #{media.url}" if options.verbose
|
65
|
-
|
66
|
-
creation_id: container_id,
|
67
|
-
access_token: config.access_token
|
68
|
-
})
|
132
|
+
publish_container(container_id, config)
|
69
133
|
Result.new(post: post, status: :posted)
|
70
134
|
end
|
71
135
|
|
72
136
|
def publish_carousel(post, config, options)
|
73
137
|
media_containers = post.medias.take(10).map { |media|
|
74
138
|
puts "Creating media resource for URL - #{media.url}" if options.verbose
|
75
|
-
|
76
|
-
:media_type => media.media_type,
|
77
|
-
:is_carousel_item => true,
|
78
|
-
:access_token => config.access_token,
|
79
|
-
media.video? ? :video_url : :image_url => media.url
|
80
|
-
}.compact)
|
81
|
-
res[:id]
|
139
|
+
create_carousel_media_container(media, config)
|
82
140
|
}
|
83
|
-
post.medias.
|
84
|
-
|
141
|
+
post.medias.zip(media_containers).each { |media, container_id|
|
142
|
+
if media.video?
|
143
|
+
wait_for_media_to_upload!(media.url, container_id, config, options)
|
144
|
+
end
|
85
145
|
}
|
86
146
|
|
87
147
|
puts "Creating carousel media resource for post - #{post.url}" if options.verbose
|
88
|
-
carousel_id =
|
89
|
-
|
90
|
-
media_type: post.media_type,
|
91
|
-
children: media_containers.join(","),
|
92
|
-
access_token: config.access_token
|
93
|
-
})[:id]
|
148
|
+
carousel_id = create_carousel_container(post, media_containers, config)
|
149
|
+
|
94
150
|
wait_for_media_to_upload!(post.url, carousel_id, config, options)
|
95
151
|
|
96
152
|
puts "Publishing carousel media for post - #{post.url}" if options.verbose
|
97
|
-
|
98
|
-
creation_id: carousel_id,
|
99
|
-
access_token: config.access_token
|
100
|
-
})
|
153
|
+
publish_container(carousel_id, config)
|
101
154
|
Result.new(post: post, status: :posted)
|
102
155
|
end
|
103
156
|
|
@@ -110,18 +163,16 @@ module Feed2Gram
|
|
110
163
|
raise FacebookSucksAtDownloadingFilesError
|
111
164
|
end
|
112
165
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
else
|
124
|
-
warn "Unexpected status code (#{res[:status_code]}) uploading: #{url}"
|
166
|
+
begin
|
167
|
+
puts "Uploading after waiting #{wait_attempts * SECONDS_PER_UPLOAD_CHECK} seconds for IG to download #{url}" if options.verbose
|
168
|
+
if upload_finished?(container_id, config)
|
169
|
+
break
|
170
|
+
else
|
171
|
+
wait_attempts += 1
|
172
|
+
sleep SECONDS_PER_UPLOAD_CHECK
|
173
|
+
end
|
174
|
+
rescue => e
|
175
|
+
warn e.message
|
125
176
|
break
|
126
177
|
end
|
127
178
|
end
|
@@ -1,10 +1,22 @@
|
|
1
1
|
module Feed2Gram
|
2
2
|
class RefreshesToken
|
3
3
|
def refresh!(config, options)
|
4
|
+
puts "Refreshing Facebook OAuth token" if options.verbose
|
5
|
+
new_access_token = call(config)
|
6
|
+
return unless new_access_token
|
7
|
+
|
8
|
+
config.access_token = new_access_token
|
9
|
+
config.access_token_refreshed_at = Time.now.utc
|
10
|
+
|
11
|
+
puts "Updating Facebook OAuth token in: #{options.config_path}" if options.verbose
|
12
|
+
File.write(options.config_path, config.as_yaml)
|
13
|
+
end
|
14
|
+
|
15
|
+
# "API mode"
|
16
|
+
def call(config)
|
4
17
|
return unless config.access_token_refreshed_at.nil? ||
|
5
18
|
config.access_token_refreshed_at < Time.now - (60 * 60)
|
6
19
|
|
7
|
-
puts "Refreshing Facebook OAuth token" if options.verbose
|
8
20
|
data = Http.get("/oauth/access_token", {
|
9
21
|
grant_type: "fb_exchange_token",
|
10
22
|
client_id: config.facebook_app_id,
|
@@ -12,11 +24,7 @@ module Feed2Gram
|
|
12
24
|
fb_exchange_token: config.access_token
|
13
25
|
})
|
14
26
|
|
15
|
-
|
16
|
-
config.access_token_refreshed_at = Time.now.utc
|
17
|
-
|
18
|
-
puts "Updating Facebook OAuth token in: #{options.config_path}" if options.verbose
|
19
|
-
File.write(options.config_path, config.as_yaml)
|
27
|
+
data[:access_token]
|
20
28
|
end
|
21
29
|
end
|
22
30
|
end
|
data/lib/feed2gram/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: feed2gram
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Searls
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-10-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -77,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
77
|
- !ruby/object:Gem::Version
|
78
78
|
version: '0'
|
79
79
|
requirements: []
|
80
|
-
rubygems_version: 3.5.
|
80
|
+
rubygems_version: 3.5.11
|
81
81
|
signing_key:
|
82
82
|
specification_version: 4
|
83
83
|
summary: Reads an Atom feed and posts its entries to Instagram
|