@antcoder/birdxtwittercli 0.8.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.
Files changed (214) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/LICENSE +21 -0
  3. package/README.md +388 -0
  4. package/dist/cli/pagination.d.ts +35 -0
  5. package/dist/cli/pagination.d.ts.map +1 -0
  6. package/dist/cli/pagination.js +43 -0
  7. package/dist/cli/pagination.js.map +1 -0
  8. package/dist/cli/program.d.ts +5 -0
  9. package/dist/cli/program.d.ts.map +1 -0
  10. package/dist/cli/program.js +113 -0
  11. package/dist/cli/program.js.map +1 -0
  12. package/dist/cli/shared.d.ts +77 -0
  13. package/dist/cli/shared.d.ts.map +1 -0
  14. package/dist/cli/shared.js +327 -0
  15. package/dist/cli/shared.js.map +1 -0
  16. package/dist/cli.d.ts +12 -0
  17. package/dist/cli.d.ts.map +1 -0
  18. package/dist/cli.js +29 -0
  19. package/dist/cli.js.map +1 -0
  20. package/dist/commands/bookmarks.d.ts +4 -0
  21. package/dist/commands/bookmarks.d.ts.map +1 -0
  22. package/dist/commands/bookmarks.js +189 -0
  23. package/dist/commands/bookmarks.js.map +1 -0
  24. package/dist/commands/check.d.ts +4 -0
  25. package/dist/commands/check.d.ts.map +1 -0
  26. package/dist/commands/check.js +43 -0
  27. package/dist/commands/check.js.map +1 -0
  28. package/dist/commands/follow.d.ts +4 -0
  29. package/dist/commands/follow.d.ts.map +1 -0
  30. package/dist/commands/follow.js +91 -0
  31. package/dist/commands/follow.js.map +1 -0
  32. package/dist/commands/help.d.ts +4 -0
  33. package/dist/commands/help.d.ts.map +1 -0
  34. package/dist/commands/help.js +19 -0
  35. package/dist/commands/help.js.map +1 -0
  36. package/dist/commands/home.d.ts +4 -0
  37. package/dist/commands/home.d.ts.map +1 -0
  38. package/dist/commands/home.js +43 -0
  39. package/dist/commands/home.js.map +1 -0
  40. package/dist/commands/lists.d.ts +4 -0
  41. package/dist/commands/lists.d.ts.map +1 -0
  42. package/dist/commands/lists.js +213 -0
  43. package/dist/commands/lists.js.map +1 -0
  44. package/dist/commands/news.d.ts +4 -0
  45. package/dist/commands/news.d.ts.map +1 -0
  46. package/dist/commands/news.js +131 -0
  47. package/dist/commands/news.js.map +1 -0
  48. package/dist/commands/post.d.ts +4 -0
  49. package/dist/commands/post.d.ts.map +1 -0
  50. package/dist/commands/post.js +101 -0
  51. package/dist/commands/post.js.map +1 -0
  52. package/dist/commands/query-ids.d.ts +4 -0
  53. package/dist/commands/query-ids.d.ts.map +1 -0
  54. package/dist/commands/query-ids.js +80 -0
  55. package/dist/commands/query-ids.js.map +1 -0
  56. package/dist/commands/read.d.ts +4 -0
  57. package/dist/commands/read.d.ts.map +1 -0
  58. package/dist/commands/read.js +152 -0
  59. package/dist/commands/read.js.map +1 -0
  60. package/dist/commands/search.d.ts +4 -0
  61. package/dist/commands/search.d.ts.map +1 -0
  62. package/dist/commands/search.js +115 -0
  63. package/dist/commands/search.js.map +1 -0
  64. package/dist/commands/unbookmark.d.ts +4 -0
  65. package/dist/commands/unbookmark.d.ts.map +1 -0
  66. package/dist/commands/unbookmark.js +36 -0
  67. package/dist/commands/unbookmark.js.map +1 -0
  68. package/dist/commands/user-tweets.d.ts +4 -0
  69. package/dist/commands/user-tweets.d.ts.map +1 -0
  70. package/dist/commands/user-tweets.js +109 -0
  71. package/dist/commands/user-tweets.js.map +1 -0
  72. package/dist/commands/users.d.ts +4 -0
  73. package/dist/commands/users.d.ts.map +1 -0
  74. package/dist/commands/users.js +295 -0
  75. package/dist/commands/users.js.map +1 -0
  76. package/dist/index.d.ts +2 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.js +2 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/lib/cli-args.d.ts +7 -0
  81. package/dist/lib/cli-args.d.ts.map +1 -0
  82. package/dist/lib/cli-args.js +25 -0
  83. package/dist/lib/cli-args.js.map +1 -0
  84. package/dist/lib/cookies.d.ts +31 -0
  85. package/dist/lib/cookies.d.ts.map +1 -0
  86. package/dist/lib/cookies.js +173 -0
  87. package/dist/lib/cookies.js.map +1 -0
  88. package/dist/lib/extract-bookmark-folder-id.d.ts +2 -0
  89. package/dist/lib/extract-bookmark-folder-id.d.ts.map +1 -0
  90. package/dist/lib/extract-bookmark-folder-id.js +20 -0
  91. package/dist/lib/extract-bookmark-folder-id.js.map +1 -0
  92. package/dist/lib/extract-list-id.d.ts +2 -0
  93. package/dist/lib/extract-list-id.d.ts.map +1 -0
  94. package/dist/lib/extract-list-id.js +19 -0
  95. package/dist/lib/extract-list-id.js.map +1 -0
  96. package/dist/lib/extract-tweet-id.d.ts +2 -0
  97. package/dist/lib/extract-tweet-id.d.ts.map +1 -0
  98. package/dist/lib/extract-tweet-id.js +14 -0
  99. package/dist/lib/extract-tweet-id.js.map +1 -0
  100. package/dist/lib/features.json +17 -0
  101. package/dist/lib/index.d.ts +10 -0
  102. package/dist/lib/index.d.ts.map +1 -0
  103. package/dist/lib/index.js +4 -0
  104. package/dist/lib/index.js.map +1 -0
  105. package/dist/lib/normalize-handle.d.ts +6 -0
  106. package/dist/lib/normalize-handle.d.ts.map +1 -0
  107. package/dist/lib/normalize-handle.js +31 -0
  108. package/dist/lib/normalize-handle.js.map +1 -0
  109. package/dist/lib/output.d.ts +29 -0
  110. package/dist/lib/output.d.ts.map +1 -0
  111. package/dist/lib/output.js +88 -0
  112. package/dist/lib/output.js.map +1 -0
  113. package/dist/lib/paginate-cursor.d.ts +27 -0
  114. package/dist/lib/paginate-cursor.d.ts.map +1 -0
  115. package/dist/lib/paginate-cursor.js +37 -0
  116. package/dist/lib/paginate-cursor.js.map +1 -0
  117. package/dist/lib/query-ids.json +20 -0
  118. package/dist/lib/runtime-features.d.ts +19 -0
  119. package/dist/lib/runtime-features.d.ts.map +1 -0
  120. package/dist/lib/runtime-features.js +151 -0
  121. package/dist/lib/runtime-features.js.map +1 -0
  122. package/dist/lib/runtime-query-ids.d.ts +33 -0
  123. package/dist/lib/runtime-query-ids.d.ts.map +1 -0
  124. package/dist/lib/runtime-query-ids.js +264 -0
  125. package/dist/lib/runtime-query-ids.js.map +1 -0
  126. package/dist/lib/thread-filters.d.ts +8 -0
  127. package/dist/lib/thread-filters.d.ts.map +1 -0
  128. package/dist/lib/thread-filters.js +124 -0
  129. package/dist/lib/thread-filters.js.map +1 -0
  130. package/dist/lib/twitter-client-base.d.ts +38 -0
  131. package/dist/lib/twitter-client-base.d.ts.map +1 -0
  132. package/dist/lib/twitter-client-base.js +129 -0
  133. package/dist/lib/twitter-client-base.js.map +1 -0
  134. package/dist/lib/twitter-client-bookmarks.d.ts +7 -0
  135. package/dist/lib/twitter-client-bookmarks.d.ts.map +1 -0
  136. package/dist/lib/twitter-client-bookmarks.js +61 -0
  137. package/dist/lib/twitter-client-bookmarks.js.map +1 -0
  138. package/dist/lib/twitter-client-constants.d.ts +44 -0
  139. package/dist/lib/twitter-client-constants.d.ts.map +1 -0
  140. package/dist/lib/twitter-client-constants.js +51 -0
  141. package/dist/lib/twitter-client-constants.js.map +1 -0
  142. package/dist/lib/twitter-client-engagement.d.ts +16 -0
  143. package/dist/lib/twitter-client-engagement.d.ts.map +1 -0
  144. package/dist/lib/twitter-client-engagement.js +81 -0
  145. package/dist/lib/twitter-client-engagement.js.map +1 -0
  146. package/dist/lib/twitter-client-features.d.ts +14 -0
  147. package/dist/lib/twitter-client-features.d.ts.map +1 -0
  148. package/dist/lib/twitter-client-features.js +347 -0
  149. package/dist/lib/twitter-client-features.js.map +1 -0
  150. package/dist/lib/twitter-client-follow.d.ts +8 -0
  151. package/dist/lib/twitter-client-follow.d.ts.map +1 -0
  152. package/dist/lib/twitter-client-follow.js +178 -0
  153. package/dist/lib/twitter-client-follow.js.map +1 -0
  154. package/dist/lib/twitter-client-home.d.ts +13 -0
  155. package/dist/lib/twitter-client-home.d.ts.map +1 -0
  156. package/dist/lib/twitter-client-home.js +138 -0
  157. package/dist/lib/twitter-client-home.js.map +1 -0
  158. package/dist/lib/twitter-client-lists.d.ts +12 -0
  159. package/dist/lib/twitter-client-lists.d.ts.map +1 -0
  160. package/dist/lib/twitter-client-lists.js +390 -0
  161. package/dist/lib/twitter-client-lists.js.map +1 -0
  162. package/dist/lib/twitter-client-media.d.ts +11 -0
  163. package/dist/lib/twitter-client-media.d.ts.map +1 -0
  164. package/dist/lib/twitter-client-media.js +136 -0
  165. package/dist/lib/twitter-client-media.js.map +1 -0
  166. package/dist/lib/twitter-client-news.d.ts +47 -0
  167. package/dist/lib/twitter-client-news.d.ts.map +1 -0
  168. package/dist/lib/twitter-client-news.js +283 -0
  169. package/dist/lib/twitter-client-news.js.map +1 -0
  170. package/dist/lib/twitter-client-posting.d.ts +8 -0
  171. package/dist/lib/twitter-client-posting.d.ts.map +1 -0
  172. package/dist/lib/twitter-client-posting.js +218 -0
  173. package/dist/lib/twitter-client-posting.js.map +1 -0
  174. package/dist/lib/twitter-client-search.d.ts +19 -0
  175. package/dist/lib/twitter-client-search.d.ts.map +1 -0
  176. package/dist/lib/twitter-client-search.js +157 -0
  177. package/dist/lib/twitter-client-search.js.map +1 -0
  178. package/dist/lib/twitter-client-timelines.d.ts +23 -0
  179. package/dist/lib/twitter-client-timelines.d.ts.map +1 -0
  180. package/dist/lib/twitter-client-timelines.js +471 -0
  181. package/dist/lib/twitter-client-timelines.js.map +1 -0
  182. package/dist/lib/twitter-client-tweet-detail.d.ts +25 -0
  183. package/dist/lib/twitter-client-tweet-detail.d.ts.map +1 -0
  184. package/dist/lib/twitter-client-tweet-detail.js +295 -0
  185. package/dist/lib/twitter-client-tweet-detail.js.map +1 -0
  186. package/dist/lib/twitter-client-types.d.ts +407 -0
  187. package/dist/lib/twitter-client-types.d.ts.map +1 -0
  188. package/dist/lib/twitter-client-types.js +2 -0
  189. package/dist/lib/twitter-client-types.js.map +1 -0
  190. package/dist/lib/twitter-client-user-lookup.d.ts +16 -0
  191. package/dist/lib/twitter-client-user-lookup.d.ts.map +1 -0
  192. package/dist/lib/twitter-client-user-lookup.js +224 -0
  193. package/dist/lib/twitter-client-user-lookup.js.map +1 -0
  194. package/dist/lib/twitter-client-user-tweets.d.ts +22 -0
  195. package/dist/lib/twitter-client-user-tweets.d.ts.map +1 -0
  196. package/dist/lib/twitter-client-user-tweets.js +154 -0
  197. package/dist/lib/twitter-client-user-tweets.js.map +1 -0
  198. package/dist/lib/twitter-client-users.d.ts +9 -0
  199. package/dist/lib/twitter-client-users.d.ts.map +1 -0
  200. package/dist/lib/twitter-client-users.js +358 -0
  201. package/dist/lib/twitter-client-users.js.map +1 -0
  202. package/dist/lib/twitter-client-utils.d.ts +173 -0
  203. package/dist/lib/twitter-client-utils.d.ts.map +1 -0
  204. package/dist/lib/twitter-client-utils.js +511 -0
  205. package/dist/lib/twitter-client-utils.js.map +1 -0
  206. package/dist/lib/twitter-client.d.ts +23 -0
  207. package/dist/lib/twitter-client.d.ts.map +1 -0
  208. package/dist/lib/twitter-client.js +21 -0
  209. package/dist/lib/twitter-client.js.map +1 -0
  210. package/dist/lib/version.d.ts +6 -0
  211. package/dist/lib/version.d.ts.map +1 -0
  212. package/dist/lib/version.js +174 -0
  213. package/dist/lib/version.js.map +1 -0
  214. package/package.json +61 -0
