Merge branch 'rainbowcat' of github.com:clansty/Q2TG into rainbowcat

This commit is contained in:
Nofated095 2024-06-03 09:05:59 +08:00
commit c632bc2bb1
86 changed files with 5953 additions and 2747 deletions

View File

@ -46,3 +46,5 @@ jobs:
platforms: linux/amd64
cache-from: type=gha
cache-to: type=gha,mode=max
secrets: |
"npmrc=//npm.pkg.github.com/:_authToken=${{ secrets.GPR_TOKEN }}"

1
.npmrc Normal file
View File

@ -0,0 +1 @@
@icqqjs:registry=https://npm.pkg.github.com

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"github-actions.workflows.pinned.workflows": [
".github/workflows/main.yml"
]
}

View File

@ -12,23 +12,26 @@ ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
FROM base AS build-env
FROM base AS build
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt update && apt-get --no-install-recommends install -y \
python3 build-essential pkg-config \
libpixman-1-dev libcairo2-dev libpango1.0-dev libgif-dev libjpeg62-turbo-dev libpng-dev librsvg2-dev libvips-dev
COPY package.json pnpm-lock.yaml /app/
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml /app/
COPY patches /app/patches
COPY main/package.json /app/main/
FROM build-env AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store,sharing=locked pnpm install --prod --frozen-lockfile
FROM build-env AS build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store,sharing=locked pnpm install --frozen-lockfile
COPY src tsconfig.json /app/
COPY prisma /app/
RUN pnpm exec prisma generate
RUN pnpm run build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store,sharing=locked \
--mount=type=secret,id=npmrc,target=/root/.npmrc \
pnpm install --frozen-lockfile
COPY main/src main/tsconfig.json /app/main/
COPY main/prisma /app/main/
RUN cd main && pnpm exec prisma generate
RUN cd main && pnpm run build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store,sharing=locked \
--mount=type=secret,id=npmrc,target=/root/.npmrc \
pnpm deploy --filter=q2tg-main --prod deploy
FROM debian:bookworm-slim AS tgs-to-gif-build
ADD https://github.com/conan-io/conan/releases/download/1.61.0/conan-ubuntu-64.deb /tmp/conan.deb
@ -44,17 +47,28 @@ RUN conan install .
RUN sed -i 's/\${CONAN_LIBS}/z/g' CMakeLists.txt
RUN cmake CMakeLists.txt && make
FROM base AS build-front
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml /app/
COPY patches /app/patches
COPY ui/package.json /app/ui/
RUN --mount=type=cache,id=pnpm,target=/pnpm/store,sharing=locked pnpm install --frozen-lockfile
COPY ui/index.html ui/tsconfig.json ui/vite.config.ts /app/ui/
COPY ui/src /app/ui/src
RUN cd ui && pnpm run build
FROM base
COPY assets /app/
COPY --from=tgs-to-gif-build /app/bin/tgs_to_gif /usr/local/bin/tgs_to_gif
ENV TGS_TO_GIF=/usr/local/bin/tgs_to_gif
COPY package.json pnpm-lock.yaml /app/
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY prisma /app/
COPY main/assets /app/assets
COPY --from=build /app/deploy /app
COPY main/prisma /app/
RUN pnpm exec prisma generate
COPY --from=build /app/build /app/build
COPY --from=build-front /app/ui/dist /app/front
ENV UI_PATH=/app/front
ENV DATA_DIR=/app/data
EXPOSE 8080
CMD pnpm start

View File

@ -43,6 +43,9 @@ services:
- postgres
- zinclabs
- sign
ports:
# 如果要使用 RICH_HEADER 需要将端口发布到公网
- 8080:8080
volumes:
- q2tg:/app/data
- /var/run/docker.sock:/var/run/docker.sock
@ -65,6 +68,8 @@ services:
- BAIDU_APP_ID=
- BAIDU_API_KEY=
- BAIDU_SECRET_KEY=
# 如果你需要使用 /flags set RICH_HEADER 来显示头像,则需将 q2tg 8080 端口发布到公网,可以使用 cloudflare tunnel
#- WEB_ENDPOINT=https://yourichheader.com 填写你发布到公网的域名
# 如果需要通过代理联网,那么设置下面两个变量
#- PROXY_IP=
#- PROXY_PORT=

135
main/assets/richHeader.ejs Normal file
View File

@ -0,0 +1,135 @@
<html lang="zh">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<% const now = new Date() %>
<meta property="og:image"
content="https://q1.qlogo.cn/g?b=qq&nk=<%= userId %>&s=0&time=<%= now.getFullYear() %>-<%= now.getMonth() %>-<%= now.getDate() %>"/>
<% if (!!title) { %>
<meta property="og:site_name" content="「<%= title %>」"/>
<% }else { %>
<meta property="og:site_name" content="<%= role %>"/>
<% } %>
<meta property="og:title" content="<%= name %>"/>
<title>群成员:<%= name %></title>
<style>
html, body {
padding: 0;
margin: 0;
color: #303133;
}
* {
box-sizing: border-box;
}
#app {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 20px;
}
#avatar, #card {
width: 100%;
max-width: 500px;
}
#card {
padding: 0 20px;
line-height: 1.8em;
}
.badge {
border-radius: 0.5em;
color: #fff;
padding: 0 0.2em;
}
.badge-owner {
background-color: #FDCE3A !important;
}
.badge-admin {
background-color: #2FE1D8 !important;
}
.badge-member {
background-color: #ADB5CA;
}
.badge-hasTitle {
background-color: #D88BFF;
}
.secondary {
color: #606266;
font-size: small;
}
.detailItem {
font-size: smaller;
margin-top: 0.5em;
}
@media screen and (min-width: 900px) {
#app {
flex-direction: row;
height: 100%;
}
#avatar {
width: 400px;
}
#card {
width: fit-content;
}
}
</style>
</head>
<body>
<div id="app">
<img id="avatar" src="https://q1.qlogo.cn/g?b=qq&nk=<%= userId %>&s=0" alt="头像">
<div id="card">
<div>
<span class="badge badge-<%= role %> <%= title && 'badge-hasTitle' %>"><%= title || role %></span>
<%= name %>
</div>
<% if(nickname !== name) { %>
<div class="secondary"><%= nickname %></div>
<% } %>
<div class="secondary"><%= userId %>
<% if(qid){ %>
<span style="padding-left: 1em">QID: <%= qid %></span>
<% } %>
<% if(email){ %>
<span style="padding-left: 1em"><%= email %></span>
<% } %>
</div>
<% if(location) { %>
<div class="secondary"><%= location %></div>
<% } %>
<% if(birthday) { %>
<div class="detailItem">
<div class="secondary">生日</div>
<%= birthday %>
</div>
<% } %>
<div class="detailItem">
<div class="secondary">加入时间</div>
<%= joinTime %>
</div>
<div class="detailItem">
<div class="secondary">上次发言时间</div>
<%= lastSentTime %>
</div>
<div class="detailItem">
<div class="secondary">注册时间</div>
<%= regTime %>
</div>
</div>
</div>
</body>
</html>

