mirror of
https://gitlab.com/niansa/SomeBot.git
synced 2025-03-06 20:48:26 +01:00
485 lines
24 KiB
C++
485 lines
24 KiB
C++
#include "globalchat.hpp"
|
||
#include "../bot.hpp"
|
||
#include "../util.hpp"
|
||
|
||
#include <string_view>
|
||
#include <optional>
|
||
#include <tuple>
|
||
#include <thread>
|
||
#include <chrono>
|
||
#include <exception>
|
||
|
||
|
||
|
||
namespace GlobalchatExternal {
|
||
std::string MessageInfo::encode() const {
|
||
return Util::int_as_hex(author)+'-'+Util::int_as_hex(message)+'-'+Util::int_as_hex(channel)+'-'+Util::int_as_hex(guild);
|
||
}
|
||
MessageInfo MessageInfo::decode(std::string_view str) {
|
||
// Find start message code
|
||
auto pos_start = str.rfind(message_code_idenfitication);
|
||
if (pos_start == str.npos) {
|
||
throw DecodeError();
|
||
}
|
||
pos_start += message_code_idenfitication.size();
|
||
// Find end of message code
|
||
auto pos_end = str.find(' ', pos_start);
|
||
if (pos_end == str.npos) {
|
||
pos_end = str.size()-1;
|
||
}
|
||
// Get message code as string
|
||
std::string_view message_code{str.data()+pos_start, str.size()-pos_start-(str.size()-pos_end)};
|
||
// Find all IDs
|
||
auto fres = Util::split_str(message_code, '-', 3);
|
||
if (fres.size() != 4) {
|
||
throw DecodeError();
|
||
}
|
||
return MessageInfo{Util::hex_as_int<uint64_t>(fres[0]), Util::hex_as_int<uint64_t>(fres[1]), Util::hex_as_int<uint64_t>(fres[2]), Util::hex_as_int<uint64_t>(fres[3])};
|
||
}
|
||
MessageInfo MessageInfo::decode_message(const dpp::message& msg) {
|
||
// Check that message has embeds
|
||
if (msg.embeds.empty()) {
|
||
throw DecodeError();
|
||
}
|
||
// Return message info
|
||
return decode(msg.embeds[0].description);
|
||
}
|
||
}
|
||
|
||
|
||
class Globalchat {
|
||
Bot *bot;
|
||
|
||
std::unordered_map<dpp::snowflake, dpp::guild> guildCache;
|
||
|
||
bool is_bad_message(std::string message) {
|
||
// Lowercase message
|
||
for (auto& c : message) {
|
||
c = tolower(c);
|
||
}
|
||
// String blacklist
|
||
const static std::vector<std::string_view> blacklist = {"https://", "http://", "discord.gg/", "]("};
|
||
for (const auto word : blacklist) {
|
||
if (message.find(word) != message.npos) {
|
||
return true;
|
||
}
|
||
}
|
||
// Message seems alright
|
||
return false;
|
||
}
|
||
|
||
void broadcast_message(const dpp::message_create_t& event, bool origin = true) {
|
||
using namespace GlobalchatExternal;
|
||
|
||
auto original_msg = event.msg;
|
||
// Filter bad messages
|
||
bool is_bad = is_bad_message(original_msg.content);
|
||
if (is_bad) {
|
||
original_msg.content = "*Nachricht entspricht nicht den Richtlinien*";
|
||
}
|
||
// Get user info
|
||
unsigned color = Util::get_color_of_user(original_msg.author);
|
||
bool banned = false;
|
||
bot->db << "SELECT color, banned FROM globalchat_user_settings "
|
||
"WHERE id = ?;"
|
||
<< std::to_string(original_msg.author.id)
|
||
>> [&](int _color, int _banned) {
|
||
if (_color != -1) color = _color;
|
||
banned = _banned;
|
||
};
|
||
// Make sure user isn't banned
|
||
if (banned) {
|
||
return;
|
||
}
|
||
// Create embed
|
||
dpp::message globalchat_msg;
|
||
std::string guild_name;
|
||
try {
|
||
guild_name = guildCache.at(original_msg.guild_id).name;
|
||
} catch (...) {
|
||
guild_name = "Unbekannt (extern?)";
|
||
auto [bots, bots_lock] = bot->get_locked_property<std::vector<Bot*>>("main_all_instances");
|
||
for (auto bot : *bots) {
|
||
try {
|
||
guild_name = bot->get_loaded_module<Globalchat>("Globalchat")->guildCache.at(original_msg.guild_id).name + " (extern)";
|
||
break;
|
||
} catch (...) {}
|
||
}
|
||
}
|
||
auto messageCode = MessageInfo{original_msg.author.id, original_msg.id, original_msg.channel_id, original_msg.guild_id}.encode();
|
||
globalchat_msg.add_embed(dpp::embed()
|
||
.set_title(":earth_africa: Globalchat")
|
||
.set_description(":speaking_head: ["+original_msg.author.format_username()+"]("+std::string(MessageInfo::message_code_idenfitication)+messageCode+" 'Inspection Data')")
|
||
.set_thumbnail(original_msg.author.get_avatar_url())
|
||
.set_color(color)
|
||
.add_field(original_msg.member.get_nickname().empty()?original_msg.author.username:original_msg.member.get_nickname(), original_msg.content+"\n⠀")
|
||
.set_footer(dpp::embed_footer().set_text("Gesendet von: "+guild_name))
|
||
);
|
||
// Process message reference
|
||
std::string replied_to_primary_id;
|
||
if (original_msg.message_reference.message_id) {
|
||
// Get primary message ID
|
||
bot->db << "SELECT primary_id FROM globalchat_messages "
|
||
"WHERE id = ?;"
|
||
<< std::to_string(original_msg.message_reference.message_id)
|
||
>> [&](std::string primary_id) {
|
||
replied_to_primary_id = primary_id;
|
||
};
|
||
// Get message content
|
||
auto [bots, bots_lock] = bot->get_locked_property<std::vector<Bot*>>("main_all_instances");
|
||
for (auto& bot : *bots) {
|
||
try {
|
||
auto replied_to_message = bot->cluster.message_get_sync(original_msg.message_reference.message_id, original_msg.channel_id);
|
||
if (!replied_to_message.embeds.empty()) {
|
||
const auto& embed = replied_to_message.embeds[0];
|
||
if (!embed.fields.empty()) {
|
||
auto content = embed.fields[0].value;
|
||
content.erase(content.size()-3, 3);
|
||
globalchat_msg.set_content("> "+content);
|
||
}
|
||
}
|
||
break;
|
||
} catch (...) {}
|
||
}
|
||
}
|
||
// Broadcast to all globalchat channels
|
||
bot->db << "SELECT globalchat_channel, id FROM globalchat_guild_settings;"
|
||
>> [&](std::string channel_id_str, std::string guild_id_str) {
|
||
if (channel_id_str.empty())
|
||
return;
|
||
globalchat_msg.set_channel_id(std::stoul(channel_id_str));
|
||
// Set reply
|
||
if (original_msg.message_reference.message_id) {
|
||
bot->db << "SELECT id FROM globalchat_messages "
|
||
"WHERE primary_id = ? AND guild_id = ?;"
|
||
<< replied_to_primary_id << guild_id_str
|
||
>> [&](std::string id) {
|
||
globalchat_msg.set_reference(id, guild_id_str, channel_id_str);
|
||
};
|
||
}
|
||
// Send message
|
||
bot->cluster.message_create(globalchat_msg, [=](const dpp::confirmation_callback_t& ccb) {
|
||
if (ccb.is_error()) {
|
||
// Unset global chat
|
||
unset_globalchat_channel(guild_id_str);
|
||
return;
|
||
}
|
||
// Special treatment for bad messages
|
||
if (!is_bad) {
|
||
// Add this message to database
|
||
bot->db << "INSERT INTO globalchat_messages (primary_id, id, guild_id, author_id) VALUES (?, ?, ?, ?);"
|
||
<< std::to_string(original_msg.id) << std::to_string(ccb.get<dpp::message>().id) << guild_id_str << std::to_string(original_msg.author.id);
|
||
} else {
|
||
auto msg = ccb.get<dpp::message>();
|
||
// Add reaction
|
||
bot->cluster.message_add_reaction(msg, "💣", [this, msg](const dpp::confirmation_callback_t&) {
|
||
// Delete message soon (random delay to avoid deleting many messages at once)
|
||
std::thread([this, msg]() {
|
||
std::this_thread::sleep_for(std::chrono::seconds((msg.id%10)+6));
|
||
bot->cluster.message_delete(msg.id, msg.channel_id);
|
||
}).detach();
|
||
});
|
||
}
|
||
});
|
||
};
|
||
// Broadcast to other instances
|
||
if (origin) {
|
||
// We NEED to avoid having the lock held when broadcasting!!!
|
||
std::vector<Globalchat*> globalchat_instances;
|
||
{
|
||
auto [bots, bots_lock] = bot->get_locked_property<std::vector<Bot*>>("main_all_instances");
|
||
for (auto bot : *bots) {
|
||
if (bot == this->bot) {
|
||
continue;
|
||
}
|
||
try {
|
||
globalchat_instances.push_back(bot->get_loaded_module<Globalchat>("Globalchat"));
|
||
} catch (...) {}
|
||
}
|
||
}
|
||
for (auto globalchat : globalchat_instances) {
|
||
globalchat->broadcast_message(event, false);
|
||
}
|
||
}
|
||
}
|
||
|
||
void db_add_guild(const dpp::snowflake& guild_id) {
|
||
bot->db << "INSERT OR IGNORE INTO globalchat_guild_settings (id) VALUES (?);"
|
||
<< std::to_string(guild_id);
|
||
}
|
||
void db_add_user(const dpp::snowflake& user_id) {
|
||
bot->db << "INSERT OR IGNORE INTO globalchat_user_settings (id, color, banned) VALUES (?, -1, 0);"
|
||
<< std::to_string(user_id);
|
||
}
|
||
|
||
void set_globalchat_channel(dpp::snowflake guild_id, dpp::snowflake channel_id) {
|
||
// Unset global chat
|
||
bot->db << "UPDATE globalchat_guild_settings "
|
||
"SET globalchat_channel = ? "
|
||
"WHERE id = ?;"
|
||
<< std::to_string(channel_id)
|
||
<< std::to_string(guild_id);
|
||
return;
|
||
}
|
||
void unset_globalchat_channel(dpp::snowflake guild_id) {
|
||
// Unset global chat
|
||
bot->db << "UPDATE globalchat_guild_settings "
|
||
"SET globalchat_channel = NULL "
|
||
"WHERE id = ?;"
|
||
<< std::to_string(guild_id);
|
||
return;
|
||
}
|
||
|
||
void ban_user(dpp::snowflake user_id) {
|
||
bot->db << "UPDATE globalchat_user_settings "
|
||
"SET banned = TRUE "
|
||
"WHERE id = ?;"
|
||
<< std::to_string(user_id);
|
||
}
|
||
void unban_user(dpp::snowflake user_id) {
|
||
bot->db << "UPDATE globalchat_user_settings "
|
||
"SET banned = FALSE "
|
||
"WHERE id = ?;"
|
||
<< std::to_string(user_id);
|
||
}
|
||
|
||
void set_globalchat_channel_topic(dpp::channel channel) {
|
||
channel.set_topic("Dies ist der Globalchat des *"+bot->cluster.me.username+"* Bots. Viel Spaß!\n"
|
||
"\n"
|
||
"**Regeln:**\n"
|
||
" **0.** Common Sense\n"
|
||
" **1.** Keine Links\n"
|
||
" **2.** Kein Trolling\n"
|
||
" **3.** Keine Diskussionen über Moderationsentscheidungen\n"
|
||
" **4.** Keine hitzigen religiösen Diskussionen\n"
|
||
" **5.** Keine ernsthaften Beleidigungen");
|
||
bot->cluster.channel_edit(channel);
|
||
}
|
||
|
||
public:
|
||
Globalchat(Bot *_bot) : bot(_bot) {
|
||
bot->cluster.intents |= dpp::intents::i_message_content | dpp::intents::i_guilds;
|
||
|
||
bot->db << "CREATE TABLE IF NOT EXISTS globalchat_guild_settings ("
|
||
" id TEXT PRIMARY KEY NOT NULL,"
|
||
" globalchat_channel TEXT,"
|
||
" UNIQUE(id)"
|
||
");";
|
||
bot->db << "CREATE TABLE IF NOT EXISTS globalchat_user_settings ("
|
||
" id TEXT PRIMARY KEY NOT NULL,"
|
||
" color INTEGER,"
|
||
" banned INTEGER,"
|
||
" UNIQUE(id)"
|
||
");";
|
||
bot->db << "CREATE TABLE IF NOT EXISTS globalchat_messages ("
|
||
" primary_id TEXT,"
|
||
" id TEXT,"
|
||
" guild_id TEXT,"
|
||
" author_id TEXT"
|
||
");";
|
||
|
||
bot->cluster.on_message_create([&](const dpp::message_create_t& message) {
|
||
// Make sure sender isn't a bot
|
||
if (message.msg.author.is_bot())
|
||
return;
|
||
|
||
// Get current globalchat channel for server
|
||
dpp::snowflake globalchat_channel;
|
||
{
|
||
std::string globalchat_channel_str;
|
||
bot->db << "SELECT globalchat_channel FROM globalchat_guild_settings "
|
||
"WHERE id = ?;"
|
||
<< std::to_string(message.msg.guild_id)
|
||
>> std::tie(globalchat_channel_str);
|
||
if (globalchat_channel_str.empty()) {
|
||
return;
|
||
}
|
||
globalchat_channel = std::stoul(globalchat_channel_str);
|
||
}
|
||
// If this channel is globalchat channel, broadcast message
|
||
if (message.msg.channel_id == globalchat_channel) {
|
||
std::thread([this, message]() {
|
||
broadcast_message(message);
|
||
bot->cluster.message_delete(message.msg.id, message.msg.channel_id);
|
||
}).detach();
|
||
return;
|
||
}
|
||
});
|
||
bot->cluster.on_guild_create([&](const dpp::guild_create_t& guild) {
|
||
// Make sure guild is in database
|
||
db_add_guild(guild.created->id);
|
||
// Add guild to cache
|
||
guildCache[guild.created->id] = *guild.created;
|
||
// Set topic in its globalchat channel
|
||
try {
|
||
// Get globalchat channel ID
|
||
std::string globalchat_channel_str;
|
||
bot->db << "SELECT globalchat_channel FROM globalchat_guild_settings "
|
||
"WHERE id = ?;"
|
||
<< std::to_string(guild.created->id)
|
||
>> std::tie(globalchat_channel_str);
|
||
// Get channel itself
|
||
bot->cluster.channel_get(globalchat_channel_str, [this, guild_id = guild.created->id](const dpp::confirmation_callback_t& ccb) {
|
||
if (ccb.is_error()) {
|
||
// Unset globalchat channel
|
||
unset_globalchat_channel(guild_id);
|
||
return;
|
||
}
|
||
// Set channel topic
|
||
set_globalchat_channel_topic(ccb.get<dpp::channel>());
|
||
});
|
||
} catch (...) {}
|
||
});
|
||
bot->cluster.on_guild_delete([&](const dpp::guild_delete_t& guild) {
|
||
// Delete guild from database
|
||
bot->db << "DELETE FROM globalchat_guild_settings "
|
||
"WHERE id = ?;"
|
||
<< std::to_string(guild.deleted->id);
|
||
});
|
||
bot->cluster.on_guild_member_remove([&](const dpp::guild_member_remove_t& guild) {
|
||
// Delete empty guilds
|
||
if (guild.removing_guild->member_count == 1) {
|
||
bot->cluster.guild_delete(guild.removing_guild->id);
|
||
}
|
||
});
|
||
|
||
bot->add_chatcommand(Bot::ChatCommand({"set_gc", "globalchat"}, "Verwende den Globalchat", dpp::slashcommand().add_option(dpp::command_option(dpp::command_option_type::co_channel, "channel", "Der Kanal in dem der Globalchat laufen soll", true))), [&](const dpp::slashcommand_t& event) {
|
||
auto channel_id = std::get<dpp::snowflake>(event.get_parameter("channel"));
|
||
// Check that user has the correct permissions
|
||
if (!event.command.get_guild().base_permissions(event.command.member).has(dpp::permissions::p_manage_channels)) {
|
||
event.reply(dpp::message("Du brauchst die Kanalverwaltungsberechtigung, um dieses Kommando zu verwenden.").set_flags(dpp::message_flags::m_ephemeral));
|
||
return;
|
||
}
|
||
// Update globalchat ID
|
||
set_globalchat_channel(event.command.guild_id, channel_id);
|
||
// Set channel topic
|
||
bot->cluster.channel_get(channel_id, [=](const dpp::confirmation_callback_t& ccb) {
|
||
if (ccb.is_error()) {
|
||
return;
|
||
}
|
||
set_globalchat_channel_topic(ccb.get<dpp::channel>());
|
||
});
|
||
// Reply
|
||
event.reply("Der Globalchat läuft jetzt in <#"+std::to_string(channel_id)+">");
|
||
});
|
||
bot->add_chatcommand(Bot::ChatCommand({"reset_gc", "globalchat_stop"}, "Stoppe den Globalchat"), [&](const dpp::slashcommand_t& event) {
|
||
// Check that user has the correct permissions
|
||
if (!event.command.get_guild().base_permissions(event.command.member).has(dpp::permissions::p_manage_channels)) {
|
||
event.reply("Du brauchst die Kanalverwaltungsberechtigung, um dieses Kommando zu verwenden.");
|
||
return;
|
||
}
|
||
// Update globalchat ID
|
||
unset_globalchat_channel(event.command.guild_id);
|
||
// Reply
|
||
event.reply("Der Globalchat wurde gestoppt");
|
||
});
|
||
bot->add_chatcommand(Bot::ChatCommand({"gc_color", "globalchat_color", "globalchat_farbe"}, "Setze Farbe im Globalchat", dpp::slashcommand().add_option(dpp::command_option(dpp::command_option_type::co_string, "color", "Farbcode oder Name", true))), [&](const dpp::slashcommand_t& event) {
|
||
auto color_str = std::get<std::string>(event.get_parameter("color"));
|
||
auto color_code = Util::str_to_color(color_str);
|
||
if (color_code == -1U) {
|
||
event.reply(dpp::message("Farbe ist ungültig. Versuche es mit einem Farbcode.").set_flags(dpp::message_flags::m_ephemeral));
|
||
return;
|
||
}
|
||
// Set user color in database
|
||
db_add_user(event.command.usr.id);
|
||
bot->db << "UPDATE globalchat_user_settings "
|
||
"SET color = ? "
|
||
"WHERE id = ?;"
|
||
<< color_code
|
||
<< std::to_string(event.command.usr.id);
|
||
// Send success reply
|
||
event.reply(dpp::message().add_embed(dpp::embed().set_title("Deine Farbe im Globalchat wurde festgelegt!").set_color(color_code).set_image("https://singlecolorimage.com/get/"+Util::int_as_hex(color_code)+"/400x100")).set_flags(dpp::message_flags::m_ephemeral));
|
||
});
|
||
bot->add_chatcommand(Bot::ChatCommand({"gc_ban", "globalchat_ban"}, "Verbanne jemanden aus dem Globalchat", dpp::slashcommand().add_option(dpp::command_option(dpp::command_option_type::co_user, "user", "Benutzer", true))), [&](const dpp::slashcommand_t& event) {
|
||
auto user_id = std::get<dpp::snowflake>(event.get_parameter("user"));
|
||
// Set user to banned
|
||
db_add_user(user_id);
|
||
ban_user(user_id);
|
||
// Report success
|
||
event.reply(dpp::message("Okay!").set_flags(dpp::message_flags::m_ephemeral));
|
||
}, bot->config.management_guild_id);
|
||
bot->add_chatcommand(Bot::ChatCommand({"gc_unban", "globalchat_unban"}, "Entbanne jemanden aus dem Globalchat", dpp::slashcommand().add_option(dpp::command_option(dpp::command_option_type::co_user, "user", "Benutzer", true))), [&](const dpp::slashcommand_t& event) {
|
||
auto user_id = std::get<dpp::snowflake>(event.get_parameter("user"));
|
||
// Set user to non-banned
|
||
unban_user(user_id);
|
||
// Report success
|
||
event.reply(dpp::message("Okay!").set_flags(dpp::message_flags::m_ephemeral));
|
||
}, bot->config.management_guild_id);
|
||
bot->add_messagecommand(Bot::MessageCommand({"GC Ban"}, "Verbanne den Sender der Nachricht"), [&](const dpp::message_context_menu_t& event) {
|
||
const auto& target_message = event.get_message();
|
||
// Get message info
|
||
auto messageinfo = GlobalchatExternal::MessageInfo::decode_message(target_message);
|
||
// Ban sender
|
||
db_add_user(messageinfo.author);
|
||
ban_user(messageinfo.author);
|
||
// Report success
|
||
event.reply(dpp::message("Okay!").set_flags(dpp::message_flags::m_ephemeral));
|
||
}, bot->config.management_guild_id);
|
||
bot->add_messagecommand(Bot::MessageCommand({"GC Unban"}, "Entbanne den Sender der Nachricht"), [&](const dpp::message_context_menu_t& event) {
|
||
const auto& target_message = event.get_message();
|
||
// Get message info
|
||
auto messageinfo = GlobalchatExternal::MessageInfo::decode_message(target_message);
|
||
// Ban sender
|
||
unban_user(messageinfo.author);
|
||
// Report success
|
||
event.reply(dpp::message("Okay!").set_flags(dpp::message_flags::m_ephemeral));
|
||
}, bot->config.management_guild_id);
|
||
bot->add_messagecommand(Bot::MessageCommand({"GC Nachricht löschen"}, "Lösche eine Nachricht im Globalchat"), [&](const dpp::message_context_menu_t& event) {
|
||
const auto& target_message = event.get_message();
|
||
// Get message ID
|
||
GlobalchatExternal::MessageInfo messageinfo;
|
||
try {
|
||
messageinfo = GlobalchatExternal::MessageInfo::decode_message(target_message);
|
||
} catch (GlobalchatExternal::MessageInfo::DecodeError& e) {
|
||
event.reply(dpp::message("Fehler beim Dekodieren der Nachricht. Ist sie wirklich Teil des Globalchats?").set_flags(dpp::message_flags::m_ephemeral));
|
||
return;
|
||
}
|
||
const auto& [author_id, primary_id, channel_id, guild_id] = messageinfo;
|
||
// Check for error
|
||
if (primary_id.empty()) {
|
||
event.reply(dpp::message("Ich konnte diese Nachricht nicht identifizieren. Möglicherweise ist sie kein Teil des Globalchats, das Embed wurde gelöscht oder sie ist zu alt.").set_flags(dpp::message_flags::m_ephemeral));
|
||
return;
|
||
}
|
||
// Check if user is allowed to delete this message
|
||
if (event.command.guild_id != bot->config.management_guild_id && dpp::snowflake(author_id) != event.command.get_issuing_user().id) {
|
||
event.reply(dpp::message("Du darfst diese Nachricht nicht löschen.").set_flags(dpp::message_flags::m_ephemeral));
|
||
return;
|
||
}
|
||
// Delete message everywhere
|
||
auto [bots, bots_lock] = bot->get_locked_property<std::vector<Bot*>>("main_all_instances");
|
||
for (auto bot : *bots) {
|
||
// Make sure module is enabled
|
||
if (!bot->is_module_enabled("Globalchat")) {
|
||
continue;
|
||
}
|
||
// Get all messages broadcasted
|
||
bot->db << "SELECT id, guild_id FROM globalchat_messages "
|
||
"WHERE primary_id = ?;"
|
||
<< std::to_string(primary_id)
|
||
>> [&, bot](std::string message_id_str, std::string guild_id_str) {
|
||
std::string channel_id_str;
|
||
bot->db << "SELECT globalchat_channel FROM globalchat_guild_settings "
|
||
"WHERE id = ?;"
|
||
<< guild_id_str
|
||
>> channel_id_str;
|
||
// If guild is management guild, only mark it as deleted, otherwise delete
|
||
if (guild_id_str != std::to_string(bot->config.management_guild_id)) {
|
||
bot->cluster.message_delete(message_id_str, channel_id_str);
|
||
} else {
|
||
bot->cluster.message_get(message_id_str, channel_id_str, [bot](const dpp::confirmation_callback_t& ccb) {
|
||
if (ccb.is_error()) {
|
||
return;
|
||
}
|
||
auto msg = ccb.get<dpp::message>();
|
||
msg.set_content(msg.content+"\n*(gelöscht)*");
|
||
bot->cluster.message_edit(msg);
|
||
});
|
||
}
|
||
};
|
||
}
|
||
// Report success
|
||
event.reply(dpp::message("Okay!").set_flags(dpp::message_flags::m_ephemeral));
|
||
});
|
||
}
|
||
};
|
||
BOT_ADD_MODULE(Globalchat);
|