birdclaw 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +37 -9
- package/package.json +4 -6
- package/src/cli.ts +176 -20
- package/src/lib/archive-import.ts +25 -0
- package/src/lib/backup.ts +269 -33
- package/src/lib/bird-actions.ts +17 -0
- package/src/lib/bird.ts +189 -0
- package/src/lib/blocks.ts +3 -3
- package/src/lib/bookmark-sync-job.ts +3 -3
- package/src/lib/db.ts +313 -14
- package/src/lib/dms-live.ts +3 -3
- package/src/lib/identity-search-index.ts +369 -0
- package/src/lib/mention-threads-live.ts +5 -9
- package/src/lib/mentions-live.ts +18 -7
- package/src/lib/moderation-target.ts +4 -4
- package/src/lib/moderation-write.ts +3 -3
- package/src/lib/profile-affiliation-hydration.ts +189 -0
- package/src/lib/profile-affiliations.ts +262 -0
- package/src/lib/profile-bio-entities.ts +318 -0
- package/src/lib/profile-history.ts +164 -0
- package/src/lib/profile-hydration.ts +4 -28
- package/src/lib/profile-resolver.ts +428 -0
- package/src/lib/queries.ts +391 -57
- package/src/lib/research.ts +11 -0
- package/src/lib/seed.ts +2 -2
- package/src/lib/sqlite.ts +143 -0
- package/src/lib/timeline-collections-live.ts +7 -7
- package/src/lib/timeline-live.ts +16 -9
- package/src/lib/tweet-account-edges.ts +39 -0
- package/src/lib/types.ts +74 -0
- package/src/lib/url-expansion.ts +147 -0
- package/src/lib/whois.ts +1060 -0
- package/src/lib/x-profile.ts +140 -12
- package/src/lib/xurl.ts +7 -6
package/src/lib/dms-live.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type Database from "
|
|
1
|
+
import type { Database } from "./sqlite";
|
|
2
2
|
import {
|
|
3
3
|
type BirdDmConversation,
|
|
4
4
|
type BirdDmEvent,
|
|
@@ -25,7 +25,7 @@ function assertBirdLimit(limit: number) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function resolveAccount(db: Database
|
|
28
|
+
function resolveAccount(db: Database, accountId?: string) {
|
|
29
29
|
const row = accountId
|
|
30
30
|
? (db
|
|
31
31
|
.prepare("select id, handle from accounts where id = ?")
|
|
@@ -112,7 +112,7 @@ function getLatestEvent(events: BirdDmEvent[]) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
function mergeDirectMessagesIntoLocalStore(
|
|
115
|
-
db: Database
|
|
115
|
+
db: Database,
|
|
116
116
|
accountId: string,
|
|
117
117
|
accountUsername: string,
|
|
118
118
|
payload: {
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import type { Database } from "./sqlite";
|
|
2
|
+
import { fetchProfileAffiliations } from "./profile-affiliations";
|
|
3
|
+
import { fetchProfileBioEntities } from "./profile-bio-entities";
|
|
4
|
+
import { fetchProfileSnapshots } from "./profile-history";
|
|
5
|
+
import type { ProfileRecord } from "./types";
|
|
6
|
+
|
|
7
|
+
interface IdentityIndexEntry {
|
|
8
|
+
profileId: string;
|
|
9
|
+
kind: string;
|
|
10
|
+
value: string;
|
|
11
|
+
source: string;
|
|
12
|
+
weight: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const KIND_WEIGHTS: Record<string, number> = {
|
|
16
|
+
affiliation: 90,
|
|
17
|
+
bio_handle: 75,
|
|
18
|
+
bio_company: 65,
|
|
19
|
+
profile_history: 55,
|
|
20
|
+
profile_handle: 45,
|
|
21
|
+
profile_name: 35,
|
|
22
|
+
profile_bio: 35,
|
|
23
|
+
profile_location: 20,
|
|
24
|
+
profile_verified_type: 15,
|
|
25
|
+
profile_bio_url: 12,
|
|
26
|
+
profile_url: 10,
|
|
27
|
+
bio_domain: 8,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function normalizeIndexValue(value: string) {
|
|
31
|
+
return value.trim().toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function addEntry(
|
|
35
|
+
entries: Map<string, IdentityIndexEntry>,
|
|
36
|
+
entry: Omit<IdentityIndexEntry, "weight"> & { weight?: number },
|
|
37
|
+
) {
|
|
38
|
+
const value = entry.value.trim();
|
|
39
|
+
if (!value) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const key = `${entry.profileId}:${entry.kind}:${entry.source}:${value.toLowerCase()}`;
|
|
43
|
+
if (!entries.has(key)) {
|
|
44
|
+
entries.set(key, {
|
|
45
|
+
...entry,
|
|
46
|
+
value,
|
|
47
|
+
weight: entry.weight ?? KIND_WEIGHTS[entry.kind] ?? 10,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseJsonObject(value: unknown) {
|
|
53
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(value) as unknown;
|
|
58
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
59
|
+
? (parsed as Record<string, unknown>)
|
|
60
|
+
: undefined;
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getUrlEntityExpandedUrl(entity: unknown) {
|
|
67
|
+
if (!entity || typeof entity !== "object") {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const record = entity as Record<string, unknown>;
|
|
71
|
+
const expanded = record.expandedUrl ?? record.expanded_url ?? record.url;
|
|
72
|
+
return typeof expanded === "string" && expanded.length > 0
|
|
73
|
+
? expanded
|
|
74
|
+
: undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getProfileBioUrls(profile: ProfileRecord) {
|
|
78
|
+
const description = profile.entities?.description;
|
|
79
|
+
if (!description || typeof description !== "object") {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
const urls = (description as { urls?: unknown }).urls;
|
|
83
|
+
if (!Array.isArray(urls)) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
return urls
|
|
87
|
+
.map(getUrlEntityExpandedUrl)
|
|
88
|
+
.filter((url): url is string => Boolean(url));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function profileFromRow(row: Record<string, unknown>): ProfileRecord {
|
|
92
|
+
const entities = parseJsonObject(row.entities_json);
|
|
93
|
+
return {
|
|
94
|
+
id: String(row.id),
|
|
95
|
+
handle: String(row.handle),
|
|
96
|
+
displayName: String(row.display_name),
|
|
97
|
+
bio: String(row.bio ?? ""),
|
|
98
|
+
followersCount: Number(row.followers_count ?? 0),
|
|
99
|
+
followingCount: Number(row.following_count ?? 0),
|
|
100
|
+
avatarHue: Number(row.avatar_hue ?? 0),
|
|
101
|
+
...(typeof row.avatar_url === "string" && row.avatar_url.length > 0
|
|
102
|
+
? { avatarUrl: row.avatar_url }
|
|
103
|
+
: {}),
|
|
104
|
+
...(typeof row.location === "string" && row.location.length > 0
|
|
105
|
+
? { location: row.location }
|
|
106
|
+
: {}),
|
|
107
|
+
...(typeof row.url === "string" && row.url.length > 0
|
|
108
|
+
? { url: row.url }
|
|
109
|
+
: {}),
|
|
110
|
+
...(typeof row.verified_type === "string" && row.verified_type.length > 0
|
|
111
|
+
? { verifiedType: row.verified_type }
|
|
112
|
+
: {}),
|
|
113
|
+
...(entities ? { entities } : {}),
|
|
114
|
+
createdAt: String(row.created_at),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function collectProfileEntries(
|
|
119
|
+
profile: ProfileRecord,
|
|
120
|
+
): Map<string, IdentityIndexEntry> {
|
|
121
|
+
const entries = new Map<string, IdentityIndexEntry>();
|
|
122
|
+
addEntry(entries, {
|
|
123
|
+
profileId: profile.id,
|
|
124
|
+
kind: "profile_handle",
|
|
125
|
+
value: profile.handle,
|
|
126
|
+
source: "profile",
|
|
127
|
+
});
|
|
128
|
+
addEntry(entries, {
|
|
129
|
+
profileId: profile.id,
|
|
130
|
+
kind: "profile_name",
|
|
131
|
+
value: profile.displayName,
|
|
132
|
+
source: "profile",
|
|
133
|
+
});
|
|
134
|
+
addEntry(entries, {
|
|
135
|
+
profileId: profile.id,
|
|
136
|
+
kind: "profile_bio",
|
|
137
|
+
value: profile.bio,
|
|
138
|
+
source: "profile",
|
|
139
|
+
});
|
|
140
|
+
if (profile.location) {
|
|
141
|
+
addEntry(entries, {
|
|
142
|
+
profileId: profile.id,
|
|
143
|
+
kind: "profile_location",
|
|
144
|
+
value: profile.location,
|
|
145
|
+
source: "profile",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (profile.url) {
|
|
149
|
+
addEntry(entries, {
|
|
150
|
+
profileId: profile.id,
|
|
151
|
+
kind: "profile_url",
|
|
152
|
+
value: profile.url,
|
|
153
|
+
source: "profile",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (profile.verifiedType) {
|
|
157
|
+
addEntry(entries, {
|
|
158
|
+
profileId: profile.id,
|
|
159
|
+
kind: "profile_verified_type",
|
|
160
|
+
value: profile.verifiedType,
|
|
161
|
+
source: "profile",
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
for (const url of getProfileBioUrls(profile)) {
|
|
165
|
+
addEntry(entries, {
|
|
166
|
+
profileId: profile.id,
|
|
167
|
+
kind: "profile_bio_url",
|
|
168
|
+
value: url,
|
|
169
|
+
source: "profile",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return entries;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function addAffiliationEntries(
|
|
176
|
+
entries: Map<string, IdentityIndexEntry>,
|
|
177
|
+
profileId: string,
|
|
178
|
+
values: Array<string | null | undefined>,
|
|
179
|
+
source = "affiliation",
|
|
180
|
+
) {
|
|
181
|
+
for (const value of values) {
|
|
182
|
+
if (!value) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
addEntry(entries, {
|
|
186
|
+
profileId,
|
|
187
|
+
kind: "affiliation",
|
|
188
|
+
value,
|
|
189
|
+
source,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function syncIdentitySearchIndexForProfileIds(
|
|
195
|
+
db: Database,
|
|
196
|
+
profileIds: string[],
|
|
197
|
+
) {
|
|
198
|
+
const uniqueProfileIds = Array.from(new Set(profileIds)).filter(Boolean);
|
|
199
|
+
if (uniqueProfileIds.length === 0) {
|
|
200
|
+
return { profiles: 0, entries: 0 };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const placeholders = uniqueProfileIds.map(() => "?").join(",");
|
|
204
|
+
const rows = db
|
|
205
|
+
.prepare(
|
|
206
|
+
`
|
|
207
|
+
select id, handle, display_name, bio, followers_count, following_count,
|
|
208
|
+
avatar_hue, avatar_url, location, url, verified_type, entities_json, created_at
|
|
209
|
+
from profiles
|
|
210
|
+
where id in (${placeholders})
|
|
211
|
+
`,
|
|
212
|
+
)
|
|
213
|
+
.all(...uniqueProfileIds) as Array<Record<string, unknown>>;
|
|
214
|
+
const profiles = rows.map(profileFromRow);
|
|
215
|
+
const affiliationsByProfile = fetchProfileAffiliations(db, uniqueProfileIds);
|
|
216
|
+
const bioEntitiesByProfile = fetchProfileBioEntities(db, uniqueProfileIds);
|
|
217
|
+
const snapshotsByProfile = fetchProfileSnapshots(db, uniqueProfileIds);
|
|
218
|
+
const entries = new Map<string, IdentityIndexEntry>();
|
|
219
|
+
|
|
220
|
+
for (const profile of profiles) {
|
|
221
|
+
for (const [key, entry] of collectProfileEntries(profile)) {
|
|
222
|
+
entries.set(key, entry);
|
|
223
|
+
}
|
|
224
|
+
for (const affiliation of affiliationsByProfile.get(profile.id) ?? []) {
|
|
225
|
+
addAffiliationEntries(entries, profile.id, [
|
|
226
|
+
affiliation.organizationName,
|
|
227
|
+
affiliation.organizationHandle,
|
|
228
|
+
affiliation.label,
|
|
229
|
+
affiliation.url,
|
|
230
|
+
affiliation.organizationProfileId,
|
|
231
|
+
]);
|
|
232
|
+
}
|
|
233
|
+
for (const entity of bioEntitiesByProfile.get(profile.id) ?? []) {
|
|
234
|
+
addEntry(entries, {
|
|
235
|
+
profileId: profile.id,
|
|
236
|
+
kind:
|
|
237
|
+
entity.kind === "handle"
|
|
238
|
+
? "bio_handle"
|
|
239
|
+
: entity.kind === "domain"
|
|
240
|
+
? "bio_domain"
|
|
241
|
+
: "bio_company",
|
|
242
|
+
value: entity.value,
|
|
243
|
+
source: "bio_entity",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
for (const snapshot of snapshotsByProfile.get(profile.id) ?? []) {
|
|
247
|
+
addEntry(entries, {
|
|
248
|
+
profileId: profile.id,
|
|
249
|
+
kind: "profile_history",
|
|
250
|
+
value: snapshot.handle,
|
|
251
|
+
source: "history",
|
|
252
|
+
});
|
|
253
|
+
addEntry(entries, {
|
|
254
|
+
profileId: profile.id,
|
|
255
|
+
kind: "profile_history",
|
|
256
|
+
value: snapshot.displayName,
|
|
257
|
+
source: "history",
|
|
258
|
+
});
|
|
259
|
+
addEntry(entries, {
|
|
260
|
+
profileId: profile.id,
|
|
261
|
+
kind: "profile_history",
|
|
262
|
+
value: snapshot.bio,
|
|
263
|
+
source: "history",
|
|
264
|
+
});
|
|
265
|
+
if (snapshot.location) {
|
|
266
|
+
addEntry(entries, {
|
|
267
|
+
profileId: profile.id,
|
|
268
|
+
kind: "profile_history",
|
|
269
|
+
value: snapshot.location,
|
|
270
|
+
source: "history",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (snapshot.url) {
|
|
274
|
+
addEntry(entries, {
|
|
275
|
+
profileId: profile.id,
|
|
276
|
+
kind: "profile_history",
|
|
277
|
+
value: snapshot.url,
|
|
278
|
+
source: "history",
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
for (const affiliation of snapshot.affiliations) {
|
|
282
|
+
if (!affiliation || typeof affiliation !== "object") {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const record = affiliation as Record<string, unknown>;
|
|
286
|
+
addAffiliationEntries(
|
|
287
|
+
entries,
|
|
288
|
+
profile.id,
|
|
289
|
+
[
|
|
290
|
+
record.organizationName,
|
|
291
|
+
record.organizationHandle,
|
|
292
|
+
record.label,
|
|
293
|
+
record.url,
|
|
294
|
+
].filter((value): value is string => typeof value === "string"),
|
|
295
|
+
"history",
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const now = new Date().toISOString();
|
|
302
|
+
const deleteStatement = db.prepare(
|
|
303
|
+
`delete from identity_search_index where profile_id = ?`,
|
|
304
|
+
);
|
|
305
|
+
const insertStatement = db.prepare(
|
|
306
|
+
`
|
|
307
|
+
insert into identity_search_index (
|
|
308
|
+
profile_id, kind, value, normalized_value, source, weight, updated_at
|
|
309
|
+
) values (?, ?, ?, ?, ?, ?, ?)
|
|
310
|
+
on conflict(profile_id, kind, value, source) do update set
|
|
311
|
+
normalized_value = excluded.normalized_value,
|
|
312
|
+
weight = excluded.weight,
|
|
313
|
+
updated_at = excluded.updated_at
|
|
314
|
+
`,
|
|
315
|
+
);
|
|
316
|
+
db.transaction(() => {
|
|
317
|
+
for (const profileId of uniqueProfileIds) {
|
|
318
|
+
deleteStatement.run(profileId);
|
|
319
|
+
}
|
|
320
|
+
for (const entry of entries.values()) {
|
|
321
|
+
insertStatement.run(
|
|
322
|
+
entry.profileId,
|
|
323
|
+
entry.kind,
|
|
324
|
+
entry.value,
|
|
325
|
+
normalizeIndexValue(entry.value),
|
|
326
|
+
entry.source,
|
|
327
|
+
entry.weight,
|
|
328
|
+
now,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
})();
|
|
332
|
+
|
|
333
|
+
return { profiles: profiles.length, entries: entries.size };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function ensureIdentitySearchIndexForDmProfiles(
|
|
337
|
+
db: Database,
|
|
338
|
+
account?: string,
|
|
339
|
+
) {
|
|
340
|
+
const params: string[] = [];
|
|
341
|
+
let accountClause = "";
|
|
342
|
+
if (account && account !== "all") {
|
|
343
|
+
accountClause = "where c.account_id = ?";
|
|
344
|
+
params.push(account);
|
|
345
|
+
}
|
|
346
|
+
const rows = db
|
|
347
|
+
.prepare(
|
|
348
|
+
`
|
|
349
|
+
select distinct c.participant_profile_id as profile_id
|
|
350
|
+
from dm_conversations c
|
|
351
|
+
left join identity_search_index isi
|
|
352
|
+
on isi.profile_id = c.participant_profile_id
|
|
353
|
+
${accountClause}
|
|
354
|
+
group by c.participant_profile_id
|
|
355
|
+
having count(isi.profile_id) = 0
|
|
356
|
+
`,
|
|
357
|
+
)
|
|
358
|
+
.all(...params) as Array<{ profile_id: string }>;
|
|
359
|
+
|
|
360
|
+
return syncIdentitySearchIndexForProfileIds(
|
|
361
|
+
db,
|
|
362
|
+
rows.map((row) => row.profile_id),
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export const __test__ = {
|
|
367
|
+
normalizeIndexValue,
|
|
368
|
+
getProfileBioUrls,
|
|
369
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type Database from "
|
|
1
|
+
import type { Database } from "./sqlite";
|
|
2
2
|
import { listThreadViaBird } from "./bird";
|
|
3
3
|
import { getNativeDb } from "./db";
|
|
4
4
|
import type { XurlMentionData, XurlMentionsResponse } from "./types";
|
|
@@ -39,7 +39,7 @@ function getMediaCount(tweet: XurlMentionData) {
|
|
|
39
39
|
).length;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function replaceTweetFts(db: Database
|
|
42
|
+
function replaceTweetFts(db: Database, tweetId: string, text: string) {
|
|
43
43
|
db.prepare("delete from tweets_fts where tweet_id = ?").run(tweetId);
|
|
44
44
|
db.prepare("insert into tweets_fts (tweet_id, text) values (?, ?)").run(
|
|
45
45
|
tweetId,
|
|
@@ -47,7 +47,7 @@ function replaceTweetFts(db: Database.Database, tweetId: string, text: string) {
|
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function resolveAccount(db: Database
|
|
50
|
+
function resolveAccount(db: Database, accountId?: string) {
|
|
51
51
|
const row = accountId
|
|
52
52
|
? (db
|
|
53
53
|
.prepare("select id, handle from accounts where id = ?")
|
|
@@ -73,11 +73,7 @@ function resolveAccount(db: Database.Database, accountId?: string) {
|
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function listRecentMentionIds(
|
|
77
|
-
db: Database.Database,
|
|
78
|
-
accountId: string,
|
|
79
|
-
limit: number,
|
|
80
|
-
) {
|
|
76
|
+
function listRecentMentionIds(db: Database, accountId: string, limit: number) {
|
|
81
77
|
return (
|
|
82
78
|
db
|
|
83
79
|
.prepare(
|
|
@@ -105,7 +101,7 @@ function mergeMentionThreadIntoLocalStore({
|
|
|
105
101
|
mentionIds,
|
|
106
102
|
payload,
|
|
107
103
|
}: {
|
|
108
|
-
db: Database
|
|
104
|
+
db: Database;
|
|
109
105
|
accountId: string;
|
|
110
106
|
accountHandle: string;
|
|
111
107
|
mentionIds: Set<string>;
|
package/src/lib/mentions-live.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type Database from "
|
|
1
|
+
import type { Database } from "./sqlite";
|
|
2
2
|
import { listMentionsViaBird } from "./bird";
|
|
3
3
|
import type { MentionsDataSource } from "./config";
|
|
4
4
|
import { getNativeDb } from "./db";
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
XurlMentionsResponse,
|
|
13
13
|
XurlMentionUser,
|
|
14
14
|
} from "./types";
|
|
15
|
+
import { upsertTweetAccountEdge } from "./tweet-account-edges";
|
|
15
16
|
import { ensureStubProfileForXUser, upsertProfileFromXUser } from "./x-profile";
|
|
16
17
|
import { listMentionsViaXurl, lookupUsersByHandles } from "./xurl";
|
|
17
18
|
|
|
@@ -64,7 +65,7 @@ function parseMaxPages(value?: number) {
|
|
|
64
65
|
return Math.floor(value);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
function resolveAccount(db: Database
|
|
68
|
+
function resolveAccount(db: Database, accountId?: string) {
|
|
68
69
|
const row = accountId
|
|
69
70
|
? (db
|
|
70
71
|
.prepare("select id, handle from accounts where id = ?")
|
|
@@ -149,7 +150,7 @@ function getMediaCount(tweet: XurlMentionData) {
|
|
|
149
150
|
).length;
|
|
150
151
|
}
|
|
151
152
|
|
|
152
|
-
function replaceTweetFts(db: Database
|
|
153
|
+
function replaceTweetFts(db: Database, tweetId: string, text: string) {
|
|
153
154
|
db.prepare("delete from tweets_fts where tweet_id = ?").run(tweetId);
|
|
154
155
|
db.prepare("insert into tweets_fts (tweet_id, text) values (?, ?)").run(
|
|
155
156
|
tweetId,
|
|
@@ -158,9 +159,10 @@ function replaceTweetFts(db: Database.Database, tweetId: string, text: string) {
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
function mergeMentionsIntoLocalStore(
|
|
161
|
-
db: Database
|
|
162
|
+
db: Database,
|
|
162
163
|
accountId: string,
|
|
163
164
|
payload: XurlMentionsResponse,
|
|
165
|
+
source: MentionsDataSource,
|
|
164
166
|
) {
|
|
165
167
|
const usersById = new Map(
|
|
166
168
|
(payload.includes?.users ?? []).map((user) => [user.id, user]),
|
|
@@ -173,9 +175,9 @@ function mergeMentionsIntoLocalStore(
|
|
|
173
175
|
entities_json, media_json, quoted_tweet_id
|
|
174
176
|
) values (?, ?, ?, 'mention', ?, ?, 0, null, ?, ?, 0, 0, ?, '[]', null)
|
|
175
177
|
on conflict(id) do update set
|
|
176
|
-
account_id =
|
|
178
|
+
account_id = tweets.account_id,
|
|
177
179
|
author_profile_id = excluded.author_profile_id,
|
|
178
|
-
kind = excluded.kind,
|
|
180
|
+
kind = case when tweets.kind = 'home' then tweets.kind else excluded.kind end,
|
|
179
181
|
text = excluded.text,
|
|
180
182
|
created_at = excluded.created_at,
|
|
181
183
|
like_count = excluded.like_count,
|
|
@@ -189,6 +191,7 @@ function mergeMentionsIntoLocalStore(
|
|
|
189
191
|
);
|
|
190
192
|
|
|
191
193
|
const writePayload = db.transaction(() => {
|
|
194
|
+
const seenAt = new Date().toISOString();
|
|
192
195
|
for (const tweet of payload.data) {
|
|
193
196
|
const author =
|
|
194
197
|
usersById.get(tweet.author_id) ??
|
|
@@ -210,6 +213,14 @@ function mergeMentionsIntoLocalStore(
|
|
|
210
213
|
getMediaCount(tweet),
|
|
211
214
|
JSON.stringify(toLocalEntities(tweet)),
|
|
212
215
|
);
|
|
216
|
+
upsertTweetAccountEdge(db, {
|
|
217
|
+
accountId,
|
|
218
|
+
tweetId: tweet.id,
|
|
219
|
+
kind: "mention",
|
|
220
|
+
source,
|
|
221
|
+
seenAt,
|
|
222
|
+
rawJson: JSON.stringify(tweet),
|
|
223
|
+
});
|
|
213
224
|
replaceTweetFts(db, tweet.id, tweet.text);
|
|
214
225
|
}
|
|
215
226
|
});
|
|
@@ -408,7 +419,7 @@ async function exportMentionsViaCachedLiveSource({
|
|
|
408
419
|
all: fetchAll,
|
|
409
420
|
parsedMaxPages,
|
|
410
421
|
});
|
|
411
|
-
mergeMentionsIntoLocalStore(db, resolvedAccount.accountId, payload);
|
|
422
|
+
mergeMentionsIntoLocalStore(db, resolvedAccount.accountId, payload, mode);
|
|
412
423
|
writeSyncCache(cacheKey, payload, db);
|
|
413
424
|
|
|
414
425
|
if (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type Database from "
|
|
1
|
+
import type { Database } from "./sqlite";
|
|
2
2
|
import { lookupProfileViaBird } from "./bird-actions";
|
|
3
3
|
import { getNativeDb } from "./db";
|
|
4
4
|
import type { ProfileRecord, XurlMentionUser } from "./types";
|
|
@@ -41,7 +41,7 @@ export function normalizeProfileQuery(value: string) {
|
|
|
41
41
|
return (urlMatch?.[1] ?? withoutPrefix).replace(/^@/, "").trim();
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export function getDefaultAccountId(db: Database
|
|
44
|
+
export function getDefaultAccountId(db: Database) {
|
|
45
45
|
const row = db
|
|
46
46
|
.prepare(
|
|
47
47
|
`
|
|
@@ -55,7 +55,7 @@ export function getDefaultAccountId(db: Database.Database) {
|
|
|
55
55
|
return row?.id ?? "acct_primary";
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
export function getAccountHandle(db: Database
|
|
58
|
+
export function getAccountHandle(db: Database, accountId: string) {
|
|
59
59
|
const row = db
|
|
60
60
|
.prepare("select handle from accounts where id = ?")
|
|
61
61
|
.get(accountId) as { handle: string } | undefined;
|
|
@@ -63,7 +63,7 @@ export function getAccountHandle(db: Database.Database, accountId: string) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export function resolveLocalProfile(
|
|
66
|
-
db: Database
|
|
66
|
+
db: Database,
|
|
67
67
|
normalizedQuery: string,
|
|
68
68
|
): ResolvedModerationProfile | null {
|
|
69
69
|
const row = db
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type Database from "
|
|
1
|
+
import type { Database } from "./sqlite";
|
|
2
2
|
import type { ActionsTransport } from "./config";
|
|
3
3
|
import { getNativeDb } from "./db";
|
|
4
4
|
import {
|
|
@@ -43,7 +43,7 @@ export async function resolveModerationTarget({
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function writeModerationRow(
|
|
46
|
-
db: Database
|
|
46
|
+
db: Database,
|
|
47
47
|
table: "blocks" | "mutes",
|
|
48
48
|
accountId: string,
|
|
49
49
|
profileId: string,
|
|
@@ -61,7 +61,7 @@ export function writeModerationRow(
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export function deleteModerationRow(
|
|
64
|
-
db: Database
|
|
64
|
+
db: Database,
|
|
65
65
|
table: "blocks" | "mutes",
|
|
66
66
|
accountId: string,
|
|
67
67
|
profileId: string,
|