mirror of
https://github.com/ReVanced/revanced-bots.git
synced 2026-01-19 01:03:58 +00:00
feat: refactor and new features (#7)
* feat: refactor and new features + Refactored the codebase + OCR support in bots + Server sends training data every minute + Not using collectors for Discord feedback buttons anymore + Fixed grammar mistakes + Configs are now seperated + Tokens are no longer in configs - Like feedback doesn't work for Discord yet * feat: remove feedback button once voted * feat: role blacklist * feat: thread name check * feat: error handler for training * fix: bot crashing when a webhook msg is sent * refactor: remove debugging lines * feat: allow fixing mistake at votes in discord bot
This commit is contained in:
64
apps/server/src/PROTOCOL.md
Normal file
64
apps/server/src/PROTOCOL.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Server Protocol
|
||||
|
||||
The server uses TCP for connection and BSON for messages, so you need to serialize and deserialize the messages.
|
||||
|
||||
## AI
|
||||
|
||||
Sending the server this JSON (BSON) will send you back the AI predictions.
|
||||
|
||||
```json
|
||||
{
|
||||
"op": 1,
|
||||
"id": "String",
|
||||
"text": "How do i download ReVanced?"
|
||||
}
|
||||
```
|
||||
|
||||
And the server would return something like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": 2,
|
||||
"id": "String",
|
||||
"response": [
|
||||
{
|
||||
"confidence": 0.99,
|
||||
"id": "String",
|
||||
"name": "revanced_download"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Training the AI
|
||||
|
||||
To train the AI, send the server a JSON (BSON) like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": 3,
|
||||
"label": "revanced_download",
|
||||
"text": "how to download revanced"
|
||||
}
|
||||
```
|
||||
|
||||
## OCR
|
||||
|
||||
Sending the server this JSON (BSON) will send you back the read text.
|
||||
|
||||
```json
|
||||
{
|
||||
"op": 5,
|
||||
"id": "String",
|
||||
"url": "https://cdn.discordapp.com/attachments/1033338556493606963/1033338557231796224/Screenshot_20221022-121318.jpg"
|
||||
}
|
||||
```
|
||||
|
||||
And the server would return something like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": 6,
|
||||
"id": "String",
|
||||
"ocrText": "..."
|
||||
}
|
||||
```
|
||||
5
apps/server/src/events/index.js
Normal file
5
apps/server/src/events/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import runAI from './runAI.js';
|
||||
import runOCR from './runOCR.js';
|
||||
import trainAI from './trainAI.js';
|
||||
|
||||
export { runAI, runOCR, trainAI };
|
||||
24
apps/server/src/events/runAI.js
Normal file
24
apps/server/src/events/runAI.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { serialize } from 'bson';
|
||||
|
||||
export default async function runAI(client, data) {
|
||||
const witAIReq = await fetch(
|
||||
`https://api.wit.ai/message?v=20230215&q=${encodeURI(data.text)}`,
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${process.env.WIT_AI_TOKEN}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const response = await witAIReq.json();
|
||||
|
||||
client.write(
|
||||
serialize({
|
||||
op: 2,
|
||||
id: data.id,
|
||||
response: response.intents
|
||||
})
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
61
apps/server/src/events/runOCR.js
Normal file
61
apps/server/src/events/runOCR.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { createWorker } from 'tesseract.js';
|
||||
import { serialize } from 'bson';
|
||||
import EventEmitter from 'node:events';
|
||||
|
||||
const worker = await createWorker();
|
||||
await worker.loadLanguage('eng');
|
||||
await worker.initialize('eng');
|
||||
|
||||
async function recognize({ client, data }) {
|
||||
const { data: { text } } = await worker.recognize(data.url);
|
||||
|
||||
client.write(
|
||||
serialize({
|
||||
op: 6,
|
||||
id: data.id,
|
||||
ocrText: text
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
class Queue extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.isRunning = false;
|
||||
this.items = []
|
||||
}
|
||||
|
||||
push(item) {
|
||||
this.items.push(item);
|
||||
this.emit('item', item)
|
||||
}
|
||||
|
||||
shift() {
|
||||
return this.items.shift();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const queue = new Queue();
|
||||
|
||||
queue.on('item', async ({ client, data }) => {
|
||||
if (!queue.isRunning) {
|
||||
queue.isRunning = true;
|
||||
await recognize(queue.items.shift());
|
||||
queue.isRunning = false;
|
||||
queue.emit('finished');
|
||||
}
|
||||
});
|
||||
|
||||
queue.on('finished', async () => {
|
||||
if (queue.items.length !== 0) {
|
||||
queue.isRunning = true;
|
||||
await recognize(queue.items.shift());
|
||||
queue.isRunning = false;
|
||||
queue.emit('finished');
|
||||
}
|
||||
});
|
||||
|
||||
export default async function runOCR(client, data) {
|
||||
queue.push({ client, data });
|
||||
}
|
||||
17
apps/server/src/events/trainAI.js
Normal file
17
apps/server/src/events/trainAI.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export default async function trainAI(data) {
|
||||
fetch('https://api.wit.ai/utterances', {
|
||||
headers: {
|
||||
authorization: `Bearer ${process.env.WIT_AI_TOKEN}`
|
||||
},
|
||||
body: JSON.stringify([
|
||||
{
|
||||
text: data.text,
|
||||
intent: data.label,
|
||||
entities: [],
|
||||
traits: []
|
||||
}
|
||||
]),
|
||||
method: 'POST'
|
||||
});
|
||||
return;
|
||||
}
|
||||
30
apps/server/src/index.js
Normal file
30
apps/server/src/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { createServer } from 'node:net';
|
||||
import { deserialize } from 'bson';
|
||||
import { runAI, runOCR, trainAI } from './events/index.js';
|
||||
|
||||
const server = createServer(async (client) => {
|
||||
client.on('data', async (data) => {
|
||||
const eventData = deserialize(data, {
|
||||
allowObjectSmallerThanBufferSize: true
|
||||
});
|
||||
|
||||
switch (eventData.op) {
|
||||
case 1: {
|
||||
runAI(client, eventData);
|
||||
break;
|
||||
}
|
||||
|
||||
case 3: {
|
||||
trainAI(eventData);
|
||||
break;
|
||||
}
|
||||
|
||||
case 5: {
|
||||
await runOCR(client, eventData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(process.env.PORT || 3000);
|
||||
228
apps/server/src/package-lock.json
generated
Normal file
228
apps/server/src/package-lock.json
generated
Normal file
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bson": "^5.3.0",
|
||||
"tesseract.js": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bmp-js": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
|
||||
"integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw=="
|
||||
},
|
||||
"node_modules/bson": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-5.3.0.tgz",
|
||||
"integrity": "sha512-ukmCZMneMlaC5ebPHXIkP8YJzNl5DC41N5MAIvKDqLggdao342t4McltoJBQfQya/nHBWAcSsYRqlXPoQkTJag==",
|
||||
"engines": {
|
||||
"node": ">=14.20.1"
|
||||
}
|
||||
},
|
||||
"node_modules/idb-keyval": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
|
||||
"integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg=="
|
||||
},
|
||||
"node_modules/is-electron": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz",
|
||||
"integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg=="
|
||||
},
|
||||
"node_modules/is-url": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
|
||||
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.11",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz",
|
||||
"integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/opencollective-postinstall": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
|
||||
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
|
||||
"bin": {
|
||||
"opencollective-postinstall": "index.js"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||
},
|
||||
"node_modules/tesseract.js": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-4.1.0.tgz",
|
||||
"integrity": "sha512-sCfWJtei6Ykc7uLiWs5H4IKDAhwsMauEVDwUiSkEpzMo4kH+7n8QDBPPNRtGJoZ4NJBf1WSlcbU+Puf64GjOfw==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bmp-js": "^0.1.0",
|
||||
"idb-keyval": "^6.2.0",
|
||||
"is-electron": "^2.2.2",
|
||||
"is-url": "^1.2.4",
|
||||
"node-fetch": "^2.6.9",
|
||||
"opencollective-postinstall": "^2.0.3",
|
||||
"regenerator-runtime": "^0.13.3",
|
||||
"tesseract.js-core": "^4.0.4",
|
||||
"wasm-feature-detect": "^1.2.11",
|
||||
"zlibjs": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tesseract.js-core": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-4.0.4.tgz",
|
||||
"integrity": "sha512-MJ+vtktjAaT0681uPl6TDUPhbRbpD/S9emko5rtorgHRZpQo7R3BG7h+3pVHgn1KjfNf1bvnx4B7KxEK8YKqpg=="
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/wasm-feature-detect": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.5.1.tgz",
|
||||
"integrity": "sha512-GHr23qmuehNXHY4902/hJ6EV5sUANIJC3R/yMfQ7hWDg3nfhlcJfnIL96R2ohpIwa62araN6aN4bLzzzq5GXkg=="
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zlibjs": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz",
|
||||
"integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"bmp-js": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
|
||||
"integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw=="
|
||||
},
|
||||
"bson": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-5.3.0.tgz",
|
||||
"integrity": "sha512-ukmCZMneMlaC5ebPHXIkP8YJzNl5DC41N5MAIvKDqLggdao342t4McltoJBQfQya/nHBWAcSsYRqlXPoQkTJag=="
|
||||
},
|
||||
"idb-keyval": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
|
||||
"integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg=="
|
||||
},
|
||||
"is-electron": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz",
|
||||
"integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg=="
|
||||
},
|
||||
"is-url": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
|
||||
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.11",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz",
|
||||
"integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==",
|
||||
"requires": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"opencollective-postinstall": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
|
||||
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q=="
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||
},
|
||||
"tesseract.js": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-4.1.0.tgz",
|
||||
"integrity": "sha512-sCfWJtei6Ykc7uLiWs5H4IKDAhwsMauEVDwUiSkEpzMo4kH+7n8QDBPPNRtGJoZ4NJBf1WSlcbU+Puf64GjOfw==",
|
||||
"requires": {
|
||||
"bmp-js": "^0.1.0",
|
||||
"idb-keyval": "^6.2.0",
|
||||
"is-electron": "^2.2.2",
|
||||
"is-url": "^1.2.4",
|
||||
"node-fetch": "^2.6.9",
|
||||
"opencollective-postinstall": "^2.0.3",
|
||||
"regenerator-runtime": "^0.13.3",
|
||||
"tesseract.js-core": "^4.0.4",
|
||||
"wasm-feature-detect": "^1.2.11",
|
||||
"zlibjs": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"tesseract.js-core": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-4.0.4.tgz",
|
||||
"integrity": "sha512-MJ+vtktjAaT0681uPl6TDUPhbRbpD/S9emko5rtorgHRZpQo7R3BG7h+3pVHgn1KjfNf1bvnx4B7KxEK8YKqpg=="
|
||||
},
|
||||
"tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"wasm-feature-detect": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.5.1.tgz",
|
||||
"integrity": "sha512-GHr23qmuehNXHY4902/hJ6EV5sUANIJC3R/yMfQ7hWDg3nfhlcJfnIL96R2ohpIwa62araN6aN4bLzzzq5GXkg=="
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"requires": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"zlibjs": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz",
|
||||
"integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w=="
|
||||
}
|
||||
}
|
||||
}
|
||||
14
apps/server/src/package.json
Normal file
14
apps/server/src/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"keywords": [],
|
||||
"author": "Reis Can",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"bson": "^5.3.0",
|
||||
"tesseract.js": "^4.1.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user