Toot toooooooot (Bluesky-Mastodon cross-poster)
1require 'io/console'
2
3require_relative 'bluesky_account'
4require_relative 'mastodon_account'
5
6class Tootify
7 def initialize
8 @bluesky = BlueskyAccount.new
9 @mastodon = MastodonAccount.new
10 end
11
12 def login_bluesky(handle)
13 handle = handle.gsub(/^@/, '')
14
15 print "App password: "
16 password = STDIN.noecho(&:gets).chomp
17 puts
18
19 @bluesky.login_with_password(handle, password)
20 end
21
22 def login_mastodon(handle)
23 print "Email: "
24 email = STDIN.gets.chomp
25
26 print "Password: "
27 password = STDIN.noecho(&:gets).chomp
28 puts
29
30 @mastodon.oauth_login(handle, email, password)
31 end
32
33 def sync
34 likes = @bluesky.fetch_likes
35
36 likes.each do |r|
37 like_uri = r['uri']
38 post_uri = r['value']['subject']['uri']
39 repo, collection, rkey = post_uri.split('/')[2..4]
40
41 next unless repo == @bluesky.did && collection == 'app.bsky.feed.post'
42
43 begin
44 record = @bluesky.fetch_record(repo, collection, rkey)
45 rescue Minisky::ClientErrorResponse => e
46 puts "Record not found: #{post_uri}"
47 @bluesky.delete_record_at(like_uri)
48 next
49 end
50
51 post_to_mastodon(record['value'])
52
53 @bluesky.delete_record_at(like_uri)
54 end
55 end
56
57 def post_to_mastodon(record)
58 p record
59
60 text = expand_facets(record)
61
62 if link = link_embed(record)
63 if !text.include?(link)
64 text += ' ' unless text.end_with?(' ')
65 text += link
66 end
67 end
68
69 p @mastodon.post_status(text)
70 end
71
72 def expand_facets(record)
73 bytes = record['text'].bytes
74 offset = 0
75
76 if facets = record['facets']
77 facets.sort_by { |f| f['index']['byteStart'] }.each do |f|
78 if link = f['features'].detect { |ft| ft['$type'] == 'app.bsky.richtext.facet#link' }
79 left = f['index']['byteStart']
80 right = f['index']['byteEnd']
81 content = link['uri'].bytes
82
83 bytes[(left + offset) ... (right + offset)] = content
84 offset += content.length - (right - left)
85 end
86 end
87 end
88
89 bytes.pack('C*').force_encoding('UTF-8')
90 end
91
92 def link_embed(record)
93 record['embed'] && record['embed']['external'] && record['embed']['external']['uri']
94 end
95end