v0.9.0
This commit is contained in:
66
bot.js
66
bot.js
@@ -1,66 +0,0 @@
|
|||||||
/** __ __ __ ___ ___ __ __ ___
|
|
||||||
* / ' / \ |__) | |__ \_/ |__) / \ |
|
|
||||||
* \__. \__/ | \ | |___ / \ |__) \__/ |
|
|
||||||
*
|
|
||||||
* @copyright © 2021 hepller
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Импорт модулей
|
|
||||||
const {VK} = require('vk-io')
|
|
||||||
const Markov = require('markov-generator')
|
|
||||||
|
|
||||||
// Импорт функций
|
|
||||||
const Utils = require('./utils.js')
|
|
||||||
|
|
||||||
// Конфиг, паттерн и файл проекта
|
|
||||||
const config = require('./config')
|
|
||||||
const data = require('./data')
|
|
||||||
const project = require('./package')
|
|
||||||
|
|
||||||
// Инициализация vk-io
|
|
||||||
const vk = new VK({token: config.token, v: 5.131})
|
|
||||||
|
|
||||||
// Сообщения при запуске
|
|
||||||
console.log(`Cortex Bot v${project.version}`)
|
|
||||||
console.log('VK API connection ...')
|
|
||||||
|
|
||||||
// Запуск обработчика событий ВК
|
|
||||||
vk.updates.startPolling()
|
|
||||||
.then(() => console.log('Successfully connected'))
|
|
||||||
|
|
||||||
// Обработчик сообщений ВК
|
|
||||||
vk.updates.on('message_new', async ctx => {
|
|
||||||
|
|
||||||
// Игнорирование лишних сообщений
|
|
||||||
if (!ctx.isChat || ctx.isOutbox || ctx.isGroup) return
|
|
||||||
|
|
||||||
// Генерация сообщения с определенным шансом
|
|
||||||
if (Utils.getWithChance(config.chance)) {
|
|
||||||
|
|
||||||
// Сообщение в консоль о начале генеарции
|
|
||||||
console.log(`Generation started ${Utils.getTimeString()} <#${ctx.chatId}>`)
|
|
||||||
|
|
||||||
// Статус набора текста
|
|
||||||
await ctx.setActivity()
|
|
||||||
|
|
||||||
// Время в начале генерации
|
|
||||||
const start_time = Date.now()
|
|
||||||
|
|
||||||
// Генерация текста
|
|
||||||
const sentence = new Markov({input: data, minLenght: config.min_words}).makeChain()
|
|
||||||
|
|
||||||
// Предупреждение при превышении лимита символов
|
|
||||||
if (sentence.split('').length > config.max_symbols) return console.log(`#${ctx.chatId} Symbols limit exceeded (${sentence.split('').length}/${config.max_symbols})`)
|
|
||||||
|
|
||||||
// Время в конце генерации (преобразованное в милисекунды)
|
|
||||||
const final_time = (Date.now() - start_time) % 1000
|
|
||||||
|
|
||||||
// Отправка сообщения
|
|
||||||
await ctx.send(config.format ? Utils.formatText(sentence) : sentence)
|
|
||||||
.then(() => console.log(`Text generated ${final_time}ms <#${ctx.chatId}>`))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Обработка возможных ошибок
|
|
||||||
process.on('uncaughtException', error => console.error(`Error: ${error.message}`))
|
|
||||||
process.on('unhandledRejection', error => console.error(`Error: ${error.message}`))
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"token": "TOKEN",
|
|
||||||
"chance": 10,
|
|
||||||
"min_words": 2,
|
|
||||||
"max_symbols": 250,
|
|
||||||
"format": true
|
|
||||||
}
|
|
||||||
40
config.yml
Normal file
40
config.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# __ __ __ ___ ___
|
||||||
|
# / / \ |__) | |__ \_/
|
||||||
|
# \__ \__/ | \ | |___ / \
|
||||||
|
|
||||||
|
# Настройки VK API
|
||||||
|
longpoll:
|
||||||
|
# Токен доступа
|
||||||
|
token: TOKEN
|
||||||
|
# Версия
|
||||||
|
version: 5.131
|
||||||
|
# ID группы с ботом
|
||||||
|
# * При работе на странице укажите 0
|
||||||
|
group_id: ID
|
||||||
|
|
||||||
|
# Отправка сообщений
|
||||||
|
messages:
|
||||||
|
# Шанс генерации текста
|
||||||
|
chance: 100
|
||||||
|
# Максимальное кол-во символов в тексте
|
||||||
|
# * Если символов в тексте будет больше данного значения сообщение не отправится
|
||||||
|
symbols_limit: 250
|
||||||
|
# ID бесед, в которых бот активен
|
||||||
|
# Чтобы бот работал везде поставьте значение <<all>>
|
||||||
|
active_chats: all
|
||||||
|
# Форматирование текста
|
||||||
|
format: false
|
||||||
|
# Имитация написания текста
|
||||||
|
# * При включении появляется задержка перед отправкой сообщений
|
||||||
|
writing_imitation: true
|
||||||
|
|
||||||
|
# Генерация текста
|
||||||
|
generation:
|
||||||
|
# Минимальное кол-во слов в генерируемом тексте
|
||||||
|
min_words: 5
|
||||||
|
|
||||||
|
# Настройки БД
|
||||||
|
database:
|
||||||
|
# Сохранение сообщений пользователей
|
||||||
|
# * Одинаковые строки не сохраняются
|
||||||
|
data_save: true
|
||||||
95
core/core.js
Normal file
95
core/core.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/** __ __ __ ___ ___
|
||||||
|
* / / \ |__) | |__ \_/
|
||||||
|
* \__ \__/ | \ | |___ / \
|
||||||
|
*
|
||||||
|
* @copyright © 2021-2022 hepller
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Импорт зависимостей
|
||||||
|
import { VK } from 'vk-io'
|
||||||
|
import YAML from 'yaml'
|
||||||
|
import Markov from 'markov-generator'
|
||||||
|
|
||||||
|
// Импорт системных модулей
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
|
||||||
|
// Импорт компонентов ядра
|
||||||
|
import Logger from './logger.js'
|
||||||
|
import Utils from './utils.js'
|
||||||
|
import DataManager from './data_manager.js'
|
||||||
|
|
||||||
|
// Парсинг конфигураций проекта
|
||||||
|
const config = YAML.parse(readFileSync('config.yml', 'utf8'))
|
||||||
|
const project = JSON.parse(readFileSync('package.json', 'utf8'))
|
||||||
|
|
||||||
|
// Инициализация vk-io
|
||||||
|
const vk = new VK(
|
||||||
|
|
||||||
|
// Определение места работы бота (группа / страница)
|
||||||
|
config.longpoll.group_id
|
||||||
|
|
||||||
|
// При работе в группе
|
||||||
|
? {token: config.longpoll.token, v: config.longpoll.version, pollingGroupId: config.longpoll.group_id}
|
||||||
|
|
||||||
|
// При работе на странице
|
||||||
|
: {token: config.longpoll.token, v: config.longpoll.version}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Сообщение о запуске
|
||||||
|
Logger.info(`Cortex Bot v${project.version} запущен`)
|
||||||
|
|
||||||
|
// Сообщение о подключении к VK API
|
||||||
|
Logger.info('Подключение к VK Long Poll ...')
|
||||||
|
|
||||||
|
// Старт получения событий ВК
|
||||||
|
vk.updates.startPolling().then(() => Logger.info('VK Long Poll подключен'))
|
||||||
|
|
||||||
|
// Обработчик сообщений ВК
|
||||||
|
vk.updates.on('message_new', async ctx => {
|
||||||
|
|
||||||
|
// Игнорирование лишних сообщений
|
||||||
|
if (!ctx.isChat || ctx.isOutbox) return
|
||||||
|
if (config.messages.active_chats != 'all' && !config.messages.active_chats.includes(ctx.chatId)) return
|
||||||
|
|
||||||
|
// Логирование сообщений
|
||||||
|
Logger.info(`#${ctx.chatId} ${ctx.senderId} '->' ${ctx.text || '<no_text>'} ${ctx?.attachments}`)
|
||||||
|
|
||||||
|
// Получение чата из БД
|
||||||
|
ctx.chat = DataManager.getChat(ctx.chatId)
|
||||||
|
|
||||||
|
// Запись сообщения в БД
|
||||||
|
if (!ctx.isGroup && ctx.text && config.database.data_save && !ctx.chat.data.includes(ctx.text)) DataManager.writeChatData(ctx.chatId, ctx.text)
|
||||||
|
|
||||||
|
// Генерация сообщения с определенным шансом
|
||||||
|
if (Utils.getWithChance(config.messages.chance)) {
|
||||||
|
|
||||||
|
// Сообщение в консоль о генерации текста
|
||||||
|
Logger.info(`#${ctx.chatId} Генерация текста ...`)
|
||||||
|
|
||||||
|
// Генерация текста
|
||||||
|
// * Если данных будет недостаточно произойдет ошибка <<Maximum call stack size exceeded>>
|
||||||
|
const sentence = new Markov({input: ctx.chat.data, minLength: config.generation.min_words}).makeChain()
|
||||||
|
|
||||||
|
// Форматирование текста (если включено)
|
||||||
|
const formatted_sentence = config.messages.format ? Utils.formatString(sentence) : sentence
|
||||||
|
|
||||||
|
// Предупреждение при превышении лимита символов
|
||||||
|
if (formatted_sentence.split('').length > config.messages.symbols_limit) return Logger.warn(`#${ctx.chatId} Превышен лимит символов (${formatted_sentence.split('').length}/${config.messages.symbols_limit})`)
|
||||||
|
|
||||||
|
// Установка статуса набора текста
|
||||||
|
ctx.setActivity()
|
||||||
|
|
||||||
|
// Задержка перед отправкой сообщений для имитации написания (если включено)
|
||||||
|
// * Высчитывается на основе количества символов в сгенерированном тексте
|
||||||
|
if (config.messages.writing_imitation) await Utils.sleep(formatted_sentence.split('').length * 2 + '00')
|
||||||
|
|
||||||
|
// Отправка сообщения
|
||||||
|
ctx.send(formatted_sentence).then(() => Logger.info(`#${ctx.chatId} Сообщение сгенерировано ("${sentence}")`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Логирование неперехваченных исключений
|
||||||
|
process.on('uncaughtException', error => Logger.error(error.stack))
|
||||||
|
|
||||||
|
// Логирование необработанных ошибок (promise error)
|
||||||
|
process.on('unhandledRejection', error => Logger.error(error.stack))
|
||||||
56
core/data_manager.js
Normal file
56
core/data_manager.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/** __ __ __ ___ ___
|
||||||
|
* / / \ |__) | |__ \_/
|
||||||
|
* \__ \__/ | \ | |___ / \
|
||||||
|
*
|
||||||
|
* @copyright © 2021-2022 hepller
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Импорт системных модулей
|
||||||
|
import { readFileSync, writeFileSync } from 'fs'
|
||||||
|
|
||||||
|
// Получение БД
|
||||||
|
const db = JSON.parse(readFileSync('data.json', 'utf8'))
|
||||||
|
|
||||||
|
/** Менеджер БД */
|
||||||
|
export default class DataManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает чат из БД
|
||||||
|
* @param {number} chat_id ID чата
|
||||||
|
*/
|
||||||
|
static getChat(chat_id) {
|
||||||
|
|
||||||
|
// Поиск чата в БД
|
||||||
|
let chat = db.chats.find(chat => chat.id == chat_id)
|
||||||
|
|
||||||
|
// Добавление чата в БД при его отсутствии
|
||||||
|
if (!chat) {
|
||||||
|
|
||||||
|
// Добавление
|
||||||
|
db.chats.push({id: chat_id, data: []})
|
||||||
|
|
||||||
|
// Запись
|
||||||
|
writeFileSync('data.json', JSON.stringify(db, null, '\t'))
|
||||||
|
|
||||||
|
// Обновление значения переменной
|
||||||
|
chat = db.chats.find(chat => chat.id == chat_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращение объекта чата
|
||||||
|
return chat
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Записывает новые данные для чата
|
||||||
|
* @param {number} chat_id ID чата
|
||||||
|
* @param {string} data Данные
|
||||||
|
*/
|
||||||
|
static writeChatData(chat_id, data) {
|
||||||
|
|
||||||
|
// Добавление данных в БД
|
||||||
|
DataManager.getChat(chat_id).data.push(data)
|
||||||
|
|
||||||
|
// Запись БД
|
||||||
|
writeFileSync('data.json', JSON.stringify(db, null, '\t'))
|
||||||
|
}
|
||||||
|
}
|
||||||
37
core/logger.js
Normal file
37
core/logger.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/** __ __ __ ___ ___
|
||||||
|
* / / \ |__) | |__ \_/
|
||||||
|
* \__ \__/ | \ | |___ / \
|
||||||
|
*
|
||||||
|
* @copyright © 2021-2022 hepller
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Импорт компонентов ядра
|
||||||
|
import Utils from './utils.js'
|
||||||
|
|
||||||
|
/** Логер */
|
||||||
|
export default class Logger {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выводит в лог информацию
|
||||||
|
* @param {string} text Текст сообщения
|
||||||
|
*/
|
||||||
|
static info(text) {
|
||||||
|
console.log(`[${Utils.getTimeString()} INFO]: ${text}`.replace(/\n/g, '\\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выводит в лог предупреждения
|
||||||
|
* @param {string} text Текст предупреждения
|
||||||
|
*/
|
||||||
|
static warn(text) {
|
||||||
|
console.log(`\u001B[33m[${Utils.getTimeString()} WARN]: ${text}\u001B[0m`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выводит в лог ошибки
|
||||||
|
* @param {string} text Текст ошибки
|
||||||
|
*/
|
||||||
|
static error(text) {
|
||||||
|
console.log(`\u001B[31m[${Utils.getTimeString()} ERRO]: ${text}\u001B[0m`)
|
||||||
|
}
|
||||||
|
}
|
||||||
53
core/utils.js
Normal file
53
core/utils.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/** __ __ __ ___ ___
|
||||||
|
* / / \ |__) | |__ \_/
|
||||||
|
* \__ \__/ | \ | |___ / \
|
||||||
|
*
|
||||||
|
* @copyright © 2021-2022 hepller
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Функции */
|
||||||
|
export default class Utils {
|
||||||
|
|
||||||
|
/** Возвращает строку со временем в формате `hh:mm:ss` */
|
||||||
|
static getTimeString() {
|
||||||
|
|
||||||
|
// Переменные
|
||||||
|
let hours = new Date().getHours()
|
||||||
|
let minutes = new Date().getMinutes()
|
||||||
|
let seconds = new Date().getSeconds()
|
||||||
|
|
||||||
|
// Исправление одинарных символов
|
||||||
|
if (hours < 10) hours = `0${hours}`
|
||||||
|
if (minutes < 10) minutes = `0${minutes}`
|
||||||
|
if (seconds < 10) seconds = `0${seconds}`
|
||||||
|
|
||||||
|
// Возвращение строки
|
||||||
|
return `${hours}:${minutes}:${seconds}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает `true` с определенным шансом
|
||||||
|
* @param {number} likelihood Вероятность (%)
|
||||||
|
*/
|
||||||
|
static getWithChance(likelihood) {
|
||||||
|
|
||||||
|
// Замена некорректных значений
|
||||||
|
if (likelihood < 0) likelihood = 0
|
||||||
|
if (likelihood > 100) likelihood = 100
|
||||||
|
|
||||||
|
// Возвращение значения
|
||||||
|
return Math.random() * 100 < likelihood
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Останавливает выполнение функции на указанное время
|
||||||
|
* @param {number} ms Время остановки (в миллисекундах)
|
||||||
|
*/
|
||||||
|
static sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
|
||||||
|
static formatString(text) {
|
||||||
|
return text.toLowerCase().charAt(0).toUpperCase() + text.slice(1) + '.'
|
||||||
|
}
|
||||||
|
}
|
||||||
2
license
2
license
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright © 2021 hepller
|
Copyright © 2021-2022 hepller
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -1,20 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "cortex-bot",
|
"name": "cortex-bot",
|
||||||
"version": "0.7.3",
|
"version": "0.9.0",
|
||||||
"description": "vk text generator bot",
|
"description": "VK delirium generator bot",
|
||||||
"main": "bot.js",
|
"main": "core/core.js",
|
||||||
|
"type": "module",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/hepller/cortex-bot.git"
|
"url": "git+https://github.com/hepller/cortex-bot.git"
|
||||||
},
|
},
|
||||||
"author": "hepller",
|
"author": "hepller",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/hepller/cortex-bot/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/hepller/cortex-bot#readme",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"markov-generator": "^1.2.3",
|
"markov-generator": "^1.2.3",
|
||||||
"vk-io": "^4.4.0"
|
"vk-io": "^4.4.0",
|
||||||
|
"yaml": "^1.10.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
readme.md
41
readme.md
@@ -1,43 +1,20 @@
|
|||||||
# 🧠 Cortex Bot
|
# 🧠 Cortex Bot 
|
||||||
|
|
||||||
Бот ВКонтакте для генерации сообщений с помощью цепей Маркова
|
> Бот [ВКонтакте](https://vk.com) для генерации сообщений с помощью цепей Маркова
|
||||||
|
|
||||||
---
|

|
||||||
|

|
||||||
|
|
||||||
## ⚙ Конфигурация
|
## ⚙ Конфигурация
|
||||||
|
|
||||||
- `token` - Токен доступа VK API
|
Все настройки бота и их описания находятся в файле `config.yml`
|
||||||
- `chance` - Шанс генерации сообщения
|
|
||||||
- `min_words` - Минимальное кол-во слов в сгенерированном сообщении
|
|
||||||
- `max_symbols` - Максимальное кол-во символов в сообщении
|
|
||||||
- `format` - Форматирование текста (заглавная буква и точка в конце)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💾 Данные
|
## 💾 Данные
|
||||||
|
|
||||||
Перед началом использования загрузите свой датасет с достаточным объемом текста в файл `data.json`
|
Перед началом полноценной работы боту нужно достаточное количество строк для генерации текста. При недостатке данных для генерации будет возникать ошибка `Maximum call stack size exceeded`
|
||||||
|
|
||||||
**Пример небольшого датасета:**
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
"Я вот подумал. Если быстро заморозить человека, то в какой момент он умрет?",
|
|
||||||
"есть еще три:\n\nгносеоногия (эпистемология) - о познании",
|
|
||||||
"логика - о мышлении (из нее как ы слеуют большинство наук естественных и абстрактных (базируются на логических законах)",
|
|
||||||
"ну вот тут открытый же вопрос. С т.з. медицины: есть критерии естественной смерти, которые гуглятся",
|
|
||||||
"а с т.з. онтологии - тут может готового ответа и не быть. В этом как бы и отличие - очень часто нет готовых и четких ответов",
|
|
||||||
"ну, например, когда между нейронами сигналы перестанут бегать",
|
|
||||||
"Читаю, но как бы там все неолнозначно",
|
|
||||||
"клеточная смерть - не очень катит, т.к. те же волосы и ногти долго еще растут",
|
|
||||||
"Пульса нету, сердечной активности нету"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
_При недостатке данных для генерации будет возникать ошибка `Maximum call stack size exceeded`_
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 Лицензия
|
## 📄 Лицензия
|
||||||
|
|
||||||
Проект распространяется под лицензией **MIT**
|
Copyright © 2021-2022 [hepller](https://github.com/hepller)
|
||||||
|
|
||||||
|
Проект распространяется под лицензией [MIT](license)
|
||||||
|
|||||||
43
utils.js
43
utils.js
@@ -1,43 +0,0 @@
|
|||||||
/** __ __ __ ___ ___ __ __ ___
|
|
||||||
* / ' / \ |__) | |__ \_/ |__) / \ |
|
|
||||||
* \__. \__/ | \ | |___ / \ |__) \__/ |
|
|
||||||
*
|
|
||||||
* @copyright © 2021 hepller
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Функции */
|
|
||||||
module.exports = class Utils {
|
|
||||||
|
|
||||||
/** Получает текущее время в формате `hh:mm:ss` */
|
|
||||||
static getTimeString() {
|
|
||||||
|
|
||||||
// Переменные
|
|
||||||
let hours = new Date().getHours()
|
|
||||||
let minutes = new Date().getMinutes()
|
|
||||||
let seconds = new Date().getSeconds()
|
|
||||||
|
|
||||||
// Исправление одинарных символов
|
|
||||||
if (hours < 10) hours = `0${hours}`
|
|
||||||
if (minutes < 10) minutes = `0${minutes}`
|
|
||||||
if (seconds < 10) seconds = `0${seconds}`
|
|
||||||
|
|
||||||
// Возвращение строки
|
|
||||||
return `${hours}:${minutes}:${seconds}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Возвращает `true` с определенным шансом
|
|
||||||
* @param {number} likelihood Вероятность (%)
|
|
||||||
*/
|
|
||||||
static getWithChance(likelihood) {
|
|
||||||
return Math.random() * 100 < likelihood
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Форматирует текст (устанавливает заглавную букву и точку в конце)
|
|
||||||
* @param {string} text Текст для форматирования
|
|
||||||
*/
|
|
||||||
static formatText(text) {
|
|
||||||
return text.charAt(0).toUpperCase() + text.slice(1) + '.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user