mirror of https://github.com/Nofated095/Q2TG.git
Merge branch 'rainbowcat' of github.com:clansty/Q2TG into rainbowcat
This commit is contained in:
commit
c632bc2bb1
|
@ -46,3 +46,5 @@ jobs:
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
secrets: |
|
||||||
|
"npmrc=//npm.pkg.github.com/:_authToken=${{ secrets.GPR_TOKEN }}"
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"github-actions.workflows.pinned.workflows": [
|
||||||
|
".github/workflows/main.yml"
|
||||||
|
]
|
||||||
|
}
|
46
Dockerfile
46
Dockerfile
|
@ -12,23 +12,26 @@ ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
FROM base AS build-env
|
FROM base AS build
|
||||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
apt update && apt-get --no-install-recommends install -y \
|
apt update && apt-get --no-install-recommends install -y \
|
||||||
python3 build-essential pkg-config \
|
python3 build-essential pkg-config \
|
||||||
libpixman-1-dev libcairo2-dev libpango1.0-dev libgif-dev libjpeg62-turbo-dev libpng-dev librsvg2-dev libvips-dev
|
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 \
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store,sharing=locked pnpm install --prod --frozen-lockfile
|
--mount=type=secret,id=npmrc,target=/root/.npmrc \
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
FROM build-env AS build
|
COPY main/src main/tsconfig.json /app/main/
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store,sharing=locked pnpm install --frozen-lockfile
|
COPY main/prisma /app/main/
|
||||||
COPY src tsconfig.json /app/
|
RUN cd main && pnpm exec prisma generate
|
||||||
COPY prisma /app/
|
RUN cd main && pnpm run build
|
||||||
RUN pnpm exec prisma generate
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store,sharing=locked \
|
||||||
RUN pnpm run build
|
--mount=type=secret,id=npmrc,target=/root/.npmrc \
|
||||||
|
pnpm deploy --filter=q2tg-main --prod deploy
|
||||||
|
|
||||||
FROM debian:bookworm-slim AS tgs-to-gif-build
|
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
|
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 sed -i 's/\${CONAN_LIBS}/z/g' CMakeLists.txt
|
||||||
RUN cmake CMakeLists.txt && make
|
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
|
FROM base
|
||||||
COPY assets /app/
|
|
||||||
|
|
||||||
COPY --from=tgs-to-gif-build /app/bin/tgs_to_gif /usr/local/bin/tgs_to_gif
|
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
|
ENV TGS_TO_GIF=/usr/local/bin/tgs_to_gif
|
||||||
|
|
||||||
COPY package.json pnpm-lock.yaml /app/
|
COPY main/assets /app/assets
|
||||||
COPY --from=prod-deps /app/node_modules /app/node_modules
|
|
||||||
COPY prisma /app/
|
COPY --from=build /app/deploy /app
|
||||||
|
COPY main/prisma /app/
|
||||||
RUN pnpm exec prisma generate
|
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
|
ENV DATA_DIR=/app/data
|
||||||
|
EXPOSE 8080
|
||||||
CMD pnpm start
|
CMD pnpm start
|
||||||
|
|
|
@ -43,6 +43,9 @@ services:
|
||||||
- postgres
|
- postgres
|
||||||
- zinclabs
|
- zinclabs
|
||||||
- sign
|
- sign
|
||||||
|
ports:
|
||||||
|
# 如果要使用 RICH_HEADER 需要将端口发布到公网
|
||||||
|
- 8080:8080
|
||||||
volumes:
|
volumes:
|
||||||
- q2tg:/app/data
|
- q2tg:/app/data
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
@ -65,6 +68,8 @@ services:
|
||||||
- BAIDU_APP_ID=
|
- BAIDU_APP_ID=
|
||||||
- BAIDU_API_KEY=
|
- BAIDU_API_KEY=
|
||||||
- BAIDU_SECRET_KEY=
|
- BAIDU_SECRET_KEY=
|
||||||
|
# 如果你需要使用 /flags set RICH_HEADER 来显示头像,则需将 q2tg 8080 端口发布到公网,可以使用 cloudflare tunnel
|
||||||
|
#- WEB_ENDPOINT=https://yourichheader.com 填写你发布到公网的域名
|
||||||
# 如果需要通过代理联网,那么设置下面两个变量
|
# 如果需要通过代理联网,那么设置下面两个变量
|
||||||
#- PROXY_IP=
|
#- PROXY_IP=
|
||||||
#- PROXY_PORT=
|
#- PROXY_PORT=
|
||||||
|
|
|
@ -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>
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -90,6 +90,7 @@ model ForwardPair {
|
||||||
instanceId Int @default(0)
|
instanceId Int @default(0)
|
||||||
instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||||
flags Int @default(0)
|
flags Int @default(0)
|
||||||
|
apiKey String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
|
|
||||||
@@unique([qqRoomId, instanceId])
|
@@unique([qqRoomId, instanceId])
|
||||||
@@unique([tgChatId, instanceId])
|
@@unique([tgChatId, instanceId])
|
|
@ -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);
|
||||||
|
},
|
||||||
|
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -4,22 +4,19 @@ import {
|
||||||
Friend,
|
Friend,
|
||||||
Group,
|
Group,
|
||||||
GroupMessageEvent,
|
GroupMessageEvent,
|
||||||
LogLevel,
|
LogLevel, Platform, PrivateMessage,
|
||||||
Platform, PrivateMessage,
|
PrivateMessageEvent,
|
||||||
PrivateMessageEvent, XmlElem,
|
} from '@icqqjs/icqq';
|
||||||
} from 'icqq';
|
|
||||||
import Buffer from 'buffer';
|
|
||||||
import { execSync } from 'child_process';
|
|
||||||
import random from '../utils/random';
|
import random from '../utils/random';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import fsP from 'fs/promises';
|
import fsP from 'fs/promises';
|
||||||
import { Config } from 'icqq/lib/client';
|
import { Config } from '@icqqjs/icqq/lib/client';
|
||||||
import dataPath from '../helpers/dataPath';
|
import dataPath from '../helpers/dataPath';
|
||||||
import os from 'os';
|
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 { randomBytes } from 'crypto';
|
||||||
import { escapeXml, gzip, timestamp } from 'icqq/lib/common';
|
import { gzip, timestamp } from '@icqqjs/icqq/lib/common';
|
||||||
import { pb } from 'icqq/lib/core';
|
import { pb } from '@icqqjs/icqq/lib/core';
|
||||||
import env from '../models/env';
|
import env from '../models/env';
|
||||||
|
|
||||||
const LOG_LEVEL: LogLevel = env.OICQ_LOG_LEVEL;
|
const LOG_LEVEL: LogLevel = env.OICQ_LOG_LEVEL;
|
|
@ -12,7 +12,7 @@ import TelegramChat from './TelegramChat';
|
||||||
import TelegramSession from '../models/TelegramSession';
|
import TelegramSession from '../models/TelegramSession';
|
||||||
import { LogLevel } from 'telegram/extensions/Logger';
|
import { LogLevel } from 'telegram/extensions/Logger';
|
||||||
import { BigInteger } from 'big-integer';
|
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 { PromisedNetSockets, PromisedWebSockets } from 'telegram/extensions';
|
||||||
import { ConnectionTCPFull, ConnectionTCPObfuscated } from 'telegram/network';
|
import { ConnectionTCPFull, ConnectionTCPObfuscated } from 'telegram/network';
|
||||||
import env from '../models/env';
|
import env from '../models/env';
|
||||||
|
@ -62,7 +62,7 @@ export default class Telegram {
|
||||||
connection: env.TG_CONNECTION === 'websocket' ? ConnectionTCPObfuscated : ConnectionTCPFull,
|
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') {
|
public static async create(startArgs: UserAuthParams | BotAuthParams, appName = 'Q2TG') {
|
|
@ -28,6 +28,10 @@ const personalPrivateCommands = [
|
||||||
command: 'login',
|
command: 'login',
|
||||||
description: '当 QQ 处于下线状态时,使用此命令重新登录 QQ',
|
description: '当 QQ 处于下线状态时,使用此命令重新登录 QQ',
|
||||||
}),
|
}),
|
||||||
|
new Api.BotCommand({
|
||||||
|
command: 'flags',
|
||||||
|
description: 'WARNING: EXPERIMENTAL FEATURES AHEAD!',
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 服务器零号实例的管理员
|
// 服务器零号实例的管理员
|
||||||
|
@ -57,6 +61,10 @@ const inChatCommands = [
|
||||||
command: 'search',
|
command: 'search',
|
||||||
description: '搜索消息',
|
description: '搜索消息',
|
||||||
}),
|
}),
|
||||||
|
new Api.BotCommand({
|
||||||
|
command: 'q',
|
||||||
|
description: '生成 QuotLy 图片',
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const groupInChatCommands = [
|
const groupInChatCommands = [
|
||||||
|
@ -93,6 +101,10 @@ const personalInChatCommands = [
|
||||||
command: 'nick',
|
command: 'nick',
|
||||||
description: '获取/设置群名片',
|
description: '获取/设置群名片',
|
||||||
}),
|
}),
|
||||||
|
new Api.BotCommand({
|
||||||
|
command: 'mute',
|
||||||
|
description: '设置 QQ 成员禁言',
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
|
@ -9,6 +9,10 @@ enum flags {
|
||||||
RICH_HEADER = 1 << 7,
|
RICH_HEADER = 1 << 7,
|
||||||
NO_QUOTE_PIN = 1 << 8,
|
NO_QUOTE_PIN = 1 << 8,
|
||||||
NO_FORWARD_OTHER_BOT = 1 << 9,
|
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;
|
export default flags;
|
|
@ -19,11 +19,11 @@ export default class AliveCheckController {
|
||||||
}
|
}
|
||||||
|
|
||||||
await message.reply({
|
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) => {
|
const boolToStr = (value: boolean) => {
|
||||||
return value ? '好' : '坏';
|
return value ? '好' : '坏';
|
||||||
};
|
};
|
||||||
|
@ -32,7 +32,6 @@ export default class AliveCheckController {
|
||||||
for (const instance of instances) {
|
for (const instance of instances) {
|
||||||
const oicq = instance.oicq;
|
const oicq = instance.oicq;
|
||||||
const tgBot = instance.tgBot;
|
const tgBot = instance.tgBot;
|
||||||
}
|
|
||||||
|
|
||||||
return messageParts.join('\n\n');
|
return messageParts.join('\n\n');
|
||||||
};
|
};
|
|
@ -9,7 +9,7 @@ import {
|
||||||
MemberDecreaseEvent,
|
MemberDecreaseEvent,
|
||||||
MemberIncreaseEvent,
|
MemberIncreaseEvent,
|
||||||
PrivateMessageEvent,
|
PrivateMessageEvent,
|
||||||
} from 'icqq';
|
} from '@icqqjs/icqq';
|
||||||
import Instance from '../models/Instance';
|
import Instance from '../models/Instance';
|
||||||
import { getLogger, Logger } from 'log4js';
|
import { getLogger, Logger } from 'log4js';
|
||||||
import { editFlags } from '../utils/flagControl';
|
import { editFlags } from '../utils/flagControl';
|
|
@ -2,7 +2,8 @@ import DeleteMessageService from '../services/DeleteMessageService';
|
||||||
import Telegram from '../client/Telegram';
|
import Telegram from '../client/Telegram';
|
||||||
import OicqClient from '../client/OicqClient';
|
import OicqClient from '../client/OicqClient';
|
||||||
import { Api } from 'telegram';
|
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';
|
import Instance from '../models/Instance';
|
||||||
|
|
||||||
export default class DeleteMessageController {
|
export default class DeleteMessageController {
|
|
@ -8,7 +8,7 @@ import {
|
||||||
GroupPokeEvent,
|
GroupPokeEvent,
|
||||||
MemberIncreaseEvent,
|
MemberIncreaseEvent,
|
||||||
PrivateMessageEvent,
|
PrivateMessageEvent,
|
||||||
} from 'icqq';
|
} from '@icqqjs/icqq';
|
||||||
import db from '../models/db';
|
import db from '../models/db';
|
||||||
import { Api } from 'telegram';
|
import { Api } from 'telegram';
|
||||||
import { getLogger, Logger } from 'log4js';
|
import { getLogger, Logger } from 'log4js';
|
||||||
|
@ -93,6 +93,7 @@ export default class ForwardController {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onTelegramUserMessage = async (message: Api.Message) => {
|
private onTelegramUserMessage = async (message: Api.Message) => {
|
||||||
|
if (!message.sender) return;
|
||||||
if (!('bot' in message.sender) || !message.sender.bot) return;
|
if (!('bot' in message.sender) || !message.sender.bot) return;
|
||||||
const pair = this.instance.forwardPairs.find(message.chat);
|
const pair = this.instance.forwardPairs.find(message.chat);
|
||||||
if (!pair) return;
|
if (!pair) return;
|
||||||
|
@ -176,6 +177,7 @@ export default class ForwardController {
|
||||||
private onQqPoke = async (event: FriendPokeEvent | GroupPokeEvent) => {
|
private onQqPoke = async (event: FriendPokeEvent | GroupPokeEvent) => {
|
||||||
const target = event.notice_type === 'friend' ? event.friend : event.group;
|
const target = event.notice_type === 'friend' ? event.friend : event.group;
|
||||||
const pair = this.instance.forwardPairs.find(target);
|
const pair = this.instance.forwardPairs.find(target);
|
||||||
|
if (!pair) return;
|
||||||
if ((pair?.flags | this.instance.flags) & flags.DISABLE_POKE) return;
|
if ((pair?.flags | this.instance.flags) & flags.DISABLE_POKE) return;
|
||||||
let operatorName: string, targetName: string;
|
let operatorName: string, targetName: string;
|
||||||
if (target instanceof Friend) {
|
if (target instanceof Friend) {
|
|
@ -1,13 +1,14 @@
|
||||||
import Instance from '../models/Instance';
|
import Instance from '../models/Instance';
|
||||||
import Telegram from '../client/Telegram';
|
import Telegram from '../client/Telegram';
|
||||||
import OicqClient from '../client/OicqClient';
|
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 { Pair } from '../models/Pair';
|
||||||
import { Api } from 'telegram';
|
import { Api } from 'telegram';
|
||||||
import db from '../models/db';
|
import db from '../models/db';
|
||||||
import BigInteger from 'big-integer';
|
import BigInteger from 'big-integer';
|
||||||
import helper from '../helpers/forwardHelper';
|
import helper from '../helpers/forwardHelper';
|
||||||
import { getLogger, Logger } from 'log4js';
|
import { getLogger, Logger } from 'log4js';
|
||||||
|
import flags from '../constants/flags';
|
||||||
|
|
||||||
type ActionSubjectTg = {
|
type ActionSubjectTg = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -40,6 +41,7 @@ export default class {
|
||||||
if (event.message_type !== 'group') return;
|
if (event.message_type !== 'group') return;
|
||||||
const pair = this.instance.forwardPairs.find(event.group);
|
const pair = this.instance.forwardPairs.find(event.group);
|
||||||
if (!pair) return;
|
if (!pair) return;
|
||||||
|
if ((pair.flags | this.instance.flags) & flags.DISABLE_SLASH_COMMAND) return;
|
||||||
const chain = [...event.message];
|
const chain = [...event.message];
|
||||||
while (chain.length && chain[0].type !== 'text') {
|
while (chain.length && chain[0].type !== 'text') {
|
||||||
chain.shift();
|
chain.shift();
|
||||||
|
@ -108,6 +110,7 @@ export default class {
|
||||||
private onTelegramMessage = async (message: Api.Message) => {
|
private onTelegramMessage = async (message: Api.Message) => {
|
||||||
const pair = this.instance.forwardPairs.find(message.chat);
|
const pair = this.instance.forwardPairs.find(message.chat);
|
||||||
if (!pair) return;
|
if (!pair) return;
|
||||||
|
if ((pair.flags | this.instance.flags) & flags.DISABLE_SLASH_COMMAND) return;
|
||||||
const exec = COMMAND_REGEX.exec(message.message);
|
const exec = COMMAND_REGEX.exec(message.message);
|
||||||
if (!exec) return;
|
if (!exec) return;
|
||||||
const action = exec[2] || exec[3];
|
const action = exec[2] || exec[3];
|
|
@ -4,7 +4,7 @@ import Instance from '../models/Instance';
|
||||||
import Telegram from '../client/Telegram';
|
import Telegram from '../client/Telegram';
|
||||||
import OicqClient from '../client/OicqClient';
|
import OicqClient from '../client/OicqClient';
|
||||||
import { Api } from 'telegram';
|
import { Api } from 'telegram';
|
||||||
import { Group } from 'icqq';
|
import { Group } from '@icqqjs/icqq';
|
||||||
import RecoverMessageHelper from '../helpers/RecoverMessageHelper';
|
import RecoverMessageHelper from '../helpers/RecoverMessageHelper';
|
||||||
import flags from '../constants/flags';
|
import flags from '../constants/flags';
|
||||||
import { editFlags } from '../utils/flagControl';
|
import { editFlags } from '../utils/flagControl';
|
||||||
|
@ -43,6 +43,13 @@ export default class InChatCommandsController {
|
||||||
case '/poke':
|
case '/poke':
|
||||||
await this.service.poke(message, pair);
|
await this.service.poke(message, pair);
|
||||||
return true;
|
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':
|
case '/forwardoff':
|
||||||
pair.flags |= flags.DISABLE_Q2TG | flags.DISABLE_TG2Q;
|
pair.flags |= flags.DISABLE_Q2TG | flags.DISABLE_TG2Q;
|
||||||
await message.reply({ message: '转发已禁用' });
|
await message.reply({ message: '转发已禁用' });
|
||||||
|
@ -84,7 +91,7 @@ export default class InChatCommandsController {
|
||||||
return true;
|
return true;
|
||||||
case '/nick':
|
case '/nick':
|
||||||
if (this.instance.workMode !== 'personal' || !message.senderId?.eq(this.instance.owner)) return false;
|
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) {
|
if (!params) {
|
||||||
await message.reply({
|
await message.reply({
|
||||||
message: `群名片:<i>${pair.qq.pickMember(this.instance.qqUin, true).card}</i>`,
|
message: `群名片:<i>${pair.qq.pickMember(this.instance.qqUin, true).card}</i>`,
|
|
@ -1,7 +1,7 @@
|
||||||
import Instance from '../models/Instance';
|
import Instance from '../models/Instance';
|
||||||
import Telegram from '../client/Telegram';
|
import Telegram from '../client/Telegram';
|
||||||
import OicqClient from '../client/OicqClient';
|
import OicqClient from '../client/OicqClient';
|
||||||
import { GroupMessageEvent, MiraiElem, PrivateMessageEvent } from 'icqq';
|
import { GroupMessageEvent, MiraiElem, PrivateMessageEvent } from '@icqqjs/icqq';
|
||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
constructor(private readonly instance: Instance,
|
constructor(private readonly instance: Instance,
|
|
@ -2,7 +2,7 @@ import Instance from '../models/Instance';
|
||||||
import Telegram from '../client/Telegram';
|
import Telegram from '../client/Telegram';
|
||||||
import OicqClient from '../client/OicqClient';
|
import OicqClient from '../client/OicqClient';
|
||||||
import { getLogger, Logger } from 'log4js';
|
import { getLogger, Logger } from 'log4js';
|
||||||
import { Group, GroupMessageEvent, PrivateMessageEvent } from 'icqq';
|
import { Group, GroupMessageEvent, PrivateMessageEvent } from '@icqqjs/icqq';
|
||||||
import { Api } from 'telegram';
|
import { Api } from 'telegram';
|
||||||
import quotly from 'quote-api/methods/generate.js';
|
import quotly from 'quote-api/methods/generate.js';
|
||||||
import { CustomFile } from 'telegram/client/uploads';
|
import { CustomFile } from 'telegram/client/uploads';
|
|
@ -2,7 +2,7 @@ import { getLogger, Logger } from 'log4js';
|
||||||
import Instance from '../models/Instance';
|
import Instance from '../models/Instance';
|
||||||
import Telegram from '../client/Telegram';
|
import Telegram from '../client/Telegram';
|
||||||
import OicqClient from '../client/OicqClient';
|
import OicqClient from '../client/OicqClient';
|
||||||
import { FriendRequestEvent, GroupInviteEvent } from 'icqq';
|
import { FriendRequestEvent, GroupInviteEvent } from '@icqqjs/icqq';
|
||||||
import { getAvatar } from '../utils/urls';
|
import { getAvatar } from '../utils/urls';
|
||||||
import { CustomFile } from 'telegram/client/uploads';
|
import { CustomFile } from 'telegram/client/uploads';
|
||||||
import { Button } from 'telegram/tl/custom/button';
|
import { Button } from 'telegram/tl/custom/button';
|
|
@ -3,7 +3,7 @@ import Telegram from '../client/Telegram';
|
||||||
import OicqClient from '../client/OicqClient';
|
import OicqClient from '../client/OicqClient';
|
||||||
import { Pair } from '../models/Pair';
|
import { Pair } from '../models/Pair';
|
||||||
import { Api } from 'telegram';
|
import { Api } from 'telegram';
|
||||||
import { GroupMessage, PrivateMessage } from 'icqq';
|
import { GroupMessage, PrivateMessage } from '@icqqjs/icqq';
|
||||||
import db from '../models/db';
|
import db from '../models/db';
|
||||||
import { format } from 'date-and-time';
|
import { format } from 'date-and-time';
|
||||||
import lottie from '../constants/lottie';
|
import lottie from '../constants/lottie';
|
|
@ -3,9 +3,11 @@ import { CustomFile } from 'telegram/client/uploads';
|
||||||
import { base64decode } from 'nodejs-base64';
|
import { base64decode } from 'nodejs-base64';
|
||||||
import { getLogger } from 'log4js';
|
import { getLogger } from 'log4js';
|
||||||
import { Entity } from 'telegram/define';
|
import { Entity } from 'telegram/define';
|
||||||
import { ForwardMessage } from 'icqq';
|
import { ForwardMessage } from '@icqqjs/icqq';
|
||||||
import { Api } from 'telegram';
|
import { Api } from 'telegram';
|
||||||
import { imageSize } from 'image-size';
|
import { imageSize } from 'image-size';
|
||||||
|
import env from '../models/env';
|
||||||
|
import { md5Hex } from '../utils/hashing';
|
||||||
|
|
||||||
const log = getLogger('ForwardHelper');
|
const log = getLogger('ForwardHelper');
|
||||||
|
|
||||||
|
@ -30,7 +32,7 @@ export default {
|
||||||
|| dimensions.width + dimensions.height > 10000
|
|| dimensions.width + dimensions.height > 10000
|
||||||
) {
|
) {
|
||||||
// 让 Telegram 服务器下载
|
// 让 Telegram 服务器下载
|
||||||
return url
|
return url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (allowWebp) {
|
if (allowWebp) {
|
||||||
|
@ -192,4 +194,17 @@ export default {
|
||||||
}
|
}
|
||||||
return null;
|
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();
|
||||||
|
},
|
||||||
};
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import { Platform } from 'icqq';
|
import { Platform } from '@icqqjs/icqq';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
convertTextToPlatform(text: string): Platform {
|
convertTextToPlatform(text: string): Platform {
|
|
@ -1,6 +1,8 @@
|
||||||
import { configure, getLogger } from 'log4js';
|
import { configure, getLogger } from 'log4js';
|
||||||
import Instance from './models/Instance';
|
import Instance from './models/Instance';
|
||||||
import db from './models/db';
|
import db from './models/db';
|
||||||
|
import api from './api';
|
||||||
|
import env from './models/env';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
configure({
|
configure({
|
||||||
|
@ -8,7 +10,7 @@ import db from './models/db';
|
||||||
console: { type: 'console' },
|
console: { type: 'console' },
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
default: { level: 'debug', appenders: ['console'] },
|
default: { level: env.LOG_LEVEL, appenders: ['console'] },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const log = getLogger('Main');
|
const log = getLogger('Main');
|
||||||
|
@ -20,6 +22,9 @@ import db from './models/db';
|
||||||
process.on('unhandledRejection', error => {
|
process.on('unhandledRejection', error => {
|
||||||
log.error('UnhandledException: ', error);
|
log.error('UnhandledException: ', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await api.startListening();
|
||||||
|
|
||||||
const instanceEntries = await db.instance.findMany();
|
const instanceEntries = await db.instance.findMany();
|
||||||
|
|
||||||
if (!instanceEntries.length) {
|
if (!instanceEntries.length) {
|
|
@ -1,4 +1,4 @@
|
||||||
import { Friend, Group } from 'icqq';
|
import { Friend, Group } from '@icqqjs/icqq';
|
||||||
import TelegramChat from '../client/TelegramChat';
|
import TelegramChat from '../client/TelegramChat';
|
||||||
import OicqClient from '../client/OicqClient';
|
import OicqClient from '../client/OicqClient';
|
||||||
import Telegram from '../client/Telegram';
|
import Telegram from '../client/Telegram';
|
||||||
|
@ -28,7 +28,7 @@ export default class ForwardPairs {
|
||||||
const tg = await tgBot.getChat(Number(i.tgChatId));
|
const tg = await tgBot.getChat(Number(i.tgChatId));
|
||||||
const tgUserChat = await tgUser.getChat(Number(i.tgChatId));
|
const tgUserChat = await tgUser.getChat(Number(i.tgChatId));
|
||||||
if (qq && tg && tgUserChat) {
|
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) {
|
catch (e) {
|
||||||
|
@ -51,7 +51,7 @@ export default class ForwardPairs {
|
||||||
instanceId: this.instanceId,
|
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;
|
return dbEntry;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getLogger } from 'log4js';
|
import { getLogger } from 'log4js';
|
||||||
import { Friend, Group } from 'icqq';
|
import { Friend, Group } from '@icqqjs/icqq';
|
||||||
import TelegramChat from '../client/TelegramChat';
|
import TelegramChat from '../client/TelegramChat';
|
||||||
import getAboutText from '../utils/getAboutText';
|
import getAboutText from '../utils/getAboutText';
|
||||||
import { md5 } from '../utils/hashing';
|
import { md5 } from '../utils/hashing';
|
||||||
|
@ -9,6 +9,12 @@ import db from './db';
|
||||||
const log = getLogger('ForwardPair');
|
const log = getLogger('ForwardPair');
|
||||||
|
|
||||||
export class Pair {
|
export class Pair {
|
||||||
|
private static readonly apiKeyMap = new Map<string, Pair>();
|
||||||
|
|
||||||
|
public static getByApiKey(key: string) {
|
||||||
|
return this.apiKeyMap.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
// 群成员的 tg 账号对应它对应的 QQ 账号获取到的 Group 对象
|
// 群成员的 tg 账号对应它对应的 QQ 账号获取到的 Group 对象
|
||||||
// 只有群组模式有效
|
// 只有群组模式有效
|
||||||
// public readonly instanceMapForTg = {} as { [tgUserId: string]: Group };
|
// public readonly instanceMapForTg = {} as { [tgUserId: string]: Group };
|
||||||
|
@ -19,7 +25,11 @@ export class Pair {
|
||||||
// public readonly tgUser: TelegramChat,
|
// public readonly tgUser: TelegramChat,
|
||||||
public dbId: number,
|
public dbId: number,
|
||||||
private _flags: number,
|
private _flags: number,
|
||||||
|
public readonly apiKey: string,
|
||||||
) {
|
) {
|
||||||
|
if (apiKey) {
|
||||||
|
Pair.apiKeyMap.set(apiKey, this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新 TG 群组的头像和简介
|
// 更新 TG 群组的头像和简介
|
|
@ -3,7 +3,9 @@ import path from 'path';
|
||||||
|
|
||||||
const configParsed = z.object({
|
const configParsed = z.object({
|
||||||
DATA_DIR: z.string().default(path.resolve('./data')),
|
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'),
|
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(),
|
FFMPEG_PATH: z.string().optional(),
|
||||||
FFPROBE_PATH: z.string().optional(),
|
FFPROBE_PATH: z.string().optional(),
|
||||||
SIGN_API: z.string().url().optional(),
|
SIGN_API: z.string().url().optional(),
|
||||||
|
@ -29,6 +31,10 @@ const configParsed = z.object({
|
||||||
BAIDU_API_KEY: z.string().optional(),
|
BAIDU_API_KEY: z.string().optional(),
|
||||||
BAIDU_SECRET_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'),
|
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);
|
}).safeParse(process.env);
|
||||||
|
|
||||||
if (!configParsed.success) {
|
if (!configParsed.success) {
|
|
@ -1,5 +1,5 @@
|
||||||
import Telegram from '../client/Telegram';
|
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 { Button } from 'telegram/tl/custom/button';
|
||||||
import { getLogger, Logger } from 'log4js';
|
import { getLogger, Logger } from 'log4js';
|
||||||
import { getAvatar } from '../utils/urls';
|
import { getAvatar } from '../utils/urls';
|
|
@ -2,7 +2,7 @@ import Telegram from '../client/Telegram';
|
||||||
import { getLogger, Logger } from 'log4js';
|
import { getLogger, Logger } from 'log4js';
|
||||||
import { Api } from 'telegram';
|
import { Api } from 'telegram';
|
||||||
import db from '../models/db';
|
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 Instance from '../models/Instance';
|
||||||
import { Pair } from '../models/Pair';
|
import { Pair } from '../models/Pair';
|
||||||
import { consumer } from '../utils/highLevelFunces';
|
import { consumer } from '../utils/highLevelFunces';
|
|
@ -10,8 +10,8 @@ import {
|
||||||
Quotable,
|
Quotable,
|
||||||
segment,
|
segment,
|
||||||
Sendable,
|
Sendable,
|
||||||
} from 'icqq';
|
} from '@icqqjs/icqq';
|
||||||
import { fetchFile, getBigFaceUrl, getImageUrlByMd5 } from '../utils/urls';
|
import { fetchFile, getBigFaceUrl, getImageUrlByMd5, isContainsUrl } from '../utils/urls';
|
||||||
import { ButtonLike, FileLike } from 'telegram/define';
|
import { ButtonLike, FileLike } from 'telegram/define';
|
||||||
import { getLogger, Logger } from 'log4js';
|
import { getLogger, Logger } from 'log4js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
@ -26,7 +26,7 @@ import fsP from 'fs/promises';
|
||||||
import eviltransform from 'eviltransform';
|
import eviltransform from 'eviltransform';
|
||||||
import silk from '../encoding/silk';
|
import silk from '../encoding/silk';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { md5Hex } from '../utils/hashing';
|
import { md5B64, md5Hex } from '../utils/hashing';
|
||||||
import Instance from '../models/Instance';
|
import Instance from '../models/Instance';
|
||||||
import { Pair } from '../models/Pair';
|
import { Pair } from '../models/Pair';
|
||||||
import OicqClient from '../client/OicqClient';
|
import OicqClient from '../client/OicqClient';
|
||||||
|
@ -38,15 +38,19 @@ import { QQMessageSent } from '../types/definitions';
|
||||||
import ZincSearch from 'zincsearch-node';
|
import ZincSearch from 'zincsearch-node';
|
||||||
import { speech as AipSpeechClient } from 'baidu-aip-sdk';
|
import { speech as AipSpeechClient } from 'baidu-aip-sdk';
|
||||||
import random from '../utils/random';
|
import random from '../utils/random';
|
||||||
import { escapeXml } from 'icqq/lib/common';
|
import { escapeXml } from '@icqqjs/icqq/lib/common';
|
||||||
import Docker from 'dockerode';
|
import Docker from 'dockerode';
|
||||||
import ReplyKeyboardHide = Api.ReplyKeyboardHide;
|
import ReplyKeyboardHide = Api.ReplyKeyboardHide;
|
||||||
import env from '../models/env';
|
import env from '../models/env';
|
||||||
import { CustomFile } from 'telegram/client/uploads';
|
import { CustomFile } from 'telegram/client/uploads';
|
||||||
import flags from '../constants/flags';
|
import flags from '../constants/flags';
|
||||||
import BigInteger from 'big-integer';
|
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 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
|
// noinspection FallThroughInSwitchStatementJS
|
||||||
export default class ForwardService {
|
export default class ForwardService {
|
||||||
|
@ -144,7 +148,14 @@ export default class ForwardService {
|
||||||
message = '[<i>转发多条消息(未配置)</i>]';
|
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;
|
let url: string;
|
||||||
switch (elem.type) {
|
switch (elem.type) {
|
||||||
case 'text': {
|
case 'text': {
|
||||||
|
@ -169,6 +180,10 @@ export default class ForwardService {
|
||||||
case 'at': {
|
case 'at': {
|
||||||
if (event.source?.user_id === elem.qq || event.source?.user_id === this.oicq.uin)
|
if (event.source?.user_id === elem.qq || event.source?.user_id === this.oicq.uin)
|
||||||
break;
|
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 'face':
|
||||||
case 'sface': {
|
case 'sface': {
|
||||||
|
@ -377,19 +392,16 @@ export default class ForwardService {
|
||||||
else if (files.length) {
|
else if (files.length) {
|
||||||
messageToSend.file = files;
|
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;
|
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/telegram/MessageContent.cpp#L2575
|
||||||
// https://github.com/tdlib/td/blob/437c2d0c6e0ad104022d5ad86ddc8aedc41cb7a8/td/generate/scheme/telegram_api.tl#L1841
|
// https://github.com/tdlib/td/blob/437c2d0c6e0ad104022d5ad86ddc8aedc41cb7a8/td/generate/scheme/telegram_api.tl#L1841
|
||||||
// https://github.com/gram-js/gramjs/pull/633
|
// https://github.com/gram-js/gramjs/pull/633
|
||||||
messageToSend.file = new Api.InputMediaWebPage({
|
messageToSend.file = new Api.InputMediaWebPage({
|
||||||
url: url.toString(),
|
url: helper.generateRichHeaderUrl(pair.apiKey, event.sender.user_id, messageHeader),
|
||||||
forceSmallMedia: true,
|
forceSmallMedia: true,
|
||||||
optional: true,
|
optional: true,
|
||||||
});
|
});
|
||||||
|
@ -457,10 +469,10 @@ export default class ForwardService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async forwardFromTelegram(message: Api.Message, pair: Pair): Promise<Array<QQMessageSent>> {
|
public async forwardFromTelegram(message: Api.Message, pair: Pair): Promise<Array<QQMessageSent>> {
|
||||||
// console.log(message);
|
|
||||||
try {
|
try {
|
||||||
const tempFiles: FileResult[] = [];
|
const tempFiles: FileResult[] = [];
|
||||||
let chain: Sendable = [];
|
let chain: (string | MessageElem)[] = [];
|
||||||
|
let markdown: string[] = [], markdownCompatible = true;
|
||||||
const senderId = Number(message.senderId || message.sender?.id);
|
const senderId = Number(message.senderId || message.sender?.id);
|
||||||
// 这条消息在 tg 中被回复的时候显示的
|
// 这条消息在 tg 中被回复的时候显示的
|
||||||
let brief = '', isSpoilerPhoto = false;
|
let brief = '', isSpoilerPhoto = false;
|
||||||
|
@ -469,14 +481,37 @@ export default class ForwardService {
|
||||||
// 要是隐私设置了,应该会有这个,然后下面两个都获取不到
|
// 要是隐私设置了,应该会有这个,然后下面两个都获取不到
|
||||||
(message.fwdFrom?.fromName ||
|
(message.fwdFrom?.fromName ||
|
||||||
helper.getUserDisplayName(await message.forward.getChat() || await message.forward.getSender())) :
|
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) {
|
if ((pair.flags | this.instance.flags) & flags.COLOR_EMOJI_PREFIX) {
|
||||||
messageHeader = emoji.tgColor((message.sender as Api.User)?.color || message.senderId.toJSNumber()) + messageHeader;
|
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 ||
|
if (message.photo instanceof Api.Photo ||
|
||||||
// stickers 和以文件发送的图片都是这个
|
// stickers 和以文件发送的图片都是这个
|
||||||
message.document?.mimeType?.startsWith('image/')) {
|
IMAGE_MIMES.includes(message.document?.mimeType)) {
|
||||||
if ('spoiler' in message.media && message.media.spoiler) {
|
if ('spoiler' in message.media && message.media.spoiler) {
|
||||||
isSpoilerPhoto = true;
|
isSpoilerPhoto = true;
|
||||||
const msgList: Forwardable[] = [{
|
const msgList: Forwardable[] = [{
|
||||||
|
@ -511,13 +546,10 @@ export default class ForwardService {
|
||||||
></item><source name="Q2TG" icon="" action="" appid="-1" /></msg>`.replaceAll('\n', ''),
|
></item><source name="Q2TG" icon="" action="" appid="-1" /></msg>`.replaceAll('\n', ''),
|
||||||
});
|
});
|
||||||
brief += '[Spoiler 图片]';
|
brief += '[Spoiler 图片]';
|
||||||
|
markdownCompatible = false;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
chain.push({
|
useImage(await message.downloadMedia({}) as Buffer, !!message.sticker);
|
||||||
type: 'image',
|
|
||||||
file: await message.downloadMedia({}),
|
|
||||||
asface: !!message.sticker,
|
|
||||||
});
|
|
||||||
brief += '[图片]';
|
brief += '[图片]';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -529,28 +561,24 @@ export default class ForwardService {
|
||||||
else if (file.mimeType === 'video/webm' || message.gif) {
|
else if (file.mimeType === 'video/webm' || message.gif) {
|
||||||
// 把 webm 转换成 gif
|
// 把 webm 转换成 gif
|
||||||
const convertedPath = await convert.webm2gif(message.document.id.toString(16), () => message.downloadMedia({}));
|
const convertedPath = await convert.webm2gif(message.document.id.toString(16), () => message.downloadMedia({}));
|
||||||
chain.push({
|
// markdown 里的 gif 不能动
|
||||||
type: 'image',
|
markdownCompatible = false;
|
||||||
file: convertedPath,
|
useImage(await fsP.readFile(convertedPath), true);
|
||||||
asface: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const temp = await createTempFile();
|
const temp = await createTempFile();
|
||||||
tempFiles.push(temp);
|
tempFiles.push(temp);
|
||||||
await fsP.writeFile(temp.path, await message.downloadMedia({}));
|
await fsP.writeFile(temp.path, await message.downloadMedia({}));
|
||||||
chain.push(segment.video(temp.path));
|
chain.push(segment.video(temp.path));
|
||||||
|
markdownCompatible = false;
|
||||||
}
|
}
|
||||||
brief += '[视频]';
|
brief += '[视频]';
|
||||||
}
|
}
|
||||||
else if (message.sticker) {
|
else if (message.sticker) {
|
||||||
// 一定是 tgs
|
// 一定是 tgs
|
||||||
const gifPath = await convert.tgs2gif(message.sticker.id.toString(16), () => message.downloadMedia({}));
|
const gifPath = await convert.tgs2gif(message.sticker.id.toString(16), () => message.downloadMedia({}));
|
||||||
chain.push({
|
useImage(await fsP.readFile(gifPath), true);
|
||||||
type: 'image',
|
markdownCompatible = false;
|
||||||
file: gifPath,
|
|
||||||
asface: true,
|
|
||||||
});
|
|
||||||
brief += '[贴纸]';
|
brief += '[贴纸]';
|
||||||
}
|
}
|
||||||
else if (message.voice) {
|
else if (message.voice) {
|
||||||
|
@ -559,6 +587,7 @@ export default class ForwardService {
|
||||||
await fsP.writeFile(temp.path, await message.downloadMedia({}));
|
await fsP.writeFile(temp.path, await message.downloadMedia({}));
|
||||||
const bufSilk = await silk.encode(temp.path);
|
const bufSilk = await silk.encode(temp.path);
|
||||||
chain.push(segment.record(bufSilk));
|
chain.push(segment.record(bufSilk));
|
||||||
|
markdownCompatible = false;
|
||||||
if (this.speechClient) {
|
if (this.speechClient) {
|
||||||
const pcmPath = await createTempFile({ postfix: '.pcm' });
|
const pcmPath = await createTempFile({ postfix: '.pcm' });
|
||||||
tempFiles.push(pcmPath);
|
tempFiles.push(pcmPath);
|
||||||
|
@ -579,13 +608,14 @@ export default class ForwardService {
|
||||||
}
|
}
|
||||||
else if (message.poll) {
|
else if (message.poll) {
|
||||||
const poll = message.poll.poll;
|
const poll = message.poll.poll;
|
||||||
chain.push(`${poll.multipleChoice ? '多' : '单'}选投票:\n${poll.question}`);
|
useText(`${poll.multipleChoice ? '多' : '单'}选投票:\n${poll.question}`);
|
||||||
chain.push(...poll.answers.map(answer => `\n - ${answer.text}`));
|
chain.push('\n');
|
||||||
|
useText(poll.answers.map(answer => ` - ${answer.text}`).join('\n'));
|
||||||
brief += '[投票]';
|
brief += '[投票]';
|
||||||
}
|
}
|
||||||
else if (message.contact) {
|
else if (message.contact) {
|
||||||
const contact = message.contact;
|
const contact = message.contact;
|
||||||
chain.push(`名片:\n` +
|
useText(`名片:\n` +
|
||||||
contact.firstName + (contact.lastName ? ' ' + contact.lastName : '') +
|
contact.firstName + (contact.lastName ? ' ' + contact.lastName : '') +
|
||||||
(contact.phoneNumber ? `\n电话:${contact.phoneNumber}` : ''));
|
(contact.phoneNumber ? `\n电话:${contact.phoneNumber}` : ''));
|
||||||
brief += '[名片]';
|
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);
|
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})`));
|
chain.push(segment.location(geo.lat, geo.lng, `${message.venue.title} (${message.venue.address})`));
|
||||||
brief += `[位置:${message.venue.title}]`;
|
brief += `[位置:${message.venue.title}]`;
|
||||||
|
markdownCompatible = false;
|
||||||
}
|
}
|
||||||
else if (message.geo instanceof Api.GeoPoint) {
|
else if (message.geo instanceof Api.GeoPoint) {
|
||||||
// 普通的位置,没有名字
|
// 普通的位置,没有名字
|
||||||
const geo: { lat: number, lng: number } = eviltransform.wgs2gcj(message.geo.lat, message.geo.long);
|
const geo: { lat: number, lng: number } = eviltransform.wgs2gcj(message.geo.lat, message.geo.long);
|
||||||
chain.push(segment.location(geo.lat, geo.lng, '选中的位置'));
|
chain.push(segment.location(geo.lat, geo.lng, '选中的位置'));
|
||||||
brief += '[位置]';
|
brief += '[位置]';
|
||||||
|
markdownCompatible = false;
|
||||||
}
|
}
|
||||||
else if (message.media instanceof Api.MessageMediaDocument && message.media.document instanceof Api.Document) {
|
else if (message.media instanceof Api.MessageMediaDocument && message.media.document instanceof Api.Document) {
|
||||||
const file = message.media.document;
|
const file = message.media.document;
|
||||||
const fileNameAttribute =
|
const fileNameAttribute =
|
||||||
file.attributes.find(attribute => attribute instanceof Api.DocumentAttributeFilename) as Api.DocumentAttributeFilename;
|
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.mimeType}\n` +
|
||||||
`大小:${file.size}`);
|
`大小:${file.size}`);
|
||||||
if (file.size.leq(50 * 1024 * 1024)) {
|
if (file.size.leq(50 * 1024 * 1024)) {
|
||||||
chain.push('\n文件正在上传中…');
|
chain.push('\n');
|
||||||
|
useText('文件正在上传中…');
|
||||||
if (pair.qq instanceof Group) {
|
if (pair.qq instanceof Group) {
|
||||||
pair.qq.fs.upload(await message.downloadMedia({}), '/',
|
pair.qq.fs.upload(await message.downloadMedia({}), '/',
|
||||||
fileNameAttribute ? fileNameAttribute.fileName : 'file')
|
fileNameAttribute ? fileNameAttribute.fileName : 'file')
|
||||||
|
@ -647,29 +680,32 @@ export default class ForwardService {
|
||||||
}
|
}
|
||||||
chain.push(messageLeft, ...newChain);
|
chain.push(messageLeft, ...newChain);
|
||||||
brief += message.message;
|
brief += message.message;
|
||||||
|
markdown.push(markdownEscape(message.message));
|
||||||
}
|
}
|
||||||
// Q2TG Bot 转发的消息目前不会包含 custom emoji
|
// Q2TG Bot 转发的消息目前不会包含 custom emoji
|
||||||
else if (message.forward?.senderId?.eq?.(this.tgBot.me.id) && /^.*: ?$/.test(message.message.split('\n')[0])) {
|
else if (message.forward?.senderId?.eq?.(this.tgBot.me.id) && /^.*: ?$/.test(message.message.split('\n')[0])) {
|
||||||
// 复读了某一条来自 QQ 的消息 (Repeat as forward)
|
// 复读了某一条来自 QQ 的消息 (Repeat as forward)
|
||||||
const originalMessage = message.message.includes('\n') ?
|
const originalMessage = message.message.includes('\n') ?
|
||||||
message.message.substring(message.message.indexOf('\n') + 1) : '';
|
message.message.substring(message.message.indexOf('\n') + 1) : '';
|
||||||
chain.push(originalMessage);
|
useText(originalMessage);
|
||||||
brief += originalMessage;
|
brief += originalMessage;
|
||||||
|
|
||||||
messageHeader = helper.getUserDisplayName(message.sender) + ' 转发自 ' +
|
messageHeader = helper.getUserDisplayName(message.sender) + ' 转发自 ' +
|
||||||
message.message.substring(0, message.message.indexOf(':')) + ': \n';
|
message.message.substring(0, message.message.indexOf(':')) + ': \n';
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
chain.push(message.message);
|
useText(message.message);
|
||||||
brief += message.message;
|
brief += message.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理回复
|
// 处理回复
|
||||||
let source: Quotable;
|
let source: Quotable;
|
||||||
if (message.replyToMsgId) {
|
if (message.replyToMsgId || message.replyTo) {
|
||||||
|
markdownCompatible = false;
|
||||||
try {
|
try {
|
||||||
const quote = await db.message.findFirst({
|
console.log(message.replyTo);
|
||||||
|
const quote = message.replyToMsgId && await db.message.findFirst({
|
||||||
where: {
|
where: {
|
||||||
tgChatId: Number(pair.tg.id),
|
tgChatId: Number(pair.tg.id),
|
||||||
tgMsgId: message.replyToMsgId,
|
tgMsgId: message.replyToMsgId,
|
||||||
|
@ -678,7 +714,7 @@ export default class ForwardService {
|
||||||
});
|
});
|
||||||
if (quote) {
|
if (quote) {
|
||||||
source = {
|
source = {
|
||||||
message: quote.brief || ' ',
|
message: message.replyTo?.quoteText || quote.brief || ' ',
|
||||||
seq: quote.seq,
|
seq: quote.seq,
|
||||||
rand: Number(quote.rand),
|
rand: Number(quote.rand),
|
||||||
user_id: Number(quote.qqSenderId),
|
user_id: Number(quote.qqSenderId),
|
||||||
|
@ -687,7 +723,7 @@ export default class ForwardService {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
source = {
|
source = {
|
||||||
message: '回复消息找不到',
|
message: message.replyTo?.quoteText || '回复消息找不到',
|
||||||
seq: 1,
|
seq: 1,
|
||||||
time: Math.floor(new Date().getTime() / 1000),
|
time: Math.floor(new Date().getTime() / 1000),
|
||||||
rand: 1,
|
rand: 1,
|
||||||
|
@ -720,6 +756,7 @@ export default class ForwardService {
|
||||||
&& chainableElements.length
|
&& chainableElements.length
|
||||||
&& this.instance.workMode
|
&& this.instance.workMode
|
||||||
&& pair.instanceMapForTg[senderId]
|
&& pair.instanceMapForTg[senderId]
|
||||||
|
&& !((pair.flags | this.instance.flags) & flags.DISABLE_SEAMLESS)
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const messageSent = await pair.instanceMapForTg[senderId].sendMsg([
|
const messageSent = await pair.instanceMapForTg[senderId].sendMsg([
|
||||||
|
@ -747,6 +784,12 @@ export default class ForwardService {
|
||||||
|
|
||||||
if (this.instance.workMode === 'group' && !isSpoilerPhoto) {
|
if (this.instance.workMode === 'group' && !isSpoilerPhoto) {
|
||||||
chainableElements.unshift(messageHeader);
|
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>;
|
const qqMessages = [] as Array<QQMessageSent>;
|
||||||
if (chainableElements.length) {
|
if (chainableElements.length) {
|
||||||
|
@ -757,8 +800,13 @@ export default class ForwardService {
|
||||||
eqq: { type: 'tg', tgUid: senderId, noSplitSender: this.instance.workMode === 'personal', version: 2 },
|
eqq: { type: 'tg', tgUid: senderId, noSplitSender: this.instance.workMode === 'personal', version: 2 },
|
||||||
}, undefined, 0),
|
}, 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({
|
qqMessages.push({
|
||||||
...await pair.qq.sendMsg(chainableElements, source),
|
...await pair.qq.sendMsg(messageToSend, source),
|
||||||
brief,
|
brief,
|
||||||
senderId: this.oicq.uin,
|
senderId: this.oicq.uin,
|
||||||
});
|
});
|
|
@ -8,7 +8,7 @@ import { Pair } from '../models/Pair';
|
||||||
import { CustomFile } from 'telegram/client/uploads';
|
import { CustomFile } from 'telegram/client/uploads';
|
||||||
import { getAvatar } from '../utils/urls';
|
import { getAvatar } from '../utils/urls';
|
||||||
import db from '../models/db';
|
import db from '../models/db';
|
||||||
import { Friend, Group } from 'icqq';
|
import { Friend, Group } from '@icqqjs/icqq';
|
||||||
import { format } from 'date-and-time';
|
import { format } from 'date-and-time';
|
||||||
import ZincSearch from 'zincsearch-node';
|
import ZincSearch from 'zincsearch-node';
|
||||||
import env from '../models/env';
|
import env from '../models/env';
|
||||||
|
@ -146,4 +146,73 @@ export default class InChatCommandsService {
|
||||||
});
|
});
|
||||||
return rpy.join('\n');
|
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}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import Telegram from '../client/Telegram';
|
import Telegram from '../client/Telegram';
|
||||||
import { getLogger, Logger } from 'log4js';
|
import { getLogger, Logger } from 'log4js';
|
||||||
import { BigInteger } from 'big-integer';
|
import { BigInteger } from 'big-integer';
|
||||||
import { Platform } from 'icqq';
|
import { Platform } from '@icqqjs/icqq';
|
||||||
import { MarkupLike } from 'telegram/define';
|
import { MarkupLike } from 'telegram/define';
|
||||||
import OicqClient from '../client/OicqClient';
|
import OicqClient from '../client/OicqClient';
|
||||||
import { Button } from 'telegram/tl/custom/button';
|
import { Button } from 'telegram/tl/custom/button';
|
|
@ -1,4 +1,4 @@
|
||||||
import { MessageRet } from 'icqq';
|
import { MessageRet } from '@icqqjs/icqq';
|
||||||
|
|
||||||
export type WorkMode = 'group' | 'personal';
|
export type WorkMode = 'group' | 'personal';
|
||||||
export type QQMessageSent = MessageRet & { senderId: number, brief: string };
|
export type QQMessageSent = MessageRet & { senderId: number, brief: string };
|
|
@ -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) {
|
export default async function getAboutText(entity: Friend | Group, html: boolean) {
|
||||||
let text: string;
|
let text: string;
|
|
@ -1,5 +1,5 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Friend, Group } from 'icqq';
|
import { Friend, Group } from '@icqqjs/icqq';
|
||||||
|
|
||||||
export function getAvatarUrl(room: number | bigint | Friend | Group): string {
|
export function getAvatarUrl(room: number | bigint | Friend | Group): string {
|
||||||
if (!room) return '';
|
if (!room) return '';
|
||||||
|
@ -32,3 +32,7 @@ export async function fetchFile(url: string): Promise<Buffer> {
|
||||||
export function getAvatar(room: number | Friend | Group) {
|
export function getAvatar(room: number | Friend | Group) {
|
||||||
return fetchFile(getAvatarUrl(room));
|
return fetchFile(getAvatarUrl(room));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isContainsUrl(msg: string): boolean {
|
||||||
|
return msg.includes("https://") || msg.includes("http://")
|
||||||
|
}
|
56
package.json
56
package.json
|
@ -1,57 +1,15 @@
|
||||||
{
|
{
|
||||||
"name": "q2tg",
|
"name": "q2tg",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx src/index.ts",
|
"dev": "pnpm run --stream --parallel dev",
|
||||||
"build": "tsc",
|
"build": "pnpm run --stream --parallel build"
|
||||||
"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": {
|
"devDependencies": {
|
||||||
"@types/cli-progress": "^3.11.5",
|
"typescript": "^5.4.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"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"pnpm": {
|
||||||
"@prisma/client": "5.9.1",
|
"patchedDependencies": {
|
||||||
"axios": "^1.6.7",
|
"@icqqjs/icqq@1.2.0": "patches/@icqqjs__icqq@1.2.0.patch"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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],
|
7720
pnpm-lock.yaml
7720
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,3 @@
|
||||||
|
packages:
|
||||||
|
- main
|
||||||
|
- ui
|
|
@ -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>
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
render() {
|
||||||
|
return <div>nya!</div>;
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createApp } from 'vue';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
createApp(App)
|
||||||
|
.mount('#app');
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in New Issue