mirror of https://github.com/Nofated095/Q2TG.git
283 lines
9.8 KiB
TypeScript
283 lines
9.8 KiB
TypeScript
import Telegram from '../client/Telegram';
|
||
import OicqClient from '../client/OicqClient';
|
||
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';
|
||
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';
|
||
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 {
|
||
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);
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|