birdclaw 0.4.1 → 0.5.1
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 +52 -0
- package/README.md +113 -7
- package/bin/birdclaw.mjs +50 -11
- package/package.json +30 -28
- package/playwright.config.ts +1 -0
- package/public/birdclaw-mark.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +2 -2
- package/scripts/browser-perf.mjs +399 -0
- package/scripts/build-docs-site.mjs +940 -0
- package/scripts/docs-site-assets.mjs +311 -0
- package/scripts/run-vitest.mjs +21 -0
- package/scripts/sanitize-node-options.mjs +23 -0
- package/scripts/start-test-server.mjs +29 -0
- package/src/cli.ts +496 -19
- package/src/components/AppNav.tsx +66 -29
- package/src/components/AvatarChip.tsx +10 -5
- package/src/components/BrandMark.tsx +67 -0
- package/src/components/ConversationThread.tsx +126 -0
- package/src/components/DmWorkspace.tsx +118 -105
- package/src/components/EmbeddedTweetCard.tsx +20 -14
- package/src/components/FeedState.tsx +147 -0
- package/src/components/InboxCard.tsx +104 -90
- package/src/components/LinkPreviewCard.tsx +270 -0
- package/src/components/ProfilePreview.tsx +8 -3
- package/src/components/SavedTimelineView.tsx +89 -71
- package/src/components/SyncNowButton.tsx +105 -0
- package/src/components/ThemeSlider.tsx +10 -59
- package/src/components/TimelineCard.tsx +326 -86
- package/src/components/TimelineRouteFrame.tsx +156 -0
- package/src/components/TweetMediaGrid.tsx +120 -23
- package/src/components/TweetRichText.tsx +19 -4
- package/src/components/useTimelineRouteData.ts +137 -0
- package/src/lib/api-client.ts +229 -0
- package/src/lib/archive-finder.ts +24 -20
- package/src/lib/archive-import.ts +1582 -67
- package/src/lib/authored-live.ts +1074 -0
- package/src/lib/backup.ts +316 -14
- package/src/lib/bird-actions.ts +1 -10
- package/src/lib/bird-command.ts +57 -0
- package/src/lib/bird.ts +89 -5
- package/src/lib/config.ts +1 -1
- package/src/lib/conversation-surface.ts +174 -0
- package/src/lib/db.ts +193 -4
- package/src/lib/follow-graph.ts +1053 -0
- package/src/lib/link-index.ts +11 -98
- package/src/lib/link-insights.ts +834 -0
- package/src/lib/link-preview-metadata.ts +334 -0
- package/src/lib/media-fetch.ts +787 -0
- package/src/lib/media-includes.ts +165 -0
- package/src/lib/mention-threads-live.ts +535 -43
- package/src/lib/mentions-live.ts +623 -19
- package/src/lib/moderation-target.ts +6 -0
- package/src/lib/profile-hydration.ts +1 -1
- package/src/lib/profile-resolver.ts +115 -1
- package/src/lib/queries.ts +326 -35
- package/src/lib/seed.ts +127 -8
- package/src/lib/timeline-collections-live.ts +145 -22
- package/src/lib/timeline-live.ts +10 -15
- package/src/lib/tweet-account-edges.ts +9 -2
- package/src/lib/tweet-render.ts +97 -0
- package/src/lib/types.ts +185 -2
- package/src/lib/ui.ts +383 -147
- package/src/lib/url-expansion-store.ts +110 -0
- package/src/lib/url-expansion.ts +20 -3
- package/src/lib/web-sync.ts +443 -0
- package/src/lib/x-profile.ts +7 -2
- package/src/lib/xurl.ts +296 -12
- package/src/routeTree.gen.ts +126 -0
- package/src/routes/__root.tsx +7 -3
- package/src/routes/api/conversation.tsx +34 -0
- package/src/routes/api/link-insights.tsx +79 -0
- package/src/routes/api/link-preview.tsx +43 -0
- package/src/routes/api/profile-hydrate.tsx +51 -0
- package/src/routes/api/sync.tsx +59 -0
- package/src/routes/blocks.tsx +111 -86
- package/src/routes/dms.tsx +172 -87
- package/src/routes/inbox.tsx +98 -86
- package/src/routes/index.tsx +22 -115
- package/src/routes/links.tsx +928 -0
- package/src/routes/mentions.tsx +22 -115
- package/src/styles.css +169 -43
- package/vite.config.ts +8 -0
package/src/lib/tweet-render.ts
CHANGED
|
@@ -10,6 +10,14 @@ type TweetSegment =
|
|
|
10
10
|
| ({ kind: "url" } & TweetUrlEntity)
|
|
11
11
|
| ({ kind: "hashtag" } & TweetHashtagEntity);
|
|
12
12
|
|
|
13
|
+
type UrlExpansion = Pick<TweetUrlEntity, "expandedUrl" | "displayUrl"> &
|
|
14
|
+
Partial<
|
|
15
|
+
Pick<TweetUrlEntity, "title" | "description" | "imageUrl" | "siteName">
|
|
16
|
+
>;
|
|
17
|
+
|
|
18
|
+
const RAW_URL_PATTERN = /https?:\/\/[^\s<>"'`]+/g;
|
|
19
|
+
const TRAILING_URL_PUNCTUATION = /[),.;:!?]+$/;
|
|
20
|
+
|
|
13
21
|
const MARKDOWN_ESCAPE_CHARACTERS = new Set([
|
|
14
22
|
"\\",
|
|
15
23
|
"`",
|
|
@@ -38,6 +46,95 @@ function escapeMarkdown(text: string) {
|
|
|
38
46
|
.join("");
|
|
39
47
|
}
|
|
40
48
|
|
|
49
|
+
export function displayUrlForLink(url: string) {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = new URL(url);
|
|
52
|
+
const host = parsed.hostname.replace(/^www\./, "");
|
|
53
|
+
const suffix = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
54
|
+
return suffix === "/" ? host : `${host}${suffix}`;
|
|
55
|
+
} catch {
|
|
56
|
+
return url;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function spansOverlap(
|
|
61
|
+
leftStart: number,
|
|
62
|
+
leftEnd: number,
|
|
63
|
+
rightStart: number,
|
|
64
|
+
rightEnd: number,
|
|
65
|
+
) {
|
|
66
|
+
return leftStart < rightEnd && rightStart < leftEnd;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function enrichFallbackUrlEntities(
|
|
70
|
+
text: string,
|
|
71
|
+
entities: TweetEntities,
|
|
72
|
+
resolveExpansion?: (rawUrl: string) => UrlExpansion | null | undefined,
|
|
73
|
+
): TweetEntities {
|
|
74
|
+
const existingUrls = entities.urls ?? [];
|
|
75
|
+
const enrichedExistingUrls = existingUrls.map((entry) => {
|
|
76
|
+
const expansion = resolveExpansion?.(entry.url);
|
|
77
|
+
if (!expansion) return entry;
|
|
78
|
+
const expandedUrl = expansion.expandedUrl || entry.expandedUrl;
|
|
79
|
+
return {
|
|
80
|
+
...entry,
|
|
81
|
+
expandedUrl,
|
|
82
|
+
displayUrl: expansion.displayUrl || displayUrlForLink(expandedUrl),
|
|
83
|
+
...(expansion.title ? { title: expansion.title } : {}),
|
|
84
|
+
...(expansion && "description" in expansion
|
|
85
|
+
? { description: expansion.description ?? null }
|
|
86
|
+
: {}),
|
|
87
|
+
...(expansion.imageUrl ? { imageUrl: expansion.imageUrl } : {}),
|
|
88
|
+
...(expansion.siteName ? { siteName: expansion.siteName } : {}),
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
const fallbackUrls: TweetUrlEntity[] = [];
|
|
92
|
+
|
|
93
|
+
for (const match of text.matchAll(RAW_URL_PATTERN)) {
|
|
94
|
+
const rawMatch = match[0];
|
|
95
|
+
const url = rawMatch.replace(TRAILING_URL_PUNCTUATION, "");
|
|
96
|
+
const start = match.index ?? 0;
|
|
97
|
+
const end = start + url.length;
|
|
98
|
+
if (!url) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
enrichedExistingUrls.some((entry) =>
|
|
103
|
+
spansOverlap(start, end, entry.start, entry.end),
|
|
104
|
+
)
|
|
105
|
+
) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const expansion = resolveExpansion?.(url);
|
|
110
|
+
const expandedUrl = expansion?.expandedUrl || url;
|
|
111
|
+
fallbackUrls.push({
|
|
112
|
+
url,
|
|
113
|
+
expandedUrl,
|
|
114
|
+
displayUrl: expansion?.displayUrl || displayUrlForLink(expandedUrl),
|
|
115
|
+
start,
|
|
116
|
+
end,
|
|
117
|
+
...(expansion?.title ? { title: expansion.title } : {}),
|
|
118
|
+
...(expansion && "description" in expansion
|
|
119
|
+
? { description: expansion.description ?? null }
|
|
120
|
+
: {}),
|
|
121
|
+
...(expansion?.imageUrl ? { imageUrl: expansion.imageUrl } : {}),
|
|
122
|
+
...(expansion?.siteName ? { siteName: expansion.siteName } : {}),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (fallbackUrls.length === 0) {
|
|
127
|
+
return { ...entities, urls: enrichedExistingUrls };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
...entities,
|
|
132
|
+
urls: [...enrichedExistingUrls, ...fallbackUrls].sort(
|
|
133
|
+
(left, right) => left.start - right.start,
|
|
134
|
+
),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
41
138
|
export function collectTweetSegments(entities: TweetEntities): TweetSegment[] {
|
|
42
139
|
return [
|
|
43
140
|
...(entities.mentions?.map((entry) => ({
|
package/src/lib/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ResourceKind = "home" | "mentions" | "dms";
|
|
1
|
+
export type ResourceKind = "home" | "mentions" | "authored" | "dms";
|
|
2
2
|
export type InboxKind = "mixed" | "mentions" | "dms";
|
|
3
3
|
|
|
4
4
|
export type ReplyFilter = "all" | "replied" | "unreplied";
|
|
@@ -88,6 +88,8 @@ export interface TweetUrlEntity {
|
|
|
88
88
|
end: number;
|
|
89
89
|
title?: string;
|
|
90
90
|
description?: string | null;
|
|
91
|
+
imageUrl?: string | null;
|
|
92
|
+
siteName?: string | null;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
export interface TweetHashtagEntity {
|
|
@@ -109,17 +111,29 @@ export interface TweetMediaItem {
|
|
|
109
111
|
width?: number;
|
|
110
112
|
height?: number;
|
|
111
113
|
thumbnailUrl?: string;
|
|
114
|
+
durationMs?: number;
|
|
115
|
+
variants?: Array<{
|
|
116
|
+
url: string;
|
|
117
|
+
contentType?: string;
|
|
118
|
+
bitRate?: number;
|
|
119
|
+
}>;
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
export interface EmbeddedTweet {
|
|
115
123
|
id: string;
|
|
116
124
|
text: string;
|
|
117
125
|
createdAt: string;
|
|
126
|
+
replyToId?: string | null;
|
|
118
127
|
author: ProfileRecord;
|
|
119
128
|
entities: TweetEntities;
|
|
120
129
|
media: TweetMediaItem[];
|
|
121
130
|
}
|
|
122
131
|
|
|
132
|
+
export interface TweetConversationResponse {
|
|
133
|
+
anchorId: string;
|
|
134
|
+
items: EmbeddedTweet[];
|
|
135
|
+
}
|
|
136
|
+
|
|
123
137
|
export interface BlockItem {
|
|
124
138
|
accountId: string;
|
|
125
139
|
accountHandle: string;
|
|
@@ -143,10 +157,11 @@ export interface TimelineItem {
|
|
|
143
157
|
id: string;
|
|
144
158
|
accountId: string;
|
|
145
159
|
accountHandle: string;
|
|
146
|
-
kind: "home" | "mention" | "like" | "bookmark";
|
|
160
|
+
kind: "home" | "mention" | "authored" | "like" | "bookmark";
|
|
147
161
|
text: string;
|
|
148
162
|
searchSnippet?: string;
|
|
149
163
|
createdAt: string;
|
|
164
|
+
replyToId?: string | null;
|
|
150
165
|
isReplied: boolean;
|
|
151
166
|
likeCount: number;
|
|
152
167
|
mediaCount: number;
|
|
@@ -203,6 +218,8 @@ export interface LinkIndexItem {
|
|
|
203
218
|
expandedHandle?: string | null;
|
|
204
219
|
title?: string | null;
|
|
205
220
|
description?: string | null;
|
|
221
|
+
imageUrl?: string | null;
|
|
222
|
+
siteName?: string | null;
|
|
206
223
|
error?: string | null;
|
|
207
224
|
source: string;
|
|
208
225
|
updatedAt: string;
|
|
@@ -217,6 +234,86 @@ export interface LinkSearchItem {
|
|
|
217
234
|
linkedTweet?: TimelineItem | null;
|
|
218
235
|
}
|
|
219
236
|
|
|
237
|
+
export type LinkInsightKind = "links" | "videos";
|
|
238
|
+
export type LinkInsightRange = "today" | "week" | "month" | "year" | "all";
|
|
239
|
+
export type LinkInsightSort = "rank" | "recent" | "comments";
|
|
240
|
+
export type LinkInsightSource = "all" | "tweet" | "dm";
|
|
241
|
+
|
|
242
|
+
export interface LinkInsightMention {
|
|
243
|
+
id: string;
|
|
244
|
+
sourceKind: "dm" | "tweet";
|
|
245
|
+
sourceId: string;
|
|
246
|
+
sourceUrl?: string | null;
|
|
247
|
+
sourceLabel: string;
|
|
248
|
+
shortUrl: string;
|
|
249
|
+
conversationId?: string | null;
|
|
250
|
+
createdAt: string;
|
|
251
|
+
text: string;
|
|
252
|
+
rawText: string;
|
|
253
|
+
commentText: string;
|
|
254
|
+
sharedContentText?: string | null;
|
|
255
|
+
hasComment: boolean;
|
|
256
|
+
isPureShare: boolean;
|
|
257
|
+
timelineTweetId?: string | null;
|
|
258
|
+
contentTweetId?: string | null;
|
|
259
|
+
contentTweetUrl?: string | null;
|
|
260
|
+
contentAuthor?: ProfileRecord | null;
|
|
261
|
+
media: TweetMediaItem[];
|
|
262
|
+
direction?: string | null;
|
|
263
|
+
accountHandle?: string | null;
|
|
264
|
+
sharedBy?: ProfileRecord | null;
|
|
265
|
+
participant?: ProfileRecord | null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface LinkInsightItem {
|
|
269
|
+
id: string;
|
|
270
|
+
kind: LinkInsightKind;
|
|
271
|
+
url: string;
|
|
272
|
+
canonicalKey: string;
|
|
273
|
+
displayUrl: string;
|
|
274
|
+
host: string;
|
|
275
|
+
title?: string | null;
|
|
276
|
+
description?: string | null;
|
|
277
|
+
shareCount: number;
|
|
278
|
+
uniqueSharers: number;
|
|
279
|
+
totalInfluence: number;
|
|
280
|
+
mentionCount: number;
|
|
281
|
+
commentCount: number;
|
|
282
|
+
pureShareCount: number;
|
|
283
|
+
hiddenMentionCount: number;
|
|
284
|
+
firstSeenAt: string;
|
|
285
|
+
lastSeenAt: string;
|
|
286
|
+
topSharer?: ProfileRecord | null;
|
|
287
|
+
sharers: ProfileRecord[];
|
|
288
|
+
mentions: LinkInsightMention[];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface LinkInsightQuery {
|
|
292
|
+
kind?: LinkInsightKind;
|
|
293
|
+
range?: LinkInsightRange;
|
|
294
|
+
sort?: LinkInsightSort;
|
|
295
|
+
source?: LinkInsightSource;
|
|
296
|
+
since?: string;
|
|
297
|
+
until?: string;
|
|
298
|
+
limit?: number;
|
|
299
|
+
commentsLimit?: number;
|
|
300
|
+
now?: Date;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface LinkInsightResponse {
|
|
304
|
+
kind: LinkInsightKind;
|
|
305
|
+
range: LinkInsightRange;
|
|
306
|
+
sort: LinkInsightSort;
|
|
307
|
+
source: LinkInsightSource;
|
|
308
|
+
since: string | null;
|
|
309
|
+
until: string | null;
|
|
310
|
+
items: LinkInsightItem[];
|
|
311
|
+
stats: {
|
|
312
|
+
occurrences: number;
|
|
313
|
+
groups: number;
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
220
317
|
export interface DmSearchMatchItem {
|
|
221
318
|
message: DmMessageItem;
|
|
222
319
|
before: DmMessageItem[];
|
|
@@ -361,6 +458,8 @@ export interface XurlPublicMetrics {
|
|
|
361
458
|
impression_count?: number;
|
|
362
459
|
followers_count?: number;
|
|
363
460
|
following_count?: number;
|
|
461
|
+
tweet_count?: number;
|
|
462
|
+
listed_count?: number;
|
|
364
463
|
}
|
|
365
464
|
|
|
366
465
|
export interface XurlMentionUser {
|
|
@@ -377,6 +476,7 @@ export interface XurlMentionUser {
|
|
|
377
476
|
affiliation?: Record<string, unknown>;
|
|
378
477
|
public_metrics?: XurlPublicMetrics;
|
|
379
478
|
created_at?: string;
|
|
479
|
+
protected?: boolean;
|
|
380
480
|
}
|
|
381
481
|
|
|
382
482
|
export interface XurlMentionData {
|
|
@@ -385,6 +485,8 @@ export interface XurlMentionData {
|
|
|
385
485
|
text: string;
|
|
386
486
|
created_at: string;
|
|
387
487
|
conversation_id?: string;
|
|
488
|
+
in_reply_to_user_id?: string;
|
|
489
|
+
attachments?: XurlTweetAttachments;
|
|
388
490
|
entities?: Record<string, unknown>;
|
|
389
491
|
referenced_tweets?: XurlReferencedTweet[];
|
|
390
492
|
public_metrics?: XurlPublicMetrics;
|
|
@@ -398,9 +500,12 @@ export interface XurlReferencedTweet {
|
|
|
398
500
|
|
|
399
501
|
export interface XurlUserTweet {
|
|
400
502
|
id: string;
|
|
503
|
+
author_id?: string;
|
|
401
504
|
text: string;
|
|
402
505
|
created_at: string;
|
|
403
506
|
conversation_id?: string;
|
|
507
|
+
attachments?: XurlTweetAttachments;
|
|
508
|
+
entities?: Record<string, unknown>;
|
|
404
509
|
referenced_tweets?: XurlReferencedTweet[];
|
|
405
510
|
public_metrics?: XurlPublicMetrics;
|
|
406
511
|
edit_history_tweet_ids?: string[];
|
|
@@ -412,12 +517,50 @@ export interface XurlTweetData {
|
|
|
412
517
|
text: string;
|
|
413
518
|
created_at: string;
|
|
414
519
|
conversation_id?: string;
|
|
520
|
+
in_reply_to_user_id?: string;
|
|
521
|
+
attachments?: XurlTweetAttachments;
|
|
415
522
|
entities?: Record<string, unknown>;
|
|
416
523
|
referenced_tweets?: XurlReferencedTweet[];
|
|
417
524
|
public_metrics?: XurlPublicMetrics;
|
|
418
525
|
edit_history_tweet_ids?: string[];
|
|
419
526
|
}
|
|
420
527
|
|
|
528
|
+
export interface XurlTweetAttachments {
|
|
529
|
+
media_keys?: string[];
|
|
530
|
+
poll_ids?: string[];
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export interface XurlMediaItem {
|
|
534
|
+
media_key: string;
|
|
535
|
+
type: "photo" | "video" | "animated_gif" | string;
|
|
536
|
+
url?: string;
|
|
537
|
+
preview_image_url?: string;
|
|
538
|
+
duration_ms?: number;
|
|
539
|
+
width?: number;
|
|
540
|
+
height?: number;
|
|
541
|
+
alt_text?: string;
|
|
542
|
+
public_metrics?: XurlPublicMetrics;
|
|
543
|
+
variants?: Array<{
|
|
544
|
+
url: string;
|
|
545
|
+
content_type: string;
|
|
546
|
+
bit_rate?: number;
|
|
547
|
+
}>;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export type XurlMedia = XurlMediaItem;
|
|
551
|
+
|
|
552
|
+
export interface XurlTweetIncludes {
|
|
553
|
+
users?: XurlMentionUser[];
|
|
554
|
+
tweets?: XurlTweetData[];
|
|
555
|
+
media?: XurlMedia[];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export interface XurlUserTweetsResponse {
|
|
559
|
+
items: XurlUserTweet[];
|
|
560
|
+
nextToken: string | null;
|
|
561
|
+
includes?: XurlTweetIncludes;
|
|
562
|
+
}
|
|
563
|
+
|
|
421
564
|
export interface ProfileReplyItem {
|
|
422
565
|
id: string;
|
|
423
566
|
text: string;
|
|
@@ -447,6 +590,7 @@ export interface XurlMentionsResponse {
|
|
|
447
590
|
data: XurlMentionData[];
|
|
448
591
|
includes?: {
|
|
449
592
|
users?: XurlMentionUser[];
|
|
593
|
+
media?: XurlMediaItem[];
|
|
450
594
|
};
|
|
451
595
|
meta?: Record<string, unknown>;
|
|
452
596
|
}
|
|
@@ -455,6 +599,45 @@ export interface XurlTweetsResponse {
|
|
|
455
599
|
data: XurlTweetData[];
|
|
456
600
|
includes?: {
|
|
457
601
|
users?: XurlMentionUser[];
|
|
602
|
+
media?: XurlMediaItem[];
|
|
458
603
|
};
|
|
459
604
|
meta?: Record<string, unknown>;
|
|
460
605
|
}
|
|
606
|
+
|
|
607
|
+
export type FollowDirection = "followers" | "following";
|
|
608
|
+
|
|
609
|
+
export interface XurlFollowUsersResponse {
|
|
610
|
+
data: XurlMentionUser[];
|
|
611
|
+
meta?: Record<string, unknown>;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export interface FollowGraphProfile {
|
|
615
|
+
id: string;
|
|
616
|
+
externalUserId: string;
|
|
617
|
+
handle: string;
|
|
618
|
+
displayName: string;
|
|
619
|
+
bio: string;
|
|
620
|
+
followersCount: number;
|
|
621
|
+
publicMetrics: XurlPublicMetrics;
|
|
622
|
+
avatarUrl?: string;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export type FollowEventKind = "started" | "ended";
|
|
626
|
+
|
|
627
|
+
export interface FollowGraphEvent {
|
|
628
|
+
eventAt: string;
|
|
629
|
+
direction: FollowDirection;
|
|
630
|
+
kind: FollowEventKind;
|
|
631
|
+
snapshotId: string;
|
|
632
|
+
profile: FollowGraphProfile;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export interface FollowGraphSummary {
|
|
636
|
+
accountId: string;
|
|
637
|
+
followers: number;
|
|
638
|
+
following: number;
|
|
639
|
+
mutuals: number;
|
|
640
|
+
nonMutualFollowing: number;
|
|
641
|
+
lastCompleteSnapshots: Partial<Record<FollowDirection, string>>;
|
|
642
|
+
lastIncompleteSnapshots: Partial<Record<FollowDirection, string>>;
|
|
643
|
+
}
|