mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2024-11-21 09:36:35 +00:00
commit
f51ffc091d
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "qq-chat",
|
"name": "qq-chat",
|
||||||
"version": "9.9.15-28327",
|
"version": "9.9.15-28418",
|
||||||
"verHash": "512caf78",
|
"verHash": "512caf78",
|
||||||
"linuxVersion": "3.2.12-28327",
|
"linuxVersion": "3.2.12-28327",
|
||||||
"linuxVerHash": "f60e8252",
|
"linuxVerHash": "f60e8252",
|
||||||
@ -18,7 +18,7 @@
|
|||||||
"qd": "externals/devtools/cli/index.js"
|
"qd": "externals/devtools/cli/index.js"
|
||||||
},
|
},
|
||||||
"main": "./loadNapCat.js",
|
"main": "./loadNapCat.js",
|
||||||
"buildVersion": "28327",
|
"buildVersion": "28418",
|
||||||
"isPureShell": true,
|
"isPureShell": true,
|
||||||
"isByteCodeShell": true,
|
"isByteCodeShell": true,
|
||||||
"platform": "win32",
|
"platform": "win32",
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"name": "NapCatQQ",
|
"name": "NapCatQQ",
|
||||||
"slug": "NapCat.Framework",
|
"slug": "NapCat.Framework",
|
||||||
"description": "高性能的 OneBot 11 协议实现",
|
"description": "高性能的 OneBot 11 协议实现",
|
||||||
"version": "2.6.15",
|
"version": "2.6.16",
|
||||||
"icon": "./logo.png",
|
"icon": "./logo.png",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "napcat",
|
"name": "napcat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.6.15",
|
"version": "2.6.16",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:framework": "vite build --mode framework",
|
"build:framework": "vite build --mode framework",
|
||||||
"build:shell": "vite build --mode shell",
|
"build:shell": "vite build --mode shell",
|
||||||
|
@ -1 +1 @@
|
|||||||
export const napCatVersion = '2.6.15';
|
export const napCatVersion = '2.6.16';
|
||||||
|
8
src/core/external/appid.json
vendored
8
src/core/external/appid.json
vendored
@ -22,5 +22,13 @@
|
|||||||
"3.2.12-28327":{
|
"3.2.12-28327":{
|
||||||
"appid": 537249393,
|
"appid": 537249393,
|
||||||
"qua": "V1_LNX_NQ_3.2.12_28327_GW_B"
|
"qua": "V1_LNX_NQ_3.2.12_28327_GW_B"
|
||||||
|
},
|
||||||
|
"9.9.15-28418":{
|
||||||
|
"appid": 537249321,
|
||||||
|
"qua": "V1_WIN_NQ_9.9.15_28418_GW_B"
|
||||||
|
},
|
||||||
|
"3.2.12-28418":{
|
||||||
|
"appid": 537249393,
|
||||||
|
"qua": "V1_LNX_NQ_3.2.12_28418_GW_B"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -20,16 +20,15 @@ import { LogLevel, LogWrapper } from '@/common/log';
|
|||||||
import { NodeIKernelLoginService } from '@/core/services';
|
import { NodeIKernelLoginService } from '@/core/services';
|
||||||
import { QQBasicInfoWrapper } from '@/common/qq-basic-info';
|
import { QQBasicInfoWrapper } from '@/common/qq-basic-info';
|
||||||
import { NapCatPathWrapper } from '@/common/path';
|
import { NapCatPathWrapper } from '@/common/path';
|
||||||
import path from 'node:path';
|
import path, { resolve } from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { hostname, systemName, systemVersion } from '@/common/system';
|
import { hostname, systemName, systemVersion } from '@/common/system';
|
||||||
import { NTEventWrapper } from '@/common/event';
|
import { NTEventWrapper } from '@/common/event';
|
||||||
import { DataSource, GroupMember, KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/entities';
|
import { ChatType, DataSource, GroupMember, KickedOffLineInfo, Peer, SelfInfo, SelfStatusInfo } from '@/core/entities';
|
||||||
import { NapCatConfigLoader } from '@/core/helper/config';
|
import { NapCatConfigLoader } from '@/core/helper/config';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { NodeIKernelGroupListener, NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
|
import { NodeIKernelGroupListener, NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
|
||||||
import { proxiedListenerOf } from '@/common/proxy-handler';
|
import { proxiedListenerOf } from '@/common/proxy-handler';
|
||||||
|
|
||||||
export * from './wrapper';
|
export * from './wrapper';
|
||||||
export * from './entities';
|
export * from './entities';
|
||||||
export * from './services';
|
export * from './services';
|
||||||
@ -99,6 +98,7 @@ export class NapCatCore {
|
|||||||
if (!fs.existsSync(this.NapCatTempPath)) {
|
if (!fs.existsSync(this.NapCatTempPath)) {
|
||||||
fs.mkdirSync(this.NapCatTempPath, { recursive: true });
|
fs.mkdirSync(this.NapCatTempPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.initNapCatCoreListeners().then().catch(this.context.logger.logError.bind(this.context.logger));
|
this.initNapCatCoreListeners().then().catch(this.context.logger.logError.bind(this.context.logger));
|
||||||
|
|
||||||
this.context.logger.setFileLogEnabled(
|
this.context.logger.setFileLogEnabled(
|
||||||
@ -248,7 +248,7 @@ export class NapCatCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function genSessionConfig(
|
export async function genSessionConfig(
|
||||||
guid:string,
|
guid: string,
|
||||||
QQVersionAppid: string,
|
QQVersionAppid: string,
|
||||||
QQVersion: string,
|
QQVersion: string,
|
||||||
selfUin: string,
|
selfUin: string,
|
||||||
|
37
src/core/proto/Message.ts
Normal file
37
src/core/proto/Message.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import * as pb from 'protobufjs';
|
||||||
|
|
||||||
|
|
||||||
|
export const BodyInner = new pb.Type("BodyInner")
|
||||||
|
.add(new pb.Field("msgType", 1, "uint32", "optional"))
|
||||||
|
.add(new pb.Field("subType", 2, "uint32", "optional"))
|
||||||
|
|
||||||
|
export const NoifyData = new pb.Type("NoifyData")
|
||||||
|
.add(new pb.Field("skip", 1, "bytes", "optional"))
|
||||||
|
.add(new pb.Field("innerData", 2, "bytes", "optional"))
|
||||||
|
|
||||||
|
export const MsgHead = new pb.Type("MsgHead")
|
||||||
|
.add(BodyInner)
|
||||||
|
.add(NoifyData)
|
||||||
|
.add(new pb.Field("bodyInner", 2, "BodyInner", "optional"))
|
||||||
|
.add(new pb.Field("noifyData", 3, "NoifyData", "optional"));
|
||||||
|
|
||||||
|
export const Message = new pb.Type("Message")
|
||||||
|
.add(MsgHead)
|
||||||
|
.add(new pb.Field("msgHead", 1, "MsgHead"))
|
||||||
|
|
||||||
|
export const SubDetail = new pb.Type("SubDetail")
|
||||||
|
.add(new pb.Field("msgSeq", 1, "uint32"))
|
||||||
|
.add(new pb.Field("msgTime", 2, "uint32"))
|
||||||
|
.add(new pb.Field("senderUid", 6, "string"))
|
||||||
|
|
||||||
|
export const RecallDetails = new pb.Type("RecallDetails")
|
||||||
|
.add(SubDetail)
|
||||||
|
.add(new pb.Field("operatorUid", 1, "string"))
|
||||||
|
.add(new pb.Field("subDetail", 3, "SubDetail"))
|
||||||
|
|
||||||
|
export const RecallGroup = new pb.Type("RecallGroup")
|
||||||
|
.add(RecallDetails)
|
||||||
|
.add(new pb.Field("type", 1, "int32"))
|
||||||
|
.add(new pb.Field("peerUid", 4, "uint32"))
|
||||||
|
.add(new pb.Field("recallDetails", 11, "RecallDetails"))
|
||||||
|
.add(new pb.Field("grayTipsSeq", 37, "uint32"))
|
BIN
src/native/external/MoeHoo.win32.node
vendored
Normal file
BIN
src/native/external/MoeHoo.win32.node
vendored
Normal file
Binary file not shown.
23
src/native/index.ts
Normal file
23
src/native/index.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { constants } from "node:os";
|
||||||
|
import path from "path";
|
||||||
|
import { dlopen } from "process";
|
||||||
|
export class Native {
|
||||||
|
platform: string;
|
||||||
|
supportedPlatforms = ['win32'];
|
||||||
|
MoeHooExport: any = { exports: {} };
|
||||||
|
recallHookEnabled: boolean = false;
|
||||||
|
constructor(nodePath: string, platform: string = process.platform) {
|
||||||
|
this.platform = platform;
|
||||||
|
if (!this.supportedPlatforms.includes(this.platform)) {
|
||||||
|
throw new Error(`Platform ${this.platform} is not supported`);
|
||||||
|
}
|
||||||
|
dlopen(this.MoeHooExport, path.join(nodePath, './native/MoeHoo.win32.node'), constants.dlopen.RTLD_LAZY);
|
||||||
|
}
|
||||||
|
isSetReCallEnabled(): boolean {
|
||||||
|
return this.recallHookEnabled;
|
||||||
|
}
|
||||||
|
registerRecallCallback(callback: (hex: string) => any): void {
|
||||||
|
this.recallHookEnabled = true;
|
||||||
|
return this.MoeHooExport.exports.registMsgPush(callback);
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import BaseAction from '../BaseAction';
|
|||||||
import { ActionName } from '../types';
|
import { ActionName } from '../types';
|
||||||
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
|
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
|
||||||
import { MessageUnique } from '@/common/message-unique';
|
import { MessageUnique } from '@/common/message-unique';
|
||||||
|
import { RawMessage } from '@/core';
|
||||||
|
|
||||||
|
|
||||||
export type ReturnDataType = OB11Message
|
export type ReturnDataType = OB11Message
|
||||||
@ -32,13 +33,17 @@ class GetMsg extends BaseAction<Payload, OB11Message> {
|
|||||||
throw new Error('消息不存在');
|
throw new Error('消息不存在');
|
||||||
}
|
}
|
||||||
const peer = { guildId: '', peerUid: msgIdWithPeer?.Peer.peerUid, chatType: msgIdWithPeer.Peer.chatType };
|
const peer = { guildId: '', peerUid: msgIdWithPeer?.Peer.peerUid, chatType: msgIdWithPeer.Peer.chatType };
|
||||||
const msg = await this.core.apis.MsgApi.getMsgsByMsgId(
|
let orimsg = this.obContext.recallMsgCache.get(msgIdWithPeer.MsgId);
|
||||||
peer,
|
let msg: RawMessage;
|
||||||
[msgIdWithPeer?.MsgId || payload.message_id.toString()]);
|
if (orimsg) {
|
||||||
const retMsg = await this.obContext.apis.MsgApi.parseMessage(msg.msgList[0]);
|
msg = orimsg;
|
||||||
|
} else {
|
||||||
|
msg = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgIdWithPeer?.MsgId || payload.message_id.toString()])).msgList[0];
|
||||||
|
}
|
||||||
|
const retMsg = await this.obContext.apis.MsgApi.parseMessage(msg);
|
||||||
if (!retMsg) throw Error('消息为空');
|
if (!retMsg) throw Error('消息为空');
|
||||||
try {
|
try {
|
||||||
retMsg.message_id = MessageUnique.createUniqueMsgId(peer, msg.msgList[0].msgId)!;
|
retMsg.message_id = MessageUnique.createUniqueMsgId(peer, msg.msgId)!;
|
||||||
retMsg.message_seq = retMsg.message_id;
|
retMsg.message_seq = retMsg.message_id;
|
||||||
retMsg.real_id = retMsg.message_id;
|
retMsg.real_id = retMsg.message_id;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
NodeIKernelBuddyListener,
|
NodeIKernelBuddyListener,
|
||||||
NodeIKernelGroupListener,
|
NodeIKernelGroupListener,
|
||||||
NodeIKernelMsgListener,
|
NodeIKernelMsgListener,
|
||||||
|
Peer,
|
||||||
RawMessage,
|
RawMessage,
|
||||||
SendStatusType,
|
SendStatusType,
|
||||||
} from '@/core';
|
} from '@/core';
|
||||||
@ -43,8 +44,9 @@ import { OB11FriendRecallNoticeEvent } from '@/onebot/event/notice/OB11FriendRec
|
|||||||
import { OB11GroupRecallNoticeEvent } from '@/onebot/event/notice/OB11GroupRecallNoticeEvent';
|
import { OB11GroupRecallNoticeEvent } from '@/onebot/event/notice/OB11GroupRecallNoticeEvent';
|
||||||
import { LRUCache } from '@/common/lru-cache';
|
import { LRUCache } from '@/common/lru-cache';
|
||||||
import { NodeIKernelRecentContactListener } from '@/core/listeners/NodeIKernelRecentContactListener';
|
import { NodeIKernelRecentContactListener } from '@/core/listeners/NodeIKernelRecentContactListener';
|
||||||
import { OB11ProfileLikeEvent } from './event/notice/OB11ProfileLikeEvent';
|
import { Native } from '@/native';
|
||||||
import { profileLikeTip, ProfileLikeTipType, SysMessage, SysMessageType } from '@/core/proto/ProfileLike';
|
import { Message, RecallGroup } from '@/core/proto/Message';
|
||||||
|
|
||||||
//OneBot实现类
|
//OneBot实现类
|
||||||
export class NapCatOneBot11Adapter {
|
export class NapCatOneBot11Adapter {
|
||||||
readonly core: NapCatCore;
|
readonly core: NapCatCore;
|
||||||
@ -54,8 +56,9 @@ export class NapCatOneBot11Adapter {
|
|||||||
apis: StableOneBotApiWrapper;
|
apis: StableOneBotApiWrapper;
|
||||||
networkManager: OB11NetworkManager;
|
networkManager: OB11NetworkManager;
|
||||||
actions: ActionMap;
|
actions: ActionMap;
|
||||||
|
nativeCore: Native | undefined;
|
||||||
private bootTime = Date.now() / 1000;
|
private bootTime = Date.now() / 1000;
|
||||||
|
recallMsgCache = new LRUCache<string, RawMessage>(100);
|
||||||
|
|
||||||
constructor(core: NapCatCore, context: InstanceContext, pathWrapper: NapCatPathWrapper) {
|
constructor(core: NapCatCore, context: InstanceContext, pathWrapper: NapCatPathWrapper) {
|
||||||
this.core = core;
|
this.core = core;
|
||||||
@ -70,10 +73,54 @@ export class NapCatOneBot11Adapter {
|
|||||||
};
|
};
|
||||||
this.actions = createActionMap(this, core);
|
this.actions = createActionMap(this, core);
|
||||||
this.networkManager = new OB11NetworkManager();
|
this.networkManager = new OB11NetworkManager();
|
||||||
|
this.registerNative(core, context).then().catch();
|
||||||
this.InitOneBot()
|
this.InitOneBot()
|
||||||
.catch(e => this.context.logger.logError.bind(this.context.logger)('初始化OneBot失败', e));
|
.catch(e => this.context.logger.logError.bind(this.context.logger)('初始化OneBot失败', e));
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
async registerNative(core: NapCatCore, context: InstanceContext) {
|
||||||
|
this.nativeCore = new Native(context.pathWrapper.binaryPath);
|
||||||
|
this.nativeCore.registerRecallCallback(async (hex: string) => {
|
||||||
|
try {
|
||||||
|
let data = Message.decode(Buffer.from(hex, 'hex')) as any;
|
||||||
|
//data.MsgHead.BodyInner.MsgType SubType
|
||||||
|
let bodyInner = data.msgHead?.bodyInner;
|
||||||
|
//context.logger.log("[appNative] Parse MsgType:" + bodyInner.msgType + " / SubType:" + bodyInner.subType);
|
||||||
|
if (bodyInner && bodyInner.msgType == 732 && bodyInner.subType == 17) {
|
||||||
|
let RecallData = Buffer.from(data.msgHead.noifyData.innerData);
|
||||||
|
//跳过 4字节 群号 + 不知道的1字节 +2字节 长度
|
||||||
|
let uid = RecallData.readUint32BE();
|
||||||
|
const buffer = Buffer.from(RecallData.toString('hex').slice(14), 'hex');
|
||||||
|
let seq: number = (RecallGroup.decode(buffer) as any).recallDetails.subDetail.msgSeq;
|
||||||
|
let peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: uid.toString() };
|
||||||
|
context.logger.log("[Native] 群消息撤回 Peer: " + uid.toString() + " / MsgSeq:" + seq);
|
||||||
|
let msgs = await core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, seq.toString());
|
||||||
|
this.recallMsgCache.put(msgs.msgList[0].msgId, msgs.msgList[0]);
|
||||||
|
// let ob11 = await this.apis.MsgApi.parseMessage(msgs.msgList[0], 'array')
|
||||||
|
// .catch(e => this.context.logger.logError.bind(this.context.logger)('处理消息失败', e));
|
||||||
|
// if (ob11) {
|
||||||
|
// const { sendElements, deleteAfterSentFiles } = await this.apis.MsgApi.createSendElements(ob11.message as OB11MessageData[], peer);
|
||||||
|
// this.apis.MsgApi.sendMsgWithOb11UniqueId(peer, sendElements, deleteAfterSentFiles);
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// this.apis.MsgApi.sendMsg(peer, [{
|
||||||
|
// elementType: 1,
|
||||||
|
// elementId: '',
|
||||||
|
// textElement: {
|
||||||
|
// content: "[Native] 群消息撤回 Peer: " + uid.toString() + " / MsgSeq:" + seq,
|
||||||
|
// atType: 0,
|
||||||
|
// atUid: '',
|
||||||
|
// atTinyId: '',
|
||||||
|
// atNtUid: '',
|
||||||
|
// },
|
||||||
|
// }]);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
context.logger.logWarn("[Native] Error:", (error as Error).message, ' HEX:', hex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
async InitOneBot() {
|
async InitOneBot() {
|
||||||
const selfInfo = this.core.selfInfo;
|
const selfInfo = this.core.selfInfo;
|
||||||
const ob11Config = this.configLoader.configData;
|
const ob11Config = this.configLoader.configData;
|
||||||
@ -523,7 +570,6 @@ export class NapCatOneBot11Adapter {
|
|||||||
}
|
}
|
||||||
}).catch(e => this.context.logger.logError.bind(this.context.logger)('constructPrivateEvent error: ', e));
|
}).catch(e => this.context.logger.logError.bind(this.context.logger)('constructPrivateEvent error: ', e));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async emitRecallMsg(msgList: RawMessage[], cache: LRUCache<string, boolean>) {
|
private async emitRecallMsg(msgList: RawMessage[], cache: LRUCache<string, boolean>) {
|
||||||
for (const message of msgList) {
|
for (const message of msgList) {
|
||||||
// log("message update", message.sendStatus, message.msgId, message.msgSeq)
|
// log("message update", message.sendStatus, message.msgId, message.msgSeq)
|
||||||
@ -555,7 +601,7 @@ export class NapCatOneBot11Adapter {
|
|||||||
parseInt(message.peerUin),
|
parseInt(message.peerUin),
|
||||||
parseInt(message.senderUin),
|
parseInt(message.senderUin),
|
||||||
parseInt(operatorId),
|
parseInt(operatorId),
|
||||||
oriMessageId,
|
oriMessageId
|
||||||
);
|
);
|
||||||
this.networkManager.emitEvent(groupRecallEvent)
|
this.networkManager.emitEvent(groupRecallEvent)
|
||||||
.catch(e => this.context.logger.logError.bind(this.context.logger)('处理群消息撤回失败', e));
|
.catch(e => this.context.logger.logError.bind(this.context.logger)('处理群消息撤回失败', e));
|
||||||
|
@ -54,7 +54,6 @@ export async function NCoreInitShell() {
|
|||||||
const loginService = wrapper.NodeIKernelLoginService.get();
|
const loginService = wrapper.NodeIKernelLoginService.get();
|
||||||
|
|
||||||
const session = wrapper.NodeIQQNTWrapperSession.create();
|
const session = wrapper.NodeIQQNTWrapperSession.create();
|
||||||
|
|
||||||
// from get dataPath
|
// from get dataPath
|
||||||
const [dataPath, dataPathGlobal] = (() => {
|
const [dataPath, dataPathGlobal] = (() => {
|
||||||
if (os.platform() === 'darwin') {
|
if (os.platform() === 'darwin') {
|
||||||
|
@ -30,7 +30,7 @@ async function onSettingWindowCreated(view: Element) {
|
|||||||
SettingItem(
|
SettingItem(
|
||||||
'<span id="napcat-update-title">Napcat</span>',
|
'<span id="napcat-update-title">Napcat</span>',
|
||||||
undefined,
|
undefined,
|
||||||
SettingButton('V2.6.15', 'napcat-update-button', 'secondary'),
|
SettingButton('V2.6.16', 'napcat-update-button', 'secondary'),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
SettingList([
|
SettingList([
|
||||||
|
@ -164,7 +164,7 @@ async function onSettingWindowCreated(view) {
|
|||||||
SettingItem(
|
SettingItem(
|
||||||
'<span id="napcat-update-title">Napcat</span>',
|
'<span id="napcat-update-title">Napcat</span>',
|
||||||
void 0,
|
void 0,
|
||||||
SettingButton("V2.6.15", "napcat-update-button", "secondary")
|
SettingButton("V2.6.16", "napcat-update-button", "secondary")
|
||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
SettingList([
|
SettingList([
|
||||||
|
@ -41,6 +41,7 @@ const FrameworkBaseConfigPlugin: PluginOption[] = [
|
|||||||
const ShellBaseConfigPlugin: PluginOption[] = [
|
const ShellBaseConfigPlugin: PluginOption[] = [
|
||||||
cp({
|
cp({
|
||||||
targets: [
|
targets: [
|
||||||
|
{ src: './src/native/external', dest: 'dist/native', flatten: false },
|
||||||
{ src: './static/', dest: 'dist/static/', flatten: false },
|
{ src: './static/', dest: 'dist/static/', flatten: false },
|
||||||
{ src: './src/core/external/napcat.json', dest: 'dist/config/' },
|
{ src: './src/core/external/napcat.json', dest: 'dist/config/' },
|
||||||
{ src: './src/onebot/config/onebot11.json', dest: 'dist/config/' },
|
{ src: './src/onebot/config/onebot11.json', dest: 'dist/config/' },
|
||||||
|
Loading…
x
Reference in New Issue
Block a user