Skip to content

Commit 16aae82

Browse files
committed
feat(grok): update API fetch logic and response handling; improve pagination and data normalization
1 parent 9689686 commit 16aae82

File tree

2 files changed

+191
-91
lines changed

2 files changed

+191
-91
lines changed

src/extractors/grok.ts

Lines changed: 92 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,23 @@
11
/* ──────────────────────────────────────────────
2-
* Extractor – Grok (grok.com / x.com/i/grok)
2+
* Extractor – Grok (grok.com)
33
*
4-
* Uses X/Twitter infrastructure.
5-
* Requires x-csrf-token from ct0 cookie.
4+
* Endpoint: /rest/app-chat/conversations
5+
* Auth: cookie-based (sso + sso-rw). No CSRF header required.
6+
* Pagination: pageSize + cursor (X/Twitter API style).
67
* ────────────────────────────────────────────── */
78

89
import type { Extractor } from "./base";
910
import type { Message, Thread } from "../types/schema";
1011
import { uuid, jitteredDelay } from "../utils/helpers";
1112

12-
const BASE = ""; // Rely on relative path since we inject into either grok.com or x.com
13-
14-
// In browser context content-scripts, getting cookies dynamically is harder via chrome.cookies unless it's sent from BG.
15-
// However, since we are doing a same-origin fetch from the content-script,
16-
// the browser handles `credentials` and cookies automatically for most cases.
17-
// We still need the CSRF/ct0 header if required.
18-
function getCsrfFromDocument(): string {
19-
const match = document.cookie.match(/(?:^|;\s*)ct0=([^;]*)/);
20-
if (!match) throw new Error("Grok: ct0 CSRF cookie not found in document.cookie");
21-
return match[1];
22-
}
13+
const BASE = "";
14+
const PAGE_SIZE = 60;
2315

