feat: include raw tweet data in mapping

This commit is contained in:
Peter Steinberger
2026-01-05 00:30:01 +01:00
parent 3728f7a4f5
commit c4fa04e0b2
3 changed files with 83 additions and 5 deletions
+2
View File
@@ -227,6 +227,8 @@ export interface TweetData {
quotedTweet?: TweetData; quotedTweet?: TweetData;
// Media attachments (photos, videos, GIFs) // Media attachments (photos, videos, GIFs)
media?: TweetMedia[]; media?: TweetMedia[];
// Raw GraphQL tweet result (only when includeRaw is enabled)
_raw?: GraphqlTweetResult;
} }
export interface GetTweetResult { export interface GetTweetResult {
+32 -5
View File
@@ -224,7 +224,19 @@ export function unwrapTweetResult(result: GraphqlTweetResult | undefined): Graph
return result; return result;
} }
export function mapTweetResult(result: GraphqlTweetResult | undefined, quoteDepth: number): TweetData | undefined { export interface MapTweetResultOptions {
quoteDepth: number;
includeRaw?: boolean;
}
export function mapTweetResult(
result: GraphqlTweetResult | undefined,
quoteDepthOrOptions: number | MapTweetResultOptions,
): TweetData | undefined {
const options: MapTweetResultOptions =
typeof quoteDepthOrOptions === 'number' ? { quoteDepth: quoteDepthOrOptions } : quoteDepthOrOptions;
const { quoteDepth, includeRaw = false } = options;
const userResult = result?.core?.user_results?.result; const userResult = result?.core?.user_results?.result;
const userLegacy = userResult?.legacy; const userLegacy = userResult?.legacy;
const userCore = userResult?.core; const userCore = userResult?.core;
@@ -244,13 +256,13 @@ export function mapTweetResult(result: GraphqlTweetResult | undefined, quoteDept
if (quoteDepth > 0) { if (quoteDepth > 0) {
const quotedResult = unwrapTweetResult(result.quoted_status_result?.result); const quotedResult = unwrapTweetResult(result.quoted_status_result?.result);
if (quotedResult) { if (quotedResult) {
quotedTweet = mapTweetResult(quotedResult, quoteDepth - 1); quotedTweet = mapTweetResult(quotedResult, { quoteDepth: quoteDepth - 1, includeRaw });
} }
} }
const media = extractMedia(result); const media = extractMedia(result);
return { const tweetData: TweetData = {
id: result.rest_id, id: result.rest_id,
text, text,
createdAt: result.legacy?.created_at, createdAt: result.legacy?.created_at,
@@ -267,6 +279,12 @@ export function mapTweetResult(result: GraphqlTweetResult | undefined, quoteDept
quotedTweet, quotedTweet,
media, media,
}; };
if (includeRaw) {
(tweetData as TweetData & { _raw: GraphqlTweetResult })._raw = result;
}
return tweetData;
} }
export function findTweetInInstructions( export function findTweetInInstructions(
@@ -358,6 +376,11 @@ export function collectTweetResultsFromEntry(entry: {
return results; return results;
} }
export interface ParseTweetsOptions {
quoteDepth: number;
includeRaw?: boolean;
}
export function parseTweetsFromInstructions( export function parseTweetsFromInstructions(
instructions: instructions:
| Array<{ | Array<{
@@ -400,8 +423,12 @@ export function parseTweetsFromInstructions(
}>; }>;
}> }>
| undefined, | undefined,
quoteDepth: number, quoteDepthOrOptions: number | ParseTweetsOptions,
): TweetData[] { ): TweetData[] {
const options: ParseTweetsOptions =
typeof quoteDepthOrOptions === 'number' ? { quoteDepth: quoteDepthOrOptions } : quoteDepthOrOptions;
const { quoteDepth, includeRaw = false } = options;
const tweets: TweetData[] = []; const tweets: TweetData[] = [];
const seen = new Set<string>(); const seen = new Set<string>();
@@ -409,7 +436,7 @@ export function parseTweetsFromInstructions(
for (const entry of instruction.entries ?? []) { for (const entry of instruction.entries ?? []) {
const results = collectTweetResultsFromEntry(entry); const results = collectTweetResultsFromEntry(entry);
for (const result of results) { for (const result of results) {
const mapped = mapTweetResult(result, quoteDepth); const mapped = mapTweetResult(result, { quoteDepth, includeRaw });
if (!mapped || seen.has(mapped.id)) { if (!mapped || seen.has(mapped.id)) {
continue; continue;
} }
+49
View File
@@ -185,6 +185,55 @@ describe('TwitterClient lists', () => {
expect(result.lists?.[0].isPrivate).toBe(true); expect(result.lists?.[0].isPrivate).toBe(true);
}); });
it('handles missing mode', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
data: {
user: {
result: {
timeline: {
timeline: {
instructions: [
{
entries: [
{
content: {
itemContent: {
list: {
id_str: '9999',
name: 'No Mode',
mode: null,
},
},
},
},
],
},
],
},
},
},
},
},
}),
});
const client = new TwitterClient({ cookies: validCookies });
const clientPrivate = client as unknown as TwitterClientPrivate;
clientPrivate.getCurrentUser = async () => ({
success: true,
user: { id: '12345', username: 'testuser', name: 'Test User' },
});
clientPrivate.getListOwnershipsQueryIds = async () => ['test'];
const result = await client.getOwnedLists(100);
expect(result.success).toBe(true);
expect(result.lists?.[0].isPrivate).toBe(false);
});
it('returns empty array when no lists exist', async () => { it('returns empty array when no lists exist', async () => {
mockFetch.mockResolvedValueOnce({ mockFetch.mockResolvedValueOnce({
ok: true, ok: true,