yz-yuki-plugin 2.0.8-3 → 2.0.8-5

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 CHANGED
@@ -1,4 +1,5 @@
1
1
  # 2.0.8
2
+ * 添加微博登录功能(待续)
2
3
  * 更新B站动态标题获取
3
4
  * 合并分支dev与dev3,减少维护成本
4
5
 
package/README.md CHANGED
@@ -186,7 +186,7 @@ https://m.weibo.cn/u/7643376782 # 7643376782 为崩坏星穹铁道博主uid
186
186
  | 扫码B站登录 | app扫码获取登录ck | `#扫码B站登录` |
187
187
  | 取消B站登录 | 删除扫码获取的B站CK | `#取消B站登陆` |
188
188
  | 查看B站登录信息 | 查看app扫码登录的信息和状态 | `#我的B站登录` |
189
- | 绑定B站ck | 配置手动本地获取的B站CK,仅限私聊/私信,权限:Master | `#绑定B本地站ck: xxx` |
189
+ | 绑定B站ck | 配置手动本地获取的B站CK,仅限私聊/私信,权限:Master | `#绑定B站本地ck: xxx` |
190
190
  | 删除B站ck | 删除手动获取的B站cookie,权限:Master | `#删除B站本地ck` |
191
191
  | 查看B站ck | 查看当前启用的B站ck,仅限私聊 | `#我的B站ck` |
192
192
  | 刷新B站临时ck | 重新获取并刷新redis缓存的未绑定自己的B站ck而自动获取的 临时B站cookie | `#刷新B站临时ck` |
@@ -200,6 +200,12 @@ https://m.weibo.cn/u/7643376782 # 7643376782 为崩坏星穹铁道博主uid
200
200
  | 手动推送微博订阅 | 手动触发定时推送任务,权限:Bot的Master | `#执行微博任务` |
201
201
  | 查看博主信息 | 通过uid查看博主信息 | `#微博博主 uid` |
202
202
  | 搜索微博博主 | 根据关键词在微博搜索大V博主的信息 | `#搜索微博博主 xxx` |
203
+ | 扫码微博登录(待完成) | app扫码获取登录ck | `#扫码微博登录` |
204
+ | 取消微博登录 | 删除扫码获取的微博CK | `#取消微博登陆` |
205
+ | 查看微博登录信息 | 查看app扫码登录的信息和状态 | `#我的微博登录` |
206
+ | 绑定微博ck | 配置手动本地获取的微博CK,仅限私聊/私信,权限:Master | `#绑定微博本地ck: xxx` |
207
+ | 删除微博ck | 删除手动获取的微博cookie,权限:Master | `#删除微博本地ck` |
208
+ | 查看微博ck | 查看当前启用的微博ck,仅限私聊 | `#我的微博ck` |
203
209
  ||||
204
210
  | **其他指令** | | |
205
211
  | 查看版本信息 | 查看版本信息 | `#优纪版本` |
@@ -50,6 +50,9 @@
50
50
  - icon: wondrous_lovely_flower
51
51
  title: '#刷新B站临时ck'
52
52
  desc: '刷新自动获取的临时B站cookie'
53
+ - icon: romaritime_flower
54
+ title: 'B站视频链接解析'
55
+ desc: '直接发送B站视频链接即可'
53
56
  - group: 微博功能
54
57
  list:
55
58
  - icon: diagram
@@ -73,6 +76,24 @@
73
76
  - icon: shell
74
77
  title: '#搜索微博博主原神'
75
78
  desc: '根据关键词在微博搜索大V博主的信息'
79
+ - icon: 雷神瞳共鸣石
80
+ title: '#扫码微博登录'
81
+ desc: 'app扫码获取登录ck(待完成)'
82
+ - icon: everamber
83
+ title: '#取消微博登陆'
84
+ desc: '删除扫码获取的微博CK'
85
+ - icon: condessence_crystal
86
+ title: '#我的微博登录'
87
+ desc: '查看app扫码登录的信息和状态'
88
+ - icon: romaritime_flower
89
+ title: '#绑定微博本地ck: ***'
90
+ desc: '配置本地获取的微博CK'
91
+ - icon: unfading_silky_grace
92
+ title: '#删除微博本地ck'
93
+ desc: '删除手动获取的微博ck'
94
+ - icon: delightful_encounter
95
+ title: '#我的微博ck'
96
+ desc: '查看当前启用的微博ck,仅限私聊'
76
97
  - group: 其他指令
77
98
  list:
78
99
  - icon: 风车
