Toot toooooooot (Bluesky-Mastodon cross-poster)

added support for cross-posting threads/replies

(somewhat experimental and I'll probably need to heavily refactor this)

+1
.gitignore
··· 1 1 .DS_Store 2 + config/*.csv 2 3 config/*.yml
+2 -2
app/mastodon_account.rb
··· 46 46 client.create_app(APP_NAME, 'urn:ietf:wg:oauth:2.0:oob', scopes) 47 47 end 48 48 49 - def post_status(text, media_ids = nil) 49 + def post_status(text, media_ids = nil, parent_id = nil) 50 50 instance = @config['handle'].split('@').last 51 51 api = MastodonAPI.new(instance, @config['access_token']) 52 - api.post_status(text, media_ids) 52 + api.post_status(text, media_ids, parent_id) 53 53 end 54 54 55 55 def upload_media(data, filename, content_type, alt = nil)
+2 -1
app/mastodon_api.rb
··· 58 58 get_json("/accounts/#{user_id}/statuses", params) 59 59 end 60 60 61 - def post_status(text, media_ids = nil) 61 + def post_status(text, media_ids = nil, parent_id = nil) 62 62 params = { status: text } 63 63 params['media_ids[]'] = media_ids if media_ids 64 + params['in_reply_to_id'] = parent_id if parent_id 64 65 65 66 post_json("/statuses", params) 66 67 end
+23
app/post_history.rb
··· 1 + class PostHistory 2 + HISTORY_FILE = File.expand_path(File.join(__dir__, '..', 'config', 'history.csv')) 3 + 4 + def initialize 5 + if File.exist?(HISTORY_FILE) 6 + @id_map = File.read(HISTORY_FILE).lines.map { |l| l.strip.split(',') }.then { |pairs| Hash[pairs] } 7 + else 8 + @id_map = {} 9 + end 10 + end 11 + 12 + def [](bluesky_rkey) 13 + @id_map[bluesky_rkey] 14 + end 15 + 16 + def add(bluesky_rkey, mastodon_id) 17 + @id_map[bluesky_rkey] = mastodon_id 18 + 19 + File.open(HISTORY_FILE, 'a') do |f| 20 + f.puts("#{bluesky_rkey},#{mastodon_id}") 21 + end 22 + end 23 + end
+36 -9
app/tootify.rb
··· 2 2 3 3 require_relative 'bluesky_account' 4 4 require_relative 'mastodon_account' 5 + require_relative 'post_history' 5 6 6 7 class Tootify 7 8 attr_accessor :check_interval ··· 9 10 def initialize 10 11 @bluesky = BlueskyAccount.new 11 12 @mastodon = MastodonAccount.new 13 + @history = PostHistory.new 12 14 @check_interval = 60 13 15 end 14 16 ··· 58 60 next 59 61 end 60 62 61 - if record['value']['reply'] 62 - puts "Skipping reply" 63 - @bluesky.delete_record_at(like_uri) 64 - next 63 + if reply = record['value']['reply'] 64 + parent_uri = reply['parent']['uri'] 65 + prepo = parent_uri.split('/')[2] 66 + 67 + if prepo != @bluesky.did 68 + puts "Skipping reply to someone else" 69 + @bluesky.delete_record_at(like_uri) 70 + next 71 + else 72 + # self-reply, we'll try to cross-post it 73 + end 65 74 end 66 75 67 - records << [record['value'], like_uri] 76 + records << [record['value'], rkey, like_uri] 68 77 end 69 78 70 - records.sort_by { |x| x[0]['createdAt'] }.each do |record, like_uri| 71 - post_to_mastodon(record) 79 + records.sort_by { |x| x[0]['createdAt'] }.each do |record, rkey, like_uri| 80 + mastodon_parent_id = nil 81 + 82 + if reply = record['reply'] 83 + parent_uri = reply['parent']['uri'] 84 + prkey = parent_uri.split('/')[4] 85 + 86 + if parent_id = @history[prkey] 87 + mastodon_parent_id = parent_id 88 + else 89 + puts "Skipping reply to a post that wasn't cross-posted" 90 + @bluesky.delete_record_at(like_uri) 91 + next 92 + end 93 + end 94 + 95 + response = post_to_mastodon(record, mastodon_parent_id) 96 + p response 97 + 98 + @history.add(rkey, response['id']) 72 99 @bluesky.delete_record_at(like_uri) 73 100 end 74 101 end ··· 80 107 end 81 108 end 82 109 83 - def post_to_mastodon(record) 110 + def post_to_mastodon(record, mastodon_parent_id = nil) 84 111 p record 85 112 86 113 text = expand_facets(record) ··· 121 148 text += "\n\n" + tags.map { |t| '#' + t.gsub(' ', '') }.join(' ') 122 149 end 123 150 124 - p @mastodon.post_status(text, media_ids) 151 + @mastodon.post_status(text, media_ids, mastodon_parent_id) 125 152 end 126 153 127 154 def expand_facets(record)