Toot toooooooot (Bluesky-Mastodon cross-poster)

added partial support for Mastodon quotes

At the moment works for quotes of posts bridged from Mastodon into Bluesky, doesn't yet work for quotes of Bluesky-native posts that are bridged to Mastodon

Note: requires a re-login to Mastodon because of expanded OAuth scopes. Also works only on instances with the new quote API (which wasn't officially released yet) already deployed.

+22 -3
app/mastodon_account.rb
··· 4 4 class MastodonAccount 5 5 APP_NAME = "Tootify" 6 6 CONFIG_FILE = File.expand_path(File.join(__dir__, '..', 'config', 'mastodon.yml')) 7 - OAUTH_SCOPES = 'read:accounts read:statuses write:media write:statuses' 7 + 8 + OAUTH_SCOPES = [ 9 + 'read:accounts', 10 + 'read:statuses', 11 + 'read:search', 12 + 'write:media', 13 + 'write:statuses' 14 + ].join(' ') 8 15 9 16 def initialize 10 17 @config = File.exist?(CONFIG_FILE) ? YAML.load(File.read(CONFIG_FILE)) : {} ··· 52 59 api.register_oauth_app(APP_NAME, OAUTH_SCOPES) 53 60 end 54 61 55 - def post_status(text, media_ids = nil, parent_id = nil) 62 + def instance_info 56 63 instance = @config['handle'].split('@').last 57 64 api = MastodonAPI.new(instance, @config['access_token']) 58 - api.post_status(text, media_ids, parent_id) 65 + api.instance_info 66 + end 67 + 68 + def post_status(text, media_ids: nil, parent_id: nil, quoted_status_id: nil) 69 + instance = @config['handle'].split('@').last 70 + api = MastodonAPI.new(instance, @config['access_token']) 71 + api.post_status(text, media_ids: media_ids, parent_id: parent_id, quoted_status_id: quoted_status_id) 59 72 end 60 73 61 74 def upload_media(data, filename, content_type, alt = nil) ··· 63 76 api = MastodonAPI.new(instance, @config['access_token']) 64 77 api.upload_media(data, filename, content_type, alt) 65 78 end 79 + 80 + def search_post_by_url(url) 81 + instance = @config['handle'].split('@').last 82 + api = MastodonAPI.new(instance, @config['access_token']) 83 + api.search_post_by_url(url) 84 + end 66 85 end
+16 -1
app/mastodon_api.rb
··· 74 74 get_json("/accounts/verify_credentials") 75 75 end 76 76 77 + def instance_info 78 + get_json("https://#{@host}/api/v2/instance") 79 + end 80 + 77 81 def lookup_account(username) 78 82 json = get_json("/accounts/lookup", { acct: username }) 79 83 raise UnexpectedResponseError.new unless json.is_a?(Hash) && json['id'].is_a?(String) ··· 84 88 get_json("/accounts/#{user_id}/statuses", params) 85 89 end 86 90 87 - def post_status(text, media_ids = nil, parent_id = nil) 91 + def post_status(text, media_ids: nil, parent_id: nil, quoted_status_id: nil) 88 92 params = { status: text } 89 93 params['media_ids[]'] = media_ids if media_ids 90 94 params['in_reply_to_id'] = parent_id if parent_id 95 + params['quoted_status_id'] = quoted_status_id if quoted_status_id 91 96 92 97 post_json("/statuses", params) 93 98 end ··· 127 132 128 133 def get_media(media_id) 129 134 get_json("/media/#{media_id}") 135 + end 136 + 137 + def search_post_by_url(url) 138 + json = get_json("https://#{@host}/api/v2/search", { 139 + q: url, 140 + type: 'statuses', 141 + resolve: true 142 + }) 143 + 144 + json['statuses'] && json['statuses'][0] 130 145 end 131 146 132 147 def get_json(path, params = {})
+18 -3
app/tootify.rb
··· 135 135 136 136 if collection == 'app.bsky.feed.post' 137 137 link_to_append = bsky_post_link(repo, rkey) 138 + instance_info = @mastodon.instance_info 138 139 139 - if @config['extract_link_from_quotes'] 140 + if instance_info.dig('api_versions', 'mastodon').to_i >= 7 140 141 quoted_record = fetch_record_by_at_uri(quote_uri) 141 142 143 + # TODO: we need to wait for Bridgy to add support for quote_authorizations 144 + quoted_post_url = quoted_record['bridgyOriginalUrl'] #|| "https://bsky.brid.gy/convert/ap/#{quote_uri}" 145 + 146 + if quoted_post_url && (local_post = @mastodon.search_post_by_url(quoted_post_url)) 147 + quote_policy = local_post.dig('quote_approval', 'current_user') 148 + 149 + if quote_policy == 'automatic' || quote_policy == 'manual' 150 + quote_id = local_post['id'] 151 + end 152 + end 153 + end 154 + 155 + if !quote_id && @config['extract_link_from_quotes'] 156 + quoted_record ||= fetch_record_by_at_uri(quote_uri) 142 157 quote_link = link_embed(quoted_record) 143 158 144 159 if quote_link.nil? ··· 151 166 end 152 167 end 153 168 154 - append_link(text, link_to_append) unless text.include?(link_to_append) 169 + append_link(text, link_to_append) unless quote_id || text.include?(link_to_append) 155 170 end 156 171 end 157 172 ··· 191 206 text += "\n\n" + tags.map { |t| '#' + t.gsub(' ', '') }.join(' ') 192 207 end 193 208 194 - @mastodon.post_status(text, media_ids, mastodon_parent_id) 209 + @mastodon.post_status(text, media_ids: media_ids, parent_id: mastodon_parent_id, quoted_status_id: quote_id) 195 210 end 196 211 197 212 def fetch_record_by_at_uri(quote_uri)