65
main/package.json Normal file
View File

@ -0,0 +1,65 @@
{
"name": "q2tg-main",
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "prisma db push --accept-data-loss --skip-generate && node build/index.js",
"prisma": "prisma",
"import": "ts-node tools/import"
},
"bin": "build/index.js",
"files": [
"build",
"assets"
],
"devDependencies": {
"@types/cli-progress": "^3.11.5",
"@types/date-and-time": "^3.0.3",
"@types/dockerode": "^3.3.29",
"@types/ejs": "^3.1.5",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/lodash": "^4.17.1",
"@types/markdown-escape": "^1.1.3",
"@types/node": "^20.12.8",
"@types/probe-image-size": "^7.2.4",
"@types/prompts": "^2.4.9",
"tsx": "^4.9.0"
},
"dependencies": {
"@fastify/http-proxy": "^9.5.0",
"@fastify/static": "^7.0.3",
"@icqqjs/icqq": "1.2.0",
"@prisma/client": "5.13.0",
"axios": "^1.6.8",
"baidu-aip-sdk": "^4.16.15",
"big-integer": "^1.6.52",
"cli-progress": "^3.12.0",
"date-and-time": "^3.1.1",
"dockerode": "^4.0.2",
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"eviltransform": "^0.2.2",
"fastify": "^4.26.2",
"file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2",
"image-size": "^1.1.1",
"lodash": "^4.17.21",
"log4js": "^6.9.1",
"markdown-escape": "^2.0.0",
"nodejs-base64": "^2.0.0",
"prisma": "5.13.0",
"probe-image-size": "^7.2.3",
"prompts": "^2.4.2",
"quote-api": "https://github.com/Clansty/quote-api/archive/014b21138afbbe0e12c91b00561414b1e851fc0f.tar.gz",
"sharp": "^0.33.3",
"silk-sdk": "^0.2.2",
"telegram": "https://github.com/clansty/gramjs/releases/download/2.19.10%2Brevert_media/telegram-2.19.10.tgz",
"tmp-promise": "^3.0.3",
"undici": "^6.15.0",
"zincsearch-node": "^2.1.1",
"zod": "^3.23.6"
},
"engines": {
"node": "^14.13.1 || >=16.0.0"
}
}

View File

@ -90,6 +90,7 @@ model ForwardPair {
instanceId Int @default(0)
instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
flags Int @default(0)
apiKey String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
@@unique([qqRoomId, instanceId])
@@unique([tgChatId, instanceId])

39
main/src/api/index.ts Normal file
View File

@ -0,0 +1,39 @@
import { getLogger } from 'log4js';
import Fastify from 'fastify';
import FastifyProxy from '@fastify/http-proxy';
import FastifyStatic from '@fastify/static';
import env from '../models/env';
import richHeader from './richHeader';
import telegramAvatar from './telegramAvatar';
const log = getLogger('Web Api');
const fastify = Fastify();
fastify.get('/', async (request, reply) => {
return { hello: 'Q2TG' };
});
fastify.register(richHeader, { prefix: '/richHeader' });
fastify.register(telegramAvatar, { prefix: '/telegramAvatar' });
if (env.UI_PROXY) {
fastify.register(FastifyProxy, {
upstream: env.UI_PROXY,
prefix: '/ui',
rewritePrefix: '/ui',
websocket: true,
});
}
else if (env.UI_PATH) {
fastify.register(FastifyStatic, {
root: env.UI_PATH,
prefix: '/ui',
});
}
export default {
async startListening() {
await fastify.listen({ port: env.LISTEN_PORT, host: '0.0.0.0' });
log.info('Listening on', env.LISTEN_PORT);
},
};

View File

@ -0,0 +1,47 @@
import { FastifyPluginCallback } from 'fastify';
import { Pair } from '../models/Pair';
import ejs from 'ejs';
import fs from 'fs';
import { Group } from '@icqqjs/icqq';
import { format } from 'date-and-time';
const template = ejs.compile(fs.readFileSync('./assets/richHeader.ejs', 'utf-8'));
export default ((fastify, opts, done) => {
fastify.get<{
Params: { apiKey: string, userId: string }
}>('/:apiKey/:userId', async (request, reply) => {
const pair = Pair.getByApiKey(request.params.apiKey);
if (!pair) {
reply.code(404);
return 'Group not found';
}
const group = pair.qq as Group;
const members = await group.getMemberMap();
const member = members.get(Number(request.params.userId));
if (!member) {
reply.code(404);
return 'Member not found';
}
const profile = await pair.qq.client.getProfile(member.user_id);
reply.type('text/html');
return template({
userId: request.params.userId,
title: member.title,
name: member.card || member.nickname,
role: member.role,
joinTime: format(new Date(member.join_time * 1000), 'YYYY-MM-DD HH:mm'),
lastSentTime: format(new Date(member.last_sent_time * 1000), 'YYYY-MM-DD HH:mm'),
regTime: format(new Date(profile.regTimestamp * 1000), 'YYYY-MM-DD HH:mm'),
location: [profile.country, profile.province, profile.city].join(' ').trim(),
nickname: member.nickname,
email: profile.email,
qid: profile.QID,
signature: profile.signature,
birthday: (profile.birthday || []).join('/'),
});
});
done();
}) as FastifyPluginCallback;

View File

@ -0,0 +1,52 @@
import { FastifyPluginCallback } from 'fastify';
import Instance from '../models/Instance';
import convert from '../helpers/convert';
import Telegram from '../client/Telegram';
import { Api } from 'telegram';
import BigInteger from 'big-integer';
import { getLogger } from 'log4js';
import fs from 'fs';
const log = getLogger('telegramAvatar');
const userAvatarFileIdCache = new Map<string, BigInteger.BigInteger>();
const getUserAvatarFileId = async (tgBot: Telegram, userId: string) => {
let cached = userAvatarFileIdCache.get(userId);
if (cached) return cached;
const user = await tgBot.getChat(userId);
if ('photo' in user.entity && user.entity.photo instanceof Api.UserProfilePhoto) {
cached = user.entity.photo.photoId;
}
else {
cached = BigInteger.zero;
}
userAvatarFileIdCache.set(userId, cached);
return cached;
};
const getUserAvatarPath = async (tgBot: Telegram, userId: string) => {
const fileId = await getUserAvatarFileId(tgBot, userId);
if (fileId.eq(0)) return '';
return await convert.cachedBuffer(fileId.toString(16) + '.jpg', () => tgBot.downloadEntityPhoto(userId));
};
export default ((fastify, opts, done) => {
fastify.get<{
Params: { instanceId: string, userId: string }
}>('/:instanceId/:userId', async (request, reply) => {
log.debug('请求头像', request.params.userId);
const instance = Instance.instances.find(it => it.id.toString() === request.params.instanceId);
const avatar = await getUserAvatarPath(instance.tgBot, request.params.userId);
if (!avatar) {
reply.code(404);
return;
}
reply.type('image/jpeg');
return fs.createReadStream(avatar);
});
done();
}) as FastifyPluginCallback;

View File

@ -4,22 +4,19 @@ import {
Friend,
Group,
GroupMessageEvent,
LogLevel,
Platform, PrivateMessage,
PrivateMessageEvent, XmlElem,
} from 'icqq';
import Buffer from 'buffer';
import { execSync } from 'child_process';
LogLevel, Platform, PrivateMessage,
PrivateMessageEvent,
} from '@icqqjs/icqq';
import random from '../utils/random';
import fs from 'fs';
import fsP from 'fs/promises';
import { Config } from 'icqq/lib/client';
import { Config } from '@icqqjs/icqq/lib/client';
import dataPath from '../helpers/dataPath';
import os from 'os';
import { Converter, Image, rand2uuid } from 'icqq/lib/message';
import { Converter, Image, rand2uuid } from '@icqqjs/icqq/lib/message';
import { randomBytes } from 'crypto';
import { escapeXml, gzip, timestamp } from 'icqq/lib/common';
import { pb } from 'icqq/lib/core';
import { gzip, timestamp } from '@icqqjs/icqq/lib/common';
import { pb } from '@icqqjs/icqq/lib/core';
import env from '../models/env';
const LOG_LEVEL: LogLevel = env.OICQ_LOG_LEVEL;