2416
async function apiFetch<T>(path: string): Promise<T> {
25-
const csrf = getCsrfFromDocument();
2617
const res = await fetch(`${BASE}${path}`, {
18+
credentials: "include",
2719
headers: {
2820
"Content-Type": "application/json",
29-
"x-csrf-token": csrf,
3021
},
3122
});
3223
if (!res.ok) {
@@ -40,85 +31,132 @@ async function apiFetch<T>(path: string): Promise<T> {
4031
/* ── raw response shapes ──────────────────────────────────── */
4132

4233
interface RawConvListItem {
43-
id: string;
34+
conversationId: string;
4435
title?: string;
45-
created_at: string;
46-
updated_at: string;
36+
createTime?: string;
37+
modifyTime?: string;
38+
// fallback snake_case (older API versions)
39+
id?: string;
40+
created_at?: string;
41+
updated_at?: string;
4742
}
4843

44+
/** The list endpoint returns either a plain array or a wrapped object. */
45+
type RawConvListResponse =
46+
| RawConvListItem[]
47+
| { conversations: RawConvListItem[]; nextCursor?: string; cursor?: string };
48+
4949
interface RawGrokMessage {
50-
id: string;
51-
role: "user" | "assistant";
52-
text: string;
53-
created_at: string;
50+
id?: string;
51+
messageId?: string;
52+
sender?: "human" | "assistant";
53+
role?: "user" | "assistant";
54+
message?: string;
55+
text?: string;
56+
createTime?: string;
57+
created_at?: string;
5458
model?: string;
5559
}
5660

5761
interface RawConversation {
58-
id: string;
62+
conversationId?: string;
63+
id?: string;
5964
title?: string;
60-
created_at: string;
61-
updated_at: string;
62-
messages: RawGrokMessage[];
65+
createTime?: string;
66+
modifyTime?: string;
67+
created_at?: string;
68+
updated_at?: string;
69+
responses?: RawGrokMessage[];
70+
messages?: RawGrokMessage[];
6371
}
6472

6573
/* ── helpers ──────────────────────────────────────────────── */
6674

75+
function normaliseItem(item: RawConvListItem): { providerId: string; title: string; createdAt: string; updatedAt: string } {
76+
return {
77+
providerId: item.conversationId ?? item.id ?? "",
78+
title: item.title || "Untitled",
79+
createdAt: item.createTime ?? item.created_at ?? "",
80+
updatedAt: item.modifyTime ?? item.updated_at ?? item.createTime ?? item.created_at ?? "",
81+
};
82+
}
83+
6784
function mapMessages(raw: RawGrokMessage[]): Message[] {
68-
return raw.map((m) => ({
69-
id: m.id,
70-
role: m.role,
71-
content: m.text,
72-
createdAt: m.created_at,
73-
model: m.model,
74-
}));
85+
return raw.map((m) => {
86+
const role: "user" | "assistant" =
87+
m.role === "user" || m.sender === "human" ? "user" : "assistant";
88+
return {
89+
id: m.messageId ?? m.id ?? uuid(),
90+
role,
91+
content: m.message ?? m.text ?? "",
92+
createdAt: m.createTime ?? m.created_at ?? "",
93+
model: m.model,
94+
};
95+
});
7596
}
7697

7798
/* ── Extractor implementation ─────────────────────────────── */
7899

79100
export const grokExtractor: Extractor = {
80101
async listConversations() {
81102
const all: { providerId: string; title: string; createdAt: string; updatedAt: string }[] = [];
82-
let offset = 0;
83-
const limit = 50;
103+
let cursor: string | undefined;
84104

85105
// eslint-disable-next-line no-constant-condition
86106
while (true) {
87-
const list = await apiFetch<RawConvListItem[]>(
88-
`/rest/api/conversations?limit=${limit}&offset=${offset}`
107+
const qs = cursor
108+
? `pageSize=${PAGE_SIZE}&cursor=${encodeURIComponent(cursor)}`
109+
: `pageSize=${PAGE_SIZE}`;
110+
111+
const raw = await apiFetch<RawConvListResponse>(
112+
`/rest/app-chat/conversations?${qs}`
89113
);
90-
if (!list.length) break;
91-
92-
for (const item of list) {
93-
all.push({
94-
providerId: item.id,
95-
title: item.title || "Untitled",
96-
createdAt: item.created_at,
97-
updatedAt: item.updated_at,
98-
});
114+
115+
let items: RawConvListItem[];
116+
let nextCursor: string | undefined;
117+
118+
if (Array.isArray(raw)) {
119+
items = raw;
120+
} else {
121+
items = raw.conversations ?? [];
122+
nextCursor = raw.nextCursor ?? raw.cursor;
123+
}
124+
125+
if (!items.length) break;
126+
127+
for (const item of items) {
128+
all.push(normaliseItem(item));
99129
}
100130

101-
offset += limit;
102-
if (list.length < limit) break;
103-
await jitteredDelay(500, 1200);
131+
if (nextCursor) {
132+
cursor = nextCursor;
133+
await jitteredDelay(500, 1200);
134+
} else if (items.length < PAGE_SIZE) {
135+
break;
136+
} else {
137+
// No cursor returned but page was full — stop to avoid infinite loop.
138+
break;
139+
}
104140
}
105141

106142
return all;
107143
},
108144

109145
async fetchThread(conversationId: string): Promise<Thread> {
110146
const raw = await apiFetch<RawConversation>(
111-
`/rest/api/conversations/${conversationId}`
147+
`/rest/app-chat/conversations/${conversationId}`
112148
);
113149

150+
const messages = mapMessages(raw.responses ?? raw.messages ?? []);
151+
114152
return {
115153
id: uuid(),
116154
providerId: conversationId,
117155
provider: "grok",
118156
title: raw.title || "Untitled",
119-
createdAt: raw.created_at,
120-
updatedAt: raw.updated_at,
121-
messages: mapMessages(raw.messages ?? []),
157+
createdAt: raw.createTime ?? raw.created_at ?? "",
158+
updatedAt: raw.modifyTime ?? raw.updated_at ?? raw.createTime ?? raw.created_at ?? "",
159+
messages,
122160
tags: [],
123161
};
124162
},

0 commit comments

Comments
 (0)