koishi-plugin-vr-fever 0.0.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.
Files changed (60) hide show
  1. package/README.md +21 -0
  2. package/lib/commands/weibo.d.ts +7 -0
  3. package/lib/commands/weibo.js +177 -0
  4. package/lib/index.d.ts +37 -0
  5. package/lib/index.js +110 -0
  6. package/lib/service/comment/fetch-for-timeline.d.ts +4 -0
  7. package/lib/service/comment/fetch-for-timeline.js +41 -0
  8. package/lib/service/comment/filter.d.ts +3 -0
  9. package/lib/service/comment/filter.js +25 -0
  10. package/lib/service/comment/formatter.d.ts +3 -0
  11. package/lib/service/comment/formatter.js +42 -0
  12. package/lib/service/comment/normalizer.d.ts +2 -0
  13. package/lib/service/comment/normalizer.js +53 -0
  14. package/lib/service/comment/types.d.ts +23 -0
  15. package/lib/service/comment/types.js +2 -0
  16. package/lib/service/drawer/html-builder.d.ts +6 -0
  17. package/lib/service/drawer/html-builder.js +510 -0
  18. package/lib/service/drawer/image-resolver.d.ts +10 -0
  19. package/lib/service/drawer/image-resolver.js +174 -0
  20. package/lib/service/drawer/index.d.ts +4 -0
  21. package/lib/service/drawer/index.js +12 -0
  22. package/lib/service/drawer/screenshot.d.ts +7 -0
  23. package/lib/service/drawer/screenshot.js +75 -0
  24. package/lib/service/drawer/types.d.ts +42 -0
  25. package/lib/service/drawer/types.js +2 -0
  26. package/lib/service/login.d.ts +4 -0
  27. package/lib/service/login.js +79 -0
  28. package/lib/service/poll.d.ts +8 -0
  29. package/lib/service/poll.js +100 -0
  30. package/lib/service/timeline/index.d.ts +3 -0
  31. package/lib/service/timeline/index.js +19 -0
  32. package/lib/service/timeline/normalizer.d.ts +18 -0
  33. package/lib/service/timeline/normalizer.js +141 -0
  34. package/lib/service/timeline/text-formatter.d.ts +3 -0
  35. package/lib/service/timeline/text-formatter.js +114 -0
  36. package/lib/service/timeline/types.d.ts +72 -0
  37. package/lib/service/timeline/types.js +9 -0
  38. package/lib/service/weibo-fetch.d.ts +14 -0
  39. package/lib/service/weibo-fetch.js +66 -0
  40. package/lib/service/weibo-http.d.ts +3 -0
  41. package/lib/service/weibo-http.js +27 -0
  42. package/lib/util/constants.d.ts +18 -0
  43. package/lib/util/constants.js +43 -0
  44. package/lib/util/html.d.ts +1 -0
  45. package/lib/util/html.js +10 -0
  46. package/lib/util/parse-render-output.d.ts +2 -0
  47. package/lib/util/parse-render-output.js +13 -0
  48. package/lib/util/puppeteer-cookie.d.ts +47 -0
  49. package/lib/util/puppeteer-cookie.js +552 -0
  50. package/lib/util/save-screenshot.d.ts +14 -0
  51. package/lib/util/save-screenshot.js +26 -0
  52. package/lib/util/send-msg.d.ts +4 -0
  53. package/lib/util/send-msg.js +12 -0
  54. package/lib/util/timer.d.ts +2 -0
  55. package/lib/util/timer.js +10 -0
  56. package/lib/util/weibo-date.d.ts +4 -0
  57. package/lib/util/weibo-date.js +47 -0
  58. package/lib/util/weibo-media.d.ts +8 -0
  59. package/lib/util/weibo-media.js +26 -0
  60. package/package.json +61 -0