View File

@ -12,7 +12,7 @@ import TelegramChat from './TelegramChat';
import TelegramSession from '../models/TelegramSession';
import { LogLevel } from 'telegram/extensions/Logger';
import { BigInteger } from 'big-integer';
import { EditMessageParams, IterMessagesParams } from 'telegram/client/messages';
import { IterMessagesParams } from 'telegram/client/messages';
import { PromisedNetSockets, PromisedWebSockets } from 'telegram/extensions';
import { ConnectionTCPFull, ConnectionTCPObfuscated } from 'telegram/network';
import env from '../models/env';
@ -62,7 +62,7 @@ export default class Telegram {
connection: env.TG_CONNECTION === 'websocket' ? ConnectionTCPObfuscated : ConnectionTCPFull,
},
);
// this.client.logger.setLevel(LogLevel.WARN);
this.client.logger.setLevel(env.TG_LOG_LEVEL as LogLevel);
}
public static async create(startArgs: UserAuthParams | BotAuthParams, appName = 'Q2TG') {

View File

@ -28,6 +28,10 @@ const personalPrivateCommands = [
command: 'login',
description: '当 QQ 处于下线状态时,使用此命令重新登录 QQ',
}),
new Api.BotCommand({
command: 'flags',
description: 'WARNING: EXPERIMENTAL FEATURES AHEAD!',
}),
];
// 服务器零号实例的管理员
@ -57,6 +61,10 @@ const inChatCommands = [
command: 'search',
description: '搜索消息',
}),
new Api.BotCommand({
command: 'q',
description: '生成 QuotLy 图片',
}),
];
const groupInChatCommands = [
@ -93,6 +101,10 @@ const personalInChatCommands = [
command: 'nick',
description: '获取/设置群名片',
}),
new Api.BotCommand({
command: 'mute',
description: '设置 QQ 成员禁言',
}),
];
export default {

View File

@ -9,6 +9,10 @@ enum flags {
RICH_HEADER = 1 << 7,
NO_QUOTE_PIN = 1 << 8,
NO_FORWARD_OTHER_BOT = 1 << 9,
USE_MARKDOWN = 1 << 10,
DISABLE_SEAMLESS = 1 << 11,
NO_FLASH_PIC = 1 << 12,
DISABLE_SLASH_COMMAND = 1 << 13,
}
export default flags;

View File

@ -19,11 +19,11 @@ export default class AliveCheckController {
}
await message.reply({
message: this.genMessage(this.instance.id === 0 ? Instance.instances : [this.instance]),
message: await this.genMessage(this.instance.id === 0 ? Instance.instances : [this.instance]),
});
};
private genMessage(instances: Instance[]): string {
private async genMessage(instances: Instance[]): Promise<string> {
const boolToStr = (value: boolean) => {
return value ? '好' : '坏';
};
@ -32,7 +32,6 @@ export default class AliveCheckController {
for (const instance of instances) {
const oicq = instance.oicq;
const tgBot = instance.tgBot;
}
return messageParts.join('\n\n');
};

View File

@ -9,7 +9,7 @@ import {
MemberDecreaseEvent,
MemberIncreaseEvent,
PrivateMessageEvent,
} from 'icqq';
} from '@icqqjs/icqq';
import Instance from '../models/Instance';
import { getLogger, Logger } from 'log4js';
import { editFlags } from '../utils/flagControl';

View File

@ -2,7 +2,8 @@ import DeleteMessageService from '../services/DeleteMessageService';
import Telegram from '../client/Telegram';
import OicqClient from '../client/OicqClient';
import { Api } from 'telegram';
import { FriendRecallEvent, GroupRecallEvent } from 'icqq';
import { FriendRecallEvent, GroupRecallEvent } from '@icqqjs/icqq';
import { DeletedMessageEvent } from 'telegram/events/DeletedMessage';
import Instance from '../models/Instance';
export default class DeleteMessageController {

View File

@ -8,7 +8,7 @@ import {
GroupPokeEvent,
MemberIncreaseEvent,
PrivateMessageEvent,
} from 'icqq';
} from '@icqqjs/icqq';
import db from '../models/db';
import { Api } from 'telegram';
import { getLogger, Logger } from 'log4js';
@ -93,6 +93,7 @@ export default class ForwardController {
};
private onTelegramUserMessage = async (message: Api.Message) => {
if (!message.sender) return;
if (!('bot' in message.sender) || !message.sender.bot) return;
const pair = this.instance.forwardPairs.find(message.chat);
if (!pair) return;
@ -176,6 +177,7 @@ export default class ForwardController {
private onQqPoke = async (event: FriendPokeEvent | GroupPokeEvent) => {
const target = event.notice_type === 'friend' ? event.friend : event.group;
const pair = this.instance.forwardPairs.find(target);
if (!pair) return;
if ((pair?.flags | this.instance.flags) & flags.DISABLE_POKE) return;
let operatorName: string, targetName: string;
if (target instanceof Friend) {

View File

@ -1,13 +1,14 @@
import Instance from '../models/Instance';
import Telegram from '../client/Telegram';
import OicqClient from '../client/OicqClient';
import { AtElem, Group, GroupMessageEvent, PrivateMessageEvent, Sendable } from 'icqq';
import { AtElem, Group, GroupMessageEvent, PrivateMessageEvent, Sendable } from '@icqqjs/icqq';
import { Pair } from '../models/Pair';
import { Api } from 'telegram';
import db from '../models/db';
import BigInteger from 'big-integer';
import helper from '../helpers/forwardHelper';
import { getLogger, Logger } from 'log4js';
import flags from '../constants/flags';
type ActionSubjectTg = {
name: string;
@ -40,6 +41,7 @@ export default class {
if (event.message_type !== 'group') return;
const pair = this.instance.forwardPairs.find(event.group);
if (!pair) return;
if ((pair.flags | this.instance.flags) & flags.DISABLE_SLASH_COMMAND) return;
const chain = [...event.message];
while (chain.length && chain[0].type !== 'text') {
chain.shift();
@ -108,6 +110,7 @@ export default class {
private onTelegramMessage = async (message: Api.Message) => {
const pair = this.instance.forwardPairs.find(message.chat);
if (!pair) return;
if ((pair.flags | this.instance.flags) & flags.DISABLE_SLASH_COMMAND) return;
const exec = COMMAND_REGEX.exec(message.message);
if (!exec) return;
const action = exec[2] || exec[3];

View File

@ -4,7 +4,7 @@ import Instance from '../models/Instance';
import Telegram from '../client/Telegram';
import OicqClient from '../client/OicqClient';
import { Api } from 'telegram';
import { Group } from 'icqq';
import { Group } from '@icqqjs/icqq';
import RecoverMessageHelper from '../helpers/RecoverMessageHelper';
import flags from '../constants/flags';
import { editFlags } from '../utils/flagControl';
@ -43,6 +43,13 @@ export default class InChatCommandsController {
case '/poke':
await this.service.poke(message, pair);
return true;
case '/unmute':
messageParts.unshift('0');
case '/mute':
if (this.instance.workMode !== 'personal' || !message.senderId?.eq(this.instance.owner)) return false;
if (!(pair.qq instanceof Group)) return true;
await this.service.mute(message, pair, messageParts);
return true;
case '/forwardoff':
pair.flags |= flags.DISABLE_Q2TG | flags.DISABLE_TG2Q;
await message.reply({ message: '转发已禁用' });
@ -84,7 +91,7 @@ export default class InChatCommandsController {
return true;
case '/nick':
if (this.instance.workMode !== 'personal' || !message.senderId?.eq(this.instance.owner)) return false;
if (!(pair.qq instanceof Group)) return;
if (!(pair.qq instanceof Group)) return true;
if (!params) {
await message.reply({
message: `群名片:<i>${pair.qq.pickMember(this.instance.qqUin, true).card}</i>`,

View File

@ -1,7 +1,7 @@
import Instance from '../models/Instance';
import Telegram from '../client/Telegram';
import OicqClient from '../client/OicqClient';
import { GroupMessageEvent, MiraiElem, PrivateMessageEvent } from 'icqq';
import { GroupMessageEvent, MiraiElem, PrivateMessageEvent } from '@icqqjs/icqq';
export default class {
constructor(private readonly instance: Instance,

View File

@ -2,7 +2,7 @@ import Instance from '../models/Instance';
import Telegram from '../client/Telegram';
import OicqClient from '../client/OicqClient';
import { getLogger, Logger } from 'log4js';
import { Group, GroupMessageEvent, PrivateMessageEvent } from 'icqq';
import { Group, GroupMessageEvent, PrivateMessageEvent } from '@icqqjs/icqq';
import { Api } from 'telegram';
import quotly from 'quote-api/methods/generate.js';
import { CustomFile } from 'telegram/client/uploads';

View File

@ -2,7 +2,7 @@ import { getLogger, Logger } from 'log4js';
import Instance from '../models/Instance';
import Telegram from '../client/Telegram';
import OicqClient from '../client/OicqClient';
import { FriendRequestEvent, GroupInviteEvent } from 'icqq';
import { FriendRequestEvent, GroupInviteEvent } from '@icqqjs/icqq';
import { getAvatar } from '../utils/urls';
import { CustomFile } from 'telegram/client/uploads';
import { Button } from 'telegram/tl/custom/button';

View File

@ -3,7 +3,7 @@ import Telegram from '../client/Telegram';
import OicqClient from '../client/OicqClient';
import { Pair } from '../models/Pair';
import { Api } from 'telegram';
import { GroupMessage, PrivateMessage } from 'icqq';
import { GroupMessage, PrivateMessage } from '@icqqjs/icqq';
import db from '../models/db';
import { format } from 'date-and-time';
import lottie from '../constants/lottie';

View File

@ -3,9 +3,11 @@ import { CustomFile } from 'telegram/client/uploads';
import { base64decode } from 'nodejs-base64';
import { getLogger } from 'log4js';
import { Entity } from 'telegram/define';
import { ForwardMessage } from 'icqq';
import { ForwardMessage } from '@icqqjs/icqq';
import { Api } from 'telegram';
import { imageSize } from 'image-size';
import env from '../models/env';
import { md5Hex } from '../utils/hashing';
const log = getLogger('ForwardHelper');
@ -30,7 +32,7 @@ export default {
|| dimensions.width + dimensions.height > 10000
) {
// 让 Telegram 服务器下载
return url
return url;
}
}
if (allowWebp) {
@ -192,4 +194,17 @@ export default {
}
return null;
},
generateRichHeaderUrl(apiKey: string, userId: number, messageHeader = '') {
const url = new URL(`${env.WEB_ENDPOINT}/richHeader/${apiKey}/${userId}`);
// 防止群名片刷新慢
messageHeader && url.searchParams.set('hash', md5Hex(messageHeader).substring(0, 10));
return url.toString();
},
generateTelegramAvatarUrl(instanceId: number, userId: number) {
if (!env.WEB_ENDPOINT) return '';
const url = new URL(`${env.WEB_ENDPOINT}/telegramAvatar/${instanceId}/${userId}`);
return url.toString();
},
};

View File

@ -1,4 +1,4 @@
import { Platform } from 'icqq';
import { Platform } from '@icqqjs/icqq';
export default {
convertTextToPlatform(text: string): Platform {

View File

@ -1,6 +1,8 @@
import { configure, getLogger } from 'log4js';
import Instance from './models/Instance';
import db from './models/db';
import api from './api';
import env from './models/env';
(async () => {
configure({
@ -8,7 +10,7 @@ import db from './models/db';
console: { type: 'console' },
},
categories: {
default: { level: 'debug', appenders: ['console'] },
default: { level: env.LOG_LEVEL, appenders: ['console'] },
},
});
const log = getLogger('Main');
@ -20,6 +22,9 @@ import db from './models/db';
process.on('unhandledRejection', error => {
log.error('UnhandledException: ', error);
});
await api.startListening();
const instanceEntries = await db.instance.findMany();
if (!instanceEntries.length) {

View File

@ -1,4 +1,4 @@
import { Friend, Group } from 'icqq';
import { Friend, Group } from '@icqqjs/icqq';
import TelegramChat from '../client/TelegramChat';
import OicqClient from '../client/OicqClient';
import Telegram from '../client/Telegram';
@ -28,7 +28,7 @@ export default class ForwardPairs {
const tg = await tgBot.getChat(Number(i.tgChatId));
const tgUserChat = await tgUser.getChat(Number(i.tgChatId));
if (qq && tg && tgUserChat) {
this.pairs.push(new Pair(qq, tg, tgUserChat, i.id, i.flags));
this.pairs.push(new Pair(qq, tg, tgUserChat, i.id, i.flags, i.apiKey));
}
}
catch (e) {
@ -51,7 +51,7 @@ export default class ForwardPairs {
instanceId: this.instanceId,
},
});
this.pairs.push(new Pair(qq, tg, tgUser, dbEntry.id, dbEntry.flags));
this.pairs.push(new Pair(qq, tg, tgUser, dbEntry.id, dbEntry.flags, dbEntry.apiKey));
return dbEntry;
}

View File

@ -1,5 +1,5 @@
import { getLogger } from 'log4js';
import { Friend, Group } from 'icqq';
import { Friend, Group } from '@icqqjs/icqq';
import TelegramChat from '../client/TelegramChat';
import getAboutText from '../utils/getAboutText';
import { md5 } from '../utils/hashing';
@ -9,6 +9,12 @@ import db from './db';
const log = getLogger('ForwardPair');
export class Pair {
private static readonly apiKeyMap = new Map<string, Pair>();
public static getByApiKey(key: string) {
return this.apiKeyMap.get(key);
}
// 群成员的 tg 账号对应它对应的 QQ 账号获取到的 Group 对象
// 只有群组模式有效
// public readonly instanceMapForTg = {} as { [tgUserId: string]: Group };
@ -19,7 +25,11 @@ export class Pair {
// public readonly tgUser: TelegramChat,
public dbId: number,
private _flags: number,
public readonly apiKey: string,
) {
if (apiKey) {
Pair.apiKeyMap.set(apiKey, this);
}
}
// 更新 TG 群组的头像和简介

View File

@ -3,7 +3,9 @@ import path from 'path';
const configParsed = z.object({
DATA_DIR: z.string().default(path.resolve('./data')),
LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'mark', 'off']).default('info'),
OICQ_LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'mark', 'off']).default('warn'),
TG_LOG_LEVEL: z.enum(['none', 'error', 'warn', 'info', 'debug']).default('warn'),
FFMPEG_PATH: z.string().optional(),
FFPROBE_PATH: z.string().optional(),
SIGN_API: z.string().url().optional(),
@ -29,6 +31,10 @@ const configParsed = z.object({
BAIDU_API_KEY: z.string().optional(),
BAIDU_SECRET_KEY: z.string().optional(),
DISABLE_FILE_UPLOAD_TIP: z.string().transform((v) => ['true', '1', 'yes'].includes(v.toLowerCase())).default('false'),
LISTEN_PORT: z.string().regex(/^\d+$/).transform(Number).default('8080'),
UI_PATH: z.string().optional(),
UI_PROXY: z.string().url().optional(),
WEB_ENDPOINT: z.string().url().optional(),
}).safeParse(process.env);
if (!configParsed.success) {

View File

@ -1,5 +1,5 @@
import Telegram from '../client/Telegram';
import { Friend, FriendInfo, Group, GroupInfo } from 'icqq';
import { Friend, FriendInfo, Group, GroupInfo } from '@icqqjs/icqq';
import { Button } from 'telegram/tl/custom/button';
import { getLogger, Logger } from 'log4js';
import { getAvatar } from '../utils/urls';

View File

@ -2,7 +2,7 @@ import Telegram from '../client/Telegram';
import { getLogger, Logger } from 'log4js';
import { Api } from 'telegram';
import db from '../models/db';
import { Friend, FriendRecallEvent, Group, GroupRecallEvent } from 'icqq';
import { Friend, FriendRecallEvent, Group, GroupRecallEvent } from '@icqqjs/icqq';
import Instance from '../models/Instance';
import { Pair } from '../models/Pair';
import { consumer } from '../utils/highLevelFunces';

View File

@ -10,8 +10,8 @@ import {
Quotable,
segment,
Sendable,
} from 'icqq';
import { fetchFile, getBigFaceUrl, getImageUrlByMd5 } from '../utils/urls';
} from '@icqqjs/icqq';
import { fetchFile, getBigFaceUrl, getImageUrlByMd5, isContainsUrl } from '../utils/urls';
import { ButtonLike, FileLike } from 'telegram/define';
import { getLogger, Logger } from 'log4js';
import path from 'path';
@ -26,7 +26,7 @@ import fsP from 'fs/promises';
import eviltransform from 'eviltransform';
import silk from '../encoding/silk';
import axios from 'axios';
import { md5Hex } from '../utils/hashing';
import { md5B64, md5Hex } from '../utils/hashing';
import Instance from '../models/Instance';
import { Pair } from '../models/Pair';
import OicqClient from '../client/OicqClient';
@ -38,15 +38,19 @@ import { QQMessageSent } from '../types/definitions';
import ZincSearch from 'zincsearch-node';
import { speech as AipSpeechClient } from 'baidu-aip-sdk';
import random from '../utils/random';
import { escapeXml } from 'icqq/lib/common';
import { escapeXml } from '@icqqjs/icqq/lib/common';
import Docker from 'dockerode';
import ReplyKeyboardHide = Api.ReplyKeyboardHide;
import env from '../models/env';
import { CustomFile } from 'telegram/client/uploads';
import flags from '../constants/flags';
import BigInteger from 'big-integer';
import { Image } from '@icqqjs/icqq/lib/message';
import probe from 'probe-image-size';
import markdownEscape from 'markdown-escape';
const NOT_CHAINABLE_ELEMENTS = ['flash', 'record', 'video', 'location', 'share', 'json', 'xml', 'poke'];
const IMAGE_MIMES = ['image/jpeg', 'image/png', 'image/apng', 'image/webp', 'image/gif', 'image/bmp', 'image/tiff', 'image/x-icon', 'image/avif', 'image/heic', 'image/heif'];
// noinspection FallThroughInSwitchStatementJS
export default class ForwardService {
@ -144,7 +148,14 @@ export default class ForwardService {
message = '[<i>转发多条消息(未配置)</i>]';
}
};
for (const elem of event.message) {
for (let elem of event.message) {
if (elem.type === 'flash' && (pair.flags | this.instance.flags) & flags.NO_FLASH_PIC) {
message += '<i>[闪照]</i>';
elem = {
...elem,
type: 'image',
};
}
let url: string;
switch (elem.type) {
case 'text': {
@ -169,6 +180,10 @@ export default class ForwardService {
case 'at': {
if (event.source?.user_id === elem.qq || event.source?.user_id === this.oicq.uin)
break;
if (env.WEB_ENDPOINT && typeof elem.qq === 'number') {
message += `<a href="${helper.generateRichHeaderUrl(pair.apiKey, elem.qq)}">[<i>${helper.htmlEscape(elem.text)}</i>]</a>`;
break;
}
}
case 'face':
case 'sface': {
@ -377,19 +392,16 @@ export default class ForwardService {
else if (files.length) {
messageToSend.file = files;
}
else if (event.message_type === 'group' && (pair.flags | this.instance.flags) & flags.RICH_HEADER) {
else if (event.message_type === 'group' && (pair.flags | this.instance.flags) & flags.RICH_HEADER && env.WEB_ENDPOINT
// 当消息包含链接时不显示 RICH HEADER
&& !isContainsUrl(message)) {
// 没有文件时才能显示链接预览
richHeaderUsed = true;
const url = new URL('https://q2tg-header.clansty.workers.dev');
url.searchParams.set('name', sender);
url.searchParams.set('title', 'title' in event.sender ? event.sender.title : '');
url.searchParams.set('role', 'role' in event.sender ? event.sender.role : '');
url.searchParams.set('id', event.sender.user_id.toString());
// https://github.com/tdlib/td/blob/437c2d0c6e0ad104022d5ad86ddc8aedc41cb7a8/td/telegram/MessageContent.cpp#L2575
// https://github.com/tdlib/td/blob/437c2d0c6e0ad104022d5ad86ddc8aedc41cb7a8/td/generate/scheme/telegram_api.tl#L1841
// https://github.com/gram-js/gramjs/pull/633
messageToSend.file = new Api.InputMediaWebPage({
url: url.toString(),
url: helper.generateRichHeaderUrl(pair.apiKey, event.sender.user_id, messageHeader),
forceSmallMedia: true,
optional: true,
});
@ -457,10 +469,10 @@ export default class ForwardService {
}
public async forwardFromTelegram(message: Api.Message, pair: Pair): Promise<Array<QQMessageSent>> {
// console.log(message);
try {
const tempFiles: FileResult[] = [];
let chain: Sendable = [];
let chain: (string | MessageElem)[] = [];
let markdown: string[] = [], markdownCompatible = true;
const senderId = Number(message.senderId || message.sender?.id);
// 这条消息在 tg 中被回复的时候显示的
let brief = '', isSpoilerPhoto = false;
@ -469,14 +481,37 @@ export default class ForwardService {
// 要是隐私设置了,应该会有这个,然后下面两个都获取不到
(message.fwdFrom?.fromName ||
helper.getUserDisplayName(await message.forward.getChat() || await message.forward.getSender())) :
'') +
': \n';
'');
markdown.push(`![头像 #30px#30px](${helper.generateTelegramAvatarUrl(this.instance.id, senderId)}) **${messageHeader}**`);
messageHeader += ': \n';
if ((pair.flags | this.instance.flags) & flags.COLOR_EMOJI_PREFIX) {
messageHeader = emoji.tgColor((message.sender as Api.User)?.color || message.senderId.toJSNumber()) + messageHeader;
}
const useImage = (image: Buffer, asface: boolean) => {
const md5 = md5Hex(image);
const dimensions = probe.sync(image);
let width = dimensions.width;
let height = dimensions.height;
if (asface) {
width /= 2;
height /= 2;
}
markdown.push(`![image #${width}px#${height}px](${getImageUrlByMd5(md5)})`);
chain.push({
type: 'image',
file: image,
asface,
});
};
const useText = (text: string) => {
markdown.push(markdownEscape(text));
chain.push(text);
};
if (message.photo instanceof Api.Photo ||
// stickers 和以文件发送的图片都是这个
message.document?.mimeType?.startsWith('image/')) {
IMAGE_MIMES.includes(message.document?.mimeType)) {
if ('spoiler' in message.media && message.media.spoiler) {
isSpoilerPhoto = true;
const msgList: Forwardable[] = [{
@ -511,13 +546,10 @@ export default class ForwardService {
></item><source name="Q2TG" icon="" action="" appid="-1" /></msg>`.replaceAll('\n', ''),
});
brief += '[Spoiler 图片]';
markdownCompatible = false;
}
else {
chain.push({
type: 'image',
file: await message.downloadMedia({}),
asface: !!message.sticker,
});
useImage(await message.downloadMedia({}) as Buffer, !!message.sticker);
brief += '[图片]';
}
}
@ -529,28 +561,24 @@ export default class ForwardService {
else if (file.mimeType === 'video/webm' || message.gif) {
// 把 webm 转换成 gif
const convertedPath = await convert.webm2gif(message.document.id.toString(16), () => message.downloadMedia({}));
chain.push({
type: 'image',
file: convertedPath,
asface: true,
});
// markdown 里的 gif 不能动
markdownCompatible = false;
useImage(await fsP.readFile(convertedPath), true);
}
else {
const temp = await createTempFile();
tempFiles.push(temp);
await fsP.writeFile(temp.path, await message.downloadMedia({}));
chain.push(segment.video(temp.path));
markdownCompatible = false;
}
brief += '[视频]';
}
else if (message.sticker) {
// 一定是 tgs
const gifPath = await convert.tgs2gif(message.sticker.id.toString(16), () => message.downloadMedia({}));
chain.push({
type: 'image',
file: gifPath,
asface: true,
});
useImage(await fsP.readFile(gifPath), true);
markdownCompatible = false;
brief += '[贴纸]';
}
else if (message.voice) {
@ -559,6 +587,7 @@ export default class ForwardService {
await fsP.writeFile(temp.path, await message.downloadMedia({}));
const bufSilk = await silk.encode(temp.path);
chain.push(segment.record(bufSilk));
markdownCompatible = false;
if (this.speechClient) {
const pcmPath = await createTempFile({ postfix: '.pcm' });
tempFiles.push(pcmPath);
@ -579,13 +608,14 @@ export default class ForwardService {
}
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}`));
useText(`${poll.multipleChoice ? '多' : '单'}选投票:\n${poll.question}`);
chain.push('\n');
useText(poll.answers.map(answer => ` - ${answer.text}`).join('\n'));
brief += '[投票]';
}
else if (message.contact) {
const contact = message.contact;
chain.push(`名片:\n` +
useText(`名片:\n` +
contact.firstName + (contact.lastName ? ' ' + contact.lastName : '') +
(contact.phoneNumber ? `\n电话${contact.phoneNumber}` : ''));
brief += '[名片]';
@ -595,22 +625,25 @@ export default class ForwardService {
const geo: { lat: number, lng: number } = eviltransform.wgs2gcj(message.venue.geo.lat, message.venue.geo.long);
chain.push(segment.location(geo.lat, geo.lng, `${message.venue.title} (${message.venue.address})`));
brief += `[位置:${message.venue.title}]`;
markdownCompatible = false;
}
else if (message.geo instanceof Api.GeoPoint) {
// 普通的位置,没有名字
const geo: { lat: number, lng: number } = eviltransform.wgs2gcj(message.geo.lat, message.geo.long);
chain.push(segment.location(geo.lat, geo.lng, '选中的位置'));
brief += '[位置]';
markdownCompatible = false;
}
else if (message.media instanceof Api.MessageMediaDocument && message.media.document instanceof Api.Document) {
const file = message.media.document;
const fileNameAttribute =
file.attributes.find(attribute => attribute instanceof Api.DocumentAttributeFilename) as Api.DocumentAttributeFilename;
chain.push(`文件:${fileNameAttribute ? fileNameAttribute.fileName : ''}\n` +
useText(`文件:${fileNameAttribute ? fileNameAttribute.fileName : ''}\n` +
`类型:${file.mimeType}\n` +
`大小:${file.size}`);
if (file.size.leq(50 * 1024 * 1024)) {
chain.push('\n文件正在上传中…');
chain.push('\n');
useText('文件正在上传中…');
if (pair.qq instanceof Group) {
pair.qq.fs.upload(await message.downloadMedia({}), '/',
fileNameAttribute ? fileNameAttribute.fileName : 'file')
@ -647,29 +680,32 @@ export default class ForwardService {
}
chain.push(messageLeft, ...newChain);
brief += message.message;
markdown.push(markdownEscape(message.message));
}
// Q2TG Bot 转发的消息目前不会包含 custom emoji
else if (message.forward?.senderId?.eq?.(this.tgBot.me.id) && /^.*: ?$/.test(message.message.split('\n')[0])) {
// 复读了某一条来自 QQ 的消息 (Repeat as forward)
const originalMessage = message.message.includes('\n') ?
message.message.substring(message.message.indexOf('\n') + 1) : '';
chain.push(originalMessage);
useText(originalMessage);
brief += originalMessage;
messageHeader = helper.getUserDisplayName(message.sender) + ' 转发自 ' +
message.message.substring(0, message.message.indexOf(':')) + ': \n';
}
else {
chain.push(message.message);
useText(message.message);
brief += message.message;
}
}
// 处理回复
let source: Quotable;
if (message.replyToMsgId) {
if (message.replyToMsgId || message.replyTo) {
markdownCompatible = false;
try {
const quote = await db.message.findFirst({
console.log(message.replyTo);
const quote = message.replyToMsgId && await db.message.findFirst({
where: {
tgChatId: Number(pair.tg.id),
tgMsgId: message.replyToMsgId,
@ -678,7 +714,7 @@ export default class ForwardService {
});
if (quote) {
source = {
message: quote.brief || ' ',
message: message.replyTo?.quoteText || quote.brief || ' ',
seq: quote.seq,
rand: Number(quote.rand),
user_id: Number(quote.qqSenderId),
@ -687,7 +723,7 @@ export default class ForwardService {
}
else {
source = {
message: '回复消息找不到',
message: message.replyTo?.quoteText || '回复消息找不到',
seq: 1,
time: Math.floor(new Date().getTime() / 1000),
rand: 1,
@ -720,6 +756,7 @@ export default class ForwardService {
&& chainableElements.length
&& this.instance.workMode
&& pair.instanceMapForTg[senderId]
&& !((pair.flags | this.instance.flags) & flags.DISABLE_SEAMLESS)
) {
try {
const messageSent = await pair.instanceMapForTg[senderId].sendMsg([
@ -747,6 +784,12 @@ export default class ForwardService {
if (this.instance.workMode === 'group' && !isSpoilerPhoto) {
chainableElements.unshift(messageHeader);
if (markdownCompatible && (pair.flags | this.instance.flags) & flags.USE_MARKDOWN) {
chainableElements.push({
type: 'markdown',
content: markdown.join('\n'),
});
}
}
const qqMessages = [] as Array<QQMessageSent>;
if (chainableElements.length) {
@ -757,8 +800,13 @@ export default class ForwardService {
eqq: { type: 'tg', tgUid: senderId, noSplitSender: this.instance.workMode === 'personal', version: 2 },
}, undefined, 0),
});
let messageToSend: Sendable = chainableElements;
if (chainableElements.some(it => typeof it === 'object' && it.type === 'markdown')) {
this.log.debug(chainableElements);
messageToSend = await pair.qq.uploadLongMsg(chainableElements);
}
qqMessages.push({
...await pair.qq.sendMsg(chainableElements, source),
...await pair.qq.sendMsg(messageToSend, source),
brief,
senderId: this.oicq.uin,
});

View File

@ -8,7 +8,7 @@ import { Pair } from '../models/Pair';
import { CustomFile } from 'telegram/client/uploads';
import { getAvatar } from '../utils/urls';
import db from '../models/db';
import { Friend, Group } from 'icqq';
import { Friend, Group } from '@icqqjs/icqq';
import { format } from 'date-and-time';
import ZincSearch from 'zincsearch-node';
import env from '../models/env';
@ -146,4 +146,73 @@ export default class InChatCommandsService {
});
return rpy.join('\n');
}
// 禁言 QQ 成员
public async mute(message: Api.Message, pair: Pair, args: string[]) {
try {
const group = pair.qq as Group;
if(!(group.is_admin||group.is_owner)){
await message.reply({
message: '<i>无管理员权限</i>',
});
return;
}
let target: number;
if (message.replyToMsgId) {
const dbEntry = await db.message.findFirst({
where: {
tgChatId: pair.tgId,
tgMsgId: message.replyToMsgId,
},
});
if (dbEntry) {
target = Number(dbEntry.qqSenderId);
}
}
if (!target) {
await message.reply({
message: '<i>请回复一条消息</i>',
});
return;
}
if (!args.length) {
await message.reply({
message: '<i>请输入禁言的时间</i>',
});
return;
}
let time = Number(args[0]);
if (isNaN(time)) {
const unit = args[0].substring(args[0].length - 1, args[0].length);
time = Number(args[0].substring(0, args[0].length - 1));
switch (unit) {
case 'd':
time *= 24;
case 'h':
time *= 60;
case 'm':
time *= 60;
break;
default:
time = NaN;
}
}
if (isNaN(time)) {
await message.reply({
message: '<i>请输入正确的时间</i>',
});
return;
}
await group.muteMember(target, time);
await message.reply({
message: '<i>成功</i>',
});
}
catch (e) {
await message.reply({
message: `<i>错误</i>\n${e.message}`,
});
}
}
}

View File

@ -1,7 +1,7 @@
import Telegram from '../client/Telegram';
import { getLogger, Logger } from 'log4js';
import { BigInteger } from 'big-integer';
import { Platform } from 'icqq';
import { Platform } from '@icqqjs/icqq';
import { MarkupLike } from 'telegram/define';
import OicqClient from '../client/OicqClient';
import { Button } from 'telegram/tl/custom/button';

View File

@ -1,4 +1,4 @@
import { MessageRet } from 'icqq';
import { MessageRet } from '@icqqjs/icqq';
export type WorkMode = 'group' | 'personal';
export type QQMessageSent = MessageRet & { senderId: number, brief: string };

View File

@ -1,4 +1,4 @@
import { Friend, Group } from 'icqq';
import { Friend, Group } from '@icqqjs/icqq';
export default async function getAboutText(entity: Friend | Group, html: boolean) {
let text: string;

View File

@ -1,5 +1,5 @@
import axios from 'axios';
import { Friend, Group } from 'icqq';
import { Friend, Group } from '@icqqjs/icqq';
export function getAvatarUrl(room: number | bigint | Friend | Group): string {
if (!room) return '';
@ -32,3 +32,7 @@ export async function fetchFile(url: string): Promise<Buffer> {
export function getAvatar(room: number | Friend | Group) {
return fetchFile(getAvatarUrl(room));
}
export function isContainsUrl(msg: string): boolean {
return msg.includes("https://") || msg.includes("http://")
}

View File

@ -1,57 +1,15 @@
{
"name": "q2tg",
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "prisma db push --accept-data-loss --skip-generate && node build/index.js",
"prisma": "prisma",
"import": "ts-node tools/import"
"dev": "pnpm run --stream --parallel dev",
"build": "pnpm run --stream --parallel build"
},
"bin": "build/index.js",
"files": [
"build",
"assets"
],
"devDependencies": {
"@types/cli-progress": "^3.11.5",
"@types/date-and-time": "^3.0.3",
"@types/dockerode": "^3.3.23",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/lodash": "^4.14.202",
"@types/node": "^20.11.17",
"@types/prompts": "^2.4.9",
"tsx": "^4.7.0",
"typescript": "^5.3.3"
"typescript": "^5.4.5"
},
"dependencies": {
"@prisma/client": "5.9.1",
"axios": "^1.6.7",
"baidu-aip-sdk": "^4.16.15",
"big-integer": "^1.6.52",
"cli-progress": "^3.11.2",
"date-and-time": "^3.1.1",
"dockerode": "^4.0.2",
"dotenv": "^16.4.1",
"eviltransform": "^0.2.2",
"file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2",
"icqq": "^0.6.8",
"image-size": "^1.1.1",
"lodash": "^4.17.21",
"log4js": "^6.6.1",
"nodejs-base64": "^2.0.0",
"prisma": "5.9.1",
"prompts": "^2.4.2",
"quote-api": "https://github.com/Clansty/quote-api/archive/014b21138afbbe0e12c91b00561414b1e851fc0f.tar.gz",
"sharp": "^0.33.2",
"silk-sdk": "^0.2.2",
"telegram": "https://github.com/clansty/gramjs/releases/download/2.19.10%2Brevert_media/telegram-2.19.10.tgz",
"tmp-promise": "^3.0.3",
"undici": "^6.4.0",
"zincsearch-node": "^2.1.0",
"zod": "^3.22.4"
},
"engines": {
"node": "^14.13.1 || >=16.0.0"
"pnpm": {
"patchedDependencies": {
"@icqqjs/icqq@1.2.0": "patches/@icqqjs__icqq@1.2.0.patch"
}
}
}

View File

@ -0,0 +1,71 @@
diff --git a/lib/common.d.ts b/lib/common.d.ts
index d27f6298a041607768ee58b0d1e75c8bdcedafe1..d4d90b2ef8b63baf1edb84b0ab3118bbf421c515 100644
--- a/lib/common.d.ts
+++ b/lib/common.d.ts
@@ -54,5 +54,11 @@ export interface UserProfile {
signature: string;
/** 自定义的QID */
QID: string;
+ nickname: string;
+ country: string;
+ province: string;
+ city: string;
+ email: string;
+ birthday: [number, number, number];
}
export * from "./core/constants";
diff --git a/lib/internal/internal.js b/lib/internal/internal.js
index ee137c44c92b947dcc7d851bb04f319c9a070f68..4bb7d5d082156f76974269e220051539162792dd 100644
--- a/lib/internal/internal.js
+++ b/lib/internal/internal.js
@@ -86,9 +86,17 @@ async function getUserProfile(uin = this.uin) {
});
// 有需要自己加!
return {
+ nickname: String(profile[20002]),
+ country: String(profile[20003]),
+ province: String(profile[20004]),
+ city: String(profile[20020]),
+ email: String(profile[20011]),
signature: String(profile[102]),
regTimestamp: profile[20026],
- QID: String(profile[27394])
+ QID: String(profile[27394]),
+ birthday: profile[20031].toBuffer().length === 4 ?
+ [profile[20031].toBuffer().slice(0,2).readUInt16BE(), profile[20031].toBuffer().slice(2,3).readUInt8(), profile[20031].toBuffer().slice(3).readUInt8()] :
+ undefined,
};
}
exports.getUserProfile = getUserProfile;
diff --git a/lib/message/converter.js b/lib/message/converter.js
index 27a659a3290fadd990a1a980918515a6ded4978f..e1bbe1470f302c30e7adea92f433a6e3929064e3 100644
--- a/lib/message/converter.js
+++ b/lib/message/converter.js
@@ -111,7 +111,7 @@ class Converter {
return;
}
if (qq === "all") {
- var q = 0, flag = 1, display = "全体成员";
+ var q = 0, flag = 1, display = text || "全体成员";
}
else {
var q = Number(qq), flag = 0, display = text || String(qq);
@@ -121,7 +121,6 @@ class Converter {
display = member?.card || member?.nickname || display;
}
}
- display = "@" + display;
if (dummy)
return this._text(display);
const buf = Buffer.allocUnsafe(6);
@@ -535,10 +534,6 @@ class Converter {
quote(source) {
const elems = new Converter(source.message || "", this.ext).elems;
const tmp = this.brief;
- if (!this.ext?.dm) {
- this.at({ type: "at", qq: source.user_id });
- this.elems.unshift(this.elems.pop());
- }
this.elems.unshift({
45: {
1: [source.seq],

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,3 @@
packages:
- main
- ui

12
ui/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Q2TG Web UI</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

19
ui/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "q2tg-webui",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev:expose": "vite --host",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@vitejs/plugin-vue-jsx": "^3.1.0",
"naive-ui": "^2.38.2",
"sass": "^1.76.0",
"vite": "^5.2.11",
"vue": "^3.4.26",
"vue-tg": "^0.6.1"
}
}

13
ui/src/App.tsx Normal file
View File

@ -0,0 +1,13 @@
import {defineComponent} from 'vue';
import {dateZhCN, NConfigProvider, zhCN} from 'naive-ui';
import Index from "./Index";
export default defineComponent({
render() {
return (
<NConfigProvider locale={zhCN} dateLocale={dateZhCN}>
<Index/>
</NConfigProvider>
);
},
});

7
ui/src/Index.tsx Normal file
View File

@ -0,0 +1,7 @@
import { defineComponent } from 'vue';
export default defineComponent({
render() {
return <div>nya!</div>;
},
});

1
ui/src/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

5
ui/src/main.ts Normal file
View File

@ -0,0 +1,5 @@
import { createApp } from 'vue';
import App from './App';
createApp(App)
.mount('#app');

32
ui/tsconfig.json Normal file
View File

@ -0,0 +1,32 @@
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": [
"esnext",
"dom"
],
"paths": {
"@": [
"./src"
],
"@/*": [
"./src/*"
]
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx"
]
}

13
ui/vite.config.ts Normal file
View File

@ -0,0 +1,13 @@
import { defineConfig } from 'vite';
import vueJsx from '@vitejs/plugin-vue-jsx';
// https://vitejs.dev/config/
export default defineConfig({
base: '/ui/',
plugins: [vueJsx()],
resolve: {
alias: {
'@': '/src',
},
},
});