mirror of https://github.com/Nofated095/Q2TG.git
feat: 从 TG 到 QQ 到消息转发
This commit is contained in:
parent
61c79bb0e7
commit
848d926cc6
|
@ -21,7 +21,8 @@
|
||||||
"log4js": "^6.4.1",
|
"log4js": "^6.4.1",
|
||||||
"nodejs-base64": "^2.0.0",
|
"nodejs-base64": "^2.0.0",
|
||||||
"oicq": "^2.2.0",
|
"oicq": "^2.2.0",
|
||||||
"telegram": "^2.5.0"
|
"telegram": "^2.5.0",
|
||||||
|
"tmp-promise": "^3.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.13.1 || >=16.0.0"
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Client, createClient, Platform } from 'oicq';
|
import { Client, LogLevel, Platform } from 'oicq';
|
||||||
import * as Buffer from 'buffer';
|
import * as Buffer from 'buffer';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import random from '../utils/random';
|
import random from '../utils/random';
|
||||||
|
@ -7,6 +7,8 @@ import fsP from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { Config } from 'oicq/lib/client';
|
import { Config } from 'oicq/lib/client';
|
||||||
|
|
||||||
|
const LOG_LEVEL: LogLevel = 'warn';
|
||||||
|
|
||||||
interface CreateOicqParams {
|
interface CreateOicqParams {
|
||||||
uin: number;
|
uin: number;
|
||||||
password: string;
|
password: string;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Api, TelegramClient } from 'telegram';
|
import { Api, TelegramClient } from 'telegram';
|
||||||
import { StringSession } from 'telegram/sessions';
|
|
||||||
import { BotAuthParams, UserAuthParams } from 'telegram/client/auth';
|
import { BotAuthParams, UserAuthParams } from 'telegram/client/auth';
|
||||||
import { NewMessage, NewMessageEvent } from 'telegram/events';
|
import { NewMessage, NewMessageEvent } from 'telegram/events';
|
||||||
import { EditedMessage, EditedMessageEvent } from 'telegram/events/EditedMessage';
|
import { EditedMessage, EditedMessageEvent } from 'telegram/events/EditedMessage';
|
||||||
|
@ -12,7 +11,7 @@ import os from 'os';
|
||||||
import TelegramChat from './TelegramChat';
|
import TelegramChat from './TelegramChat';
|
||||||
import TelegramSession from './TelegramSession';
|
import TelegramSession from './TelegramSession';
|
||||||
|
|
||||||
type MessageHandler = (message: Api.Message) => Promise<boolean>;
|
type MessageHandler = (message: Api.Message) => Promise<boolean | void>;
|
||||||
|
|
||||||
export default class Telegram {
|
export default class Telegram {
|
||||||
private readonly client: TelegramClient;
|
private readonly client: TelegramClient;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import forwardPairs from '../providers/forwardPairs';
|
||||||
import { DiscussMessageEvent, Friend, Group, GroupMessageEvent, PrivateMessageEvent } from 'oicq';
|
import { DiscussMessageEvent, Friend, Group, GroupMessageEvent, PrivateMessageEvent } from 'oicq';
|
||||||
import db from '../providers/db';
|
import db from '../providers/db';
|
||||||
import helper from '../helpers/forwardHelper';
|
import helper from '../helpers/forwardHelper';
|
||||||
|
import { Api } from 'telegram';
|
||||||
|
|
||||||
export default class ForwardController {
|
export default class ForwardController {
|
||||||
private readonly forwardService: ForwardService;
|
private readonly forwardService: ForwardService;
|
||||||
|
@ -14,10 +15,11 @@ export default class ForwardController {
|
||||||
private readonly oicq: OicqClient) {
|
private readonly oicq: OicqClient) {
|
||||||
this.forwardService = new ForwardService(tgBot, oicq);
|
this.forwardService = new ForwardService(tgBot, oicq);
|
||||||
forwardPairs.init(oicq, tgBot)
|
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;
|
let target: Friend | Group;
|
||||||
if (event.message_type === 'private') {
|
if (event.message_type === 'private') {
|
||||||
target = event.friend;
|
target = event.friend;
|
||||||
|
@ -28,8 +30,8 @@ export default class ForwardController {
|
||||||
else return;
|
else return;
|
||||||
const pair = forwardPairs.find(target);
|
const pair = forwardPairs.find(target);
|
||||||
if (!pair) return;
|
if (!pair) return;
|
||||||
const tgMsg = await this.forwardService.forwardFromQq(event, pair);
|
const tgMessage = await this.forwardService.forwardFromQq(event, pair);
|
||||||
if (tgMsg) {
|
if (tgMessage) {
|
||||||
// 更新数据库
|
// 更新数据库
|
||||||
await db.message.create({
|
await db.message.create({
|
||||||
data: {
|
data: {
|
||||||
|
@ -41,7 +43,31 @@ export default class ForwardController {
|
||||||
rand: event.rand,
|
rand: event.rand,
|
||||||
pktnum: event.pktnum,
|
pktnum: event.pktnum,
|
||||||
tgChatId: Number(pair.tg.id),
|
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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,10 @@ import { CustomFile } from 'telegram/client/uploads';
|
||||||
import { Friend, Group } from 'oicq';
|
import { Friend, Group } from 'oicq';
|
||||||
import { base64decode } from 'nodejs-base64';
|
import { base64decode } from 'nodejs-base64';
|
||||||
import { getLogger } from 'log4js';
|
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');
|
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()
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Friend, Group } from 'oicq';
|
import { Friend, Group } from 'oicq';
|
||||||
import TelegramChat from '../client/TelegramChat';
|
import TelegramChat from '../client/TelegramChat';
|
||||||
import { Api } from 'telegram';
|
|
||||||
import OicqClient from '../client/OicqClient';
|
import OicqClient from '../client/OicqClient';
|
||||||
import Telegram from '../client/Telegram';
|
import Telegram from '../client/Telegram';
|
||||||
import db from './db';
|
import db from './db';
|
||||||
|
import { Entity } from 'telegram/define';
|
||||||
|
|
||||||
export type Pair = {
|
export type Pair = {
|
||||||
qq: Friend | Group;
|
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) {
|
if (target instanceof Friend) {
|
||||||
return this.pairs.find(e => e.qq instanceof Friend && e.qq.user_id === target.user_id);
|
return this.pairs.find(e => e.qq instanceof Friend && e.qq.user_id === target.user_id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Telegram from '../client/Telegram';
|
import Telegram from '../client/Telegram';
|
||||||
import OicqClient from '../client/OicqClient';
|
import OicqClient from '../client/OicqClient';
|
||||||
import { GroupMessageEvent, PrivateMessageEvent } from 'oicq';
|
import { GroupMessageEvent, PrivateMessageEvent, Quotable, segment, Sendable } from 'oicq';
|
||||||
import { Pair } from '../providers/forwardPairs';
|
import { Pair } from '../providers/forwardPairs';
|
||||||
import { fetchFile, getBigFaceUrl, getImageUrlByMd5 } from '../utils/urls';
|
import { fetchFile, getBigFaceUrl, getImageUrlByMd5 } from '../utils/urls';
|
||||||
import { FileLike, MarkupLike } from 'telegram/define';
|
import { FileLike, MarkupLike } from 'telegram/define';
|
||||||
|
@ -12,6 +12,11 @@ import helper from '../helpers/forwardHelper';
|
||||||
import db from '../providers/db';
|
import db from '../providers/db';
|
||||||
import { Button } from 'telegram/tl/custom/button';
|
import { Button } from 'telegram/tl/custom/button';
|
||||||
import { SendMessageParams } from 'telegram/client/messages';
|
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
|
// noinspection FallThroughInSwitchStatementJS
|
||||||
export default class ForwardService {
|
export default class ForwardService {
|
||||||
|
@ -177,7 +182,101 @@ export default class ForwardService {
|
||||||
return await pair.tg.sendMessage(messageToSend);
|
return await pair.tg.sendMessage(messageToSend);
|
||||||
}
|
}
|
||||||
catch (e) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "nodenext",
|
"module": "CommonJS",
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
|
|
21
yarn.lock
21
yarn.lock
|
@ -1552,6 +1552,7 @@ __metadata:
|
||||||
oicq: ^2.2.0
|
oicq: ^2.2.0
|
||||||
prisma: latest
|
prisma: latest
|
||||||
telegram: ^2.5.0
|
telegram: ^2.5.0
|
||||||
|
tmp-promise: ^3.0.3
|
||||||
ts-node: ^10.5.0
|
ts-node: ^10.5.0
|
||||||
tsc: ^2.0.4
|
tsc: ^2.0.4
|
||||||
typescript: ^4.5.5
|
typescript: ^4.5.5
|
||||||
|
@ -1601,7 +1602,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"rimraf@npm:^3.0.2":
|
"rimraf@npm:^3.0.0, rimraf@npm:^3.0.2":
|
||||||
version: 3.0.2
|
version: 3.0.2
|
||||||
resolution: "rimraf@npm:3.0.2"
|
resolution: "rimraf@npm:3.0.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -1845,6 +1846,24 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"token-types@npm:^5.0.0-alpha.2":
|
||||||
version: 5.0.0-alpha.2
|
version: 5.0.0-alpha.2
|
||||||
resolution: "token-types@npm:5.0.0-alpha.2"
|
resolution: "token-types@npm:5.0.0-alpha.2"
|
||||||
|
|
Loading…
Reference in New Issue