feat: 从 TG 到 QQ 到消息转发

This commit is contained in:
Clansty 2022-02-26 18:15:40 +08:00
parent 61c79bb0e7
commit 848d926cc6
No known key found for this signature in database
GPG Key ID: 05F8479BA63A8E92
9 changed files with 178 additions and 15 deletions

View File

@ -21,7 +21,8 @@
"log4js": "^6.4.1",
"nodejs-base64": "^2.0.0",
"oicq": "^2.2.0",
"telegram": "^2.5.0"
"telegram": "^2.5.0",
"tmp-promise": "^3.0.3"
},
"engines": {
"node": "^14.13.1 || >=16.0.0"

View File

@ -1,4 +1,4 @@
import { Client, createClient, Platform } from 'oicq';
import { Client, LogLevel, Platform } from 'oicq';
import * as Buffer from 'buffer';
import { execSync } from 'child_process';
import random from '../utils/random';
@ -7,6 +7,8 @@ import fsP from 'fs/promises';
import path from 'path';
import { Config } from 'oicq/lib/client';
const LOG_LEVEL: LogLevel = 'warn';
interface CreateOicqParams {
uin: number;
password: string;

View File

@ -1,5 +1,4 @@
import { Api, TelegramClient } from 'telegram';
import { StringSession } from 'telegram/sessions';
import { BotAuthParams, UserAuthParams } from 'telegram/client/auth';
import { NewMessage, NewMessageEvent } from 'telegram/events';
import { EditedMessage, EditedMessageEvent } from 'telegram/events/EditedMessage';
@ -12,7 +11,7 @@ import os from 'os';
import TelegramChat from './TelegramChat';
import TelegramSession from './TelegramSession';
type MessageHandler = (message: Api.Message) => Promise<boolean>;
type MessageHandler = (message: Api.Message) => Promise<boolean | void>;
export default class Telegram {
private readonly client: TelegramClient;

View File

@ -5,6 +5,7 @@ import forwardPairs from '../providers/forwardPairs';
import { DiscussMessageEvent, Friend, Group, GroupMessageEvent, PrivateMessageEvent } from 'oicq';
import db from '../providers/db';
import helper from '../helpers/forwardHelper';
import { Api } from 'telegram';
export default class ForwardController {
private readonly forwardService: ForwardService;
@ -14,10 +15,11 @@ export default class ForwardController {
private readonly oicq: OicqClient) {
this.forwardService = new ForwardService(tgBot, oicq);
forwardPairs.init(oicq, tgBot)
.then(() => oicq.on('message', this.onQqMsg));
.then(() => oicq.on('message', this.onQqMessage))
.then(() => tgBot.addNewMessageEventHandler(this.onTelegramMessage));
}
private onQqMsg = async (event: PrivateMessageEvent | GroupMessageEvent | DiscussMessageEvent) => {
private onQqMessage = async (event: PrivateMessageEvent | GroupMessageEvent | DiscussMessageEvent) => {
let target: Friend | Group;
if (event.message_type === 'private') {
target = event.friend;
@ -28,8 +30,8 @@ export default class ForwardController {
else return;
const pair = forwardPairs.find(target);
if (!pair) return;
const tgMsg = await this.forwardService.forwardFromQq(event, pair);
if (tgMsg) {
const tgMessage = await this.forwardService.forwardFromQq(event, pair);
if (tgMessage) {
// 更新数据库
await db.message.create({
data: {
@ -41,7 +43,31 @@ export default class ForwardController {
rand: event.rand,
pktnum: event.pktnum,
tgChatId: Number(pair.tg.id),
tgMsgId: tgMsg.id,
tgMsgId: tgMessage.id,
},
});
}
};
private onTelegramMessage = async (message: Api.Message) => {
const pair = forwardPairs.find(message.chat);
if (!pair) return;
const qqMessageSent = await this.forwardService.forwardFromTelegram(message, pair);
// 返回的信息不太够
const qqMessage = await this.oicq.getMsg(qqMessageSent.message_id);
if (qqMessage) {
// 更新数据库
await db.message.create({
data: {
qqRoomId: helper.getRoomId(pair.qq),
qqSenderId: qqMessage.sender.user_id,
time: qqMessage.time,
brief: qqMessage.raw_message,
seq: qqMessage.seq,
rand: qqMessage.rand,
pktnum: qqMessage.pktnum,
tgChatId: Number(pair.tg.id),
tgMsgId: message.id,
},
});
}

View File

@ -3,6 +3,10 @@ import { CustomFile } from 'telegram/client/uploads';
import { Friend, Group } from 'oicq';
import { base64decode } from 'nodejs-base64';
import { getLogger } from 'log4js';
import { Entity } from 'telegram/define';
import { Api } from 'telegram';
import ChatForbidden = Api.ChatForbidden;
import ChatEmpty = Api.ChatEmpty;
const log = getLogger('ForwardHelper');
@ -119,4 +123,17 @@ export default {
};
}
},
getUserDisplayName(user: Entity) {
if ('firstName' in user) {
return user.firstName +
(user.lastName ? ' ' + user.lastName : '');
}
else if('title' in user){
return user.title
}
else if('id' in user){
return user.id.toString()
}
},
};

View File

@ -1,9 +1,9 @@
import { Friend, Group } from 'oicq';
import TelegramChat from '../client/TelegramChat';
import { Api } from 'telegram';
import OicqClient from '../client/OicqClient';
import Telegram from '../client/Telegram';
import db from './db';
import { Entity } from 'telegram/define';
export type Pair = {
qq: Friend | Group;
@ -34,7 +34,7 @@ class ForwardPairsInternal {
});
}
public find(target: Friend | Group | TelegramChat | Api.Chat | number) {
public find(target: Friend | Group | TelegramChat | Entity | number) {
if (target instanceof Friend) {
return this.pairs.find(e => e.qq instanceof Friend && e.qq.user_id === target.user_id);
}

View File

@ -1,6 +1,6 @@
import Telegram from '../client/Telegram';
import OicqClient from '../client/OicqClient';
import { GroupMessageEvent, PrivateMessageEvent } from 'oicq';
import { GroupMessageEvent, PrivateMessageEvent, Quotable, segment, Sendable } from 'oicq';
import { Pair } from '../providers/forwardPairs';
import { fetchFile, getBigFaceUrl, getImageUrlByMd5 } from '../utils/urls';
import { FileLike, MarkupLike } from 'telegram/define';
@ -12,6 +12,11 @@ import helper from '../helpers/forwardHelper';
import db from '../providers/db';
import { Button } from 'telegram/tl/custom/button';
import { SendMessageParams } from 'telegram/client/messages';
import { Api } from 'telegram';
import { config } from '../providers/userConfig';
import { file as createTempFile, FileResult } from 'tmp-promise';
import fsP from 'fs/promises';
import GeoPoint = Api.GeoPoint;
// noinspection FallThroughInSwitchStatementJS
export default class ForwardService {
@ -177,7 +182,101 @@ export default class ForwardService {
return await pair.tg.sendMessage(messageToSend);
}
catch (e) {
this.log.error('从 QQ 到 TG 到消息转发失败', e);
this.log.error('从 QQ 到 TG 的消息转发失败', e);
}
}
async forwardFromTelegram(message: Api.Message, pair: Pair) {
try {
const tempFiles: FileResult[] = [];
const chain: Sendable = [];
config.workMode === 'group' && chain.push(helper.getUserDisplayName(message.sender) +
(message.forward ? ' Forwarded from ' + helper.getUserDisplayName(message.forward.chat || message.forward.sender) : '') +
': \n');
console.log(message.document);
if (message.photo instanceof Api.Photo ||
// stickers 和以文件发送的图片都是这个
message.document?.mimeType?.startsWith('image/')) {
chain.push(segment.image(await message.downloadMedia({})));
}
else if (message.video || message.videoNote || message.gif) {
const file = message.video || message.videoNote || message.gif;
if (file.size > 20 * 1024 * 1024) {
chain.push('[视频大于 20MB]');
}
else {
const temp = await createTempFile();
tempFiles.push(temp);
await fsP.writeFile(temp.path, await message.downloadMedia({}));
chain.push(segment.video(temp.path));
}
}
else if (message.voice) {
// TODO
chain.push('语音');
}
else if (message.poll) {
const poll = message.poll.poll;
chain.push(`${poll.multipleChoice ? '多' : '单'}选投票:\n${poll.question}`);
chain.push(...poll.answers.map(answer => `\n - ${answer.text}`));
}
else if (message.contact) {
const contact = message.contact;
chain.push(`名片:\n` +
contact.firstName + (contact.lastName ? ' ' + contact.lastName : '') +
(contact.phoneNumber ? `\n电话${contact.phoneNumber}` : ''));
}
else if (message.venue && message.venue.geo instanceof GeoPoint) {
// 地标
chain.push(segment.location(message.venue.geo.lat, message.venue.geo.long, `${message.venue.title} (${message.venue.address})`));
}
else if (message.geo instanceof GeoPoint) {
// 普通的位置,没有名字
chain.push(segment.location(message.geo.lat, message.geo.long, '选中的位置'));
}
else if (message.media instanceof Api.MessageMediaDocument && message.media.document instanceof Api.Document) {
// TODO 转发比较小的群文件
const file = message.media.document;
const fileNameAttribute =
file.attributes.find(attribute => attribute instanceof Api.DocumentAttributeFilename) as Api.DocumentAttributeFilename;
chain.push(`文件:${fileNameAttribute ? fileNameAttribute.fileName : ''}\n` +
`类型:${file.mimeType}\n` +
`大小:${file.size}`);
}
message.message && chain.push(message.message);
// 处理回复
let source: Quotable;
if (message.replyToMsgId) {
try {
const quote = await db.message.findFirst({
where: {
tgChatId: Number(pair.tg.id),
tgMsgId: message.replyToMsgId,
},
});
if (quote) {
source = {
message: quote.brief,
seq: quote.seq,
rand: quote.rand,
user_id: quote.qqSenderId,
time: quote.time,
};
}
}
catch (e) {
this.log.error('查找回复消息失败', e);
}
}
const qqMessage = await pair.qq.sendMsg(chain);
tempFiles.forEach(it => it.cleanup());
return qqMessage;
}
catch (e) {
this.log.error('从 TG 到 QQ 的消息转发失败', e);
}
}
}

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "nodenext",
"module": "CommonJS",
"target": "ESNext",
"esModuleInterop": true,
"sourceMap": false,

View File

@ -1552,6 +1552,7 @@ __metadata:
oicq: ^2.2.0
prisma: latest
telegram: ^2.5.0
tmp-promise: ^3.0.3
ts-node: ^10.5.0
tsc: ^2.0.4
typescript: ^4.5.5
@ -1601,7 +1602,7 @@ __metadata:
languageName: node
linkType: hard
"rimraf@npm:^3.0.2":
"rimraf@npm:^3.0.0, rimraf@npm:^3.0.2":
version: 3.0.2
resolution: "rimraf@npm:3.0.2"
dependencies:
@ -1845,6 +1846,24 @@ __metadata:
languageName: node
linkType: hard
"tmp-promise@npm:^3.0.3":
version: 3.0.3
resolution: "tmp-promise@npm:3.0.3"
dependencies:
tmp: ^0.2.0
checksum: f854f5307dcee6455927ec3da9398f139897faf715c5c6dcee6d9471ae85136983ea06662eba2edf2533bdcb0fca66d16648e79e14381e30c7fb20be9c1aa62c
languageName: node
linkType: hard
"tmp@npm:^0.2.0":
version: 0.2.1
resolution: "tmp@npm:0.2.1"
dependencies:
rimraf: ^3.0.0
checksum: 8b1214654182575124498c87ca986ac53dc76ff36e8f0e0b67139a8d221eaecfdec108c0e6ec54d76f49f1f72ab9325500b246f562b926f85bcdfca8bf35df9e
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"