@@ -297,7 +297,7 @@ message.use(async (e) => {
297
297
  e.reply('未取得bot主人身份,无权限删除B站登录ck');
298
298
  }
299
299
  }, [/^(#|\/)(yuki|优纪)?(取消|删除|del|DEL)(b站|B站|bili|bilibili|哔哩|哔哩哔哩)本地(ck|CK|cookie|COOKIE)$/]);
300
- /** 当前正在使用的B站ck */
300
+ /** 当前正在使用的本地B站ck */
301
301
  message.use(async (e) => {
302
302
  if (e.isGroup) {
303
303
  await e.reply('注意账号安全,请私聊查看叭');
@@ -325,7 +325,7 @@ message.use(async (e) => {
325
325
  e.reply('未取得bot主人身份,无权限查看当前使用的B站cookie');
326
326
  }
327
327
  }
328
- }, [/^(#|\/)(yuki|优纪)?(取消|删除|del|DEL)(b站|B站|bili|bilibili|哔哩|哔哩哔哩)本地(ck|CK|cookie|COOKIE)$/]);
328
+ }, [/^(#|\/)(yuki|优纪)?我的(b站|B站|bili|bilibili|哔哩|哔哩哔哩)(ck|CK|cookie|COOKIE)$/]);
329
329
  /** 删除并刷新redis缓存的临时B站ck */
330
330
  message.use(async (e) => {
331
331
  try {
package/lib/apps/weibo.js CHANGED
@@ -1,8 +1,10 @@
1
- import { Messages } from 'yunzaijs';
1
+ import { WeiboWebDataFetcher } from '../models/weibo/weibo.main.get.web.data.js';
2
+ import { WeiboMainModels } from '../models/weibo/weibo.main.models.js';
2
3
  import { WeiboQuery } from '../models/weibo/weibo.main.query.js';
3
4
  import { WeiboTask } from '../models/weibo/weibo.main.task.js';
4
5
  import Config from '../utils/config.js';
5
- import { WeiboWebDataFetcher } from '../models/weibo/weibo.main.get.web.data.js';
6
+ import lodash from 'lodash';
7
+ import { Messages, Redis } from 'yunzaijs';
6
8
 
7
9
  const message = new Messages('message');
8
10
  let weiboPushData = Config.getConfigData('config', 'weibo', 'push');
@@ -124,6 +126,119 @@ message.use(async (e) => {
124
126
  e.reply(`${isDel ? '删除' : '修改'}微博推送成功~\n${uid}`);
125
127
  }
126
128
  }, [/^(#|\/)(yuki|优纪)?(取消|删除|del|DEL)(微博|weibo|WEIBO)推送\s*(视频\s*|图文\s*|文章\s*|转发\s*)*.*$/]);
129
+ /** 扫码登录微博 */
130
+ message.use(async (e) => {
131
+ if (!e.isMaster) {
132
+ e.reply('未取得bot主人身份,无权限配置微博登录ck');
133
+ }
134
+ else {
135
+ const LoginCk = await WeiboMainModels.readLoginCookie();
136
+ if (LoginCk) {
137
+ e.reply(`当前已有微博登录ck,请勿重复扫码!\n如需更换,请先删除当前登录再扫码:\n#yuki删除微博登录`);
138
+ }
139
+ else {
140
+ try {
141
+ const tokenKey = await WeiboMainModels.applyLoginQRCode(e);
142
+ if (tokenKey && tokenKey.rid) {
143
+ let weiboLoginCk = await WeiboMainModels.pollLoginQRCode(e, tokenKey.qrid, tokenKey.rid, tokenKey.X_CSRF_TOKEN);
144
+ if (weiboLoginCk) {
145
+ if (lodash.trim(weiboLoginCk).length != 0) {
146
+ await WeiboMainModels.saveLoginCookie(e, weiboLoginCk);
147
+ e.reply(`get weibo LoginCk:成功!`);
148
+ }
149
+ else {
150
+ e.reply(`get weibo LoginCk:失败X﹏X`);
151
+ }
152
+ }
153
+ }
154
+ }
155
+ catch (Error) {
156
+ global?.logger?.info(`yuki-plugin Login weibo Failed:${Error}`);
157
+ }
158
+ }
159
+ }
160
+ }, [/^(#|\/)(yuki|优纪)?(扫码|添加|ADD|add)(微博|weibo|WEIBO)登录$/]);
161
+ /** 删除登陆的微博ck */
162
+ message.use(async (e) => {
163
+ if (e.isMaster) {
164
+ await Redis.set('Yz:yuki:weibo:loginCookie', '', { EX: 3600 * 24 * 180 });
165
+ e.reply(`扫码登陆的微博cookie已删除~`);
166
+ }
167
+ else {
168
+ e.reply('未取得bot主人身份,无权限删除微博登录ck');
169
+ }
170
+ }, [/^(#|\/)(yuki|优纪)?(取消|删除|del|DEL)(微博|weibo|WEIBO)登录$/]);
171
+ /** 显示我的微博登录信息 */
172
+ message.use(async (e) => {
173
+ if (e.isMaster) {
174
+ await WeiboMainModels.checkWeiboLogin(e);
175
+ }
176
+ else {
177
+ e.reply('未取得bot主人身份,无权限查看微博登录状态');
178
+ }
179
+ }, [/^(#|\/)(yuki|优纪)?我的(微博|weibo|WEIBO)登录$/]);
180
+ /** 手动绑定本地获取的微博cookie */
181
+ message.use(async (e) => {
182
+ if (e.isMaster) {
183
+ if (e.isPrivate) {
184
+ await e.reply('请注意账号安全,请手动撤回发送的cookie,并私聊进行添加绑定!');
185
+ }
186
+ else {
187
+ let localBiliCookie = e.msg.replace(/^(#|\/)(yuki|优纪)?(绑定|添加|ADD|add)(微博|weibo|WEIBO)(ck|CK|cookie|COOKIE)(:|:)?/g, '').trim();
188
+ const XSRF_TOKEN = await WeiboMainModels.readSavedCookieItems(localBiliCookie, ['XSRF-TOKEN'], false);
189
+ if (XSRF_TOKEN) {
190
+ //筛选ck
191
+ localBiliCookie = await WeiboMainModels.readSavedCookieItems(localBiliCookie, ['XSRF-TOKEN', 'SUB', 'SUBP', 'SRF', 'SCF', 'SRT', ' _T_WM', 'M_WEIBOCN_PARAMS', 'SSOLoginState', 'ALF'], false);
192
+ await WeiboMainModels.saveLocalBiliCk(localBiliCookie);
193
+ logger.mark(`${e.logFnc} 保存微博cookie成功 [XSRF_TOKEN: ${XSRF_TOKEN}]`);
194
+ let uidMsg = [`好耶~绑定微博cookie成功:\nXSRF_TOKEN: ${XSRF_TOKEN}`];
195
+ await e.reply(uidMsg);
196
+ }
197
+ else {
198
+ e.reply('绑定的微博cookie无效,请检查后重新添加!');
199
+ return false;
200
+ }
201
+ }
202
+ }
203
+ else {
204
+ e.reply('未取得bot主人身份,无权限配置B站登录ck');
205
+ }
206
+ }, [/^^(#|\/)(yuki|优纪)?(绑定|添加|ADD|add)(微博|weibo|WEIBO)本地(ck|CK|cookie|COOKIE)(:|:)?.*$/]);
207
+ /** 删除绑定的本地微博ck */
208
+ message.use(async (e) => {
209
+ if (e.isMaster) {
210
+ await WeiboMainModels.saveLocalBiliCk('');
211
+ await e.reply(`手动绑定的微博ck已删除~`);
212
+ }
213
+ else {
214
+ e.reply('未取得bot主人身份,无权限删除B站登录ck');
215
+ }
216
+ }, [/^(#|\/)(yuki|优纪)?(取消|删除|del|DEL)(微博|weibo|WEIBO)本地(ck|CK|cookie|COOKIE)$/]);
217
+ /** 查看当前正在使用的本地微博ck */
218
+ message.use(async (e) => {
219
+ if (e.isGroup) {
220
+ await e.reply('注意账号安全,请私聊查看叭');
221
+ }
222
+ else {
223
+ if (e.isMaster) {
224
+ let { cookie, mark } = await WeiboMainModels.readSyncCookie();
225
+ if (mark === 'localCk') {
226
+ e.reply(`当前使用本地获取的微博cookie:`);
227
+ e.reply(`${cookie}`);
228
+ }
229
+ else if (mark === 'loginCk') {
230
+ e.reply(`当前使用扫码登录的微博cookie:`);
231
+ e.reply(`${cookie}`);
232
+ }
233
+ else if (mark == 'ckIsEmpty') {
234
+ e.reply(`当前无可使用的微博cookie。`);
235
+ }
236
+ }
237
+ else {
238
+ e.reply('未取得bot主人身份,无权限查看当前使用的B站cookie');
239
+ }
240
+ }
241
+ }, [/^(#|\/)(yuki|优纪)?我的(微博|weibo|WEIBO)(ck|CK|cookie|COOKIE)$/]);
127
242
  /** 订阅的全部微博推送列表 */
128
243
  message.use(async (e) => {
129
244
  if (!e.isMaster) {
@@ -155,17 +270,19 @@ message.use(async (e) => {
155
270
  messages.push('\n>>>>>>群组微博订阅<<<<<<');
156
271
  Object.keys(subData.group).forEach(groupId => {
157
272
  messages.push(`\n<群组${groupId}>:`);
158
- subData.group[groupId].forEach((item) => {
159
- const types = new Set();
160
- if (item.type && item.type.length) {
161
- item.type.forEach((typeItem) => {
162
- if (typeMap[typeItem]) {
163
- types.add(typeMap[typeItem]);
164
- }
165
- });
166
- }
167
- messages.push(`${item.uid}:${item.name} ${types.size ? `[${Array.from(types).join('、')}]` : ' [全部动态]'}`);
168
- });
273
+ if (subData.group) {
274
+ subData.group[groupId].forEach((item) => {
275
+ const types = new Set();
276
+ if (item.type && item.type.length) {
277
+ item.type.forEach((typeItem) => {
278
+ if (typeMap[typeItem]) {
279
+ types.add(typeMap[typeItem]);
280
+ }
281
+ });
282
+ }
283
+ messages.push(`${item.uid}:${item.name} ${types.size ? `[${Array.from(types).join('、')}]` : ' [全部动态]'}`);
284
+ });
285
+ }
169
286
  });
170
287
  }
171
288
  else {
@@ -176,17 +293,19 @@ message.use(async (e) => {
176
293
  messages.push('\n>>>>>>私聊微博订阅<<<<<<');
177
294
  Object.keys(subData.private).forEach(userId => {
178
295
  messages.push(`\n<用户${userId}>:`);
179
- subData.private[userId].forEach((item) => {
180
- const types = new Set();
181
- if (item.type && item.type.length) {
182
- item.type.forEach((typeItem) => {
183
- if (typeMap[typeItem]) {
184
- types.add(typeMap[typeItem]);
185
- }
186
- });
187
- }
188
- messages.push(`${item.uid}:${item.name} ${types.size ? `[${Array.from(types).join('、')}]` : ' [全部动态]'}`);
189
- });
296
+ if (subData.private) {
297
+ subData.private[userId].forEach((item) => {
298
+ const types = new Set();
299
+ if (item.type && item.type.length) {
300
+ item.type.forEach((typeItem) => {
301
+ if (typeMap[typeItem]) {
302
+ types.add(typeMap[typeItem]);
303
+ }
304
+ });
305
+ }
306
+ messages.push(`${item.uid}:${item.name} ${types.size ? `[${Array.from(types).join('、')}]` : ' [全部动态]'}`);
307
+ });
308
+ }
190
309
  });
191
310
  }
192
311
  else {
@@ -11,7 +11,9 @@ function App({ data }) {
11
11
  React.createElement("div", { className: "txt-0 text-center mt-3 mb-3 p-1 text-blue-500" },
12
12
  "Created By yuki-plugin",
13
13
  React.createElement("br", null),
14
- "\u626B\u7801\u767B\u5F55B\u7AD9\u83B7\u53D6CK"),
14
+ "\u626B\u7801\u767B\u5F55",
15
+ data.name,
16
+ "\u83B7\u53D6CK"),
15
17
  React.createElement("div", { className: "QrCode m-auto" },
16
18
  React.createElement("img", { className: "qr-code w-72 h-72 ml-7", src: data.url, alt: "\u4E8C\u7EF4\u7801" })),
17
19
  React.createElement("div", { className: "txt-1 text-center mt-3 mb-3 p-1 text-red-600" },
@@ -1,3 +1,8 @@
1
+ import BiliApi from './bilibili.main.api.js';
2
+ import { gen_buvid_fp } from './bilibili.risk.buid.fp.js';
3
+ import { getBiliTicket } from './bilibili.risk.ticket.js';
4
+ import { renderPage } from '../../utils/image.js';
5
+ import { _paths } from '../../utils/paths.js';
1
6
  import axios from 'axios';
2
7
  import fs__default from 'fs';
3
8
  import lodash from 'lodash';
@@ -7,11 +12,6 @@ import path from 'path';
7
12
  import QRCode from 'qrcode';
8
13
  import YAML from 'yaml';
9
14
  import { Redis, Segment } from 'yunzaijs';
10
- import { renderPage } from '../../utils/image.js';
11
- import { _paths } from '../../utils/paths.js';
12
- import BiliApi from './bilibili.main.api.js';
13
- import { gen_buvid_fp } from './bilibili.risk.buid.fp.js';
14
- import { getBiliTicket } from './bilibili.risk.ticket.js';
15
15
 
16
16
  /**
17
17
  * *******************************************************************
@@ -35,7 +35,7 @@ async function applyLoginQRCode(e) {
35
35
  const qrcodeUrl = res?.data?.url;
36
36
  let loginUrlQrcodeData = await QRCode.toDataURL(`${qrcodeUrl}`);
37
37
  const LoginPropsData = {
38
- data: { url: loginUrlQrcodeData }
38
+ data: { name: 'B站', url: loginUrlQrcodeData }
39
39
  };
40
40
  const ScreenshotOptionsData = {
41
41
  saveHtmlfile: false,
@@ -251,7 +251,7 @@ async function readLocalBiliCk() {
251
251
  if (!fs__default.existsSync(dir)) {
252
252
  fs__default.mkdirSync(dir, { recursive: true }); // 创建目录,包括父目录
253
253
  }
254
- const files = fs__default.readdirSync(dir).filter((file) => file.endsWith('.yaml'));
254
+ const files = fs__default.readdirSync(dir).filter((file) => file.endsWith('biliCookie.yaml'));
255
255
  const readFile = promisify(fs__default.readFile);
256
256
  const promises = files.map((file) => readFile(path.join(dir, file), 'utf8'));
257
257
  const contents = await Promise.all(promises);
@@ -30,14 +30,121 @@ class WeiboApi {
30
30
  /**统一设置header */
31
31
  get WEIBO_HEADERS() {
32
32
  return {
33
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
34
- 'Accept-language': 'zh-CN,zh;q=0.9',
35
- 'Authority': 'm.weibo.cn',
36
- 'Cache-control': 'max-age=0',
37
- 'Sec-fetch-dest': 'empty',
38
- 'Sec-fetch-mode': 'same-origin',
39
- 'Sec-fetch-site': 'same-origin',
40
- 'Upgrade-insecure-requests': '1',
33
+ 'Accept': 'application/json, text/plain, */*',
34
+ 'Accept-Language': 'zh-CN,en-US;q=0.5',
35
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
36
+ 'X-Requested-With': 'XMLHttpRequest',
37
+ 'MWeibo-Pwa': '1',
38
+ 'X-XSRF-TOKEN': '',
39
+ 'Sec-Fetch-Dest': 'empty',
40
+ 'Sec-Fetch-Mode': 'cors',
41
+ 'Sec-Fetch-Site': 'same-origin',
42
+ 'Connection': 'keep-alive',
43
+ 'User-agent': this.USER_AGENT
44
+ };
45
+ }
46
+ get WEIBO_GET_X_CSRF_TOKEN_HEADERS() {
47
+ return {
48
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
49
+ 'Accept-Language': 'zh-CN,en-US;q=0.5',
50
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
51
+ 'Sec-GPC': '1',
52
+ 'Connection': 'keep-alive',
53
+ 'Upgrade-Insecure-Requests': '1',
54
+ 'Sec-Fetch-Dest': 'document',
55
+ 'Sec-Fetch-Mode': 'navigate',
56
+ 'Sec-Fetch-Site': 'none',
57
+ 'Sec-Fetch-User': '?1',
58
+ 'Priority': 'u=0, i',
59
+ 'User-agent': this.USER_AGENT
60
+ };
61
+ }
62
+ get WEIBO_LOGIN_QR_CODE_HEADERS() {
63
+ return {
64
+ 'Accept': `application/json, text/plain, */*`,
65
+ 'Accept-Language': 'zh-CN,en-US;q=0.5',
66
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
67
+ 'X-Requested-With': 'XMLHttpRequest',
68
+ 'X-CSRF-TOKEN': '',
69
+ 'Sec-GPC': '1',
70
+ 'Connection': 'keep-alive',
71
+ 'Referer': 'https://passport.weibo.com/sso/signin?entry=wapsso&source=wapssowb&url=https%3A%2F%2Fm.weibo.cn%2F',
72
+ 'Cookie': '',
73
+ 'Sec-Fetch-Dest': 'empty',
74
+ 'Sec-Fetch-Mode': 'cors',
75
+ 'Sec-Fetch-Site': 'same-origin',
76
+ 'Priority': 'u=0',
77
+ 'User-agent': this.USER_AGENT
78
+ };
79
+ }
80
+ get WEIBO_LOGIN_QR_CODE_IMAGE_HEADERS() {
81
+ return {
82
+ 'Accept': `image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5`,
83
+ 'Accept-Language': 'zh-CN,en-US;q=0.5',
84
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
85
+ 'Sec-GPC': '1',
86
+ 'Connection': 'keep-alive',
87
+ 'Referer': 'https://passport.weibo.com/',
88
+ 'Cookie': '',
89
+ 'Sec-Fetch-Dest': 'Image',
90
+ 'Sec-Fetch-Mode': 'no-cors',
91
+ 'Sec-Fetch-Site': 'cross-site',
92
+ 'Priority': 'u=4, i',
93
+ 'User-agent': this.USER_AGENT
94
+ };
95
+ }
96
+ get WEIBO_GET_BD_TOKEN_HEADERS() {
97
+ return {
98
+ 'Accept': '*/*',
99
+ 'Accept-Language': 'zh-CN,en-US;q=0.5',
100
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
101
+ 'Content-Type': 'application/x-www-form-urlencoded',
102
+ 'Host': 'passport.weibo.com',
103
+ 'Origin': 'https://passport.weibo.com',
104
+ 'Priority': 'u=4',
105
+ 'Referer': 'https://passport.weibo.com/sso/signin?entry=wapsso&source=wapsso&url=https%3A%2F%2Fm.weibo.cn%2F',
106
+ 'Connection': 'keep-alive',
107
+ 'Cookie': '',
108
+ 'Sec-Fetch-Dest': 'empty',
109
+ 'Sec-Fetch-Mode': 'no-cors',
110
+ 'Sec-Fetch-Site': 'same-origin',
111
+ 'Sec-GPC': '1',
112
+ 'TE': 'trailers',
113
+ 'User-Agent': this.USER_AGENT
114
+ };
115
+ }
116
+ get WEIBO_POLL_LOGIN_STATUS_HEADERS() {
117
+ return {
118
+ 'Accept': 'application/json, text/plain, */*',
119
+ 'Accept-Language': 'zh-CN,en-US;q=0.5',
120
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
121
+ 'X-Requested-With': 'XMLHttpRequest',
122
+ 'X-CSRF-TOKEN': '',
123
+ 'Sec-GPC': '1',
124
+ 'Connection': 'keep-alive',
125
+ 'Referer': 'https://passport.weibo.com/sso/signin?entry=wapsso&source=wapssowb&url=https%3A%2F%2Fm.weibo.cn%2F',
126
+ 'Cookie': '',
127
+ 'Sec-Fetch-Dest': 'empty',
128
+ 'Sec-Fetch-Mode': 'cors',
129
+ 'Sec-Fetch-Site': 'same-origin',
130
+ 'TE': 'trailers',
131
+ 'User-agent': this.USER_AGENT
132
+ };
133
+ }
134
+ get WEIBO_COOKIE_HEADERS() {
135
+ return {
136
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
137
+ 'Accept-Language': 'zh-CN,en-US;q=0.5',
138
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
139
+ 'Connection': 'keep-alive',
140
+ 'Referer': 'https://passport.weibo.com/sso/signin?entry=wapsso&source=wapsso&url=https%3A%2F%2Fm.weibo.cn%2F%3Fjumpfrom%3Dweibocom',
141
+ 'Cookie': '',
142
+ 'Upgrade-Insecure-Requests': '1',
143
+ 'Sec-Fetch-Dest': 'document',
144
+ 'Sec-Fetch-Mode': 'navigate',
145
+ 'Sec-Fetch-Site': 'same-origin',
146
+ 'Priority': 'u=0, i',
147
+ 'TE': 'trailers',
41
148
  'User-agent': this.USER_AGENT
42
149
  };
43
150
  }
@@ -1,7 +1,19 @@
1
- import axios from 'axios';
2
1
  import WeiboApi from './weibo.main.api.js';
2
+ import { WeiboMainModels } from './weibo.main.models.js';
3
3
  import { WeiboQuery } from './weibo.main.query.js';
4
+ import axios from 'axios';
5
+ import { randomInt } from 'crypto';
6
+ import lodash from 'lodash';
4
7
 
8
+ function generateRandomString(length = 6) {
9
+ const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
10
+ let result = '';
11
+ for (let i = 0; i < length; i++) {
12
+ const randomIndex = randomInt(0, characters.length);
13
+ result += characters[randomIndex];
14
+ }
15
+ return result;
16
+ }
5
17
  class WeiboWebDataFetcher {
6
18
  e;
7
19
  constructor(e) { }
@@ -10,25 +22,55 @@ class WeiboWebDataFetcher {
10
22
  const param = { containerid: '100505' + target };
11
23
  const url = new URL(WeiboApi.WEIBO_API.weiboGetIndex);
12
24
  url.search = new URLSearchParams(param).toString();
25
+ const { cookie, mark } = await WeiboMainModels.readSyncCookie();
26
+ let COOKIE = '', XSRF_TOKEN = '';
27
+ if (String(cookie).includes('XSRF-TOKEN')) {
28
+ COOKIE = cookie;
29
+ XSRF_TOKEN = await WeiboMainModels.readSavedCookieItems(cookie, ['XSRF-TOKEN'], false);
30
+ }
31
+ else {
32
+ logger.warn(`优纪插件:获取博主信息,未登录,如获取报错或失败请先 #扫码微博登录`);
33
+ XSRF_TOKEN = generateRandomString();
34
+ COOKIE = `XSRF_TOKEN=${XSRF_TOKEN}`;
35
+ }
13
36
  const resp = await axios(url.toString(), {
14
37
  method: 'GET',
15
38
  timeout: 10000,
16
- headers: { 'accept': '*/*', 'Content-Type': 'application/json', 'referer': 'https://m.weibo.cn' }
39
+ headers: lodash.merge(WeiboApi.WEIBO_HEADERS, { 'X-XSRF-TOKEN': `${XSRF_TOKEN}`, 'Cookie': `${COOKIE}`, 'referer': 'https://m.weibo.cn' })
17
40
  });
41
+ // 处理 Set-Cookie 响应头
42
+ if (resp.headers['set-cookie']) {
43
+ await WeiboMainModels.updateCookieWithSetCookie(resp.headers['set-cookie'], mark);
44
+ }
18
45
  return resp;
19
46
  }
20
47
  /**通过关键词搜索微博大v */
21
48
  async searchBloggerInfo(keyword) {
22
49
  const url = WeiboApi.WEIBO_API.weiboAjaxSearch;
50
+ const { cookie, mark } = await WeiboMainModels.readSyncCookie();
51
+ let COOKIE = '', XSRF_TOKEN = '';
23
52
  const params = {
24
53
  q: keyword
25
54
  };
55
+ if (String(cookie).includes('XSRF-TOKEN')) {
56
+ COOKIE = cookie;
57
+ XSRF_TOKEN = await WeiboMainModels.readSavedCookieItems(cookie, ['XSRF-TOKEN'], false);
58
+ }
59
+ else {
60
+ logger.warn(`优纪插件:搜索微博大V,未登录,如获取报错或失败请先 #扫码微博登录`);
61
+ XSRF_TOKEN = generateRandomString();
62
+ COOKIE = `XSRF_TOKEN=${XSRF_TOKEN}`;
63
+ }
26
64
  const resp = await axios(url, {
27
65
  method: 'GET',
28
66
  params,
29
67
  timeout: 10000,
30
- headers: { 'accept': '*/*', 'Content-Type': 'application/json', 'referer': 'https://s.weibo.com' }
68
+ headers: lodash.merge(WeiboApi.WEIBO_HEADERS, { 'X-XSRF-TOKEN': `${XSRF_TOKEN}`, 'Cookie': `${COOKIE}`, 'referer': 'https://m.weibo.cn' })
31
69
  });
70
+ // 处理 Set-Cookie 响应头
71
+ if (resp.headers['set-cookie']) {
72
+ await WeiboMainModels.updateCookieWithSetCookie(resp.headers['set-cookie'], mark);
73
+ }
32
74
  return resp;
33
75
  }
34
76
  /**获取主页动态资源相关数组 */
@@ -37,12 +79,27 @@ class WeiboWebDataFetcher {
37
79
  const url = new URL(WeiboApi.WEIBO_API.weiboGetIndex);
38
80
  url.search = new URLSearchParams(params).toString();
39
81
  await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * (6500 - 1000 + 1) + 1000)));
82
+ const { cookie, mark } = await WeiboMainModels.readSyncCookie();
83
+ let COOKIE = '', XSRF_TOKEN = '';
84
+ if (String(cookie).includes('XSRF-TOKEN')) {
85
+ COOKIE = cookie;
86
+ XSRF_TOKEN = await WeiboMainModels.readSavedCookieItems(cookie, ['XSRF-TOKEN'], false);
87
+ }
88
+ else {
89
+ logger.warn(`优纪插件:获取主页动态资源,未登录,如获取报错或失败请先 #扫码微博登录`);
90
+ XSRF_TOKEN = generateRandomString();
91
+ COOKIE = `XSRF_TOKEN=${XSRF_TOKEN}`;
92
+ }
40
93
  try {
41
94
  const response = await axios(url.toString(), {
42
95
  method: 'GET',
43
96
  timeout: 10000,
44
- headers: { 'accept': '*/*', 'Content-Type': 'application/json', 'referer': 'https://m.weibo.cn' }
97
+ headers: lodash.merge(WeiboApi.WEIBO_HEADERS, { 'X-XSRF-TOKEN': `${XSRF_TOKEN}`, 'Cookie': `${COOKIE}`, 'referer': 'https://m.weibo.cn' })
45
98
  });
99
+ // 处理 Set-Cookie 响应头
100
+ if (response.headers['set-cookie']) {
101
+ await WeiboMainModels.updateCookieWithSetCookie(response.headers['set-cookie'], mark);
102
+ }
46
103
  const { ok, data, msg } = response?.data;
47
104
  if (!ok && msg !== '这里还没有内容') {
48
105
  throw new Error(response?.config.url);
@@ -0,0 +1,347 @@
1
+ import WeiboApi from './weibo.main.api.js';
2
+ import { getRidFromBd } from './weibo.risk.bd.rid.js';
3
+ import { parse, serialize } from 'cookie';
4
+ import lodash from 'lodash';
5
+ import moment from 'moment';
6
+ import fetch from 'node-fetch';
7
+ import { Segment, Redis } from 'yunzaijs';
8
+
9
+ class WeiboMainModels {
10
+ /**
11
+ * *******************************************************************
12
+ * Login 相关
13
+ * *******************************************************************
14
+ */
15
+ /**申请登陆二维码(web端) */
16
+ static async applyLoginQRCode(e) {
17
+ const response = await fetch('https://passport.weibo.com/sso/signin?entry=wapsso&source=wapssowb&url=https://m.weibo.cn/', {
18
+ method: 'GET',
19
+ headers: lodash.merge(WeiboApi.WEIBO_GET_X_CSRF_TOKEN_HEADERS, { Host: 'passport.weibo.com' }),
20
+ redirect: 'follow'
21
+ });
22
+ const setCookie = response.headers.get('set-cookie');
23
+ const tokenMatch = setCookie?.match(/X-CSRF-TOKEN=([^;]+)/);
24
+ const X_CSRF_TOKEN = tokenMatch ? tokenMatch[1].replace(/X-CSRF-TOKEN=/g, '') : null;
25
+ if (X_CSRF_TOKEN) {
26
+ const resData = (await fetch('https://passport.weibo.com/sso/v2/qrcode/image?entry=wapsso&size=180', {
27
+ method: 'GET',
28
+ headers: lodash.merge(WeiboApi.WEIBO_LOGIN_QR_CODE_HEADERS, {
29
+ 'Host': 'passport.weibo.com',
30
+ 'X-CSRF-TOKEN': X_CSRF_TOKEN,
31
+ 'Cookie': `X-CSRF-TOKEN=${X_CSRF_TOKEN}`
32
+ }),
33
+ redirect: 'follow'
34
+ }).then(res => res.json()));
35
+ if (resData?.retcode === 20000000) {
36
+ const qrid = resData?.data?.qrid;
37
+ const qrcodeUrl = resData?.data?.image;
38
+ let msg = [];
39
+ if (qrid && qrcodeUrl) {
40
+ const imgResponse = await fetch(qrcodeUrl, {
41
+ method: 'GET',
42
+ headers: WeiboApi.WEIBO_LOGIN_QR_CODE_IMAGE_HEADERS,
43
+ redirect: 'follow'
44
+ });
45
+ if (!imgResponse.ok) {
46
+ logger.error(`获取微博登录二维码失败: ${imgResponse.status}`);
47
+ throw new Error(`获取微博登录图片失败,状态码: ${imgResponse.status}`);
48
+ }
49
+ // 等待3秒再获取rid
50
+ await new Promise(resolve => setTimeout(resolve, 3000));
51
+ const ridData = await getRidFromBd(X_CSRF_TOKEN);
52
+ if (ridData?.retcode === 20000000) {
53
+ const rid = ridData?.data?.rid;
54
+ const arrayBuffer = await imgResponse.arrayBuffer();
55
+ msg.push(Segment.image(Buffer.from(arrayBuffer)));
56
+ e.reply('请在3分钟内扫码以完成微博登陆绑定');
57
+ e.reply(msg);
58
+ logger.info(`优纪插件: 如果发送二维码图片消息失败可复制如下URL, 浏览器访问此URL查看二维码并扫码`);
59
+ logger.info(`优纪插件: 微博登陆二维码URL: ${qrcodeUrl}`);
60
+ return { qrid, rid, X_CSRF_TOKEN };
61
+ }
62
+ else {
63
+ logger.error('微博登录:获取rid失败');
64
+ e.reply(`获取微博登录rid密钥失败: ${JSON.stringify(ridData)},\n接口逆向进度受阻,未完成,无法登录。\n无妨,已切换启用临时ck`);
65
+ throw new Error(`获取微博登录rid密钥失败: ${JSON.stringify(ridData)},\n接口逆向进度受阻,未完成,无法登录。\n无妨,已切换启用临时ck`);
66
+ }
67
+ }
68
+ }
69
+ else {
70
+ e.reply(`获取微博登录二维码失败: ${JSON.stringify(resData)}`);
71
+ throw new Error(`获取微博登录二维码失败: ${JSON.stringify(resData)}`);
72
+ }
73
+ }
74
+ else {
75
+ logger.error('微博登录:获取X_CSRF_TOKEN失败');
76
+ return false;
77
+ }
78
+ }
79
+ /**处理扫码结果 */
80
+ static async pollLoginQRCode(e, qrid, rid, X_CSRF_TOKEN) {
81
+ const url = `https://passport.weibo.com/sso/v2/qrcode/check?entry=wapsso&source=wapssowb&url=https://m.weibo.cn/&qrid=${qrid}&rid=${rid}&ver=20250520`;
82
+ const response = await fetch(url, {
83
+ method: 'GET',
84
+ headers: lodash.merge(WeiboApi.WEIBO_POLL_LOGIN_STATUS_HEADERS, {
85
+ 'X-CSRF-TOKEN': X_CSRF_TOKEN,
86
+ 'Cookie': `X-CSRF-TOKEN=${X_CSRF_TOKEN}`
87
+ }),
88
+ redirect: 'follow'
89
+ });
90
+ if (!response.ok) {
91
+ throw new Error(`处理B站登录token网络请求失败,状态码: ${response.status}`);
92
+ }
93
+ const data = (await response.json());
94
+ if (data?.retcode === 20000000) {
95
+ // 获取 cookie url
96
+ const getCookieUrl = data?.data?.url;
97
+ if (getCookieUrl) {
98
+ const CookieResp = await fetch(getCookieUrl, {
99
+ method: 'GET',
100
+ headers: lodash.merge(WeiboApi.WEIBO_COOKIE_HEADERS, { Cookie: `X-CSRF-TOKEN=${X_CSRF_TOKEN}` }),
101
+ redirect: 'follow'
102
+ });
103
+ const setCookie = CookieResp.headers.get('set-cookie');
104
+ if (setCookie) {
105
+ await this.saveLoginCookie(e, setCookie);
106
+ e.reply(`~微博登陆成功~`);
107
+ return setCookie;
108
+ }
109
+ else {
110
+ e.reply(`获取微博登录Cookie失败: ${JSON.stringify(CookieResp.headers)}`);
111
+ return null;
112
+ }
113
+ }
114
+ }
115
+ else if (data?.retcode === 50114001) {
116
+ // 未扫码
117
+ // 继续轮询
118
+ await new Promise(resolve => setTimeout(resolve, 2000));
119
+ global?.logger?.mark(`优纪插件:扫码微博登录:未扫码,轮询中...`);
120
+ return this.pollLoginQRCode(e, qrid, rid, X_CSRF_TOKEN);
121
+ }
122
+ else if (data?.retcode === 50114002) {
123
+ // 已扫码未确认
124
+ // 继续轮询
125
+ await new Promise(resolve => setTimeout(resolve, 2000));
126
+ return this.pollLoginQRCode(e, qrid, rid, X_CSRF_TOKEN);
127
+ }
128
+ else if (data?.retcode === 50114003) {
129
+ // 二维码已失效
130
+ e.reply('微博登陆二维码已失效');
131
+ return null;
132
+ }
133
+ else {
134
+ e.reply('处理微博登录扫码结果出错');
135
+ throw new Error(`处理微博登录扫码结果出错: ${JSON.stringify(data)}`);
136
+ }
137
+ return null;
138
+ }
139
+ /**查看app扫码登陆获取的ck的有效状态*/
140
+ static async checkWeiboLogin(e) {
141
+ const LoginCookie = await this.readLoginCookie();
142
+ if (String(LoginCookie).trim().length < 10) {
143
+ e.reply('啊咧?微博登录CK呢?哦,没 #扫码微博登录# 或缓存失效了啊,那没事了。');
144
+ return;
145
+ }
146
+ else {
147
+ const { cookie, mark } = await WeiboMainModels.readSyncCookie();
148
+ let COOKIE = '', XSRF_TOKEN = '';
149
+ if (String(cookie).includes('XSRF-TOKEN')) {
150
+ COOKIE = cookie;
151
+ XSRF_TOKEN = await WeiboMainModels.readSavedCookieItems(cookie, ['XSRF-TOKEN'], false);
152
+ }
153
+ const resData = (await fetch('https://m.weibo.cn/api/config', {
154
+ method: 'GET',
155
+ headers: lodash.merge(WeiboApi.WEIBO_HEADERS, {
156
+ 'X-XSRF-TOKEN': `${XSRF_TOKEN}`,
157
+ 'Cookie': `${COOKIE}`,
158
+ 'Host': 'm.weibo.cn',
159
+ 'Referer': 'https://m.weibo.cn'
160
+ }),
161
+ redirect: 'follow'
162
+ }).then(res => res.json()));
163
+ global?.logger?.debug(`微博验证登录状态:${JSON.stringify(resData)}`);
164
+ if (resData.data?.login === true) {
165
+ const uid = Number(resData.data.uid);
166
+ const user_token = resData.data.user_token;
167
+ const infoRes = (await fetch(`https://m.weibo.cn/profile/info?uid=${uid}`, {
168
+ method: 'GET',
169
+ headers: lodash.merge(WeiboApi.WEIBO_HEADERS, {
170
+ 'X-XSRF-TOKEN': `${XSRF_TOKEN}`,
171
+ 'Cookie': `${COOKIE}`,
172
+ 'Host': 'm.weibo.cn',
173
+ 'x-h5-user-token': `${user_token}`,
174
+ 'Referer': `https://m.weibo.cn/profile/${uid}?user_token=${user_token}`
175
+ }),
176
+ redirect: 'follow'
177
+ }).then(res => res.json()));
178
+ let uname = infoRes.data?.user?.screen_name;
179
+ let mid = infoRes.data?.user?.id;
180
+ let follow_count = infoRes.data?.user?.follow_count;
181
+ let svip = infoRes.data?.user?.svip;
182
+ const cookie = await this.readLoginCookie();
183
+ const ALF = this.extractALFValue(cookie);
184
+ let TLL = 0;
185
+ if (ALF) {
186
+ TLL = Number(ALF) * 1000;
187
+ }
188
+ const EXP_TIME = moment(TLL * 1000).format('YYYY年MM月DD日 HH:mm:ss');
189
+ e.reply(`~微博账号已登陆~\n昵称:${uname}\nuid:${mid}\nsvip等级:${svip}\n关注:${follow_count}\n登录失效时间:${EXP_TIME}`);
190
+ }
191
+ else {
192
+ // 处理其他情况
193
+ e.reply('意外情况,未能获取微博登录ck的有效状态');
194
+ return;
195
+ }
196
+ }
197
+ }
198
+ /** 获取ALF值*/
199
+ static extractALFValue(cookieString) {
200
+ const match = cookieString.match(/ALF=(\d+)/);
201
+ if (match && match[1]) {
202
+ return parseInt(match[1], 10);
203
+ }
204
+ return null;
205
+ }
206
+ /**
207
+ * *******************************************************************
208
+ * cookie相关
209
+ * *******************************************************************
210
+ */
211
+ /**保存扫码登录的weiboLoginCK*/
212
+ static async saveLoginCookie(e, weiboLoginCk) {
213
+ if (weiboLoginCk && weiboLoginCk.length > 0) {
214
+ const weiboLoginCkKey = 'Yz:yuki:weibo:loginCookie';
215
+ Redis.set(weiboLoginCkKey, weiboLoginCk, { EX: 3600 * 24 * 300 });
216
+ }
217
+ else {
218
+ e.reply('扫码超时');
219
+ }
220
+ }
221
+ /** 读取扫码登陆后缓存的weiboCookie */
222
+ static async readLoginCookie() {
223
+ const CK_KEY = 'Yz:yuki:weibo:loginCookie';
224
+ const tempCk = await Redis.get(CK_KEY);
225
+ return tempCk ? tempCk : '';
226
+ }
227
+ /** 读取手动绑定的weibo CK */
228
+ static async readLocalBiliCk() {
229
+ const CK_KEY = 'Yz:yuki:weibo:localCookie';
230
+ const tempCk = await Redis.get(CK_KEY);
231
+ return tempCk ? tempCk : '';
232
+ }
233
+ /** 覆盖保存手动获取绑定的weibo ck */
234
+ static async saveLocalBiliCk(data) {
235
+ const weiboLoginCkKey = 'Yz:yuki:weibo:localCookie';
236
+ Redis.set(weiboLoginCkKey, data, { EX: 3600 * 24 * 300 });
237
+ }
238
+ /** 读取扫码登陆后缓存的weibo cookie的有效时间 */
239
+ static async readLoginCookieTTL() {
240
+ const CK_KEY = 'Yz:yuki:weibo:loginCookie';
241
+ const tempCk = await Redis.get(CK_KEY);
242
+ if (tempCk) {
243
+ const LoginCookieTTL = await Redis.ttl(CK_KEY);
244
+ return LoginCookieTTL;
245
+ }
246
+ else {
247
+ return -2;
248
+ }
249
+ }
250
+ /** 综合获取ck,返回优先级:localCK > loginCK */
251
+ static async readSyncCookie() {
252
+ const localCk = await this.readLocalBiliCk();
253
+ const loginCk = await this.readLoginCookie();
254
+ const validCk = (ck) => ck?.includes('SUB=') && ck?.includes('SUBP=');
255
+ if (validCk(localCk)) {
256
+ return { cookie: localCk, mark: 'localCk' };
257
+ }
258
+ else if (validCk(loginCk)) {
259
+ return { cookie: loginCk + ';', mark: 'loginCk' };
260
+ }
261
+ else {
262
+ return { cookie: '', mark: 'ckIsEmpty' };
263
+ }
264
+ }
265
+ /**
266
+ * 综合读取、筛选 传入的或本地或redis存储的cookie的item
267
+ * @param {string} mark 读取存储的CK类型,'localCK' 'loginCK' 或传入值 'xxx'并进行筛选
268
+ * @param {Array} items 选取获取CK的项 选全部值:items[0] = 'all' ,或选取其中的值 ['XSRF-TOKEN', 'SUB', 'SUBP', 'SRF', 'SCF', 'SRT', ' _T_WM', 'M_WEIBOCN_PARAMS', 'SSOLoginState','ALF']
269
+ * @param {boolean} isInverted 控制正取和反取,true为反取,默认为false正取
270
+ * @returns {string}
271
+ **/
272
+ static async readSavedCookieItems(mark, items, isInverted = false) {
273
+ let ckString;
274
+ switch (mark) {
275
+ case 'localCK':
276
+ ckString = await this.readLocalBiliCk();
277
+ break;
278
+ case 'loginCK':
279
+ ckString = await this.readLoginCookie();
280
+ break;
281
+ default:
282
+ ckString = mark;
283
+ }
284
+ const Wck = lodash.trim(ckString);
285
+ if (!Wck) {
286
+ return '';
287
+ }
288
+ if (items[0] === 'all') {
289
+ return Wck;
290
+ }
291
+ const cookiePairs = String(Wck)
292
+ .trim()
293
+ .match(/(\w+)=([^;|,]+)/g) //正则 /(\w+)=([^;]+);/g 匹配 a=b 的内容,并分组为 [^;|,]+ 来匹配值,其中 [^;|,] 表示除了分号和,以外的任意字符
294
+ ?.map(match => match.split('='))
295
+ .filter(([key, value]) => (isInverted ? !items.includes(key) : items.includes(key)) && value !== '')
296
+ .map(([key, value]) => `${key}=${value}`)
297
+ .join(';') || '';
298
+ return cookiePairs;
299
+ }
300
+ // 取反读取ck、筛选 传入的或本地或redis存储的cookie的item
301
+ static async readSavedCookieOtherItems(mark, items) {
302
+ return await this.readSavedCookieItems(mark, items, true);
303
+ }
304
+ /**更新cookie */
305
+ static async updateCookieWithSetCookie(setCookieHeaders, mark) {
306
+ // 1. 读取当前保存的 Cookie
307
+ const currentCookie = await this.readLoginCookie();
308
+ // 使用 Record<string, string | undefined> 类型接收解析结果
309
+ const cookieObj = currentCookie ? parse(currentCookie) : {};
310
+ // 2. 解析 Set-Cookie 响应头并更新 Cookie 对象
311
+ setCookieHeaders.forEach(header => {
312
+ const parsedCookie = parse(header.split(';')[0]);
313
+ Object.entries(parsedCookie).forEach(([key, value]) => {
314
+ if (value === 'deleted') {
315
+ delete cookieObj[key];
316
+ }
317
+ else if (value !== undefined) {
318
+ // 添加类型守卫
319
+ cookieObj[key] = value;
320
+ }
321
+ });
322
+ });
323
+ // 3. 过滤掉 undefined 值并重新组合为字符串
324
+ const validCookies = {};
325
+ Object.entries(cookieObj).forEach(([key, value]) => {
326
+ if (value !== undefined) {
327
+ validCookies[key] = value;
328
+ }
329
+ });
330
+ const updatedCookie = Object.entries(validCookies)
331
+ .map(([key, value]) => serialize(key, value))
332
+ .join('; ');
333
+ // 4. 保存更新后的 Cookie
334
+ if (updatedCookie) {
335
+ let key = '';
336
+ if (mark === 'loginCK') {
337
+ key = 'Yz:yuki:weibo:loginCookie';
338
+ }
339
+ else if (mark === 'localCK') {
340
+ key = 'Yz:yuki:weibo:localCookie';
341
+ }
342
+ Redis.set(key, updatedCookie, { EX: 3600 * 24 * 300 });
343
+ }
344
+ }
345
+ }
346
+
347
+ export { WeiboMainModels };
@@ -0,0 +1,309 @@
1
+ import WeiboApi from './weibo.main.api.js';
2
+ import lodash from 'lodash';
3
+ import crypto from 'crypto';
4
+
5
+ /**
6
+ * 生成更真实的鼠标轨迹数据(贝塞尔曲线模拟)
7
+ * @returns {Array<Array<number>>} 鼠标轨迹数据 [[x偏移, y偏移, 时间戳], ...]
8
+ */
9
+ function generateRealisticMouseTrack() {
10
+ const tracks = [];
11
+ const now = Date.now();
12
+ // 生成最近5分钟内的时间点
13
+ const startTime = now - 5 * 60 * 1000;
14
+ const endTime = now;
15
+ // 起始点和结束点
16
+ const startX = Math.floor(Math.random() * 200);
17
+ const startY = Math.floor(Math.random() * 200);
18
+ const endX = 200 + Math.floor(Math.random() * 800); // 页面常见宽度范围内
19
+ const endY = 100 + Math.floor(Math.random() * 600); // 页面常见高度范围内
20
+ // 控制点(用于贝塞尔曲线)
21
+ const controlX = startX + (endX - startX) * 0.3 + Math.random() * 200 - 100;
22
+ const controlY = startY + (endY - startY) * 0.7 + Math.random() * 200 - 100;
23
+ // 生成轨迹点数量
24
+ const pointCount = 30 + Math.floor(Math.random() * 40);
25
+ // 时间间隔
26
+ let currentTime = startTime;
27
+ const timeStep = (endTime - startTime) / pointCount;
28
+ for (let i = 0; i <= pointCount; i++) {
29
+ const t = i / pointCount;
30
+ // 二次贝塞尔曲线计算当前位置
31
+ const x = (1 - t) * (1 - t) * startX + 2 * (1 - t) * t * controlX + t * t * endX;
32
+ const y = (1 - t) * (1 - t) * startY + 2 * (1 - t) * t * controlY + t * t * endY;
33
+ // 计算与上一个点的偏移量
34
+ if (i === 0) {
35
+ // 第一个点,偏移量为0
36
+ tracks.push([0, 0, Math.floor(currentTime)]);
37
+ }
38
+ else {
39
+ const prevPoint = tracks[tracks.length - 1];
40
+ const deltaX = Math.round(x - (prevPoint[0] + prevPoint[0])); // 简化计算
41
+ const deltaY = Math.round(y - (prevPoint[1] + prevPoint[1]));
42
+ tracks.push([deltaX, deltaY, Math.floor(currentTime)]);
43
+ }
44
+ // 更新时间戳
45
+ currentTime += timeStep + (Math.random() * 200 - 100); // 添加随机波动
46
+ }
47
+ return tracks;
48
+ }
49
+ /**
50
+ * 生成浏览器指纹数据
51
+ * @returns {Object} 包含浏览器指纹信息的对象
52
+ */
53
+ function generateFingerprint() {
54
+ const fingerprintData = { fp: {}, bh: {}, r: {} };
55
+ // 浏览器指纹信息
56
+ fingerprintData.fp = {
57
+ // 版本信息
58
+ 0: '1.2.1',
59
+ // 是否支持某特性(布尔值)
60
+ 1: {
61
+ s: 1,
62
+ v: false
63
+ },
64
+ // 语言信息
65
+ 2: {
66
+ s: 1,
67
+ v: ['lang']
68
+ },
69
+ // User Agent 字符串
70
+ 3: {
71
+ s: 1,
72
+ v: WeiboApi.USER_AGENT.replace(/Mozilla\//g, '')
73
+ },
74
+ // 错误信息
75
+ 4: {
76
+ s: 1,
77
+ v: "TypeError: Cannot read properties of null (reading '0')\n at W (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:14066)\n at Object.NiOqR (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:3108)\n at https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:29253\n at Array.map (<anonymous>)\n at je (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:29193)\n at Object.Qhlex (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:5250)\n at Ie (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:25296)\n at Object.NsuAP (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:4977)\n at Pe (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:24928)\n at Module.Ve [as get] (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:24667)"
78
+ },
79
+ // 数值型数据
80
+ 5: {
81
+ s: 1,
82
+ v: 33
83
+ },
84
+ // 函数字符串表示
85
+ 6: {
86
+ s: 1,
87
+ v: 'function bind() { [native code] }'
88
+ },
89
+ // 语言环境
90
+ 7: {
91
+ s: 1,
92
+ v: [['zh-CN']]
93
+ },
94
+ // 布尔值
95
+ 8: {
96
+ s: 1,
97
+ v: true
98
+ },
99
+ // 布尔值
100
+ 9: {
101
+ s: 1,
102
+ v: false
103
+ },
104
+ // 布尔值
105
+ 10: {
106
+ s: 1,
107
+ v: true
108
+ },
109
+ // 数值
110
+ 11: {
111
+ s: 1,
112
+ v: 5
113
+ },
114
+ // 错误信息
115
+ 12: {
116
+ s: -1,
117
+ e: ''
118
+ },
119
+ // 日期字符串
120
+ 13: {
121
+ s: 1,
122
+ v: '20030107'
123
+ },
124
+ // 数值
125
+ 14: {
126
+ s: 1,
127
+ v: 50
128
+ },
129
+ // 完整的 User Agent
130
+ 15: {
131
+ s: 1,
132
+ v: WeiboApi.USER_AGENT
133
+ },
134
+ // WebGL 信息
135
+ 16: {
136
+ s: 1,
137
+ v: {
138
+ vendor: 'WebKit',
139
+ renderer: 'WebKit WebGL'
140
+ }
141
+ },
142
+ // 外部对象字符串表示
143
+ 17: {
144
+ s: 1,
145
+ v: '[object External]'
146
+ },
147
+ // 屏幕尺寸信息
148
+ 18: {
149
+ s: 1,
150
+ v: {
151
+ ow: 1920,
152
+ oh: 1152,
153
+ iw: 257,
154
+ ih: 1031
155
+ }
156
+ },
157
+ // 浏览器名称
158
+ 19: {
159
+ s: 1,
160
+ v: 'chrome'
161
+ },
162
+ // 浏览器内核
163
+ 20: {
164
+ s: 1,
165
+ v: 'chromium'
166
+ },
167
+ // 布尔值
168
+ 21: {
169
+ s: 1,
170
+ v: false
171
+ },
172
+ // 布尔值
173
+ 22: {
174
+ s: 1,
175
+ v: true
176
+ },
177
+ // 触摸支持信息
178
+ 23: {
179
+ s: 1,
180
+ v: {
181
+ ots: false,
182
+ mtp: 0,
183
+ mmtp: -1
184
+ }
185
+ }
186
+ };
187
+ // 行为数据(鼠标轨迹和键盘操作)
188
+ fingerprintData.bh = {
189
+ // 鼠标移动轨迹 [x偏移, y偏移, 时间戳毫秒]
190
+ mt: generateRealisticMouseTrack(),
191
+ // 键盘操作统计
192
+ kt: {
193
+ down: 0,
194
+ up: 0
195
+ }
196
+ };
197
+ // 追踪设置
198
+ fingerprintData.r = {
199
+ isTraceKeyboard: true,
200
+ isTraceMouse: true
201
+ };
202
+ return fingerprintData;
203
+ }
204
+ /**
205
+ * 生成 AES 加密密钥和初始化向量
206
+ * @returns {Promise<Object>} 包含 key 和 iv 的对象
207
+ */
208
+ function generateAESKey() {
209
+ // 生成 AES 密钥 (16位)
210
+ const key = crypto.randomBytes(16);
211
+ // 生成初始化向量
212
+ const iv = crypto.randomBytes(16);
213
+ return {
214
+ key: Buffer.from(key).toString('base64'),
215
+ iv: Buffer.from(iv).toString('base64')
216
+ };
217
+ }
218
+ /**
219
+ * RSA 加密函数 (模拟浏览器 Web Crypto API)
220
+ * @param {string} data - 待加密数据
221
+ * @param {Uint8Array} publicKeyDer - DER 格式的公钥
222
+ * @returns {Buffer} 加密后的数据
223
+ */
224
+ function rsaEncrypt(data, publicKeyDer) {
225
+ // 将 DER 格式的公钥转换为 PEM 格式
226
+ const base64Key = Buffer.from(publicKeyDer).toString('base64');
227
+ const keyLines = base64Key.match(/.{1,64}/g) ?? [];
228
+ const publicKeyPem = `-----BEGIN PUBLIC KEY-----\n${keyLines.join('\n')}\n-----END PUBLIC KEY-----`;
229
+ // 使用 RSA-OAEP-SHA256 算法加密
230
+ return crypto.publicEncrypt({
231
+ key: publicKeyPem,
232
+ padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
233
+ oaepHash: 'sha256'
234
+ }, Buffer.from(data, 'binary'));
235
+ }
236
+ /**
237
+ * AES CBC 加密函数 (模拟浏览器 Web Crypto API)
238
+ * @param {string} data - 待加密数据
239
+ * @param {string} keyBase64 - Base64 编码的密钥
240
+ * @param {string} ivBase64 - Base64 编码的初始化向量
241
+ * @returns {string} Base64 编码的加密结果
242
+ */
243
+ function aesEncrypt(data, keyBase64, ivBase64) {
244
+ const key = Buffer.from(keyBase64, 'base64');
245
+ const iv = Buffer.from(ivBase64, 'base64');
246
+ const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
247
+ cipher.setAutoPadding(true);
248
+ let encrypted = cipher.update(data, 'utf8', 'binary');
249
+ encrypted += cipher.final('binary');
250
+ return Buffer.from(encrypted, 'binary').toString('base64');
251
+ }
252
+ /**
253
+ * 数据加密主函数
254
+ * @param {string} jsonData - JSON 格式的指纹数据
255
+ * @returns {Promise<string>} 加密后的数据
256
+ */
257
+ async function encryptData(jsonData) {
258
+ // 原始的 RSA 公钥(DER 格式),与源代码完全一致
259
+ const publicKeyDer = new Uint8Array([
260
+ 48, 129, 159, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 129, 141, 0, 48, 129, 137, 2, 129, 129, 0, 180, 249, 101, 74, 227, 247, 222, 230,
261
+ 24, 220, 10, 149, 183, 131, 164, 185, 20, 166, 164, 114, 158, 71, 46, 151, 77, 71, 226, 23, 78, 67, 177, 246, 197, 249, 213, 39, 243, 55, 38, 112, 17, 64,
262
+ 135, 155, 109, 50, 185, 61, 21, 105, 106, 245, 148, 212, 127, 7, 18, 227, 255, 40, 199, 241, 65, 211, 167, 185, 232, 5, 186, 189, 245, 59, 161, 214, 48,
263
+ 160, 251, 21, 92, 187, 172, 83, 152, 11, 85, 72, 37, 137, 87, 104, 63, 39, 86, 6, 150, 84, 6, 178, 229, 220, 144, 133, 131, 212, 47, 139, 232, 185, 192, 97,
264
+ 89, 137, 170, 141, 39, 19, 85, 4, 153, 238, 75, 93, 243, 96, 206, 72, 135, 91, 2, 3, 1, 0, 1
265
+ ]);
266
+ // 生成 AES 密钥和 IV
267
+ const { key, iv } = generateAESKey();
268
+ // RSA 加密 AES 密钥(将密钥重复两次后加密)
269
+ const keyBinary = Buffer.from(key, 'base64').toString('binary');
270
+ const encryptedKey = rsaEncrypt(keyBinary + keyBinary, publicKeyDer);
271
+ // AES 加密数据
272
+ const encryptedData = aesEncrypt(jsonData, key, iv);
273
+ // 组合加密数据(与原始代码保持一致的格式)
274
+ const result = '01' + Buffer.from('01' + encryptedKey.toString('binary') + '02' + Buffer.from(encryptedData, 'base64').toString('binary')).toString('base64');
275
+ return result;
276
+ }
277
+ /**
278
+ * 生成请求载荷
279
+ * @returns {Promise<string>} 加密后的载荷数据
280
+ */
281
+ async function genBdPayload() {
282
+ const fingerprint = generateFingerprint();
283
+ const jsonString = JSON.stringify(fingerprint);
284
+ const encryptedData = await encryptData(jsonString);
285
+ return encryptedData;
286
+ }
287
+ /**
288
+ * 访问bd接口获取rid
289
+ * @param {string} X_CSRF_TOKEN - X_CSRF_TOKEN
290
+ * @returns {Promise<JSON>} 服务器响应结果
291
+ */
292
+ async function getRidFromBd(X_CSRF_TOKEN) {
293
+ const params = new URLSearchParams();
294
+ const payload = await genBdPayload();
295
+ payload && params.append('data', payload);
296
+ params.append('from', 'weibo');
297
+ const ridData = (await fetch('https://passport.weibo.com/sso/bd', {
298
+ method: 'POST',
299
+ headers: lodash.merge(WeiboApi.WEIBO_GET_BD_TOKEN_HEADERS, {
300
+ Origin: 'https://passport.weibo.com',
301
+ Cookie: `X-CSRF-TOKEN=${X_CSRF_TOKEN}`
302
+ }),
303
+ body: params,
304
+ redirect: 'follow'
305
+ }).then(res => res.json()));
306
+ return ridData;
307
+ }
308
+
309
+ export { genBdPayload, getRidFromBd };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yz-yuki-plugin",
3
- "version": "2.0.8-3",
3
+ "version": "2.0.8-5",
4
4
  "description": "优纪插件,yunzaijs 关于 微博推送、B站推送 等功能的拓展插件",
5
5
  "author": "snowtafir",
6
6
  "type": "module",
@@ -28,6 +28,7 @@
28
28
  "axios": "^1.7.9",
29
29
  "chalk": "^5.3.0",
30
30
  "chokidar": "4.0.1",
31
+ "cookie": "^1.0.2",
31
32
  "debug": "^4.3.6",
32
33
  "jsdom": "^25.0.1",
33
34
  "json5": "^2.2.3",