mirror of
https://github.com/QuasarApp/qTbot.git
synced 2025-05-20 17:09:33 +00:00
adde first implementation for the keyboard support
This commit is contained in:
parent
f984355a33
commit
09333936f2
@ -6,17 +6,21 @@
|
|||||||
//#
|
//#
|
||||||
|
|
||||||
#include "telegramsendmsg.h"
|
#include "telegramsendmsg.h"
|
||||||
|
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
namespace qTbot {
|
namespace qTbot {
|
||||||
|
|
||||||
TelegramSendMsg::TelegramSendMsg(const QVariant &chatId,
|
TelegramSendMsg::TelegramSendMsg(const QVariant &chatId,
|
||||||
const QString &text,
|
const QString &text,
|
||||||
|
const QMap<QString, QJsonObject> &extraObjects,
|
||||||
unsigned long long replyToMessageId,
|
unsigned long long replyToMessageId,
|
||||||
bool markdown,
|
bool markdown,
|
||||||
bool disableWebPagePreview):
|
bool disableWebPagePreview)
|
||||||
|
:
|
||||||
TelegramSingleRquest("sendMessage")
|
TelegramSingleRquest("sendMessage")
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
QMap<QString, QVariant> args {{"chat_id", chatId}, {"text", text}};
|
QMap<QString, QVariant> args {{"chat_id", chatId}, {"text", text}};
|
||||||
|
|
||||||
if (replyToMessageId) {
|
if (replyToMessageId) {
|
||||||
@ -31,6 +35,10 @@ TelegramSendMsg::TelegramSendMsg(const QVariant &chatId,
|
|||||||
args["disable_web_page_preview"] = disableWebPagePreview;
|
args["disable_web_page_preview"] = disableWebPagePreview;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (auto it = extraObjects.begin(); it != extraObjects.end(); it = std::next(it)) {
|
||||||
|
args[it.key()] = QJsonDocument(it.value()).toJson();
|
||||||
|
}
|
||||||
|
|
||||||
setArgs(args);
|
setArgs(args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,11 @@ class QTBOT_EXPORT TelegramSendMsg: public TelegramSingleRquest
|
|||||||
public:
|
public:
|
||||||
TelegramSendMsg(const QVariant& chatId,
|
TelegramSendMsg(const QVariant& chatId,
|
||||||
const QString& text,
|
const QString& text,
|
||||||
|
const QMap<QString, QJsonObject>& extraObjects = {},
|
||||||
unsigned long long replyToMessageId = 0,
|
unsigned long long replyToMessageId = 0,
|
||||||
bool markdown = true,
|
bool markdown = true,
|
||||||
bool disableWebPagePreview = false);
|
bool disableWebPagePreview = false
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#endif // TELEGRAMSENDMSG_H
|
#endif // TELEGRAMSENDMSG_H
|
||||||
|
@ -38,7 +38,7 @@ void IBot::incomeNewUpdate(const QSharedPointer<iUpdate> &message) {
|
|||||||
_processed.insert(id);
|
_processed.insert(id);
|
||||||
_notProcessedUpdates[id] = message;
|
_notProcessedUpdates[id] = message;
|
||||||
|
|
||||||
emit sigReceiveUpdate(message);
|
handleIncomeNewUpdate(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +84,10 @@ QString IBot::defaultFileStorageLocation() const {
|
|||||||
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
|
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void IBot::handleIncomeNewUpdate(const QSharedPointer<iUpdate> & message) {
|
||||||
|
emit sigReceiveUpdate(message);
|
||||||
|
}
|
||||||
|
|
||||||
void IBot::doRemoveFinishedRequests() {
|
void IBot::doRemoveFinishedRequests() {
|
||||||
for (auto address: qAsConst(_toRemove)) {
|
for (auto address: qAsConst(_toRemove)) {
|
||||||
_replayStorage.remove(address);
|
_replayStorage.remove(address);
|
||||||
|
@ -184,6 +184,12 @@ protected:
|
|||||||
* @return default file storage.
|
* @return default file storage.
|
||||||
*/
|
*/
|
||||||
virtual QString defaultFileStorageLocation() const;
|
virtual QString defaultFileStorageLocation() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief handleIncomeNewUpdate This method just emit the sigReceiveUpdate signal.
|
||||||
|
* @note you may override this method for filter the sigReceiveUpdate signal or for handling new updates.
|
||||||
|
*/
|
||||||
|
virtual void handleIncomeNewUpdate(const QSharedPointer<iUpdate>& );
|
||||||
signals:
|
signals:
|
||||||
/**
|
/**
|
||||||
* @brief sigReceiveUpdate emit when but receive any updates from users.
|
* @brief sigReceiveUpdate emit when but receive any updates from users.
|
||||||
|
@ -20,6 +20,8 @@
|
|||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
#include <qTbot/messages/telegramfile.h>
|
#include <qTbot/messages/telegramfile.h>
|
||||||
#include <qTbot/messages/telegramfile.h>
|
#include <qTbot/messages/telegramfile.h>
|
||||||
@ -28,6 +30,7 @@
|
|||||||
namespace qTbot {
|
namespace qTbot {
|
||||||
|
|
||||||
ITelegramBot::ITelegramBot() {
|
ITelegramBot::ITelegramBot() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ITelegramBot::~ITelegramBot() {
|
ITelegramBot::~ITelegramBot() {
|
||||||
@ -60,6 +63,7 @@ bool ITelegramBot::sendMessage(const QVariant &chatId, const QString &text) {
|
|||||||
|
|
||||||
bool ITelegramBot::sendSpecificMessage(const QVariant & chatId,
|
bool ITelegramBot::sendSpecificMessage(const QVariant & chatId,
|
||||||
const QString &text,
|
const QString &text,
|
||||||
|
const QMap<QString, QJsonObject> &extraObjects,
|
||||||
unsigned long long replyToMessageId,
|
unsigned long long replyToMessageId,
|
||||||
bool markdown,
|
bool markdown,
|
||||||
bool disableWebPagePreview) {
|
bool disableWebPagePreview) {
|
||||||
@ -73,6 +77,90 @@ bool ITelegramBot::sendSpecificMessage(const QVariant & chatId,
|
|||||||
|
|
||||||
auto msg = QSharedPointer<TelegramSendMsg>::create(chatId,
|
auto msg = QSharedPointer<TelegramSendMsg>::create(chatId,
|
||||||
text,
|
text,
|
||||||
|
extraObjects,
|
||||||
|
replyToMessageId,
|
||||||
|
markdown,
|
||||||
|
disableWebPagePreview);
|
||||||
|
|
||||||
|
return bool(sendRequest(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ITelegramBot::sendSpecificMessage(const QVariant &chatId,
|
||||||
|
const QString &text,
|
||||||
|
const QList<QString> &keyboard,
|
||||||
|
bool onTimeKeyboard,
|
||||||
|
bool autoResizeKeyboard,
|
||||||
|
unsigned long long replyToMessageId,
|
||||||
|
bool markdown,
|
||||||
|
bool disableWebPagePreview) {
|
||||||
|
|
||||||
|
if (!chatId.isValid() || chatId.isNull())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMap<QString, QJsonObject> extraObjects;
|
||||||
|
QJsonObject keyboardJson;
|
||||||
|
QJsonArray keyboardArray;
|
||||||
|
for (auto it = keyboard.begin(); it != keyboard.end(); it = std::next(it)) {
|
||||||
|
auto&& callBackKey = QString("callback_data_%0").arg(rand());
|
||||||
|
keyboardArray.push_back(QJsonObject{ {"text", *it} });
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboardJson["keyboard"] = keyboardArray;
|
||||||
|
|
||||||
|
keyboardJson["resize_keyboard"] = autoResizeKeyboard;
|
||||||
|
keyboardJson["one_time_keyboard"] = onTimeKeyboard;
|
||||||
|
|
||||||
|
extraObjects["reply_markup"] = keyboardJson;
|
||||||
|
|
||||||
|
auto msg = QSharedPointer<TelegramSendMsg>::create(chatId,
|
||||||
|
text,
|
||||||
|
extraObjects,
|
||||||
|
replyToMessageId,
|
||||||
|
markdown,
|
||||||
|
disableWebPagePreview);
|
||||||
|
|
||||||
|
return bool(sendRequest(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ITelegramBot::sendSpecificMessage(const QVariant &chatId,
|
||||||
|
const QString &text,
|
||||||
|
const QMap<QString, std::function<void()> > &keyboard,
|
||||||
|
bool onTimeKeyboard,
|
||||||
|
bool autoResizeKeyboard,
|
||||||
|
unsigned long long replyToMessageId,
|
||||||
|
bool markdown,
|
||||||
|
bool disableWebPagePreview) {
|
||||||
|
|
||||||
|
if (!chatId.isValid() || chatId.isNull())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMap<QString, QJsonObject> extraObjects;
|
||||||
|
QJsonObject keyboardJson;
|
||||||
|
QJsonArray keyboardArray;
|
||||||
|
for (auto it = keyboard.begin(); it != keyboard.end(); it = std::next(it)) {
|
||||||
|
auto&& callBackKey = QString("callback_data_%0").arg(rand());
|
||||||
|
keyboardArray.push_back(QJsonObject{ {"text", it.key()}, {"callback_data", callBackKey } });
|
||||||
|
_handleButtons[callBackKey] = {it.value(), onTimeKeyboard};
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboardJson["inline_keyboard"] = keyboardArray;
|
||||||
|
|
||||||
|
keyboardJson["resize_keyboard"] = autoResizeKeyboard;
|
||||||
|
keyboardJson["one_time_keyboard"] = onTimeKeyboard;
|
||||||
|
|
||||||
|
extraObjects["reply_markup"] = keyboardJson;
|
||||||
|
|
||||||
|
auto msg = QSharedPointer<TelegramSendMsg>::create(chatId,
|
||||||
|
text,
|
||||||
|
extraObjects,
|
||||||
replyToMessageId,
|
replyToMessageId,
|
||||||
markdown,
|
markdown,
|
||||||
disableWebPagePreview);
|
disableWebPagePreview);
|
||||||
@ -180,6 +268,24 @@ void ITelegramBot::onRequestError(const QSharedPointer<TelegramUpdateAnswer> &an
|
|||||||
arg(ansverWithError->errorDescription());
|
arg(ansverWithError->errorDescription());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ITelegramBot::handleIncomeNewUpdate(const QSharedPointer<iUpdate> & update) {
|
||||||
|
IBot::handleIncomeNewUpdate(update);
|
||||||
|
|
||||||
|
if (auto&& tupdate = update.dynamicCast<TelegramUpdate>()) {
|
||||||
|
if (auto&& msg = tupdate->message()) {
|
||||||
|
auto &&handleButtonKey = msg->text();
|
||||||
|
|
||||||
|
auto [cb, isOneTimeKeyboard] = _handleButtons.value(handleButtonKey);
|
||||||
|
|
||||||
|
cb();
|
||||||
|
|
||||||
|
if (isOneTimeKeyboard) {
|
||||||
|
_handleButtons.remove(handleButtonKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ITelegramBot::handleLogin() {
|
void ITelegramBot::handleLogin() {
|
||||||
|
|
||||||
if (_loginReplay) {
|
if (_loginReplay) {
|
||||||
|
@ -37,16 +37,17 @@ public:
|
|||||||
bool sendMessage(const QVariant &chatId, const QString& text) override;
|
bool sendMessage(const QVariant &chatId, const QString& text) override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Sends a message to a chat or channel.
|
* @brief Sends a message to a chat or channel with optional additional objects.
|
||||||
*
|
*
|
||||||
* The `sendMessage` method is used to send a message to a chat. This method allows
|
* The `sendSpecificMessage` method is used to send a message to a chat. This method allows
|
||||||
* sending text messages to chats and channels. You can also specify additional
|
* sending text messages to chats and channels. You can also specify additional
|
||||||
* parameters such as text formatting, a reply to a specific message, and disabling
|
* parameters such as text formatting, a reply to a specific message, disabling
|
||||||
* web page preview.
|
* web page preview, and including extra JSON objects in the message.
|
||||||
*
|
*
|
||||||
* @param chatId The identifier of the chat or channel to which the message will be sent.
|
* @param chatId The identifier of the chat or channel to which the message will be sent.
|
||||||
* It can be a string, number, or another valid data type containing the chat identifier.
|
* It can be a string, number, or another valid data type containing the chat identifier.
|
||||||
* @param text The text of the message to be sent.
|
* @param text The text of the message to be sent.
|
||||||
|
* @param extraObjects A map containing additional JSON objects to include in the message.
|
||||||
* @param replyToMessageId The identifier of the message to which you want to reply. If you want to send
|
* @param replyToMessageId The identifier of the message to which you want to reply. If you want to send
|
||||||
* a regular message without a reply, leave this field as 0.
|
* a regular message without a reply, leave this field as 0.
|
||||||
* @param markdown A flag indicating whether the message text should be formatted using Markdown.
|
* @param markdown A flag indicating whether the message text should be formatted using Markdown.
|
||||||
@ -60,21 +61,119 @@ public:
|
|||||||
* Usage examples:
|
* Usage examples:
|
||||||
* @code
|
* @code
|
||||||
* // Send a plain text message
|
* // Send a plain text message
|
||||||
* bool result = sendMessage(chatId, "Hello, world!");
|
* bool result = sendSpecificMessage(chatId, "Hello, world!");
|
||||||
*
|
*
|
||||||
* // Send a formatted text message as a reply to another message
|
* // Send a formatted text message as a reply to another message with additional JSON objects
|
||||||
* bool result = sendMessage(chatId, "This is a reply to your message.", messageId);
|
* QMap<QString, QJsonObject> additionalObjects = {
|
||||||
|
* {"key1", {"value1"}},
|
||||||
|
* {"key2", {"value2"}}
|
||||||
|
* };
|
||||||
|
* bool result = sendSpecificMessage(chatId, "This is a reply with additional objects.", additionalObjects, messageId);
|
||||||
*
|
*
|
||||||
* // Send a message with disabled web page preview
|
* // Send a message with disabled web page preview
|
||||||
* bool result = sendMessage(chatId, "Check)", 0, true, true);
|
* bool result = sendSpecificMessage(chatId, "Check)", {}, 0, true, true);
|
||||||
* @endcode
|
* @endcode
|
||||||
*/
|
*/
|
||||||
bool sendSpecificMessage(const QVariant &chatId,
|
bool sendSpecificMessage(const QVariant &chatId,
|
||||||
const QString& text,
|
const QString& text,
|
||||||
|
const QMap<QString, QJsonObject> &extraObjects = {},
|
||||||
unsigned long long replyToMessageId = 0,
|
unsigned long long replyToMessageId = 0,
|
||||||
bool markdown = true,
|
bool markdown = true,
|
||||||
bool disableWebPagePreview = false);
|
bool disableWebPagePreview = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sends a message to a chat or channel with an optional custom keyboard **inline keyboard**.
|
||||||
|
*
|
||||||
|
* The `sendSpecificMessage` method is used to send a message to a chat. This method allows
|
||||||
|
* sending text messages to chats and channels. You can also specify additional
|
||||||
|
* parameters such as text formatting, a reply to a specific message, disabling
|
||||||
|
* web page preview, and providing a custom keyboard for interaction.
|
||||||
|
*
|
||||||
|
* @param chatId The identifier of the chat or channel to which the message will be sent.
|
||||||
|
* It can be a string, number, or another valid data type containing the chat identifier.
|
||||||
|
* @param text The text of the message to be sent.
|
||||||
|
* @param keyboard A map containing buttons and corresponding callback functions for a custom keyboard.
|
||||||
|
* @param replyToMessageId The identifier of the message to which you want to reply. If you want to send
|
||||||
|
* a regular message without a reply, leave this field as 0.
|
||||||
|
* @param markdown A flag indicating whether the message text should be formatted using Markdown.
|
||||||
|
* If `true`, the text will be formatted using Markdown syntax. If `false`, the text will be
|
||||||
|
* sent as plain text.
|
||||||
|
* @param disableWebPagePreview A flag indicating whether to disable web page preview in the message.
|
||||||
|
* If `true`, web page preview will be disabled.
|
||||||
|
*
|
||||||
|
* @return `true` if the message was successfully sent, and `false` otherwise.
|
||||||
|
*
|
||||||
|
* Usage examples:
|
||||||
|
* @code
|
||||||
|
* // Send a plain text message
|
||||||
|
* bool result = sendSpecificMessage(chatId, "Hello, world!");
|
||||||
|
*
|
||||||
|
* // Send a formatted text message as a reply to another message with a custom keyboard
|
||||||
|
* QMap<QString, std::function<void>> customKeyboard = {
|
||||||
|
* {"Button 1", [](){ Callback function for Button 1 }},
|
||||||
|
* {"Button 2", [](){ Callback function for Button 2 }}
|
||||||
|
* };
|
||||||
|
* bool result = sendSpecificMessage(chatId, "This is a reply with a custom keyboard.", customKeyboard, true, messageId);
|
||||||
|
*
|
||||||
|
* // Send a message with disabled web page preview
|
||||||
|
* bool result = sendSpecificMessage(chatId, "Check)", {}, false, 0, true, true);
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
bool sendSpecificMessage(const QVariant &chatId,
|
||||||
|
const QString& text,
|
||||||
|
const QMap<QString, std::function<void()>> &keyboard,
|
||||||
|
bool onTimeKeyboard = false,
|
||||||
|
bool autoResizeKeyboard = false,
|
||||||
|
unsigned long long replyToMessageId = 0,
|
||||||
|
bool markdown = true,
|
||||||
|
bool disableWebPagePreview = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief sendSpecificMessage sends a specific message with a keyboard to a chat.
|
||||||
|
*
|
||||||
|
* This method sends a specific message with a keyboard to a chat. It allows
|
||||||
|
* you to customize various aspects of the message, such as the text, keyboard,
|
||||||
|
* and other options.
|
||||||
|
*
|
||||||
|
* @param chatId The identifier of the chat to which the message will be sent.
|
||||||
|
* @param text The text of the message to be sent.
|
||||||
|
* @param keyboard A list of text buttons for the keyboard placed below the message.
|
||||||
|
* @param onTimeKeyboard A flag indicating whether the keyboard should be hidden after
|
||||||
|
* selecting one of the buttons (one-time keyboard).
|
||||||
|
* @param autoResizeKeyboard A flag indicating whether the keyboard should automatically
|
||||||
|
* adjust its size based on the number of buttons.
|
||||||
|
* @param replyToMessageId The identifier of the message to which this message is a reply
|
||||||
|
* (if applicable).
|
||||||
|
* @param markdown A flag indicating whether Markdown formatting is supported in the message text.
|
||||||
|
* @param disableWebPagePreview A flag indicating whether to disable web page preview in the message.
|
||||||
|
*
|
||||||
|
* @return Returns true if the message was successfully sent, and false in case of an error.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* @code
|
||||||
|
* bool result = sendSpecificMessage(chatId, "Hello, how are you?", {"Button 1", "Button 2"}, true, true, 0, true, false);
|
||||||
|
* if (result) {
|
||||||
|
* qDebug() << "Message sent successfully.";
|
||||||
|
* } else {
|
||||||
|
* qDebug() << "Error sending the message.";
|
||||||
|
* }
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* @note If the onTimeKeyboard parameter is set to true, the keyboard will be hidden after selecting
|
||||||
|
* one of the buttons, and the user won't be able to use it again.
|
||||||
|
* @note If the autoResizeKeyboard parameter is set to true, the keyboard will automatically adjust
|
||||||
|
* its size based on the number of buttons.
|
||||||
|
* @note The markdown and disableWebPagePreview parameters allow you to configure text formatting
|
||||||
|
* and web page preview options in the message.
|
||||||
|
*/
|
||||||
|
bool sendSpecificMessage(const QVariant &chatId,
|
||||||
|
const QString& text,
|
||||||
|
const QList<QString> &keyboard,
|
||||||
|
bool onTimeKeyboard = false,
|
||||||
|
bool autoResizeKeyboard = false,
|
||||||
|
unsigned long long replyToMessageId = 0,
|
||||||
|
bool markdown = true,
|
||||||
|
bool disableWebPagePreview = false);
|
||||||
|
|
||||||
[[nodiscard("do not forget to save shared pointer of file handler, because it's will not save inner bot object.")]]
|
[[nodiscard("do not forget to save shared pointer of file handler, because it's will not save inner bot object.")]]
|
||||||
QSharedPointer<iFile> getFile(const QString& fileId, iFile::Type fileType = iFile::Type::Ram) override;
|
QSharedPointer<iFile> getFile(const QString& fileId, iFile::Type fileType = iFile::Type::Ram) override;
|
||||||
@ -178,6 +277,9 @@ protected:
|
|||||||
* @param ansverWithError - This is ansver object with error descriptions. and codes errors.
|
* @param ansverWithError - This is ansver object with error descriptions. and codes errors.
|
||||||
*/
|
*/
|
||||||
virtual void onRequestError(const QSharedPointer<TelegramUpdateAnswer>& ansverWithError) const;
|
virtual void onRequestError(const QSharedPointer<TelegramUpdateAnswer>& ansverWithError) const;
|
||||||
|
|
||||||
|
void handleIncomeNewUpdate(const QSharedPointer<iUpdate> &) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handleLogin();
|
void handleLogin();
|
||||||
void handleLoginErr(QNetworkReply::NetworkError err);
|
void handleLoginErr(QNetworkReply::NetworkError err);
|
||||||
@ -190,11 +292,10 @@ private:
|
|||||||
unsigned long long _id = 0;
|
unsigned long long _id = 0;
|
||||||
QString _username;
|
QString _username;
|
||||||
QSharedPointer<QNetworkReply> _loginReplay;
|
QSharedPointer<QNetworkReply> _loginReplay;
|
||||||
|
QMap<QString, QPair<std::function<void()>, bool>> _handleButtons;
|
||||||
|
|
||||||
QHash<QString, QSharedPointer<TelegramFile>> _filesMetaInfo;
|
QHash<QString, QSharedPointer<TelegramFile>> _filesMetaInfo;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#endif // ITELEGRAMBOT_H
|
#endif // ITELEGRAMBOT_H
|
||||||
|
Loading…
x
Reference in New Issue
Block a user