birdclaw 0.1.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 +32 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/bin/birdclaw.mjs +28 -0
- package/package.json +100 -0
- package/playwright.config.ts +31 -0
- package/public/favicon.ico +0 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +25 -0
- package/public/robots.txt +3 -0
- package/src/cli-moderation.ts +210 -0
- package/src/cli.ts +450 -0
- package/src/components/AppNav.tsx +49 -0
- package/src/components/AvatarChip.tsx +47 -0
- package/src/components/DmWorkspace.tsx +246 -0
- package/src/components/EmbeddedTweetCard.tsx +44 -0
- package/src/components/InboxCard.tsx +136 -0
- package/src/components/ProfilePreview.tsx +55 -0
- package/src/components/ThemeSlider.tsx +124 -0
- package/src/components/TimelineCard.tsx +118 -0
- package/src/components/TweetMediaGrid.tsx +39 -0
- package/src/components/TweetRichText.tsx +85 -0
- package/src/lib/actions-transport.ts +173 -0
- package/src/lib/archive-finder.ts +128 -0
- package/src/lib/archive-import.ts +736 -0
- package/src/lib/avatar-cache.ts +184 -0
- package/src/lib/bird-actions.ts +200 -0
- package/src/lib/bird.ts +183 -0
- package/src/lib/blocklist.ts +119 -0
- package/src/lib/blocks-write.ts +118 -0
- package/src/lib/blocks.ts +326 -0
- package/src/lib/config.ts +152 -0
- package/src/lib/db.ts +326 -0
- package/src/lib/dms-live.ts +279 -0
- package/src/lib/inbox.ts +210 -0
- package/src/lib/mentions-export.ts +147 -0
- package/src/lib/mentions-live.ts +475 -0
- package/src/lib/moderation-target.ts +171 -0
- package/src/lib/moderation-write.ts +72 -0
- package/src/lib/mutes-write.ts +118 -0
- package/src/lib/mutes.ts +77 -0
- package/src/lib/openai.ts +86 -0
- package/src/lib/present.ts +20 -0
- package/src/lib/profile-hydration.ts +144 -0
- package/src/lib/profile-replies.ts +60 -0
- package/src/lib/queries.ts +725 -0
- package/src/lib/seed.ts +486 -0
- package/src/lib/sync-cache.ts +65 -0
- package/src/lib/theme-transition.ts +117 -0
- package/src/lib/theme.tsx +157 -0
- package/src/lib/tweet-render.ts +115 -0
- package/src/lib/types.ts +316 -0
- package/src/lib/ui.ts +270 -0
- package/src/lib/x-profile.ts +203 -0
- package/src/lib/x-web.ts +168 -0
- package/src/lib/xurl.ts +492 -0
- package/src/routeTree.gen.ts +282 -0
- package/src/router.tsx +20 -0
- package/src/routes/__root.tsx +64 -0
- package/src/routes/api/action.tsx +88 -0
- package/src/routes/api/avatar.tsx +41 -0
- package/src/routes/api/blocks.tsx +32 -0
- package/src/routes/api/inbox.tsx +35 -0
- package/src/routes/api/query.tsx +72 -0
- package/src/routes/api/status.tsx +15 -0
- package/src/routes/blocks.tsx +352 -0
- package/src/routes/dms.tsx +262 -0
- package/src/routes/inbox.tsx +201 -0
- package/src/routes/index.tsx +125 -0
- package/src/routes/mentions.tsx +125 -0
- package/src/styles.css +109 -0
- package/tsconfig.json +30 -0
- package/vite.config.ts +23 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
export type ThemeValue = "system" | "light" | "dark";
|
|
11
|
+
export type ResolvedTheme = "light" | "dark";
|
|
12
|
+
|
|
13
|
+
const THEME_STORAGE_KEY = "birdclaw-theme";
|
|
14
|
+
|
|
15
|
+
interface ThemeContextValue {
|
|
16
|
+
isReady: boolean;
|
|
17
|
+
theme: ThemeValue;
|
|
18
|
+
resolvedTheme: ResolvedTheme;
|
|
19
|
+
setTheme: (theme: ThemeValue) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
23
|
+
|
|
24
|
+
function getSystemTheme(): ResolvedTheme {
|
|
25
|
+
if (
|
|
26
|
+
typeof window === "undefined" ||
|
|
27
|
+
typeof window.matchMedia !== "function"
|
|
28
|
+
) {
|
|
29
|
+
return "light";
|
|
30
|
+
}
|
|
31
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
32
|
+
? "dark"
|
|
33
|
+
: "light";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveTheme(theme: ThemeValue): ResolvedTheme {
|
|
37
|
+
return theme === "system" ? getSystemTheme() : theme;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function applyThemeToDocument(theme: ThemeValue) {
|
|
41
|
+
if (typeof document === "undefined") return;
|
|
42
|
+
|
|
43
|
+
const resolvedTheme = resolveTheme(theme);
|
|
44
|
+
document.documentElement.dataset.themePreference = theme;
|
|
45
|
+
document.documentElement.dataset.theme = resolvedTheme;
|
|
46
|
+
document.documentElement.style.colorScheme = resolvedTheme;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readInitialTheme(): ThemeValue {
|
|
50
|
+
if (typeof document !== "undefined") {
|
|
51
|
+
const value = document.documentElement.dataset.themePreference;
|
|
52
|
+
if (value === "light" || value === "dark" || value === "system") {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof window !== "undefined") {
|
|
58
|
+
const stored = window.localStorage.getItem(THEME_STORAGE_KEY);
|
|
59
|
+
if (stored === "light" || stored === "dark" || stored === "system") {
|
|
60
|
+
return stored;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return "system";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const themeScript = `
|
|
68
|
+
(() => {
|
|
69
|
+
const storageKey = "${THEME_STORAGE_KEY}";
|
|
70
|
+
const root = document.documentElement;
|
|
71
|
+
const stored = window.localStorage.getItem(storageKey);
|
|
72
|
+
const preference =
|
|
73
|
+
stored === "light" || stored === "dark" || stored === "system"
|
|
74
|
+
? stored
|
|
75
|
+
: "system";
|
|
76
|
+
const resolved =
|
|
77
|
+
preference === "system"
|
|
78
|
+
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
79
|
+
? "dark"
|
|
80
|
+
: "light"
|
|
81
|
+
: preference;
|
|
82
|
+
root.dataset.themePreference = preference;
|
|
83
|
+
root.dataset.theme = resolved;
|
|
84
|
+
root.style.colorScheme = resolved;
|
|
85
|
+
})();
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
89
|
+
const [theme, setThemeState] = useState<ThemeValue>("system");
|
|
90
|
+
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>("light");
|
|
91
|
+
const [isReady, setIsReady] = useState(false);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const initialTheme = readInitialTheme();
|
|
95
|
+
setThemeState(initialTheme);
|
|
96
|
+
setResolvedTheme(resolveTheme(initialTheme));
|
|
97
|
+
applyThemeToDocument(initialTheme);
|
|
98
|
+
setIsReady(true);
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!isReady) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
applyThemeToDocument(theme);
|
|
107
|
+
setResolvedTheme(resolveTheme(theme));
|
|
108
|
+
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
|
|
109
|
+
|
|
110
|
+
if (theme !== "system" || typeof window.matchMedia !== "function") {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
115
|
+
const onChange = () => {
|
|
116
|
+
const nextResolvedTheme = media.matches ? "dark" : "light";
|
|
117
|
+
setResolvedTheme(nextResolvedTheme);
|
|
118
|
+
document.documentElement.dataset.theme = nextResolvedTheme;
|
|
119
|
+
document.documentElement.style.colorScheme = nextResolvedTheme;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if ("addEventListener" in media) {
|
|
123
|
+
media.addEventListener("change", onChange);
|
|
124
|
+
} else {
|
|
125
|
+
media.addListener(onChange);
|
|
126
|
+
}
|
|
127
|
+
return () => {
|
|
128
|
+
if ("removeEventListener" in media) {
|
|
129
|
+
media.removeEventListener("change", onChange);
|
|
130
|
+
} else {
|
|
131
|
+
media.removeListener(onChange);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}, [isReady, theme]);
|
|
135
|
+
|
|
136
|
+
const value = useMemo<ThemeContextValue>(
|
|
137
|
+
() => ({
|
|
138
|
+
isReady,
|
|
139
|
+
theme,
|
|
140
|
+
resolvedTheme,
|
|
141
|
+
setTheme: setThemeState,
|
|
142
|
+
}),
|
|
143
|
+
[isReady, resolvedTheme, theme],
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function useTheme() {
|
|
152
|
+
const context = useContext(ThemeContext);
|
|
153
|
+
if (!context) {
|
|
154
|
+
throw new Error("useTheme must be used within ThemeProvider");
|
|
155
|
+
}
|
|
156
|
+
return context;
|
|
157
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TweetEntities,
|
|
3
|
+
TweetHashtagEntity,
|
|
4
|
+
TweetMentionEntity,
|
|
5
|
+
TweetUrlEntity,
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
type TweetSegment =
|
|
9
|
+
| ({ kind: "mention" } & TweetMentionEntity)
|
|
10
|
+
| ({ kind: "url" } & TweetUrlEntity)
|
|
11
|
+
| ({ kind: "hashtag" } & TweetHashtagEntity);
|
|
12
|
+
|
|
13
|
+
const MARKDOWN_ESCAPE_CHARACTERS = new Set([
|
|
14
|
+
"\\",
|
|
15
|
+
"`",
|
|
16
|
+
"*",
|
|
17
|
+
"_",
|
|
18
|
+
"{",
|
|
19
|
+
"}",
|
|
20
|
+
"[",
|
|
21
|
+
"]",
|
|
22
|
+
"(",
|
|
23
|
+
")",
|
|
24
|
+
"#",
|
|
25
|
+
"+",
|
|
26
|
+
".",
|
|
27
|
+
"!",
|
|
28
|
+
"|",
|
|
29
|
+
">",
|
|
30
|
+
"-",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function escapeMarkdown(text: string) {
|
|
34
|
+
return [...text]
|
|
35
|
+
.map((character) =>
|
|
36
|
+
MARKDOWN_ESCAPE_CHARACTERS.has(character) ? `\\${character}` : character,
|
|
37
|
+
)
|
|
38
|
+
.join("");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function collectTweetSegments(entities: TweetEntities): TweetSegment[] {
|
|
42
|
+
return [
|
|
43
|
+
...(entities.mentions?.map((entry) => ({
|
|
44
|
+
...entry,
|
|
45
|
+
kind: "mention" as const,
|
|
46
|
+
})) ?? []),
|
|
47
|
+
...(entities.urls?.map((entry) => ({ ...entry, kind: "url" as const })) ??
|
|
48
|
+
[]),
|
|
49
|
+
...(entities.hashtags?.map((entry) => ({
|
|
50
|
+
...entry,
|
|
51
|
+
kind: "hashtag" as const,
|
|
52
|
+
})) ?? []),
|
|
53
|
+
].sort((left, right) => left.start - right.start);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderTweetText(
|
|
57
|
+
text: string,
|
|
58
|
+
entities: TweetEntities,
|
|
59
|
+
renderSegment: (segment: TweetSegment, fallback: string) => string,
|
|
60
|
+
) {
|
|
61
|
+
const segments = collectTweetSegments(entities);
|
|
62
|
+
let cursor = 0;
|
|
63
|
+
let output = "";
|
|
64
|
+
|
|
65
|
+
for (const segment of segments) {
|
|
66
|
+
if (
|
|
67
|
+
segment.start < cursor ||
|
|
68
|
+
segment.end <= segment.start ||
|
|
69
|
+
segment.end > text.length
|
|
70
|
+
) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
output += text.slice(cursor, segment.start);
|
|
75
|
+
const fallback = text.slice(segment.start, segment.end);
|
|
76
|
+
output += renderSegment(segment, fallback);
|
|
77
|
+
cursor = segment.end;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
output += text.slice(cursor);
|
|
81
|
+
return output;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function renderTweetPlainText(text: string, entities: TweetEntities) {
|
|
85
|
+
return renderTweetText(text, entities, (segment, fallback) => {
|
|
86
|
+
if (segment.kind === "url") {
|
|
87
|
+
return segment.expandedUrl;
|
|
88
|
+
}
|
|
89
|
+
if (segment.kind === "mention") {
|
|
90
|
+
return `@${segment.username}`;
|
|
91
|
+
}
|
|
92
|
+
if (segment.kind === "hashtag") {
|
|
93
|
+
return `#${segment.tag}`;
|
|
94
|
+
}
|
|
95
|
+
return fallback;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function renderTweetMarkdown(text: string, entities: TweetEntities) {
|
|
100
|
+
return renderTweetText(text, entities, (segment, fallback) => {
|
|
101
|
+
if (segment.kind === "url") {
|
|
102
|
+
return `[${escapeMarkdown(segment.displayUrl)}](${segment.expandedUrl})`;
|
|
103
|
+
}
|
|
104
|
+
if (segment.kind === "mention") {
|
|
105
|
+
const label = `@${segment.username}`;
|
|
106
|
+
return segment.profile
|
|
107
|
+
? `[${escapeMarkdown(label)}](https://x.com/${segment.username})`
|
|
108
|
+
: escapeMarkdown(label);
|
|
109
|
+
}
|
|
110
|
+
if (segment.kind === "hashtag") {
|
|
111
|
+
return escapeMarkdown(`#${segment.tag}`);
|
|
112
|
+
}
|
|
113
|
+
return escapeMarkdown(fallback);
|
|
114
|
+
});
|
|
115
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
export type ResourceKind = "home" | "mentions" | "dms";
|
|
2
|
+
export type InboxKind = "mixed" | "mentions" | "dms";
|
|
3
|
+
|
|
4
|
+
export type ReplyFilter = "all" | "replied" | "unreplied";
|
|
5
|
+
|
|
6
|
+
export interface AccountRecord {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
handle: string;
|
|
10
|
+
transport: string;
|
|
11
|
+
isDefault: number;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ProfileRecord {
|
|
16
|
+
id: string;
|
|
17
|
+
handle: string;
|
|
18
|
+
displayName: string;
|
|
19
|
+
bio: string;
|
|
20
|
+
followersCount: number;
|
|
21
|
+
avatarHue: number;
|
|
22
|
+
avatarUrl?: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TweetMentionEntity {
|
|
27
|
+
username: string;
|
|
28
|
+
id?: string;
|
|
29
|
+
start: number;
|
|
30
|
+
end: number;
|
|
31
|
+
profile?: ProfileRecord;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TweetUrlEntity {
|
|
35
|
+
url: string;
|
|
36
|
+
expandedUrl: string;
|
|
37
|
+
displayUrl: string;
|
|
38
|
+
start: number;
|
|
39
|
+
end: number;
|
|
40
|
+
title?: string;
|
|
41
|
+
description?: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TweetHashtagEntity {
|
|
45
|
+
tag: string;
|
|
46
|
+
start: number;
|
|
47
|
+
end: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TweetEntities {
|
|
51
|
+
mentions?: TweetMentionEntity[];
|
|
52
|
+
urls?: TweetUrlEntity[];
|
|
53
|
+
hashtags?: TweetHashtagEntity[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TweetMediaItem {
|
|
57
|
+
url: string;
|
|
58
|
+
type: "image" | "video" | "gif" | "unknown";
|
|
59
|
+
altText?: string;
|
|
60
|
+
width?: number;
|
|
61
|
+
height?: number;
|
|
62
|
+
thumbnailUrl?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface EmbeddedTweet {
|
|
66
|
+
id: string;
|
|
67
|
+
text: string;
|
|
68
|
+
createdAt: string;
|
|
69
|
+
author: ProfileRecord;
|
|
70
|
+
entities: TweetEntities;
|
|
71
|
+
media: TweetMediaItem[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface BlockItem {
|
|
75
|
+
accountId: string;
|
|
76
|
+
accountHandle: string;
|
|
77
|
+
source: string;
|
|
78
|
+
blockedAt: string;
|
|
79
|
+
profile: ProfileRecord;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface BlockSearchItem {
|
|
83
|
+
profile: ProfileRecord;
|
|
84
|
+
isBlocked: boolean;
|
|
85
|
+
blockedAt?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface BlockListResponse {
|
|
89
|
+
items: BlockItem[];
|
|
90
|
+
matches: BlockSearchItem[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface TimelineItem {
|
|
94
|
+
id: string;
|
|
95
|
+
accountId: string;
|
|
96
|
+
accountHandle: string;
|
|
97
|
+
kind: Exclude<ResourceKind, "dms">;
|
|
98
|
+
text: string;
|
|
99
|
+
createdAt: string;
|
|
100
|
+
isReplied: boolean;
|
|
101
|
+
likeCount: number;
|
|
102
|
+
mediaCount: number;
|
|
103
|
+
bookmarked: boolean;
|
|
104
|
+
liked: boolean;
|
|
105
|
+
author: ProfileRecord;
|
|
106
|
+
entities: TweetEntities;
|
|
107
|
+
media: TweetMediaItem[];
|
|
108
|
+
replyToTweet?: EmbeddedTweet | null;
|
|
109
|
+
quotedTweet?: EmbeddedTweet | null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface DmMessageItem {
|
|
113
|
+
id: string;
|
|
114
|
+
conversationId: string;
|
|
115
|
+
text: string;
|
|
116
|
+
createdAt: string;
|
|
117
|
+
direction: "inbound" | "outbound";
|
|
118
|
+
isReplied: boolean;
|
|
119
|
+
mediaCount: number;
|
|
120
|
+
sender: ProfileRecord;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface DmConversationItem {
|
|
124
|
+
id: string;
|
|
125
|
+
accountId: string;
|
|
126
|
+
accountHandle: string;
|
|
127
|
+
title: string;
|
|
128
|
+
lastMessageAt: string;
|
|
129
|
+
lastMessagePreview: string;
|
|
130
|
+
unreadCount: number;
|
|
131
|
+
needsReply: boolean;
|
|
132
|
+
influenceScore: number;
|
|
133
|
+
influenceLabel: string;
|
|
134
|
+
participant: ProfileRecord;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface TimelineQuery {
|
|
138
|
+
resource: Exclude<ResourceKind, "dms">;
|
|
139
|
+
account?: string;
|
|
140
|
+
search?: string;
|
|
141
|
+
replyFilter?: ReplyFilter;
|
|
142
|
+
limit?: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface DmQuery {
|
|
146
|
+
account?: string;
|
|
147
|
+
participant?: string;
|
|
148
|
+
search?: string;
|
|
149
|
+
replyFilter?: ReplyFilter;
|
|
150
|
+
minFollowers?: number;
|
|
151
|
+
maxFollowers?: number;
|
|
152
|
+
minInfluenceScore?: number;
|
|
153
|
+
maxInfluenceScore?: number;
|
|
154
|
+
sort?: "recent" | "influence";
|
|
155
|
+
limit?: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface TransportStatus {
|
|
159
|
+
installed: boolean;
|
|
160
|
+
availableTransport: "xurl" | "local";
|
|
161
|
+
statusText: string;
|
|
162
|
+
rawStatus?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export type ModerationAction = "block" | "unblock" | "mute" | "unmute";
|
|
166
|
+
export type ModerationTransportKind = "bird" | "xurl";
|
|
167
|
+
|
|
168
|
+
export interface ModerationActionTransportResult {
|
|
169
|
+
ok: boolean;
|
|
170
|
+
output: string;
|
|
171
|
+
transport: ModerationTransportKind;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface ArchiveCandidate {
|
|
175
|
+
path: string;
|
|
176
|
+
name: string;
|
|
177
|
+
size: number;
|
|
178
|
+
sizeFormatted: string;
|
|
179
|
+
modifiedTime: string;
|
|
180
|
+
dateFormatted: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface QueryEnvelope {
|
|
184
|
+
accounts: AccountRecord[];
|
|
185
|
+
archives: ArchiveCandidate[];
|
|
186
|
+
transport: TransportStatus;
|
|
187
|
+
stats: {
|
|
188
|
+
home: number;
|
|
189
|
+
mentions: number;
|
|
190
|
+
dms: number;
|
|
191
|
+
needsReply: number;
|
|
192
|
+
inbox: number;
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface QueryResponse {
|
|
197
|
+
resource: ResourceKind;
|
|
198
|
+
items: TimelineItem[] | DmConversationItem[];
|
|
199
|
+
selectedConversation?: {
|
|
200
|
+
conversation: DmConversationItem;
|
|
201
|
+
messages: DmMessageItem[];
|
|
202
|
+
} | null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface InboxItem {
|
|
206
|
+
id: string;
|
|
207
|
+
entityId: string;
|
|
208
|
+
entityKind: "mention" | "dm";
|
|
209
|
+
accountId: string;
|
|
210
|
+
accountHandle: string;
|
|
211
|
+
title: string;
|
|
212
|
+
text: string;
|
|
213
|
+
createdAt: string;
|
|
214
|
+
needsReply: boolean;
|
|
215
|
+
influenceScore: number;
|
|
216
|
+
participant: ProfileRecord;
|
|
217
|
+
source: "heuristic" | "openai";
|
|
218
|
+
score: number;
|
|
219
|
+
summary: string;
|
|
220
|
+
reasoning: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface InboxQuery {
|
|
224
|
+
kind?: InboxKind;
|
|
225
|
+
minScore?: number;
|
|
226
|
+
hideLowSignal?: boolean;
|
|
227
|
+
limit?: number;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface InboxResponse {
|
|
231
|
+
items: InboxItem[];
|
|
232
|
+
stats: {
|
|
233
|
+
total: number;
|
|
234
|
+
openai: number;
|
|
235
|
+
heuristic: number;
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface XurlPublicMetrics {
|
|
240
|
+
retweet_count?: number;
|
|
241
|
+
reply_count?: number;
|
|
242
|
+
like_count?: number;
|
|
243
|
+
quote_count?: number;
|
|
244
|
+
bookmark_count?: number;
|
|
245
|
+
impression_count?: number;
|
|
246
|
+
followers_count?: number;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface XurlMentionUser {
|
|
250
|
+
id: string;
|
|
251
|
+
name: string;
|
|
252
|
+
username: string;
|
|
253
|
+
description?: string;
|
|
254
|
+
profile_image_url?: string;
|
|
255
|
+
public_metrics?: XurlPublicMetrics;
|
|
256
|
+
created_at?: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface XurlMentionData {
|
|
260
|
+
id: string;
|
|
261
|
+
author_id: string;
|
|
262
|
+
text: string;
|
|
263
|
+
created_at: string;
|
|
264
|
+
conversation_id?: string;
|
|
265
|
+
entities?: Record<string, unknown>;
|
|
266
|
+
public_metrics?: XurlPublicMetrics;
|
|
267
|
+
edit_history_tweet_ids?: string[];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface XurlReferencedTweet {
|
|
271
|
+
type: string;
|
|
272
|
+
id: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface XurlUserTweet {
|
|
276
|
+
id: string;
|
|
277
|
+
text: string;
|
|
278
|
+
created_at: string;
|
|
279
|
+
conversation_id?: string;
|
|
280
|
+
referenced_tweets?: XurlReferencedTweet[];
|
|
281
|
+
public_metrics?: XurlPublicMetrics;
|
|
282
|
+
edit_history_tweet_ids?: string[];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface ProfileReplyItem {
|
|
286
|
+
id: string;
|
|
287
|
+
text: string;
|
|
288
|
+
createdAt: string;
|
|
289
|
+
conversationId?: string;
|
|
290
|
+
replyToTweetId?: string;
|
|
291
|
+
likeCount: number;
|
|
292
|
+
replyCount: number;
|
|
293
|
+
retweetCount: number;
|
|
294
|
+
quoteCount: number;
|
|
295
|
+
bookmarkCount: number;
|
|
296
|
+
impressionCount: number;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export interface ProfileRepliesResponse {
|
|
300
|
+
profile: ProfileRecord;
|
|
301
|
+
externalUserId: string;
|
|
302
|
+
items: ProfileReplyItem[];
|
|
303
|
+
meta: {
|
|
304
|
+
scannedCount: number;
|
|
305
|
+
returnedCount: number;
|
|
306
|
+
nextToken: string | null;
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export interface XurlMentionsResponse {
|
|
311
|
+
data: XurlMentionData[];
|
|
312
|
+
includes?: {
|
|
313
|
+
users?: XurlMentionUser[];
|
|
314
|
+
};
|
|
315
|
+
meta?: Record<string, unknown>;
|
|
316
|
+
}
|