+3
Gemfile
+3
Gemfile
+39
Gemfile.lock
+39
Gemfile.lock
···
1
1
GEM
2
2
remote: https://rubygems.org/
3
3
specs:
4
+
activemodel (7.2.2.1)
5
+
activesupport (= 7.2.2.1)
6
+
activerecord (7.2.2.1)
7
+
activemodel (= 7.2.2.1)
8
+
activesupport (= 7.2.2.1)
9
+
timeout (>= 0.4.0)
10
+
activesupport (7.2.2.1)
11
+
base64
12
+
benchmark (>= 0.3)
13
+
bigdecimal
14
+
concurrent-ruby (~> 1.0, >= 1.3.1)
15
+
connection_pool (>= 2.2.5)
16
+
drb
17
+
i18n (>= 1.6, < 2)
18
+
logger (>= 1.4.2)
19
+
minitest (>= 5.1)
20
+
securerandom (>= 0.3)
21
+
tzinfo (~> 2.0, >= 2.0.5)
4
22
base64 (0.2.0)
23
+
benchmark (0.4.1)
24
+
bigdecimal (3.2.2)
25
+
concurrent-ruby (1.3.5)
26
+
connection_pool (2.5.3)
5
27
didkit (0.2.3)
28
+
drb (2.2.3)
29
+
i18n (1.14.7)
30
+
concurrent-ruby (~> 1.0)
6
31
io-console (0.7.2)
7
32
json (2.10.2)
33
+
logger (1.7.0)
34
+
mini_portile2 (2.8.9)
8
35
minisky (0.5.0)
9
36
base64 (~> 0.1)
37
+
minitest (5.25.5)
10
38
net-http (0.4.1)
11
39
uri
40
+
securerandom (0.4.1)
41
+
sqlite3 (2.7.1)
42
+
mini_portile2 (~> 2.8.0)
43
+
sqlite3 (2.7.1-aarch64-linux-gnu)
44
+
sqlite3 (2.7.1-arm64-darwin)
45
+
sqlite3 (2.7.1-x86_64-linux-gnu)
46
+
timeout (0.4.3)
47
+
tzinfo (2.0.6)
48
+
concurrent-ruby (~> 1.0)
12
49
uri (0.13.2)
13
50
yaml (0.3.0)
14
51
···
19
56
x86_64-linux
20
57
21
58
DEPENDENCIES
59
+
activerecord (~> 7.2)
22
60
didkit (~> 0.2)
23
61
io-console (~> 0.5)
24
62
json (~> 2.5)
25
63
minisky (~> 0.5)
26
64
net-http (~> 0.2)
65
+
sqlite3 (~> 2.7)
27
66
uri (~> 0.13)
28
67
yaml (~> 0.1)
29
68
+2
-1
README.md
+2
-1
README.md
···
55
55
56
56
* `bluesky.yml` – created when you log in, stores Bluesky user ID/password and access tokens
57
57
* `mastodon.yml` – created when you log in, stores Mastodon user ID/password and access tokens
58
-
* `history.csv` – stores a mapping between Bluesky and Mastodon post IDs; used for reply references in threads
59
58
* `tootify.yml` - optional additional configuration
60
59
61
60
The config in `tootify.yml` currently supports one option:
62
61
63
62
- `extract_link_from_quotes: true` – if enabled, posts which are quotes of someone else's post which includes a link will be "collapsed" into a normal post that just includes that link directly without the quote (so the link card on Mastodon will show info about the link and not the quoted bsky.app post)
63
+
64
+
There is also an SQLite database file that's automatically created in `db/history.sqlite3`. It stores a mapping between Bluesky and Mastodon post IDs, and is used to maintain reply references in threads.
64
65
65
66
66
67
## Credits
+16
app/database.rb
+16
app/database.rb
···
1
+
require 'active_record'
2
+
3
+
module Database
4
+
DB_FILE = File.expand_path(File.join(__dir__, '..', 'db', 'history.sqlite3'))
5
+
MIGRATIONS_PATH = File.expand_path(File.join(__dir__, '..', 'db', 'migrate'))
6
+
7
+
def self.init
8
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: DB_FILE)
9
+
run_migrations
10
+
end
11
+
12
+
def self.run_migrations
13
+
migration_context = ActiveRecord::MigrationContext.new(MIGRATIONS_PATH)
14
+
migration_context.migrate
15
+
end
16
+
end
+10
app/post.rb
+10
app/post.rb
-23
app/post_history.rb
-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
+9
-6
app/tootify.rb
+9
-6
app/tootify.rb
···
2
2
require 'yaml'
3
3
4
4
require_relative 'bluesky_account'
5
+
require_relative 'database'
5
6
require_relative 'mastodon_account'
6
-
require_relative 'post_history'
7
+
require_relative 'post'
7
8
8
9
class Tootify
9
10
CONFIG_FILE = File.expand_path(File.join(__dir__, '..', 'config', 'tootify.yml'))
···
13
14
def initialize
14
15
@bluesky = BlueskyAccount.new
15
16
@mastodon = MastodonAccount.new
16
-
@history = PostHistory.new
17
17
@config = load_config
18
18
@check_interval = 60
19
+
20
+
Database.init
19
21
end
20
22
21
23
def load_config
···
86
88
87
89
if reply = record['reply']
88
90
parent_uri = reply['parent']['uri']
89
-
prkey = parent_uri.split('/')[4]
91
+
parent_rkey = parent_uri.split('/')[4]
90
92
91
-
if parent_id = @history[prkey]
92
-
mastodon_parent_id = parent_id
93
+
if parent_post = Post.find_by(bluesky_rkey: parent_rkey)
94
+
mastodon_parent_id = parent_post.mastodon_id
93
95
else
94
96
puts "Skipping reply to a post that wasn't cross-posted"
95
97
@bluesky.delete_record_at(like_uri)
···
100
102
response = post_to_mastodon(record, mastodon_parent_id)
101
103
p response
102
104
103
-
@history.add(rkey, response['id'])
105
+
Post.create!(bluesky_rkey: rkey, mastodon_id: response['id'])
106
+
104
107
@bluesky.delete_record_at(like_uri)
105
108
end
106
109
end