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 p @mastodon.post_status(text)
62 end
63
64 def expand_facets(record)
65 bytes = record['text'].bytes
66 offset = 0
67
68 if facets = record['facets']
69 facets.sort_by { |f| f['index']['byteStart'] }.each do |f|
70 if link = f['features'].detect { |ft| ft['$type'] == 'app.bsky.richtext.facet#link' }
71 left = f['index']['byteStart']
72 right = f['index']['byteEnd']
73 content = link['uri'].bytes
74
75 bytes[(left + offset) ... (right + offset)] = content
76 offset += content.length - (right - left)
77 end
78 end
79 end
80
81 bytes.pack('C*').force_encoding('UTF-8')
82 end
83end