增加chronocat无头模式的QQNT推送 用于无法使go-cqhttp的接替 (#2141)

* 增加chronocat无头模式的QQNT推送

* Chronocat发送时没有配置群号或个人消息号发送出错

* 增加系统通知、去除注释

* 增加系统通知

---------

Co-authored-by: child <wulincheng@javalc.com>
This commit is contained in:
LinCheng Wu 2023-10-14 20:56:32 +08:00 committed by GitHub
parent 9f7beb934d
commit 5055045d22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 295 additions and 26 deletions

View File

@ -18,6 +18,7 @@ export enum NotificationMode {
'pushMe' = 'pushMe', 'pushMe' = 'pushMe',
'feishu' = 'feishu', 'feishu' = 'feishu',
'webhook' = 'webhook', 'webhook' = 'webhook',
'chronocat' = 'Chronocat',
} }
abstract class NotificationBaseInfo { abstract class NotificationBaseInfo {
@ -108,6 +109,12 @@ export class PushMeNotification extends NotificationBaseInfo {
public pushMeKey: string = ''; public pushMeKey: string = '';
} }
export class ChronocatNotification extends NotificationBaseInfo {
public chronocatURL: string = '';
public chronocatQQ: string = '';
public chronocatToekn: string = '';
}
export class WebhookNotification extends NotificationBaseInfo { export class WebhookNotification extends NotificationBaseInfo {
public webhookHeaders: string = ''; public webhookHeaders: string = '';
public webhookBody: string = ''; public webhookBody: string = '';
@ -140,4 +147,6 @@ export interface NotificationInfo
EmailNotification, EmailNotification,
PushMeNotification, PushMeNotification,
WebhookNotification, WebhookNotification,
ChronocatNotification,
LarkNotification {} LarkNotification {}

View File

@ -1,12 +1,12 @@
import { NotificationInfo } from '../data/notify';
import { Service, Inject } from 'typedi';
import winston from 'winston';
import UserService from './user';
import got from 'got';
import nodemailer from 'nodemailer';
import crypto from 'crypto'; import crypto from 'crypto';
import got from 'got';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import nodemailer from 'nodemailer';
import { Inject, Service } from 'typedi';
import winston from 'winston';
import { parseBody, parseHeaders } from '../config/util'; import { parseBody, parseHeaders } from '../config/util';
import { NotificationInfo } from '../data/notify';
import UserService from './user';
@Service() @Service()
export default class NotificationService { export default class NotificationService {
@ -31,6 +31,7 @@ export default class NotificationService {
['pushMe', this.pushMe], ['pushMe', this.pushMe],
['webhook', this.webhook], ['webhook', this.webhook],
['lark', this.lark], ['lark', this.lark],
['chronocat', this.chronocat],
]); ]);
private title = ''; private title = '';
@ -195,7 +196,8 @@ export default class NotificationService {
} }
private async bark() { private async bark() {
let { barkPush, barkIcon, barkSound, barkGroup, barkLevel, barkUrl } = this.params; let { barkPush, barkIcon, barkSound, barkGroup, barkLevel, barkUrl } =
this.params;
if (!barkPush.startsWith('http')) { if (!barkPush.startsWith('http')) {
barkPush = `https://api.day.app/${barkPush}`; barkPush = `https://api.day.app/${barkPush}`;
} }
@ -588,6 +590,63 @@ export default class NotificationService {
} }
} }
private async chronocat() {
const { chronocatURL, chronocatQQ, chronocatToekn } = this.params;
try {
const user_ids = chronocatQQ
.match(/user_id=(\d+)/g)
?.map((match: any) => match.split('=')[1]);
const group_ids = chronocatQQ
.match(/group_id=(\d+)/g)
?.map((match: any) => match.split('=')[1]);
const url = `${chronocatURL}/api/message/send`;
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${chronocatToekn}`,
};
for (const [chat_type, ids] of [
[1, user_ids],
[2, group_ids],
]) {
if (!ids) {
continue;
}
let _ids: any = ids;
for (const chat_id of _ids) {
const data = {
peer: {
chatType: chat_type,
peerUin: chat_id,
},
elements: [
{
elementType: 1,
textElement: {
content: `${this.title}\n\n${this.content}`,
},
},
],
};
const res: any = await got.post(url, {
...this.gotOption,
json: data,
headers,
});
if (res.body === 'success') {
return true;
} else {
throw new Error(res.body);
}
}
}
return false;
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async webhook() { private async webhook() {
const { const {
webhookUrl, webhookUrl,

View File

@ -173,4 +173,13 @@ export SMTP_NAME=""
## PUSHME_KEY (必填)填写PushMe APP上获取的push_key ## PUSHME_KEY (必填)填写PushMe APP上获取的push_key
export PUSHME_KEY="" export PUSHME_KEY=""
## 13. CHRONOCAT
## CHRONOCAT_URL 推送 http://127.0.0.1:16530
## CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥
## CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如user_id=xxx;group_id=xxxx;group_id=xxxxx
## CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/
export CHRONOCAT_URL=""
export CHRONOCAT_QQ="" #
export CHRONOCAT_TOKEN=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可

View File

@ -150,6 +150,15 @@ let SMTP_NAME = '';
//此处填你的PushMe KEY. //此处填你的PushMe KEY.
let PUSHME_KEY = ''; let PUSHME_KEY = '';
// =======================================CHRONOCAT通知设置区域===========================================
// CHRONOCAT_URL Red协议连接地址 例: http://127.0.0.1:16530
// CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥
// CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群
// CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/
let CHRONOCAT_URL = ''; // CHRONOCAT Red协议连接地址
let CHRONOCAT_TOKEN = ''; //CHRONOCAT 生成的访问密钥
let CHRONOCAT_QQ = ''; // 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如user_id=xxx;group_id=xxxx;group_id=xxxxx
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) { if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL; GOTIFY_URL = process.env.GOTIFY_URL;
@ -306,6 +315,16 @@ if (process.env.SMTP_NAME) {
if (process.env.PUSHME_KEY) { if (process.env.PUSHME_KEY) {
PUSHME_KEY = process.env.PUSHME_KEY; PUSHME_KEY = process.env.PUSHME_KEY;
} }
if (process.env.CHRONOCAT_URL) {
CHRONOCAT_URL = process.env.CHRONOCAT_URL;
}
if (process.env.CHRONOCAT_QQ) {
CHRONOCAT_QQ = process.env.CHRONOCAT_QQ;
}
if (process.env.CHRONOCAT_TOKEN) {
CHRONOCAT_TOKEN = process.env.CHRONOCAT_TOKEN;
}
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
/** /**
@ -355,6 +374,7 @@ async function sendNotify(
fsBotNotify(text, desp), //飞书机器人 fsBotNotify(text, desp), //飞书机器人
smtpNotify(text, desp), //SMTP 邮件 smtpNotify(text, desp), //SMTP 邮件
PushMeNotify(text, desp, params), //PushMe PushMeNotify(text, desp, params), //PushMe
ChronocatNotify(text, desp), // Chronocat
]); ]);
} }
@ -1171,6 +1191,81 @@ function PushMeNotify(text, desp, params = {}) {
}); });
} }
function ChronocatNotify(title, desp) {
return new Promise((resolve) => {
if (!CHRONOCAT_TOKEN || !CHRONOCAT_QQ || !CHRONOCAT_URL) {
console.log(
'CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送',
);
return;
}
console.log('CHRONOCAT 服务启动');
const user_ids = CHRONOCAT_QQ.match(/user_id=(\d+)/g)?.map(
(match) => match.split('=')[1],
);
const group_ids = CHRONOCAT_QQ.match(/group_id=(\d+)/g)?.map(
(match) => match.split('=')[1],
);
const url = `${CHRONOCAT_URL}/api/message/send`;
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${CHRONOCAT_TOKEN}`,
};
for (const [chat_type, ids] of [
[1, user_ids],
[2, group_ids],
]) {
if (!ids) {
continue;
}
for (const chat_id of ids) {
const data = {
peer: {
chatType: chat_type,
peerUin: chat_id,
},
elements: [
{
elementType: 1,
textElement: {
content: `${title}\n\n${desp}`,
},
},
],
};
const options = {
url: url,
json: data,
headers,
timeout,
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('Chronocat发送QQ通知消息失败\n');
console.log(err);
} else {
data = JSON.parse(data);
if (chat_type === 1) {
console.log(`QQ个人消息:${ids}推送成功!`);
} else {
console.log(`QQ群消息:${ids}推送成功!`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
}
}
});
}
module.exports = { module.exports = {
sendNotify, sendNotify,
BARK_PUSH, BARK_PUSH,

View File

@ -102,6 +102,9 @@ push_config = {
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写 'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
'PUSHME_KEY': '', # PushMe 酱的 PUSHME_KEY 'PUSHME_KEY': '', # PushMe 酱的 PUSHME_KEY
'CHRONOCAT_QQ': '', # qq号
'CHRONOCAT_TOKEN': '', # CHRONOCAT 的token
'CHRONOCAT_URL': '' # CHRONOCAT的url地址
} }
notify_function = [] notify_function = []
# fmt: on # fmt: on
@ -664,6 +667,57 @@ def pushme(title: str, content: str) -> None:
else: else:
print(f"PushMe 推送失败!{response.status_code} {response.text}") print(f"PushMe 推送失败!{response.status_code} {response.text}")
def chronocat(title: str, content: str) -> None:
"""
使用 CHRONOCAT 推送消息
"""
if not push_config.get("CHRONOCAT_URL") or not push_config.get("CHRONOCAT_QQ") or not push_config.get(
"CHRONOCAT_TOKEN"):
print("CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送")
return
print("CHRONOCAT 服务启动")
user_ids = re.findall(r"user_id=(\d+)", push_config.get("CHRONOCAT_QQ"))
group_ids = re.findall(r"group_id=(\d+)", push_config.get("CHRONOCAT_QQ"))
url = f'{push_config.get("CHRONOCAT_URL")}/api/message/send'
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {push_config.get("CHRONOCAT_TOKEN")}'
}
for chat_type, ids in [(1, user_ids), (2, group_ids)]:
if not ids:
continue
for chat_id in ids:
data = {
"peer": {
"chatType": chat_type,
"peerUin": chat_id
},
"elements": [
{
"elementType": 1,
"textElement": {
"content": f'{title}\n\n{content}'
}
}
]
}
response = requests.post(url, headers=headers, data=json.dumps(data))
if response.status_code == 200:
if chat_type == 1:
print(f'QQ个人消息:{ids}推送成功!')
else:
print(f'QQ群消息:{ids}推送成功!')
else:
if chat_type == 1:
print(f'QQ个人消息:{ids}推送失败!')
else:
print(f'QQ群消息:{ids}推送失败!')
def one() -> str: def one() -> str:
""" """

View File

@ -98,6 +98,7 @@ export default {
{ value: 'email', label: intl.get('邮箱') }, { value: 'email', label: intl.get('邮箱') },
{ value: 'lark', label: intl.get('飞书机器人') }, { value: 'lark', label: intl.get('飞书机器人') },
{ value: 'pushMe', label: 'PushMe' }, { value: 'pushMe', label: 'PushMe' },
{ value: 'chronocat', label: 'Chronocat' },
{ value: 'webhook', label: intl.get('自定义通知') }, { value: 'webhook', label: intl.get('自定义通知') },
{ value: 'closed', label: intl.get('已关闭') }, { value: 'closed', label: intl.get('已关闭') },
], ],
@ -126,14 +127,16 @@ export default {
goCqHttpBot: [ goCqHttpBot: [
{ {
label: 'goCqHttpBotUrl', label: 'goCqHttpBotUrl',
tip: intl.get('推送到个人QQ: http://127.0.0.1/send_private_msghttp://127.0.0.1/send_group_msg', tip: intl.get(
'推送到个人QQ: http://127.0.0.1/send_private_msghttp://127.0.0.1/send_group_msg',
), ),
required: true, required: true,
}, },
{ label: 'goCqHttpBotToken', tip: intl.get('访问密钥'), required: true }, { label: 'goCqHttpBotToken', tip: intl.get('访问密钥'), required: true },
{ {
label: 'goCqHttpBotQq', label: 'goCqHttpBotQq',
tip: intl.get('如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群', tip: intl.get(
'如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群',
), ),
required: true, required: true,
}, },
@ -153,14 +156,16 @@ export default {
}, },
{ {
label: 'pushDeerUrl', label: 'pushDeerUrl',
tip: intl.get('PushDeer的自架API endpoint默认是 https://api2.pushdeer.com/message/push', tip: intl.get(
'PushDeer的自架API endpoint默认是 https://api2.pushdeer.com/message/push',
), ),
}, },
], ],
bark: [ bark: [
{ {
label: 'barkPush', label: 'barkPush',
tip: intl.get('Bark的信息IP/设备码例如https://api.day.app/XXXXXXXX', tip: intl.get(
'Bark的信息IP/设备码例如https://api.day.app/XXXXXXXX',
), ),
required: true, required: true,
}, },
@ -188,7 +193,8 @@ export default {
telegramBot: [ telegramBot: [
{ {
label: 'telegramBotToken', label: 'telegramBotToken',
tip: intl.get('telegram机器人的token例如1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw', tip: intl.get(
'telegram机器人的token例如1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw',
), ),
required: true, required: true,
}, },
@ -201,7 +207,8 @@ export default {
{ label: 'telegramBotProxyPort', tip: intl.get('代理端口') }, { label: 'telegramBotProxyPort', tip: intl.get('代理端口') },
{ {
label: 'telegramBotProxyAuth', label: 'telegramBotProxyAuth',
tip: intl.get('telegram代理配置认证参数用户名与密码用英文冒号连接 user:password', tip: intl.get(
'telegram代理配置认证参数用户名与密码用英文冒号连接 user:password',
), ),
}, },
{ {
@ -212,20 +219,23 @@ export default {
dingtalkBot: [ dingtalkBot: [
{ {
label: 'dingtalkBotToken', label: 'dingtalkBotToken',
tip: intl.get('钉钉机器人webhook token例如5a544165465465645d0f31dca676e7bd07415asdasd', tip: intl.get(
'钉钉机器人webhook token例如5a544165465465645d0f31dca676e7bd07415asdasd',
), ),
required: true, required: true,
}, },
{ {
label: 'dingtalkBotSecret', label: 'dingtalkBotSecret',
tip: intl.get('密钥机器人安全设置页面加签一栏下面显示的SEC开头的字符串', tip: intl.get(
'密钥机器人安全设置页面加签一栏下面显示的SEC开头的字符串',
), ),
}, },
], ],
weWorkBot: [ weWorkBot: [
{ {
label: 'weWorkBotKey', label: 'weWorkBotKey',
tip: intl.get('企业微信机器人的webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770)例如693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa', tip: intl.get(
'企业微信机器人的webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770)例如693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa',
), ),
required: true, required: true,
}, },
@ -237,7 +247,8 @@ export default {
weWorkApp: [ weWorkApp: [
{ {
label: 'weWorkAppKey', label: 'weWorkAppKey',
tip: intl.get('corpid、corpsecret、touser(注:多个成员ID使用|隔开)、agentid、消息类型(选填,不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号)例如wwcfrs,B-76WERQ,qinglong,1000001,2COat', tip: intl.get(
'corpid、corpsecret、touser(注:多个成员ID使用|隔开)、agentid、消息类型(选填,不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号)例如wwcfrs,B-76WERQ,qinglong,1000001,2COat',
), ),
required: true, required: true,
}, },
@ -249,7 +260,8 @@ export default {
aibotk: [ aibotk: [
{ {
label: 'aibotkKey', label: 'aibotkKey',
tip: intl.get('密钥key智能微秘书个人中心获取apikey申请地址https://wechat.aibotk.com/signup?from=ql', tip: intl.get(
'密钥key智能微秘书个人中心获取apikey申请地址https://wechat.aibotk.com/signup?from=ql',
), ),
required: true, required: true,
}, },
@ -265,7 +277,8 @@ export default {
}, },
{ {
label: 'aibotkName', label: 'aibotkName',
tip: intl.get('要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称', tip: intl.get(
'要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称',
), ),
required: true, required: true,
}, },
@ -273,7 +286,8 @@ export default {
iGot: [ iGot: [
{ {
label: 'iGotPushKey', label: 'iGotPushKey',
tip: intl.get('iGot的信息推送key例如https://push.hellyw.com/XXXXXXXX', tip: intl.get(
'iGot的信息推送key例如https://push.hellyw.com/XXXXXXXX',
), ),
required: true, required: true,
}, },
@ -281,20 +295,23 @@ export default {
pushPlus: [ pushPlus: [
{ {
label: 'pushPlusToken', label: 'pushPlusToken',
tip: intl.get('微信扫码登录后一对一推送或一对多推送下面的token(您的Token)不提供PUSH_PLUS_USER则默认为一对一推送参考 https://www.pushplus.plus/', tip: intl.get(
'微信扫码登录后一对一推送或一对多推送下面的token(您的Token)不提供PUSH_PLUS_USER则默认为一对一推送参考 https://www.pushplus.plus/',
), ),
required: true, required: true,
}, },
{ {
label: 'pushPlusUser', label: 'pushPlusUser',
tip: intl.get('一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)', tip: intl.get(
'一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)',
), ),
}, },
], ],
lark: [ lark: [
{ {
label: 'larkKey', label: 'larkKey',
tip: intl.get('飞书群组机器人https://www.feishu.cn/hc/zh-CN/articles/360024984973', tip: intl.get(
'飞书群组机器人https://www.feishu.cn/hc/zh-CN/articles/360024984973',
), ),
required: true, required: true,
}, },
@ -302,7 +319,8 @@ export default {
email: [ email: [
{ {
label: 'emailService', label: 'emailService',
tip: intl.get('邮箱服务名称比如126、163、Gmail、QQ等支持列表https://nodemailer.com/smtp/well-known/', tip: intl.get(
'邮箱服务名称比如126、163、Gmail、QQ等支持列表https://nodemailer.com/smtp/well-known/',
), ),
required: true, required: true,
}, },
@ -316,6 +334,29 @@ export default {
required: true, required: true,
}, },
], ],
chronocat: [
{
label: 'chronocatURL',
tip: intl.get(
'Chronocat Red 服务的连接地址 https://chronocat.vercel.app/install/docker/official/',
),
required: true,
},
{
label: 'chronocatQQ',
tip: intl.get(
'个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如user_id=xxx;group_id=xxxx;group_id=xxxxx',
),
required: true,
},
{
label: 'chronocatToken',
tip: intl.get(
'docker安装在持久化config目录下的chronocat.yml文件可找到',
),
required: true,
},
],
webhook: [ webhook: [
{ {
label: 'webhookMethod', label: 'webhookMethod',
@ -335,7 +376,8 @@ export default {
}, },
{ {
label: 'webhookUrl', label: 'webhookUrl',
tip: intl.get('请求链接以http或者https开头。url或者body中必须包含$title$content可选对应api内容的位置', tip: intl.get(
'请求链接以http或者https开头。url或者body中必须包含$title$content可选对应api内容的位置',
), ),
required: true, required: true,
placeholder: 'https://xxx.cn/api?content=$title\n', placeholder: 'https://xxx.cn/api?content=$title\n',
@ -347,7 +389,8 @@ export default {
}, },
{ {
label: 'webhookBody', label: 'webhookBody',
tip: intl.get('请求体格式key1: value1多个换行分割。url或者body中必须包含$title$content可选对应api内容的位置', tip: intl.get(
'请求体格式key1: value1多个换行分割。url或者body中必须包含$title$content可选对应api内容的位置',
), ),
placeholder: 'key1: $title\nkey2: $content', placeholder: 'key1: $title\nkey2: $content',
}, },