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