Compare commits

..
25 Commits
Author SHA1 Message Date
shanmite 236fcb6284 pref: 去除对unzipper的依赖
feat: 专栏转发率达一半以上时跳过
2021-07-28 13:02:12 +08:00
shanmite 8bc053b352 fix: 增加dyid长度校验 2021-07-27 23:34:10 +08:00
shanmite f5a8ad75d2 feat: 判断专栏是否已查看 2021-07-27 22:10:26 +08:00
shanmite cbce4b113d feat: 增加从专栏获取抽奖的功能 2021-07-27 21:48:12 +08:00
shanmite a63456bd29 pref: 调整结构 2021-07-27 14:51:59 +08:00
shanmite 329e6931dd 更新readme 2021-07-18 23:55:49 +08:00
shanmite 309371fbd7 pref: 重构部分代码,优化提示 2021-07-18 23:37:27 +08:00
shanmite 6f81360d2a fix: 多账号状态清零 2021-07-18 22:27:48 +08:00
shanmite 4e377f1c41 cli: 更改颜色 2021-07-18 17:35:33 +08:00
shanmite 79c550db14 fix: 滤除过期官方抽奖 2021-07-18 17:24:11 +08:00
shanmite 28c2af93cf fix: 开奖时间过滤失效 2021-07-18 16:52:54 +08:00
shanmite 11aec87eed fix: 循环运行 2021-07-18 12:45:49 +08:00
shanmite 2442d5c46d build: 支持arm 2021-07-18 10:53:10 +08:00
shanmite a21b0dd33e feat: 清理新增clear_white_list设置 2021-07-18 10:28:14 +08:00
shanmite ce3274c258 fix: 无uid时不判断粉丝数 2021-07-18 10:08:23 +08:00
shanmite 455c81ee55 doc: 更新readme 2021-07-17 20:17:06 +08:00
shanmite 18760ebaac perf: 重构代码
- 分离动态筛选部分的网络请求和数据处理
- 优化部分ifelse判断
feat:
- 新增NOT_GO_LOTTERY环境变量
- 边转边存dyid
- 滤除的dyid也进行存储
fix: 修复部分bug
2021-07-17 17:59:56 +08:00
shanmite 266a657a8f feat: 日志相关
- 彩色输出
- 环境变量LOTTERY_LOG_LEVEL更改日志等级
feat: 设置相关
-  lottery_loop_wait 抽奖循环
- check_loop_wait 检奖循环
- clear_loop_wait 清理循环
2021-07-16 23:31:58 +08:00
shanmite ebf7440ef2 fix: 部分动态无法获取描述 2021-07-15 20:08:33 +08:00
shanmite a0e668bcb5 fix: 移除所有GithubAction相关代码 2021-07-15 11:12:35 +08:00
shanmite a47472a3e4 feat: 新增loop_wait设置 2021-07-15 10:35:13 +08:00
shanmite 3698c81384 doc: 更新说明文档,调整输出信息 2021-07-14 14:35:24 +08:00
shanmite ad7df1aa74 fix: is_imitator为true时不发送评论 2021-07-11 20:43:10 +08:00
shanmite 29f9ff1381 feat: 可修改关键词设置 2021-07-11 17:10:09 +08:00
shanmite 4f7fb6142e feat: 可在设置修改uid和tag 2021-07-10 20:37:02 +08:00
32 changed files with 1189 additions and 3299 deletions
+1 -2
View File
@@ -1,7 +1,6 @@
node_modules/
dyids/
dist/
lib/dyid*.txt
dyid.zip
package-lock.json
env.js
my_config.js
+2 -2
View File
@@ -1,9 +1,9 @@
# Latest Nodejs + Alpine Linux
FROM node:lts-alpine
MAINTAINER [email protected]
MAINTAINER shanmiteko@github.com
LABEL version="0.1" description="自动参与B站动态抽奖"
LABEL version="1.0" description="自动参与B站动态抽奖"
WORKDIR /lottery
+34 -34
View File
@@ -5,15 +5,17 @@
- [获取COOKIE](#获取cookie)
- [本地运行](#本地运行)
- [可执行文件](#可执行文件)
- [Windows](#windows)
- [Linux](#linux)
- [以源码方式运行](#以源码方式运行)
- [Windows](#windows)
- [Linux](#linux)
- [Docker](#docker)
- [防重复转发](#防重复转发)
- [检测中奖](#检测中奖)
- [检测未读信息, 已读未读信息](#检测未读信息-已读未读信息)
- [中奖推送(可选)](#中奖推送可选)
- [设置说明](#设置说明)
- [自定义设置](#自定义设置)
[Github仓库链接](https://github.com/shanmiteko/LotteryAutoScript)
已实现功能:
> 点赞 评论 转发 @好友 带话题 随机动态 检测开奖 清理
@@ -38,7 +40,7 @@ Chrome浏览器:
2. 进入Application找到Cookies栏中的SESSDATA将HttpOnly选项**取消**勾选
(此步骤是为了方便后续采用JS获取Cookies)
(此步骤是为了方便后续采用JS获取Cookies,获取完毕后应再次勾选)
![取消httponly](doc/pic/getCookies.png)
@@ -66,20 +68,29 @@ Chrome浏览器:
↓↓
### 本地运行
具体操作详见[env.example.js](env.example.js)文件内注释
#### 可执行文件
[打开终端](https://cn.bing.com/search?q=%E5%A6%82%E4%BD%95%E5%9C%A8%E5%BD%93%E5%89%8D%E7%9B%AE%E5%BD%95%E6%89%93%E5%BC%80%E7%BB%88%E7%AB%AF)运行内部的可执行文件即可
1. [[下载](https://github.com/shanmiteko/LotteryAutoScript/releases)|[cnpmjs镜像下载](https://github.com.cnpmjs.org/shanmiteko/LotteryAutoScript/releases)|[Fastgit镜像下载](https://hub.fastgit.org/shanmiteko/LotteryAutoScript/releases)]压缩包并解压后有三个文件
```
env.js 便捷设置环境变量和多账号参数
lottery.exe 可执行文件
my_config.js 自定义设置文件
```
2. `env.js`中填入`COOKIE`和推送参数
3. `my_config.js`中自定义设置
4. 在当前目录下打开终端运行可执行文件`lottery`
5. 输入`lottery start`启动抽奖|`lottery check`检测中奖|`lottery clear`清理动态
> 或者新建文本文档写入`lottery start`然后更改扩展名为.bat点击运行
6. 运行截图
![lottery_start](doc/pic/lottery_start.png)
[下载](https://github.com/shanmiteko/LotteryAutoScript/releases)
#### 以源码方式运行
[cnpmjs镜像下载](https://github.com.cnpmjs.org/shanmiteko/LotteryAutoScript/releases)
<details>
[Fastgit镜像下载](https://hub.fastgit.org/shanmiteko/LotteryAutoScript/releases)
<summary>点击显示详细说明</summary>
#### Windows
如何搭建运行环境
##### Windows
step1: 下载代码到本地
@@ -131,11 +142,15 @@ npm i && npm run clear
注: **本地运行时可在设置中增大扫描页数**
> [windows系统定时运行](./doc/win_schedule.md)
> ~~[windows系统定时运行](./doc/win_schedule.md)~~
#### Linux
脚本已内置定时运行功能
##### Linux
[linux系统配置与定时运行](./doc/linux_schedule.md)
</details>
### Docker
1.初始化
@@ -171,16 +186,14 @@ cd lottery
进入lottery上一级目录
```bash
docker image rm shanmite/lottery_auto_docker
docker image rm -f shanmite/lottery_auto_docker
rm -rf lottery/
```
----------------------------------------
## 防重复转发
脚本将转发过的动态都写入`dyids/dyid*.txt`文件中
完整转发一轮后才会进行写入操作, 勿过早关闭运行
脚本将转发过的动态和被过滤的动态都写入`dyids/dyid*.txt`文件中
----------------------------------------
@@ -226,19 +239,6 @@ rm -rf lottery/
----------------------------------------
## 设置说明
### 自定义设置
- **默认设置**存放于[config.js](lib/config.js)和[script.js](lib/Script.js)中
- 修改默认设置(非必要)
- 本地运行 => 在项目根目录下新建my_config.js文件将设置填在其中
- 多账号的设置
分别存储于键`"config_1"``"config_2"`...`"config_n"`中, 例如
```json
{
"config_1": {
"model": "00"
},
"config_2": {
"model": "11"
}
}
```
详见[env.example.js](./env.example.js)文件内部注释
详见[my_config.example.js](./my_config.example.js)文件内部注释
+2 -1
View File
@@ -45,7 +45,8 @@ vim env.js
`"PAT"``"GITHUB_REPOSITORY"`不填也行
5.设置定时工作
~~5.设置定时工作~~
> [linux系统定时运行](https://zhuanlan.zhihu.com/p/58719487)
输入:
Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

+11 -12
View File
@@ -1,14 +1,13 @@
/**
* 使用时请将此文件命名为env.js
* 注: 请打开扩展名显示
*/
/**
* ## 账号相关参数
* ## 账号相关
* - `COOKIE` 是必填项
* - `NUMBER` 表示是第几个账号
* - `PAT` 与 `GITHUB_REPOSITORY` 如果之前在Github Action上运行过脚本, 可填写已下载转发过的动态dyid, 之后可移除
*
* - `CLEAR` 是否启用清理功能
* - `ENABLE_MULTIPLE_ACCOUNT` 是否启用多账号
* - `MULTIPLE_ACCOUNT_PARM` 多账号参数(JSON格式)
* ## 调试相关
* - `LOTTERY_LOG_LEVEL` 输出日志等级 Error<Warn<Info<Debug 1<2<3<4
* - `NOT_GO_LOTTERY` 关闭抽奖行为
* ## 多账号
* 1. 将 ENABLE_MULTIPLE_ACCOUNT 的值改为true
* 2. 将账号信息依次填写于 multiple_account_parm 中, 参考例子类推
@@ -20,10 +19,10 @@ const account_parm = {
COOKIE: "",
NUMBER: 1,
CLEAR: true,
LOCALLAUNCH: true,
PAT: "",
GITHUB_REPOSITORY: "用户名/仓库名",
ENABLE_MULTIPLE_ACCOUNT: false
ENABLE_MULTIPLE_ACCOUNT: false,
MULTIPLE_ACCOUNT_PARM: "",
LOTTERY_LOG_LEVEL: 3,
NOT_GO_LOTTERY: ""
}
/**
+110 -52
View File
@@ -1,5 +1,5 @@
const Ajax = require('./Ajax');
const { strToJson, tooltip } = require('./Base');
const { strToJson, log } = require('./Util');
const GlobalVar = require('./GlobalVar');
/**
@@ -74,16 +74,15 @@ const BiliAPI = {
return new Promise((resolve) => {
Ajax.get({
url: 'https://api.bilibili.com/x/msgfeed/unread',
hasCookies: true,
success: responseText => {
let res = strToJson(responseText);
if (res.code === 0) {
const { at } = res.data;
resolve(at)
tooltip.log(`[获取未读@]成功 数量: ${at}`)
log.info('获取未读@', `成功 数量: ${at}`)
} else {
resolve(-1)
tooltip.log(`[获取未读@]失败\n${responseText}`)
log.error('获取未读@', `失败\n${responseText}`)
}
}
})
@@ -122,7 +121,7 @@ const BiliAPI = {
success: responseText => {
let res = strToJson(responseText);
if (res.code === 0) {
tooltip.log('[获取一页私信](20)成功 ' + (ts_16 ? 'end_ts->' + ts_16 : '第一页'));
log.info('获取一页私信(20)', '成功 ' + (ts_16 ? 'end_ts->' + ts_16 : '第一页'));
/**@type {Array} */
const sessions = res.data.session_list || [];
const has_more = res.data.has_more;
@@ -133,7 +132,7 @@ const BiliAPI = {
})
resolve({ has_more, data })
} else {
tooltip.log(`[获取私信]失败\n${responseText}`);
log.error('获取私信', `失败\n${responseText}`);
resolve({ has_more: 0, data: [] });
}
}
@@ -148,16 +147,15 @@ const BiliAPI = {
return new Promise((resolve) => {
Ajax.get({
url: 'https://api.vc.bilibili.com/session_svr/v1/session_svr/single_unread',
hasCookies: true,
success: responseText => {
let res = strToJson(responseText);
if (res.code === 0) {
const { unfollow_unread, follow_unread } = res.data;
resolve({ unfollow_unread, follow_unread });
tooltip.log(`[获取未读私信]成功 已关注未读数: ${follow_unread}, 未关注未读数 ${unfollow_unread}`);
log.info('获取未读私信', `成功 已关注未读数: ${follow_unread}, 未关注未读数 ${unfollow_unread}`);
} else {
resolve(null);
tooltip.log(`[获取未读私信]失败\n${responseText}`);
log.error('获取未读私信', `失败\n${responseText}`);
}
}
})
@@ -181,9 +179,9 @@ const BiliAPI = {
success: responseText => {
let res = strToJson(responseText);
if (res.code === 0) {
tooltip.log(`[私信已读]成功 -> talker_id: ${talker_id}`);
log.info('私信已读', `成功 -> talker_id: ${talker_id}`);
} else {
tooltip.log(`[私信已读]失败 -> talker_id: ${talker_id}`);
log.error('私信已读', `失败 -> talker_id: ${talker_id}`);
}
}
})
@@ -203,16 +201,34 @@ const BiliAPI = {
success: responseText => {
let res = strToJson(responseText);
if (res.code === 0) {
tooltip.log('[获取关注列表]成功');
log.info('获取关注列表', '成功');
resolve(res.data.list.toString());
} else {
tooltip.log(`[获取关注列表]失败\n${responseText}`);
log.error('获取关注列表', `失败\n${responseText}`);
resolve(null);
}
}
});
});
},
/**
* 获取一个动态的细节
* @param {string} dyid
* @return {Promise<string>}
*/
getOneDynamicByDyid(dyid) {
return new Promise((resolve) => {
Ajax.get({
url: 'https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/get_dynamic_detail',
queryStringsObj: {
dynamic_id: dyid
},
success: responseText => {
resolve(responseText)
}
})
});
},
/**
* 获取一组动态的信息
* @param {number} UID
@@ -254,11 +270,10 @@ const BiliAPI = {
queryStringsObj: {
tag_name: tagename
},
hasCookies: false,
success: responseText => {
const res = strToJson(responseText);
if (res.code !== 0) {
tooltip.log('获取TagID失败');
log.error('获取TagID', '失败');
resolve(-1);
} else {
resolve(res.data.tag_id);
@@ -306,6 +321,49 @@ const BiliAPI = {
});
});
},
/**
* 搜索专栏
* @param {string} keyword
* @return {Promise<Array<number>>}
*/
searchArticlesByKeyword(keyword) {
return new Promise((resolve) => {
Ajax.get({
url: 'https://api.bilibili.com/x/web-interface/search/type',
queryStringsObj: {
keyword,
page: 1,
order: 'pubdate',
search_type: 'article'
},
success: responseText => {
const res = JSON.parse(responseText);
if (res.code === 0) {
log.info('搜索专栏', '成功 关键词: ' + keyword)
resolve(res.data.result.map(it => it.id))
} else {
log.error('搜索专栏', '失败 原因:\n' + responseText)
resolve([])
}
}
})
});
},
/**
* 获取专栏内容
* @param {number} cv
* @returns {Promise<string>}
*/
getOneArticleByCv(cv) {
return new Promise((resolve) => {
Ajax.get({
url: `https://www.bilibili.com/read/cv${cv}`,
success: responseText => {
resolve(responseText)
}
})
});
},
/**
* 获取粉丝数
* @param {number} uid
@@ -325,7 +383,7 @@ const BiliAPI = {
if (res.code === 0) {
resolve(res.data.follower);
} else {
tooltip.warn(`[获取粉丝数]尝试切换线路`);
log.warn('获取粉丝数', '尝试切换线路');
Ajax.get({
url: 'https://api.bilibili.com/x/relation/stat',
queryStringsObj: {
@@ -334,10 +392,10 @@ const BiliAPI = {
success: responseText => {
const res = strToJson(responseText);
if (res.code === 0) {
log.info('获取粉丝数', 'ok');
resolve(res.data.follower);
tooltip.log(`[获取粉丝数]ok`);
} else {
tooltip.warn(`获取粉丝数出错,可能是访问过频繁\n${responseText}`);
log.error('获取粉丝数', `出错 可能是访问过频繁\n${responseText}`);
resolve(-1);
}
}
@@ -365,7 +423,6 @@ const BiliAPI = {
queryStringsObj: {
dynamic_id: dyid
},
hasCookies: false,
success: responseText => {
const res = strToJson(responseText);
/(?<=_prize_cmt":").*(?=")/.exec();
@@ -389,7 +446,7 @@ const BiliAPI = {
isMe: isMe
});
} else {
tooltip.log(`获取开奖信息失败\n${responseText}`);
log.error('获取开奖信息', `失败\n${responseText}`);
resolve({
ts: -1,
text: '获取开奖信息失败',
@@ -427,16 +484,16 @@ const BiliAPI = {
/* 重复关注code also equal 0 */
const res = strToJson(responseText)
if (res.code === 0) {
tooltip.log('[自动关注]关注+1');
log.info('自动关注', '关注+1');
resolve(0);
} else if (res.code === 22002) {
tooltip.log('[自动关注]您已被对方拉入黑名单');
log.error('自动关注', '您已被对方拉入黑名单');
resolve(1);
} else if (res.code === 22015) {
tooltip.log('[自动关注]您的账号异常无法关注');
log.error('自动关注', '您的账号异常无法关注');
resolve(2);
} else {
tooltip.log(`[自动关注]失败 尝试切换线路\n${responseText}`);
log.warn('自动关注', `失败 尝试切换线路\n${responseText}`);
Ajax.post({
url: 'https://api.vc.bilibili.com/feed/v1/feed/SetUserFollow',
data: {
@@ -446,10 +503,10 @@ const BiliAPI = {
},
success: responseText => {
if (/^{"code":0/.test(responseText)) {
tooltip.log('[自动关注]关注+1');
log.info('自动关注', '关注+1');
resolve(0);
} else {
tooltip.log(`[自动关注]失败 尝试切换另一条线路\n${responseText}`);
log.warn('自动关注', `失败 尝试切换另一条线路\n${responseText}`);
Ajax.post({
url: 'https://api.bilibili.com/x/relation/batch/modify',
data: {
@@ -460,10 +517,10 @@ const BiliAPI = {
},
success: responseText => {
if (/^{"code":0/.test(responseText)) {
tooltip.log('[自动关注]关注+1');
log.info('自动关注', '关注+1');
resolve(0);
} else {
tooltip.log(`[自动关注]失败\n${responseText}`);
log.error('自动关注', `失败\n${responseText}`);
resolve(1);
}
}
@@ -496,10 +553,10 @@ const BiliAPI = {
success: responseText => {
/* 重复移动code also equal 0 */
if (/^{"code":0/.test(responseText)) {
tooltip.log('[移动分区]up主分区移动成功');
log.info('移动分区', 'up主分区移动成功');
resolve(0);
} else {
tooltip.log(`[移动分区]up主分区移动失败\n${responseText}`);
log.error('移动分区', `up主分区移动失败\n${responseText}`);
resolve(1);
}
}
@@ -525,10 +582,10 @@ const BiliAPI = {
success: responseText => {
const res = strToJson(responseText);
if (res.code === 0) {
tooltip.log('[自动取关]取关成功');
log.info('自动取关', '取关成功');
resolve(true)
} else {
tooltip.log(`[自动取关]取关失败\n${responseText}`);
log.error('自动取关', `取关失败\n${responseText}`);
resolve(false)
}
}
@@ -554,10 +611,10 @@ const BiliAPI = {
},
success: responseText => {
if (/^{"code":0/.test(responseText)) {
tooltip.log('[自动点赞]点赞成功');
log.info('自动点赞', '点赞成功');
resolve(0);
} else {
tooltip.log(`[转发动态]点赞失败\n${responseText}`);
log.error('自动点赞', `点赞失败\n${responseText}`);
resolve(1);
}
}
@@ -594,10 +651,10 @@ const BiliAPI = {
},
success: responseText => {
if (/^{"code":0/.test(responseText)) {
tooltip.log('[转发动态]成功转发一条动态');
log.info('转发动态', '成功转发一条动态');
resolve(0);
} else {
tooltip.log(`[转发动态]转发动态失败\n${responseText}`);
log.error('转发动态', `转发动态失败\n${responseText}`);
resolve(1);
}
}
@@ -640,9 +697,9 @@ const BiliAPI = {
data,
success: responseText => {
if (/^{"code":0/.test(responseText)) {
tooltip.log('[发布动态]成功创建一条随机内容的动态');
log.info('发布动态', '成功创建一条随机内容的动态');
} else {
tooltip.warn(`[发布动态]发布动态失败\n${responseText}`);
log.error('发布动态', `发布动态失败\n${responseText}`);
}
resolve()
}
@@ -665,10 +722,10 @@ const BiliAPI = {
retry: false,
success: responseText => {
if (/^{"code":0/.test(responseText)) {
tooltip.log('[删除动态]成功删除一条动态');
log.info('删除动态', '成功删除一条动态');
resolve(true);
} else {
tooltip.log(`[删除动态]删除动态失败\n${responseText}`);
log.error('删除动态', `删除动态失败\n${responseText}`);
resolve(false);
}
}
@@ -701,19 +758,19 @@ const BiliAPI = {
success: responseText => {
const res = strToJson(responseText);
if (res.code === 0) {
show ? tooltip.log('[自动评论]评论成功') : void 0;
show && log.info('自动评论', '评论成功');
resolve(0);
} else if (res.code === -404) {
show ? tooltip.log('[自动评论]原动态已删除') : void 0;
show && log.error('自动评论', '原动态已删除');
resolve(0);
} else if (res.code === 12002) {
show ? tooltip.log('[自动评论]评论区已关闭') : void 0;
show && log.error('自动评论', '评论区已关闭');
resolve(0);
} else if (res.code === 12015) {
show ? tooltip.log('[自动评论]需要输入验证码') : void 0;
show && log.error('自动评论', '需要输入验证码');
resolve(1);
} else {
show ? tooltip.log(`[自动评论]评论失败\n${responseText}`) : void 0;
show && log.error('自动评论', `评论失败\n${responseText}`);
resolve(0);
}
}
@@ -738,10 +795,10 @@ const BiliAPI = {
if (res.code === 0) {
const data = res.data.filter((it) => it.name === name);
if (data.length) {
tooltip.log('[获取分区id]成功');
log.info('获取分区id', '成功');
tagid = data[0].tagid
} else {
tooltip.log('[获取分区id]失败 无指定分区');
log.warn('获取分区id', '失败 无指定分区');
}
if (name === '此处存放因抽奖临时关注的up') {
if (typeof tagid === 'undefined') {
@@ -753,7 +810,7 @@ const BiliAPI = {
resolve(tagid)
}
} else {
tooltip.warn(`[获取分区id]访问出错 可在my_config里手动填入\n${responseText}`)
log.error('获取分区id', `访问出错 可在my_config里手动填入\n${responseText}`)
resolve(tagid)
}
}
@@ -776,11 +833,11 @@ const BiliAPI = {
success: responseText => {
let obj = strToJson(responseText);
if (obj.code === 0) {
tooltip.log('[新建分区]分区新建成功')
log.info('新建分区', '分区新建成功')
let { tagid } = obj.data /* 获取tagid */
resolve(tagid)
} else {
tooltip.warn(`[新建分区]分区新建失败\n${responseText}`);
log.error('新建分区', `分区新建失败\n${responseText}`);
resolve(undefined);
}
}
@@ -810,10 +867,10 @@ const BiliAPI = {
res.data.forEach(d => {
uids.push(d.mid);
})
tooltip.log(`[获取分组${tagid}]成功获取取关分区列表${n}`);
log.info(`获取分组${tagid}`, `成功获取取关分区列表${n}`);
resolve(uids)
} else {
tooltip.warn(`[获取分组]获取取关分区列表失败\n${responseText}`);
log.error(`获取分组${tagid}`, `获取取关分区列表失败\n${responseText}`);
resolve(uids)
}
}
@@ -822,4 +879,5 @@ const BiliAPI = {
}
};
module.exports = BiliAPI;
-156
View File
@@ -1,156 +0,0 @@
const { createReadStream, createWriteStream } = require('fs')
const unzip = require('unzipper');
const { strToJson, tooltip } = require("./Base")
const { HttpRequest } = require("./HttpRequest")
const { GITHUB_REPOSITORY } = process.env;
const GihubAPI = {
/**
* 列出第一个构件
* @returns {Promise<{id: number, size_in_bytes: number}>}
*/
listArtifacts() {
return new Promise((resolve) => {
HttpRequest({
method: 'GET',
url: `https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/artifacts`,
config: {
retry: false
},
query: {
page: 1,
per_page: 5
},
headers: {
Accept: 'application/json, text/plain, */*',
},
success: res => {
const data = strToJson(res.body);
if (data.total_count) {
resolve(data.artifacts[0])
} else {
resolve({})
tooltip.log('之前无构件');
}
},
failure: err => {
tooltip.log(err);
resolve({})
}
})
});
},
/**
* 下载构件
* @param {number} id
* @returns {Promise<boolean>} success: true
*/
downloadArtifacts(id) {
return new Promise((resolve) => {
HttpRequest({
method: 'GET',
url: `https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/artifacts/${id}/zip`,
headers: {
accept: 'application/vnd.github.v3+json',
authorization: `token ${process.env.PAT}`
},
config: {
redirect: true,
},
stream: true,
success: res => {
let recv_length = 0;
const wtbs = createWriteStream('dyid.zip');
res.resStream.on('data', chuck => {
recv_length += chuck.length
tooltip.log(`已收到:${recv_length} Bytes`)
})
res.resStream.pipe(wtbs)
wtbs.on('finish', () => {
tooltip.log('下载完成开始解压')
createReadStream('dyid.zip').pipe(unzip.Extract({
path: 'lib',
}).on('close', () => {
tooltip.log('解压完成')
resolve(true)
}))
}).on('error', () => {
wtbs.destroy()
resolve(false)
})
},
failure: err => {
tooltip.log(err);
resolve(false)
}
})
});
},
/**
* 检查是否正在抽奖或取关
* @returns {Promise<boolean>}
*/
hasLotteryRun() {
return new Promise((resolve) => {
HttpRequest({
method: 'GET',
url: `https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/runs`,
query: {
status: "in_progress"
},
headers: {
accept: 'application/vnd.github.v3+json',
},
success: res => {
const { workflow_runs } = strToJson(res.body);
if (workflow_runs instanceof Array) {
workflow_runs.filter(wr => /sweepstakes|clear/.test(wr.name)).length > 1 ?
resolve(true) : resolve(false)
} else {
resolve(true)
}
},
failure: err => {
tooltip.log(err);
resolve(true)
}
})
});
},
/**
* 触发一个workflow
* @param {string} yaml_file
*/
trigger(yaml_file) {
return new Promise((resolve) => {
HttpRequest({
method: 'POST',
url: `https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/workflows/${yaml_file}/dispatches`,
contents: {
ref: 'main'
},
headers: {
accept: 'application/vnd.github.v3+json',
'content-type': 'application/json',
authorization: `token ${process.env.PAT}`
},
success: res => {
if (res.body === '') {
tooltip.log('成功触发'+yaml_file);
} else {
tooltip.log(`触发${yaml_file}失败 响应:\n${res.body}`);
}
resolve()
},
failure: err => {
tooltip.log(err);
resolve()
}
})
});
}
}
module.exports = GihubAPI;
-3
View File
@@ -6,9 +6,6 @@ let GlobalVar = {
set(key, value) {
this.inner[key] = value
},
updateAll(inner) {
this.inner = inner;
}
};
+246 -164
View File
@@ -1,4 +1,4 @@
const Base = require('./Base');
const Util = require('./Util');
const BiliAPI = require('./BiliAPI');
const { sendNotify } = require('./sendNotify');
const eventBus = require('./eventBus');
@@ -6,6 +6,7 @@ const Public = require('./Public');
const GlobalVar = require("./GlobalVar");
const config = require("./config");
const MyStorage = require('./MyStorage');
const { log, hasEnv } = Util;
/**
* 监视器
@@ -13,13 +14,18 @@ const MyStorage = require('./MyStorage');
class Monitor extends Public {
/**
* @constructor
* @param {number | string} param
* @param {[string, number | string]} lottery_param
*/
constructor(param) {
constructor(lottery_param) {
super();
typeof param === 'number' ? this.UID = param : this.tag_name = param;
this.lottery_param = lottery_param
this.tagid = config.partition_id; /* tagid初始化 */
this.attentionList = ''; /* 转为字符串的所有关注的up主uid */
this.LotteryInfoMap = new Map([
['UIDs', this.getLotteryInfoByUID.bind(this)],
['TAGs', this.getLotteryInfoByTag.bind(this)],
['Articles', this.getLotteryInfoByArticle.bind(this)],
]);
}
/**
* 初始化
@@ -36,32 +42,9 @@ class Monitor extends Public {
return
}
}
this.attentionList = await BiliAPI.getAttentionList(GlobalVar.get("myUID")); /* 获取关注列表 */
await this.startLottery();
}
/**
* 启动
* @returns {Promise<boolean>}
*/
async startLottery() {
const allLottery = await this.filterLotteryInfo();
let status = 1;
if (allLottery instanceof Array) {
if (allLottery.length) {
let dyids = [];
for (const Lottery of allLottery) {
status = await this.go(Lottery);
if (status % 2) break
dyids.push(Lottery.dyid);
}
if (dyids.length) MyStorage.updateDyid(dyids.toString())
Base.tooltip.log('开始转发下一组动态');
} else {
status = 0;
Base.tooltip.log('无未转发抽奖');
}
}
switch (status) {
/** 关注列表初始化 */
this.attentionList = await BiliAPI.getAttentionList(GlobalVar.get("myUID"));
switch (await this.startLottery()) {
case 0:
eventBus.emit('Turn_on_the_Monitor')
break;
@@ -84,149 +67,243 @@ class Monitor extends Public {
case 41:
eventBus.emit('Turn_off_the_Monitor', '转发失败')
break
case 51:
eventBus.emit('Turn_off_the_Monitor', '获取开奖时间失败')
break
case 61:
eventBus.emit('Turn_off_the_Monitor', '获取关注数失败')
break
default:
eventBus.emit('Turn_off_the_Monitor', '出错 status:' + status)
eventBus.emit('Turn_off_the_Monitor', '未知错误')
break;
}
}
/**
* 启动
* @returns {Promise<number>}
*/
async startLottery() {
const allLottery = await this.filterLotteryInfo()
, len = allLottery.length;
log.info('筛选动态', `筛选完毕(${len})`);
if (len) {
for (const Lottery of allLottery) {
let status = 0;
if (Lottery.isOfficialLottery) {
let { ts } = await BiliAPI.getLotteryNotice(Lottery.dyid);
const ts_10 = Date.now() / 1000;
if (ts < 0) {
return 51
}
if (ts < ts_10) {
log.info('过滤', '已过开奖时间')
MyStorage.updateDyid(Lottery.dyid)
continue
}
if (ts > ts_10 + config.maxday * 86400) {
log.info('过滤', '超过指定开奖时间')
MyStorage.updateDyid(Lottery.dyid)
continue
}
} else if (Lottery.uid[0]) {
const followerNum = await BiliAPI.getUserInfo(Lottery.uid[0]);
if (followerNum < 0) {
return 61
}
if (followerNum < config.minfollower) {
log.info('过滤', `粉丝数(${followerNum})小于指定数量`)
MyStorage.updateDyid(Lottery.dyid)
continue
}
}
status = await this.go(Lottery)
if (status % 2 !== 0) {
return status
}
MyStorage.updateDyid(Lottery.dyid);
await Util.delay(config.wait * (Math.random() + 0.5));
}
log.info('抽奖', '开始转发下一组动态');
return 0
} else {
log.info('抽奖', '无未转发抽奖');
return 0
}
}
/**
* 抽奖配置
* @typedef {object} LotteryOptions
* @property {number[]} uid 用户标识
* @property {string} dyid 动态标识
* @property {number} type 动态类型
* @property {string} relay_chat 动态类型
* @property {boolean} isOfficialLottery 是否官方抽奖
* @property {string} relay_chat 转发词
* @property {string} ctrl 定位@
* @property {string} rid 评论类型
* @property {string} rid 评论标识
* @property {number} chat_type 评论类型
*/
/**
* @returns {Promise<LotteryOptions[] | null>}
* @returns {Promise<LotteryOptions[]>}
*/
async filterLotteryInfo() {
const self = this,
protoLotteryInfo = typeof self.UID === 'number'
? await self.getLotteryInfoByUID(self.UID)
: await self.getLotteryInfoByTag(self.tag_name);
const { lottery_param, LotteryInfoMap, attentionList } = this;
let protoLotteryInfo = await LotteryInfoMap.get(lottery_param[0])(lottery_param[1]);
if (protoLotteryInfo === null)
return [];
log.info('筛选动态', `开始筛选(${protoLotteryInfo.length})`);
log.debug('未进行筛选的动态信息', protoLotteryInfo);
/** 所有抽奖信息 */
let alllotteryinfo = [];
const { model, chatmodel, is_imitator, maxday: _maxday, minfollower, skip_official_verify, only_followed, at_users, blockword, blacklist } = config;
const maxday = _maxday * 86400
let dyids_set = new Set();
for (const info of protoLotteryInfo) {
const { lottery_info_type, uids, uname, dyid, official_verify, ctrl, befilter, rid, des, type, hasOfficialLottery } = info;
const { key_words, model, chatmodel, is_imitator, only_followed, at_users, blockword, blacklist } = config;
/**Map<String, Boolean> */
let dyids_map = new Map();
/**dyid去重 */
if (dyids_set.has(dyid)) continue;
dyids_set.add(dyid);
/**判断是转发源动态还是现动态 */
const uid = lottery_info_type === 'tag' ? uids[0] : uids[1];
const now_ts_10 = Date.now() / 1000;
let onelotteryinfo = {};
let isLottery = false;
let isSendChat = false;
let isBlock = false;
let ts = 0;
const description = typeof des === 'string' ? des : '';
for (let index = 0; index < blockword.length; index++) {
const word = blockword[index];
const reg = new RegExp(word);
isBlock = reg.test(description) ? true : false;
if (isBlock) break;
}
if (isBlock) continue;
const needAt = /(?:@|艾特)[^@|(艾特)]*?好友/.test(description);
const needTopic = (/(?<=[带加上](?:话题|tag))#.*#/i.exec(description) || [])[0];
const isTwoLevelRelay = /\/\/@/.test(description);
const haslottery = /[抽奖]/.test(description);
const hasGuanZhuan = /[转关].*[转关]/.test(description);
if (lottery_info_type === 'uid' && is_imitator) {
isLottery = true;
} else if (hasOfficialLottery && model[0] === '1') {
({ ts } = await BiliAPI.getLotteryNotice(dyid));
if (ts < 0) { alllotteryinfo = null; break }
isLottery = ts > now_ts_10 && ts < now_ts_10 + maxday;
isSendChat = chatmodel[0] === '1';
} else if (!hasOfficialLottery && model[1] === '1' && haslottery && hasGuanZhuan && !isTwoLevelRelay) {
({ ts } = Base.getLotteryNotice(description));
if (official_verify && skip_official_verify) {
isLottery = ts === 0 || (ts > now_ts_10 && ts < now_ts_10 + maxday);
} else {
const followerNum = await BiliAPI.getUserInfo(uid);
if (followerNum < 0) { alllotteryinfo = null; break }
if (followerNum < minfollower) continue;
isLottery = !befilter && (ts === 0 || (ts > now_ts_10 && ts < now_ts_10 + maxday));
}
isSendChat = chatmodel[1] === '1';
/**去重 */
protoLotteryInfo = protoLotteryInfo.filter(({ dyid }) => {
if (dyids_map.has(dyid)) {
return false
}
dyids_map.set(dyid, false);
return true
});
/**并发查询dyid */
await Promise.all(
[...dyids_map.keys()]
.map(it => MyStorage
.searchDyid(it)
.then(hasIt => dyids_map.set(it, hasIt))
)
)
/* 检查动态是否满足要求 */
await Util.try_for_each(protoLotteryInfo, async function ({
lottery_info_type, uids,
uname, dyid,
ctrl, rid, des, type,
hasOfficialLottery
}) {
/* 遇到转发过就退出 */
if (dyids_map.get(dyid)) return false;
const
/**判断是转发源动态还是现动态 */
uid = lottery_info_type === ('tag' || 'article') ? uids[0] : uids[1]
, isFollowed = (new RegExp(uid)).test(attentionList)
, description = typeof des === 'string' ? des : ''
, needAt = /(?:@|艾特)[^@|(艾特)]*?好友/.test(description)
, needTopic = (/(?<=[带加上](?:话题|tag))#.*#/i.exec(description) || [])[0]
, isRelayDynamic = type === 1
, isTwoLevelDynamic = /\/\/@/.test(description)
, has_key_words = key_words.every(it => new RegExp(it).test(description))
, isBlock = new RegExp(blockword.join('|')).test(description)
, isLottery =
(is_imitator && lottery_info_type === 'uid' && model !== '00')
|| (hasOfficialLottery && model[0] === '1')
|| (!hasOfficialLottery && model[1] === '1' && !isTwoLevelDynamic && has_key_words)
, isSendChat =
(is_imitator && lottery_info_type === 'uid' && chatmodel !== '00')
|| (hasOfficialLottery && chatmodel[0] === '1')
|| (!hasOfficialLottery && chatmodel[1] === '1');
/**屏蔽词 */
if (isBlock) return false;
/**若勾选只转已关注 */
if (only_followed && !isFollowed) return false;
/* 获取黑名单并去重合并 */
const { blacklist: remote_blacklist } = GlobalVar.get("remoteconfig")
, new_blacklist = remote_blacklist
? [...new Set([...blacklist.split(','), ...remote_blacklist.split(',')])].join()
: blacklist;
if ((new RegExp(dyid + '|' + uid)).test(new_blacklist)) return false;
if (isLottery) {
/* 判断是否关注过 */
const isFollowed = (new RegExp(uid)).test(self.attentionList);
if (only_followed && !isFollowed) continue;
/* 判断是否转发过 */
const isRelay = await MyStorage.searchDyid(dyid);
/* 获取黑名单并去重合并 */
const { blacklist: remote_blacklist } = GlobalVar.get("remoteconfig");
const new_blacklist = remote_blacklist ?
Array.from(new Set([...blacklist.split(','), ...remote_blacklist.split(',')])).toString() : blacklist;
/* 进行判断 */
if ((new RegExp(dyid + '|' + uid)).test(new_blacklist)) continue;
onelotteryinfo.uid = [] /**初始化待关注列表 */
if (!isFollowed) onelotteryinfo.uid.push(uid);
if (!isRelay) {
onelotteryinfo.dyid = dyid;
let RandomStr = Base.getRandomOne(config.relay);
let new_ctrl = [];
if (needTopic) {
RandomStr += needTopic
}
if (needAt) {
at_users.forEach(it => {
new_ctrl.push({
data: String(it[1]),
location: RandomStr.length,
length: it[0].length + 1,
type: 1
})
RandomStr += '@' + it[0]
})
}
if (type === 1) {
/* 转发内容长度+'//'+'@'+用户名+':'+源内容 */
const addlength = RandomStr.length + 2 + uname.length + 1 + 1;
onelotteryinfo.relay_chat = RandomStr + `//@${uname}:` + des;
let onelotteryinfo = {};
onelotteryinfo.isOfficialLottery = hasOfficialLottery;
/**初始化待关注列表 */
onelotteryinfo.uid = []
if (!isFollowed) {
onelotteryinfo.uid.push(uid);
}
onelotteryinfo.dyid = dyid;
let
/**转发评语 */
RandomStr = Util.getRandomOne(config.relay),
/**控制字段 */
new_ctrl = [];
/* 是否需要带话题 */
if (needTopic) {
RandomStr += needTopic
}
/* 是否需要@ */
if (needAt) {
at_users.forEach(it => {
new_ctrl.push({
data: String(uid),
location: RandomStr.length + 2,
length: uname.length + 1,
data: String(it[1]),
location: RandomStr.length,
length: it[0].length + 1,
type: 1
})
ctrl.map(item => {
item.location += addlength;
return item;
}).forEach(it => new_ctrl.push(it))
if (!(new RegExp(uids[1])).test(self.attentionList))
onelotteryinfo.uid.push(uids[1]);
} else {
onelotteryinfo.relay_chat = RandomStr;
}
onelotteryinfo.ctrl = JSON.stringify(new_ctrl);
RandomStr += '@' + it[0]
})
}
/* 是否是转发的动态 */
if (isRelayDynamic) {
/* 转发内容长度+'//'+'@'+用户名+':'+源内容 */
const addlength = RandomStr.length + 2 + uname.length + 1 + 1;
onelotteryinfo.relay_chat = RandomStr + `//@${uname}:` + des;
new_ctrl.push({
data: String(uid),
location: RandomStr.length + 2,
length: uname.length + 1,
type: 1
})
ctrl.map(item => {
item.location += addlength;
return item;
}).forEach(it => new_ctrl.push(it))
if (!(new RegExp(uids[1])).test(attentionList))
onelotteryinfo.uid.push(uids[1]);
} else {
onelotteryinfo.relay_chat = RandomStr;
}
onelotteryinfo.ctrl = JSON.stringify(new_ctrl);
/* 根据动态的类型决定评论的类型 */
onelotteryinfo.type = type === 2 ?
11 : type === 4 || type === 1 ?
17 : type === 8 ?
1 : 0;
onelotteryinfo.chat_type =
type === 2
? 11
: type === 4 || type === 1
? 17
: type === 8
? 1
: 0;
/* 是否评论 */
if (isSendChat) onelotteryinfo.rid = rid;
if (onelotteryinfo.dyid) alllotteryinfo.push(onelotteryinfo);
alllotteryinfo.push(onelotteryinfo);
}
}
})
return alllotteryinfo;
}
/**
@@ -241,34 +318,41 @@ class Monitor extends Public {
* 41 - 转发失败
*/
async go(option) {
let status = 1;
const { uid, dyid, type, rid, relay_chat, ctrl } = option;
log.debug('正在转发的动态信息', option);
if (hasEnv('NOT_GO_LOTTERY')) {
log.info('NOT_GO_LOTTERY', 'ON');
return 0
}
const { uid, dyid, chat_type, rid, relay_chat, ctrl } = option;
/* 评论 */
if (typeof rid === 'string' && type !== 0) {
const send = () => BiliAPI.sendChat(rid, Base.getRandomOne(config.chat) || relay_chat, type);
if (rid && chat_type) {
let status = 0;
const max_retry_times = 5;
for (let times = 0; times < max_retry_times && status; times++) {
status = await send();
if (status) {
Base.tooltip.log(`将在 ${times + 1} 分钟后再次发送评论(${times + 1}/${max_retry_times})`)
await Base.delay(60 * 1000 * (times + 1))
for (let times = 0; times < max_retry_times; times++) {
status = await BiliAPI.sendChat(rid, Util.getRandomOne(config.chat), chat_type)
if (!status) {
break
}
log.info('自动评论', `将在 ${times + 1} 分钟后再次发送评论(${times + 1}/${max_retry_times})`)
await Util.delay(60 * 1000 * (times + 1))
}
if (status) return 11;
}
/* 关注 */
if (uid.length) {
status = 0
for (let index = 0; index < uid.length && !status; index++) {
const one_uid = uid[index];
if (typeof one_uid === 'number') {
status = await BiliAPI.autoAttention(uid);
await Base.delay(5000);
if (!status) await BiliAPI.movePartition(uid, this.tagid);
}
}
const [u1, u2] = uid
if (u1) {
let status = await BiliAPI.autoAttention(u1)
if (status) return 20 + status;
status = await BiliAPI.movePartition(u1, this.tagid)
if (status) return 20 + status;
if (u2) {
Util.delay(5000)
status = await BiliAPI.autoAttention(u2)
if (status) return 20 + status;
status = await BiliAPI.movePartition(u2, this.tagid)
if (status) return 20 + status;
}
}
/* 点赞 */
@@ -277,11 +361,9 @@ class Monitor extends Public {
/* 转发 */
if (await BiliAPI.autoRelay(GlobalVar.get("myUID"), dyid, relay_chat, ctrl)) return 41;
/* 延时 */
await Base.delay(config.wait * (Math.random() + 0.5));
return 0
}
}
module.exports = Monitor;
+11 -31
View File
@@ -1,31 +1,7 @@
const Base = require("./Base");
const GihubAPI = require("./GithubAPI");
const Util = require("./Util");
const { log } = Util;
const MyStorage = {
/**
* 初始化dyid.txt
*/
init: () => {
return new Promise((resolve) => {
GihubAPI.listArtifacts().then(async artifact => {
const { id, size_in_bytes } = artifact;
if (size_in_bytes) {
GihubAPI.downloadArtifacts(id).then(async status => {
if (status) {
Base.tooltip.log('存储初始化成功');
} else {
Base.tooltip.log('存储初始化失败');
await Base.createFile('dyid.txt', '', 'a')
}
resolve()
})
} else {
await Base.createFile('dyid.txt', '', 'a')
resolve()
}
})
});
},
/**
* 搜索dyid
* @param {string} dyid
@@ -34,7 +10,7 @@ const MyStorage = {
searchDyid: (dyid) => {
return new Promise((resolve) => {
const Rdyid = new RegExp(dyid);
const rs = Base.readDyidFile(Number(process.env.NUMBER));
const rs = Util.readDyidFile(Number(process.env.NUMBER));
let status = false;
rs.on('data', chunk => {
if (Rdyid.test(chunk)) {
@@ -45,7 +21,7 @@ const MyStorage = {
resolve(status)
})
rs.on('error', err => {
Base.tooltip.log(err)
log.error('搜索dyid', err)
resolve(status)
})
})
@@ -55,19 +31,23 @@ const MyStorage = {
* @param {string} dyid
*/
updateDyid: (dyid) => {
Base.tooltip.log('写入已转发过的动态信息');
log.info('更新dyid', `写入${dyid}`);
if (dyid.length !== Util.dyid_length) {
log.error('更新dyid', `dyid(${dyid})长度不为18 若出现此问题请即时通知开发者`)
}
return new Promise((resolve) => {
const ws = Base.writeDyidFile(Number(process.env.NUMBER));
const ws = Util.writeDyidFile(Number(process.env.NUMBER));
ws.write(dyid + ',', () => {
ws.destroy();
resolve()
})
ws.on('error', err => {
Base.tooltip.log(err)
log.error('更新dyid', err)
resolve()
})
});
}
}
module.exports = MyStorage;
+310 -192
View File
@@ -1,176 +1,239 @@
const Base = require('./Base');
const Util = require('./Util');
const BiliAPI = require('./BiliAPI');
const config = require("./config");
const MyStorage = require('./MyStorage');
const { log } = Util
/**
* 解析dynamic_detail_card
* 提取出的有用动态信息
* @typedef {object} UsefulDynamicInfo
* @property {number} uid
* @property {string} uname
* @property {number} createtime 10
* @property {string} rid_str
* @property {string} dynamic_id
* @property {number} type
* @property {string} description
* @property {boolean} hasOfficialLottery
* @property {Array<Object.<string,string|number>>} ctrl
*
* @property {number} origin_uid
* @property {string} origin_uname
* @property {string} origin_rid_str
* @property {string} origin_dynamic_id
* @property {number} orig_type
* @property {string} origin_description
* @property {boolean} origin_hasOfficialLottery
*
* @param {object} dynamic_detail_card
* @return {UsefulDynamicInfo}
*/
function parseDynamicCard(dynamic_detail_card) {
const { strToJson } = Util;
/**临时储存单个动态中的信息 */
let obj = {};
const { desc, card, extension, extend_json } = dynamic_detail_card
, { info } = desc.user_profile
, cardToJson = strToJson(card)
, { item } = cardToJson;
/* 转发者的UID */
obj.uid = info.uid;
/* 转发者的name */
obj.uname = info.uname;
/* 动态的ts10 */
obj.createtime = desc.timestamp
/* 动态类型 */
obj.type = desc.type
/* 用于发送评论 */
obj.rid_str = desc.rid_str.length > 12 ? desc.dynamic_id_str : desc.rid_str;
/* 源动态类型 */
obj.orig_type = desc.orig_type
/* 转发者的动态ID !!!!此为大数需使用字符串值,不然JSON.parse()会有丢失精度 */
obj.dynamic_id = desc.dynamic_id_str;
/* 定位@信息 */
obj.ctrl = (extend_json && strToJson(extend_json).ctrl) || [];
/* 是否有官方抽奖 */
obj.hasOfficialLottery = extension && extension.lott && true;
/* 转发者的描述 后两个分别是视频动态的描述和视频本身的描述*/
obj.description = (item && (item.content || item.description || cardToJson.dynamic || cardToJson.desc)) || '';
if (obj.type === 1) {
const { origin_extension, origin } = cardToJson
, originToJson = strToJson(origin)
, { user, item } = originToJson;
/* 被转发者的UID */
obj.origin_uid = desc.origin.uid;
/* 被转发者的rid(用于发评论) */
obj.origin_rid_str = desc.origin.rid_str.length > 12 ? desc.origin.dynamic_id_str : desc.origin.rid_str;
/* 被转发者的动态的ID !!!!此为大数需使用字符串值,不然JSON.parse()会有丢失精度 */
obj.origin_dynamic_id = desc.orig_dy_id_str;
/* 是否有官方抽奖 */
obj.origin_hasOfficialLottery = origin_extension && origin_extension.lott;
/* 被转发者的name */
obj.origin_uname = (user && (user.name || user.uname)) || '';
/* 被转发者的描述 */
obj.origin_description = (item && (item.content || item.description || originToJson.dynamic || originToJson.desc)) || '';
}
return obj
}
/**
* 处理来自个人动态或话题页面的一组动态数据
* @param {String} res
* @returns {{modifyDynamicResArray: UsefulDynamicInfo[], nextinfo: {has_more: number, next_offset: string}} | UsefulDynamicInfo |null}
*/
function modifyDynamicRes(res) {
const
strToJson = Util.strToJson
, { data, code } = strToJson(res)
, { cards = [], has_more, offset } = data;
if (code !== 0) {
log.error('处理动态数据', '获取动态数据出错,可能是访问太频繁 \n' + res);
return null;
}
if (!cards.length) {
log.warn('处理动态数据', '未找到任何动态信息')
}
const
/**
* 字符串offset防止损失精度
*/
next = {
has_more,
next_offset: typeof offset === 'string'
? offset
: /(?<=next_offset":)[0-9]+/.exec(res)[0]
},
/**
* 储存获取到的一组动态中的信息
*/
array = next.has_more === 0
? []
: cards.map(parseDynamicCard)
log.info('处理动态数据', `动态数据读取完毕(${cards.length})(${next.has_more})`);
return {
modifyDynamicResArray: array,
nextinfo: next
}
}
/**
* 基础功能
*/
class Public {
constructor() { }
/**
* 提取出的有用动态信息
* @typedef {object} UsefulDynamicInfo
* @property {number} uid
* @property {string} uname
* @property {boolean} official_verify
* @property {number} createtime 10
* @property {string} rid_str
* @property {string} dynamic_id
* @property {number} type
* @property {string} description
* @property {boolean} hasOfficialLottery
* @property {Array<Object.<string,string|number>>} ctrl
*
* @property {number} origin_uid
* @property {string} origin_uname
* @property {boolean} origin_official_verify
* @property {string} origin_rid_str
* @property {string} origin_dynamic_id
* @property {number} orig_type
* @property {string} origin_description
* @property {boolean} origin_hasOfficialLottery
*/
/**
* 检查所有的动态信息
* @param {string} UID 指定的用户UID
* @param {number} pages 读取页数
* @param {number} time 时延
* @param {string} [_offset] 默认'0'
* @returns {Promise<{allModifyDynamicResArray: UsefulDynamicInfo[];offset: string}>} 获取前 `pages*12` 个动态信息
*/
async checkAllDynamic(hostuid, pages, time = 0, _offset = '0') {
Base.tooltip.log(`准备读取${pages}页动态`);
const mDR = this.modifyDynamicRes,
getOneDynamicInfoByUID = BiliAPI.getOneDynamicInfoByUID,
curriedGetOneDynamicInfoByUID = Base.curryify(getOneDynamicInfoByUID); /* 柯里化的请求函数 */
/**
* 储存了特定UID的请求函数
*/
let hadUidGetOneDynamicInfoByUID = curriedGetOneDynamicInfoByUID(hostuid);
/**
* 储存所有经过整理后信息
* [{}{}...{}]
*/
let allModifyDynamicResArray = [];
let offset = _offset;
for (let i = 0; i < pages; i++) {
Base.tooltip.log(`正在读取其中第${i + 1}页动态`);
let OneDynamicInfo = await hadUidGetOneDynamicInfoByUID(offset);
const mDRdata = mDR(OneDynamicInfo);
if (mDRdata === null) {
break;
}
/**
* 储存一片动态信息
* [{}{}...{}]
*/
const mDRArry = mDRdata.modifyDynamicResArray,
nextinfo = mDRdata.nextinfo;
if (nextinfo.has_more === 0) {
offset = nextinfo.next_offset;
Base.tooltip.log(`成功读取${i + 1}页信息(已经是最后一页了故无法读取更多)`);
break;
} else {
allModifyDynamicResArray.push.apply(allModifyDynamicResArray, mDRArry);
i + 1 < pages ? Base.tooltip.log(`开始读取第${i + 2}页动态信息`) : Base.tooltip.log(`${pages}页信息全部成功读取完成`);
offset = nextinfo.next_offset;
}
await Base.delay(time);
}
return ({ allModifyDynamicResArray, offset });
}
/**
* 互动抽奖
* 处理来自动态页面的数据
* @param {String} res
* @returns {{modifyDynamicResArray: UsefulDynamicInfo[];nextinfo: {has_more: number;next_offset: string;};} | null}
*/
modifyDynamicRes(res) {
const strToJson = Base.strToJson,
jsonRes = strToJson(res),
{ data } = jsonRes;
if (jsonRes.code !== 0) {
Base.tooltip.warn('获取动态数据出错,可能是访问太频繁');
return null;
}
/* 字符串防止损失精度 */
const offset = typeof data.offset === 'string' ? data.offset : /(?<=next_offset":)[0-9]*/.exec(res)[0]
, next = {
has_more: data.has_more,
next_offset: offset
};
/**
* 储存获取到的一组动态中的信息
*/
let array = [];
if (next.has_more === 0) {
Base.tooltip.log('动态数据读取完毕');
} else {
/**
* 空动态无cards
*/
const Cards = data.cards;
Cards.forEach(onecard => {
/**临时储存单个动态中的信息 */
let obj = {};
const { desc, card } = onecard
, { info, card: user_profile_card } = desc.user_profile
, { official_verify } = user_profile_card
, cardToJson = strToJson(card);
obj.uid = info.uid; /* 转发者的UID */
obj.uname = info.uname;/* 转发者的name */
obj.official_verify = official_verify.type > -1 ? true : false; /* 是否官方号 */
obj.createtime = desc.timestamp /* 动态的ts10 */
obj.type = desc.type /* 动态类型 */
obj.rid_str = desc.rid_str.length > 12 ? desc.dynamic_id_str : desc.rid_str;/* 用于发送评论 */
obj.orig_type = desc.orig_type /* 源动态类型 */
obj.dynamic_id = desc.dynamic_id_str; /* 转发者的动态ID !!!!此为大数需使用字符串值,不然JSON.parse()会有丢失精度 */
const { extension, extend_json } = onecard;
obj.ctrl = (typeof extend_json === 'undefined') ? [] : strToJson(extend_json).ctrl || []; /* 定位@信息 */
obj.hasOfficialLottery = (typeof extension === 'undefined') ? false : typeof extension.lott === 'undefined' ? false : true; /* 是否有官方抽奖 */
const item = cardToJson.item || {};
obj.description = item.content || item.description || ''; /* 转发者的描述 */
if (obj.type === 1) {
obj.origin_uid = desc.origin.uid; /* 被转发者的UID */
obj.origin_rid_str = desc.origin.rid_str.length > 12 ? desc.origin.dynamic_id_str : desc.origin.rid_str; /* 被转发者的rid(用于发评论) */
obj.origin_dynamic_id = desc.orig_dy_id_str; /* 被转发者的动态的ID !!!!此为大数需使用字符串值,不然JSON.parse()会有丢失精度 */
const { origin_extension, origin_user } = cardToJson;
try {
obj.origin_official_verify = typeof origin_user === 'undefined' ?
false : origin_user.card.official_verify.type < 0 ?
false : true; /* 是否官方号 */
} catch (error) {
obj.origin_official_verify = false;
}
obj.origin_hasOfficialLottery = typeof origin_extension === 'undefined' ?
false : typeof origin_extension.lott === 'undefined' ?
false : true; /* 是否有官方抽奖 */
const origin = cardToJson.origin || '{}';
const { user, item } = strToJson(origin);
obj.origin_uname = typeof user === 'undefined' ? '' : user.name || user.uname || ''; /* 被转发者的name */
obj.origin_description = typeof item === 'undefined' ? '' : item.content || item.description || ''; /* 被转发者的描述 */
}
array.push(obj);
});
}
return {
modifyDynamicResArray: array,
nextinfo: next
};
}
/**
* 整理后的抽奖信息
* @typedef {object} LotteryInfo
* @property {string} lottery_info_type
* @property {number[]} uids `[uid,ouid]`
* @property {string} uname
* @property {Array<{}>} ctrl
* @property {string} dyid
* @property {boolean} befilter
* @property {boolean} official_verify 官方认证
* @property {string} rid
* @property {string} des
* @property {number} type
* @property {boolean} hasOfficialLottery 是否官方
*/
/**
* 检查指定用户的所有的动态信息
* @param {string} UID 指定的用户UID
* @param {number} pages 读取页数
* @param {number} time 时延
* @param {string} [offset] 默认'0'
* @returns {Promise<{allModifyDynamicResArray: UsefulDynamicInfo[], offset: string}>} 获取前 `pages*12` 个动态信息
*/
async checkAllDynamic(hostuid, pages, time = 0, offset = '0') {
log.info('检查所有动态', `准备读取${pages}页动态`);
const { getOneDynamicInfoByUID } = BiliAPI,
/**
* 柯里化请求函数
*/
curriedGetOneDynamicInfoByUID = Util.curryify(getOneDynamicInfoByUID),
/**
* 储存了特定UID的请求函数
*/
hadUidGetOneDynamicInfoByUID = curriedGetOneDynamicInfoByUID(hostuid);
/**
* 储存所有经过整理后信息
* @type { UsefulDynamicInfo[] }
*/
let allModifyDynamicResArray = [];
for (let i = 0; i < pages; i++) {
log.info('检查所有动态', `正在读取其中第${i + 1}页动态`);
const
OneDynamicInfo = await hadUidGetOneDynamicInfoByUID(offset),
mDRdata = modifyDynamicRes(OneDynamicInfo);
if (mDRdata === null) {
break;
}
const
/**
* 一片动态
*/
mDRArry = mDRdata.modifyDynamicResArray,
nextinfo = mDRdata.nextinfo;
if (nextinfo.has_more === 0) {
offset = nextinfo.next_offset;
log.info('检查所有动态', `成功读取${i + 1}页信息(已经是最后一页了故无法读取更多)`);
break;
} else {
/**合并 */
allModifyDynamicResArray.push.apply(allModifyDynamicResArray, mDRArry);
log.info('检查所有动态', `开始读取第${i + 2}页动态信息`)
offset = nextinfo.next_offset;
}
await Util.delay(time);
}
log.info('检查所有动态', `${pages}页信息读取完成`)
return ({ allModifyDynamicResArray, offset });
}
/**
* 获取最新动态信息(转发子动态)
* 并初步整理
* @param {string} UID
* @returns {Promise<LotteryInfo[] | null>}
*/
async getLotteryInfoByUID(UID) {
log.info('获取动态', `开始获取用户${UID}的动态信息`);
const { allModifyDynamicResArray } = await this.checkAllDynamic(UID, config.uid_scan_page, config.search_wait);
if (!allModifyDynamicResArray.length) return null;
const fomatdata = allModifyDynamicResArray.map(o => {
return {
lottery_info_type: 'uid',
uids: [o.uid, o.origin_uid],
uname: o.origin_uname,
ctrl: [],
dyid: o.origin_dynamic_id,
rid: o.origin_rid_str,
des: o.origin_description,
type: o.orig_type,
hasOfficialLottery: o.origin_hasOfficialLottery
}
}).filter(a => a.type != 0)
log.info('获取动态', `成功获取用户${UID}的动态信息`);
return fomatdata;
}
/**
* 获取tag下的抽奖信息(转发母动态)
* 并初步整理
@@ -178,71 +241,126 @@ class Public {
* @returns {Promise<LotteryInfo[] | null>}
*/
async getLotteryInfoByTag(tag_name) {
const self = this,
const
tag_id = await BiliAPI.getTagIDByTagName(tag_name),
hotdy = await BiliAPI.getHotDynamicInfoByTagID(tag_id),
modDR = self.modifyDynamicRes(hotdy);
modDR = modifyDynamicRes(hotdy);
if (modDR === null) return null;
Base.tooltip.log(`开始获取带话题#${tag_name}#的动态信息`);
Base.tooltip.log('成功获取热门动态');
let mDRdata = modDR.modifyDynamicResArray; /* 热门动态 */
log.info('获取动态', `开始获取带话题#${tag_name}#的动态信息`);
log.info('获取动态', '成功获取热门动态');
/**
* 热门动态
*/
let mDRdata = modDR.modifyDynamicResArray;
let next_offset = modDR.nextinfo.next_offset;
for (let index = 0; index < config.scan_page_num; index++) {
Base.tooltip.log(`成功读取${index + 1}页动态`);
const newdy = await BiliAPI.getOneDynamicInfoByTag(tag_name, next_offset);
const _modify = self.modifyDynamicRes(newdy);
for (let index = 0; index < config.tag_scan_page; index++) {
log.info('获取动态', `读取第${index + 1}页动态`);
const
newdy = await BiliAPI.getOneDynamicInfoByTag(tag_name, next_offset),
_modify = modifyDynamicRes(newdy);
if (_modify === null) return null;
mDRdata.push.apply(mDRdata, _modify.modifyDynamicResArray);
next_offset = _modify.nextinfo.next_offset;
await Base.delay(config.search_wait);
await Util.delay(config.search_wait);
}
const fomatdata = mDRdata.map(o => {
const hasOrigin = o.type === 1;
return {
lottery_info_type: 'tag',
uids: [o.uid, o.origin_uid],
uname: o.uname,
ctrl: o.ctrl,
dyid: o.dynamic_id,
official_verify: o.official_verify,
befilter: hasOrigin,
rid: o.rid_str,
des: o.description,
type: o.type,
hasOfficialLottery: o.hasOfficialLottery
};
})
Base.tooltip.log(`成功获取带话题#${tag_name}#的动态信息`);
log.info('获取动态', `成功获取带话题#${tag_name}#的动态信息`);
return fomatdata
}
/**
* 获取最新动态信息(转发子动态)
* 并初步整理
* @param {string} UID
* 从专栏中获取抽奖信息
* @param {string} key_words
* @returns {Promise<LotteryInfo[] | null>}
*/
async getLotteryInfoByUID(UID) {
Base.tooltip.log(`开始获取用户${UID}动态信息`);
const { allModifyDynamicResArray: aMDRA } = await this.checkAllDynamic(UID, config.scan_page_num, config.search_wait);
if (!aMDRA.length) return null;
const fomatdata = aMDRA.map(o => {
return {
lottery_info_type: 'uid',
uids: [o.uid, o.origin_uid],
uname: o.origin_uname,
ctrl: [],
dyid: o.origin_dynamic_id,
official_verify: o.origin_official_verify,
befilter: false,
rid: o.origin_rid_str,
des: o.origin_description,
type: o.orig_type,
hasOfficialLottery: o.origin_hasOfficialLottery
async getLotteryInfoByArticle(key_words) {
log.info('获取动态', `开始获取含关键词${key_words}专栏信息`);
const cvs = (await BiliAPI.searchArticlesByKeyword(key_words)).slice(0, config.article_scan_page);
let dyinfos = [];
for (const cv of cvs) {
const
content = await BiliAPI.getOneArticleByCv(cv),
dyids = content.match(/(?<=t.bilibili.com\/)[0-9]+/g) || [],
/**判断此专栏是否查看过的权重 */
weight = dyids.length / 2;
let { length } = dyids;
log.info('获取动态', `提取专栏(${cv})中提及的dyid(${length})`)
for (const dyid of dyids) {
let _weight = 0;
const isRelayed = await MyStorage.searchDyid(dyid);
if (isRelayed) {
_weight += 1;
}
if (_weight >= weight) {
log.info('获取动态', `1/2动态曾经转过,该专栏或已查看,故中止`)
dyinfos = []
break
}
if (dyid.length === Util.dyid_length) {
if (!isRelayed) {
log.info('获取动态', `查看动态(${dyid})的细节 (${length--})`)
const res = await BiliAPI.getOneDynamicByDyid(dyid)
, { code, data } = Util.strToJson(res)
, { card } = data;
if (code !== 0) {
log.error('获取动态', '获取动态数据出错,可能是访问太频繁 \n' + res)
break
}
await Util.delay(2000)
if (card) {
dyinfos.push(parseDynamicCard(card));
}
} else {
log.info('获取动态', `动态(${dyid})已转发过 (${length--})`)
}
} else {
log.warn('获取动态', `动态(${dyid})无效 (${length--})`)
}
}
}).filter(a => a.type === 0 ? false : true)
Base.tooltip.log(`成功获取用户${UID}的动态信息`);
return fomatdata;
}
const fomatdata = dyinfos.map(o => {
return {
lottery_info_type: 'article',
uids: [o.uid, o.origin_uid],
uname: o.uname,
ctrl: o.ctrl,
dyid: o.dynamic_id,
rid: o.rid_str,
des: o.description,
type: o.type,
hasOfficialLottery: o.hasOfficialLottery
};
})
log.info('获取动态', `成功获取含关键词${key_words}的专栏信息`);
return fomatdata
}
}
module.exports = Public;
module.exports = Public;
-52
View File
@@ -1,52 +0,0 @@
const { config_file, tooltip } = require('./Base');
const default_script = {
author: '@shanmite',
UIDs: [
31252386,
35719643,
689277291
],
TAGs: [
'互动抽奖',
'抽奖',
'转发抽奖',
'动态抽奖',
]
};
/**
* 自己修改过的设置
*/
const my_config = (() => {
let _my_config = {}
if (process.env.LOCALLAUNCH) {
try {
const { UIDs, TAGs } = require(config_file);
if (UIDs) _my_config.UIDs = UIDs;
if (TAGs) _my_config.TAGs = TAGs;
} catch (e) {
tooltip.log("[script]无自定义设置\n" + e);
}
} else {
try {
const { MY_CONFIG } = process.env;
if (MY_CONFIG) {
const { UIDs, TAGs } = JSON.parse(MY_CONFIG);
if (UIDs) _my_config.UIDs = UIDs;
if (TAGs) _my_config.TAGs = TAGs;
}
} catch (_) {
tooltip.log("[script]MY_CONFIG语法错误");
}
}
return _my_config;
})();
const script = {
...default_script,
...my_config
}
module.exports = script;
+98 -89
View File
@@ -1,3 +1,4 @@
const chalk = require('chalk');
const fs = require('fs');
const path = require('path');
const { HttpRequest } = require("./HttpRequest");
@@ -5,7 +6,7 @@ const { HttpRequest } = require("./HttpRequest");
/**
* 基础工具
*/
const Base = {
const Util = {
/**环境变量设置文件 */
env_file: path.join(process.cwd(), "env.js"),
/**配置文件 */
@@ -17,7 +18,7 @@ const Base = {
* 超出精度的数转为字符串
* @param {string} params
* @return {object}
* 返回对象
* 返回对象 解析失败返回 `{}`
*/
strToJson(params) {
const isJSON = (str => {
@@ -34,11 +35,22 @@ const Base = {
})(params);
return isJSON ? isJSON : {}
},
/**
* @template T
* @param {Array<T>} iter
* @param {(value: T) => Promise<Boolean>} fn 返回true整体退出
*/
async try_for_each(iter, fn) {
for (const item of iter) {
if (await fn(item)) break
}
},
/**
* 函数柯里化
* @param {function} func
* @template T
* @param {(arg, arg) => T} func
* 要被柯里化的函数
* @returns {function}
* @returns {(arg) => (arg) => T)}
* 一次接受一个参数并返回一个接受余下参数的函数
*/
curryify(func) {
@@ -57,6 +69,7 @@ const Base = {
* @returns {Promise<void>}
*/
delay(time) {
Util.log.info('时延', `${time}ms`);
return new Promise(resolve => {
setTimeout(() => {
resolve();
@@ -82,108 +95,99 @@ const Base = {
},
/**
* 随机获取数组中的一个元素
* @param {any[]} arr
* @returns {any}
* @template T
* @param {T[]} arr
* @returns {T}
*/
getRandomOne(arr) {
let RandomOne = null;
if (arr instanceof Array && arr.length) {
if (Array.isArray(arr) && arr.length) {
RandomOne = arr[parseInt(Math.random() * arr.length)];
}
return RandomOne
},
/**提示信息 */
tooltip: {
log: (msg) => {
console.log("%s: %s", new Date().toLocaleString(), msg)
},
warn: (msg) => {
console.log("%s: %s", new Date().toLocaleString(), msg)
}
hasEnv(env_name) {
return process.env[env_name] ? true : false;
},
/**
* 提取开奖信息
* @param {string} des 描述
* @returns {
{
ts: number|0;
text:string|'开奖时间: 未填写开奖时间';
item:string|'请自行查看';
isMe:string|'请自行查看';
/**日志 */
log: {
level: 0,
/**
* 初始化默认level为3
*/
init() {
let _level = Number(process.env.LOTTERY_LOG_LEVEL);
this.level = isNaN(_level) ? 3 : _level;
},
/**
* @param {String|Array<String>} msg
* @param {String} [split] 分隔符
*/
proPrint(msg, split = ' ') {
console.log([msg].flat().join(split))
},
debug(context, msg) {
if (this.level > 3) {
if (msg instanceof Object) msg = JSON.stringify(msg, null, 4);
this.proPrint([chalk.hex('#64B3FF')(`[${Date()}]`), chalk.grey("[Debug]"), chalk.hex('#FFA500')(`[${context}]`), chalk.hex('#0070BB')(`[\n${msg}\n]`)])
}
* }
*/
getLotteryNotice(des) {
const r = /([\d零一二两三四五六七八九十]+)[.月]([\d零一二两三四五六七八九十]+)[日号]?/;
if (des === '') return {
ts: 0,
text: `开奖时间: 未填写开奖时间`,
item: '请自行查看',
isMe: '请自行查看'
},
info(context, msg) {
if (this.level > 2)
this.proPrint([chalk.hex('#64B3FF')(`[${Date()}]`), chalk.grey("[Info]"), chalk.hex('#FFA500')(`[${context}]`), chalk.hex('#48BB31')(`[${msg}]`)])
},
warn(context, msg) {
if (this.level > 1)
this.proPrint([chalk.hex('#64B3FF')(`[${Date()}]`), chalk.grey("[Warn]"), chalk.hex('#FFA500')(`[${context}]`), chalk.hex('#BBBB23')(`[${msg}]`)])
},
error(context, msg) {
if (this.level > 0)
this.proPrint([chalk.hex('#64B3FF')(`[${Date()}]`), chalk.grey("[Error]"), chalk.hex('#FFA500')(`[${context}]`), chalk.hex('#FF0006')(`[${msg}]`)])
}
const _date = r.exec(des) || [];
const timestamp10 = ((month, day) => {
if (month && day) {
let date = new Date(`${new Date(Date.now()).getFullYear()}-${month}-${day} 23:59:59`).getTime()
if (!isNaN(date)) return date / 1000;
}
return 0
})(_date[1], _date[2])
if (timestamp10 === 0) return {
ts: 0,
text: `开奖时间: 未填写开奖时间`,
item: '请自行查看',
isMe: '请自行查看'
}
const timestamp13 = timestamp10 * 1000,
time = new Date(timestamp13);
const remain = (() => {
const timestr = ((timestamp13 - Date.now()) / 86400000).toString()
, timearr = timestr.replace(/(\d+)\.(\d+)/, "$1,0.$2").split(',');
const text = timearr[0][0] === '-' ? `开奖时间已过${timearr[0].substring(1)}天余${parseInt(timearr[1] * 24)}小时` : `还有${timearr[0]}天余${parseInt(timearr[1] * 24)}小时`;
return text
})();
return {
ts: timestamp10,
text: `开奖时间: ${time.toLocaleString()} ${remain}`,
item: '请自行查看',
isMe: '请自行查看'
};
},
/**
* 获取远程设置
* @returns {Promise<JSON>}
*/
getRemoteConfig: () => new Promise((resolve) => {
HttpRequest({
method: 'GET',
url: 'https://gitee.com/shanmite/lottery-notice/raw/master/notice.json',
headers: {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
accept: 'text/plain; charset=utf-8',
referer: 'https://gitee.com/shanmite/lottery-notice/blob/master/notice.json',
},
success: res => {
try {
const body = JSON.parse(res.body);
if (body.node_msg) console.log(body.node_msg);
resolve(body.config)
} catch (error) {
resolve(JSON.parse('{}'))
getRemoteConfig() {
return new Promise((resolve) => {
HttpRequest({
method: 'GET',
url: 'https://gitee.com/shanmite/lottery-notice/raw/master/notice.json',
headers: {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
accept: 'text/plain; charset=utf-8',
referer: 'https://gitee.com/shanmite/lottery-notice/blob/master/notice.json',
},
success: res => {
try {
const body = JSON.parse(res.body);
if (body.node_msg) this.log.info('公告', '\n' + body.node_msg + '\n');
resolve(body.config)
} catch (error) {
resolve(JSON.parse('{}'))
}
},
failure: err => {
this.log.error('获取远程设置', '获取远程设置错误: ' + err);
resolve(JSON.parse('{}'));
}
},
failure: err => {
console.log('获取远程设置错误: ' + err);
resolve(JSON.parse('{}'));
}
})
})
}),
},
/**
*
* @param {string} ts 13位时间戳
* @param {number} zoneOffset 东时区记做正数
* 是否存在文件或目录
* @param {string} path
* @returns
*/
transformTimeZone: (ts, zoneOffset) => new Date(new Date(ts).getTime() + zoneOffset * 60 * 60 * 1000).toLocaleString(),
hasFileOrDir(path) {
try {
fs.accessSync(path, fs.constants.F_OK)
} catch (_) {
return false
}
return true
},
/**
* 生成文件夹
* @param {string} dirname
@@ -225,6 +229,10 @@ const Base = {
})
});
},
/**
* dyid长度
*/
dyid_length: 18,
/**
* 读取dyid文件
* @param {number} num
@@ -232,7 +240,7 @@ const Base = {
*/
readDyidFile(num) {
const fpath = num < 2 ? path.join(this.dyids_dir, 'dyid.txt') : path.join(this.dyids_dir, `dyid${num}.txt`);
return fs.createReadStream(fpath, { encoding: 'utf8', highWaterMark: 19 * 1000 })
return fs.createReadStream(fpath, { encoding: 'utf8', highWaterMark: (this.dyid_length + 1) * 1000 })
},
/**
* 追加dyid
@@ -245,4 +253,5 @@ const Base = {
}
};
module.exports = Base;
module.exports = Util;
+15 -25
View File
@@ -1,13 +1,9 @@
const Base = require("./Base");
const { log, delay } = require("./Util");
const BiliAPI = require("./BiliAPI");
const { trigger } = require("./GithubAPI");
const Public = require("./Public");
const GlobalVar = require('./GlobalVar');
const { followWhiteList } = require("./config");
const config = require("./config");
/** Github运行限制时间*/
const MAX_TIME = 6 * 60 * 60 * 1000 - 6 * 60 * 1000;
/**动态偏移量 */
let offset = '0';
@@ -17,16 +13,19 @@ let offset = '0';
*/
async function getFollowList() {
const tagid = await BiliAPI.checkMyPartition(config.clear_partition);
if (typeof tagid === 'undefined') { Base.tooltip.log('未能成功获取关注分区id'); return }
if (typeof tagid === 'undefined') {
log.info('获取关注列表', '未能成功获取关注分区id');
return
}
let rmup = [];
for (let index = 1; index < 42; index++) {
const uids = await BiliAPI.getPartitionUID(tagid, index);
await Base.delay(2e3);
await delay(2e3);
if (!uids.length) break;
rmup.push(...uids)
}
return followWhiteList.length
? rmup.filter(uid => followWhiteList.split(',').indexOf(String(uid)) === -1)
return config.clear_white_list.length
? rmup.filter(uid => config.clear_white_list.split(',').indexOf(String(uid)) === -1)
: rmup;
}
@@ -34,17 +33,7 @@ async function getFollowList() {
* 清理动态和关注
*/
async function clear() {
config.updata(process.env.NUMBER);
const { clear_max_day, clear_remove_dynamic, clear_remove_attention, clear_remove_delay, clear_dynamic_type } = config;
if (!process.env.LOCALLAUNCH) {
setTimeout(() => {
Base.tooltip.log('运行时间超过Actions上限 - 6小时');
Base.tooltip.log('尝试再次触发clear 并中止此任务')
trigger('clear.yml').then(() => {
process.exit(0);
})
}, MAX_TIME);
}
const { clear_white_list, clear_max_day, clear_remove_dynamic, clear_remove_attention, clear_remove_delay, clear_dynamic_type } = config;
let success = true;
const uid_list = await getFollowList();
if (!clear_remove_dynamic && clear_remove_attention) {
@@ -54,7 +43,7 @@ async function clear() {
/* 取消关注 */
success = await BiliAPI.cancelAttention(uid);
/* 延时 */
await Base.delay(clear_remove_delay);
await delay(clear_remove_delay);
if (!success) break;
}
@@ -63,9 +52,9 @@ async function clear() {
let before_separate = [];
const MY_UID = Number(GlobalVar.get('myUID'));
for (let index = 0; ; index++) {
log.info('清理动态', `开始读取第${index + 1}页(12条)`);
const { allModifyDynamicResArray, offset: _offset } = await Public.prototype.checkAllDynamic(GlobalVar.get("myUID"), 1, 5 * 1000, offset);
offset = _offset;
Base.tooltip.log(`开始读取第${index + 1}页(12条)`);
for (let index = 0; index < allModifyDynamicResArray.length; index++) {
const res = allModifyDynamicResArray[index];
const { type, dynamic_id, createtime } = res;
@@ -74,7 +63,8 @@ async function clear() {
if ((now - createtime) > 86400 * clear_max_day) {
/* 移除动态 */
if (dynamic_id
&& clear_remove_dynamic) {
&& clear_remove_dynamic
&& !(new RegExp(dynamic_id).test(clear_white_list))) {
success = await BiliAPI.rmDynamic(dynamic_id)
}
@@ -88,7 +78,7 @@ async function clear() {
}
/* 延时 */
await Base.delay(clear_remove_delay);
await delay(clear_remove_delay);
if (!success) break;
} else {
@@ -96,7 +86,7 @@ async function clear() {
}
}
}
Base.tooltip.log(`${index}页中的转发动态与关注全部处理成功`)
log.info('清理动态', `${index + 1}页中的转发动态与关注全部处理成功`)
if (offset === '0' || !success) break;
}
}
+7 -14
View File
@@ -1,14 +1,5 @@
const { config_file, tooltip } = require('./Base');
const my_config = (() => {
let _my_config = {}
try {
_my_config = require(config_file)
} catch (e) {
tooltip.log("[config]自定义设置异常 原因如下:\n" + e);
}
return _my_config;
})();
const { config_file } = require('./Util');
const my_config = require(config_file);
const config = {
...my_config["default_config"],
@@ -16,9 +7,11 @@ const config = {
* @param {string} n
*/
updata(n) {
const new_config = my_config[`config_${n}`] || {};
Object.entries(new_config)
.forEach(([k, v]) => this[k] = v)
const new_config = my_config[`config_${n}`];
if (new_config) {
Object.entries(new_config)
.forEach(([k, v]) => this[k] = v)
}
}
};
+28 -47
View File
@@ -1,80 +1,62 @@
const Base = require('./Base');
const Util = require('./Util');
const BiliAPI = require('./BiliAPI');
const GihubAPI = require("./GithubAPI");
const eventBus = require('./eventBus');
const GlobalVar = require('./GlobalVar');
const Public = require('./Public');
const Monitor = require('./Monitor');
const { sendNotify } = require('./sendNotify');
const config = require('./config');
/** Github运行限制时间*/
const MAX_TIME = 6 * 60 * 60 * 1000 - 6 * 60 * 1000;
const { log } = Util;
async function createRandomDynamic(num) {
Base.tooltip.log(`准备创建${num}条随机动态`);
if (config.create_dy) {
log.info('随机动态', `准备创建${num}条随机动态`);
const Dynamic = await Public.prototype.checkAllDynamic(GlobalVar.get("myUID"), 1);
if ((Dynamic.allModifyDynamicResArray[0] || { type: 0 }).type === 1) {
for (let index = 0; index < num; index++) {
await BiliAPI.createDynamic(Base.getRandomOne(config.dy_contents));
await Base.delay(2000);
await BiliAPI.createDynamic(Util.getRandomOne(config.dy_contents));
await Util.delay(2000);
}
} else {
Base.tooltip.log('已有非抽奖动态故无需创建');
log.info('随机动态', '已有非抽奖动态故无需创建');
}
}
}
/**
* 主函数
* 抽奖主函数
* @param {string} cookie
* @returns {Promise<void>}
*/
function start() {
return new Promise(resolve => {
config.updata(process.env.NUMBER);
let times = Base.counter();
let times = Util.counter();
/* 注册事件 */
eventBus.on('Turn_on_the_Monitor', async () => {
const lotterys = GlobalVar.get("Lottery");
if (lotterys.length === 0) {
Base.tooltip.log('抽奖信息为空');
log.info('抽奖', '抽奖信息为空');
resolve();
return;
}
if (times.value() === lotterys.length) {
await createRandomDynamic(config.create_dy_num);
Base.tooltip.log('所有动态转发完毕');
log.info('抽奖', '所有动态转发完毕');
times.clear();
eventBus.emit('Turn_off_the_Monitor', '目前无抽奖信息,过一会儿再来看看吧')
return;
}
const lottery = lotterys[times.next()];
const nlottery = Number(lottery);
await (new Monitor(isNaN(nlottery) ? lottery : nlottery)).init();
await (new Monitor(lottery)).init();
});
eventBus.on('Turn_off_the_Monitor', async (msg) => {
await createRandomDynamic(config.create_dy_num);
Base.tooltip.log('[运行结束]程序自动关闭 原因: ' + msg);
log.info('结束抽奖', '原因: ' + msg);
eventBus.flush();
resolve();
})
if (process.env.LOCALLAUNCH) {
eventBus.emit('Turn_on_the_Monitor');
} else {
setTimeout(() => {
eventBus.emit('Turn_off_the_Monitor', '运行时间超过Actions上限 - 6小时')
}, MAX_TIME);
GihubAPI.hasLotteryRun().then(s => {
if (s) {
Base.tooltip.log("有冲突的工作流故终止此任务");
resolve()
} else {
eventBus.emit('Turn_on_the_Monitor');
}
})
}
eventBus.emit('Turn_on_the_Monitor');
});
}
@@ -82,24 +64,23 @@ function start() {
* 是否中奖
*/
async function isMe() {
config.updata(process.env.NUMBER);
let desp = '';
const UnreadAtNum = await BiliAPI.getUnreadAtNum();
const UnreadSessionNum = await BiliAPI.getUnreadSessionNum();
const { follow_unread, unfollow_unread } = UnreadSessionNum || { unfollow_unread: 0, follow_unread: 0 };
if (UnreadAtNum > 0) {
Base.tooltip.log('<-- 正在检查at');
log.info('中奖检测', '<-- 正在检查at');
const MyAtInfo = await BiliAPI.getMyAtInfo();
MyAtInfo.forEach(async AtInfo => {
const { at_time, up_uname, business, source_content, url } = AtInfo
desp += `发生时间: ${Base.transformTimeZone(at_time * 1000, 8)} \n\n`
desp += `发生时间: ${new Date(at_time * 1000).toLocaleString()} \n\n`
desp += `用户: ${up_uname} \n\n`
desp += `${business}中@了你(${GlobalVar.get("myUID")}) \n\n`
desp += `原内容为: ${source_content} \n\n`
desp += `[直达链接](${url}) \n\n`
desp += `\n\n\n\n`
});
Base.tooltip.log('--> OK');
log.info('中奖检测', '--> OK');
}
if (follow_unread + unfollow_unread > 0) {
const check = async (type) => {
@@ -112,9 +93,9 @@ async function isMe() {
session_t = session_ts;
if (talker_id) {
BiliAPI.updateSessionStatus(talker_id);
await Base.delay(1000);
if (/中奖|获得|填写|写上|提供|地址|支付宝账号|码|大会员/.test(content)) {
desp += `发生时间: ${Base.transformTimeZone(timestamp * 1000, 8)} \n\n`
await Util.delay(1000);
if (/中奖|获得|填写|写上|提供|收货地址|支付宝账号|码|大会员/.test(content)) {
desp += `发生时间: ${new Date(timestamp * 1000).toLocaleString()} \n\n`
desp += `用户: ${sender_uid} \n\n`
desp += `私信你(${GlobalVar.get("myUID")})说: ${content} \n\n`
desp += `[直达链接](https://message.bilibili.com/#/whisper/mid${sender_uid}) \n\n`
@@ -123,27 +104,27 @@ async function isMe() {
}
}
if (MySession.has_more) {
await Base.delay(3e3);
await Util.delay(3e3);
MySession = await BiliAPI.getSessionInfo(type, session_t)
}
max++
} while (MySession.has_more && max < 10);
}
if (follow_unread) {
Base.tooltip.log('<-- 正在检查已关注者的私信')
log.info('中奖检测', '<-- 正在检查已关注者的私信')
await check(1)
}
if (unfollow_unread) {
Base.tooltip.log('<-- 正在检查未关注者的私信')
log.info('中奖检测', '<-- 正在检查未关注者的私信')
await check(2)
}
Base.tooltip.log('--> OK');
log.info('中奖检测', '--> OK');
}
if (desp) {
Base.tooltip.log(desp);
log.info(desp);
await sendNotify('可能中奖了', desp);
} else {
Base.tooltip.log("未中奖");
log.info('中奖检测', "未中奖");
}
return;
}
@@ -154,10 +135,10 @@ async function isMe() {
*/
async function checkCookie(num) {
if (await BiliAPI.getMyinfo()) {
Base.tooltip.log('成功登录');
log.info('Cookie有效性检测', '成功登录');
return true;
} else {
Base.tooltip.log(`登录失败 COOKIE${num} 已失效 UID:${GlobalVar.get("myUID")}`);
log.error('Cookie有效性检测', `登录失败 COOKIE${num} 已失效 UID:${GlobalVar.get("myUID")}`);
await sendNotify('动态抽奖出错-登录失败', `COOKIE${num} 已失效 UID:${GlobalVar.get("myUID")}`)
return false;
}
+58 -58
View File
@@ -1,4 +1,4 @@
const { tooltip } = require("./Base");
const { log } = require("./Util");
const { HttpRequest } = require("./HttpRequest");
const { createTransport } = require("nodemailer");
@@ -202,26 +202,26 @@ function serverNotify(text, desp) {
try {
const data = JSON.parse(res.body);
if (data.errno === 0) {
tooltip.log('server酱发送通知消息成功\n')
log.info('发送通知','server酱发送通知消息成功\n')
} else if (data.errno === 1024) {
// 一分钟内发送相同的内容会触发
tooltip.log(`server酱发送通知消息异常: ${data.errmsg}\n`)
log.error('发送通知',`server酱发送通知消息异常: ${data.errmsg}\n`)
} else {
tooltip.log(`server酱发送通知消息异常\n${JSON.stringify(data)}`)
log.error('发送通知',`server酱发送通知消息异常\n${JSON.stringify(data)}`)
}
} catch (error) {
tooltip.log(error);
log.error('发送通知',error);
} finally {
resolve()
}
},
failure: err => {
tooltip.log('server酱 发送通知调用API失败!!\n')
log.error('发送通知','server酱 发送通知调用API失败!!\n')
throw new Error(err);
}
})
} else {
tooltip.log('您未提供server酱的SCKEY,取消微信推送消息通知\n');
log.warn('发送通知','您未提供server酱的SCKEY,取消微信推送消息通知\n');
resolve()
}
})
@@ -247,23 +247,23 @@ function serverNotifyTurbo(text, desp) {
try {
const data = JSON.parse(res.body);
if (data.code === 0) {
tooltip.log('server酱(Turbo版)发送通知消息成功\n')
log.info('发送通知','server酱(Turbo版)发送通知消息成功\n')
} else {
tooltip.log(`server酱(Turbo版)发送通知消息异常\n${JSON.stringify(data)}`)
log.error('发送通知',`server酱(Turbo版)发送通知消息异常\n${JSON.stringify(data)}`)
}
} catch (error) {
tooltip.log(error);
log.error('发送通知',error);
} finally {
resolve()
}
},
failure: err => {
tooltip.log('server酱(Turbo版) 发送通知调用API失败!!\n')
log.error('发送通知','server酱(Turbo版) 发送通知调用API失败!!\n')
throw new Error(err);
}
})
} else {
tooltip.log('您未提供server酱(Turbo版)的SCKEY,取消微信推送消息通知\n');
log.warn('发送通知','您未提供server酱(Turbo版)的SCKEY,取消微信推送消息通知\n');
resolve()
}
})
@@ -301,25 +301,25 @@ function coolPush(text, desp) {
try {
const data = JSON.parse(res.body);
if (data.code === 200) {
tooltip.log(`酷推发送${pushMode(QQ_MODE)}通知消息成功\n`)
log.info('发送通知',`酷推发送${pushMode(QQ_MODE)}通知消息成功\n`)
} else if (data.code === 400) {
tooltip.log(`QQ酷推(Cool Push)发送${pushMode(QQ_MODE)}推送失败:${data}\n`)
log.error('发送通知',`QQ酷推(Cool Push)发送${pushMode(QQ_MODE)}推送失败:${data}\n`)
} else {
tooltip.log(`酷推推送异常: ${data.msg}`);
log.error('发送通知',`酷推推送异常: ${data.msg}`);
}
} catch (error) {
tooltip.log(error);
log.error('发送通知',error);
} finally {
resolve()
}
},
failure: err => {
tooltip.log(`酷推 发送${pushMode(QQ_MODE)}通知调用API失败!!\n`)
log.error('发送通知',`酷推 发送${pushMode(QQ_MODE)}通知调用API失败!!\n`)
throw new Error(err);
}
})
} else {
tooltip.log('您未提供酷推的SKEY,取消QQ推送消息通知\n');
log.warn('发送通知','您未提供酷推的SKEY,取消QQ推送消息通知\n');
resolve()
}
})
@@ -345,24 +345,24 @@ function barkNotify(text, desp, params = {}) {
try {
const data = JSON.parse(res.body);
if (data.code === 200) {
tooltip.log('Bark APP发送通知消息成功\n')
log.info('发送通知','Bark APP发送通知消息成功\n')
} else {
tooltip.log(`${data.message}\n`);
log.error('发送通知',`${data.message}\n`);
}
} catch (error) {
tooltip.log(error);
log.error('发送通知',error);
} finally {
resolve()
}
},
failure: err => {
tooltip.log('Bark APP发送通知调用API失败!!\n');
log.error('发送通知','Bark APP发送通知调用API失败!!\n');
resolve();
throw new Error(err)
}
})
} else {
tooltip.log('您未提供Bark的APP推送BARK_PUSH,取消Bark推送消息通知\n');
log.warn('发送通知','您未提供Bark的APP推送BARK_PUSH,取消Bark推送消息通知\n');
resolve()
}
})
@@ -391,20 +391,20 @@ function tgBotNotify(text, desp) {
try {
const data = JSON.parse(res.body);
if (data.ok) {
tooltip.log('Telegram发送通知消息完成。\n')
log.info('发送通知','Telegram发送通知消息完成。\n')
} else if (data.error_code === 400) {
tooltip.log('请主动给bot发送一条消息并检查接收用户ID是否正确。\n')
log.error('发送通知','请主动给bot发送一条消息并检查接收用户ID是否正确。\n')
} else if (data.error_code === 401) {
tooltip.log('Telegram bot token 填写错误。\n')
log.error('发送通知','Telegram bot token 填写错误。\n')
}
} catch (error) {
tooltip.log(error);
log.error('发送通知',error);
} finally {
resolve()
}
},
failure: err => {
tooltip.log('telegram发送通知消息失败!!\n')
log.error('发送通知','telegram发送通知消息失败!!\n')
resolve()
throw new Error(err)
}
@@ -417,7 +417,7 @@ function tgBotNotify(text, desp) {
}
HttpRequest(options)
} else {
tooltip.log('您未提供telegram机器人推送所需的TG_BOT_TOKEN和TG_USER_ID,取消telegram推送消息通知\n');
log.warn('发送通知','您未提供telegram机器人推送所需的TG_BOT_TOKEN和TG_USER_ID,取消telegram推送消息通知\n');
resolve()
}
})
@@ -430,7 +430,7 @@ function ddBotNotify(text, desp) {
const dateNow = Date.now();
const hmac = crypto.createHmac('sha256', DD_BOT_SECRET);
hmac.update(`${dateNow}\n${DD_BOT_SECRET}`);
const result = encodeURIComponent(hmac.digest('base64'));
const result = encodeURIComponent(hmac.digest('Util64'));
HttpRequest({
method: 'POST',
url: `https://oapi.dingtalk.com/robot/send`,
@@ -456,18 +456,18 @@ function ddBotNotify(text, desp) {
try {
const data = JSON.parse(res.body);
if (data.errcode === 0) {
tooltip.log('钉钉发送通知消息完成。\n')
log.info('发送通知','钉钉发送通知消息完成。\n')
} else {
tooltip.log(`${data.errmsg}\n`)
log.error('发送通知',`${data.errmsg}\n`)
}
} catch (e) {
tooltip.log(e);
log.error('发送通知',e);
} finally {
resolve();
}
},
failure: err => {
tooltip.log('钉钉发送通知消息失败!!\n')
log.error('发送通知','钉钉发送通知消息失败!!\n')
throw new Error(err);
}
})
@@ -495,24 +495,24 @@ function ddBotNotify(text, desp) {
try {
const data = JSON.parse(res.body);
if (data.errcode === 0) {
tooltip.log('钉钉发送通知消息完成。\n')
log.info('发送通知','钉钉发送通知消息完成。\n')
} else {
tooltip.log(`${data.errmsg}\n`)
log.error('发送通知',`${data.errmsg}\n`)
}
} catch (e) {
tooltip.log(e);
log.error('发送通知',e);
} finally {
resolve();
}
},
failure: err => {
tooltip.log('钉钉发送通知消息失败!!\n');
log.error('发送通知','钉钉发送通知消息失败!!\n');
resolve();
throw new Error(err)
}
})
} else {
tooltip.log('您未提供钉钉机器人推送所需的DD_BOT_TOKEN或者DD_BOT_SECRET,取消钉钉推送消息通知\n');
log.warn('发送通知','您未提供钉钉机器人推送所需的DD_BOT_TOKEN或者DD_BOT_SECRET,取消钉钉推送消息通知\n');
resolve()
}
})
@@ -541,24 +541,24 @@ function qywxBotNotify(text, desp) {
try {
const data = JSON.parse(res.body);
if (data.errcode === 0) {
tooltip.log('企业微信发送通知消息完成。\n');
log.info('发送通知','企业微信发送通知消息完成。\n');
} else {
tooltip.log(`${data.errmsg}\n`);
log.error('发送通知',`${data.errmsg}\n`);
}
} catch (e) {
tooltip.log(e);
log.error('发送通知',e);
} finally {
resolve();
}
},
failure: err => {
tooltip.log('企业微信发送通知消息失败!!\n');
log.error('发送通知','企业微信发送通知消息失败!!\n');
resolve();
throw new Error(err)
}
})
} else {
tooltip.log('您未提供企业微信机器人推送所需的QYWX_KEY,取消企业微信推送消息通知\n');
log.warn('发送通知','您未提供企业微信机器人推送所需的QYWX_KEY,取消企业微信推送消息通知\n');
resolve();
}
});
@@ -570,7 +570,7 @@ function iGotNotify(text, desp, params = {}) {
// 校验传入的IGOT_PUSH_KEY是否有效
const IGOT_PUSH_KEY_REGX = new RegExp("^[a-zA-Z0-9]{24}$")
if (!IGOT_PUSH_KEY_REGX.test(IGOT_PUSH_KEY)) {
tooltip.log('您所提供的IGOT_PUSH_KEY无效\n')
log.error('发送通知','您所提供的IGOT_PUSH_KEY无效\n')
resolve()
return
}
@@ -593,24 +593,24 @@ function iGotNotify(text, desp, params = {}) {
try {
const data = JSON.parse(res.body);
if (data.ret === 0) {
tooltip.log('iGot发送通知消息成功\n')
log.info('发送通知','iGot发送通知消息成功\n')
} else {
tooltip.log(`iGot发送通知消息失败:${data.errMsg}\n`)
log.error('发送通知',`iGot发送通知消息失败:${data.errMsg}\n`)
}
} catch (e) {
tooltip.log(e);
log.error('发送通知',e);
} finally {
resolve();
}
},
failure: err => {
tooltip.log('iGot 发送通知调用API失败!!\n')
log.error('发送通知','iGot 发送通知调用API失败!!\n')
resolve();
throw new Error(err)
}
})
} else {
tooltip.log('您未提供iGot的推送IGOT_PUSH_KEY,取消iGot推送消息通知\n');
log.warn('发送通知','您未提供iGot的推送IGOT_PUSH_KEY,取消iGot推送消息通知\n');
resolve()
}
})
@@ -639,24 +639,24 @@ function pushPlusNotify(text, desp) {
try {
const data = JSON.parse(res.body);
if (data.code === 200) {
tooltip.log(`push+发送${PUSH_PLUS_USER ? '一对多' : '一对一'}通知消息完成。\n`)
log.info('发送通知',`push+发送${PUSH_PLUS_USER ? '一对多' : '一对一'}通知消息完成。\n`)
} else {
tooltip.log(`push+发送${PUSH_PLUS_USER ? '一对多' : '一对一'}通知消息失败:${data.msg}\n`)
log.error('发送通知',`push+发送${PUSH_PLUS_USER ? '一对多' : '一对一'}通知消息失败:${data.msg}\n`)
}
} catch (e) {
tooltip.log(e);
log.error('发送通知',e);
} finally {
resolve();
}
},
failure: err => {
tooltip.log(`push+发送${PUSH_PLUS_USER ? '一对多' : '一对一'}通知消息失败!!\n`)
log.error('发送通知',`push+发送${PUSH_PLUS_USER ? '一对多' : '一对一'}通知消息失败!!\n`)
resolve();
throw new Error(err)
}
})
} else {
tooltip.log('您未提供push+推送所需的PUSH_PLUS_TOKEN,取消push+推送消息通知\n');
log.warn('发送通知','您未提供push+推送所需的PUSH_PLUS_TOKEN,取消push+推送消息通知\n');
resolve()
}
})
@@ -679,12 +679,12 @@ async function email(text, desp) {
text: desp,
})
} catch (e) {
tooltip.log(`email发送失败 原因: ${e.message}`);
log.error('发送通知',`email发送失败 原因: ${e.message}`);
return
}
tooltip.log('email发送成功');
log.info('发送通知','email发送成功');
} else {
tooltip.log('您未提供email推送所需的所有参数故取消email推送消息通知\n');
log.warn('发送通知','您未提供email推送所需的所有参数故取消email推送消息通知\n');
}
}
+23 -33
View File
@@ -1,48 +1,38 @@
const Base = require("./Base");
const Util = require("./Util");
const GlobalVar = require("./GlobalVar");
const MyStorage = require("./MyStorage");
const Script = require("./Script");
const config = require("./config");
const key_map = new Map([['DedeUserID', 'myUID'], ['bili_jct', 'csrf']]);
let __GlobalVar = {
cookie: '',
/**自己的UID*/
myUID: '',
/**防跨站请求伪造*/
csrf: '',
/**
* 抽奖信息
* @type {(string|number)[]}
*/
Lottery: [...Script.UIDs, ...Script.TAGs],
remoteconfig: {},
}
/**
* 生成全局变量文件
* 更新config
* @param {string} cookie
* @param {number} n
*/
async function setVariable(cookie, n) {
if (cookie) {
const key = ['DedeUserID', 'bili_jct']
__GlobalVar.cookie = cookie;
config.updata(process.env.NUMBER);
GlobalVar.set('cookie', cookie);
cookie.split(/\s*;\s*/).forEach(item => {
const _item = item.split('=');
if (key.indexOf(_item[0]) !== -1) __GlobalVar[_item[0]] = _item[1];
})
__GlobalVar.csrf = __GlobalVar.bili_jct;
__GlobalVar.myUID = __GlobalVar.DedeUserID;
__GlobalVar.remoteconfig = await Base.getRemoteConfig();
GlobalVar.updateAll(__GlobalVar);
}
await Base.createDir('dyids');
if (process.env.PAT) {
if (process.env.CI) {
process.env.NUMBER = 1;
}
await MyStorage.init()
} else {
await Base.createFile(n < 2 ? 'dyid.txt' : `dyid${n}.txt`, '', 'a')
if (key_map.has(_item[0]))
GlobalVar.set(key_map.get(_item[0]), _item[1]);
});
const { UIDs = [], TAGs = [], Articles = [] } = config;
GlobalVar.set('Lottery', [
...UIDs.map(it => ['UIDs', it]),
...TAGs.map(it => ['TAGs', it]),
...Articles.map(it => ['Articles', it])
]);
GlobalVar.set('remoteconfig', await Util.getRemoteConfig());
}
await Util.createDir('dyids');
await Util.createFile(n < 2 ? 'dyid.txt' : `dyid${n}.txt`, '', 'a')
return
}
+111 -71
View File
@@ -1,90 +1,130 @@
const { env_file, tooltip, delay } = require("./lib/Base");
const { env_file, config_file, log, delay, hasFileOrDir } = require("./lib/Util");
const metainfo = [
` _ _ _ _____ _ _ `,
` | | | | | | / ____| (_) | | `,
` | | ___ | |_| |_ ___ _ __ _ _| (___ ___ _ __ _ _ __ | |_ `,
` | | / _ \\| __| __/ _ \\ '__| | | |\\___ \\ / __| '__| | '_ \\| __|`,
` | |___| (_) | |_| || __/ | | |_| |____) | (__| | | | |_) | |_ `,
` |______\\___/ \\__|\\__\\___|_| \\__, |_____/ \\___|_| |_| .__/ \\__|`,
` __/ | | | `,
` |___/ |_| `,
` `,
` v1.9.6 by shanmite`,
]
/**多账号存储 */
let multiple_account = [];
/**循环等待时间 */
let loop_wait = 0;
if (!process.env.CI) {
const { initEnv, multiple_account_parm } = require(env_file);
if (multiple_account_parm) {
multiple_account = multiple_account_parm;
}
initEnv()
}
/**
* @returns {Promise<string>} 错误信息
*/
async function main() {
const { COOKIE, NUMBER, CLEAR, PAT, LOCALLAUNCH, ENABLE_MULTIPLE_ACCOUNT, MULTIPLE_ACCOUNT } = process.env;
if (LOCALLAUNCH || PAT) {
if (ENABLE_MULTIPLE_ACCOUNT) {
let muti_acco = multiple_account.length
? multiple_account
: JSON.parse(MULTIPLE_ACCOUNT);
const { COOKIE, NUMBER, CLEAR, ENABLE_MULTIPLE_ACCOUNT, MULTIPLE_ACCOUNT_PARM } = process.env;
if (ENABLE_MULTIPLE_ACCOUNT) {
let muti_acco = multiple_account.length
? multiple_account
: JSON.parse(MULTIPLE_ACCOUNT_PARM);
process.env.ENABLE_MULTIPLE_ACCOUNT = '';
process.env.ENABLE_MULTIPLE_ACCOUNT = '';
for (const acco of muti_acco) {
process.env.COOKIE = acco.COOKIE;
process.env.NUMBER = acco.NUMBER;
process.env.CLEAR = acco.CLEAR;
await main();
await delay(acco.WAIT);
}
} else {
if (COOKIE) {
const { setVariable } = require("./lib/setVariable");
await setVariable(COOKIE, Number(NUMBER));
for (const acco of muti_acco) {
process.env.COOKIE = acco.COOKIE;
process.env.NUMBER = acco.NUMBER;
process.env.CLEAR = acco.CLEAR;
await main();
await delay(acco.WAIT);
}
const { start, isMe, checkCookie } = require("./lib/lottery-in-nodejs");
const { clear } = require("./lib/clear");
/**多账号状态还原 */
process.env.ENABLE_MULTIPLE_ACCOUNT = ENABLE_MULTIPLE_ACCOUNT;
} else {
if (!COOKIE) {
return '请查看README文件, 在env.js指定位置填入cookie'
}
const { setVariable } = require("./lib/setVariable");
await setVariable(COOKIE, Number(NUMBER));
tooltip.log('[LotteryAutoScript] 账号' + NUMBER);
const { start, isMe, checkCookie } = require("./lib/lottery-in-nodejs");
const { clear } = require("./lib/clear");
if (await checkCookie(NUMBER)) {
const mode = process.env.lottery_mode;
switch (mode) {
case 'start':
tooltip.log('开始参与抽奖');
await start();
break;
case 'check':
tooltip.log('检查是否中奖');
await isMe();
break;
case 'clear':
if (CLEAR) {
tooltip.log('开始清理动态');
await clear();
tooltip.log('清理动态完毕');
}
break;
default:
console.log(`Usage: lottery-in-bili [OPTIONS]`)
console.log(`错误OPTIONS: ${mode} 正确OPTIONS: start,check,clear`);
log.info('main', '当前为第' + NUMBER + '个账号');
if (await checkCookie(NUMBER)) {
const mode = process.env.lottery_mode;
const help_msg = "用法: lottery [OPTIONS]\n\nOPTIONS:\n\tstart 启动抽奖\n\tcheck 中奖检查\n\tclear 清理动态和关注\n\thelp 帮助信息";
const { lottery_loop_wait, check_loop_wait, clear_loop_wait } = require("./lib/config");
switch (mode) {
case 'start':
log.info('抽奖', '开始运行');
loop_wait = lottery_loop_wait;
await start();
break;
case 'check':
log.info('中奖检测', '检查是否中奖');
loop_wait = check_loop_wait;
await isMe();
break;
case 'clear':
if (CLEAR) {
log.info('清理动态', '开始运行');
loop_wait = clear_loop_wait;
await clear();
}
}
} else {
tooltip.log('请查看README文件, 在env.js指定位置填入cookie')
break;
case 'help':
return help_msg
case undefined:
return "未提供以下参数\n\t[OPTIONS]\n\n" + help_msg
default:
return `提供了错误的[OPTIONS] -> ${mode}\n\n` + help_msg
}
}
} else {
tooltip.log('请查看README文件, 填入相应的PAT, 若是本地运行则设LOCALLAUNCH为true');
}
}
(async function () {
let metainfo = '';
metainfo += ` _ _ _ _____ _ _ \n`;
metainfo += ` | | | | | | / ____| (_) | | \n`;
metainfo += ` | | ___ | |_| |_ ___ _ __ _ _| (___ ___ _ __ _ _ __ | |_ \n`;
metainfo += ` | | / _ \\| __| __/ _ \\ '__| | | |\\___ \\ / __| '__| | '_ \\| __|\n`;
metainfo += ` | |___| (_) | |_| || __/ | | |_| |____) | (__| | | | |_) | |_ \n`;
metainfo += ` |______\\___/ \\__|\\__\\___|_| \\__, |_____/ \\___|_| |_| .__/ \\__|\n`;
metainfo += ` __/ | | | \n`;
metainfo += ` |___/ |_| \n`;
metainfo += ` \n`;
metainfo += ` by shanmite\n`;
console.log(metainfo);
log.proPrint(metainfo, '\n')
if (hasFileOrDir(env_file)) {
const { initEnv, multiple_account_parm } = require(env_file);
if (multiple_account_parm) {
multiple_account = multiple_account_parm;
}
initEnv();
log.init();
log.info('环境变量初始化', '成功加载env.js文件');
} else {
log.init();
log.warn('环境变量初始化', '未在当前目录下找到env.js文件 也可在环境变量中设置所需参数');
return
}
if (hasFileOrDir(config_file)) {
require("./lib/config");
log.info('配置文件初始化', '成功加载my_config.js文件');
} else {
log.error('配置文件初始化', '未在当前目录下找到my_config.js文件');
return
}
/**OPTIONS */
process.env.lottery_mode = process.argv[2]
await main();
await delay(5 * 1000);
process.exit(0)
const err_msg = await main();
if (err_msg) {
log.error('错误', '\n' + err_msg + '\n');
log.warn('结束运行', '5秒后自动退出');
await delay(5 * 1000);
} else {
while (loop_wait) {
log.info('程序休眠', `${loop_wait / 1000}秒后再次启动`)
await delay(loop_wait)
await main()
}
log.info('结束运行', '未设置休眠时间')
}
process.exit(0);
})()
+72 -21
View File
@@ -3,6 +3,41 @@ module.exports = {
* 默认设置(公用)
*/
default_config: {
/**
* 监视更转的用户uid
*/
UIDs: [
31252386,
689277291,
241675899
],
/**
* 监视的专栏关键词
*/
Articles: [
'抽奖合集'
],
/**
* 监视的tag
*/
TAGs: [
'互动抽奖',
'转发抽奖',
'动态抽奖',
'抽奖',
],
/**
* 动态中的关键词(表示须同时满足以下条件)
* 符合js正则表达式的字符串
*/
key_words: [
"[抽奖]",
"[转关].*[转关]"
],
/**
* - '00' 关闭自动抽奖
* - '10' 只转发官方抽奖
@@ -25,9 +60,19 @@ module.exports = {
is_imitator: false,
/**
* - 在uid或tag里检索的页数
* - 在uid里检索的页数
*/
scan_page_num: 3,
uid_scan_page: 3,
/**
* - 在tag里检索的页数
*/
tag_scan_page: 3,
/**
* - 获取专栏数量
*/
article_scan_page: 3,
/**
* - 开奖时间距离现在的最大天数
@@ -35,12 +80,20 @@ module.exports = {
*/
maxday: Infinity,
/**
* - 循环等待时间(指所有操作完毕后的休眠时间)
* - 单位毫秒
*/
lottery_loop_wait: 60 * 60 * 1000,
check_loop_wait: 0,
clear_loop_wait: 0,
/**
* - 转发间隔时间
* - 单位毫秒
* - 上下浮动50%
*/
wait: 100000,
wait: 30 * 1000,
/**
* - 检索动态间隔
@@ -53,11 +106,6 @@ module.exports = {
*/
minfollower: 1000,
/**
* 粉丝数限制是否跳过有官方认证的up
*/
skip_official_verify: true,
/**
* - 只转发已关注的
*/
@@ -92,19 +140,15 @@ module.exports = {
at_users: [['转发抽奖娘', 294887687], ['你的工具人老公', 100680137]],
/**
* 同步 https://gitee.com/shanmite/lottery-notice/raw/master/notice.json
* - 同步 https://gitee.com/shanmite/lottery-notice/raw/master/notice.json
* - 英文逗号分隔 如: 1,2,3
*/
blacklist: '1,2',
blacklist: '',
/**
* 屏蔽词
*/
blockword: ["脚本抽奖", "恭喜", "结果", "抽奖号"],
/**
* 取关白名单
*/
followWhiteList: '1,2',
blockword: ["脚本抽奖", "恭喜", "结果", "抽奖号", "钓鱼", "涨粉"],
/**
* 转发评语
@@ -123,7 +167,8 @@ module.exports = {
],
/**
* 抽奖UP用户分组id(网页端点击分区后地址栏中的tagid)
* - 抽奖UP用户分组id(网页端点击分区后地址栏中的tagid)
* - 自动获取
*/
partition_id: 0,
@@ -133,12 +178,19 @@ module.exports = {
is_exception: false,
/**
* 取关分区
* - 清理白名单uid或dyid
* - 英文逗号分隔 如: 1,2,3
*/
clear_white_list: '',
/**
* - 取关分区
* - 默认为: 此处存放因抽奖临时关注的up
*/
clear_partition: '',
/**
* 清理多少天的动态或关注
* 清理多少天之前的动态或关注
*/
clear_max_day: 30,
@@ -179,5 +231,4 @@ module.exports = {
config_1: {},
config_2: {},
config_3: {}
}
}
-2144
View File
File diff suppressed because it is too large Load Diff
+10 -8
View File
@@ -7,8 +7,10 @@
"start": "node main.js start",
"clear": "node main.js clear",
"check": "node main.js check",
"pkg": "npx pkg . && powershell .\\script\\pkg\\pkg.ps1",
"pkg_arm": "npx pkg -t linux-arm -o dist/lottery-in-bili-linux-arm main.js"
"pkg_x86": "npx pkg . && PowerShell .\\script\\pkg\\pkg.ps1",
"pkg_clean_dist": "PowerShell -Command \"& {Remove-Item -Path .\\dist\\ -Recurse}\"",
"pkg_arm64": "npx pkg -t linux-arm64 -o dist/lottery main.js && cp my_config.example.js my_config.js && cp env.example.js dist/env.js",
"pkg_armv7": "npx pkg -t linux-armv7 -o dist/lottery main.js && cp my_config.example.js my_config.js && cp env.example.js dist/env.js"
},
"bin": "main.js",
"pkg": {
@@ -21,7 +23,7 @@
},
"repository": {
"type": "git",
"url": "https://github.com/shanmite/AutoScript.git"
"url": "https://github.com/shanmiteko/AutoScript.git"
},
"keywords": [
"auto",
@@ -30,15 +32,15 @@
"author": "shanmite",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/shanmite/AutoScript/issues"
"url": "https://github.com/shanmiteko/AutoScript/issues"
},
"homepage": "https://github.com/shanmite/AutoScript#readme",
"homepage": "https://github.com/shanmiteko/AutoScript#readme",
"devDependencies": {
"eslint": "^7.17.0",
"pkg": "^5.2.1"
},
"dependencies": {
"nodemailer": "^6.5.0",
"unzipper": "^0.10.11"
"chalk": "^4.1.1",
"nodemailer": "^6.5.0"
}
}
}
+19 -9
View File
@@ -7,10 +7,17 @@ DYID_FOLDER=dyids
# 设置环境变量文件
ENV_FILE=env.js
# 自定义设置文件
CONFIG_FILE=my_config.json
CONFIG_FILE=my_config.js
# docker仓库
DOCKER_REPO=shanmite/lottery_auto_docker
# cdn
CDN=https://cdn.staticaly.com/gh/shanmiteko/LotteryAutoScript/main
# env.example.js文件
ENV_EXAMPLE="$CDN/env.example.js"
# my_config.example.js文件
CONFIG_EXAMPLE="$CDN/my_config.example.js"
# 新建脚本目录
if [ ! -d "$SCRIPT_FOLDER" ]; then
echo "create $SCRIPT_FOLDER"
mkdir $SCRIPT_FOLDER
@@ -18,6 +25,7 @@ fi
cd $SCRIPT_FOLDER/
# 新建dyid储存目录
if [ ! -d "$DYID_FOLDER" ]; then
echo "create $DYID_FOLDER/"
mkdir $DYID_FOLDER
@@ -25,16 +33,18 @@ else
echo "$DYID_FOLDER/ exists"
fi
# 新建环境变量设置文件
if [ ! -f "$ENV_FILE" ]; then
echo "create $ENV_FILE"
curl -fsSL https://cdn.staticaly.com/gh/shanmite/LotteryAutoScript/main/env.example.js -o $ENV_FILE
curl -fsSL $ENV_EXAMPLE -o $ENV_FILE
else
echo "$ENV_FILE exists"
fi
# 新建配置文件
if [ ! -f "$CONFIG_FILE" ]; then
echo "create $CONFIG_FILE"
echo "{}" > $CONFIG_FILE
curl -fsSL $CONFIG_EXAMPLE -o $CONFIG_FILE
else
echo "$CONFIG_FILE exists"
fi
@@ -45,8 +55,8 @@ docker -v && docker pull $DOCKER_REPO
echo "create start.sh"
echo -e "#!/bin/bash\n\
docker run \
-v $PWD/env.js:/lottery/env.js \
-v $PWD/my_config.json:/lottery/my_config.json \
-v $PWD/$ENV_FILE:/lottery/$ENV_FILE \
-v $PWD/$CONFIG_FILE:/lottery/$CONFIG_FILE \
-v $PWD/$DYID_FOLDER/:/lottery/$DYID_FOLDER/ \
$DOCKER_REPO \
start" \
@@ -56,8 +66,8 @@ chmod 777 start.sh
echo "create check.sh"
echo -e "#!/bin/bash\n\
docker run \
-v $PWD/env.js:/lottery/env.js \
-v $PWD/my_config.json:/lottery/my_config.json \
-v $PWD/$ENV_FILE:/lottery/$ENV_FILE \
-v $PWD/$CONFIG_FILE:/lottery/$CONFIG_FILE \
-v $PWD/$DYID_FOLDER/:/lottery/$DYID_FOLDER/ \
$DOCKER_REPO \
check" \
@@ -67,8 +77,8 @@ chmod 777 check.sh
echo "create clear.sh"
echo -e "#!/bin/bash\n\
docker run \
-v $PWD/env.js:/lottery/env.js \
-v $PWD/my_config.json:/lottery/my_config.json \
-v $PWD/$ENV_FILE:/lottery/$ENV_FILE \
-v $PWD/$CONFIG_FILE:/lottery/$CONFIG_FILE \
-v $PWD/$DYID_FOLDER/:/lottery/$DYID_FOLDER/ \
$DOCKER_REPO \
clear" \
+21 -17
View File
@@ -6,15 +6,11 @@ $ENV_FILE = "env.js"
$TARGET_DIR = ".\dist"
$WIN_BIN = "lottery-in-bili-win.exe"
$LINUX_BIN = "lottery-in-bili-linux"
$MACOS_BIN = "lottery-in-bili-macos"
$WIN_X64 = "nlts-win-x64"
$LINUX_X64 = "nlts-linux-x64"
$MACOS_X64 = "nlts-macos-x64"
$MAP = @{ $WIN_X64 = $WIN_BIN; $LINUX_X64 = $LINUX_BIN; $MACOS_X64 = $MACOS_BIN}
$TRIARR = @(
@("lottery-in-bili-win.exe", "lottery.exe", "nlts-win-x64"),
@("lottery-in-bili-linux", "lottery", "nlts-linux-x64"),
@("lottery-in-bili-macos", "lottery", "nlts-macos-x64")
)
Copy-Item -Path $TEMPLATE_ENV_FILE -Destination $TARGET_DIR -Force
Copy-Item -Path $TEMPLATE_CONFIG_FILE -Destination $TARGET_DIR -Force
@@ -24,15 +20,23 @@ Set-Location -Path $TARGET_DIR
Move-Item -Path $TEMPLATE_ENV_FILE -Destination $ENV_FILE -Force
Move-Item -Path $TEMPLATE_CONFIG_FILE -Destination $CONFIG_FILE -Force
foreach($X64 in $MAP.Keys) {
$BIN = $MAP[$X64]
if (!(Test-Path $X64 -PathType Container)) {
New-Item -ItemType Directory -Force -Path $X64
foreach ($TRI in $TRIARR) {
$PROTO_BIN,$BIN,$DIR = $TRI
New-Item -ItemType Directory -Force -Path $DIR
Move-Item -Path $PROTO_BIN -Destination $BIN -Force
Move-Item -Path $BIN -Destination $DIR -Force
Copy-Item -Path $ENV_FILE -Destination $DIR -Force
Copy-Item -Path $CONFIG_FILE -Destination $DIR -Force
if ($DIR -eq "nlts-win-x64") {
New-Item -Path $DIR -Name "start.bat" -ItemType File -Value "@echo off && lottery start && pause" -Force
New-Item -Path $DIR -Name "check.bat" -ItemType File -Value "@echo off && lottery check && pause" -Force
New-Item -Path $DIR -Name "clear.bat" -ItemType File -Value "@echo off && lottery clear && pause" -Force
}
Move-Item -Path $BIN -Destination $X64 -Force
Copy-Item -Path $ENV_FILE -Destination $X64 -Force
Copy-Item -Path $CONFIG_FILE -Destination $X64 -Force
Compress-Archive -Path $X64 -DestinationPath $X64 -Force
Compress-Archive -Path $DIR -DestinationPath "$($DIR)-$(Get-Date -Format "yyyyMMd")" -Force
}
Remove-Item -Path $ENV_FILE
-6
View File
@@ -1,6 +0,0 @@
const { getDyid } = require("../lib/MyStorage");
(async () => {
let alldyid = await getDyid();
console.log(alldyid);
})()
-5
View File
@@ -1,5 +0,0 @@
const { getHiToKoTo } = require("../lib/Base");
getHiToKoTo().then(h => {
console.log(h);
})
-5
View File
@@ -1,5 +0,0 @@
const { getZhihuBillBoard } = require("../lib/Base");
getZhihuBillBoard().then(x => {
console.log(x);
})
-19
View File
@@ -1,19 +0,0 @@
const { HttpRequest } = require("../lib/HttpRequest");
HttpRequest({
method: 'GET',
url: 'https://www.www.www',
headers: {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
accept: 'application/json, text/plain, */*',
},
config: {
wait: 1000
},
success: res => {
console.log(res.body);
},
failure: err => {
console.log(err);
}
})
View File
-26
View File
@@ -1,26 +0,0 @@
const { createReadStream, createWriteStream } = require('fs');
const file = 'test/huge_file/dyid.txt';
const test = 'test/huge_file/test.txt';
const ws = createReadStream(file, { encoding: 'utf8', highWaterMark: 19 * 1000 })
const rs = createWriteStream(test, { flags: 'a' })
rs.write(Date.now() + '->', () => {
console.log('写入完毕');
rs.destroy()
})
let i = 0;
ws.on('data', chunk => {
i++;
if (/000000000000000000/.test(chunk)) {
console.log('success')
console.log('in ' + i)
// 14764824 bytes
// success
// in 778
// [Done] exited with code=0 in 0.331 seconds
}
})
-1
View File
@@ -1 +0,0 @@
===1614266851666->