mirror of
https://github.com/uowuo/abaddon.git
synced 2024-11-10 06:00:10 +00:00
show pending/failed messages
css changes: - added .failed - added .pending
This commit is contained in:
parent
88c91495c3
commit
77b43f0f24
@ -92,6 +92,8 @@ Or, do steps 1 and 2, and open CMakeLists.txt in Visual Studio if `vcpkg integra
|
||||
.message-container-avatar - Avatar for a user in a message
|
||||
.message-container-extra - Label containing BOT/Webhook
|
||||
.message-text - The text of a user message
|
||||
.pending - Extra class of .message-text for messages pending to be sent
|
||||
.failed - Extra class of .message-text for messages that failed to be sent
|
||||
.message-attachment-box - Contains attachment info
|
||||
.message-reply - Container for the replied-to message in a reply (these elements will also have .message-text set)
|
||||
.message-input - Applied to the chat input container
|
||||
|
@ -42,6 +42,7 @@ Abaddon::Abaddon()
|
||||
m_discord.signal_reaction_add().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReactionAdd));
|
||||
m_discord.signal_reaction_remove().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReactionRemove));
|
||||
m_discord.signal_guild_join_request_create().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnGuildJoinRequestCreate));
|
||||
m_discord.signal_message_sent().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageSent));
|
||||
m_discord.signal_disconnected().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnDisconnect));
|
||||
if (m_settings.GetPrefetch())
|
||||
m_discord.signal_message_create().connect([this](Snowflake id) {
|
||||
@ -226,6 +227,10 @@ void Abaddon::DiscordOnGuildJoinRequestCreate(const GuildJoinRequestCreateData &
|
||||
}
|
||||
}
|
||||
|
||||
void Abaddon::DiscordOnMessageSent(const Message &data) {
|
||||
m_main_window->UpdateChatNewMessage(data.ID);
|
||||
}
|
||||
|
||||
void Abaddon::DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_code) {
|
||||
m_main_window->UpdateComponents();
|
||||
if (close_code == GatewayCloseCode::AuthenticationFailed) {
|
||||
|
@ -75,6 +75,7 @@ public:
|
||||
void DiscordOnReactionAdd(Snowflake message_id, const Glib::ustring ¶m);
|
||||
void DiscordOnReactionRemove(Snowflake message_id, const Glib::ustring ¶m);
|
||||
void DiscordOnGuildJoinRequestCreate(const GuildJoinRequestCreateData &data);
|
||||
void DiscordOnMessageSent(const Message &data);
|
||||
void DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_code);
|
||||
|
||||
const SettingsManager &GetSettings() const;
|
||||
|
@ -51,6 +51,9 @@ ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(Snowflake id) {
|
||||
container->ID = data->ID;
|
||||
container->ChannelID = data->ChannelID;
|
||||
|
||||
if (data->Nonce.has_value())
|
||||
container->Nonce = *data->Nonce;
|
||||
|
||||
if (data->Content.size() > 0 || data->Type != MessageType::DEFAULT) {
|
||||
container->m_text_component = container->CreateTextComponent(&*data);
|
||||
container->AttachEventHandlers(*container->m_text_component);
|
||||
@ -139,6 +142,11 @@ void ChatMessageItemContainer::UpdateReactions() {
|
||||
}
|
||||
}
|
||||
|
||||
void ChatMessageItemContainer::SetFailed() {
|
||||
m_text_component->get_style_context()->remove_class("pending");
|
||||
m_text_component->get_style_context()->add_class("failed");
|
||||
}
|
||||
|
||||
void ChatMessageItemContainer::UpdateAttributes() {
|
||||
const auto data = Abaddon::Get().GetDiscordClient().GetMessage(ID);
|
||||
if (!data.has_value()) return;
|
||||
@ -176,6 +184,8 @@ void ChatMessageItemContainer::AddClickHandler(Gtk::Widget *widget, std::string
|
||||
Gtk::TextView *ChatMessageItemContainer::CreateTextComponent(const Message *data) {
|
||||
auto *tv = Gtk::manage(new Gtk::TextView);
|
||||
|
||||
if (data->IsPending)
|
||||
tv->get_style_context()->add_class("pending");
|
||||
tv->get_style_context()->add_class("message-text");
|
||||
tv->set_can_focus(false);
|
||||
tv->set_editable(false);
|
||||
|
@ -7,6 +7,8 @@ public:
|
||||
Snowflake ID;
|
||||
Snowflake ChannelID;
|
||||
|
||||
std::string Nonce;
|
||||
|
||||
ChatMessageItemContainer();
|
||||
static ChatMessageItemContainer *FromMessage(Snowflake id);
|
||||
|
||||
@ -14,6 +16,7 @@ public:
|
||||
void UpdateAttributes();
|
||||
void UpdateContent();
|
||||
void UpdateReactions();
|
||||
void SetFailed();
|
||||
|
||||
protected:
|
||||
void AddClickHandler(Gtk::Widget *widget, std::string);
|
||||
|
@ -5,6 +5,8 @@
|
||||
#include "chatinput.hpp"
|
||||
|
||||
ChatWindow::ChatWindow() {
|
||||
Abaddon::Get().GetDiscordClient().signal_message_send_fail().connect(sigc::mem_fun(*this, &ChatWindow::OnMessageSendFail));
|
||||
|
||||
m_main = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
|
||||
m_list = Gtk::manage(new Gtk::ListBox);
|
||||
m_scroll = Gtk::manage(new Gtk::ScrolledWindow);
|
||||
@ -197,6 +199,19 @@ ChatMessageItemContainer *ChatWindow::CreateMessageComponent(Snowflake id) {
|
||||
return container;
|
||||
}
|
||||
|
||||
void ChatWindow::RemoveMessageAndHeader(Gtk::Widget *widget) {
|
||||
ChatMessageHeader *header = dynamic_cast<ChatMessageHeader *>(widget->get_ancestor(Gtk::ListBoxRow::get_type()));
|
||||
if (header != nullptr) {
|
||||
if (header->GetChildContent().size() == 1) {
|
||||
m_num_rows--;
|
||||
delete header;
|
||||
} else
|
||||
delete widget;
|
||||
} else
|
||||
delete widget;
|
||||
m_num_messages--;
|
||||
}
|
||||
|
||||
constexpr static int MaxMessagesForCull = 50; // this has to be 50 cuz that magic number is used in a couple other places and i dont feel like replacing them
|
||||
void ChatWindow::ProcessNewMessage(Snowflake id, bool prepend) {
|
||||
const auto &client = Abaddon::Get().GetDiscordClient();
|
||||
@ -204,6 +219,16 @@ void ChatWindow::ProcessNewMessage(Snowflake id, bool prepend) {
|
||||
const auto data = client.GetMessage(id);
|
||||
if (!data.has_value()) return;
|
||||
|
||||
if (!data->IsPending && data->Nonce.has_value() && data->Author.ID == client.GetUserData().ID) {
|
||||
for (auto [id, widget] : m_id_to_widget) {
|
||||
if (dynamic_cast<ChatMessageItemContainer *>(widget)->Nonce == *data->Nonce) {
|
||||
RemoveMessageAndHeader(widget);
|
||||
m_id_to_widget.erase(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChatMessageHeader *last_row = nullptr;
|
||||
bool should_attach = false;
|
||||
if (m_num_rows > 0) {
|
||||
@ -222,17 +247,8 @@ void ChatWindow::ProcessNewMessage(Snowflake id, bool prepend) {
|
||||
if (m_should_scroll_to_bottom && !prepend)
|
||||
while (m_num_messages > MaxMessagesForCull) {
|
||||
auto first_it = m_id_to_widget.begin();
|
||||
ChatMessageHeader *header = dynamic_cast<ChatMessageHeader *>(first_it->second->get_ancestor(Gtk::ListBoxRow::get_type()));
|
||||
if (header != nullptr) {
|
||||
if (header->GetChildContent().size() == 1)
|
||||
delete header;
|
||||
else
|
||||
delete first_it->second;
|
||||
} else
|
||||
delete first_it->second;
|
||||
RemoveMessageAndHeader(first_it->second);
|
||||
m_id_to_widget.erase(first_it);
|
||||
m_num_rows--;
|
||||
m_num_messages--;
|
||||
}
|
||||
|
||||
ChatMessageHeader *header;
|
||||
@ -261,22 +277,24 @@ void ChatWindow::ProcessNewMessage(Snowflake id, bool prepend) {
|
||||
header->AddContent(content, prepend);
|
||||
m_id_to_widget[id] = content;
|
||||
|
||||
content->signal_action_delete().connect([this, id] {
|
||||
m_signal_action_message_delete.emit(m_active_channel, id);
|
||||
});
|
||||
content->signal_action_edit().connect([this, id] {
|
||||
m_signal_action_message_edit.emit(m_active_channel, id);
|
||||
});
|
||||
content->signal_action_reaction_add().connect([this, id](const Glib::ustring ¶m) {
|
||||
m_signal_action_reaction_add.emit(id, param);
|
||||
});
|
||||
content->signal_action_reaction_remove().connect([this, id](const Glib::ustring ¶m) {
|
||||
m_signal_action_reaction_remove.emit(id, param);
|
||||
});
|
||||
content->signal_action_channel_click().connect([this](const Snowflake &id) {
|
||||
m_signal_action_channel_click.emit(id);
|
||||
});
|
||||
content->signal_action_reply_to().connect(sigc::mem_fun(*this, &ChatWindow::StartReplying));
|
||||
if (!data->IsPending) {
|
||||
content->signal_action_delete().connect([this, id] {
|
||||
m_signal_action_message_delete.emit(m_active_channel, id);
|
||||
});
|
||||
content->signal_action_edit().connect([this, id] {
|
||||
m_signal_action_message_edit.emit(m_active_channel, id);
|
||||
});
|
||||
content->signal_action_reaction_add().connect([this, id](const Glib::ustring ¶m) {
|
||||
m_signal_action_reaction_add.emit(id, param);
|
||||
});
|
||||
content->signal_action_reaction_remove().connect([this, id](const Glib::ustring ¶m) {
|
||||
m_signal_action_reaction_remove.emit(id, param);
|
||||
});
|
||||
content->signal_action_channel_click().connect([this](const Snowflake &id) {
|
||||
m_signal_action_channel_click.emit(id);
|
||||
});
|
||||
content->signal_action_reply_to().connect(sigc::mem_fun(*this, &ChatWindow::StartReplying));
|
||||
}
|
||||
}
|
||||
|
||||
header->set_margin_left(5);
|
||||
@ -321,6 +339,15 @@ void ChatWindow::ScrollToBottom() {
|
||||
x->set_value(x->get_upper());
|
||||
}
|
||||
|
||||
void ChatWindow::OnMessageSendFail(const std::string &nonce) {
|
||||
for (auto [id, widget] : m_id_to_widget) {
|
||||
if (auto *container = dynamic_cast<ChatMessageItemContainer *>(widget); container->Nonce == nonce) {
|
||||
container->SetFailed();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChatWindow::type_signal_action_message_delete ChatWindow::signal_action_message_delete() {
|
||||
return m_signal_action_message_delete;
|
||||
}
|
||||
|
@ -47,9 +47,13 @@ protected:
|
||||
bool OnKeyPressEvent(GdkEventKey *e);
|
||||
void OnScrollEdgeOvershot(Gtk::PositionType pos);
|
||||
|
||||
void RemoveMessageAndHeader(Gtk::Widget *widget);
|
||||
|
||||
void ScrollToBottom();
|
||||
bool m_should_scroll_to_bottom = true;
|
||||
|
||||
void OnMessageSendFail(const std::string &nonce);
|
||||
|
||||
Gtk::Box *m_main;
|
||||
Gtk::ListBox *m_list;
|
||||
Gtk::ScrolledWindow *m_scroll;
|
||||
|
10
css/main.css
10
css/main.css
@ -75,10 +75,18 @@
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.message-text text, .message-reply {
|
||||
.message-text:not(.failed) text, .message-reply {
|
||||
color: @text_color;
|
||||
}
|
||||
|
||||
.message-text.pending text {
|
||||
color: shade(@text_color, 0.5);
|
||||
}
|
||||
|
||||
.message-text.failed text {
|
||||
color: #b72d4f;
|
||||
}
|
||||
|
||||
.message-reply {
|
||||
border-left: 2px solid gray;
|
||||
padding-left: 20px;
|
||||
|
@ -345,18 +345,57 @@ bool DiscordClient::CanManageMember(Snowflake guild_id, Snowflake actor, Snowfla
|
||||
return actor_highest->Position > target_highest->Position;
|
||||
}
|
||||
|
||||
void DiscordClient::ChatMessageCallback(std::string nonce, const http::response_type &response) {
|
||||
if (!CheckCode(response)) {
|
||||
m_signal_message_send_fail.emit(nonce);
|
||||
}
|
||||
}
|
||||
|
||||
void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel) {
|
||||
// @([^@#]{1,32})#(\\d{4})
|
||||
const auto nonce = std::to_string(Snowflake::FromNow());
|
||||
CreateMessageObject obj;
|
||||
obj.Content = content;
|
||||
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), [](auto) {});
|
||||
obj.Nonce = nonce;
|
||||
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce));
|
||||
// dummy data so the content can be shown while waiting for MESSAGE_CREATE
|
||||
Message tmp;
|
||||
tmp.Content = content;
|
||||
tmp.ID = nonce;
|
||||
tmp.ChannelID = channel;
|
||||
tmp.Author = GetUserData();
|
||||
tmp.IsTTS = false;
|
||||
tmp.DoesMentionEveryone = false;
|
||||
tmp.Type = MessageType::DEFAULT;
|
||||
tmp.IsPinned = false;
|
||||
tmp.Timestamp = "2000-01-01T00:00:00.000000+00:00";
|
||||
tmp.Nonce = obj.Nonce;
|
||||
tmp.IsPending = true;
|
||||
m_store.SetMessage(tmp.ID, tmp);
|
||||
m_signal_message_sent.emit(tmp);
|
||||
}
|
||||
|
||||
void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message) {
|
||||
const auto nonce = std::to_string(Snowflake::FromNow());
|
||||
CreateMessageObject obj;
|
||||
obj.Content = content;
|
||||
obj.Nonce = nonce;
|
||||
obj.MessageReference.emplace().MessageID = referenced_message;
|
||||
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), [](auto) {});
|
||||
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce));
|
||||
Message tmp;
|
||||
tmp.Content = content;
|
||||
tmp.ID = nonce;
|
||||
tmp.ChannelID = channel;
|
||||
tmp.Author = GetUserData();
|
||||
tmp.IsTTS = false;
|
||||
tmp.DoesMentionEveryone = false;
|
||||
tmp.Type = MessageType::DEFAULT;
|
||||
tmp.IsPinned = false;
|
||||
tmp.Timestamp = "2000-01-01T00:00:00.000000+00:00";
|
||||
tmp.Nonce = obj.Nonce;
|
||||
tmp.IsPending = true;
|
||||
m_store.SetMessage(tmp.ID, tmp);
|
||||
m_signal_message_sent.emit(tmp);
|
||||
}
|
||||
|
||||
void DiscordClient::DeleteMessage(Snowflake channel_id, Snowflake id) {
|
||||
@ -1861,3 +1900,11 @@ DiscordClient::type_signal_guild_join_request_update DiscordClient::signal_guild
|
||||
DiscordClient::type_signal_guild_join_request_delete DiscordClient::signal_guild_join_request_delete() {
|
||||
return m_signal_guild_join_request_delete;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_message_sent DiscordClient::signal_message_sent() {
|
||||
return m_signal_message_sent;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_message_send_fail DiscordClient::signal_message_send_fail() {
|
||||
return m_signal_message_send_fail;
|
||||
}
|
||||
|
@ -97,6 +97,8 @@ public:
|
||||
Permission ComputeOverwrites(Permission base, Snowflake member_id, Snowflake channel_id) const;
|
||||
bool CanManageMember(Snowflake guild_id, Snowflake actor, Snowflake target) const; // kick, ban, edit nickname (cant think of a better name)
|
||||
|
||||
void ChatMessageCallback(std::string nonce, const http::response_type &response);
|
||||
|
||||
void SendChatMessage(const std::string &content, Snowflake channel);
|
||||
void SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message);
|
||||
void DeleteMessage(Snowflake channel_id, Snowflake id);
|
||||
@ -306,6 +308,8 @@ public:
|
||||
typedef sigc::signal<void, GuildJoinRequestCreateData> type_signal_guild_join_request_create;
|
||||
typedef sigc::signal<void, GuildJoinRequestUpdateData> type_signal_guild_join_request_update;
|
||||
typedef sigc::signal<void, GuildJoinRequestDeleteData> type_signal_guild_join_request_delete;
|
||||
typedef sigc::signal<void, Message> type_signal_message_sent;
|
||||
typedef sigc::signal<void, std::string> type_signal_message_send_fail;
|
||||
typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting
|
||||
typedef sigc::signal<void> type_signal_connected;
|
||||
|
||||
@ -337,6 +341,8 @@ public:
|
||||
type_signal_guild_join_request_create signal_guild_join_request_create();
|
||||
type_signal_guild_join_request_update signal_guild_join_request_update();
|
||||
type_signal_guild_join_request_delete signal_guild_join_request_delete();
|
||||
type_signal_message_sent signal_message_sent();
|
||||
type_signal_message_send_fail signal_message_send_fail();
|
||||
type_signal_disconnected signal_disconnected();
|
||||
type_signal_connected signal_connected();
|
||||
|
||||
@ -369,6 +375,8 @@ protected:
|
||||
type_signal_guild_join_request_create m_signal_guild_join_request_create;
|
||||
type_signal_guild_join_request_update m_signal_guild_join_request_update;
|
||||
type_signal_guild_join_request_delete m_signal_guild_join_request_delete;
|
||||
type_signal_message_sent m_signal_message_sent;
|
||||
type_signal_message_send_fail m_signal_message_send_fail;
|
||||
type_signal_disconnected m_signal_disconnected;
|
||||
type_signal_connected m_signal_connected;
|
||||
};
|
||||
|
@ -202,6 +202,8 @@ struct Message {
|
||||
void from_json_edited(const nlohmann::json &j); // for MESSAGE_UPDATE
|
||||
|
||||
// custom fields to track changes
|
||||
bool IsPending = false; // for user-sent messages yet to be received in a MESSAGE_CREATE
|
||||
|
||||
void SetDeleted();
|
||||
void SetEdited();
|
||||
bool IsDeleted() const;
|
||||
|
@ -191,6 +191,7 @@ void to_json(nlohmann::json &j, const HeartbeatMessage &m) {
|
||||
void to_json(nlohmann::json &j, const CreateMessageObject &m) {
|
||||
j["content"] = m.Content;
|
||||
JS_IF("message_reference", m.MessageReference);
|
||||
JS_IF("nonce", m.Nonce);
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const MessageEditObject &m) {
|
||||
|
@ -291,6 +291,7 @@ struct HeartbeatMessage : GatewayMessage {
|
||||
struct CreateMessageObject {
|
||||
std::string Content;
|
||||
std::optional<MessageReferenceData> MessageReference;
|
||||
std::optional<std::string> Nonce;
|
||||
|
||||
friend void to_json(nlohmann::json &j, const CreateMessageObject &m);
|
||||
};
|
||||
|
@ -1,6 +1,9 @@
|
||||
#include "snowflake.hpp"
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <chrono>
|
||||
|
||||
constexpr static uint64_t DiscordEpochSeconds = 1420070400;
|
||||
|
||||
Snowflake::Snowflake()
|
||||
: m_num(Invalid) {}
|
||||
@ -21,6 +24,18 @@ Snowflake::Snowflake(const Glib::ustring &str) {
|
||||
m_num = Invalid;
|
||||
};
|
||||
|
||||
Snowflake Snowflake::FromNow() {
|
||||
using namespace std::chrono;
|
||||
// not guaranteed to work but it probably will anyway
|
||||
static uint64_t counter = 0;
|
||||
const auto millis_since_epoch = static_cast<uint64_t>(duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count());
|
||||
const auto epoch = millis_since_epoch - DiscordEpochSeconds * 1000;
|
||||
uint64_t snowflake = epoch << 22;
|
||||
// worker id and process id would be OR'd in here but there's no point
|
||||
snowflake |= counter++ % 4096;
|
||||
return snowflake;
|
||||
}
|
||||
|
||||
bool Snowflake::IsValid() const {
|
||||
return m_num != Invalid;
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ struct Snowflake {
|
||||
Snowflake(const std::string &str);
|
||||
Snowflake(const Glib::ustring &str);
|
||||
|
||||
static Snowflake FromNow(); // not thread safe
|
||||
|
||||
bool IsValid() const;
|
||||
std::string GetLocalTimestamp() const;
|
||||
|
||||
|
@ -259,6 +259,8 @@ void Store::SetMessage(Snowflake id, const Message &message) {
|
||||
Bind(m_set_msg_stmt, 20, nullptr);
|
||||
Bind(m_set_msg_stmt, 21, message.IsDeleted());
|
||||
Bind(m_set_msg_stmt, 22, message.IsEdited());
|
||||
Bind(m_set_msg_stmt, 23, message.IsPending);
|
||||
Bind(m_set_msg_stmt, 24, message.Nonce); // sorry
|
||||
|
||||
if (!RunInsert(m_set_msg_stmt))
|
||||
fprintf(stderr, "message insert failed: %s\n", sqlite3_errstr(m_db_err));
|
||||
@ -567,6 +569,9 @@ std::optional<Message> Store::GetMessage(Snowflake id) const {
|
||||
Get(m_get_msg_stmt, 21, tmpb);
|
||||
if (tmpb) ret.SetEdited();
|
||||
|
||||
Get(m_get_msg_stmt, 22, ret.IsPending);
|
||||
Get(m_get_msg_stmt, 23, ret.Nonce);
|
||||
|
||||
Reset(m_get_msg_stmt);
|
||||
|
||||
if (ret.MessageReference.has_value() && ret.MessageReference->MessageID.has_value()) {
|
||||
@ -749,9 +754,10 @@ bool Store::CreateTables() {
|
||||
flags INTEGER,
|
||||
stickers TEXT, /* json */
|
||||
reactions TEXT, /* json */
|
||||
/* extra */
|
||||
deleted BOOL,
|
||||
edited BOOL
|
||||
deleted BOOL, /* extra */
|
||||
edited BOOL, /* extra */
|
||||
pending BOOL, /* extra */
|
||||
nonce TEXT
|
||||
)
|
||||
)";
|
||||
|
||||
@ -951,7 +957,7 @@ bool Store::CreateStatements() {
|
||||
|
||||
constexpr const char *set_msg = R"(
|
||||
REPLACE INTO messages VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
)";
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user