mirror of https://github.com/Nofated095/Q2TG.git
feat: 可以转发消息了,session 需要存储
This commit is contained in:
parent
042f90d119
commit
ceddf86453
|
@ -17,8 +17,13 @@
|
|||
"dependencies": {
|
||||
"@prisma/client": "latest",
|
||||
"axios": "^0.26.0",
|
||||
"file-type": "^17.1.1",
|
||||
"log4js": "^6.4.1",
|
||||
"nodejs-base64": "^2.0.0",
|
||||
"oicq": "^2.2.0",
|
||||
"telegram": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.13.1 || >=16.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ model Message {
|
|||
id Int @id @default(autoincrement())
|
||||
qqRoomId Int
|
||||
qqSenderId Int
|
||||
time DateTime
|
||||
time Int
|
||||
brief String
|
||||
seq Int
|
||||
rand Int
|
||||
|
@ -22,7 +22,7 @@ model Message {
|
|||
tgChatId Int
|
||||
tgMsgId Int
|
||||
|
||||
@@unique([qqRoomId, qqSenderId, seq, rand, pktnum])
|
||||
@@unique([qqRoomId, qqSenderId, seq, rand, pktnum, time])
|
||||
@@unique([tgChatId, tgMsgId])
|
||||
}
|
||||
|
||||
|
@ -35,11 +35,11 @@ model ForwardPair {
|
|||
|
||||
model File {
|
||||
id Int @id @default(autoincrement())
|
||||
groupId Int
|
||||
roomId Int
|
||||
fileId String
|
||||
info String
|
||||
|
||||
@@unique([groupId, fileId])
|
||||
@@unique([roomId, fileId])
|
||||
}
|
||||
|
||||
model FlashPhoto {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
images: ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp']
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import Telegram from '../client/Telegram';
|
||||
import OicqClient from '../client/OicqClient';
|
||||
import ForwardService from '../services/ForwardService';
|
||||
import forwardPairs from '../providers/forwardPairs';
|
||||
import { DiscussMessageEvent, Friend, Group, GroupMessageEvent, PrivateMessageEvent } from 'oicq';
|
||||
import db from '../providers/db';
|
||||
import helper from '../helpers/forwardHelper';
|
||||
|
||||
export default class ForwardController {
|
||||
private readonly forwardService: ForwardService;
|
||||
|
||||
constructor(private readonly tgBot: Telegram,
|
||||
private readonly tgUser: Telegram,
|
||||
private readonly oicq: OicqClient) {
|
||||
this.forwardService = new ForwardService(tgBot, oicq);
|
||||
forwardPairs.init(oicq, tgBot)
|
||||
.then(() => oicq.on('message', this.onQqMsg));
|
||||
}
|
||||
|
||||
private onQqMsg = async (event: PrivateMessageEvent | GroupMessageEvent | DiscussMessageEvent) => {
|
||||
let target: Friend | Group;
|
||||
if (event.message_type === 'private') {
|
||||
target = event.friend;
|
||||
}
|
||||
else if (event.message_type === 'group') {
|
||||
target = event.group;
|
||||
}
|
||||
else return;
|
||||
const pair = forwardPairs.find(target);
|
||||
if (!pair) return;
|
||||
const tgMsg = await this.forwardService.forwardFromQq(event, pair);
|
||||
if (tgMsg) {
|
||||
// 更新数据库
|
||||
await db.message.create({
|
||||
data: {
|
||||
qqRoomId: helper.getRoomId(pair.qq),
|
||||
qqSenderId: event.sender.user_id,
|
||||
time: event.time,
|
||||
brief: event.raw_message,
|
||||
seq: event.seq,
|
||||
rand: event.rand,
|
||||
pktnum: event.pktnum,
|
||||
tgChatId: Number(pair.tg.id),
|
||||
tgMsgId: tgMsg.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
import { fetchFile } from '../utils/urls';
|
||||
import { CustomFile } from 'telegram/client/uploads';
|
||||
import { Friend, Group } from 'oicq';
|
||||
import { base64decode } from 'nodejs-base64';
|
||||
import { getLogger } from 'log4js';
|
||||
|
||||
const log = getLogger('ForwardHelper');
|
||||
|
||||
export default {
|
||||
async downloadToCustomFile(url: string) {
|
||||
const { fileTypeFromBuffer } = await import('file-type');
|
||||
const file = await fetchFile(url);
|
||||
const type = await fileTypeFromBuffer(file);
|
||||
return new CustomFile(`image.${type.ext}`, file.length, '', file);
|
||||
},
|
||||
|
||||
hSize(size: number) {
|
||||
const BYTE = 1024;
|
||||
|
||||
if (size < BYTE)
|
||||
return size + 'B';
|
||||
if (size < Math.pow(BYTE, 2))
|
||||
return (size / BYTE).toFixed(1) + 'KB';
|
||||
if (size < Math.pow(BYTE, 3))
|
||||
return (size / Math.pow(BYTE, 2)).toFixed(1) + 'MB';
|
||||
if (size < Math.pow(BYTE, 4))
|
||||
return (size / Math.pow(BYTE, 3)).toFixed(1) + 'GB';
|
||||
return (size / Math.pow(BYTE, 4)).toFixed(1) + 'TB';
|
||||
},
|
||||
|
||||
htmlEscape: (text: string) =>
|
||||
text.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>'),
|
||||
|
||||
getRoomId(room: Friend | Group) {
|
||||
if (room instanceof Friend) {
|
||||
return room.user_id;
|
||||
}
|
||||
else {
|
||||
return room.group_id;
|
||||
}
|
||||
},
|
||||
|
||||
processJson(json: string) {
|
||||
const jsonObj = JSON.parse(json);
|
||||
if (jsonObj.app === 'com.tencent.mannounce') {
|
||||
try {
|
||||
const title = base64decode(jsonObj.meta.mannounce.title);
|
||||
const content = base64decode(jsonObj.meta.mannounce.text);
|
||||
return title + '\n\n' + content;
|
||||
}
|
||||
catch (err) {
|
||||
log.error('解析群公告时出错', err);
|
||||
return '[群公告]';
|
||||
}
|
||||
}
|
||||
let appurl: string;
|
||||
const biliRegex = /(https?:\\?\/\\?\/b23\.tv\\?\/\w*)\??/;
|
||||
const zhihuRegex = /(https?:\\?\/\\?\/\w*\.?zhihu\.com\\?\/[^?"=]*)\??/;
|
||||
const biliRegex2 = /(https?:\\?\/\\?\/\w*\.?bilibili\.com\\?\/[^?"=]*)\??/;
|
||||
const jsonLinkRegex = /{.*"app":"com.tencent.structmsg".*"jumpUrl":"(https?:\\?\/\\?\/[^",]*)".*}/;
|
||||
const jsonAppLinkRegex = /"contentJumpUrl": ?"(https?:\\?\/\\?\/[^",]*)"/;
|
||||
if (biliRegex.test(json))
|
||||
appurl = json.match(biliRegex)[1].replace(/\\\//g, '/');
|
||||
else if (biliRegex2.test(json))
|
||||
appurl = json.match(biliRegex2)[1].replace(/\\\//g, '/');
|
||||
else if (zhihuRegex.test(json))
|
||||
appurl = json.match(zhihuRegex)[1].replace(/\\\//g, '/');
|
||||
else if (jsonLinkRegex.test(json))
|
||||
appurl = json.match(jsonLinkRegex)[1].replace(/\\\//g, '/');
|
||||
else if (jsonAppLinkRegex.test(json))
|
||||
appurl = json.match(jsonAppLinkRegex)[1].replace(/\\\//g, '/');
|
||||
if (appurl) {
|
||||
return appurl;
|
||||
}
|
||||
else {
|
||||
// TODO 记录无法解析的 JSON
|
||||
return '[JSON]';
|
||||
}
|
||||
},
|
||||
|
||||
processXml(xml: string):
|
||||
{ type: 'forward', resId: string } | { type: 'text', text: string } | { type: 'image', md5: string } {
|
||||
const urlRegex = /url="([^"]+)"/;
|
||||
const md5ImageRegex = /image md5="([A-F\d]{32})"/;
|
||||
let text: string;
|
||||
if (urlRegex.test(xml))
|
||||
text = xml.match(urlRegex)[1].replace(/\\\//g, '/');
|
||||
if (xml.includes('action="viewMultiMsg"')) {
|
||||
text = '[Forward multiple messages]';
|
||||
const resIdRegex = /m_resid="([\w+=/]+)"/;
|
||||
if (resIdRegex.test(xml)) {
|
||||
const resId = xml.match(resIdRegex)![1];
|
||||
return {
|
||||
type: 'forward',
|
||||
resId,
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (text) {
|
||||
text = text.replace(/&/g, '&');
|
||||
return {
|
||||
type: 'text',
|
||||
text,
|
||||
};
|
||||
}
|
||||
else if (md5ImageRegex.test(xml)) {
|
||||
const imgMd5 = xml.match(md5ImageRegex)![1];
|
||||
return {
|
||||
type: 'image',
|
||||
md5: imgMd5,
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
type: 'text',
|
||||
text: '[XML]',
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
|
@ -1,9 +1,10 @@
|
|||
import Telegram from './client/Telegram';
|
||||
import { config } from './providers/userConfig';
|
||||
import { getLogger, configure } from 'log4js';
|
||||
import { configure, getLogger } from 'log4js';
|
||||
import SetupController from './controllers/SetupController';
|
||||
import OicqClient from './client/OicqClient';
|
||||
import ConfigController from './controllers/ConfigController';
|
||||
import ForwardController from './controllers/ForwardController';
|
||||
|
||||
(async () => {
|
||||
configure({
|
||||
|
@ -49,4 +50,5 @@ import ConfigController from './controllers/ConfigController';
|
|||
log.debug('OICQ 登录完成');
|
||||
}
|
||||
new ConfigController(tgBot, tgUser, oicq);
|
||||
new ForwardController(tgBot, tgUser, oicq);
|
||||
})();
|
||||
|
|
|
@ -5,7 +5,7 @@ import OicqClient from '../client/OicqClient';
|
|||
import Telegram from '../client/Telegram';
|
||||
import db from './db';
|
||||
|
||||
type Pair = {
|
||||
export type Pair = {
|
||||
qq: Friend | Group;
|
||||
tg: TelegramChat;
|
||||
}
|
||||
|
@ -34,13 +34,17 @@ class ForwardPairsInternal {
|
|||
});
|
||||
}
|
||||
|
||||
public find(target: Friend | Group | TelegramChat | Api.Chat) {
|
||||
public find(target: Friend | Group | TelegramChat | Api.Chat | number) {
|
||||
if (target instanceof Friend) {
|
||||
return this.pairs.find(e => e.qq instanceof Friend && e.qq.user_id === target.user_id);
|
||||
}
|
||||
else if (target instanceof Group) {
|
||||
return this.pairs.find(e => e.qq instanceof Group && e.qq.group_id === target.group_id);
|
||||
}
|
||||
else if (typeof target === 'number') {
|
||||
return this.pairs.find(e => e.qq instanceof Friend && e.qq.user_id === target ||
|
||||
e.qq instanceof Group && e.qq.group_id === -target);
|
||||
}
|
||||
else {
|
||||
return this.pairs.find(e => e.tg.id.eq(target.id));
|
||||
}
|
||||
|
|
|
@ -16,14 +16,14 @@ import forwardPairs from '../providers/forwardPairs';
|
|||
const DEFAULT_FILTER_ID = 114; // 514
|
||||
|
||||
export default class ConfigService {
|
||||
private owner: TelegramChat;
|
||||
private owner: Promise<TelegramChat>;
|
||||
private log = getLogger('ConfigService');
|
||||
private filter: Api.DialogFilter;
|
||||
|
||||
constructor(private readonly tgBot: Telegram,
|
||||
private readonly tgUser: Telegram,
|
||||
private readonly oicq: OicqClient) {
|
||||
tgBot.getChat(config.owner).then(e => this.owner = e);
|
||||
this.owner = tgBot.getChat(config.owner);
|
||||
}
|
||||
|
||||
private getAssociateLink(roomId: number) {
|
||||
|
@ -31,12 +31,11 @@ export default class ConfigService {
|
|||
}
|
||||
|
||||
public async configCommands() {
|
||||
// 这个在一初始化好就要调用,所以不能直接用 this.owner
|
||||
await this.tgBot.setCommands([], new Api.BotCommandScopeUsers());
|
||||
await this.tgBot.setCommands(
|
||||
config.workMode === 'personal' ? commands.personalPrivateCommands : commands.groupPrivateCommands,
|
||||
new Api.BotCommandScopePeer({
|
||||
peer: (await this.tgBot.getChat(config.owner)).inputPeer,
|
||||
peer: (await this.owner).inputPeer,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -56,7 +55,7 @@ export default class ConfigService {
|
|||
`${e.group_name} (${e.group_id})`,
|
||||
this.getAssociateLink(-e.group_id),
|
||||
)]);
|
||||
await this.owner.createPaginatedInlineSelector(
|
||||
await (await this.owner).createPaginatedInlineSelector(
|
||||
'选择 QQ 群组' + (config.workMode === 'group' ? '\n然后选择在 TG 中的群组' : ''), buttons);
|
||||
}
|
||||
|
||||
|
@ -75,7 +74,7 @@ export default class ConfigService {
|
|||
return 1;
|
||||
}
|
||||
});
|
||||
await this.owner.createPaginatedInlineSelector('选择分组', classes.map(e => [
|
||||
await (await this.owner).createPaginatedInlineSelector('选择分组', classes.map(e => [
|
||||
Button.inline(e[1], this.tgBot.registerCallback(
|
||||
() => this.openFriendSelection(friends.filter(f => f.class_id === e[0]), e[1]),
|
||||
)),
|
||||
|
@ -83,7 +82,7 @@ export default class ConfigService {
|
|||
}
|
||||
|
||||
private async openFriendSelection(clazz: FriendInfo[], name: string) {
|
||||
await this.owner.createPaginatedInlineSelector(`选择 QQ 好友\n分组:${name}`, clazz.map(e => [
|
||||
await (await this.owner).createPaginatedInlineSelector(`选择 QQ 好友\n分组:${name}`, clazz.map(e => [
|
||||
Button.inline(`${e.remark || e.nickname} (${e.user_id})`, this.tgBot.registerCallback(
|
||||
() => this.createGroupAndLink(e.user_id, e.remark || e.nickname),
|
||||
)),
|
||||
|
@ -101,7 +100,7 @@ export default class ConfigService {
|
|||
this.log.error(`加载 ${group.group_name} (${gin}) 的头像失败`, e);
|
||||
}
|
||||
const message = `${group.group_name}\n${group.group_id}\n${group.member_count} 名成员`;
|
||||
await this.owner.sendMessage({
|
||||
await (await this.owner).sendMessage({
|
||||
message,
|
||||
file: avatar ? new CustomFile('avatar.png', avatar.length, '', avatar) : undefined,
|
||||
buttons: Button.url('关联 Telegram 群组', this.getAssociateLink(-group.group_id)),
|
||||
|
@ -125,18 +124,18 @@ export default class ConfigService {
|
|||
let isFinish = false;
|
||||
try {
|
||||
// 状态信息
|
||||
const status = await this.owner.sendMessage('正在创建 Telegram 群…');
|
||||
const status = await (await this.owner).sendMessage('正在创建 Telegram 群…');
|
||||
|
||||
// 创建群聊,拿到的是 user 的 chat
|
||||
const chat = await this.tgUser.createChat({
|
||||
title,
|
||||
users: [this.tgBot.me.id],
|
||||
users: [this.tgBot.me.username],
|
||||
});
|
||||
const chatForBot = await this.tgBot.getChat(chat.id);
|
||||
|
||||
// 设置管理员
|
||||
await status.edit({ text: '正在设置管理员…' });
|
||||
await chat.editAdmin(this.tgBot.me.username, true);
|
||||
const chatForBot = await this.tgBot.getChat(chat.id);
|
||||
|
||||
// 关联写入数据库
|
||||
await status.edit({ text: '正在写数据库…' });
|
||||
|
@ -174,7 +173,7 @@ export default class ConfigService {
|
|||
}
|
||||
catch (e) {
|
||||
this.log.error('创建群组并关联失败', e);
|
||||
await this.owner.sendMessage(`创建群组并关联${isFinish ? '成功了但没完全成功' : '失败'}\n<code>${e}</code>`);
|
||||
await (await this.owner).sendMessage(`创建群组并关联${isFinish ? '成功了但没完全成功' : '失败'}\n<code>${e}</code>`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,14 +189,13 @@ export default class ConfigService {
|
|||
catch (e) {
|
||||
message = `错误:<code>${e}</code>`;
|
||||
}
|
||||
await this.owner.sendMessage({ message });
|
||||
await (await this.owner).sendMessage({ message });
|
||||
}
|
||||
|
||||
// 创建 QQ 群组的文件夹
|
||||
public async setupFilter() {
|
||||
const result = await this.tgUser.getDialogFilters();
|
||||
this.filter = result.find(e => e.id === DEFAULT_FILTER_ID);
|
||||
this.log.debug(this.filter);
|
||||
if (!this.filter) {
|
||||
this.log.info('创建 TG 文件夹');
|
||||
// 要自己计算新的 id,随意 id 也是可以的
|
||||
|
@ -222,13 +220,13 @@ export default class ConfigService {
|
|||
if (!isSuccess) {
|
||||
this.filter = null;
|
||||
this.log.error(errorText);
|
||||
await this.owner.sendMessage(errorText);
|
||||
await (await this.owner).sendMessage(errorText);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.filter = null;
|
||||
this.log.error(errorText, e);
|
||||
await this.owner.sendMessage(errorText + `\n<code>${e}</code>`);
|
||||
await (await this.owner).sendMessage(errorText + `\n<code>${e}</code>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
import Telegram from '../client/Telegram';
|
||||
import OicqClient from '../client/OicqClient';
|
||||
import { GroupMessageEvent, PrivateMessageEvent } from 'oicq';
|
||||
import { Pair } from '../providers/forwardPairs';
|
||||
import { fetchFile, getBigFaceUrl, getImageUrlByMd5 } from '../utils/urls';
|
||||
import { FileLike, MarkupLike } from 'telegram/define';
|
||||
import { CustomFile } from 'telegram/client/uploads';
|
||||
import { getLogger } from 'log4js';
|
||||
import path from 'path';
|
||||
import exts from '../constants/exts';
|
||||
import helper from '../helpers/forwardHelper';
|
||||
import db from '../providers/db';
|
||||
import { Button } from 'telegram/tl/custom/button';
|
||||
import { SendMessageParams } from 'telegram/client/messages';
|
||||
|
||||
// noinspection FallThroughInSwitchStatementJS
|
||||
export default class ForwardService {
|
||||
private log = getLogger('ForwardService');
|
||||
|
||||
constructor(private readonly tgBot: Telegram,
|
||||
private readonly oicq: OicqClient) {
|
||||
}
|
||||
|
||||
public async forwardFromQq(event: PrivateMessageEvent | GroupMessageEvent, pair: Pair) {
|
||||
try {
|
||||
let message = '', files: FileLike[] = [], button: MarkupLike, replyTo = 0;
|
||||
let messageHeader = '';
|
||||
if (event.message_type === 'group') {
|
||||
// 产生头部,这和工作模式没有关系
|
||||
const sender = event.sender.card || event.sender.nickname;
|
||||
messageHeader = `<b>${helper.htmlEscape(sender)}</b>: `;
|
||||
}
|
||||
for (const elem of event.message) {
|
||||
let url: string;
|
||||
switch (elem.type) {
|
||||
case 'text': {
|
||||
message += elem.text;
|
||||
break;
|
||||
}
|
||||
case 'at': {
|
||||
if (event.source?.user_id === elem.qq)
|
||||
break;
|
||||
}
|
||||
case 'face':
|
||||
case 'sface': {
|
||||
message += `[${elem.text}]`;
|
||||
break;
|
||||
}
|
||||
case 'bface': {
|
||||
const file = await fetchFile(getBigFaceUrl(elem.file));
|
||||
files.push(new CustomFile('face.png', file.length, '', file));
|
||||
break;
|
||||
}
|
||||
case 'video':
|
||||
// 先获取 URL,要传给下面
|
||||
url = await pair.qq.getVideoUrl(elem.fid, elem.md5);
|
||||
case 'image':
|
||||
case 'flash':
|
||||
// TODO 闪照单独处理
|
||||
if ('url' in elem)
|
||||
url = elem.url;
|
||||
try {
|
||||
files.push(await helper.downloadToCustomFile(url));
|
||||
}
|
||||
catch (e) {
|
||||
this.log.error('下载媒体失败', e);
|
||||
// 下载失败让 Telegram 服务器下载
|
||||
files.push(url);
|
||||
}
|
||||
break;
|
||||
case 'file': {
|
||||
const extName = path.extname(elem.name);
|
||||
if (exts.images.includes(extName.toLowerCase())) {
|
||||
// 是图片
|
||||
const url = await pair.qq.getFileUrl(elem.fid);
|
||||
try {
|
||||
files.push(await helper.downloadToCustomFile(url));
|
||||
}
|
||||
catch (e) {
|
||||
this.log.error('下载媒体失败', e);
|
||||
// 下载失败让 Telegram 服务器下载
|
||||
files.push(url);
|
||||
}
|
||||
}
|
||||
else {
|
||||
message = `文件: ${elem.name}\n` +
|
||||
`大小: ${helper.hSize(elem.size)}`;
|
||||
const dbEntry = await db.file.create({
|
||||
data: { fileId: elem.fid, roomId: helper.getRoomId(pair.qq), info: message },
|
||||
});
|
||||
button = Button.url('⏬ 获取下载地址',
|
||||
`https://t.me/${this.tgBot.me.username}?start=file-${dbEntry.id}`);
|
||||
}
|
||||
}
|
||||
case 'record': {
|
||||
// TODO
|
||||
message = '[语音]';
|
||||
break;
|
||||
}
|
||||
case 'share': {
|
||||
message = elem.url;
|
||||
break;
|
||||
}
|
||||
case 'json': {
|
||||
message = helper.processJson(elem.data);
|
||||
break;
|
||||
}
|
||||
case 'xml': {
|
||||
const result = helper.processXml(elem.data);
|
||||
switch (result.type) {
|
||||
case 'text':
|
||||
message = result.text;
|
||||
break;
|
||||
case 'image':
|
||||
try {
|
||||
files.push(await helper.downloadToCustomFile(getImageUrlByMd5(result.md5)));
|
||||
}
|
||||
catch (e) {
|
||||
this.log.error('下载媒体失败', e);
|
||||
// 下载失败让 Telegram 服务器下载
|
||||
files.push(getImageUrlByMd5(result.md5));
|
||||
}
|
||||
break;
|
||||
case 'forward':
|
||||
// TODO 详细展开
|
||||
message = '[转发多条消息]';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'rps':
|
||||
case 'dice':
|
||||
message = `[${elem.type === 'rps' ? '猜拳' : '骰子'}] ${elem.id}`;
|
||||
break;
|
||||
case 'poke':
|
||||
message = `[戳一戳] ${elem.text}`;
|
||||
break;
|
||||
case 'location':
|
||||
message = `[位置] ${elem.name}\n${elem.address}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
message = helper.htmlEscape(message.trim());
|
||||
message = messageHeader + (message && messageHeader ? '\n' : '') + message;
|
||||
|
||||
// 处理回复
|
||||
if (event.source) {
|
||||
try {
|
||||
const quote = await db.message.findFirst({
|
||||
where: {
|
||||
qqRoomId: helper.getRoomId(pair.qq),
|
||||
seq: event.source.seq,
|
||||
rand: event.source.rand,
|
||||
},
|
||||
});
|
||||
if (quote) {
|
||||
replyTo = quote.tgMsgId;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.log.error('查找回复消息失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const messageToSend: SendMessageParams = {};
|
||||
message && (messageToSend.message = message);
|
||||
if (files.length === 1) {
|
||||
messageToSend.file = files[0];
|
||||
}
|
||||
else if (files.length) {
|
||||
messageToSend.file = files;
|
||||
}
|
||||
button && (messageToSend.buttons = button);
|
||||
replyTo && (messageToSend.replyTo = replyTo);
|
||||
|
||||
return await pair.tg.sendMessage(messageToSend);
|
||||
}
|
||||
catch (e) {
|
||||
this.log.error('从 QQ 到 TG 到消息转发失败', e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,15 +12,16 @@ export function getImageUrlByMd5(md5: string) {
|
|||
}
|
||||
|
||||
export function getBigFaceUrl(file: string) {
|
||||
return `https://gxh.vip.qq.com/club/item/parcel/item/${file.substring(
|
||||
0,
|
||||
2,
|
||||
)}/${file.substring(0, 32)}/300x300.png`;
|
||||
return `https://gxh.vip.qq.com/club/item/parcel/item/${file.substring(0, 2)}/${file.substring(0, 32)}/300x300.png`;
|
||||
}
|
||||
|
||||
export async function getAvatar(roomId: number): Promise<Buffer> {
|
||||
const res = await axios.get(getAvatarUrl(roomId), {
|
||||
export async function fetchFile(url: string): Promise<Buffer> {
|
||||
const res = await axios.get(url, {
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export function getAvatar(roomId: number) {
|
||||
return fetchFile(getAvatarUrl(roomId));
|
||||
}
|
||||
|
|
63
yarn.lock
63
yarn.lock
|
@ -83,6 +83,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tokenizer/token@npm:^0.3.0":
|
||||
version: 0.3.0
|
||||
resolution: "@tokenizer/token@npm:0.3.0"
|
||||
checksum: 1d575d02d2a9f0c5a4ca5180635ebd2ad59e0f18b42a65f3d04844148b49b3db35cf00b6012a1af2d59c2ab3caca59451c5689f747ba8667ee586ad717ee58e1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tootallnate/once@npm:1":
|
||||
version: 1.1.2
|
||||
resolution: "@tootallnate/once@npm:1.1.2"
|
||||
|
@ -613,6 +620,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"file-type@npm:^17.1.1":
|
||||
version: 17.1.1
|
||||
resolution: "file-type@npm:17.1.1"
|
||||
dependencies:
|
||||
readable-web-to-node-stream: ^3.0.2
|
||||
strtok3: ^7.0.0-alpha.7
|
||||
token-types: ^5.0.0-alpha.2
|
||||
checksum: fb52169bbb8f4d179ee372f8a8792880c8702f96cf2f9e55094baa6c7e81bed0be347a9f3dd849632597fe0e4c9bd6cc8d60b68292730dc52919844bbb3f2dab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"flatted@npm:^3.2.4":
|
||||
version: 3.2.5
|
||||
resolution: "flatted@npm:3.2.5"
|
||||
|
@ -1343,6 +1361,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nodejs-base64@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "nodejs-base64@npm:2.0.0"
|
||||
checksum: 1e9fc06396bd77caba14f93eec40751f00c4570346ebbaf9ded746f8309d0adc8a18bf62c55a9e0288854df7eb61ee9942b316548fd1e185de59213ce4f0fbde
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nopt@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "nopt@npm:5.0.0"
|
||||
|
@ -1453,6 +1478,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"peek-readable@npm:^5.0.0-alpha.5":
|
||||
version: 5.0.0-alpha.5
|
||||
resolution: "peek-readable@npm:5.0.0-alpha.5"
|
||||
checksum: cab949ed457dac95ae191dd412c6a0ba05e8db4842fd51704ccf2c8c16d6f3ceeefc997e8caea584a0395f229e468c0203a38a8d0ec68cfef8bacc157a006dcb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pngjs@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "pngjs@npm:6.0.0"
|
||||
|
@ -1514,7 +1546,9 @@ __metadata:
|
|||
"@prisma/client": latest
|
||||
"@types/node": ^17.0.18
|
||||
axios: ^0.26.0
|
||||
file-type: ^17.1.1
|
||||
log4js: ^6.4.1
|
||||
nodejs-base64: ^2.0.0
|
||||
oicq: ^2.2.0
|
||||
prisma: latest
|
||||
telegram: ^2.5.0
|
||||
|
@ -1544,6 +1578,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-web-to-node-stream@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "readable-web-to-node-stream@npm:3.0.2"
|
||||
dependencies:
|
||||
readable-stream: ^3.6.0
|
||||
checksum: 8c56cc62c68513425ddfa721954875b382768f83fa20e6b31e365ee00cbe7a3d6296f66f7f1107b16cd3416d33aa9f1680475376400d62a081a88f81f0ea7f9c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"retry@npm:^0.12.0":
|
||||
version: 0.12.0
|
||||
resolution: "retry@npm:0.12.0"
|
||||
|
@ -1746,6 +1789,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strtok3@npm:^7.0.0-alpha.7":
|
||||
version: 7.0.0-alpha.8
|
||||
resolution: "strtok3@npm:7.0.0-alpha.8"
|
||||
dependencies:
|
||||
"@tokenizer/token": ^0.3.0
|
||||
peek-readable: ^5.0.0-alpha.5
|
||||
checksum: 00e5c9ed0c5de537839cf443d5628f0ae88d2956ca1fdcbd45cd97372045d7179a40ec99f6d06b02c59ec2141e362142ad0a87c59506d401dbd3bd1ee242abaa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar@npm:^6.0.2, tar@npm:^6.1.2":
|
||||
version: 6.1.11
|
||||
resolution: "tar@npm:6.1.11"
|
||||
|
@ -1792,6 +1845,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"token-types@npm:^5.0.0-alpha.2":
|
||||
version: 5.0.0-alpha.2
|
||||
resolution: "token-types@npm:5.0.0-alpha.2"
|
||||
dependencies:
|
||||
"@tokenizer/token": ^0.3.0
|
||||
ieee754: ^1.2.1
|
||||
checksum: ee23eeed6f383b1072d99781d62fc7840f1296a96d47e636e36fca757debd7eb4274d31fcd2d56997606eede00b12b1e61a64610fe0ed7807d6b1c4dcf5ccc6b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ts-custom-error@npm:^3.2.0":
|
||||
version: 3.2.0
|
||||
resolution: "ts-custom-error@npm:3.2.0"
|
||||
|
|
Loading…
Reference in New Issue