Finish refactoring
This commit is contained in:
Generated
+24
@@ -856,6 +856,15 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linkify"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@@ -871,6 +880,9 @@ name = "log"
|
|||||||
version = "0.4.27"
|
version = "0.4.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lz4_flex"
|
name = "lz4_flex"
|
||||||
@@ -989,8 +1001,11 @@ dependencies = [
|
|||||||
"colored",
|
"colored",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
|
"linkify",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"rand",
|
||||||
|
"rand_chacha",
|
||||||
"scc",
|
"scc",
|
||||||
"scylla",
|
"scylla",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
@@ -1000,6 +1015,8 @@ dependencies = [
|
|||||||
"twilight-http",
|
"twilight-http",
|
||||||
"twilight-model",
|
"twilight-model",
|
||||||
"twilight-util",
|
"twilight-util",
|
||||||
|
"twilight-validate",
|
||||||
|
"unidecode",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1331,6 +1348,7 @@ dependencies = [
|
|||||||
"snap",
|
"snap",
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uuid",
|
"uuid",
|
||||||
"yoke",
|
"yoke",
|
||||||
@@ -1842,6 +1860,12 @@ version = "1.0.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unidecode"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "402bb19d8e03f1d1a7450e2bd613980869438e0666331be3e073089124aa1adc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|||||||
+8
-3
@@ -5,14 +5,17 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.88"
|
||||||
clap = { version = "4.5.42", features = ["derive", "env"] }
|
clap = { version = "4.5.42", features = ["derive", "env", "string"] }
|
||||||
colored = "3.0.0"
|
colored = "3.0.0"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
include_dir = "0.7.4"
|
include_dir = "0.7.4"
|
||||||
log = "0.4.27"
|
linkify = "0.10.0"
|
||||||
|
log = { version = "0.4.27", features = ["serde"] }
|
||||||
once_cell = "1.21.3"
|
once_cell = "1.21.3"
|
||||||
|
rand = "0.9.2"
|
||||||
|
rand_chacha = { version = "0.9.0", features = ["os_rng"] }
|
||||||
scc = "2.3.4"
|
scc = "2.3.4"
|
||||||
scylla = "1.3.1"
|
scylla = { version = "1.3.1", features = ["time-03"] }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
time = { version = "0.3.41", features = ["formatting", "local-offset", "macros"] }
|
time = { version = "0.3.41", features = ["formatting", "local-offset", "macros"] }
|
||||||
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||||
@@ -20,3 +23,5 @@ twilight-gateway = "0.16.0"
|
|||||||
twilight-http = { version = "0.16.0", features = ["simd-json"] }
|
twilight-http = { version = "0.16.0", features = ["simd-json"] }
|
||||||
twilight-model = "0.16.0"
|
twilight-model = "0.16.0"
|
||||||
twilight-util = { version = "0.16.0", features = ["builder"] }
|
twilight-util = { version = "0.16.0", features = ["builder"] }
|
||||||
|
twilight-validate = "0.16.0"
|
||||||
|
unidecode = "0.3.0"
|
||||||
|
|||||||
+45
@@ -1,5 +1,50 @@
|
|||||||
#[derive(clap::Parser)]
|
#[derive(clap::Parser)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
|
/// The Discord bot token to run the bot with.
|
||||||
#[arg(env = "PAYPHONE_TOKEN")]
|
#[arg(env = "PAYPHONE_TOKEN")]
|
||||||
pub token: String,
|
pub token: String,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
short,
|
||||||
|
value_enum,
|
||||||
|
default_value_t = LevelFilter::Info,
|
||||||
|
env = "PAYPHONE_LOGGING",
|
||||||
|
value_name = "LEVEL",
|
||||||
|
long_help = "The minimum level logs have to be to be outputted into the console, or off for no logs."
|
||||||
|
)]
|
||||||
|
pub logging: LevelFilter,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
short,
|
||||||
|
env = "PAYPHONE_DATABASE_URL",
|
||||||
|
value_name = "URL",
|
||||||
|
default_value_t = String::from("127.0.0.1:9042"))
|
||||||
|
]
|
||||||
|
pub database_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
|
||||||
|
pub enum LevelFilter {
|
||||||
|
Off,
|
||||||
|
Trace,
|
||||||
|
Debug,
|
||||||
|
Info,
|
||||||
|
Warn,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::from_over_into)]
|
||||||
|
impl Into<log::LevelFilter> for LevelFilter {
|
||||||
|
fn into(self) -> log::LevelFilter {
|
||||||
|
match self {
|
||||||
|
Self::Off => log::LevelFilter::Off,
|
||||||
|
Self::Trace => log::LevelFilter::Trace,
|
||||||
|
Self::Debug => log::LevelFilter::Debug,
|
||||||
|
Self::Info => log::LevelFilter::Info,
|
||||||
|
Self::Warn => log::LevelFilter::Warn,
|
||||||
|
Self::Error => log::LevelFilter::Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,221 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use twilight_model::channel::message::Embed;
|
|
||||||
use twilight_util::builder::embed::{
|
|
||||||
EmbedAuthorBuilder, EmbedBuilder, EmbedFieldBuilder, EmbedFooterBuilder, ImageSource,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
commands::{COMMANDS, PREFIX, get_command_by_name},
|
|
||||||
constants::{BOT_AVATAR, BOT_NAME, COLOR_RED, COLOR_WHITE},
|
|
||||||
framework::{
|
|
||||||
commands::{
|
|
||||||
Command, CommandError,
|
|
||||||
arguments::{Argument, NeedyString, ParseArguments},
|
|
||||||
context::CommandContext,
|
|
||||||
},
|
|
||||||
interactions::{InteractionError, components::Button, context::InteractionContext},
|
|
||||||
utils::EscapeDiscordMarkdown,
|
|
||||||
},
|
|
||||||
utils::ToTitleCase,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct HelpCommand;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Command for HelpCommand {
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"help"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn arguments(&self) -> &'static [Argument] {
|
|
||||||
&[Argument {
|
|
||||||
name: "command",
|
|
||||||
is_optional: true,
|
|
||||||
is_greedy: false,
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> &'static str {
|
|
||||||
"Lists all commands and shows information about them individually."
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(&self, ctx: &CommandContext, args: &str) -> Result<(), CommandError> {
|
|
||||||
let command_name: Option<NeedyString> = ParseArguments::parse(args).unwrap();
|
|
||||||
|
|
||||||
if let Some(command_name) = command_name {
|
|
||||||
if let Some(command) = get_command_by_name(&command_name) {
|
|
||||||
ctx.send()
|
|
||||||
.embed(get_command_help_embed(&command))
|
|
||||||
.component(HelpGoBackButton, 1)
|
|
||||||
.component(HelpSupportLinkButton, 1)
|
|
||||||
.await
|
|
||||||
.inspect_err(|e| log::error!("Failed to send message! {e}"))
|
|
||||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
|
||||||
} else {
|
|
||||||
ctx.send()
|
|
||||||
.embed(get_command_not_found_embed(command_name.0))
|
|
||||||
.component(HelpGoBackButton, 1)
|
|
||||||
.component(HelpSupportLinkButton, 1)
|
|
||||||
.await
|
|
||||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.send()
|
|
||||||
.embed(get_all_command_help_embed())
|
|
||||||
.component(HelpWebsiteLinkButton, 1)
|
|
||||||
.component(HelpSupportLinkButton, 1)
|
|
||||||
.component(HelpDonateLinkButton, 1)
|
|
||||||
.await
|
|
||||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_command_help_embed(command: &Arc<dyn Command>) -> Embed {
|
|
||||||
let name = format!("{PREFIX}{}", command.name());
|
|
||||||
let arguments = command
|
|
||||||
.arguments()
|
|
||||||
.iter()
|
|
||||||
.map(|arg| arg.to_string())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
let usage = if !arguments.is_empty() {
|
|
||||||
format!("{name} {arguments}")
|
|
||||||
} else {
|
|
||||||
name
|
|
||||||
};
|
|
||||||
|
|
||||||
EmbedBuilder::new()
|
|
||||||
.author(
|
|
||||||
EmbedAuthorBuilder::new(BOT_NAME)
|
|
||||||
.icon_url(ImageSource::url(BOT_AVATAR).unwrap())
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.thumbnail(ImageSource::url(BOT_AVATAR).unwrap())
|
|
||||||
.color(COLOR_WHITE)
|
|
||||||
.title(format!("{} Command", command.name().to_title_case()))
|
|
||||||
.description(command.description())
|
|
||||||
.field(EmbedFieldBuilder::new("Usage", format!("`{usage}`")).build())
|
|
||||||
.field(EmbedFieldBuilder::new(
|
|
||||||
"Aliases",
|
|
||||||
if !command.aliases().is_empty() {
|
|
||||||
command
|
|
||||||
.aliases()
|
|
||||||
.iter()
|
|
||||||
.map(|alias| format!("`{PREFIX}{alias}`"))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
} else {
|
|
||||||
String::from("*This command doesn't have aliases.*")
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.footer(EmbedFooterBuilder::new(format!(
|
|
||||||
"Use {PREFIX}help <command> to get help about a specific command."
|
|
||||||
)))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_all_command_help_embed() -> Embed {
|
|
||||||
EmbedBuilder::new()
|
|
||||||
.author(EmbedAuthorBuilder::new(BOT_NAME).icon_url(ImageSource::url(BOT_AVATAR).unwrap()))
|
|
||||||
.title("Pick up the phone and start making friends!")
|
|
||||||
.description(format!("Here is the list of all available commands. You can see more details about a specific command with `{PREFIX}help <command>`."))
|
|
||||||
.thumbnail(ImageSource::url(BOT_AVATAR).unwrap())
|
|
||||||
.color(COLOR_WHITE)
|
|
||||||
.field(EmbedFieldBuilder::new("Commands", get_commands_table()).build())
|
|
||||||
.footer(EmbedFooterBuilder::new(format!("v{} - Use {PREFIX}help <command> to get more information about a specific command.", env!("CARGO_PKG_VERSION"))))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_commands_table() -> String {
|
|
||||||
let mut table = String::new();
|
|
||||||
|
|
||||||
for command in COMMANDS.iter() {
|
|
||||||
table.push_str(&format!("{:<12}", command.name()));
|
|
||||||
}
|
|
||||||
|
|
||||||
format!("```\n{table}\n```")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_command_not_found_embed(name: String) -> Embed {
|
|
||||||
EmbedBuilder::new()
|
|
||||||
.color(COLOR_RED)
|
|
||||||
.author(EmbedAuthorBuilder::new(BOT_NAME).icon_url(ImageSource::url(BOT_AVATAR).unwrap()))
|
|
||||||
.title("That's not a command")
|
|
||||||
.description(format!("`{}` is not a valid command name (nor alias). If you put it with the prefix on, take the prefix off before trying again. For example, if you did `p.help p.call`, do `p.help call` instead.\n\nIf you still can't figure it out, press the button below to go back to the full list. Everything's there!", name.escape()))
|
|
||||||
.thumbnail(ImageSource::url(BOT_AVATAR).unwrap())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct HelpGoBackButton;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Button for HelpGoBackButton {
|
|
||||||
fn label(&self) -> Option<&'static str> {
|
|
||||||
Some("← Go back to commands list")
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn on_press(&self, ctx: InteractionContext) -> Result<(), InteractionError> {
|
|
||||||
ctx
|
|
||||||
.original_message()
|
|
||||||
.expect(
|
|
||||||
"Buttons always have the original message in the interaction context, this one didn't (got None)"
|
|
||||||
)
|
|
||||||
.edit()
|
|
||||||
.embed(get_all_command_help_embed())
|
|
||||||
.component(HelpWebsiteLinkButton, 1)
|
|
||||||
.component(HelpSupportLinkButton, 1)
|
|
||||||
.component(HelpDonateLinkButton, 1)
|
|
||||||
.await
|
|
||||||
.inspect_err(|e| log::error!("An error occurred while attempting to edit the original message of an interaction. {e:?}"))
|
|
||||||
.map_err(
|
|
||||||
|_| InteractionError::OriginalMessageEditingFailed
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct HelpSupportLinkButton;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Button for HelpSupportLinkButton {
|
|
||||||
fn label(&self) -> Option<&'static str> {
|
|
||||||
Some("Get help")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn url(&self) -> Option<&'static str> {
|
|
||||||
Some("https://discord.gg/9daSsrfTmE")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct HelpWebsiteLinkButton;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Button for HelpWebsiteLinkButton {
|
|
||||||
fn label(&self) -> Option<&'static str> {
|
|
||||||
Some("Website")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn url(&self) -> Option<&'static str> {
|
|
||||||
Some("https://payphone.lat/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct HelpDonateLinkButton;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Button for HelpDonateLinkButton {
|
|
||||||
fn label(&self) -> Option<&'static str> {
|
|
||||||
Some("Donate")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn url(&self) -> Option<&'static str> {
|
|
||||||
Some("https://ko-fi.com/payphonedev")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
mod about;
|
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use twilight_http::Client;
|
|
||||||
use twilight_model::{gateway::payload::incoming::MessageCreate, oauth::Application};
|
|
||||||
|
|
||||||
use crate::framework::{
|
|
||||||
commands::{context::CommandContext, get_command_parts, Command}, handlers::Handlers, interactions::{components::Button, registry::Registry}
|
|
||||||
};
|
|
||||||
|
|
||||||
const PREFIX: &str = "pd.";
|
|
||||||
|
|
||||||
static COMMANDS: Lazy<Vec<Arc<dyn Command>>> = Lazy::new(|| vec![Arc::new(about::HelpCommand)]);
|
|
||||||
|
|
||||||
pub fn get_command_by_name(name: &str) -> Option<Arc<dyn Command>> {
|
|
||||||
for command in COMMANDS.iter() {
|
|
||||||
if command
|
|
||||||
.identifiers()
|
|
||||||
.contains(&name.to_ascii_lowercase().as_str())
|
|
||||||
{
|
|
||||||
return Some(command.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn execute_command(
|
|
||||||
event: MessageCreate,
|
|
||||||
handlers: Arc<Handlers>
|
|
||||||
) {
|
|
||||||
let parts = get_command_parts(PREFIX, &event.content);
|
|
||||||
|
|
||||||
if let Some(parts) = parts {
|
|
||||||
if let Some(command) = get_command_by_name(parts.name) {
|
|
||||||
let _ = command
|
|
||||||
.execute(
|
|
||||||
&CommandContext::new(&event, handlers),
|
|
||||||
parts.args,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.inspect_err(|e| log::error!("Failed to execute command! {e:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn register_default_components(registry: &Registry) {
|
|
||||||
let components = vec![Arc::new(about::HelpGoBackButton)];
|
|
||||||
|
|
||||||
for component in components {
|
|
||||||
registry.register(
|
|
||||||
component.id().expect(
|
|
||||||
"All components registered by default must have an ID, there's one which didn't",
|
|
||||||
),
|
|
||||||
component,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-2
@@ -1,5 +1,4 @@
|
|||||||
pub const BOT_NAME: &str = "Payphone";
|
pub const DEFAULT_PREFIX: &str = "p.";
|
||||||
pub const BOT_AVATAR: &str = "https://images-ext-1.discordapp.net/external/BymicERFXlYKd52uNU2ihrbUoSm2e5hV3Ae7XY1436o/%3Fsize%3D1024/https/cdn.discordapp.com/avatars/1212940413795827763/f4b7180bb467b035d6e2e6f458e93b7c.png?format=webp&quality=lossless&width=1058&height=1058";
|
|
||||||
|
|
||||||
pub const COLOR_WHITE: u32 = 0xE2D9D0;
|
pub const COLOR_WHITE: u32 = 0xE2D9D0;
|
||||||
pub const COLOR_RED: u32 = 0xFF8787;
|
pub const COLOR_RED: u32 = 0xFF8787;
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ CREATE KEYSPACE IF NOT EXISTS payphone
|
|||||||
WITH replication = {
|
WITH replication = {
|
||||||
'class': 'NetworkTopologyStrategy',
|
'class': 'NetworkTopologyStrategy',
|
||||||
'replication_factor': 3
|
'replication_factor': 3
|
||||||
};
|
} AND TABLETS = {
|
||||||
|
'enabled': false
|
||||||
|
};
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS payphone.users (
|
CREATE TABLE IF NOT EXISTS payphone.users (
|
||||||
user_id BIGINT PRIMARY KEY,
|
user_id BIGINT PRIMARY KEY,
|
||||||
@@ -15,13 +17,15 @@ CREATE TABLE IF NOT EXISTS payphone.users (
|
|||||||
|
|
||||||
joined_at TIMESTAMP,
|
joined_at TIMESTAMP,
|
||||||
last_seen_at TIMESTAMP,
|
last_seen_at TIMESTAMP,
|
||||||
is_supporter_since TIMESTAMP,
|
supporter_since TIMESTAMP,
|
||||||
is_supporter_until TIMESTAMP
|
supporter_until TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS payphone.calls (
|
CREATE TABLE IF NOT EXISTS payphone.calls (
|
||||||
call_id BIGINT PRIMARY KEY,
|
call_id BIGINT PRIMARY KEY,
|
||||||
|
|
||||||
|
channel_ids LIST<BIGINT>,
|
||||||
|
|
||||||
started_at TIMESTAMP,
|
started_at TIMESTAMP,
|
||||||
finished_at TIMESTAMP,
|
finished_at TIMESTAMP,
|
||||||
);
|
);
|
||||||
@@ -30,22 +34,30 @@ CREATE TABLE IF NOT EXISTS payphone.channels (
|
|||||||
channel_id BIGINT PRIMARY KEY,
|
channel_id BIGINT PRIMARY KEY,
|
||||||
server_id BIGINT,
|
server_id BIGINT,
|
||||||
|
|
||||||
name TEXT,
|
call_id BIGINT,
|
||||||
|
|
||||||
is_searching BOOLEAN,
|
is_searching BOOLEAN,
|
||||||
|
|
||||||
recently_called_channel_ids LIST<BIGINT>
|
recently_called_server_ids LIST<BIGINT>,
|
||||||
|
|
||||||
|
webhook_id BIGINT,
|
||||||
|
webhook_token TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW IF NOT EXISTS payphone.searching_channels AS
|
||||||
|
SELECT channel_id, server_id, is_searching, recently_called_server_ids FROM payphone.channels
|
||||||
|
WHERE channel_id IS NOT NULL AND is_searching = true
|
||||||
|
PRIMARY KEY (is_searching, channel_id);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS payphone.participants (
|
CREATE TABLE IF NOT EXISTS payphone.participants (
|
||||||
call_id BIGINT,
|
call_id BIGINT,
|
||||||
user_id BIGINT,
|
user_id BIGINT,
|
||||||
|
channel_id BIGINT,
|
||||||
|
|
||||||
user_name TEXT,
|
user_name TEXT,
|
||||||
user_avatar_url TEXT,
|
user_avatar_url TEXT,
|
||||||
user_mask TEXT,
|
user_mask TEXT,
|
||||||
|
|
||||||
PRIMARY KEY (call_id, user_id)
|
PRIMARY KEY (call_id, user_id, channel_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- TTL 30 days.
|
-- TTL 30 days.
|
||||||
@@ -62,6 +74,9 @@ CREATE TABLE IF NOT EXISTS payphone.messages (
|
|||||||
attachments LIST<TEXT>,
|
attachments LIST<TEXT>,
|
||||||
sticker BIGINT,
|
sticker BIGINT,
|
||||||
|
|
||||||
|
propagation_message_id BIGINT,
|
||||||
|
propagation_channel_id BIGINT,
|
||||||
|
|
||||||
replying_to_id BIGINT,
|
replying_to_id BIGINT,
|
||||||
|
|
||||||
is_deleted BOOLEAN,
|
is_deleted BOOLEAN,
|
||||||
|
|||||||
+54
-7
@@ -1,9 +1,12 @@
|
|||||||
|
pub mod models;
|
||||||
|
|
||||||
use include_dir::{Dir, include_dir};
|
use include_dir::{Dir, include_dir};
|
||||||
use scylla::{
|
use scylla::{
|
||||||
client::{session::Session, session_builder::SessionBuilder},
|
client::{session::Session, session_builder::SessionBuilder},
|
||||||
errors::ExecutionError,
|
errors::ExecutionError,
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum DatabaseError {
|
pub enum DatabaseError {
|
||||||
@@ -44,22 +47,66 @@ pub async fn init(uri: impl ToString) -> Result<Session, DatabaseError> {
|
|||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.inspect(|_| log::info!("The migrations keyspace was successfully set up"))
|
|
||||||
.inspect_err(|e| log::error!("Could not set up migrations keyspace and table! {e:?}"))
|
.inspect_err(|e| log::error!("Could not set up migrations keyspace and table! {e:?}"))
|
||||||
.map_err(|_| DatabaseError::MigrationFailed)?;
|
.map_err(|_| DatabaseError::MigrationFailed)?;
|
||||||
|
|
||||||
for file in MIGRATIONS.files() {
|
let mut sorted_migration_files = MIGRATIONS.files().collect::<Vec<_>>();
|
||||||
|
sorted_migration_files.sort_by(|a, b| {
|
||||||
|
a.path()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
.cmp(&b.path().to_string_lossy().to_string())
|
||||||
|
});
|
||||||
|
|
||||||
|
for file in sorted_migration_files {
|
||||||
|
let migration_name = file.path().to_string_lossy().to_string();
|
||||||
|
|
||||||
|
let row = session
|
||||||
|
.query_unpaged(
|
||||||
|
"SELECT name FROM migrations.applied WHERE name = ? LIMIT 1;",
|
||||||
|
(&migration_name,),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!(
|
||||||
|
"Could not check whether the migration \"{migration_name}\" was applied! {e:?}"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map_err(|_| DatabaseError::MigrationFailed)?
|
||||||
|
.into_rows_result()
|
||||||
|
.inspect_err(|e| log::error!("The applied migrations' database result were not rows! {e:?}"))
|
||||||
|
.map_err(|_| DatabaseError::MigrationFailed)?
|
||||||
|
.maybe_first_row::<(String,)>()
|
||||||
|
.inspect_err(|e| log::error!("The applied migrations' row returned by the database could not be parsed! {e:?}"))
|
||||||
|
.map_err(|_| DatabaseError::MigrationFailed)?;
|
||||||
|
|
||||||
|
if row.is_none() {
|
||||||
execute_file(
|
execute_file(
|
||||||
&session,
|
&session,
|
||||||
file.contents_utf8()
|
file.contents_utf8()
|
||||||
.ok_or(DatabaseError::MigrationFileInvalid(
|
.ok_or(DatabaseError::MigrationFileInvalid(migration_name.clone()))?
|
||||||
file.path().to_string_lossy().to_string(),
|
|
||||||
))?
|
|
||||||
.to_string(),
|
.to_string(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.inspect_err(|e| log::error!("Could not migrate {}! {e:?}", file.path().to_str().unwrap()))
|
.inspect(|_| log::debug!("Migration {migration_name} applied!"))
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("Could not migrate {}! {e:?}", file.path().to_str().unwrap())
|
||||||
|
})
|
||||||
.map_err(|_| DatabaseError::MigrationFailed)?;
|
.map_err(|_| DatabaseError::MigrationFailed)?;
|
||||||
|
|
||||||
|
session
|
||||||
|
.query_unpaged(
|
||||||
|
"INSERT INTO migrations.applied (name, applied_at) VALUES (?, ?);",
|
||||||
|
(migration_name.clone(), OffsetDateTime::now_utc()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("Could not mark migration {migration_name} as applied in DB! {e:?}")
|
||||||
|
})
|
||||||
|
.map_err(|_| DatabaseError::MigrationFailed)?;
|
||||||
|
} else {
|
||||||
|
log::trace!("Skipped migration {migration_name} because it was already applied.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(session)
|
Ok(session)
|
||||||
@@ -85,7 +132,7 @@ async fn execute_file(session: &Session, file: String) -> Result<(), FileExecuti
|
|||||||
.await
|
.await
|
||||||
.map_err(|error| FileExecutionError {
|
.map_err(|error| FileExecutionError {
|
||||||
error,
|
error,
|
||||||
statement_index,
|
statement_index: statement_index + 1,
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,828 @@
|
|||||||
|
use scylla::{
|
||||||
|
client::session::Session,
|
||||||
|
deserialize::row::DeserializeRow,
|
||||||
|
errors::{ExecutionError, SingleRowError},
|
||||||
|
statement::{
|
||||||
|
Statement,
|
||||||
|
batch::{Batch, BatchType},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use thiserror::Error;
|
||||||
|
use time::{OffsetDateTime, UtcDateTime};
|
||||||
|
use twilight_http::Client;
|
||||||
|
use twilight_model::{
|
||||||
|
guild::{Guild as TwilightGuild, GuildInfo as TwilightGuildInfo},
|
||||||
|
id::Id,
|
||||||
|
user::User as TwilightUser,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{constants::DEFAULT_PREFIX, utils::get_random_u64};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DatabaseOrDiscordError {
|
||||||
|
#[error("An error occurred when querying the database")]
|
||||||
|
Database(#[from] DatabaseError),
|
||||||
|
|
||||||
|
#[error("An error occurred when querying the Discord API")]
|
||||||
|
Discord(#[from] twilight_http::Error),
|
||||||
|
|
||||||
|
#[error("The Discord API returned a response that could not be deserialized")]
|
||||||
|
DiscordDeserializationFailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DatabaseError {
|
||||||
|
#[error("An error occurred when querying the database")]
|
||||||
|
QueryError(ExecutionError),
|
||||||
|
|
||||||
|
#[error("The database responded but the result of the query weren't rows")]
|
||||||
|
ResultNotRows,
|
||||||
|
|
||||||
|
#[error("The resulting row could not be deserialized into an object")]
|
||||||
|
DeserializationFailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies the bytes from [`i64`] to [`u64`].
|
||||||
|
///
|
||||||
|
/// WARNING: This does NOT keep the same "represented" value. This only keeps the bytes intact,
|
||||||
|
/// e.g. 0b0101 will still be 0b0101, but the value represented will change since the type changes.
|
||||||
|
///
|
||||||
|
/// Arguments:
|
||||||
|
/// * `i` - The [`i64`] to convert.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`u64`] - The resulting [`u64`].
|
||||||
|
fn i64_to_u64(i: i64) -> u64 {
|
||||||
|
u64::from_ne_bytes(i.to_ne_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies the bytes from [`u64`] to [`i64`].
|
||||||
|
///
|
||||||
|
/// WARNING: This does NOT keep the same "represented" value. This only keeps the bytes intact,
|
||||||
|
/// e.g. 0b0101 will still be 0b0101, but the value represented will change since the type changes.
|
||||||
|
///
|
||||||
|
/// Arguments:
|
||||||
|
/// * `u` - The [`u64`] to convert.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`i64`] - The resulting [`i64`].
|
||||||
|
fn u64_to_i64(u: u64) -> i64 {
|
||||||
|
i64::from_ne_bytes(u.to_ne_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Object<'a> {
|
||||||
|
type Row: DeserializeRow<'a, 'a>;
|
||||||
|
|
||||||
|
fn from_row(row: Self::Row) -> Self;
|
||||||
|
fn to_row(value: Self) -> Self::Row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: String,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub is_masked: bool,
|
||||||
|
pub is_moderator: bool,
|
||||||
|
pub joined_at: UtcDateTime,
|
||||||
|
pub last_seen_at: UtcDateTime,
|
||||||
|
pub supporter_since: Option<UtcDateTime>,
|
||||||
|
pub supporter_until: Option<UtcDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Object<'a> for User {
|
||||||
|
type Row = (
|
||||||
|
i64,
|
||||||
|
String,
|
||||||
|
Option<String>,
|
||||||
|
bool,
|
||||||
|
bool,
|
||||||
|
OffsetDateTime,
|
||||||
|
OffsetDateTime,
|
||||||
|
Option<OffsetDateTime>,
|
||||||
|
Option<OffsetDateTime>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fn from_row(row: Self::Row) -> Self {
|
||||||
|
Self {
|
||||||
|
id: i64_to_u64(row.0),
|
||||||
|
name: row.1,
|
||||||
|
email: row.2,
|
||||||
|
is_masked: row.3,
|
||||||
|
is_moderator: row.4,
|
||||||
|
joined_at: row.5.into(),
|
||||||
|
last_seen_at: row.6.into(),
|
||||||
|
supporter_since: row.7.map(|i| i.into()),
|
||||||
|
supporter_until: row.8.map(|i| i.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_row(value: Self) -> Self::Row {
|
||||||
|
(
|
||||||
|
u64_to_i64(value.id),
|
||||||
|
value.name,
|
||||||
|
value.email,
|
||||||
|
value.is_masked,
|
||||||
|
value.is_moderator,
|
||||||
|
value.joined_at.into(),
|
||||||
|
value.last_seen_at.into(),
|
||||||
|
value.supporter_since.map(|i| i.into()),
|
||||||
|
value.supporter_until.map(|i| i.into()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub async fn get_by_id(id: u64, session: &Session) -> Result<Option<Self>, DatabaseError> {
|
||||||
|
let id = u64_to_i64(id);
|
||||||
|
|
||||||
|
let result = session
|
||||||
|
.query_unpaged(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
is_masked,
|
||||||
|
is_moderator,
|
||||||
|
joined_at,
|
||||||
|
last_seen_at,
|
||||||
|
supporter_since,
|
||||||
|
supporter_until
|
||||||
|
FROM
|
||||||
|
payphone.users
|
||||||
|
WHERE
|
||||||
|
user_id = ?
|
||||||
|
LIMIT 1;
|
||||||
|
"#,
|
||||||
|
(id,),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::QueryError)?
|
||||||
|
.into_rows_result()
|
||||||
|
.map_err(|_| DatabaseError::ResultNotRows)?;
|
||||||
|
|
||||||
|
match result.single_row::<<User as Object>::Row>() {
|
||||||
|
Ok(row) => Ok(Some(Self::from_row(row))),
|
||||||
|
Err(error) => match error {
|
||||||
|
SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => {
|
||||||
|
Err(DatabaseError::DeserializationFailed)
|
||||||
|
}
|
||||||
|
SingleRowError::UnexpectedRowCount(_) => Ok(None),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TwilightUser> for User {
|
||||||
|
fn from(value: TwilightUser) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id.get(),
|
||||||
|
name: value.name,
|
||||||
|
email: value.email,
|
||||||
|
is_masked: false,
|
||||||
|
is_moderator: false,
|
||||||
|
joined_at: UtcDateTime::now(),
|
||||||
|
last_seen_at: UtcDateTime::now(),
|
||||||
|
supporter_since: None,
|
||||||
|
supporter_until: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Server {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: String,
|
||||||
|
pub prefix: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Object<'a> for Server {
|
||||||
|
type Row = (i64, String, String);
|
||||||
|
|
||||||
|
fn from_row(row: Self::Row) -> Self {
|
||||||
|
Self {
|
||||||
|
id: i64_to_u64(row.0),
|
||||||
|
name: row.1,
|
||||||
|
prefix: row.2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_row(value: Self) -> Self::Row {
|
||||||
|
(u64_to_i64(value.id), value.name, value.prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
pub async fn get_by_id(id: u64, session: &Session) -> Result<Option<Self>, DatabaseError> {
|
||||||
|
let id = u64_to_i64(id);
|
||||||
|
|
||||||
|
let result = session
|
||||||
|
.query_unpaged(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
server_id,
|
||||||
|
name,
|
||||||
|
prefix
|
||||||
|
FROM
|
||||||
|
payphone.servers
|
||||||
|
WHERE
|
||||||
|
server_id = ?
|
||||||
|
LIMIT 1;
|
||||||
|
"#,
|
||||||
|
(id,),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::QueryError)?
|
||||||
|
.into_rows_result()
|
||||||
|
.map_err(|_| DatabaseError::ResultNotRows)?;
|
||||||
|
|
||||||
|
match result.single_row::<<Server as Object>::Row>() {
|
||||||
|
Ok(row) => Ok(Some(Self::from_row(row))),
|
||||||
|
Err(error) => match error {
|
||||||
|
SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => {
|
||||||
|
Err(DatabaseError::DeserializationFailed)
|
||||||
|
}
|
||||||
|
SingleRowError::UnexpectedRowCount(_) => Ok(None),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
|
||||||
|
session
|
||||||
|
.query_unpaged(
|
||||||
|
r#"
|
||||||
|
INSERT INTO payphone.servers (
|
||||||
|
server_id,
|
||||||
|
name,
|
||||||
|
prefix
|
||||||
|
) VALUES (?, ?, ?) IF NOT EXISTS;
|
||||||
|
"#,
|
||||||
|
Self::to_row(self.clone()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("{e:?}"))
|
||||||
|
.map_err(DatabaseError::QueryError)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upsert(&self, session: &Session) -> Result<(), DatabaseError> {
|
||||||
|
session
|
||||||
|
.query_unpaged(
|
||||||
|
r#"
|
||||||
|
INSERT INTO payphone.servers (
|
||||||
|
server_id,
|
||||||
|
name,
|
||||||
|
prefix
|
||||||
|
) VALUES (
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?
|
||||||
|
);
|
||||||
|
"#,
|
||||||
|
Self::to_row(self.clone()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::QueryError)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_by_id(id: u64, session: &Session) -> Result<(), DatabaseError> {
|
||||||
|
session
|
||||||
|
.query_unpaged(
|
||||||
|
"DELETE FROM payphone.servers WHERE server_id = ?;",
|
||||||
|
(u64_to_i64(id),),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::QueryError)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TwilightGuild> for Server {
|
||||||
|
fn from(value: TwilightGuild) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id.get(),
|
||||||
|
name: value.name,
|
||||||
|
prefix: DEFAULT_PREFIX.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TwilightGuildInfo> for Server {
|
||||||
|
fn from(value: TwilightGuildInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id.get(),
|
||||||
|
name: value.name,
|
||||||
|
prefix: DEFAULT_PREFIX.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Channel {
|
||||||
|
pub id: u64,
|
||||||
|
pub server_id: u64,
|
||||||
|
pub call_id: Option<u64>,
|
||||||
|
pub is_searching: bool,
|
||||||
|
pub recently_called_server_ids: Vec<u64>,
|
||||||
|
pub webhook_id: Option<u64>,
|
||||||
|
pub webhook_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Object<'a> for Channel {
|
||||||
|
type Row = (
|
||||||
|
i64,
|
||||||
|
i64,
|
||||||
|
Option<i64>,
|
||||||
|
bool,
|
||||||
|
Vec<i64>,
|
||||||
|
Option<i64>,
|
||||||
|
Option<String>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fn from_row(row: Self::Row) -> Self {
|
||||||
|
Self {
|
||||||
|
id: i64_to_u64(row.0),
|
||||||
|
server_id: i64_to_u64(row.1),
|
||||||
|
call_id: row.2.map(i64_to_u64),
|
||||||
|
is_searching: row.3,
|
||||||
|
recently_called_server_ids: row.4.iter().map(|&i| i64_to_u64(i)).collect(),
|
||||||
|
webhook_id: row.5.map(i64_to_u64),
|
||||||
|
webhook_token: row.6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_row(value: Self) -> Self::Row {
|
||||||
|
(
|
||||||
|
u64_to_i64(value.id),
|
||||||
|
u64_to_i64(value.server_id),
|
||||||
|
value.call_id.map(u64_to_i64),
|
||||||
|
value.is_searching,
|
||||||
|
value
|
||||||
|
.recently_called_server_ids
|
||||||
|
.iter()
|
||||||
|
.map(|&u| u64_to_i64(u))
|
||||||
|
.collect(),
|
||||||
|
value.webhook_id.map(u64_to_i64),
|
||||||
|
value.webhook_token,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Channel {
|
||||||
|
pub async fn get_by_id(id: u64, session: &Session) -> Result<Option<Self>, DatabaseError> {
|
||||||
|
let id = u64_to_i64(id);
|
||||||
|
|
||||||
|
let result = session
|
||||||
|
.query_unpaged(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
channel_id,
|
||||||
|
server_id,
|
||||||
|
call_id,
|
||||||
|
is_searching,
|
||||||
|
recently_called_server_ids,
|
||||||
|
webhook_id,
|
||||||
|
webhook_token
|
||||||
|
FROM
|
||||||
|
payphone.channels
|
||||||
|
WHERE
|
||||||
|
channel_id = ?
|
||||||
|
LIMIT 1;
|
||||||
|
"#,
|
||||||
|
(id,),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::QueryError)?
|
||||||
|
.into_rows_result()
|
||||||
|
.map_err(|_| DatabaseError::ResultNotRows)?;
|
||||||
|
|
||||||
|
match result.single_row::<<Channel as Object>::Row>() {
|
||||||
|
Ok(row) => Ok(Some(Self::from_row(row))),
|
||||||
|
Err(error) => match error {
|
||||||
|
SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => {
|
||||||
|
Err(DatabaseError::DeserializationFailed)
|
||||||
|
}
|
||||||
|
SingleRowError::UnexpectedRowCount(_) => Ok(None),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_or_create_by_id(
|
||||||
|
id: u64,
|
||||||
|
session: &Session,
|
||||||
|
http: &Client,
|
||||||
|
) -> Result<Self, DatabaseOrDiscordError> {
|
||||||
|
if let Some(channel) = Channel::get_by_id(id, session)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseOrDiscordError::Database)?
|
||||||
|
{
|
||||||
|
Ok(channel)
|
||||||
|
} else {
|
||||||
|
let webhook = http
|
||||||
|
.create_webhook(Id::new(id), "Payphone Messages")
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to create webhook for channel {id}: {e:?}"))
|
||||||
|
.map_err(DatabaseOrDiscordError::Discord)?
|
||||||
|
.model()
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to get model for webhook {id}: {e:?}"))
|
||||||
|
.map_err(|_| DatabaseOrDiscordError::DiscordDeserializationFailed)?;
|
||||||
|
|
||||||
|
let response = http
|
||||||
|
.channel(Id::new(id))
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseOrDiscordError::Discord)?
|
||||||
|
.model()
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("The discord API didn't return a parseable channel! {e:?}")
|
||||||
|
})
|
||||||
|
.map_err(|_| DatabaseOrDiscordError::DiscordDeserializationFailed)?;
|
||||||
|
|
||||||
|
let channel = Channel {
|
||||||
|
id: response.id.get(),
|
||||||
|
server_id: response.guild_id.map(|i| i.get()).unwrap_or(0),
|
||||||
|
call_id: None,
|
||||||
|
is_searching: false,
|
||||||
|
recently_called_server_ids: vec![],
|
||||||
|
webhook_id: Some(webhook.id.get()),
|
||||||
|
webhook_token: webhook.token,
|
||||||
|
};
|
||||||
|
channel
|
||||||
|
.upsert(session)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Could not upsert channel in database! {e:?}"))
|
||||||
|
.map_err(DatabaseOrDiscordError::Database)?;
|
||||||
|
|
||||||
|
Ok(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upsert(&self, session: &Session) -> Result<(), DatabaseError> {
|
||||||
|
session
|
||||||
|
.query_unpaged(
|
||||||
|
r#"
|
||||||
|
INSERT INTO payphone.channels (
|
||||||
|
channel_id,
|
||||||
|
server_id,
|
||||||
|
call_id,
|
||||||
|
is_searching,
|
||||||
|
recently_called_server_ids,
|
||||||
|
webhook_id,
|
||||||
|
webhook_token
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||||
|
"#,
|
||||||
|
Self::to_row(self.clone()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::QueryError)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cancel_search(&self, session: &Session) -> Result<(), DatabaseError> {
|
||||||
|
session
|
||||||
|
.query_unpaged(
|
||||||
|
"UPDATE payphone.channels SET is_searching = false WHERE channel_id = ?;",
|
||||||
|
(u64_to_i64(self.id),),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::QueryError)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SearchingChannel {
|
||||||
|
pub id: u64,
|
||||||
|
pub server_id: u64,
|
||||||
|
pub recently_called_server_ids: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Object<'a> for SearchingChannel {
|
||||||
|
type Row = (i64, i64, Vec<i64>);
|
||||||
|
|
||||||
|
fn from_row(row: Self::Row) -> Self {
|
||||||
|
Self {
|
||||||
|
id: i64_to_u64(row.0),
|
||||||
|
server_id: i64_to_u64(row.1),
|
||||||
|
recently_called_server_ids: row.2.iter().map(|&i| i64_to_u64(i)).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_row(value: Self) -> Self::Row {
|
||||||
|
(
|
||||||
|
u64_to_i64(value.id),
|
||||||
|
u64_to_i64(value.server_id),
|
||||||
|
value
|
||||||
|
.recently_called_server_ids
|
||||||
|
.iter()
|
||||||
|
.map(|&u| u64_to_i64(u))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchingChannel {
|
||||||
|
pub async fn get_all(session: &Session) -> Result<Vec<Self>, DatabaseError> {
|
||||||
|
let result = session
|
||||||
|
.query_unpaged(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
channel_id,
|
||||||
|
server_id,
|
||||||
|
recently_called_server_ids
|
||||||
|
FROM
|
||||||
|
payphone.searching_channels;
|
||||||
|
"#,
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::QueryError)?
|
||||||
|
.into_rows_result()
|
||||||
|
.map_err(|_| DatabaseError::ResultNotRows)?;
|
||||||
|
|
||||||
|
Ok(result
|
||||||
|
.rows::<<SearchingChannel as Object>::Row>()
|
||||||
|
.map_err(|_| DatabaseError::ResultNotRows)?
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|_| DatabaseError::DeserializationFailed)?
|
||||||
|
.into_iter()
|
||||||
|
.map(Self::from_row)
|
||||||
|
.collect::<Vec<_>>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Call {
|
||||||
|
pub id: u64,
|
||||||
|
pub channel_ids: Vec<u64>,
|
||||||
|
pub started_at: UtcDateTime,
|
||||||
|
pub finished_at: Option<UtcDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Object<'a> for Call {
|
||||||
|
type Row = (i64, Vec<i64>, OffsetDateTime, Option<OffsetDateTime>);
|
||||||
|
|
||||||
|
fn from_row(row: Self::Row) -> Self {
|
||||||
|
Self {
|
||||||
|
id: i64_to_u64(row.0),
|
||||||
|
channel_ids: row.1.iter().map(|&i| i64_to_u64(i)).collect(),
|
||||||
|
started_at: row.2.into(),
|
||||||
|
finished_at: row.3.map(|i| i.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_row(value: Self) -> Self::Row {
|
||||||
|
(
|
||||||
|
u64_to_i64(value.id),
|
||||||
|
value.channel_ids.iter().map(|&u| u64_to_i64(u)).collect(),
|
||||||
|
value.started_at.into(),
|
||||||
|
value.finished_at.map(|i| i.into()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_and_cap<T>(vec: &mut Vec<T>, value: T, max_size: usize) {
|
||||||
|
if vec.len() >= max_size {
|
||||||
|
vec.remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Call {
|
||||||
|
pub async fn get_by_id(id: u64, session: &Session) -> Result<Option<Self>, DatabaseError> {
|
||||||
|
let id = u64_to_i64(id);
|
||||||
|
|
||||||
|
let result = session
|
||||||
|
.query_unpaged(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
call_id,
|
||||||
|
channel_ids,
|
||||||
|
started_at,
|
||||||
|
finished_at
|
||||||
|
FROM
|
||||||
|
payphone.calls
|
||||||
|
WHERE
|
||||||
|
call_id = ?
|
||||||
|
LIMIT 1;
|
||||||
|
"#,
|
||||||
|
(id,),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::QueryError)?
|
||||||
|
.into_rows_result()
|
||||||
|
.map_err(|_| DatabaseError::ResultNotRows)?;
|
||||||
|
|
||||||
|
match result.single_row::<<Call as Object>::Row>() {
|
||||||
|
Ok(row) => Ok(Some(Self::from_row(row))),
|
||||||
|
Err(error) => match error {
|
||||||
|
SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => {
|
||||||
|
Err(DatabaseError::DeserializationFailed)
|
||||||
|
}
|
||||||
|
SingleRowError::UnexpectedRowCount(_) => Ok(None),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the opposite party's channel ID.
|
||||||
|
pub fn other_party_channel_id(&self, channel_id: u64) -> u64 {
|
||||||
|
if self.channel_ids[0] == channel_id {
|
||||||
|
self.channel_ids[1]
|
||||||
|
} else {
|
||||||
|
self.channel_ids[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
self.finished_at.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts a call, or marks the channel as searching if the call is not started.
|
||||||
|
///
|
||||||
|
/// Arguments:
|
||||||
|
/// * `channel` - The channel to start the call in.
|
||||||
|
/// * `session` - The Scylla session to use for database operations.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`Result<Option<Self>, DatabaseError>`] - `Ok(Some(Call))` if the call was started
|
||||||
|
/// successfully, or `Ok(None)` if the channel was marked as searching. If an error occurs, it
|
||||||
|
/// returns `Err(DatabaseError)`.
|
||||||
|
pub async fn start(
|
||||||
|
channel: &Channel,
|
||||||
|
session: &Session,
|
||||||
|
) -> Result<Option<Self>, DatabaseError> {
|
||||||
|
let searching_channels = SearchingChannel::get_all(session).await?;
|
||||||
|
|
||||||
|
let call_id = get_random_u64();
|
||||||
|
|
||||||
|
log::trace!("Call ID is {call_id}");
|
||||||
|
|
||||||
|
for searching_channel in searching_channels
|
||||||
|
.iter()
|
||||||
|
.filter(|sc| !sc.recently_called_server_ids.contains(&channel.server_id))
|
||||||
|
{
|
||||||
|
if searching_channel.server_id == channel.server_id {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel
|
||||||
|
.recently_called_server_ids
|
||||||
|
.contains(&searching_channel.server_id)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::trace!("Found a searching channel: {searching_channel:?}");
|
||||||
|
|
||||||
|
let mut searching_channel_recently_called_server_ids = searching_channel
|
||||||
|
.recently_called_server_ids
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(u64_to_i64)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
push_and_cap(
|
||||||
|
&mut searching_channel_recently_called_server_ids,
|
||||||
|
u64_to_i64(channel.server_id),
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (is_applied, ..) = session
|
||||||
|
.query_unpaged(
|
||||||
|
r#"
|
||||||
|
UPDATE payphone.channels
|
||||||
|
SET
|
||||||
|
is_searching = false,
|
||||||
|
call_id = ?,
|
||||||
|
recently_called_server_ids = ?
|
||||||
|
WHERE
|
||||||
|
channel_id = ?
|
||||||
|
IF call_id = NULL;
|
||||||
|
"#,
|
||||||
|
(
|
||||||
|
u64_to_i64(call_id),
|
||||||
|
searching_channel_recently_called_server_ids,
|
||||||
|
u64_to_i64(searching_channel.id),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to add channel to the call! {e:?}"))
|
||||||
|
.map_err(DatabaseError::QueryError)?
|
||||||
|
.into_rows_result()
|
||||||
|
.inspect_err(|e| log::error!("Failed to get result of the query! {e:?}"))
|
||||||
|
.map_err(|_| DatabaseError::ResultNotRows)?
|
||||||
|
.first_row::<(bool, Option<i64>)>()
|
||||||
|
.inspect_err(|e| log::error!("Failed to get first row of the result! {e:?}"))
|
||||||
|
.map_err(|_| DatabaseError::DeserializationFailed)?;
|
||||||
|
|
||||||
|
log::trace!("Is applied: {is_applied:?}");
|
||||||
|
|
||||||
|
if is_applied {
|
||||||
|
let mut channel_recently_called_server_ids = channel
|
||||||
|
.recently_called_server_ids
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(u64_to_i64)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
push_and_cap(
|
||||||
|
&mut channel_recently_called_server_ids,
|
||||||
|
u64_to_i64(searching_channel.server_id),
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
|
||||||
|
session
|
||||||
|
.query_unpaged(
|
||||||
|
"UPDATE payphone.channels SET call_id = ?, recently_called_server_ids = ? WHERE channel_id = ?",
|
||||||
|
(u64_to_i64(call_id), channel_recently_called_server_ids, u64_to_i64(channel.id)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to update channel {e:?}"))
|
||||||
|
.map_err(DatabaseError::QueryError)?;
|
||||||
|
|
||||||
|
let call = Call {
|
||||||
|
id: call_id,
|
||||||
|
channel_ids: vec![searching_channel.id, channel.id].into_iter().collect(),
|
||||||
|
started_at: UtcDateTime::now(),
|
||||||
|
finished_at: None,
|
||||||
|
};
|
||||||
|
call.insert(session)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to insert call {e:?}"))?;
|
||||||
|
|
||||||
|
return Ok(Some(call));
|
||||||
|
} else {
|
||||||
|
// This channel already got a call, so we skip it, preventing race conditions.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.query_unpaged(
|
||||||
|
"UPDATE payphone.channels SET is_searching = true WHERE channel_id = ? IF call_id = NULL;",
|
||||||
|
(u64_to_i64(channel.id),),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to mark channel as searching! {e:?}"))
|
||||||
|
.map_err(DatabaseError::QueryError)?;
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
|
||||||
|
session
|
||||||
|
.query_unpaged(
|
||||||
|
r#"
|
||||||
|
INSERT INTO payphone.calls (
|
||||||
|
call_id,
|
||||||
|
channel_ids,
|
||||||
|
started_at,
|
||||||
|
finished_at
|
||||||
|
) VALUES (?, ?, ?, ?);
|
||||||
|
"#,
|
||||||
|
Self::to_row(self.clone()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::QueryError)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn hangup(&self, session: &Session) -> Result<(), DatabaseError> {
|
||||||
|
let mut batch = Batch::new(BatchType::Unlogged);
|
||||||
|
|
||||||
|
batch.append_statement(Statement::new(
|
||||||
|
"UPDATE payphone.calls SET finished_at = ? WHERE call_id = ?;",
|
||||||
|
));
|
||||||
|
batch.append_statement(Statement::new(
|
||||||
|
"UPDATE payphone.channels SET call_id = NULL WHERE channel_id = ?;",
|
||||||
|
));
|
||||||
|
batch.append_statement(Statement::new(
|
||||||
|
"UPDATE payphone.channels SET call_id = NULL WHERE channel_id = ?;",
|
||||||
|
));
|
||||||
|
|
||||||
|
let call_id = u64_to_i64(self.id);
|
||||||
|
|
||||||
|
session
|
||||||
|
.batch(
|
||||||
|
&batch,
|
||||||
|
(
|
||||||
|
(OffsetDateTime::now_utc(), call_id),
|
||||||
|
(u64_to_i64(self.channel_ids[0]),),
|
||||||
|
(u64_to_i64(self.channel_ids[1]),),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::QueryError)?;
|
||||||
|
|
||||||
|
log::trace!("Hung up call {call_id}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
use twilight_model::gateway::payload::incoming::Ready;
|
|
||||||
|
|
||||||
pub async fn handle_on_ready(event: Ready) {
|
|
||||||
log::info!(
|
|
||||||
"Ready! Logged in as {}#{:>04}",
|
|
||||||
event.user.name,
|
|
||||||
event.user.discriminator
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_on_reconnect() {
|
|
||||||
log::info!("The gateway asked for a reconnection");
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use twilight_model::gateway::payload::incoming::InteractionCreate;
|
|
||||||
|
|
||||||
use crate::framework::handlers::Handlers;
|
|
||||||
|
|
||||||
pub async fn handle_on_interaction_create(event: InteractionCreate, handlers: Arc<Handlers>) {
|
|
||||||
if let Err(e) = handlers.registry.execute(event, handlers.clone()).await {
|
|
||||||
log::error!("An error occurred while processing the interaction! {e:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use twilight_model::{gateway::payload::incoming::MessageCreate};
|
|
||||||
|
|
||||||
use crate::{commands, framework::{handlers::Handlers}};
|
|
||||||
|
|
||||||
pub fn handle_on_message_create(
|
|
||||||
event: MessageCreate,
|
|
||||||
handlers: Arc<Handlers>
|
|
||||||
) -> impl Future<Output = ()> {
|
|
||||||
commands::execute_command(event, handlers)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use twilight_gateway::Event;
|
|
||||||
|
|
||||||
use crate::framework::handlers::Handlers;
|
|
||||||
|
|
||||||
mod core;
|
|
||||||
mod interactions;
|
|
||||||
mod messages;
|
|
||||||
|
|
||||||
pub async fn handle_event(event: Event, handlers: Arc<Handlers>) {
|
|
||||||
log::trace!("{event:?}");
|
|
||||||
|
|
||||||
match event {
|
|
||||||
Event::Ready(event) => core::handle_on_ready(*event).await,
|
|
||||||
Event::GatewayReconnect => core::handle_on_reconnect().await,
|
|
||||||
Event::MessageCreate(event) => {
|
|
||||||
messages::handle_on_message_create(*event, handlers).await
|
|
||||||
}
|
|
||||||
Event::InteractionCreate(event) => {
|
|
||||||
interactions::handle_on_interaction_create(*event, handlers).await
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
use twilight_model::util::ImageHash;
|
||||||
|
|
||||||
|
pub fn get_avatar_url(hash: Option<ImageHash>, user_id: u64, discriminator: u64) -> String {
|
||||||
|
if let Some(hash) = hash {
|
||||||
|
format!("https://cdn.discordapp.com/avatars/{user_id}/{hash}.webp?size=256")
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"https://cdn.discordapp.com/embed/avatars/{}.png?size=256",
|
||||||
|
match discriminator.to_string().as_str() {
|
||||||
|
"0" => (user_id >> 22) % 6,
|
||||||
|
_ => discriminator % 6,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -1 +1,2 @@
|
|||||||
pub mod events;
|
pub mod images;
|
||||||
|
pub mod sanitizing;
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
pub trait SanitizeDiscordMarkdown {
|
||||||
|
fn sanitize(&self, hard: bool) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SanitizeDiscordMarkdown for T
|
||||||
|
where
|
||||||
|
T: ToString,
|
||||||
|
{
|
||||||
|
fn sanitize(&self, hard: bool) -> String {
|
||||||
|
let input = self.to_string();
|
||||||
|
|
||||||
|
let escaped = if hard {
|
||||||
|
let mut escaped = String::with_capacity(input.len());
|
||||||
|
|
||||||
|
for ch in input.chars() {
|
||||||
|
match ch {
|
||||||
|
'\\' | '*' | '_' | '~' | '`' | '>' | '<' | '|' | '#' => {
|
||||||
|
escaped.push('\\');
|
||||||
|
escaped.push(ch);
|
||||||
|
}
|
||||||
|
_ => escaped.push(ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
escaped
|
||||||
|
} else {
|
||||||
|
input
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now block ping attempts — break mentions
|
||||||
|
escaped
|
||||||
|
.replace("@everyone", "@\u{200B}everyone")
|
||||||
|
.replace("@here", "@\u{200B}here")
|
||||||
|
.replace("<@", "<@\u{200B}") // user mentions
|
||||||
|
.replace("<@&", "<@&\u{200B}") // role mentions
|
||||||
|
.replace("<#", "<#\u{200B}") // channel mentions
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
use std::{fmt::Debug, sync::Arc};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use thiserror::Error;
|
||||||
|
use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt};
|
||||||
|
use twilight_http::Client;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
framework::{
|
||||||
|
commands::{Command, runner::CommandRunner},
|
||||||
|
events::traits::{
|
||||||
|
OnInteractionHandler, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler,
|
||||||
|
OnReadyHandler, OnReconnectHandler, OnServerCreateHandler, OnServerDeleteHandler,
|
||||||
|
OnServerUpdateHandler,
|
||||||
|
},
|
||||||
|
interactions::{components::Component, registry::ComponentRegistry},
|
||||||
|
runtime::Runtime,
|
||||||
|
},
|
||||||
|
utils::Bye,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum RuntimeError {
|
||||||
|
#[error("The application's details could not be fetched")]
|
||||||
|
ApplicationFetchingFailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait IntoPrefixes<T> {
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
runtime: &Runtime<T>,
|
||||||
|
server_id: Option<u64>,
|
||||||
|
user_id: Option<u64>,
|
||||||
|
) -> Vec<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T> IntoPrefixes<T> for String {
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
_: &Runtime<T>,
|
||||||
|
_: Option<u64>,
|
||||||
|
_: Option<u64>,
|
||||||
|
) -> Vec<String> {
|
||||||
|
vec![self.to_string()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T> IntoPrefixes<T> for &str {
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
_: &Runtime<T>,
|
||||||
|
_: Option<u64>,
|
||||||
|
_: Option<u64>,
|
||||||
|
) -> Vec<String> {
|
||||||
|
vec![self.to_string()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Send + Sync + 'static, const N: usize, S: Send + Sync + 'static> IntoPrefixes<S> for [T; N]
|
||||||
|
where
|
||||||
|
T: ToString,
|
||||||
|
{
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
_: &Runtime<S>,
|
||||||
|
_: Option<u64>,
|
||||||
|
_: Option<u64>,
|
||||||
|
) -> Vec<String> {
|
||||||
|
self.iter().map(|s| s.to_string()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<F, Fut, T> IntoPrefixes<T> for F
|
||||||
|
where
|
||||||
|
F: Fn() -> Fut + Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = Vec<String>> + Send,
|
||||||
|
{
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
_: &Runtime<T>,
|
||||||
|
_: Option<u64>,
|
||||||
|
_: Option<u64>,
|
||||||
|
) -> Vec<String> {
|
||||||
|
(self)().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Bot<T: Send + Sync = ()> {
|
||||||
|
state: Arc<T>,
|
||||||
|
prefixes: Arc<dyn IntoPrefixes<T> + Send + Sync>,
|
||||||
|
intents: Intents,
|
||||||
|
commands: Vec<Arc<dyn Command<T>>>,
|
||||||
|
components: Vec<Arc<dyn Component<T>>>,
|
||||||
|
on_ready_handlers: Vec<Arc<dyn OnReadyHandler<T>>>,
|
||||||
|
on_reconnect_handlers: Vec<Arc<dyn OnReconnectHandler<T>>>,
|
||||||
|
on_interaction_handlers: Vec<Arc<dyn OnInteractionHandler<T>>>,
|
||||||
|
on_message_handlers: Vec<Arc<dyn OnMessageHandler<T>>>,
|
||||||
|
on_message_edit_handlers: Vec<Arc<dyn OnMessageEditHandler<T>>>,
|
||||||
|
on_message_delete_handlers: Vec<Arc<dyn OnMessageDeleteHandler<T>>>,
|
||||||
|
on_server_create_handlers: Vec<Arc<dyn OnServerCreateHandler<T>>>,
|
||||||
|
on_server_update_handlers: Vec<Arc<dyn OnServerUpdateHandler<T>>>,
|
||||||
|
on_server_delete_handlers: Vec<Arc<dyn OnServerDeleteHandler<T>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static + Send + Sync> Bot<T> {
|
||||||
|
pub fn new(state: T, prefixes: impl IntoPrefixes<T> + Send + Sync + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
state: Arc::new(state),
|
||||||
|
prefixes: Arc::new(prefixes),
|
||||||
|
intents: Intents::empty(),
|
||||||
|
commands: Vec::new(),
|
||||||
|
components: Vec::new(),
|
||||||
|
on_ready_handlers: Vec::new(),
|
||||||
|
on_reconnect_handlers: Vec::new(),
|
||||||
|
on_interaction_handlers: Vec::new(),
|
||||||
|
on_message_handlers: vec![Arc::new(CommandRunner)],
|
||||||
|
on_message_edit_handlers: Vec::new(),
|
||||||
|
on_message_delete_handlers: Vec::new(),
|
||||||
|
on_server_create_handlers: Vec::new(),
|
||||||
|
on_server_update_handlers: Vec::new(),
|
||||||
|
on_server_delete_handlers: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn intent(mut self, intent: Intents) -> Self {
|
||||||
|
self.intents.insert(intent);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn command(mut self, command: impl Command<T> + 'static) -> Self {
|
||||||
|
self.commands.push(Arc::new(command));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn component(mut self, component: impl Component<T> + 'static) -> Self {
|
||||||
|
self.components.push(Arc::new(component));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_ready(mut self, handler: impl OnReadyHandler<T> + 'static) -> Self {
|
||||||
|
self.on_ready_handlers.push(Arc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_reconnect(mut self, handler: impl OnReconnectHandler<T> + 'static) -> Self {
|
||||||
|
self.on_reconnect_handlers.push(Arc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_interaction(mut self, handler: impl OnInteractionHandler<T> + 'static) -> Self {
|
||||||
|
self.on_interaction_handlers.push(Arc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_message(mut self, handler: impl OnMessageHandler<T> + 'static) -> Self {
|
||||||
|
self.on_message_handlers.push(Arc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_message_edit(mut self, handler: impl OnMessageEditHandler<T> + 'static) -> Self {
|
||||||
|
self.on_message_edit_handlers.push(Arc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_message_delete(mut self, handler: impl OnMessageDeleteHandler<T> + 'static) -> Self {
|
||||||
|
self.on_message_delete_handlers.push(Arc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_server_create(mut self, handler: impl OnServerCreateHandler<T> + 'static) -> Self {
|
||||||
|
self.on_server_create_handlers.push(Arc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_server_update(mut self, handler: impl OnServerUpdateHandler<T> + 'static) -> Self {
|
||||||
|
self.on_server_update_handlers.push(Arc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_server_delete(mut self, handler: impl OnServerDeleteHandler<T> + 'static) -> Self {
|
||||||
|
self.on_server_delete_handlers.push(Arc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(self, token: &str) -> Result<(), RuntimeError> {
|
||||||
|
let http = Client::new(token.to_string());
|
||||||
|
let app = http
|
||||||
|
.current_user_application()
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Could not fetch application data! {e:?}"))
|
||||||
|
.map_err(|_| RuntimeError::ApplicationFetchingFailed)?
|
||||||
|
.model()
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Could not parse application data! {e:?}"))
|
||||||
|
.map_err(|_| RuntimeError::ApplicationFetchingFailed)?;
|
||||||
|
let registry = ComponentRegistry::new();
|
||||||
|
|
||||||
|
for component in &self.components {
|
||||||
|
let component_id = component.custom_id();
|
||||||
|
|
||||||
|
registry.register(
|
||||||
|
component_id.expect("The component registered by default must have a custom ID"),
|
||||||
|
component.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bot = Arc::new(self);
|
||||||
|
|
||||||
|
let runtime = Arc::new(Runtime {
|
||||||
|
http,
|
||||||
|
app,
|
||||||
|
registry,
|
||||||
|
state: bot.state.clone(),
|
||||||
|
prefixes: bot.prefixes.clone(),
|
||||||
|
commands: bot.commands.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut gateway = Shard::new(ShardId::new(0, 1), token.to_string(), bot.intents);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Some(Ok(event)) = gateway.next_event(EventTypeFlags::all()).await {
|
||||||
|
let task_rt = runtime.clone();
|
||||||
|
let task_bot = bot.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match event {
|
||||||
|
Event::Ready(event) => {
|
||||||
|
for handler in &task_bot.on_ready_handlers {
|
||||||
|
handler
|
||||||
|
.on_ready(&event, task_rt.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on ready for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::GatewayReconnect => {
|
||||||
|
for handler in &task_bot.on_reconnect_handlers {
|
||||||
|
handler
|
||||||
|
.on_reconnect(task_rt.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on reconnect for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::InteractionCreate(interaction) => {
|
||||||
|
for handler in &task_bot.on_interaction_handlers {
|
||||||
|
handler
|
||||||
|
.on_interaction(&interaction, task_rt.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on interaction for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::MessageCreate(message) => {
|
||||||
|
for handler in &task_bot.on_message_handlers {
|
||||||
|
log::trace!("Running {handler:?}");
|
||||||
|
|
||||||
|
handler
|
||||||
|
.on_message(&message, task_rt.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on message for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::MessageUpdate(message) => {
|
||||||
|
for handler in &task_bot.on_message_edit_handlers {
|
||||||
|
handler
|
||||||
|
.on_message_edit(&message, task_rt.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on message edit for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::MessageDelete(message) => {
|
||||||
|
for handler in &task_bot.on_message_delete_handlers {
|
||||||
|
handler
|
||||||
|
.on_message_delete(&message, task_rt.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on message delete for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::GuildCreate(guild) => {
|
||||||
|
for handler in &task_bot.on_server_create_handlers {
|
||||||
|
handler
|
||||||
|
.on_server_create(&guild, task_rt.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on server create for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::GuildDelete(guild) => {
|
||||||
|
for handler in &task_bot.on_server_delete_handlers {
|
||||||
|
handler
|
||||||
|
.on_server_delete(&guild, task_rt.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on server delete for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::trace!("Unhandled event {:?} received", event.kind());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
use std::{fmt, ops::{Deref, DerefMut}};
|
use std::{
|
||||||
|
fmt,
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,24 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use twilight_http::Client;
|
use twilight_model::gateway::payload::incoming::MessageCreate;
|
||||||
use twilight_model::{gateway::payload::incoming::MessageCreate, oauth::Application};
|
|
||||||
|
|
||||||
use crate::framework::{commands::messages::MessageSender, handlers::Handlers, interactions::registry::Registry};
|
use crate::framework::{commands::messages::MessageSender, runtime::Runtime};
|
||||||
|
|
||||||
pub struct CommandContext<'a> {
|
pub struct CommandContext<'a, T> {
|
||||||
pub event: &'a MessageCreate,
|
pub event: &'a MessageCreate,
|
||||||
pub handlers: Arc<Handlers>
|
pub rt: Arc<Runtime<T>>,
|
||||||
|
pub prefix: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CommandContext<'a> {
|
impl<'a, T> CommandContext<'a, T>
|
||||||
pub fn new(
|
where
|
||||||
event: &'a MessageCreate,
|
T: Send + Sync + 'static,
|
||||||
handlers: Arc<Handlers>,
|
{
|
||||||
) -> Self {
|
pub fn new(event: &'a MessageCreate, rt: Arc<Runtime<T>>, prefix: &'a str) -> Self {
|
||||||
Self {
|
Self { event, rt, prefix }
|
||||||
event,
|
|
||||||
handlers
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(&self) -> MessageSender {
|
pub fn send(&self) -> MessageSender<T> {
|
||||||
MessageSender::new(
|
MessageSender::new(self.event.channel_id, self.rt.clone())
|
||||||
self.event.channel_id,
|
|
||||||
self.handlers.clone()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,27 +19,30 @@ use twilight_model::{
|
|||||||
use twilight_util::builder::InteractionResponseDataBuilder;
|
use twilight_util::builder::InteractionResponseDataBuilder;
|
||||||
|
|
||||||
use crate::framework::{
|
use crate::framework::{
|
||||||
handlers::Handlers,
|
|
||||||
interactions::components::{ActionRow, Component},
|
interactions::components::{ActionRow, Component},
|
||||||
|
runtime::Runtime,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct MessageSender {
|
pub struct MessageSender<T> {
|
||||||
channel_id: Id<ChannelMarker>,
|
channel_id: Id<ChannelMarker>,
|
||||||
|
|
||||||
handlers: Arc<Handlers>,
|
rt: Arc<Runtime<T>>,
|
||||||
|
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
embeds: Vec<Embed>,
|
embeds: Vec<Embed>,
|
||||||
|
|
||||||
// Row index, component
|
// Row index, component
|
||||||
components: Vec<(u8, Arc<dyn Component>)>,
|
components: Vec<(u8, Arc<dyn Component<T>>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageSender {
|
impl<T> MessageSender<T>
|
||||||
pub fn new(channel_id: Id<ChannelMarker>, handlers: Arc<Handlers>) -> Self {
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
pub fn new(channel_id: Id<ChannelMarker>, rt: Arc<Runtime<T>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
channel_id,
|
channel_id,
|
||||||
handlers,
|
rt,
|
||||||
content: None,
|
content: None,
|
||||||
components: vec![],
|
components: vec![],
|
||||||
embeds: vec![],
|
embeds: vec![],
|
||||||
@@ -58,14 +61,14 @@ impl MessageSender {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn component(mut self, component: impl Component + 'static, row: u8) -> Self {
|
pub fn component(mut self, component: impl Component<T> + 'static, row: u8) -> Self {
|
||||||
self.components.push((row, Arc::new(component)));
|
self.components.push((row, Arc::new(component)));
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send(self) -> Result<Response<Message>, Error> {
|
pub async fn send(self) -> Result<Response<Message>, Error> {
|
||||||
let mut builder = self.handlers.http.create_message(self.channel_id);
|
let mut builder = self.rt.http.create_message(self.channel_id);
|
||||||
|
|
||||||
if let Some(content) = &self.content {
|
if let Some(content) = &self.content {
|
||||||
builder = builder.content(content);
|
builder = builder.content(content);
|
||||||
@@ -92,9 +95,7 @@ impl MessageSender {
|
|||||||
if let Some(custom_id) = &custom_id {
|
if let Some(custom_id) = &custom_id {
|
||||||
custom_ids.push(custom_id.clone());
|
custom_ids.push(custom_id.clone());
|
||||||
|
|
||||||
self.handlers
|
self.rt.registry.register(custom_id, component.clone());
|
||||||
.registry
|
|
||||||
.register(custom_id, component.clone());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
current_row.add(custom_id.clone(), component.clone());
|
current_row.add(custom_id.clone(), component.clone());
|
||||||
@@ -111,7 +112,7 @@ impl MessageSender {
|
|||||||
|
|
||||||
if result.is_err() {
|
if result.is_err() {
|
||||||
for custom_id in custom_ids {
|
for custom_id in custom_ids {
|
||||||
self.handlers.registry.unregister(custom_id);
|
self.rt.registry.unregister(custom_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@ impl MessageSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoFuture for MessageSender {
|
impl<T: Send + Sync + 'static> IntoFuture for MessageSender<T> {
|
||||||
type Output = Result<Response<Message>, Error>;
|
type Output = Result<Response<Message>, Error>;
|
||||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
|
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
|
||||||
|
|
||||||
@@ -128,29 +129,32 @@ impl IntoFuture for MessageSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OriginalMessageUpdater {
|
pub struct OriginalMessageUpdater<T> {
|
||||||
interaction_id: Id<InteractionMarker>,
|
interaction_id: Id<InteractionMarker>,
|
||||||
interaction_token: String,
|
interaction_token: String,
|
||||||
|
|
||||||
handlers: Arc<Handlers>,
|
rt: Arc<Runtime<T>>,
|
||||||
|
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
embeds: Vec<Embed>,
|
embeds: Vec<Embed>,
|
||||||
|
|
||||||
// Row index, component
|
// Row index, component
|
||||||
components: Vec<(u8, Arc<dyn Component>)>,
|
components: Vec<(u8, Arc<dyn Component<T>>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OriginalMessageUpdater {
|
impl<T> OriginalMessageUpdater<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
pub fn new(
|
pub fn new(
|
||||||
interaction_id: Id<InteractionMarker>,
|
interaction_id: Id<InteractionMarker>,
|
||||||
interaction_token: String,
|
interaction_token: String,
|
||||||
handlers: Arc<Handlers>,
|
rt: Arc<Runtime<T>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
interaction_id,
|
interaction_id,
|
||||||
interaction_token,
|
interaction_token,
|
||||||
handlers,
|
rt,
|
||||||
content: None,
|
content: None,
|
||||||
components: vec![],
|
components: vec![],
|
||||||
embeds: vec![],
|
embeds: vec![],
|
||||||
@@ -169,7 +173,7 @@ impl OriginalMessageUpdater {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn component(mut self, component: impl Component + 'static, row: u8) -> Self {
|
pub fn component(mut self, component: impl Component<T> + 'static, row: u8) -> Self {
|
||||||
self.components.push((row, Arc::new(component)));
|
self.components.push((row, Arc::new(component)));
|
||||||
|
|
||||||
self
|
self
|
||||||
@@ -203,9 +207,7 @@ impl OriginalMessageUpdater {
|
|||||||
if let Some(custom_id) = &custom_id {
|
if let Some(custom_id) = &custom_id {
|
||||||
custom_ids.push(custom_id.clone());
|
custom_ids.push(custom_id.clone());
|
||||||
|
|
||||||
self.handlers
|
self.rt.registry.register(custom_id, component.clone());
|
||||||
.registry
|
|
||||||
.register(custom_id, component.clone());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
current_row.add(custom_id.clone(), component.clone());
|
current_row.add(custom_id.clone(), component.clone());
|
||||||
@@ -219,9 +221,9 @@ impl OriginalMessageUpdater {
|
|||||||
builder = builder.embeds(self.embeds);
|
builder = builder.embeds(self.embeds);
|
||||||
|
|
||||||
let result = self
|
let result = self
|
||||||
.handlers
|
.rt
|
||||||
.http
|
.http
|
||||||
.interaction(self.handlers.app.id)
|
.interaction(self.rt.app.id)
|
||||||
.create_response(
|
.create_response(
|
||||||
self.interaction_id,
|
self.interaction_id,
|
||||||
&self.interaction_token,
|
&self.interaction_token,
|
||||||
@@ -234,7 +236,7 @@ impl OriginalMessageUpdater {
|
|||||||
|
|
||||||
if result.is_err() {
|
if result.is_err() {
|
||||||
for custom_id in custom_ids {
|
for custom_id in custom_ids {
|
||||||
self.handlers.registry.unregister(custom_id);
|
self.rt.registry.unregister(custom_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +244,7 @@ impl OriginalMessageUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoFuture for OriginalMessageUpdater {
|
impl<T: Send + Sync + 'static> IntoFuture for OriginalMessageUpdater<T> {
|
||||||
type Output = Result<Response<EmptyBody>, Error>;
|
type Output = Result<Response<EmptyBody>, Error>;
|
||||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
|
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -6,15 +8,22 @@ use crate::framework::commands::{arguments::Argument, context::CommandContext};
|
|||||||
pub mod arguments;
|
pub mod arguments;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
|
pub mod runner;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum CommandError {
|
pub enum CommandError {
|
||||||
#[error("A message was attempted to be sent but it failed")]
|
#[error("A message was attempted to be sent but it failed")]
|
||||||
MessageSendingFailed,
|
MessageSendingFailed,
|
||||||
|
|
||||||
|
#[error("A database query failed")]
|
||||||
|
DatabaseQueryFailed,
|
||||||
|
|
||||||
|
#[error("A database operation failed due to an inconsistent state")]
|
||||||
|
DatabaseInconsistentState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Command: Send + Sync {
|
pub trait Command<T>: Send + Sync + Debug {
|
||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
fn aliases(&self) -> &'static [&'static str] {
|
fn aliases(&self) -> &'static [&'static str] {
|
||||||
&[]
|
&[]
|
||||||
@@ -36,7 +45,7 @@ pub trait Command: Send + Sync {
|
|||||||
all
|
all
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, ctx: &CommandContext, args: &str) -> Result<(), CommandError>;
|
async fn execute(&self, ctx: CommandContext<'_, T>, args: &str) -> Result<(), CommandError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CommandParts<'a> {
|
pub struct CommandParts<'a> {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use twilight_model::gateway::payload::incoming::MessageCreate;
|
||||||
|
|
||||||
|
use crate::framework::{
|
||||||
|
commands::{context::CommandContext, get_command_parts},
|
||||||
|
events::traits::{EventResult, OnMessageHandler},
|
||||||
|
runtime::Runtime,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommandRunner;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Send + Sync + 'static> OnMessageHandler<T> for CommandRunner {
|
||||||
|
async fn on_message(&self, event: &MessageCreate, rt: Arc<Runtime<T>>) -> EventResult {
|
||||||
|
if event.author.bot || event.author.system.unwrap_or(false) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefixes = rt
|
||||||
|
.get_prefixes(event.guild_id.map(|i| i.get()), Some(event.author.id.get()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
for prefix in prefixes {
|
||||||
|
if let Some(parts) = get_command_parts(&prefix, &event.content) {
|
||||||
|
let command = rt.commands.iter().find(|cmd| {
|
||||||
|
cmd.identifiers()
|
||||||
|
.contains(&parts.name.to_ascii_lowercase().as_str())
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(command) = command {
|
||||||
|
command
|
||||||
|
.execute(CommandContext::new(event, rt.clone(), &prefix), parts.args)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod traits;
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
use std::{error::Error, fmt::Debug, sync::Arc};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use twilight_model::gateway::payload::incoming::{
|
||||||
|
GuildCreate, GuildDelete, GuildUpdate, InteractionCreate, MessageCreate, MessageDelete,
|
||||||
|
MessageUpdate, Ready,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::framework::runtime::Runtime;
|
||||||
|
|
||||||
|
pub type EventResult = Result<(), Box<dyn Error>>;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait OnReadyHandler<T>: Send + Sync + Debug
|
||||||
|
where
|
||||||
|
T: Send + Sync,
|
||||||
|
{
|
||||||
|
async fn on_ready(&self, event: &Ready, rt: Arc<Runtime<T>>) -> EventResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait OnReconnectHandler<T>: Send + Sync + Debug
|
||||||
|
where
|
||||||
|
T: Send + Sync,
|
||||||
|
{
|
||||||
|
async fn on_reconnect(&self, rt: Arc<Runtime<T>>) -> EventResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait OnInteractionHandler<T>: Send + Sync + Debug
|
||||||
|
where
|
||||||
|
T: Send + Sync,
|
||||||
|
{
|
||||||
|
async fn on_interaction(&self, event: &InteractionCreate, rt: Arc<Runtime<T>>) -> EventResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait OnMessageHandler<T>: Send + Sync + Debug
|
||||||
|
where
|
||||||
|
T: Send + Sync,
|
||||||
|
{
|
||||||
|
async fn on_message(&self, event: &MessageCreate, rt: Arc<Runtime<T>>) -> EventResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait OnMessageEditHandler<T>: Send + Sync + Debug
|
||||||
|
where
|
||||||
|
T: Send + Sync,
|
||||||
|
{
|
||||||
|
async fn on_message_edit(&self, event: &MessageUpdate, rt: Arc<Runtime<T>>) -> EventResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait OnMessageDeleteHandler<T>: Send + Sync + Debug
|
||||||
|
where
|
||||||
|
T: Send + Sync,
|
||||||
|
{
|
||||||
|
async fn on_message_delete(&self, event: &MessageDelete, rt: Arc<Runtime<T>>) -> EventResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait OnServerCreateHandler<T>: Send + Sync + Debug
|
||||||
|
where
|
||||||
|
T: Send + Sync,
|
||||||
|
{
|
||||||
|
async fn on_server_create(&self, event: &GuildCreate, rt: Arc<Runtime<T>>) -> EventResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait OnServerUpdateHandler<T>: Send + Sync + Debug
|
||||||
|
where
|
||||||
|
T: Send + Sync,
|
||||||
|
{
|
||||||
|
async fn on_server_update(&self, event: &GuildUpdate, rt: Arc<Runtime<T>>) -> EventResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait OnServerDeleteHandler<T>: Send + Sync + Debug
|
||||||
|
where
|
||||||
|
T: Send + Sync,
|
||||||
|
{
|
||||||
|
async fn on_server_delete(&self, event: &GuildDelete, rt: Arc<Runtime<T>>) -> EventResult;
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
use scylla::client::session::Session;
|
|
||||||
use twilight_http::Client;
|
|
||||||
use twilight_model::oauth::Application;
|
|
||||||
|
|
||||||
use crate::framework::interactions::registry::Registry;
|
|
||||||
|
|
||||||
pub struct Handlers {
|
|
||||||
pub http: Client,
|
|
||||||
pub app: Application,
|
|
||||||
pub db: Session,
|
|
||||||
pub registry: Registry,
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::sync::Arc;
|
use std::{fmt::Debug, sync::Arc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use twilight_model::channel::message::{
|
use twilight_model::channel::message::{
|
||||||
@@ -8,18 +8,20 @@ use twilight_model::channel::message::{
|
|||||||
|
|
||||||
use crate::framework::interactions::{InteractionError, context::InteractionContext};
|
use crate::framework::interactions::{InteractionError, context::InteractionContext};
|
||||||
|
|
||||||
pub struct ActionRow {
|
type ComponentWithID<T> = (Option<String>, Arc<dyn Component<T>>);
|
||||||
components: Vec<(Option<String>, Arc<dyn Component>)>,
|
|
||||||
|
pub struct ActionRow<T> {
|
||||||
|
components: Vec<ComponentWithID<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionRow {
|
impl<T> ActionRow<T> {
|
||||||
pub fn new(components: &[(Option<String>, Arc<dyn Component>)]) -> Self {
|
pub fn new(components: &[ComponentWithID<T>]) -> Self {
|
||||||
Self {
|
Self {
|
||||||
components: components.to_vec(),
|
components: components.to_vec(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add(&mut self, id: Option<String>, component: Arc<dyn Component>) {
|
pub fn add(&mut self, id: Option<String>, component: Arc<dyn Component<T>>) {
|
||||||
self.components.push((id, component));
|
self.components.push((id, component));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +29,10 @@ impl ActionRow {
|
|||||||
self.components.len()
|
self.components.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.components.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build(self) -> TwilightComponent {
|
pub fn build(self) -> TwilightComponent {
|
||||||
TwilightComponent::ActionRow(TwilightActionRow {
|
TwilightComponent::ActionRow(TwilightActionRow {
|
||||||
components: self
|
components: self
|
||||||
@@ -39,19 +45,19 @@ impl ActionRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Component: Send + Sync {
|
pub trait Component<T>: Send + Sync + Debug {
|
||||||
fn custom_id(&self) -> Option<String> {
|
fn custom_id(&self) -> Option<String> {
|
||||||
Some(String::from("component"))
|
Some(String::from("component"))
|
||||||
}
|
}
|
||||||
fn build(&self, id: Option<String>) -> TwilightComponent;
|
fn build(&self, id: Option<String>) -> TwilightComponent;
|
||||||
|
|
||||||
async fn execute(&self, ctx: InteractionContext) -> Result<(), InteractionError>;
|
async fn execute(&self, ctx: InteractionContext<T>) -> Result<(), InteractionError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T> Component for T
|
impl<T, S: Send + Sync + 'static> Component<S> for T
|
||||||
where
|
where
|
||||||
T: Button,
|
T: Button<S>,
|
||||||
{
|
{
|
||||||
fn custom_id(&self) -> Option<String> {
|
fn custom_id(&self) -> Option<String> {
|
||||||
Button::id(self)
|
Button::id(self)
|
||||||
@@ -73,13 +79,13 @@ where
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, ctx: InteractionContext) -> Result<(), InteractionError> {
|
async fn execute(&self, ctx: InteractionContext<S>) -> Result<(), InteractionError> {
|
||||||
self.on_press(ctx).await
|
self.on_press(ctx).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Button: Send + Sync {
|
pub trait Button<S: Send + Sync + 'static>: Send + Sync + Debug {
|
||||||
fn id(&self) -> Option<String> {
|
fn id(&self) -> Option<String> {
|
||||||
if self.url().is_none() {
|
if self.url().is_none() {
|
||||||
Some(String::from("Button"))
|
Some(String::from("Button"))
|
||||||
@@ -97,7 +103,7 @@ pub trait Button: Send + Sync {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_press(&self, ctx: InteractionContext) -> Result<(), InteractionError> {
|
async fn on_press(&self, ctx: InteractionContext<S>) -> Result<(), InteractionError> {
|
||||||
ctx.defer().await
|
ctx.defer().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,30 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use twilight_http::Client;
|
|
||||||
use twilight_model::{
|
use twilight_model::{
|
||||||
gateway::payload::incoming::InteractionCreate,
|
gateway::payload::incoming::InteractionCreate,
|
||||||
http::interaction::{InteractionResponse, InteractionResponseType},
|
http::interaction::{InteractionResponse, InteractionResponseType},
|
||||||
id::{Id, marker::InteractionMarker},
|
id::{Id, marker::InteractionMarker},
|
||||||
oauth::Application,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::framework::{
|
use crate::framework::{
|
||||||
commands::messages::OriginalMessageUpdater, handlers::Handlers, interactions::{registry::Registry, InteractionError}
|
commands::messages::OriginalMessageUpdater, interactions::InteractionError, runtime::Runtime,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct InteractionContext {
|
pub struct InteractionContext<T> {
|
||||||
pub event: InteractionCreate,
|
pub event: InteractionCreate,
|
||||||
pub handlers: Arc<Handlers>,
|
pub rt: Arc<Runtime<T>>,
|
||||||
|
pub prefix: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InteractionContext {
|
impl<T> InteractionContext<T> {
|
||||||
pub fn new(
|
pub fn new(event: InteractionCreate, rt: Arc<Runtime<T>>, prefix: String) -> Self {
|
||||||
event: InteractionCreate,
|
Self { event, rt, prefix }
|
||||||
handlers: Arc<Handlers>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
event,
|
|
||||||
handlers
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn defer(&self) -> Result<(), InteractionError> {
|
pub async fn defer(&self) -> Result<(), InteractionError> {
|
||||||
self.handlers.http
|
self.rt
|
||||||
.interaction(self.handlers.app.id)
|
.http
|
||||||
|
.interaction(self.rt.app.id)
|
||||||
.create_response(
|
.create_response(
|
||||||
self.event.id,
|
self.event.id,
|
||||||
&self.event.token,
|
&self.event.token,
|
||||||
@@ -46,8 +40,9 @@ impl InteractionContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn think(&self) -> Result<(), InteractionError> {
|
pub async fn think(&self) -> Result<(), InteractionError> {
|
||||||
self.handlers.http
|
self.rt
|
||||||
.interaction(self.handlers.app.id)
|
.http
|
||||||
|
.interaction(self.rt.app.id)
|
||||||
.create_response(
|
.create_response(
|
||||||
self.event.id,
|
self.event.id,
|
||||||
&self.event.token,
|
&self.event.token,
|
||||||
@@ -62,27 +57,30 @@ impl InteractionContext {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn original_message(&self) -> Option<OriginalMessage> {
|
pub fn original_message(&self) -> Option<OriginalMessage<T>> {
|
||||||
self.event.message.as_ref().map(|_| OriginalMessage {
|
self.event.message.as_ref().map(|_| OriginalMessage {
|
||||||
interaction_id: self.event.id,
|
interaction_id: self.event.id,
|
||||||
interaction_token: self.event.token.clone(),
|
interaction_token: self.event.token.clone(),
|
||||||
handlers: self.handlers.clone()
|
rt: self.rt.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OriginalMessage {
|
pub struct OriginalMessage<T> {
|
||||||
interaction_id: Id<InteractionMarker>,
|
interaction_id: Id<InteractionMarker>,
|
||||||
interaction_token: String,
|
interaction_token: String,
|
||||||
handlers: Arc<Handlers>
|
rt: Arc<Runtime<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OriginalMessage {
|
impl<T> OriginalMessage<T>
|
||||||
pub fn edit(&self) -> OriginalMessageUpdater {
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
pub fn edit(&self) -> OriginalMessageUpdater<T> {
|
||||||
OriginalMessageUpdater::new(
|
OriginalMessageUpdater::new(
|
||||||
self.interaction_id,
|
self.interaction_id,
|
||||||
self.interaction_token.clone(),
|
self.interaction_token.clone(),
|
||||||
self.handlers.clone(),
|
self.rt.clone(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use thiserror::Error;
|
|||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
|
pub mod runner;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum InteractionError {
|
pub enum InteractionError {
|
||||||
@@ -19,4 +20,7 @@ pub enum InteractionError {
|
|||||||
|
|
||||||
#[error("The original message was attempted to be edited, but it failed")]
|
#[error("The original message was attempted to be edited, but it failed")]
|
||||||
OriginalMessageEditingFailed,
|
OriginalMessageEditingFailed,
|
||||||
|
|
||||||
|
#[error("No prefixes were returned for the event")]
|
||||||
|
PrefixesVectorEmpty,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,34 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use scc::HashMap;
|
use scc::HashMap;
|
||||||
use twilight_http::Client;
|
|
||||||
use twilight_model::{
|
use twilight_model::{
|
||||||
application::interaction::InteractionData, gateway::payload::incoming::InteractionCreate,
|
application::interaction::InteractionData, gateway::payload::incoming::InteractionCreate,
|
||||||
oauth::Application,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::framework::{handlers::Handlers, interactions::{context::InteractionContext, InteractionError}};
|
use crate::framework::{
|
||||||
|
interactions::{InteractionError, context::InteractionContext},
|
||||||
|
runtime::Runtime,
|
||||||
|
};
|
||||||
|
|
||||||
use super::components::Component;
|
use super::components::Component;
|
||||||
|
|
||||||
pub struct Registry {
|
#[derive(Default)]
|
||||||
inner: HashMap<String, Arc<dyn Component>>,
|
pub struct ComponentRegistry<T> {
|
||||||
|
inner: HashMap<String, Arc<dyn Component<T>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Registry {
|
impl<T> ComponentRegistry<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: Default::default(),
|
inner: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(&self, id: impl ToString, component: Arc<dyn Component>) {
|
pub fn register(&self, id: impl ToString, component: Arc<dyn Component<T>>) {
|
||||||
self.inner.upsert(id.to_string(), component);
|
self.inner.upsert(id.to_string(), component);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,15 +38,29 @@ impl Registry {
|
|||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
&self,
|
&self,
|
||||||
event: InteractionCreate,
|
event: &InteractionCreate,
|
||||||
handlers: Arc<Handlers>,
|
rt: Arc<Runtime<T>>,
|
||||||
) -> Result<(), InteractionError> {
|
) -> Result<(), InteractionError> {
|
||||||
if let Some(data) = &event.data {
|
if let Some(data) = &event.data {
|
||||||
match data {
|
match data {
|
||||||
InteractionData::MessageComponent(data) => {
|
InteractionData::MessageComponent(data) => {
|
||||||
if let Some(component) = self.inner.get_async(&data.custom_id).await {
|
if let Some(component) = self.inner.get_async(&data.custom_id).await {
|
||||||
|
let prefixes = rt
|
||||||
|
.get_prefixes(
|
||||||
|
event.guild_id.map(|id| id.get()),
|
||||||
|
event.author_id().map(|id| id.get()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let prefix = prefixes
|
||||||
|
.first()
|
||||||
|
.ok_or(InteractionError::PrefixesVectorEmpty)?;
|
||||||
|
|
||||||
component
|
component
|
||||||
.execute(InteractionContext::new(event, handlers))
|
.execute(InteractionContext::new(
|
||||||
|
event.clone(),
|
||||||
|
rt.clone(),
|
||||||
|
prefix.to_string(),
|
||||||
|
))
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
Err(InteractionError::InteractionNotFound)
|
Err(InteractionError::InteractionNotFound)
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use twilight_model::gateway::payload::incoming::InteractionCreate;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
framework::{
|
||||||
|
events::traits::{EventResult, OnInteractionHandler},
|
||||||
|
runtime::Runtime,
|
||||||
|
},
|
||||||
|
state::State,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct InteractionRunner;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl OnInteractionHandler<State> for InteractionRunner {
|
||||||
|
async fn on_interaction(
|
||||||
|
&self,
|
||||||
|
event: &InteractionCreate,
|
||||||
|
rt: Arc<Runtime<State>>,
|
||||||
|
) -> EventResult {
|
||||||
|
Ok(rt.registry.execute(event, rt.clone()).await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
|
pub mod bot;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod events;
|
||||||
pub mod interactions;
|
pub mod interactions;
|
||||||
pub mod utils;
|
pub mod runtime;
|
||||||
pub mod handlers;
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use twilight_http::Client;
|
||||||
|
use twilight_model::oauth::Application;
|
||||||
|
|
||||||
|
use crate::framework::{
|
||||||
|
bot::IntoPrefixes, commands::Command, interactions::registry::ComponentRegistry,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Runtime<T> {
|
||||||
|
pub http: Client,
|
||||||
|
pub app: Application,
|
||||||
|
pub registry: ComponentRegistry<T>,
|
||||||
|
pub prefixes: Arc<dyn IntoPrefixes<T> + Send + Sync + 'static>,
|
||||||
|
pub commands: Vec<Arc<dyn Command<T>>>,
|
||||||
|
pub state: Arc<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Runtime<T> {
|
||||||
|
pub async fn get_prefixes(&self, server_id: Option<u64>, user_id: Option<u64>) -> Vec<String> {
|
||||||
|
self.prefixes.clone().get(self, server_id, user_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_command(&self, name: &str) -> Option<Arc<dyn Command<T>>> {
|
||||||
|
self.commands
|
||||||
|
.iter()
|
||||||
|
.find(|cmd| {
|
||||||
|
cmd.identifiers()
|
||||||
|
.contains(&name.to_ascii_lowercase().as_str())
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
pub trait EscapeDiscordMarkdown {
|
|
||||||
fn escape(self) -> String;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> EscapeDiscordMarkdown for T
|
|
||||||
where
|
|
||||||
T: ToString,
|
|
||||||
{
|
|
||||||
fn escape(self) -> String {
|
|
||||||
let input = self.to_string();
|
|
||||||
|
|
||||||
let mut escaped = String::with_capacity(input.len());
|
|
||||||
|
|
||||||
for ch in input.chars() {
|
|
||||||
match ch {
|
|
||||||
'\\' | '*' | '_' | '~' | '`' | '>' | '|' | '[' | ']' | '(' | ')' | '#' | '+'
|
|
||||||
| '-' | '=' | '.' | '!' => {
|
|
||||||
escaped.push('\\');
|
|
||||||
escaped.push(ch);
|
|
||||||
}
|
|
||||||
_ => escaped.push(ch),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now block ping attempts — break mentions
|
|
||||||
escaped
|
|
||||||
.replace("@everyone", "@\u{200B}everyone")
|
|
||||||
.replace("@here", "@\u{200B}here")
|
|
||||||
.replace("<@", "<@\u{200B}") // user mentions
|
|
||||||
.replace("<@&", "<@&\u{200B}") // role mentions
|
|
||||||
.replace("<#", "<#\u{200B}") // channel mentions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
framework::commands::{
|
||||||
|
Command, CommandError,
|
||||||
|
arguments::{Argument, NeedyString, ParseArguments},
|
||||||
|
context::CommandContext,
|
||||||
|
},
|
||||||
|
groups::about::{
|
||||||
|
components::{
|
||||||
|
HelpDonateLinkButton, HelpGoBackButton, HelpSupportLinkButton, HelpWebsiteLinkButton,
|
||||||
|
},
|
||||||
|
embeds::{get_all_command_help_embed, get_command_help_embed, get_command_not_found_embed},
|
||||||
|
},
|
||||||
|
state::State,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HelpCommand;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Command<State> for HelpCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"help"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arguments(&self) -> &'static [Argument] {
|
||||||
|
&[Argument {
|
||||||
|
name: "command",
|
||||||
|
is_optional: true,
|
||||||
|
is_greedy: false,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Lists all commands and shows information about them individually."
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
ctx: CommandContext<'_, State>,
|
||||||
|
args: &str,
|
||||||
|
) -> Result<(), CommandError> {
|
||||||
|
let command_name: Option<NeedyString> = ParseArguments::parse(args).unwrap();
|
||||||
|
|
||||||
|
if let Some(command_name) = command_name {
|
||||||
|
if let Some(command) = ctx.rt.get_command(&command_name) {
|
||||||
|
ctx.send()
|
||||||
|
.embed(get_command_help_embed(&command, &ctx))
|
||||||
|
.component(HelpGoBackButton, 1)
|
||||||
|
.component(HelpSupportLinkButton, 1)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to send message! {e}"))
|
||||||
|
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||||
|
} else {
|
||||||
|
ctx.send()
|
||||||
|
.embed(get_command_not_found_embed(ctx.rt, command_name.0))
|
||||||
|
.component(HelpGoBackButton, 1)
|
||||||
|
.component(HelpSupportLinkButton, 1)
|
||||||
|
.await
|
||||||
|
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.send()
|
||||||
|
.embed(get_all_command_help_embed(ctx.prefix, ctx.rt))
|
||||||
|
.component(HelpWebsiteLinkButton, 1)
|
||||||
|
.component(HelpSupportLinkButton, 1)
|
||||||
|
.component(HelpDonateLinkButton, 1)
|
||||||
|
.await
|
||||||
|
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
framework::interactions::{InteractionError, components::Button, context::InteractionContext},
|
||||||
|
groups::about::embeds::get_all_command_help_embed,
|
||||||
|
state::State,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HelpGoBackButton;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Button<State> for HelpGoBackButton {
|
||||||
|
fn label(&self) -> Option<&'static str> {
|
||||||
|
Some("← Go back to commands list")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_press(&self, ctx: InteractionContext<State>) -> Result<(), InteractionError> {
|
||||||
|
ctx
|
||||||
|
.original_message()
|
||||||
|
.expect(
|
||||||
|
"Buttons always have the original message in the interaction context, this one didn't (got None)"
|
||||||
|
)
|
||||||
|
.edit()
|
||||||
|
.embed(get_all_command_help_embed(&ctx.prefix, ctx.rt))
|
||||||
|
.component(HelpWebsiteLinkButton, 1)
|
||||||
|
.component(HelpSupportLinkButton, 1)
|
||||||
|
.component(HelpDonateLinkButton, 1)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("An error occurred while attempting to edit the original message of an interaction. {e:?}"))
|
||||||
|
.map_err(
|
||||||
|
|_| InteractionError::OriginalMessageEditingFailed
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HelpSupportLinkButton;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Button<State> for HelpSupportLinkButton {
|
||||||
|
fn label(&self) -> Option<&'static str> {
|
||||||
|
Some("Get help")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url(&self) -> Option<&'static str> {
|
||||||
|
Some("https://discord.gg/9daSsrfTmE")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HelpWebsiteLinkButton;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Button<State> for HelpWebsiteLinkButton {
|
||||||
|
fn label(&self) -> Option<&'static str> {
|
||||||
|
Some("Website")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url(&self) -> Option<&'static str> {
|
||||||
|
Some("https://payphone.lat/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HelpDonateLinkButton;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Button<State> for HelpDonateLinkButton {
|
||||||
|
fn label(&self) -> Option<&'static str> {
|
||||||
|
Some("Donate")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url(&self) -> Option<&'static str> {
|
||||||
|
Some("https://ko-fi.com/payphonedev")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use twilight_model::channel::message::Embed;
|
||||||
|
use twilight_util::builder::embed::{
|
||||||
|
EmbedAuthorBuilder, EmbedBuilder, EmbedFieldBuilder, EmbedFooterBuilder, ImageSource,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
constants::{COLOR_RED, COLOR_WHITE},
|
||||||
|
discord::{images, sanitizing::SanitizeDiscordMarkdown},
|
||||||
|
framework::{
|
||||||
|
commands::{Command, context::CommandContext},
|
||||||
|
runtime::Runtime,
|
||||||
|
},
|
||||||
|
state::State,
|
||||||
|
utils::ToTitleCase,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(super) fn get_command_help_embed(
|
||||||
|
command: &Arc<dyn Command<State>>,
|
||||||
|
ctx: &CommandContext<'_, State>,
|
||||||
|
) -> Embed {
|
||||||
|
let name = format!("{}{}", ctx.prefix, command.name());
|
||||||
|
let arguments = command
|
||||||
|
.arguments()
|
||||||
|
.iter()
|
||||||
|
.map(|arg| arg.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
let usage = if !arguments.is_empty() {
|
||||||
|
format!("{name} {arguments}")
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
};
|
||||||
|
|
||||||
|
EmbedBuilder::new()
|
||||||
|
.author(
|
||||||
|
EmbedAuthorBuilder::new(ctx.rt.app.name.clone())
|
||||||
|
.icon_url(
|
||||||
|
ImageSource::url(images::get_avatar_url(
|
||||||
|
ctx.rt.app.bot.as_ref().unwrap().avatar,
|
||||||
|
ctx.rt.app.bot.as_ref().unwrap().id.get(),
|
||||||
|
ctx.rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.thumbnail(
|
||||||
|
ImageSource::url(images::get_avatar_url(
|
||||||
|
ctx.rt.app.bot.as_ref().unwrap().avatar,
|
||||||
|
ctx.rt.app.bot.as_ref().unwrap().id.get(),
|
||||||
|
ctx.rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.color(COLOR_WHITE)
|
||||||
|
.title(format!("{} Command", command.name().to_title_case()))
|
||||||
|
.description(command.description())
|
||||||
|
.field(EmbedFieldBuilder::new("Usage", format!("`{usage}`")).build())
|
||||||
|
.field(EmbedFieldBuilder::new(
|
||||||
|
"Aliases",
|
||||||
|
if !command.aliases().is_empty() {
|
||||||
|
command
|
||||||
|
.aliases()
|
||||||
|
.iter()
|
||||||
|
.map(|alias| format!("`{}{alias}`", ctx.prefix))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
} else {
|
||||||
|
String::from("*This command doesn't have aliases.*")
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.footer(EmbedFooterBuilder::new(format!(
|
||||||
|
"Use {}help <command> to get help about a specific command.",
|
||||||
|
ctx.prefix
|
||||||
|
)))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn get_all_command_help_embed<T>(prefix: &str, rt: Arc<Runtime<T>>) -> Embed {
|
||||||
|
EmbedBuilder::new()
|
||||||
|
.author(EmbedAuthorBuilder::new(rt.app.name.clone()).icon_url(ImageSource::url(images::get_avatar_url(
|
||||||
|
rt.app.bot.as_ref().unwrap().avatar,
|
||||||
|
rt.app.bot.as_ref().unwrap().id.get(),
|
||||||
|
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
))
|
||||||
|
.unwrap()))
|
||||||
|
.title("Pick up the phone and start making friends!")
|
||||||
|
.description(format!("Here is the list of all available commands. You can see more details about a specific command with `{prefix}help <command>`."))
|
||||||
|
.thumbnail(ImageSource::url(images::get_avatar_url(
|
||||||
|
rt.app.bot.as_ref().unwrap().avatar,
|
||||||
|
rt.app.bot.as_ref().unwrap().id.get(),
|
||||||
|
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
))
|
||||||
|
.unwrap())
|
||||||
|
.color(COLOR_WHITE)
|
||||||
|
.field(EmbedFieldBuilder::new("Commands", get_commands_table(rt)).build())
|
||||||
|
.footer(EmbedFooterBuilder::new(format!("v{} - Use {}help <command> to get more information about a specific command.", env!("CARGO_PKG_VERSION"), prefix)))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn get_commands_table<T>(rt: Arc<Runtime<T>>) -> String {
|
||||||
|
let mut table = String::new();
|
||||||
|
|
||||||
|
for command in rt.commands.iter() {
|
||||||
|
table.push_str(&format!("{:<12}", command.name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("```\n{table}\n```")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn get_command_not_found_embed<T>(rt: Arc<Runtime<T>>, name: String) -> Embed {
|
||||||
|
EmbedBuilder::new()
|
||||||
|
.color(COLOR_RED)
|
||||||
|
.author(EmbedAuthorBuilder::new(rt.app.name.clone()).icon_url(ImageSource::url(images::get_avatar_url(
|
||||||
|
rt.app.bot.as_ref().unwrap().avatar,
|
||||||
|
rt.app.bot.as_ref().unwrap().id.get(),
|
||||||
|
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
))
|
||||||
|
.unwrap()))
|
||||||
|
.title("That's not a command")
|
||||||
|
.description(format!("`{}` is not a valid command name (nor alias). If you put it with the prefix on, take the prefix off before trying again. For example, if you did `p.help p.call`, do `p.help call` instead.\n\nIf you still can't figure it out, press the button below to go back to the full list. Everything's there!", name.sanitize(true)))
|
||||||
|
.thumbnail(ImageSource::url(images::get_avatar_url(
|
||||||
|
rt.app.bot.as_ref().unwrap().avatar,
|
||||||
|
rt.app.bot.as_ref().unwrap().id.get(),
|
||||||
|
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
))
|
||||||
|
.unwrap())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod commands;
|
||||||
|
pub mod components;
|
||||||
|
pub mod embeds;
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use scc::HashSet;
|
||||||
|
use twilight_model::id::Id;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::models::{Call, Channel},
|
||||||
|
framework::commands::{Command, CommandError, context::CommandContext},
|
||||||
|
state::State,
|
||||||
|
utils::Bye,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct CallCommand {
|
||||||
|
/// A set of channel IDs that are currently searching for a call. This works as a lock for two
|
||||||
|
/// users not to use p.call at the same time in the same channel, as it could create a race
|
||||||
|
/// condition. IDs are removed from this set once the DB is properly updated with the channel's
|
||||||
|
/// state, meaning that not all searching channels will be in this set at all times.
|
||||||
|
channel_lock: HashSet<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CallCommand {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
channel_lock: HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Command<State> for CallCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"call"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aliases(&self) -> &'static [&'static str] {
|
||||||
|
&["c"]
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
ctx: CommandContext<'_, State>,
|
||||||
|
_args: &str,
|
||||||
|
) -> Result<(), CommandError> {
|
||||||
|
let channel = Channel::get_or_create_by_id(
|
||||||
|
ctx.event.channel_id.get(),
|
||||||
|
&ctx.rt.state.db,
|
||||||
|
&ctx.rt.http,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("Failed to get or create channel: {e:?}");
|
||||||
|
})
|
||||||
|
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||||
|
|
||||||
|
if channel.is_searching || self.channel_lock.contains(&ctx.event.channel_id.get()) {
|
||||||
|
ctx.send()
|
||||||
|
.content(format!(
|
||||||
|
"☎️ You are already searching for a call! Use `{}hangup` to hang up the phone.",
|
||||||
|
ctx.prefix
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel.call_id.is_some() {
|
||||||
|
ctx.send()
|
||||||
|
.content(format!("☎️ You are already on a call! Use `{}hangup` to hang up the phone, or `{}skip` to skip to the next call.", ctx.prefix, ctx.prefix))
|
||||||
|
.await
|
||||||
|
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.channel_lock
|
||||||
|
.insert_async(ctx.event.channel_id.get())
|
||||||
|
.await
|
||||||
|
.bye();
|
||||||
|
|
||||||
|
let call = Call::start(&channel, &ctx.rt.state.db)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("Failed to start call: {e:?}");
|
||||||
|
self.channel_lock.remove(&ctx.event.channel_id.get());
|
||||||
|
})
|
||||||
|
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||||
|
|
||||||
|
self.channel_lock
|
||||||
|
.remove_async(&ctx.event.channel_id.get())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Some(call) = call {
|
||||||
|
ctx.send().content("☎️ Call connected. Say hi!").await.bye();
|
||||||
|
ctx.rt
|
||||||
|
.http
|
||||||
|
.create_message(Id::new(call.other_party_channel_id(channel.id)))
|
||||||
|
.content("☎️ Call connected. Say hi!")
|
||||||
|
.await
|
||||||
|
.bye();
|
||||||
|
} else {
|
||||||
|
ctx.send()
|
||||||
|
.content("☎️ Searching for a call...")
|
||||||
|
.await
|
||||||
|
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HangupCommand;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Command<State> for HangupCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"hangup"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aliases(&self) -> &'static [&'static str] {
|
||||||
|
&["h", "hang"]
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
ctx: CommandContext<'_, State>,
|
||||||
|
_args: &str,
|
||||||
|
) -> Result<(), CommandError> {
|
||||||
|
let Some(channel) = Channel::get_by_id(ctx.event.channel_id.get(), &ctx.rt.state.db)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to get channel: {e:?}"))
|
||||||
|
.map_err(|_| CommandError::DatabaseQueryFailed)?
|
||||||
|
else {
|
||||||
|
ctx.send()
|
||||||
|
.content(format!(
|
||||||
|
"❌ You're not on a call! Use `{}call` to start one.",
|
||||||
|
ctx.prefix
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if channel.is_searching {
|
||||||
|
channel
|
||||||
|
.cancel_search(&ctx.rt.state.db)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("Failed to cancel search: {e:?}");
|
||||||
|
})
|
||||||
|
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||||
|
|
||||||
|
ctx.send()
|
||||||
|
.content("☎️ The phone was hung up.")
|
||||||
|
.await
|
||||||
|
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(call_id) = channel.call_id {
|
||||||
|
let Some(call) = Call::get_by_id(call_id, &ctx.rt.state.db)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to get call: {e:?}"))
|
||||||
|
.map_err(|_| CommandError::DatabaseQueryFailed)?
|
||||||
|
else {
|
||||||
|
ctx.send()
|
||||||
|
.content("❌ Call not found.")
|
||||||
|
.await
|
||||||
|
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||||
|
|
||||||
|
return Err(CommandError::DatabaseInconsistentState);
|
||||||
|
};
|
||||||
|
|
||||||
|
call.hangup(&ctx.rt.state.db)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to hang up call: {e:?}"))
|
||||||
|
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||||
|
|
||||||
|
ctx.send().content("☎️ You hung up the phone.").await.bye();
|
||||||
|
|
||||||
|
ctx.rt
|
||||||
|
.http
|
||||||
|
.create_message(Id::new(call.other_party_channel_id(channel.id)))
|
||||||
|
.content("☎️ The other party hung up the phone.")
|
||||||
|
.await
|
||||||
|
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||||
|
} else {
|
||||||
|
ctx.send()
|
||||||
|
.content(format!(
|
||||||
|
"❌ You're not on a call! Use `{}call` to start one.",
|
||||||
|
ctx.prefix
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use linkify::LinkFinder;
|
||||||
|
use twilight_model::{gateway::payload::incoming::MessageCreate, id::Id};
|
||||||
|
use twilight_validate::request::webhook_username;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::models::{Call, Channel},
|
||||||
|
discord::{images, sanitizing::SanitizeDiscordMarkdown},
|
||||||
|
framework::{
|
||||||
|
events::traits::{EventResult, OnMessageHandler},
|
||||||
|
runtime::Runtime,
|
||||||
|
},
|
||||||
|
state::State,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MessageRedirector;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl OnMessageHandler<State> for MessageRedirector {
|
||||||
|
async fn on_message(&self, event: &MessageCreate, rt: Arc<Runtime<State>>) -> EventResult {
|
||||||
|
if event.author.bot || event.author.system.unwrap_or(false) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefixes = rt
|
||||||
|
.get_prefixes(event.guild_id.map(|i| i.get()), Some(event.author.id.get()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if prefixes.iter().any(|prefix| {
|
||||||
|
event
|
||||||
|
.content
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
.starts_with(&prefix.to_ascii_lowercase())
|
||||||
|
}) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(channel) = Channel::get_by_id(event.channel_id.get(), &rt.state.db)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!(
|
||||||
|
"Failed to get channel from DB when processing message create event! {e:?}"
|
||||||
|
);
|
||||||
|
})?
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let call = if let Some(call_id) = channel.call_id {
|
||||||
|
if let Some(call) = Call::get_by_id(call_id, &rt.state.db)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!(
|
||||||
|
"Failed to get call from DB when processing message create event! {e:?}"
|
||||||
|
);
|
||||||
|
})?
|
||||||
|
{
|
||||||
|
if !call.is_active() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
call
|
||||||
|
} else {
|
||||||
|
log::error!(
|
||||||
|
"Call ID {} in channel {} does not correspond to an active call.",
|
||||||
|
call_id,
|
||||||
|
channel.id
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(opposite_channel) = Channel::get_by_id(
|
||||||
|
call.other_party_channel_id(event.channel_id.get()),
|
||||||
|
&rt.state.db,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!(
|
||||||
|
"Failed to get opposite channel from DB when processing message create event! {e:?}"
|
||||||
|
);
|
||||||
|
})?
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some((webhook_id, webhook_token)) = opposite_channel
|
||||||
|
.webhook_id
|
||||||
|
.zip(opposite_channel.webhook_token)
|
||||||
|
else {
|
||||||
|
// Handle DM calls in the future.
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let profile = get_display_profile(event);
|
||||||
|
|
||||||
|
rt.http
|
||||||
|
.execute_webhook(Id::new(webhook_id), &webhook_token)
|
||||||
|
.allowed_mentions(None)
|
||||||
|
.username(&profile.name)
|
||||||
|
.avatar_url(&profile.avatar_url)
|
||||||
|
.content(&escape_message(event))
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!(
|
||||||
|
"Failed to send message to webhook for call {} in channel {}: {e:?}",
|
||||||
|
call.id,
|
||||||
|
opposite_channel.id
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DisplayProfile {
|
||||||
|
pub name: String,
|
||||||
|
pub avatar_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_display_profile(event: &MessageCreate) -> DisplayProfile {
|
||||||
|
let member = event.member.clone();
|
||||||
|
|
||||||
|
let name = if let Some(nick) = &member.clone().and_then(|member| member.nick) {
|
||||||
|
if webhook_username(nick).is_err() {
|
||||||
|
"Invalid Nickname".to_string()
|
||||||
|
} else {
|
||||||
|
nick.clone()
|
||||||
|
}
|
||||||
|
} else if let Some(name) = &event.author.global_name {
|
||||||
|
if webhook_username(name).is_err() {
|
||||||
|
"Invalid Global Name".to_string()
|
||||||
|
} else {
|
||||||
|
name.clone()
|
||||||
|
}
|
||||||
|
} else if webhook_username(&event.author.name).is_err() {
|
||||||
|
"Invalid Username".to_string()
|
||||||
|
} else {
|
||||||
|
event.author.name.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let avatar_hash = member
|
||||||
|
.and_then(|inner| inner.avatar)
|
||||||
|
.or(event.author.avatar);
|
||||||
|
let avatar_url = images::get_avatar_url(
|
||||||
|
avatar_hash,
|
||||||
|
event.author.id.get(),
|
||||||
|
event.author.discriminator as u64,
|
||||||
|
);
|
||||||
|
|
||||||
|
DisplayProfile { name, avatar_url }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape_message(event: &MessageCreate) -> String {
|
||||||
|
let mut message = event.content.to_string();
|
||||||
|
|
||||||
|
for mention in &event.mentions {
|
||||||
|
message = message.replace(
|
||||||
|
&format!("<@{}>", mention.id),
|
||||||
|
&format!("**@{}**", mention.name.sanitize(true)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for mention in &event.mention_roles {
|
||||||
|
message = message.replace(&format!("<@&{mention}>"), "**@Role**");
|
||||||
|
}
|
||||||
|
|
||||||
|
for mention in &event.mention_channels {
|
||||||
|
message = message.replace(
|
||||||
|
&format!("<#{}>", mention.id),
|
||||||
|
&format!("**#{}**", mention.name.sanitize(true)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut finder = LinkFinder::new();
|
||||||
|
finder.url_must_have_scheme(false);
|
||||||
|
|
||||||
|
for link in finder.links(&message.clone()) {
|
||||||
|
let url = link.as_str();
|
||||||
|
|
||||||
|
message = message.replace(url, &"\\#".repeat(url.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let replacements: HashMap<char, Option<char>> = [
|
||||||
|
('@', Some('a')),
|
||||||
|
('0', Some('o')),
|
||||||
|
('1', Some('i')),
|
||||||
|
('2', Some('z')),
|
||||||
|
('3', Some('e')),
|
||||||
|
('4', Some('a')),
|
||||||
|
('5', Some('s')),
|
||||||
|
('6', Some('b')),
|
||||||
|
('7', Some('t')),
|
||||||
|
('8', Some('g')),
|
||||||
|
('9', Some('q')),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let normalized_unidecoded = unidecode::unidecode(&message)
|
||||||
|
.to_lowercase()
|
||||||
|
.split_whitespace()
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(" ");
|
||||||
|
let mut normalized = String::with_capacity(normalized_unidecoded.len());
|
||||||
|
|
||||||
|
for ch in normalized_unidecoded.chars() {
|
||||||
|
if let Some(&replacement) = replacements.get(&ch) {
|
||||||
|
if let Some(replacement) = replacement {
|
||||||
|
normalized.push(replacement);
|
||||||
|
}
|
||||||
|
} else if ch.is_alphanumeric() || ch.is_whitespace() {
|
||||||
|
normalized.push(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip everything else (punctuation not in map, zero-width, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("Escaped message: {normalized}");
|
||||||
|
|
||||||
|
message.sanitize(false)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod commands;
|
||||||
|
pub mod events;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use twilight_model::gateway::payload::incoming::Ready;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
framework::{
|
||||||
|
events::traits::{EventResult, OnReadyHandler, OnReconnectHandler},
|
||||||
|
runtime::Runtime,
|
||||||
|
},
|
||||||
|
state::State,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ReadyHandler;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl OnReadyHandler<State> for ReadyHandler {
|
||||||
|
async fn on_ready(&self, event: &Ready, _: Arc<Runtime<State>>) -> EventResult {
|
||||||
|
log::info!(
|
||||||
|
"Ready! Logged in as {}#{:>04}",
|
||||||
|
event.user.name,
|
||||||
|
event.user.discriminator
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ReconnectHandler;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl OnReconnectHandler<State> for ReconnectHandler {
|
||||||
|
async fn on_reconnect(&self, _: Arc<Runtime<State>>) -> EventResult {
|
||||||
|
log::info!("The gateway asked for a reconnection");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod events;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod about;
|
||||||
|
pub mod calls;
|
||||||
|
pub mod gateway;
|
||||||
|
pub mod servers;
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use twilight_model::gateway::payload::incoming::{GuildCreate, GuildDelete, GuildUpdate};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::models::Server,
|
||||||
|
framework::{
|
||||||
|
events::traits::{
|
||||||
|
EventResult, OnServerCreateHandler, OnServerDeleteHandler, OnServerUpdateHandler,
|
||||||
|
},
|
||||||
|
runtime::Runtime,
|
||||||
|
},
|
||||||
|
state::State,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ServerCreateHandler;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl OnServerCreateHandler<State> for ServerCreateHandler {
|
||||||
|
async fn on_server_create(&self, event: &GuildCreate, rt: Arc<Runtime<State>>) -> EventResult {
|
||||||
|
let server = match event {
|
||||||
|
GuildCreate::Available(guild) => Server::from(guild.clone()),
|
||||||
|
GuildCreate::Unavailable(_) => {
|
||||||
|
// If the guild is unavailable, Discord will later send this event again when the guild
|
||||||
|
// becomes available.
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!("The server \"{}\" was created", server.name);
|
||||||
|
|
||||||
|
server.insert(&rt.state.db).await.inspect_err(|e| {
|
||||||
|
log::error!(
|
||||||
|
"Could not insert the recently joined server into the DB!\n\n{server:?}\n\n{e:?}\n"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// TODO: Send hello message to the server's default channel on server join.
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ServerUpdateHandler;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl OnServerUpdateHandler<State> for ServerUpdateHandler {
|
||||||
|
async fn on_server_update(&self, event: &GuildUpdate, rt: Arc<Runtime<State>>) -> EventResult {
|
||||||
|
let mut server = match Server::get_by_id(event.0.id.get(), &rt.state.db)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("Could not get server from DB when it was updated! {e:?}")
|
||||||
|
})? {
|
||||||
|
Some(server) => server,
|
||||||
|
None => {
|
||||||
|
log::debug!("Server was updated but it was not in DB");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if server.name != event.name {
|
||||||
|
server.name = event.name.clone();
|
||||||
|
|
||||||
|
server
|
||||||
|
.upsert(&rt.state.db)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Could not update server's name. {e:?}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ServerDeleteHandler;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl OnServerDeleteHandler<State> for ServerDeleteHandler {
|
||||||
|
async fn on_server_delete(&self, event: &GuildDelete, rt: Arc<Runtime<State>>) -> EventResult {
|
||||||
|
Server::delete_by_id(event.id.get(), &rt.state.db)
|
||||||
|
.await
|
||||||
|
.inspect(|_| log::debug!("Deleted server with ID {} because Payphone left", event.id))
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!(
|
||||||
|
"Could not delete server with ID {} from DB when Payphone left! {e:?}",
|
||||||
|
event.id
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod events;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod framework;
|
||||||
|
pub mod utils;
|
||||||
|
pub mod state;
|
||||||
+5
-9
@@ -2,13 +2,11 @@ use colored::Colorize;
|
|||||||
use log::{Level, LevelFilter};
|
use log::{Level, LevelFilter};
|
||||||
use time::{OffsetDateTime, macros::format_description};
|
use time::{OffsetDateTime, macros::format_description};
|
||||||
|
|
||||||
pub struct Logger {
|
pub struct Logger;
|
||||||
level: Level,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl log::Log for Logger {
|
impl log::Log for Logger {
|
||||||
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
||||||
metadata.level() <= self.level
|
metadata.target() == "payphone" || metadata.target().starts_with("payphone::")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log(&self, record: &log::Record) {
|
fn log(&self, record: &log::Record) {
|
||||||
@@ -25,11 +23,9 @@ impl log::Log for Logger {
|
|||||||
fn flush(&self) {}
|
fn flush(&self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
static LOGGER: Logger = Logger { level: Level::Info };
|
pub fn init(level: LevelFilter) {
|
||||||
|
log::set_logger(&Logger)
|
||||||
pub fn init() {
|
.map(|()| log::set_max_level(level))
|
||||||
log::set_logger(&LOGGER)
|
|
||||||
.map(|()| log::set_max_level(LevelFilter::Trace))
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+37
-64
@@ -1,68 +1,38 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use twilight_gateway::{ConfigBuilder, EventTypeFlags, Intents, Shard, ShardId, StreamExt};
|
use twilight_gateway::Intents;
|
||||||
use twilight_http::Client;
|
|
||||||
use twilight_model::gateway::{
|
|
||||||
payload::outgoing::update_presence::UpdatePresencePayload,
|
|
||||||
presence::{ActivityType, MinimalActivity, Status},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::Args, commands::register_default_components, discord::events,
|
cli::Args,
|
||||||
framework::{handlers::Handlers, interactions::registry::Registry},
|
framework::bot::Bot,
|
||||||
|
groups::{
|
||||||
|
about::{commands::HelpCommand, components::HelpGoBackButton},
|
||||||
|
calls::{
|
||||||
|
commands::{CallCommand, HangupCommand},
|
||||||
|
events::MessageRedirector,
|
||||||
|
},
|
||||||
|
gateway::events::{ReadyHandler, ReconnectHandler},
|
||||||
|
servers::events::{ServerCreateHandler, ServerDeleteHandler, ServerUpdateHandler},
|
||||||
|
},
|
||||||
|
state::State,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod commands;
|
|
||||||
mod constants;
|
mod constants;
|
||||||
mod database;
|
mod database;
|
||||||
mod discord;
|
mod discord;
|
||||||
mod framework;
|
mod framework;
|
||||||
|
mod groups;
|
||||||
mod logging;
|
mod logging;
|
||||||
|
mod state;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
pub async fn main() {
|
||||||
dotenvy::dotenv().ok();
|
dotenvy::dotenv().ok();
|
||||||
|
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
logging::init();
|
logging::init(args.logging.into());
|
||||||
|
|
||||||
let intents = Intents::GUILD_MESSAGES | Intents::MESSAGE_CONTENT;
|
|
||||||
let config = ConfigBuilder::new(args.token.clone(), intents)
|
|
||||||
.presence(
|
|
||||||
UpdatePresencePayload::new(
|
|
||||||
vec![
|
|
||||||
MinimalActivity {
|
|
||||||
kind: ActivityType::Listening,
|
|
||||||
name: "Hello Payphone!".into(),
|
|
||||||
url: None,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
],
|
|
||||||
false,
|
|
||||||
None,
|
|
||||||
Status::Online,
|
|
||||||
)
|
|
||||||
.expect("Could not set presence!"),
|
|
||||||
)
|
|
||||||
.build();
|
|
||||||
let mut shard = Shard::with_config(ShardId::new(0, 1), config);
|
|
||||||
|
|
||||||
let http = Client::new(args.token.clone());
|
|
||||||
let registry = Registry::new();
|
|
||||||
let app =
|
|
||||||
http.current_user_application()
|
|
||||||
.await
|
|
||||||
.expect("Could not get information about the current application")
|
|
||||||
.model()
|
|
||||||
.await
|
|
||||||
.expect("Could not get model of the information of the current application")
|
|
||||||
;
|
|
||||||
|
|
||||||
register_default_components(®istry);
|
|
||||||
|
|
||||||
log::info!("Welcome to Payphone, Nyeki!");
|
log::info!("Welcome to Payphone, Nyeki!");
|
||||||
log::info!(
|
log::info!(
|
||||||
@@ -70,25 +40,28 @@ async fn main() {
|
|||||||
env!("CARGO_PKG_VERSION")
|
env!("CARGO_PKG_VERSION")
|
||||||
);
|
);
|
||||||
|
|
||||||
let db = database::init("127.0.0.1:9042")
|
let db = database::init(args.database_url)
|
||||||
.await
|
.await
|
||||||
.expect("Could not connect to the database!");
|
.expect("Could not connect to the database!");
|
||||||
|
|
||||||
let handlers = Arc::new(Handlers {
|
log::info!("Database connection established successfully!");
|
||||||
app,
|
|
||||||
db,
|
|
||||||
http,
|
|
||||||
registry
|
|
||||||
});
|
|
||||||
|
|
||||||
while let Some(event) = shard.next_event(EventTypeFlags::all()).await {
|
let state = State { db };
|
||||||
if let Ok(event) = event {
|
|
||||||
tokio::spawn(events::handle_event(
|
let bot = Bot::new(state, ["p."])
|
||||||
event,
|
.intent(Intents::GUILDS)
|
||||||
handlers.clone(),
|
.intent(Intents::GUILD_MESSAGES)
|
||||||
));
|
.intent(Intents::MESSAGE_CONTENT)
|
||||||
} else {
|
.command(HelpCommand)
|
||||||
log::error!("Could not receive the next event.");
|
.command(CallCommand::new())
|
||||||
}
|
.command(HangupCommand)
|
||||||
}
|
.component(HelpGoBackButton)
|
||||||
|
.on_ready(ReadyHandler)
|
||||||
|
.on_reconnect(ReconnectHandler)
|
||||||
|
.on_message(MessageRedirector)
|
||||||
|
.on_server_create(ServerCreateHandler)
|
||||||
|
.on_server_update(ServerUpdateHandler)
|
||||||
|
.on_server_delete(ServerDeleteHandler);
|
||||||
|
|
||||||
|
bot.run(&args.token).await.expect("Could not run bot!");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
use scylla::client::session::Session;
|
||||||
|
|
||||||
|
pub struct State {
|
||||||
|
pub db: Session,
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
use rand::{Rng, SeedableRng};
|
||||||
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
|
||||||
pub trait ToTitleCase {
|
pub trait ToTitleCase {
|
||||||
fn to_title_case(&self) -> String;
|
fn to_title_case(&self) -> String;
|
||||||
}
|
}
|
||||||
@@ -6,6 +9,10 @@ impl<T> ToTitleCase for T
|
|||||||
where
|
where
|
||||||
T: ToString,
|
T: ToString,
|
||||||
{
|
{
|
||||||
|
/// Removes duplicated whitespaces and capitalizes the first letter of each word.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`String`] - The title-cased version of the string.
|
||||||
fn to_title_case(&self) -> String {
|
fn to_title_case(&self) -> String {
|
||||||
self.to_string()
|
self.to_string()
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
@@ -20,3 +27,21 @@ where
|
|||||||
.join(" ")
|
.join(" ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a random `u64` generated using a cryptographically secure random number generator.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`u64`] - A randomly generated 64-bit unsigned integer.
|
||||||
|
pub fn get_random_u64() -> u64 {
|
||||||
|
let mut rng = ChaCha20Rng::from_os_rng();
|
||||||
|
|
||||||
|
rng.random()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Bye: Sized {
|
||||||
|
/// Whatever the value is, it consumes it and returns a unit value. Used to remove warnings
|
||||||
|
/// about unused results.
|
||||||
|
fn bye(self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Bye for T {}
|
||||||
|
|||||||
Reference in New Issue
Block a user