+74
app/importers/posts_importer.rb
+74
app/importers/posts_importer.rb
···
···
1
+
require 'time'
2
+
3
+
require_relative '../at_uri'
4
+
require_relative 'base_importer'
5
+
6
+
class PostsImporter < BaseImporter
7
+
def import_items
8
+
params = { repo: @did, collection: 'app.bsky.feed.post', limit: 100 }
9
+
params[:cursor] = @import.cursor if @import.cursor
10
+
11
+
loop do
12
+
response = @minisky.get_request('com.atproto.repo.listRecords', params)
13
+
14
+
records = response['records']
15
+
cursor = response['cursor']
16
+
17
+
@imported_count += records.length
18
+
@report&.update(importers: { importer_name => { :imported_items => @imported_count }})
19
+
@report&.update(importers: { importer_name => { :oldest_date => Time.parse(records.last['value']['createdAt']) }}) unless records.empty?
20
+
21
+
records.each do |record|
22
+
begin
23
+
if record['value']['embed'] && record['value']['embed']['record']
24
+
save_quote(record)
25
+
end
26
+
27
+
if record['value']['reply'] && record['value']['text'].include?('📌')
28
+
save_pin(record)
29
+
end
30
+
rescue StandardError => e
31
+
puts "Error in LikesImporter: #{record['uri']}: #{e}"
32
+
end
33
+
end
34
+
35
+
params[:cursor] = cursor
36
+
@import.update!(cursor: cursor)
37
+
38
+
break if !cursor
39
+
break if @time_limit && records.any? { |x| Time.parse(x['value']['createdAt']) < @time_limit }
40
+
end
41
+
end
42
+
43
+
def save_quote(record)
44
+
post_rkey = AT_URI(record['uri']).rkey
45
+
return if @user.quotes.where(rkey: post_rkey).exists?
46
+
47
+
post_time = Time.parse(record['value']['createdAt'])
48
+
49
+
quoted_post_uri = case record['value']['embed']['$type']
50
+
when 'app.bsky.embed.record'
51
+
record['value']['embed']['record']['uri']
52
+
when 'app.bsky.embed.recordWithMedia'
53
+
record['value']['embed']['record']['record']['uri']
54
+
else
55
+
return
56
+
end
57
+
58
+
create_item_for_post(quoted_post_uri) do |args|
59
+
@user.quotes.create!(args.merge(rkey: post_rkey, time: post_time, quote_text: record['value']['text']))
60
+
end
61
+
end
62
+
63
+
def save_pin(record)
64
+
post_rkey = AT_URI(record['uri']).rkey
65
+
return if @user.pins.where(rkey: post_rkey).exists?
66
+
67
+
post_time = Time.parse(record['value']['createdAt'])
68
+
parent_post_uri = record['value']['reply']['parent']['uri']
69
+
70
+
create_item_for_post(parent_post_uri) do |args|
71
+
@user.pins.create!(args.merge(rkey: post_rkey, time: post_time, pin_text: record['value']['text']))
72
+
end
73
+
end
74
+
end
+1
-1
app/models/import.rb
+1
-1
app/models/import.rb
+18
app/models/pin.rb
+18
app/models/pin.rb
···
···
1
+
require 'active_record'
2
+
3
+
require_relative 'post'
4
+
require_relative 'searchable'
5
+
require_relative 'user'
6
+
7
+
class Pin < ActiveRecord::Base
8
+
include Searchable
9
+
10
+
validates_presence_of :time, :rkey
11
+
validates_length_of :rkey, maximum: 13
12
+
validates :pin_text, length: { minimum: 0, maximum: 1000, allow_nil: false }
13
+
14
+
validates_presence_of :post_uri, if: -> { post_id.nil? }
15
+
16
+
belongs_to :user, foreign_key: 'actor_id'
17
+
belongs_to :post, optional: true
18
+
end
+18
app/models/quote.rb
+18
app/models/quote.rb
···
···
1
+
require 'active_record'
2
+
3
+
require_relative 'post'
4
+
require_relative 'searchable'
5
+
require_relative 'user'
6
+
7
+
class Quote < ActiveRecord::Base
8
+
include Searchable
9
+
10
+
validates_presence_of :time, :rkey
11
+
validates_length_of :rkey, maximum: 13
12
+
validates :quote_text, length: { minimum: 0, maximum: 1000, allow_nil: false }
13
+
14
+
validates_presence_of :post_uri, if: -> { post_id.nil? }
15
+
16
+
belongs_to :user, foreign_key: 'actor_id'
17
+
belongs_to :post, optional: true
18
+
end
+4
app/models/user.rb
+4
app/models/user.rb
···
2
3
require_relative 'import'
4
require_relative 'like'
5
+
require_relative 'quote'
6
+
require_relative 'pin'
7
require_relative 'post'
8
require_relative 'repost'
9
···
14
has_many :posts
15
has_many :likes, foreign_key: 'actor_id'
16
has_many :reposts, foreign_key: 'actor_id'
17
+
has_many :quotes, foreign_key: 'actor_id'
18
+
has_many :pins, foreign_key: 'actor_id'
19
has_many :imports
20
end
+8
-1
app/server.rb
+8
-1
app/server.rb
···
113
114
collection = case params[:collection]
115
when 'likes' then user.likes
116
when 'reposts' then user.reposts
117
else return json_error('InvalidParameter', 'Invalid search collection')
118
end
···
125
.after_cursor(params[:cursor])
126
.limit(PAGE_LIMIT)
127
128
-
post_uris = items.map(&:post).map(&:at_uri)
129
130
json_response(posts: post_uris, cursor: items.last&.cursor)
131
end
···
113
114
collection = case params[:collection]
115
when 'likes' then user.likes
116
+
when 'pins' then user.pins
117
+
when 'quotes' then user.quotes
118
when 'reposts' then user.reposts
119
else return json_error('InvalidParameter', 'Invalid search collection')
120
end
···
127
.after_cursor(params[:cursor])
128
.limit(PAGE_LIMIT)
129
130
+
post_uris = case params[:collection]
131
+
when 'quotes'
132
+
items.map { |x| "at://#{user.did}/app.bsky.feed.post/#{x.rkey}" }
133
+
else
134
+
items.map(&:post).map(&:at_uri)
135
+
end
136
137
json_response(posts: post_uris, cursor: items.last&.cursor)
138
end
+27
db/migrate/20250906233017_add_quotes_and_pins.rb
+27
db/migrate/20250906233017_add_quotes_and_pins.rb
···
···
1
+
class AddQuotesAndPins < ActiveRecord::Migration[7.2]
2
+
def change
3
+
create_table :quotes do |t|
4
+
t.integer "actor_id", null: false
5
+
t.string "rkey", limit: 13, null: false
6
+
t.datetime "time", null: false
7
+
t.text "quote_text", null: false
8
+
t.bigint "post_id"
9
+
t.string "post_uri"
10
+
end
11
+
12
+
add_index :quotes, [:actor_id, :time, :id], order: { time: :desc, id: :desc }
13
+
add_index :quotes, [:actor_id, :rkey], unique: true
14
+
15
+
create_table :pins do |t|
16
+
t.integer "actor_id", null: false
17
+
t.string "rkey", limit: 13, null: false
18
+
t.datetime "time", null: false
19
+
t.text "pin_text", null: false
20
+
t.bigint "post_id"
21
+
t.string "post_uri"
22
+
end
23
+
24
+
add_index :pins, [:actor_id, :time, :id], order: { time: :desc, id: :desc }
25
+
add_index :pins, [:actor_id, :rkey], unique: true
26
+
end
27
+
end
+23
-1
db/schema.rb
+23
-1
db/schema.rb
···
10
#
11
# It's strongly recommended that you check this file into your version control system.
12
13
-
ActiveRecord::Schema[7.2].define(version: 2025_09_06_005748) do
14
# These are extensions that must be enabled in order to support this database
15
enable_extension "plpgsql"
16
···
33
t.index ["actor_id", "time", "id"], name: "index_likes_on_actor_id_and_time_and_id", order: { time: :desc, id: :desc }
34
end
35
36
create_table "posts", force: :cascade do |t|
37
t.integer "user_id", null: false
38
t.string "rkey", limit: 13, null: false
···
41
t.text "data", null: false
42
t.index ["user_id", "rkey"], name: "index_posts_on_user_id_and_rkey", unique: true
43
t.index ["user_id", "time", "id"], name: "index_posts_on_user_id_and_time_and_id", order: { time: :desc, id: :desc }
44
end
45
46
create_table "reposts", force: :cascade do |t|
···
10
#
11
# It's strongly recommended that you check this file into your version control system.
12
13
+
ActiveRecord::Schema[7.2].define(version: 2025_09_06_233017) do
14
# These are extensions that must be enabled in order to support this database
15
enable_extension "plpgsql"
16
···
33
t.index ["actor_id", "time", "id"], name: "index_likes_on_actor_id_and_time_and_id", order: { time: :desc, id: :desc }
34
end
35
36
+
create_table "pins", force: :cascade do |t|
37
+
t.integer "actor_id", null: false
38
+
t.string "rkey", limit: 13, null: false
39
+
t.datetime "time", null: false
40
+
t.text "pin_text", null: false
41
+
t.bigint "post_id"
42
+
t.string "post_uri"
43
+
t.index ["actor_id", "rkey"], name: "index_pins_on_actor_id_and_rkey", unique: true
44
+
t.index ["actor_id", "time", "id"], name: "index_pins_on_actor_id_and_time_and_id", order: { time: :desc, id: :desc }
45
+
end
46
+
47
create_table "posts", force: :cascade do |t|
48
t.integer "user_id", null: false
49
t.string "rkey", limit: 13, null: false
···
52
t.text "data", null: false
53
t.index ["user_id", "rkey"], name: "index_posts_on_user_id_and_rkey", unique: true
54
t.index ["user_id", "time", "id"], name: "index_posts_on_user_id_and_time_and_id", order: { time: :desc, id: :desc }
55
+
end
56
+
57
+
create_table "quotes", force: :cascade do |t|
58
+
t.integer "actor_id", null: false
59
+
t.string "rkey", limit: 13, null: false
60
+
t.datetime "time", null: false
61
+
t.text "quote_text", null: false
62
+
t.bigint "post_id"
63
+
t.string "post_uri"
64
+
t.index ["actor_id", "rkey"], name: "index_quotes_on_actor_id_and_rkey", unique: true
65
+
t.index ["actor_id", "time", "id"], name: "index_quotes_on_actor_id_and_time_and_id", order: { time: :desc, id: :desc }
66
end
67
68
create_table "reposts", force: :cascade do |t|
+5
lib/tasks/import.rake
+5
lib/tasks/import.rake
···
1
require_relative '../../app/importers/likes_importer'
2
require_relative '../../app/importers/reposts_importer'
3
require_relative '../../app/import_report'
4
require_relative '../../app/item_queue'
5
require_relative '../../app/post_downloader'
···
18
when 'reposts'
19
queue = ItemQueue.new(user.reposts.where(post: nil).to_a)
20
importer = RepostsImporter.new(ENV['USER'])
21
when nil
22
raise "Required COLLECTION parameter missing"
23
else
···
1
require_relative '../../app/importers/likes_importer'
2
+
require_relative '../../app/importers/posts_importer'
3
require_relative '../../app/importers/reposts_importer'
4
+
5
require_relative '../../app/import_report'
6
require_relative '../../app/item_queue'
7
require_relative '../../app/post_downloader'
···
20
when 'reposts'
21
queue = ItemQueue.new(user.reposts.where(post: nil).to_a)
22
importer = RepostsImporter.new(ENV['USER'])
23
+
when 'posts'
24
+
queue = ItemQueue.new(user.quotes.where(post: nil).to_a + user.pins.where(post: nil).to_a)
25
+
importer = PostsImporter.new(ENV['USER'])
26
when nil
27
raise "Required COLLECTION parameter missing"
28
else