@@ -0,0 +1,510 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildMultiTimelineHtml = exports.buildTimelineHtml = void 0;
4
+ const constants_1 = require("../../util/constants");
5
+ const html_1 = require("../../util/html");
6
+ const weibo_date_1 = require("../../util/weibo-date");
7
+ const ACTIVITY_LABELS = {
8
+ original: "最近原创",
9
+ retweet: "最近转发",
10
+ like: "最近赞过",
11
+ };
12
+ const formatPostText = (text) => (0, html_1.escapeHtml)(text || "").replace(/\n/g, "<br/>");
13
+ const isMeaningfulRetweetComment = (text) => Boolean(text?.trim()) && text.trim() !== "转发微博";
14
+ const buildPostImages = (urls, hiddenCount = 0) => {
15
+ if (!urls.length && hiddenCount <= 0)
16
+ return "";
17
+ const gridClass = urls.length === 1
18
+ ? "post-images post-images--single"
19
+ : "post-images post-images--grid";
20
+ return `<div class="${gridClass}">
21
+ ${urls
22
+ .map((url) => `<img class="post-image" src="${(0, html_1.escapeHtml)(url)}" alt="" />`)
23
+ .join("")}
24
+ ${hiddenCount > 0 ? `<div class="post-images-more">还有 ${hiddenCount} 张图片未显示</div>` : ""}
25
+ </div>`;
26
+ };
27
+ const buildMediaCard = (coverUrl, title) => {
28
+ if (!coverUrl && !title)
29
+ return "";
30
+ if (!coverUrl) {
31
+ return title
32
+ ? `<div class="post-media-title">${(0, html_1.escapeHtml)(title)}</div>`
33
+ : "";
34
+ }
35
+ return `<div class="post-media-card">
36
+ <img class="post-media-cover" src="${(0, html_1.escapeHtml)(coverUrl)}" alt="" />
37
+ ${title ? `<div class="post-media-title">${(0, html_1.escapeHtml)(title)}</div>` : ""}
38
+ </div>`;
39
+ };
40
+ const buildPostMedia = (post) => {
41
+ const imageUrls = post.resolvedImages || [];
42
+ const mediaCover = post.resolvedMediaCover;
43
+ return `${buildMediaCard(mediaCover, post.page_info?.page_title)}
44
+ ${buildPostImages(imageUrls, post.hiddenImageCount || 0)}`;
45
+ };
46
+ const buildQuotedPost = (retweeted, fallbackAvatar) => {
47
+ const quotedUser = retweeted.user || {};
48
+ const quotedAvatar = quotedUser.avatar_large || quotedUser.profile_image_url || fallbackAvatar;
49
+ return `<div class="quoted-post">
50
+ <div class="quoted-post-header">
51
+ <img class="quoted-post-avatar" src="${(0, html_1.escapeHtml)(quotedAvatar)}" alt="" />
52
+ <div class="quoted-post-meta">
53
+ <div class="quoted-post-name">${(0, html_1.escapeHtml)(quotedUser.screen_name || "微博用户")}</div>
54
+ <div class="quoted-post-time">${(0, html_1.escapeHtml)(retweeted.createdAtText || retweeted.createdAt || "")} ${(0, html_1.escapeHtml)(retweeted.source || "")} ${(0, html_1.escapeHtml)(retweeted.region_name || "")}</div>
55
+ </div>
56
+ </div>
57
+ <div class="quoted-post-text">${formatPostText(retweeted.text)}</div>
58
+ ${buildPostMedia(retweeted)}
59
+ </div>`;
60
+ };
61
+ const buildCommentItem = (comment, fallbackAvatar, nested = false) => {
62
+ const user = comment.user || {};
63
+ const avatar = comment.resolvedAvatar ||
64
+ user.avatar_large ||
65
+ user.profile_image_url ||
66
+ fallbackAvatar;
67
+ const likes = comment.likesCount && comment.likesCount > 0
68
+ ? `<span class="comment-likes">👍 ${comment.likesCount}</span>`
69
+ : "";
70
+ const authorBadge = comment.isAuthor
71
+ ? `<span class="comment-author">博主</span>`
72
+ : "";
73
+ const replies = comment.replies?.length
74
+ ? `<div class="comment-replies">${comment.replies
75
+ .map((reply) => buildCommentItem(reply, fallbackAvatar, true))
76
+ .join("")}</div>`
77
+ : "";
78
+ return `<div class="comment-item${nested ? " comment-item--reply" : ""}">
79
+ <img class="comment-avatar" src="${(0, html_1.escapeHtml)(avatar)}" alt="" />
80
+ <div class="comment-body">
81
+ <div class="comment-meta">
82
+ <span class="comment-name">${(0, html_1.escapeHtml)(user.screen_name || "微博用户")}</span>
83
+ ${authorBadge}
84
+ <span class="comment-time">${(0, html_1.escapeHtml)(comment.createdAtText || comment.createdAt || "")}</span>
85
+ ${likes}
86
+ </div>
87
+ <div class="comment-text">${formatPostText(comment.text)}</div>
88
+ ${replies}
89
+ </div>
90
+ </div>`;
91
+ };
92
+ const buildPostComments = (comments, fallbackAvatar) => {
93
+ if (!comments?.length)
94
+ return "";
95
+ return `<div class="post-comments">
96
+ <div class="post-comments__title">最新评论</div>
97
+ ${comments.map((comment) => buildCommentItem(comment, fallbackAvatar)).join("")}
98
+ </div>`;
99
+ };
100
+ const buildActivityBadge = (post) => {
101
+ const label = ACTIVITY_LABELS[post.activityType];
102
+ if (!label)
103
+ return "";
104
+ const activityTime = post.activityAtTime != null
105
+ ? (0, weibo_date_1.formatWeiboDate)(new Date(post.activityAtTime))
106
+ : post.createdAtText || post.createdAt || "";
107
+ return `<div class="activity-badge activity-badge--${post.activityType}">
108
+ <span class="activity-badge__label">${(0, html_1.escapeHtml)(label)}</span>
109
+ ${activityTime ? `<span class="activity-badge__time">${(0, html_1.escapeHtml)(activityTime)}</span>${post.activityType === "like" ? ' <span class="activity-badge__hint">(帖子发布时间)</span>' : ""}` : ""}
110
+ </div>`;
111
+ };
112
+ const buildWeiboCardHtml = (profile, normalizedTimeline) => {
113
+ const user = profile?.user || {};
114
+ const avatar = user.avatar_hd || user.avatar_large || user.profile_image_url || "";
115
+ const cover = user.cover_image_phone || "";
116
+ const posts = normalizedTimeline
117
+ .map((post) => {
118
+ const postUser = post.user || user;
119
+ const postAvatar = postUser.avatar_large || postUser.profile_image_url || avatar;
120
+ const retweetComment = post.activityType === "retweet" && isMeaningfulRetweetComment(post.text)
121
+ ? `<div class="post-text post-text--comment">${formatPostText(post.text)}</div>`
122
+ : "";
123
+ const quotedPost = post.activityType === "retweet" && post.retweeted
124
+ ? buildQuotedPost(post.retweeted, postAvatar)
125
+ : "";
126
+ const originalBody = post.activityType !== "retweet"
127
+ ? `<div class="post-text">${formatPostText(post.text)}</div>
128
+ ${buildPostMedia(post)}`
129
+ : "";
130
+ return `
131
+ <article class="post post--${post.activityType}">
132
+ ${buildActivityBadge(post)}
133
+ <div class="post-header">
134
+ <img class="post-avatar" src="${(0, html_1.escapeHtml)(postAvatar)}" alt="" />
135
+ <div class="post-meta">
136
+ <div class="post-name">${(0, html_1.escapeHtml)(postUser.screen_name || user.screen_name || "微博用户")}</div>
137
+ <div class="post-time">${(0, html_1.escapeHtml)(post.createdAtText || post.createdAt || "")} ${(0, html_1.escapeHtml)(post.source || "")} ${(0, html_1.escapeHtml)(post.region_name || "")}</div>
138
+ </div>
139
+ </div>
140
+ ${retweetComment}
141
+ ${quotedPost}
142
+ ${originalBody}
143
+ <div class="post-stats">
144
+ <span>转发 ${post.reposts_count ?? 0}</span>
145
+ <span>评论 ${post.comments_count ?? 0}</span>
146
+ <span>赞 ${post.attitudes_count ?? 0}</span>
147
+ </div>
148
+ ${buildPostComments(post.comments, postAvatar)}
149
+ </article>
150
+ `;
151
+ })
152
+ .join("");
153
+ return `<div class="weibo-card">
154
+ <div class="cover">
155
+ ${cover ? `<img class="cover-image" src="${(0, html_1.escapeHtml)(cover)}" alt="" />` : ""}
156
+ </div>
157
+ <div class="profile">
158
+ <img class="avatar" src="${(0, html_1.escapeHtml)(avatar)}" alt="" />
159
+ <div class="profile-info">
160
+ <div class="name">${(0, html_1.escapeHtml)(user.screen_name || "微博用户")}</div>
161
+ <div class="desc">${(0, html_1.escapeHtml)(user.description || "暂无简介")}</div>
162
+ <div class="location">${(0, html_1.escapeHtml)(user.location || "")}</div>
163
+ </div>
164
+ </div>
165
+ <div class="stats">
166
+ <span><strong>${(0, html_1.escapeHtml)(user.followers_count_str || "0")}</strong>粉丝</span>
167
+ <span><strong>${user.friends_count ?? 0}</strong>关注</span>
168
+ <span><strong>${user.statuses_count ?? normalizedTimeline.length}</strong>微博</span>
169
+ </div>
170
+ <div class="timeline">
171
+ <div class="timeline-title">最近动态</div>
172
+ ${posts || '<div class="post"><div class="post-text">暂无微博</div></div>'}
173
+ </div>
174
+ </div>`;
175
+ };
176
+ const TIMELINE_PAGE_STYLES = `
177
+ * { box-sizing: border-box; margin: 0; padding: 0; }
178
+ body {
179
+ font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
180
+ background: #f3f4f6;
181
+ padding: 24px;
182
+ color: #1f2328;
183
+ }
184
+ .cover {
185
+ height: 100px;
186
+ overflow:hidden;
187
+ display: flex;
188
+ justify-content: center;
189
+ align-items: center;
190
+ width: 100%;
191
+ line-height: 0;
192
+ background: linear-gradient(135deg, #ff8a65 0%, #ff6a9b 45%, #7a5cff 100%);
193
+ }
194
+ .cover-image {
195
+ object-fit: cover;
196
+ object-position: center;
197
+ display: block;
198
+ width: 100%;
199
+ max-width: 100%;
200
+ height: auto;
201
+ }
202
+ .profile {
203
+ display: flex;
204
+ gap: 16px;
205
+ padding: 0 24px 20px;
206
+ margin-top: -36px;
207
+ align-items: flex-end;
208
+ }
209
+ .avatar {
210
+ width: 88px;
211
+ height: 88px;
212
+ border-radius: 50%;
213
+ border: 4px solid #fff;
214
+ object-fit: cover;
215
+ background: #fff;
216
+ flex-shrink: 0;
217
+ }
218
+ .profile-info { min-width: 0; padding-bottom: 4px; }
219
+ .name {
220
+ font-size: 24px;
221
+ font-weight: 800;
222
+ line-height: 1.2;
223
+ margin-bottom: 6px;
224
+ }
225
+ .desc, .location {
226
+ font-size: 13px;
227
+ color: #57606a;
228
+ line-height: 1.5;
229
+ }
230
+ .stats {
231
+ display: flex;
232
+ gap: 18px;
233
+ padding: 0 24px 18px;
234
+ font-size: 13px;
235
+ color: #57606a;
236
+ }
237
+ .stats strong {
238
+ color: #1f2328;
239
+ margin-right: 4px;
240
+ }
241
+ .timeline {
242
+ border-top: 1px solid #e6e8eb;
243
+ padding: 8px 0 12px;
244
+ }
245
+ .timeline-title {
246
+ padding: 14px 24px 8px;
247
+ font-size: 15px;
248
+ font-weight: 700;
249
+ }
250
+ .post {
251
+ padding: 16px 24px;
252
+ border-top: 1px solid #f0f2f5;
253
+ }
254
+ .activity-badge {
255
+ display: inline-flex;
256
+ align-items: center;
257
+ gap: 8px;
258
+ margin-bottom: 10px;
259
+ padding: 4px 10px;
260
+ border-radius: 999px;
261
+ font-size: 12px;
262
+ font-weight: 600;
263
+ line-height: 1.4;
264
+ }
265
+ .activity-badge__time {
266
+ font-weight: 500;
267
+ opacity: 0.85;
268
+ }
269
+ .activity-badge--original {
270
+ background: #ecfdf5;
271
+ color: #059669;
272
+ }
273
+ .activity-badge--retweet {
274
+ background: #eff6ff;
275
+ color: #2563eb;
276
+ }
277
+ .activity-badge--like {
278
+ background: #fdf2f8;
279
+ color: #db2777;
280
+ }
281
+ .post-header {
282
+ display: flex;
283
+ gap: 10px;
284
+ align-items: center;
285
+ margin-bottom: 10px;
286
+ }
287
+ .post-avatar {
288
+ width: 40px;
289
+ height: 40px;
290
+ border-radius: 50%;
291
+ object-fit: cover;
292
+ flex-shrink: 0;
293
+ }
294
+ .post-name {
295
+ font-size: 14px;
296
+ font-weight: 700;
297
+ }
298
+ .post-time {
299
+ font-size: 12px;
300
+ color: #8b949e;
301
+ margin-top: 2px;
302
+ }
303
+ .post-text {
304
+ font-size: 15px;
305
+ line-height: 1.7;
306
+ white-space: pre-wrap;
307
+ word-break: break-word;
308
+ margin-bottom: 10px;
309
+ }
310
+ .post-text--comment {
311
+ margin-bottom: 8px;
312
+ }
313
+ .quoted-post {
314
+ margin-bottom: 10px;
315
+ padding: 12px;
316
+ border: 1px solid #e6e8eb;
317
+ border-radius: 12px;
318
+ background: #f8fafc;
319
+ }
320
+ .quoted-post-header {
321
+ display: flex;
322
+ gap: 8px;
323
+ align-items: center;
324
+ margin-bottom: 8px;
325
+ }
326
+ .quoted-post-avatar {
327
+ width: 28px;
328
+ height: 28px;
329
+ border-radius: 50%;
330
+ object-fit: cover;
331
+ flex-shrink: 0;
332
+ }
333
+ .quoted-post-name {
334
+ font-size: 13px;
335
+ font-weight: 700;
336
+ color: #57606a;
337
+ }
338
+ .quoted-post-time {
339
+ font-size: 11px;
340
+ color: #8b949e;
341
+ margin-top: 2px;
342
+ }
343
+ .quoted-post-text {
344
+ font-size: 14px;
345
+ line-height: 1.6;
346
+ color: #1f2328;
347
+ white-space: pre-wrap;
348
+ word-break: break-word;
349
+ }
350
+ .post-media-card {
351
+ position: relative;
352
+ margin-bottom: 10px;
353
+ border-radius: 12px;
354
+ overflow: hidden;
355
+ background: #eef2f6;
356
+ }
357
+ .post-media-cover {
358
+ display: block;
359
+ max-width: 100%;
360
+ width: auto;
361
+ height: auto;
362
+ background: #eef2f6;
363
+ }
364
+ .post-media-title {
365
+ margin-bottom: 10px;
366
+ font-size: 13px;
367
+ color: #57606a;
368
+ line-height: 1.5;
369
+ }
370
+ .post-media-card .post-media-title {
371
+ position: absolute;
372
+ left: 0;
373
+ right: 0;
374
+ bottom: 0;
375
+ margin: 0;
376
+ padding: 10px 12px;
377
+ color: #fff;
378
+ background: linear-gradient(transparent, rgba(0, 0, 0, 0.65));
379
+ }
380
+ .post-images {
381
+ display: grid;
382
+ gap: 6px;
383
+ margin-top: 10px;
384
+ }
385
+ .post-images--single {
386
+ grid-template-columns: 1fr;
387
+ }
388
+ .post-images--grid {
389
+ grid-template-columns: repeat(2, minmax(0, 1fr));
390
+ }
391
+ .post-image {
392
+ display: block;
393
+ max-width: 100%;
394
+ width: auto;
395
+ height: auto;
396
+ border-radius: 8px;
397
+ background: #eef2f6;
398
+ }
399
+ .post-images-more {
400
+ grid-column: 1 / -1;
401
+ font-size: 12px;
402
+ color: #8b949e;
403
+ padding-top: 2px;
404
+ }
405
+ .post-stats {
406
+ display: flex;
407
+ gap: 16px;
408
+ font-size: 12px;
409
+ color: #8b949e;
410
+ }
411
+ .post-comments {
412
+ margin-top: 12px;
413
+ padding-top: 12px;
414
+ border-top: 1px solid #f0f2f5;
415
+ }
416
+ .post-comments__title {
417
+ font-size: 12px;
418
+ font-weight: 700;
419
+ color: #57606a;
420
+ margin-bottom: 8px;
421
+ }
422
+ .comment-item {
423
+ display: flex;
424
+ gap: 8px;
425
+ padding: 8px 0;
426
+ }
427
+ .comment-item + .comment-item {
428
+ border-top: 1px solid #f6f8fa;
429
+ }
430
+ .comment-item--reply {
431
+ padding-top: 6px;
432
+ }
433
+ .comment-replies {
434
+ margin-top: 8px;
435
+ padding-left: 10px;
436
+ border-left: 2px solid #e6e8eb;
437
+ }
438
+ .comment-author {
439
+ font-size: 10px;
440
+ font-weight: 700;
441
+ color: #db2777;
442
+ background: #fdf2f8;
443
+ border-radius: 999px;
444
+ padding: 1px 6px;
445
+ }
446
+ .comment-avatar {
447
+ width: 28px;
448
+ height: 28px;
449
+ border-radius: 50%;
450
+ object-fit: cover;
451
+ flex-shrink: 0;
452
+ }
453
+ .comment-body {
454
+ min-width: 0;
455
+ flex: 1;
456
+ }
457
+ .comment-meta {
458
+ display: flex;
459
+ flex-wrap: wrap;
460
+ gap: 6px;
461
+ align-items: center;
462
+ margin-bottom: 4px;
463
+ font-size: 11px;
464
+ color: #8b949e;
465
+ }
466
+ .comment-name {
467
+ font-weight: 700;
468
+ color: #57606a;
469
+ }
470
+ .comment-likes {
471
+ margin-left: auto;
472
+ }
473
+ .comment-text {
474
+ font-size: 13px;
475
+ line-height: 1.6;
476
+ color: #1f2328;
477
+ white-space: pre-wrap;
478
+ word-break: break-word;
479
+ }
480
+ #weibo-cards {
481
+ display: flex;
482
+ flex-direction: column;
483
+ gap: 24px;
484
+ width: ${constants_1.CONSTANTS.RENDER_CARD_WIDTH}px;
485
+ }
486
+ .weibo-card,
487
+ #weibo-card {
488
+ width: ${constants_1.CONSTANTS.RENDER_CARD_WIDTH}px;
489
+ background: #fff;
490
+ border-radius: 18px;
491
+ overflow: hidden;
492
+ box-shadow: 0 10px 30px rgba(31, 35, 40, 0.08);
493
+ }
494
+ `;
495
+ const wrapTimelinePage = (bodyContent) => `<!DOCTYPE html>
496
+ <html lang="zh-CN">
497
+ <head>
498
+ <meta charset="UTF-8" />
499
+ <style>${TIMELINE_PAGE_STYLES}</style>
500
+ </head>
501
+ <body>
502
+ ${bodyContent}
503
+ </body>
504
+ </html>`;
505
+ const buildTimelineHtml = (profile, normalizedTimeline) => wrapTimelinePage(buildWeiboCardHtml(profile, normalizedTimeline));
506
+ exports.buildTimelineHtml = buildTimelineHtml;
507
+ const buildMultiTimelineHtml = (entries) => wrapTimelinePage(`<div id="weibo-cards">${entries
508
+ .map(({ profile, timeline }) => buildWeiboCardHtml(profile, timeline))
509
+ .join("")}</div>`);
510
+ exports.buildMultiTimelineHtml = buildMultiTimelineHtml;
@@ -0,0 +1,10 @@
1
+ import { Context } from "koishi";
2
+ import type { NormalizedPost } from "../timeline/types";
3
+ import type { ProfileData, ResolvedMediaPost } from "./types";
4
+ export declare const normalizeImageUrl: (url: string) => string;
5
+ /** 用户主页头图:取 cover_image_phone 分号分隔后的第一张 */
6
+ export declare const getProfileCoverUrl: (coverImagePhone?: string) => string;
7
+ export declare const prepareDrawerAssets: (ctx: Context, profile: ProfileData, normalizedTimeline: NormalizedPost[]) => Promise<{
8
+ profile: ProfileData;
9
+ timeline: ResolvedMediaPost[];
10
+ }>;
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepareDrawerAssets = exports.getProfileCoverUrl = exports.normalizeImageUrl = void 0;
4
+ const constants_1 = require("../../util/constants");
5
+ const puppeteer_cookie_1 = require("../../util/puppeteer-cookie");
6
+ const weibo_media_1 = require("../../util/weibo-media");
7
+ const PLACEHOLDER_IMAGE = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PHJlY3Qgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiNlZWVmMiIvPjx0ZXh0IHg9IjUwIiB5PSI1NSIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk4YTNhZiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+8J+OiTwvdGV4dD48L3N2Zz4=";
8
+ const normalizeImageUrl = (url) => {
9
+ if (!url)
10
+ return "";
11
+ if (url.startsWith("//"))
12
+ return `https:${url}`;
13
+ return url;
14
+ };
15
+ exports.normalizeImageUrl = normalizeImageUrl;
16
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
17
+ /** 用户主页头图:取 cover_image_phone 分号分隔后的第一张 */
18
+ const getProfileCoverUrl = (coverImagePhone) => {
19
+ if (!coverImagePhone)
20
+ return "";
21
+ const first = coverImagePhone
22
+ .split(";")
23
+ .map((item) => item.trim())
24
+ .find(Boolean);
25
+ return first ? (0, exports.normalizeImageUrl)(first) : "";
26
+ };
27
+ exports.getProfileCoverUrl = getProfileCoverUrl;
28
+ const takePostImageUrls = (picIds, picInfos, budget) => {
29
+ const total = (0, weibo_media_1.countPostPics)(picIds);
30
+ const limit = Math.min(constants_1.CONSTANTS.MAX_POST_IMAGES, budget.remaining);
31
+ const urls = (0, weibo_media_1.getPostPicUrls)(picIds, picInfos, limit)
32
+ .map((url) => (0, exports.normalizeImageUrl)(url))
33
+ .filter(Boolean);
34
+ budget.remaining -= urls.length;
35
+ return {
36
+ urls,
37
+ hidden: Math.max(total - urls.length, 0),
38
+ };
39
+ };
40
+ const fetchImageAsDataUrl = async (ctx, url, cookieString, referer = "https://weibo.com/") => {
41
+ const normalizedUrl = (0, exports.normalizeImageUrl)(url);
42
+ if (!normalizedUrl || normalizedUrl.startsWith("data:"))
43
+ return normalizedUrl;
44
+ const referers = [referer, "https://weibo.com/", "https://www.weibo.com/"];
45
+ for (const currentReferer of referers) {
46
+ try {
47
+ const response = await ctx.http.get(normalizedUrl, {
48
+ responseType: "arraybuffer",
49
+ headers: {
50
+ ...(cookieString ? { cookie: cookieString } : {}),
51
+ referer: currentReferer,
52
+ "user-agent": constants_1.CONSTANTS.USER_AGENT,
53
+ accept: "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
54
+ },
55
+ });
56
+ const data = response?.data ?? response;
57
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
58
+ const mimeType = String(response?.headers?.["content-type"] || "image/jpeg").split(";")[0];
59
+ if (!buffer.length)
60
+ continue;
61
+ return `data:${mimeType};base64,${buffer.toString("base64")}`;
62
+ }
63
+ catch {
64
+ continue;
65
+ }
66
+ }
67
+ return null;
68
+ };
69
+ const resolveImageUrl = async (ctx, url, cookieString, cache, referer = "https://weibo.com/") => {
70
+ const normalizedUrl = (0, exports.normalizeImageUrl)(url);
71
+ if (!normalizedUrl)
72
+ return "";
73
+ if (cache.has(normalizedUrl))
74
+ return cache.get(normalizedUrl);
75
+ const resolved = (await fetchImageAsDataUrl(ctx, normalizedUrl, cookieString, referer)) ||
76
+ PLACEHOLDER_IMAGE;
77
+ cache.set(normalizedUrl, resolved);
78
+ if (constants_1.CONSTANTS.IMAGE_FETCH_DELAY_MS > 0) {
79
+ await sleep(constants_1.CONSTANTS.IMAGE_FETCH_DELAY_MS);
80
+ }
81
+ return resolved;
82
+ };
83
+ const prepareDrawerAssets = async (ctx, profile, normalizedTimeline) => {
84
+ const cookieString = await (0, puppeteer_cookie_1.loadCookieStringFromDatabase)(ctx);
85
+ const cache = new Map();
86
+ const user = profile?.user || {};
87
+ const avatarUrl = user.avatar_hd || user.avatar_large || user.profile_image_url || "";
88
+ const coverUrl = (0, exports.getProfileCoverUrl)(user.cover_image_phone);
89
+ const profileReferer = user.id || user.idstr
90
+ ? `https://weibo.com/u/${user.id || user.idstr}`
91
+ : "https://weibo.com/";
92
+ const resolvedProfile = {
93
+ user: {
94
+ ...user,
95
+ avatar_hd: await resolveImageUrl(ctx, avatarUrl, cookieString, cache, profileReferer),
96
+ avatar_large: await resolveImageUrl(ctx, user.avatar_large || avatarUrl, cookieString, cache, profileReferer),
97
+ profile_image_url: await resolveImageUrl(ctx, user.profile_image_url || avatarUrl, cookieString, cache, profileReferer),
98
+ cover_image_phone: coverUrl
99
+ ? await resolveImageUrl(ctx, coverUrl, cookieString, cache, profileReferer)
100
+ : "",
101
+ },
102
+ };
103
+ const resolvePostMediaFrom = async (source, budget) => {
104
+ const images = takePostImageUrls(source.pic_ids, source.pic_infos, budget);
105
+ const resolvedImages = [];
106
+ for (const url of images.urls) {
107
+ const image = await resolveImageUrl(ctx, url, cookieString, cache, profileReferer);
108
+ if (image)
109
+ resolvedImages.push(image);
110
+ }
111
+ const coverRaw = (0, weibo_media_1.getPageCoverUrl)(source.page_info);
112
+ const resolvedMediaCover = coverRaw
113
+ ? await resolveImageUrl(ctx, coverRaw, cookieString, cache, profileReferer)
114
+ : undefined;
115
+ return {
116
+ resolvedImages,
117
+ hiddenImageCount: images.hidden,
118
+ resolvedMediaCover,
119
+ };
120
+ };
121
+ const resolvedTimeline = [];
122
+ const imageBudget = {
123
+ remaining: constants_1.CONSTANTS.MAX_TIMELINE_IMAGES,
124
+ };
125
+ for (const post of normalizedTimeline) {
126
+ const postUser = post.user || {};
127
+ const postAvatar = postUser.avatar_large || postUser.profile_image_url || avatarUrl;
128
+ const retweeted = post.retweeted;
129
+ const quotedUser = retweeted?.user || {};
130
+ const quotedAvatar = quotedUser.avatar_large || quotedUser.profile_image_url || avatarUrl;
131
+ const postMedia = await resolvePostMediaFrom(post, imageBudget);
132
+ const postAvatarResolved = await resolveImageUrl(ctx, postAvatar, cookieString, cache, profileReferer);
133
+ const resolveCommentTree = async (comments) => Promise.all(comments.map(async (comment) => {
134
+ const commentUser = comment.user || {};
135
+ const commentAvatar = commentUser.avatar_large ||
136
+ commentUser.profile_image_url ||
137
+ postAvatar;
138
+ const replies = comment.replies?.length
139
+ ? await resolveCommentTree(comment.replies)
140
+ : undefined;
141
+ return {
142
+ ...comment,
143
+ resolvedAvatar: await resolveImageUrl(ctx, commentAvatar, cookieString, cache, profileReferer),
144
+ replies,
145
+ };
146
+ }));
147
+ const resolvedComments = post.comments?.length
148
+ ? await resolveCommentTree(post.comments)
149
+ : undefined;
150
+ resolvedTimeline.push({
151
+ ...post,
152
+ ...postMedia,
153
+ comments: resolvedComments,
154
+ user: {
155
+ ...postUser,
156
+ avatar_large: postAvatarResolved,
157
+ profile_image_url: postAvatarResolved,
158
+ },
159
+ retweeted: retweeted
160
+ ? {
161
+ ...retweeted,
162
+ ...(await resolvePostMediaFrom(retweeted, imageBudget)),
163
+ user: {
164
+ ...quotedUser,
165
+ avatar_large: await resolveImageUrl(ctx, quotedAvatar, cookieString, cache, profileReferer),
166
+ profile_image_url: await resolveImageUrl(ctx, quotedUser.profile_image_url || quotedAvatar, cookieString, cache, profileReferer),
167
+ },
168
+ }
169
+ : undefined,
170
+ });
171
+ }
172
+ return { profile: resolvedProfile, timeline: resolvedTimeline };
173
+ };
174
+ exports.prepareDrawerAssets = prepareDrawerAssets;
@@ -0,0 +1,4 @@
1
+ export { drawTimeline, drawTimelines, drawEntryImages } from "./screenshot";
2
+ export { buildTimelineHtml, buildMultiTimelineHtml } from "./html-builder";
3
+ export { prepareDrawerAssets } from "./image-resolver";
4
+ export type { ProfileData, ResolvedMediaPost, TimelineEntry, } from "./types";