@@ -0,0 +1,157 @@
1
+ import { TWITTER_API_BASE } from './twitter-client-constants.js';
2
+ import { buildSearchFeatures } from './twitter-client-features.js';
3
+ import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js';
4
+ const RAW_QUERY_MISSING_REGEX = /must be defined/i;
5
+ function isQueryIdMismatch(payload) {
6
+ try {
7
+ const parsed = JSON.parse(payload);
8
+ return (parsed.errors?.some((error) => {
9
+ if (error?.extensions?.code === 'GRAPHQL_VALIDATION_FAILED') {
10
+ return true;
11
+ }
12
+ if (error?.path?.includes('rawQuery') && RAW_QUERY_MISSING_REGEX.test(error.message ?? '')) {
13
+ return true;
14
+ }
15
+ return false;
16
+ }) ?? false);
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ export function withSearch(Base) {
23
+ class TwitterClientSearch extends Base {
24
+ // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
25
+ constructor(...args) {
26
+ super(...args);
27
+ }
28
+ /**
29
+ * Search for tweets matching a query
30
+ */
31
+ async search(query, count = 20, options = {}) {
32
+ return this.searchPaged(query, count, options);
33
+ }
34
+ /**
35
+ * Get all search results (paged)
36
+ */
37
+ async getAllSearchResults(query, options) {
38
+ return this.searchPaged(query, Number.POSITIVE_INFINITY, options);
39
+ }
40
+ async searchPaged(query, limit, options = {}) {
41
+ const features = buildSearchFeatures();
42
+ const pageSize = 20;
43
+ const seen = new Set();
44
+ const tweets = [];
45
+ let cursor = options.cursor;
46
+ let nextCursor;
47
+ let pagesFetched = 0;
48
+ const { includeRaw = false, maxPages } = options;
49
+ const fetchPage = async (pageCount, pageCursor) => {
50
+ let lastError;
51
+ let had404 = false;
52
+ const queryIds = await this.getSearchTimelineQueryIds();
53
+ for (const queryId of queryIds) {
54
+ const variables = {
55
+ rawQuery: query,
56
+ count: pageCount,
57
+ querySource: 'typed_query',
58
+ product: 'Latest',
59
+ ...(pageCursor ? { cursor: pageCursor } : {}),
60
+ };
61
+ const params = new URLSearchParams({
62
+ variables: JSON.stringify(variables),
63
+ });
64
+ const url = `${TWITTER_API_BASE}/${queryId}/SearchTimeline?${params.toString()}`;
65
+ try {
66
+ const response = await this.fetchWithTimeout(url, {
67
+ method: 'POST',
68
+ headers: this.getHeaders(),
69
+ body: JSON.stringify({ features, queryId }),
70
+ });
71
+ if (response.status === 404) {
72
+ had404 = true;
73
+ lastError = `HTTP ${response.status}`;
74
+ continue;
75
+ }
76
+ if (!response.ok) {
77
+ const text = await response.text();
78
+ const shouldRefreshQueryIds = (response.status === 400 || response.status === 422) && isQueryIdMismatch(text);
79
+ return {
80
+ success: false,
81
+ error: `HTTP ${response.status}: ${text.slice(0, 200)}`,
82
+ had404: had404 || shouldRefreshQueryIds,
83
+ };
84
+ }
85
+ const data = (await response.json());
86
+ if (data.errors && data.errors.length > 0) {
87
+ const shouldRefreshQueryIds = data.errors.some((error) => error?.extensions?.code === 'GRAPHQL_VALIDATION_FAILED');
88
+ return {
89
+ success: false,
90
+ error: data.errors.map((e) => e.message).join(', '),
91
+ had404: had404 || shouldRefreshQueryIds,
92
+ };
93
+ }
94
+ const instructions = data.data?.search_by_raw_query?.search_timeline?.timeline?.instructions;
95
+ const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw });
96
+ const nextCursor = extractCursorFromInstructions(instructions);
97
+ return { success: true, tweets: pageTweets, cursor: nextCursor, had404 };
98
+ }
99
+ catch (error) {
100
+ lastError = error instanceof Error ? error.message : String(error);
101
+ }
102
+ }
103
+ return { success: false, error: lastError ?? 'Unknown error fetching search results', had404 };
104
+ };
105
+ const fetchWithRefresh = async (pageCount, pageCursor) => {
106
+ const firstAttempt = await fetchPage(pageCount, pageCursor);
107
+ if (firstAttempt.success) {
108
+ return firstAttempt;
109
+ }
110
+ if (firstAttempt.had404) {
111
+ await this.refreshQueryIds();
112
+ const secondAttempt = await fetchPage(pageCount, pageCursor);
113
+ if (secondAttempt.success) {
114
+ return secondAttempt;
115
+ }
116
+ return { success: false, error: secondAttempt.error };
117
+ }
118
+ return { success: false, error: firstAttempt.error };
119
+ };
120
+ const unlimited = limit === Number.POSITIVE_INFINITY;
121
+ while (unlimited || tweets.length < limit) {
122
+ const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length);
123
+ const page = await fetchWithRefresh(pageCount, cursor);
124
+ if (!page.success) {
125
+ return { success: false, error: page.error };
126
+ }
127
+ pagesFetched += 1;
128
+ let added = 0;
129
+ for (const tweet of page.tweets) {
130
+ if (seen.has(tweet.id)) {
131
+ continue;
132
+ }
133
+ seen.add(tweet.id);
134
+ tweets.push(tweet);
135
+ added += 1;
136
+ if (!unlimited && tweets.length >= limit) {
137
+ break;
138
+ }
139
+ }
140
+ const pageCursor = page.cursor;
141
+ if (!pageCursor || pageCursor === cursor || page.tweets.length === 0 || added === 0) {
142
+ nextCursor = undefined;
143
+ break;
144
+ }
145
+ if (maxPages && pagesFetched >= maxPages) {
146
+ nextCursor = pageCursor;
147
+ break;
148
+ }
149
+ cursor = pageCursor;
150
+ nextCursor = pageCursor;
151
+ }
152
+ return { success: true, tweets, nextCursor };
153
+ }
154
+ }
155
+ return TwitterClientSearch;
156
+ }
157
+ //# sourceMappingURL=twitter-client-search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twitter-client-search.js","sourceRoot":"","sources":["../../src/lib/twitter-client-search.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
2
+ import type { SearchResult } from './twitter-client-types.js';
3
+ /** Options for timeline fetch methods */
4
+ export interface TimelineFetchOptions {
5
+ /** Include raw GraphQL response in `_raw` field */
6
+ includeRaw?: boolean;
7
+ }
8
+ /** Options for paged timeline fetch methods */
9
+ export interface TimelinePaginationOptions extends TimelineFetchOptions {
10
+ maxPages?: number;
11
+ /** Starting cursor for pagination (resume from previous fetch) */
12
+ cursor?: string;
13
+ }
14
+ export interface TwitterClientTimelineMethods {
15
+ getBookmarks(count?: number, options?: TimelineFetchOptions): Promise<SearchResult>;
16
+ getAllBookmarks(options?: TimelinePaginationOptions): Promise<SearchResult>;
17
+ getLikes(count?: number, options?: TimelineFetchOptions): Promise<SearchResult>;
18
+ getAllLikes(options?: TimelinePaginationOptions): Promise<SearchResult>;
19
+ getBookmarkFolderTimeline(folderId: string, count?: number, options?: TimelineFetchOptions): Promise<SearchResult>;
20
+ getAllBookmarkFolderTimeline(folderId: string, options?: TimelinePaginationOptions): Promise<SearchResult>;
21
+ }
22
+ export declare function withTimelines<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase): Mixin<TBase, TwitterClientTimelineMethods>;
23
+ //# sourceMappingURL=twitter-client-timelines.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twitter-client-timelines.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-timelines.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,KAAK,EAAsB,YAAY,EAAa,MAAM,2BAA2B,CAAC;AAG7F,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,+CAA+C;AAC/C,MAAM,WAAW,yBAA0B,SAAQ,oBAAoB;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,4BAA4B;IAC3C,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACpF,eAAe,CAAC,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC5E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAChF,WAAW,CAAC,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACxE,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACnH,4BAA4B,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAC5G;AAED,wBAAgB,aAAa,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAChF,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,4BAA4B,CAAC,CA4lB5C"}
@@ -0,0 +1,471 @@
1
+ import { TWITTER_API_BASE } from './twitter-client-constants.js';
2
+ import { buildBookmarksFeatures, buildLikesFeatures } from './twitter-client-features.js';
3
+ import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js';
4
+ export function withTimelines(Base) {
5
+ class TwitterClientTimelines extends Base {
6
+ // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
7
+ constructor(...args) {
8
+ super(...args);
9
+ }
10
+ logBookmarksDebug(message, data) {
11
+ if (process.env.BIRD_DEBUG_BOOKMARKS !== '1') {
12
+ return;
13
+ }
14
+ if (data) {
15
+ console.error(`[bird][debug][bookmarks] ${message}`, JSON.stringify(data));
16
+ }
17
+ else {
18
+ console.error(`[bird][debug][bookmarks] ${message}`);
19
+ }
20
+ }
21
+ async getBookmarksQueryIds() {
22
+ const primary = await this.getQueryId('Bookmarks');
23
+ return Array.from(new Set([primary, 'RV1g3b8n_SGOHwkqKYSCFw', 'tmd4ifV8RHltzn8ymGg1aw']));
24
+ }
25
+ async getBookmarkFolderQueryIds() {
26
+ const primary = await this.getQueryId('BookmarkFolderTimeline');
27
+ return Array.from(new Set([primary, 'KJIQpsvxrTfRIlbaRIySHQ']));
28
+ }
29
+ async getLikesQueryIds() {
30
+ const primary = await this.getQueryId('Likes');
31
+ return Array.from(new Set([primary, 'JR2gceKucIKcVNB_9JkhsA']));
32
+ }
33
+ /**
34
+ * Get the authenticated user's bookmarks
35
+ */
36
+ async getBookmarks(count = 20, options = {}) {
37
+ return this.getBookmarksPaged(count, options);
38
+ }
39
+ async getAllBookmarks(options) {
40
+ return this.getBookmarksPaged(Number.POSITIVE_INFINITY, options);
41
+ }
42
+ /**
43
+ * Get the authenticated user's liked tweets
44
+ */
45
+ async getLikes(count = 20, options = {}) {
46
+ return this.getLikesPaged(count, options);
47
+ }
48
+ async getAllLikes(options) {
49
+ return this.getLikesPaged(Number.POSITIVE_INFINITY, options);
50
+ }
51
+ async getLikesPaged(limit, options = {}) {
52
+ const userResult = await this.getCurrentUser();
53
+ if (!userResult.success || !userResult.user) {
54
+ return { success: false, error: userResult.error ?? 'Could not determine current user' };
55
+ }
56
+ const userId = userResult.user.id;
57
+ const features = buildLikesFeatures();
58
+ const pageSize = 20;
59
+ const seen = new Set();
60
+ const tweets = [];
61
+ let cursor = options.cursor;
62
+ let nextCursor;
63
+ let pagesFetched = 0;
64
+ const { includeRaw = false, maxPages } = options;
65
+ const fetchPage = async (pageCount, pageCursor) => {
66
+ let lastError;
67
+ let had404 = false;
68
+ const queryIds = await this.getLikesQueryIds();
69
+ for (const queryId of queryIds) {
70
+ const variables = {
71
+ userId,
72
+ count: pageCount,
73
+ includePromotedContent: false,
74
+ withClientEventToken: false,
75
+ withBirdwatchNotes: false,
76
+ withVoice: true,
77
+ ...(pageCursor ? { cursor: pageCursor } : {}),
78
+ };
79
+ const params = new URLSearchParams({
80
+ variables: JSON.stringify(variables),
81
+ features: JSON.stringify(features),
82
+ });
83
+ const url = `${TWITTER_API_BASE}/${queryId}/Likes?${params.toString()}`;
84
+ try {
85
+ const response = await this.fetchWithTimeout(url, {
86
+ method: 'GET',
87
+ headers: this.getHeaders(),
88
+ });
89
+ if (response.status === 404) {
90
+ had404 = true;
91
+ lastError = `HTTP ${response.status}`;
92
+ continue;
93
+ }
94
+ if (!response.ok) {
95
+ const text = await response.text();
96
+ return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 };
97
+ }
98
+ const data = (await response.json());
99
+ const instructions = data.data?.user?.result?.timeline?.timeline?.instructions;
100
+ if (data.errors && data.errors.length > 0) {
101
+ const message = data.errors.map((e) => e.message).join(', ');
102
+ if (!instructions) {
103
+ if (message.includes('Query: Unspecified')) {
104
+ lastError = message;
105
+ continue;
106
+ }
107
+ return { success: false, error: message, had404 };
108
+ }
109
+ }
110
+ const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw });
111
+ const extractedCursor = extractCursorFromInstructions(instructions);
112
+ return { success: true, tweets: pageTweets, cursor: extractedCursor, had404 };
113
+ }
114
+ catch (error) {
115
+ lastError = error instanceof Error ? error.message : String(error);
116
+ }
117
+ }
118
+ return { success: false, error: lastError ?? 'Unknown error fetching likes', had404 };
119
+ };
120
+ const fetchWithRefresh = async (pageCount, pageCursor) => {
121
+ const firstAttempt = await fetchPage(pageCount, pageCursor);
122
+ if (firstAttempt.success) {
123
+ return firstAttempt;
124
+ }
125
+ const shouldRefresh = firstAttempt.had404 ||
126
+ (typeof firstAttempt.error === 'string' && firstAttempt.error.includes('Query: Unspecified'));
127
+ if (shouldRefresh) {
128
+ await this.refreshQueryIds();
129
+ const secondAttempt = await fetchPage(pageCount, pageCursor);
130
+ if (secondAttempt.success) {
131
+ return secondAttempt;
132
+ }
133
+ return { success: false, error: secondAttempt.error };
134
+ }
135
+ return { success: false, error: firstAttempt.error };
136
+ };
137
+ const unlimited = limit === Number.POSITIVE_INFINITY;
138
+ while (unlimited || tweets.length < limit) {
139
+ const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length);
140
+ const page = await fetchWithRefresh(pageCount, cursor);
141
+ if (!page.success) {
142
+ return { success: false, error: page.error };
143
+ }
144
+ pagesFetched += 1;
145
+ let added = 0;
146
+ for (const tweet of page.tweets) {
147
+ if (seen.has(tweet.id)) {
148
+ continue;
149
+ }
150
+ seen.add(tweet.id);
151
+ tweets.push(tweet);
152
+ added += 1;
153
+ if (!unlimited && tweets.length >= limit) {
154
+ break;
155
+ }
156
+ }
157
+ const pageCursor = page.cursor;
158
+ if (!pageCursor || pageCursor === cursor || page.tweets.length === 0 || added === 0) {
159
+ nextCursor = undefined;
160
+ break;
161
+ }
162
+ if (maxPages && pagesFetched >= maxPages) {
163
+ nextCursor = pageCursor;
164
+ break;
165
+ }
166
+ cursor = pageCursor;
167
+ nextCursor = pageCursor;
168
+ }
169
+ return { success: true, tweets, nextCursor };
170
+ }
171
+ /**
172
+ * Get the authenticated user's bookmark folder timeline
173
+ */
174
+ async getBookmarkFolderTimeline(folderId, count = 20, options = {}) {
175
+ return this.getBookmarkFolderTimelinePaged(folderId, count, options);
176
+ }
177
+ async getAllBookmarkFolderTimeline(folderId, options) {
178
+ return this.getBookmarkFolderTimelinePaged(folderId, Number.POSITIVE_INFINITY, options);
179
+ }
180
+ async getBookmarksPaged(limit, options = {}) {
181
+ const features = buildBookmarksFeatures();
182
+ const pageSize = 20;
183
+ const seen = new Set();
184
+ const tweets = [];
185
+ let cursor = options.cursor;
186
+ let nextCursor;
187
+ let pagesFetched = 0;
188
+ const { includeRaw = false, maxPages } = options;
189
+ const fetchPage = async (pageCount, pageCursor) => {
190
+ let lastError;
191
+ let had404 = false;
192
+ const queryIds = await this.getBookmarksQueryIds();
193
+ const variables = {
194
+ count: pageCount,
195
+ includePromotedContent: false,
196
+ withDownvotePerspective: false,
197
+ withReactionsMetadata: false,
198
+ withReactionsPerspective: false,
199
+ ...(pageCursor ? { cursor: pageCursor } : {}),
200
+ };
201
+ const params = new URLSearchParams({
202
+ variables: JSON.stringify(variables),
203
+ features: JSON.stringify(features),
204
+ });
205
+ for (const queryId of queryIds) {
206
+ const url = `${TWITTER_API_BASE}/${queryId}/Bookmarks?${params.toString()}`;
207
+ try {
208
+ this.logBookmarksDebug('request bookmarks page', {
209
+ queryId,
210
+ pageCount,
211
+ hasCursor: Boolean(pageCursor),
212
+ });
213
+ const response = await this.fetchWithRetry(url, {
214
+ method: 'GET',
215
+ headers: this.getHeaders(),
216
+ });
217
+ if (response.status === 404) {
218
+ had404 = true;
219
+ lastError = `HTTP ${response.status}`;
220
+ this.logBookmarksDebug('bookmarks 404', { queryId });
221
+ continue;
222
+ }
223
+ if (!response.ok) {
224
+ const text = await response.text();
225
+ this.logBookmarksDebug('bookmarks non-200', {
226
+ queryId,
227
+ status: response.status,
228
+ body: text.slice(0, 200),
229
+ });
230
+ return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 };
231
+ }
232
+ const data = (await response.json());
233
+ const instructions = data.data?.bookmark_timeline_v2?.timeline?.instructions;
234
+ const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw });
235
+ const nextCursor = extractCursorFromInstructions(instructions);
236
+ if (data.errors && data.errors.length > 0) {
237
+ this.logBookmarksDebug('bookmarks graphql errors (non-fatal)', { queryId, errors: data.errors });
238
+ if (!instructions) {
239
+ lastError = data.errors.map((e) => e.message).join(', ');
240
+ continue;
241
+ }
242
+ }
243
+ this.logBookmarksDebug('bookmarks page parsed', {
244
+ queryId,
245
+ tweets: pageTweets.length,
246
+ hasNextCursor: Boolean(nextCursor),
247
+ });
248
+ return { success: true, tweets: pageTweets, cursor: nextCursor, had404 };
249
+ }
250
+ catch (error) {
251
+ lastError = error instanceof Error ? error.message : String(error);
252
+ this.logBookmarksDebug('bookmarks request error', { queryId, error: lastError });
253
+ }
254
+ }
255
+ return { success: false, error: lastError ?? 'Unknown error fetching bookmarks', had404 };
256
+ };
257
+ const fetchWithRefresh = async (pageCount, pageCursor) => {
258
+ const firstAttempt = await fetchPage(pageCount, pageCursor);
259
+ if (firstAttempt.success) {
260
+ return firstAttempt;
261
+ }
262
+ if (firstAttempt.had404) {
263
+ await this.refreshQueryIds();
264
+ const secondAttempt = await fetchPage(pageCount, pageCursor);
265
+ if (secondAttempt.success) {
266
+ return secondAttempt;
267
+ }
268
+ return { success: false, error: secondAttempt.error };
269
+ }
270
+ return { success: false, error: firstAttempt.error };
271
+ };
272
+ const unlimited = limit === Number.POSITIVE_INFINITY;
273
+ while (unlimited || tweets.length < limit) {
274
+ const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length);
275
+ const page = await fetchWithRefresh(pageCount, cursor);
276
+ if (!page.success) {
277
+ return { success: false, error: page.error };
278
+ }
279
+ pagesFetched += 1;
280
+ let added = 0;
281
+ for (const tweet of page.tweets) {
282
+ if (seen.has(tweet.id)) {
283
+ continue;
284
+ }
285
+ seen.add(tweet.id);
286
+ tweets.push(tweet);
287
+ added += 1;
288
+ if (!unlimited && tweets.length >= limit) {
289
+ break;
290
+ }
291
+ }
292
+ const pageCursor = page.cursor;
293
+ if (!pageCursor || pageCursor === cursor || page.tweets.length === 0 || added === 0) {
294
+ nextCursor = undefined;
295
+ break;
296
+ }
297
+ if (maxPages && pagesFetched >= maxPages) {
298
+ nextCursor = pageCursor;
299
+ break;
300
+ }
301
+ cursor = pageCursor;
302
+ nextCursor = pageCursor;
303
+ }
304
+ return { success: true, tweets, nextCursor };
305
+ }
306
+ async getBookmarkFolderTimelinePaged(folderId, limit, options = {}) {
307
+ const features = buildBookmarksFeatures();
308
+ const pageSize = 20;
309
+ const seen = new Set();
310
+ const tweets = [];
311
+ let cursor = options.cursor;
312
+ let nextCursor;
313
+ let pagesFetched = 0;
314
+ const { includeRaw = false, maxPages } = options;
315
+ const buildVariables = (pageCount, pageCursor, includeCount) => ({
316
+ bookmark_collection_id: folderId,
317
+ includePromotedContent: true,
318
+ ...(includeCount ? { count: pageCount } : {}),
319
+ ...(pageCursor ? { cursor: pageCursor } : {}),
320
+ });
321
+ const fetchPage = async (pageCount, pageCursor) => {
322
+ let lastError;
323
+ let had404 = false;
324
+ const queryIds = await this.getBookmarkFolderQueryIds();
325
+ const tryOnce = async (variables) => {
326
+ const params = new URLSearchParams({
327
+ variables: JSON.stringify(variables),
328
+ features: JSON.stringify(features),
329
+ });
330
+ for (const queryId of queryIds) {
331
+ const url = `${TWITTER_API_BASE}/${queryId}/BookmarkFolderTimeline?${params.toString()}`;
332
+ try {
333
+ this.logBookmarksDebug('request bookmark folder page', {
334
+ queryId,
335
+ pageCount,
336
+ hasCursor: Boolean(pageCursor),
337
+ includeCount: Object.hasOwn(variables, 'count'),
338
+ });
339
+ const response = await this.fetchWithRetry(url, {
340
+ method: 'GET',
341
+ headers: this.getHeaders(),
342
+ });
343
+ if (response.status === 404) {
344
+ had404 = true;
345
+ lastError = `HTTP ${response.status}`;
346
+ this.logBookmarksDebug('bookmark folder 404', { queryId });
347
+ continue;
348
+ }
349
+ if (!response.ok) {
350
+ const text = await response.text();
351
+ this.logBookmarksDebug('bookmark folder non-200', {
352
+ queryId,
353
+ status: response.status,
354
+ body: text.slice(0, 200),
355
+ });
356
+ return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 };
357
+ }
358
+ const data = (await response.json());
359
+ const instructions = data.data?.bookmark_collection_timeline?.timeline?.instructions;
360
+ const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw });
361
+ const nextCursor = extractCursorFromInstructions(instructions);
362
+ if (data.errors && data.errors.length > 0) {
363
+ this.logBookmarksDebug('bookmark folder graphql errors (non-fatal)', { queryId, errors: data.errors });
364
+ if (!instructions) {
365
+ lastError = data.errors.map((e) => e.message).join(', ');
366
+ continue;
367
+ }
368
+ }
369
+ this.logBookmarksDebug('bookmark folder page parsed', {
370
+ queryId,
371
+ tweets: pageTweets.length,
372
+ hasNextCursor: Boolean(nextCursor),
373
+ });
374
+ return { success: true, tweets: pageTweets, cursor: nextCursor, had404 };
375
+ }
376
+ catch (error) {
377
+ lastError = error instanceof Error ? error.message : String(error);
378
+ this.logBookmarksDebug('bookmark folder request error', { queryId, error: lastError });
379
+ }
380
+ }
381
+ return { success: false, error: lastError ?? 'Unknown error fetching bookmark folder', had404 };
382
+ };
383
+ let attempt = await tryOnce(buildVariables(pageCount, pageCursor, true));
384
+ if (!attempt.success && attempt.error?.includes('Variable "$count"')) {
385
+ attempt = await tryOnce(buildVariables(pageCount, pageCursor, false));
386
+ }
387
+ if (!attempt.success && attempt.error?.includes('Variable "$cursor"') && pageCursor) {
388
+ return {
389
+ success: false,
390
+ error: 'Bookmark folder pagination rejected the cursor parameter',
391
+ had404: attempt.had404,
392
+ };
393
+ }
394
+ return attempt;
395
+ };
396
+ const fetchWithRefresh = async (pageCount, pageCursor) => {
397
+ const firstAttempt = await fetchPage(pageCount, pageCursor);
398
+ if (firstAttempt.success) {
399
+ return firstAttempt;
400
+ }
401
+ if (firstAttempt.had404) {
402
+ await this.refreshQueryIds();
403
+ const secondAttempt = await fetchPage(pageCount, pageCursor);
404
+ if (secondAttempt.success) {
405
+ return secondAttempt;
406
+ }
407
+ return { success: false, error: secondAttempt.error };
408
+ }
409
+ return { success: false, error: firstAttempt.error };
410
+ };
411
+ const unlimited = limit === Number.POSITIVE_INFINITY;
412
+ while (unlimited || tweets.length < limit) {
413
+ const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length);
414
+ const page = await fetchWithRefresh(pageCount, cursor);
415
+ if (!page.success) {
416
+ return { success: false, error: page.error };
417
+ }
418
+ pagesFetched += 1;
419
+ let added = 0;
420
+ for (const tweet of page.tweets) {
421
+ if (seen.has(tweet.id)) {
422
+ continue;
423
+ }
424
+ seen.add(tweet.id);
425
+ tweets.push(tweet);
426
+ added += 1;
427
+ if (!unlimited && tweets.length >= limit) {
428
+ break;
429
+ }
430
+ }
431
+ const pageCursor = page.cursor;
432
+ if (!pageCursor || pageCursor === cursor || page.tweets.length === 0 || added === 0) {
433
+ nextCursor = undefined;
434
+ break;
435
+ }
436
+ if (maxPages && pagesFetched >= maxPages) {
437
+ nextCursor = pageCursor;
438
+ break;
439
+ }
440
+ cursor = pageCursor;
441
+ nextCursor = pageCursor;
442
+ }
443
+ return { success: true, tweets, nextCursor };
444
+ }
445
+ async fetchWithRetry(url, init) {
446
+ const maxRetries = 2;
447
+ const baseDelayMs = 500;
448
+ const retryable = new Set([429, 500, 502, 503, 504]);
449
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
450
+ const response = await this.fetchWithTimeout(url, init);
451
+ if (!retryable.has(response.status) || attempt === maxRetries) {
452
+ return response;
453
+ }
454
+ this.logBookmarksDebug('retrying bookmarks request', {
455
+ status: response.status,
456
+ attempt,
457
+ });
458
+ // Retry-After supports delta-seconds only; HTTP-date falls back to backoff.
459
+ const retryAfter = response.headers?.get?.('retry-after');
460
+ const retryAfterMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : Number.NaN;
461
+ const backoffMs = Number.isFinite(retryAfterMs)
462
+ ? retryAfterMs
463
+ : baseDelayMs * 2 ** attempt + Math.floor(Math.random() * baseDelayMs);
464
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
465
+ }
466
+ return this.fetchWithTimeout(url, init);
467
+ }
468
+ }
469
+ return TwitterClientTimelines;
470
+ }
471
+ //# sourceMappingURL=twitter-client-timelines.js.map