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
|
||||
cache-from: type=gha
|
||||
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
|
||||
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
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -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)
|
||||
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])
|
|
@ -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,
|
||||
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;
|
|
@ -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') {
|
|
@ -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 {
|
|
@ -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;
|
|
@ -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');
|
||||
};
|
|
@ -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';
|
|
@ -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 {
|
|
@ -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) {
|
|
@ -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];
|
|
@ -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>`,
|
|
@ -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,
|
|
@ -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';
|
|
@ -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';
|
|
@ -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';
|
|
@ -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();
|
||||
},
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import { Platform } from 'icqq';
|
||||
import { Platform } from '@icqqjs/icqq';
|
||||
|
||||
export default {
|
||||
convertTextToPlatform(text: string): Platform {
|
|
@ -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) {
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 群组的头像和简介
|
|
@ -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) {
|
|
@ -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';
|
|
@ -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';
|
|
@ -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,
|
||||
});
|
|
@ -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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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 };
|
|
@ -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;
|
|
@ -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://")
|
||||
}
|
56
package.json
56
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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