Compare commits
11 commits
78d5958072
...
54889bdf89
Author | SHA1 | Date | |
---|---|---|---|
54889bdf89 | |||
66561cb9fa | |||
0a5a3d366b | |||
6c48eb11ac | |||
768a5c8a42 | |||
a400a51c69 | |||
971d241c74 | |||
d35c2771a6 | |||
dfe6967f37 | |||
a9a047a7de | |||
a4eabf7173 |
25 changed files with 1324 additions and 1085 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
|||
target/
|
||||
logs/
|
||||
.helix
|
||||
result
|
||||
|
||||
.env
|
||||
|
|
1635
Cargo.lock
generated
1635
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,10 +1,11 @@
|
|||
[package]
|
||||
name = "yorokobot"
|
||||
description = "A Discord bot to handle a topic subscription system"
|
||||
description = "Discord bot implementing a topic management system"
|
||||
version = "0.2.1"
|
||||
authors = [ "Victor Mignot <dala@dalaran.fr>" ]
|
||||
license = "EUPL-1.2"
|
||||
readme = "README.md"
|
||||
repository = "https://git.dalaran.fr/dala/yorokobot"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
@ -14,6 +15,10 @@ serenity = { version="0.11", default-features = false, features = ["client", "ga
|
|||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
mongodb = { version = "2.3.0", default-features = false, features = ["tokio-runtime"] }
|
||||
serde = { version = "1.0", features = [ "derive" ] }
|
||||
log4rs = { version = "1.2.0", features = [ "console_appender", "rolling_file_appender", "compound_policy", "fixed_window_roller", "size_trigger", "gzip", "background_rotation" ] }
|
||||
log = "0.4.17"
|
||||
futures = "0.3.25"
|
||||
env_logger = "0.11.6"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code= "forbid"
|
||||
missing_docs = "forbid"
|
||||
|
|
38
README.md
38
README.md
|
@ -1,35 +1,9 @@
|
|||
# YorokoBot
|
||||
# Yorokobot
|
||||
|
||||
This bot is a remake of the original HokuBot
|
||||
using Rust with the Serenity crate and the Nix package manager.
|
||||
Yorokobot is a Discord bot that implement a topic-subscription systems.
|
||||
It allows server owner to create a list of custom topics to which users can then subscribe.
|
||||
|
||||
The main instance of this bot was made specifically for the [Archetype:Moon](https://archetype-moon.fr/)
|
||||
server and is not public.
|
||||
If you want to add the bot to your own server (or host a public version), you can run your own instance
|
||||
of Yorokobot as long as the license terms are respected.
|
||||
## Licensing
|
||||
|
||||
## What is YorokoBot ?
|
||||
|
||||
YorokoBot is a Discord bot allowing users to subscribe to tags.
|
||||
Tags represent someone's center of interest.
|
||||
If something related to this topic is post on Discord, the sender can use
|
||||
the bot to ping each users that subscribed to this topic.
|
||||
|
||||
## Why using YorokoBot when Discord roles exist ?
|
||||
|
||||
To keep it simple, to prevent a Discord server to create a ton of roles.
|
||||
|
||||
## How Yorokobot permissions are managed ?
|
||||
|
||||
Yorokobot has no permission management system.
|
||||
Discord provides for each server a way to handle who can use a specific command and on which channel.
|
||||
Once the bot in on your server, you can do a right click on the bot user and select "Manage integration".
|
||||
|
||||
## Is there a global banlist ?
|
||||
|
||||
No, at least for now.
|
||||
|
||||
## Why is not the main bot instance public ?
|
||||
|
||||
I'm currently a student and this is just a project I did because it would be useful for the Archetype-Moon Discord server.
|
||||
However, it was important for me to make it available to everyone who wanted to use it as long as they are ready to run it themselves.
|
||||
This project is licensed under the `European Union Public License v. 1.2`.
|
||||
Please refer to the `LICENSE` file if needed.
|
||||
|
|
18
default.nix
Normal file
18
default.nix
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
pkgs ? import ./pkgs.nix,
|
||||
...
|
||||
}:
|
||||
pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "yorokobot";
|
||||
version = "0.2.1";
|
||||
|
||||
src = ./.;
|
||||
|
||||
cargoHash = "sha256-jspdQtP+z8SXD7gs5G0GrvJLt6sU6RyvGyn5SPQU85U=";
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "Discord bot implementing a topic management system";
|
||||
homepage = "https://git.dalaran.fr/dala/yorokobot";
|
||||
license = licenses.eupl12;
|
||||
};
|
||||
}
|
94
flake.lock
generated
94
flake.lock
generated
|
@ -1,94 +0,0 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"locked": {
|
||||
"lastModified": 1656928814,
|
||||
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1663494472,
|
||||
"narHash": "sha256-fSowlaoXXWcAM8m9wA6u+eTJJtvruYHMA+Lb/tFi/qM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f677051b8dc0b5e2a9348941c99eea8c4b0ff28f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1659102345,
|
||||
"narHash": "sha256-Vbzlz254EMZvn28BhpN8JOi5EuKqnHZ3ujFYgFcSGvk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "11b60e4f80d87794a2a4a8a256391b37c59a1ea7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1663729386,
|
||||
"narHash": "sha256-aKdxkiYUGuvgy+eKq4jubf/gZN7TBoF6huE4w0chhDE=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "0300688a98e053712108d4e22d5bdcf9c9106d8c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
58
flake.nix
58
flake.nix
|
@ -1,58 +0,0 @@
|
|||
{
|
||||
description = "Discord Bot managing users interests.";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = {self, nixpkgs, rust-overlay, flake-utils, ...}:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs {
|
||||
inherit system overlays;
|
||||
};
|
||||
in
|
||||
with pkgs;
|
||||
{
|
||||
packages.default = pkgs.rustPlatform.buildRustPackage rec {
|
||||
pname = "yorokobot";
|
||||
version = "0.2.1";
|
||||
|
||||
src = self;
|
||||
|
||||
cargoSha256 = "sha256-FfhyVjCZRvjbSAQ8aGTegjtSh5d4vMSoxzk+xagmj9w=";
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "Discord bot implementing a topic management system";
|
||||
homepage = "https://sr.ht/~dala/yorokobot";
|
||||
license = licenses.agpl3;
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default = mkShell {
|
||||
buildInputs = [
|
||||
(
|
||||
rust-bin.stable.latest.default.override {
|
||||
extensions = [ "rust-src" ];
|
||||
}
|
||||
)
|
||||
];
|
||||
|
||||
packages = with pkgs; [
|
||||
rust-analyzer
|
||||
rustfmt
|
||||
gdb
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
set -a
|
||||
source .env
|
||||
set +a
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
80
npins/default.nix
Normal file
80
npins/default.nix
Normal file
|
@ -0,0 +1,80 @@
|
|||
# Generated by npins. Do not modify; will be overwritten regularly
|
||||
let
|
||||
data = builtins.fromJSON (builtins.readFile ./sources.json);
|
||||
version = data.version;
|
||||
|
||||
mkSource =
|
||||
spec:
|
||||
assert spec ? type;
|
||||
let
|
||||
path =
|
||||
if spec.type == "Git" then
|
||||
mkGitSource spec
|
||||
else if spec.type == "GitRelease" then
|
||||
mkGitSource spec
|
||||
else if spec.type == "PyPi" then
|
||||
mkPyPiSource spec
|
||||
else if spec.type == "Channel" then
|
||||
mkChannelSource spec
|
||||
else
|
||||
builtins.throw "Unknown source type ${spec.type}";
|
||||
in
|
||||
spec // { outPath = path; };
|
||||
|
||||
mkGitSource =
|
||||
{
|
||||
repository,
|
||||
revision,
|
||||
url ? null,
|
||||
hash,
|
||||
branch ? null,
|
||||
...
|
||||
}:
|
||||
assert repository ? type;
|
||||
# At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository
|
||||
# In the latter case, there we will always be an url to the tarball
|
||||
if url != null then
|
||||
(builtins.fetchTarball {
|
||||
inherit url;
|
||||
sha256 = hash; # FIXME: check nix version & use SRI hashes
|
||||
})
|
||||
else
|
||||
assert repository.type == "Git";
|
||||
let
|
||||
urlToName =
|
||||
url: rev:
|
||||
let
|
||||
matched = builtins.match "^.*/([^/]*)(\\.git)?$" repository.url;
|
||||
|
||||
short = builtins.substring 0 7 rev;
|
||||
|
||||
appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else "";
|
||||
in
|
||||
"${if matched == null then "source" else builtins.head matched}${appendShort}";
|
||||
name = urlToName repository.url revision;
|
||||
in
|
||||
builtins.fetchGit {
|
||||
url = repository.url;
|
||||
rev = revision;
|
||||
inherit name;
|
||||
# hash = hash;
|
||||
};
|
||||
|
||||
mkPyPiSource =
|
||||
{ url, hash, ... }:
|
||||
builtins.fetchurl {
|
||||
inherit url;
|
||||
sha256 = hash;
|
||||
};
|
||||
|
||||
mkChannelSource =
|
||||
{ url, hash, ... }:
|
||||
builtins.fetchTarball {
|
||||
inherit url;
|
||||
sha256 = hash;
|
||||
};
|
||||
in
|
||||
if version == 3 then
|
||||
builtins.mapAttrs (_: mkSource) data.pins
|
||||
else
|
||||
throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`"
|
11
npins/sources.json
Normal file
11
npins/sources.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"pins": {
|
||||
"nixpkgs": {
|
||||
"type": "Channel",
|
||||
"name": "nixpkgs-unstable",
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.05pre730187.0e82ab234249/nixexprs.tar.xz",
|
||||
"hash": "0s5snh81d5n9zxcn9n2fqk6jcinfd5ys97fcw8qyrs9dlprp1slw"
|
||||
}
|
||||
},
|
||||
"version": 3
|
||||
}
|
4
pkgs.nix
Normal file
4
pkgs.nix
Normal file
|
@ -0,0 +1,4 @@
|
|||
let
|
||||
inherit (import ./npins) nixpkgs;
|
||||
in
|
||||
import nixpkgs { }
|
23
shell.nix
Normal file
23
shell.nix
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
pkgs ? import ./pkgs.nix,
|
||||
...
|
||||
}:
|
||||
pkgs.mkShell {
|
||||
strictDeps = true;
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
rustc
|
||||
cargo
|
||||
];
|
||||
|
||||
packages = with pkgs; [
|
||||
clippy
|
||||
rust-analyzer
|
||||
rustfmt
|
||||
gdb
|
||||
nixfmt-rfc-style
|
||||
nil
|
||||
ltex-ls
|
||||
marksman
|
||||
];
|
||||
}
|
|
@ -12,10 +12,10 @@ use crate::discord::event_handler::Handler;
|
|||
///
|
||||
/// To launch Yorokobot, you have to set the following environment variables:
|
||||
/// - DISCORD_TOKEN: The secret Discord provide you when creating a new bot in the
|
||||
/// Discord Developper websites.
|
||||
/// Discord Developper websites.
|
||||
/// - MONGODB_URI: The connection string to your Mongo database.
|
||||
/// - MONGODB_DATABASE: The database to use in your Mongo instance (falcultative if given in the
|
||||
/// MONGODB_URI connection string).
|
||||
/// MONGODB_URI connection string).
|
||||
pub struct Client {
|
||||
serenity_client: SerenityClient,
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ impl Client {
|
|||
/// This method will panic if:
|
||||
/// - The MONGODB_URI environment variable is not set.
|
||||
/// - If the database to use is not specified in the connection string given with MONGODB_URI
|
||||
/// and if MONGODB_DATABASE is not set.
|
||||
/// and if MONGODB_DATABASE is not set.
|
||||
/// - If the given credentials are invalid.
|
||||
pub async fn connect(&mut self) {
|
||||
self.mongo_client = match MongoClient::with_uri_str(get_env_variable("MONGODB_URI")).await {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//! Commons elements that are used in while executing the bot commands.
|
||||
|
||||
use log::{info, warn};
|
||||
use std::sync::Arc;
|
||||
use std::{fmt::Display, sync::Arc};
|
||||
|
||||
use serenity::{
|
||||
async_trait,
|
||||
|
@ -18,31 +18,65 @@ use crate::database::Client as DatabaseClient;
|
|||
|
||||
/// The kind of errors that can be returned while executing a command.
|
||||
#[derive(Debug)]
|
||||
pub enum CommandExecutionError {
|
||||
pub enum Error {
|
||||
/// An error that is returned when the `Serenity` crate failed to get the argument from the
|
||||
/// Discord API payload.
|
||||
ArgumentExtractionError(String),
|
||||
InvalidCommand(String),
|
||||
|
||||
/// An error that is returned when the cast from the argument value to a Rust type failed.
|
||||
ArgumentDeserializationError(String),
|
||||
ArgumentDeserialization(String),
|
||||
|
||||
/// The kind of error that is returned when a Database query failed.
|
||||
DatabaseQueryError(String),
|
||||
DatabaseQuery(String),
|
||||
|
||||
/// Error returned when we fail to extract the context of a Discord commands.
|
||||
ContextRetrievalError(String),
|
||||
ContextRetrieval(String),
|
||||
|
||||
/// Error returned when sending a command to the discord API failed.
|
||||
DiscordAPICallError(String),
|
||||
DiscordAPICall(serenity::Error),
|
||||
|
||||
/// Error returned when there was an issue while using a selector embed in the command
|
||||
/// response.
|
||||
SelectorError(String),
|
||||
Selector(String),
|
||||
|
||||
/// Error returned when the given command is unknown to Yorokobot.
|
||||
UnknownCommand(String),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let string = match self {
|
||||
Error::InvalidCommand(e) => format!("Received an invalid command: {e}"),
|
||||
Error::ArgumentDeserialization(e) => {
|
||||
format!("Failed to deserialize command argument: {e}")
|
||||
}
|
||||
Error::DatabaseQuery(e) => format!("Failed to execute a database query: {e}"),
|
||||
Error::ContextRetrieval(e) => {
|
||||
format!("Failed to retrieve current session context: {e}")
|
||||
}
|
||||
Error::DiscordAPICall(e) => format!("Error during Discord API call: {e}"),
|
||||
Error::Selector(e) => format!("Met an error while using an embed selector: {e}"),
|
||||
Error::UnknownCommand(e) => format!("Met an unknown bot command: {e}"),
|
||||
};
|
||||
|
||||
write!(f, "{string}")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Error::InvalidCommand(_) => None,
|
||||
Error::ArgumentDeserialization(_) => None,
|
||||
Error::DatabaseQuery(_) => None,
|
||||
Error::ContextRetrieval(_) => None,
|
||||
Error::DiscordAPICall(error) => error.source(),
|
||||
Error::Selector(_) => None,
|
||||
Error::UnknownCommand(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An option that can/have to be passed to a Discord command.
|
||||
pub struct BotCommandOption {
|
||||
pub name: String,
|
||||
|
@ -67,11 +101,7 @@ pub trait BotCommand {
|
|||
fn options_list() -> Vec<BotCommandOption>;
|
||||
|
||||
/// Execute the command with the given `context` and using the given `database`.
|
||||
async fn run(
|
||||
&self,
|
||||
context: Context,
|
||||
database: Arc<DatabaseClient>,
|
||||
) -> Result<(), CommandExecutionError>;
|
||||
async fn run(&self, context: Context, database: Arc<DatabaseClient>) -> Result<(), Error>;
|
||||
|
||||
/// Extract and deserialize the `index`th value from the command `options`.
|
||||
fn extract_option_value(
|
||||
|
|
|
@ -20,7 +20,7 @@ use crate::database::{
|
|||
Client as DatabaseClient,
|
||||
};
|
||||
|
||||
use super::commons::{BotCommand, BotCommandOption, CommandExecutionError};
|
||||
use super::commons::{BotCommand, BotCommandOption, Error};
|
||||
|
||||
pub struct CreateTagCommand {
|
||||
interaction: ApplicationCommandInteraction,
|
||||
|
@ -50,25 +50,21 @@ impl BotCommand for CreateTagCommand {
|
|||
CreateTagCommand { interaction }
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
context: Context,
|
||||
database: Arc<DatabaseClient>,
|
||||
) -> Result<(), CommandExecutionError> {
|
||||
async fn run(&self, context: Context, database: Arc<DatabaseClient>) -> Result<(), Error> {
|
||||
// Extract tag_name parameter
|
||||
let tag_name = match self.interaction.data.options.get(0) {
|
||||
let tag_name = match self.interaction.data.options.first() {
|
||||
Some(a) => match &a.resolved {
|
||||
Some(r) => match r {
|
||||
CommandDataOptionValue::String(r_str) => Ok(r_str),
|
||||
_ => Err(CommandExecutionError::ArgumentDeserializationError(
|
||||
_ => Err(Error::ArgumentDeserialization(
|
||||
"Received non String argument for the CreateTagCommand".to_string(),
|
||||
)),
|
||||
},
|
||||
None => Err(CommandExecutionError::ArgumentDeserializationError(
|
||||
None => Err(Error::ArgumentDeserialization(
|
||||
"Could not deserialize the argument for the CreateTagCommand".to_string(),
|
||||
)),
|
||||
},
|
||||
None => Err(CommandExecutionError::ArgumentExtractionError(
|
||||
None => Err(Error::InvalidCommand(
|
||||
"Failed to get the CreateTagCommand argument".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
@ -76,7 +72,7 @@ impl BotCommand for CreateTagCommand {
|
|||
// Extract guild id from Serenity context
|
||||
let guild_id = match self.interaction.guild_id {
|
||||
Some(a) => Ok(a.to_string()),
|
||||
None => Err(CommandExecutionError::ContextRetrievalError(
|
||||
None => Err(Error::ContextRetrieval(
|
||||
"Could not fetch guild id from issued command".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
@ -84,11 +80,11 @@ impl BotCommand for CreateTagCommand {
|
|||
let guild = match database.get_by_id::<Guild>(&guild_id).await {
|
||||
Ok(query) => match query {
|
||||
Some(r) => Ok(r),
|
||||
None => Err(CommandExecutionError::ContextRetrievalError(
|
||||
None => Err(Error::ContextRetrieval(
|
||||
"Failed to retrieve the guild where the command was issued".to_string(),
|
||||
)),
|
||||
},
|
||||
Err(()) => Err(CommandExecutionError::DatabaseQueryError(
|
||||
Err(()) => Err(Error::DatabaseQuery(
|
||||
"Could not access to the database".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
@ -113,25 +109,19 @@ impl BotCommand for CreateTagCommand {
|
|||
.await
|
||||
{
|
||||
Ok(_) => Ok(String::from("Tag successfully created.")),
|
||||
Err(_) => Err(CommandExecutionError::DatabaseQueryError(
|
||||
Err(_) => Err(Error::DatabaseQuery(
|
||||
"Could not add new tag to the database".to_string(),
|
||||
)),
|
||||
}?
|
||||
};
|
||||
|
||||
match self
|
||||
.interaction
|
||||
self.interaction
|
||||
.create_interaction_response(context.http, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| message.content(response_content))
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(_e) => Err(CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to answer to the initial command".to_string(),
|
||||
)),
|
||||
}
|
||||
.map_err(Error::DiscordAPICall)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ use serenity::{
|
|||
};
|
||||
|
||||
use super::{
|
||||
commons::{BotCommandOption, CommandExecutionError},
|
||||
commons::{BotCommandOption, Error},
|
||||
BotCommand,
|
||||
};
|
||||
|
||||
|
@ -50,31 +50,27 @@ impl BotCommand for DeleteTagCommand {
|
|||
}]
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
context: Context,
|
||||
database: Arc<DatabaseClient>,
|
||||
) -> Result<(), CommandExecutionError> {
|
||||
let tag_name = match self.interaction.data.options.get(0) {
|
||||
async fn run(&self, context: Context, database: Arc<DatabaseClient>) -> Result<(), Error> {
|
||||
let tag_name = match self.interaction.data.options.first() {
|
||||
Some(a) => match &a.resolved {
|
||||
Some(r) => match r {
|
||||
CommandDataOptionValue::String(r_str) => Ok(r_str),
|
||||
_ => Err(CommandExecutionError::ArgumentDeserializationError(
|
||||
_ => Err(Error::ArgumentDeserialization(
|
||||
"Received non String argument for DeleteTagCommand".to_string(),
|
||||
)),
|
||||
},
|
||||
None => Err(CommandExecutionError::ArgumentDeserializationError(
|
||||
None => Err(Error::ArgumentDeserialization(
|
||||
"Failed to deserialize argument for DeleteTagCommand".to_string(),
|
||||
)),
|
||||
},
|
||||
None => Err(CommandExecutionError::ArgumentExtractionError(
|
||||
None => Err(Error::InvalidCommand(
|
||||
"Failed to find argument in DeleteTagCommand".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
||||
let guild_id = match self.interaction.guild_id {
|
||||
Some(r) => Ok(r.to_string()),
|
||||
None => Err(CommandExecutionError::ContextRetrievalError(
|
||||
None => Err(Error::ContextRetrieval(
|
||||
"Failed to extract guild id from current context".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
@ -82,11 +78,11 @@ impl BotCommand for DeleteTagCommand {
|
|||
let guild = match database.get_by_id::<Guild>(&guild_id).await {
|
||||
Ok(query) => match query {
|
||||
Some(r) => Ok(r),
|
||||
None => Err(CommandExecutionError::ContextRetrievalError(
|
||||
None => Err(Error::ContextRetrieval(
|
||||
"Failed to retrieve the guild where the command was issued".to_string(),
|
||||
)),
|
||||
},
|
||||
Err(()) => Err(CommandExecutionError::DatabaseQueryError(
|
||||
Err(()) => Err(Error::DatabaseQuery(
|
||||
"Failed to access to the database".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
@ -104,7 +100,7 @@ impl BotCommand for DeleteTagCommand {
|
|||
.await
|
||||
{
|
||||
Ok(_) => Ok(String::from("Successfully remove the tag")),
|
||||
Err(()) => Err(CommandExecutionError::DatabaseQueryError(
|
||||
Err(()) => Err(Error::DatabaseQuery(
|
||||
"Failed to remove tag from the database".to_string(),
|
||||
)),
|
||||
}?
|
||||
|
@ -112,18 +108,12 @@ impl BotCommand for DeleteTagCommand {
|
|||
String::from("No matching tag for this server.")
|
||||
};
|
||||
|
||||
match self
|
||||
.interaction
|
||||
self.interaction
|
||||
.create_interaction_response(context.http, |r| {
|
||||
r.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| message.content(response))
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(()),
|
||||
Err(_e) => Err(CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to answer the initial command".to_string(),
|
||||
)),
|
||||
}
|
||||
.map_err(Error::DiscordAPICall)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ use serenity::{
|
|||
use crate::database::{models::Guild, Client as DatabaseClient};
|
||||
|
||||
use super::{
|
||||
commons::{BotCommandOption, CommandExecutionError},
|
||||
commons::{BotCommandOption, Error},
|
||||
BotCommand,
|
||||
};
|
||||
|
||||
|
@ -40,14 +40,10 @@ impl BotCommand for ListTagCommand {
|
|||
ListTagCommand { interaction }
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
context: Context,
|
||||
database: Arc<DatabaseClient>,
|
||||
) -> Result<(), CommandExecutionError> {
|
||||
async fn run(&self, context: Context, database: Arc<DatabaseClient>) -> Result<(), Error> {
|
||||
let guild_id = match self.interaction.guild_id {
|
||||
Some(id) => Ok(id.to_string()),
|
||||
None => Err(CommandExecutionError::ContextRetrievalError(
|
||||
None => Err(Error::ContextRetrieval(
|
||||
"Failed to extract guild id from current context".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
@ -55,11 +51,11 @@ impl BotCommand for ListTagCommand {
|
|||
let guild = match database.get_by_id::<Guild>(&guild_id).await {
|
||||
Ok(query) => match query {
|
||||
Some(r) => Ok(r),
|
||||
None => Err(CommandExecutionError::ContextRetrievalError(
|
||||
None => Err(Error::ContextRetrieval(
|
||||
"Failed to retrieve the guild where the command was issued".to_string(),
|
||||
)),
|
||||
},
|
||||
Err(()) => Err(CommandExecutionError::DatabaseQueryError(
|
||||
Err(()) => Err(Error::DatabaseQuery(
|
||||
"Failed to access to the database".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
@ -79,19 +75,13 @@ impl BotCommand for ListTagCommand {
|
|||
}
|
||||
}
|
||||
|
||||
match self
|
||||
.interaction
|
||||
self.interaction
|
||||
.create_interaction_response(context.http, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| message.content(response_content))
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => Err(CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to answer to the initial command".to_string(),
|
||||
)),
|
||||
}
|
||||
.map_err(Error::DiscordAPICall)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ use crate::{
|
|||
discord::message_builders::embed_builder::EmbedMessageBuilder,
|
||||
};
|
||||
|
||||
use super::commons::{BotCommand, BotCommandOption, CommandExecutionError};
|
||||
use super::commons::{BotCommand, BotCommandOption, Error};
|
||||
|
||||
pub struct SourceCodeCommand {
|
||||
interaction: ApplicationCommandInteraction,
|
||||
|
@ -39,14 +39,9 @@ impl BotCommand for SourceCodeCommand {
|
|||
SourceCodeCommand { interaction }
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
context: Context,
|
||||
_: Arc<DatabaseClient>,
|
||||
) -> Result<(), CommandExecutionError> {
|
||||
async fn run(&self, context: Context, _: Arc<DatabaseClient>) -> Result<(), Error> {
|
||||
let embed_builder = EmbedMessageBuilder::new(&context).await?;
|
||||
match self
|
||||
.interaction
|
||||
self.interaction
|
||||
.create_interaction_response(context.http, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
|
@ -55,11 +50,6 @@ impl BotCommand for SourceCodeCommand {
|
|||
})
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(()),
|
||||
Err(_e) => Err(CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to answer to the issued command".to_string(),
|
||||
)),
|
||||
}
|
||||
.map_err(Error::DiscordAPICall)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ use serenity::{
|
|||
};
|
||||
|
||||
use super::{
|
||||
commons::{BotCommandOption, CommandExecutionError},
|
||||
commons::{BotCommandOption, Error},
|
||||
BotCommand,
|
||||
};
|
||||
use crate::{
|
||||
|
@ -43,26 +43,18 @@ impl BotCommand for SubscribeCommand {
|
|||
vec![]
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
context: Context,
|
||||
database: Arc<DatabaseClient>,
|
||||
) -> Result<(), CommandExecutionError> {
|
||||
async fn run(&self, context: Context, database: Arc<DatabaseClient>) -> Result<(), Error> {
|
||||
self.interaction
|
||||
.create_interaction_response(&context.http, |response| {
|
||||
response.kind(InteractionResponseType::DeferredChannelMessageWithSource)
|
||||
})
|
||||
.await
|
||||
.map_err(|_e| {
|
||||
CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to answer with a temporary response".to_string(),
|
||||
)
|
||||
})?;
|
||||
.map_err(Error::DiscordAPICall)?;
|
||||
|
||||
let user_id = self.interaction.user.id.to_string();
|
||||
let guild_id = match self.interaction.guild_id {
|
||||
Some(id) => Ok(id.to_string()),
|
||||
None => Err(CommandExecutionError::ContextRetrievalError(
|
||||
None => Err(Error::ContextRetrieval(
|
||||
"Failed to extract guild id from current context".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
@ -70,11 +62,11 @@ impl BotCommand for SubscribeCommand {
|
|||
let guild = match database.get_by_id::<Guild>(&guild_id).await {
|
||||
Ok(query) => match query {
|
||||
Some(r) => Ok(r),
|
||||
None => Err(CommandExecutionError::ContextRetrievalError(
|
||||
None => Err(Error::ContextRetrieval(
|
||||
"Failed to retrieve the guild where the command was issued".to_string(),
|
||||
)),
|
||||
},
|
||||
Err(()) => Err(CommandExecutionError::DatabaseQueryError(
|
||||
Err(()) => Err(Error::DatabaseQuery(
|
||||
"Failed to access to the database".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
@ -117,7 +109,7 @@ impl BotCommand for SubscribeCommand {
|
|||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
CommandExecutionError::DatabaseQueryError(
|
||||
Error::DatabaseQuery(
|
||||
"Failed to update user subscriptions in database".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
@ -125,25 +117,20 @@ impl BotCommand for SubscribeCommand {
|
|||
|
||||
let mut response = match self.interaction.get_interaction_response(&context).await {
|
||||
Ok(r) => Ok(r),
|
||||
Err(_e) => Err(CommandExecutionError::ContextRetrievalError(
|
||||
Err(_e) => Err(Error::ContextRetrieval(
|
||||
"Failed to fetch initial interaction response".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
||||
response.delete_reactions(&context).await.map_err(|_e| {
|
||||
CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to remove reactions from initial response".to_string(),
|
||||
)
|
||||
})?;
|
||||
response
|
||||
.delete_reactions(&context)
|
||||
.await
|
||||
.map_err(Error::DiscordAPICall)?;
|
||||
|
||||
response
|
||||
.edit(&context, |msg| msg.suppress_embeds(true).content("Done !"))
|
||||
.await
|
||||
.map_err(|_e| {
|
||||
CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to edit content of the original response message".to_string(),
|
||||
)
|
||||
})?;
|
||||
.map_err(Error::DiscordAPICall)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ use serenity::{
|
|||
};
|
||||
|
||||
use super::{
|
||||
commons::{BotCommandOption, CommandExecutionError},
|
||||
commons::{BotCommandOption, Error},
|
||||
BotCommand,
|
||||
};
|
||||
|
||||
|
@ -46,25 +46,17 @@ impl BotCommand for TagNotifyCommand {
|
|||
vec![]
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
context: Context,
|
||||
database: Arc<DatabaseClient>,
|
||||
) -> Result<(), CommandExecutionError> {
|
||||
async fn run(&self, context: Context, database: Arc<DatabaseClient>) -> Result<(), Error> {
|
||||
self.interaction
|
||||
.create_interaction_response(&context.http, |response| {
|
||||
response.kind(InteractionResponseType::DeferredChannelMessageWithSource)
|
||||
})
|
||||
.await
|
||||
.map_err(|_e| {
|
||||
CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to answer with a temporary response".to_string(),
|
||||
)
|
||||
})?;
|
||||
.map_err(Error::DiscordAPICall)?;
|
||||
|
||||
let guild_id = match self.interaction.guild_id {
|
||||
Some(id) => Ok(id.to_string()),
|
||||
None => Err(CommandExecutionError::ContextRetrievalError(
|
||||
None => Err(Error::ContextRetrieval(
|
||||
"Failed to extract guild id from current context".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
@ -72,11 +64,11 @@ impl BotCommand for TagNotifyCommand {
|
|||
let guild = match database.get_by_id::<Guild>(&guild_id).await {
|
||||
Ok(query) => match query {
|
||||
Some(r) => Ok(r),
|
||||
None => Err(CommandExecutionError::ContextRetrievalError(
|
||||
None => Err(Error::ContextRetrieval(
|
||||
"Failed to retrieve the guild where the command was issued".to_string(),
|
||||
)),
|
||||
},
|
||||
Err(()) => Err(CommandExecutionError::DatabaseQueryError(
|
||||
Err(()) => Err(Error::DatabaseQuery(
|
||||
"Failed to access to the database".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
@ -104,7 +96,7 @@ impl BotCommand for TagNotifyCommand {
|
|||
for selected_tag in selection {
|
||||
let t = match guild.tags.iter().find(|s| s.name == selected_tag) {
|
||||
Some(t) => Ok(t),
|
||||
None => Err(CommandExecutionError::ArgumentExtractionError(
|
||||
None => Err(Error::InvalidCommand(
|
||||
"No matching tag found for selection".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
@ -128,16 +120,15 @@ impl BotCommand for TagNotifyCommand {
|
|||
|
||||
let response = match self.interaction.get_interaction_response(&context).await {
|
||||
Ok(r) => Ok(r),
|
||||
Err(_e) => Err(CommandExecutionError::ContextRetrievalError(
|
||||
Err(_e) => Err(Error::ContextRetrieval(
|
||||
"Failed to fetch initial interaction response".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
||||
response.delete(&context).await.map_err(|_e| {
|
||||
CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to remove the embed message".to_string(),
|
||||
)
|
||||
})?;
|
||||
response
|
||||
.delete(&context)
|
||||
.await
|
||||
.map_err(Error::DiscordAPICall)?;
|
||||
|
||||
// We have to create a new message as editing the original response will not notify
|
||||
// the pinged users
|
||||
|
@ -150,20 +141,12 @@ impl BotCommand for TagNotifyCommand {
|
|||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|_e| {
|
||||
CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to create a new message to ping users".to_string(),
|
||||
)
|
||||
})?;
|
||||
.map_err(Error::DiscordAPICall)?;
|
||||
} else {
|
||||
self.interaction
|
||||
.delete_original_interaction_response(&context)
|
||||
.await
|
||||
.map_err(|_e| {
|
||||
CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to delete the original interaction message".to_string(),
|
||||
)
|
||||
})?;
|
||||
.map_err(Error::DiscordAPICall)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -8,7 +8,7 @@ use super::commands::{
|
|||
|
||||
use crate::{
|
||||
database::{models::Guild, Client as DatabaseClient},
|
||||
discord::commands::{commons::CommandExecutionError, DeleteTagCommand, SubscribeCommand},
|
||||
discord::commands::{commons::Error, DeleteTagCommand, SubscribeCommand},
|
||||
};
|
||||
use serenity::{
|
||||
async_trait,
|
||||
|
@ -153,7 +153,7 @@ impl EventHandler for Handler {
|
|||
.await
|
||||
} else {
|
||||
drop(users_selector);
|
||||
Err(CommandExecutionError::SelectorError(
|
||||
Err(Error::Selector(
|
||||
"User has already a selector running".to_string(),
|
||||
))
|
||||
}
|
||||
|
@ -170,13 +170,13 @@ impl EventHandler for Handler {
|
|||
.await
|
||||
} else {
|
||||
drop(users_selector);
|
||||
Err(CommandExecutionError::SelectorError(
|
||||
Err(Error::Selector(
|
||||
"User has already a selector running".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
_ => Err(CommandExecutionError::UnknownCommand(
|
||||
_ => Err(Error::UnknownCommand(
|
||||
"Received an unknown command from Discord".to_string(),
|
||||
)),
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@ use serenity::{
|
|||
prelude::Context,
|
||||
};
|
||||
|
||||
use crate::discord::commands::commons::CommandExecutionError;
|
||||
use crate::discord::commands::commons::Error;
|
||||
|
||||
const HTML_COLOR_CODE: u32 = 0xffffff;
|
||||
|
||||
|
@ -16,12 +16,11 @@ pub struct EmbedMessageBuilder {
|
|||
}
|
||||
|
||||
impl EmbedMessageBuilder {
|
||||
pub async fn new(context: &Context) -> Result<Self, CommandExecutionError> {
|
||||
let bot_user = context.http.get_current_user().await.map_err(|_e| {
|
||||
CommandExecutionError::ContextRetrievalError(
|
||||
"Failed to get current bot user".to_string(),
|
||||
)
|
||||
})?;
|
||||
pub async fn new(context: &Context) -> Result<Self, Error> {
|
||||
let bot_user =
|
||||
context.http.get_current_user().await.map_err(|_e| {
|
||||
Error::ContextRetrieval("Failed to get current bot user".to_string())
|
||||
})?;
|
||||
|
||||
let embed_author = bot_user.name.clone();
|
||||
|
||||
|
@ -62,13 +61,20 @@ impl EmbedMessageBuilder {
|
|||
|
||||
pub fn create_bot_credentials_embed(&self) -> CreateEmbed {
|
||||
self.create_embed_base()
|
||||
.title("Credentials")
|
||||
.title("Yorokobot")
|
||||
.fields(vec![
|
||||
("Creator", "This bot was created by Victor Mignot (aka Dala).\nMastodon link: https://fosstodon.org/@Dala", false),
|
||||
("License", "The source code is under the GNU Affero General Public License v3.0", false),
|
||||
("Source code", "https://sr.ht/~dala/yorokobot/", false),
|
||||
("Illustrator's Twitter", "https://twitter.com/MaewenMitzuki", false),
|
||||
("Developer's Discord Server", "https://discord.gg/e8Q4zQbJb3", false),
|
||||
("Version", env!("CARGO_PKG_VERSION"), false),
|
||||
(
|
||||
"License",
|
||||
"The source code is under the European Union Public License v1.2",
|
||||
false,
|
||||
),
|
||||
("Source code", env!("CARGO_PKG_REPOSITORY"), false),
|
||||
(
|
||||
"Illustrator's Twitter",
|
||||
"https://twitter.com/MaewenMitzuki",
|
||||
false,
|
||||
),
|
||||
])
|
||||
.to_owned()
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use serenity::{
|
|||
prelude::Context,
|
||||
};
|
||||
|
||||
use crate::discord::commands::commons::CommandExecutionError;
|
||||
use crate::discord::commands::commons::Error;
|
||||
|
||||
use super::embed_builder::EmbedMessageBuilder;
|
||||
|
||||
|
@ -64,10 +64,7 @@ impl<'a> EmbedSelector<'a> {
|
|||
selectable: Vec<String>,
|
||||
initial_selection: Option<HashSet<String>>,
|
||||
) -> Self {
|
||||
let selection = match initial_selection {
|
||||
Some(r) => r,
|
||||
None => HashSet::new(),
|
||||
};
|
||||
let selection = initial_selection.unwrap_or_default();
|
||||
|
||||
let mut selector = EmbedSelector {
|
||||
interaction,
|
||||
|
@ -86,9 +83,7 @@ impl<'a> EmbedSelector<'a> {
|
|||
selector
|
||||
}
|
||||
|
||||
pub async fn get_user_selection(
|
||||
&mut self,
|
||||
) -> Result<Option<HashSet<String>>, CommandExecutionError> {
|
||||
pub async fn get_user_selection(&mut self) -> Result<Option<HashSet<String>>, Error> {
|
||||
let embed_builder = EmbedMessageBuilder::new(self.context).await?;
|
||||
|
||||
match self
|
||||
|
@ -110,19 +105,16 @@ impl<'a> EmbedSelector<'a> {
|
|||
self.display_reactions().await?;
|
||||
Ok(self.wait_selector_end().await?)
|
||||
}
|
||||
Err(_e) => Err(CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to edit original interaction response".to_string(),
|
||||
)),
|
||||
Err(e) => Err(Error::DiscordAPICall(e)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn display_reactions(&self) -> Result<(), CommandExecutionError> {
|
||||
async fn display_reactions(&self) -> Result<(), Error> {
|
||||
if let Some(answer) = &self.embed_answer {
|
||||
answer.delete_reactions(self.context).await.map_err(|_e| {
|
||||
CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to delete reaction on the current selector".to_string(),
|
||||
)
|
||||
})?;
|
||||
answer
|
||||
.delete_reactions(self.context)
|
||||
.await
|
||||
.map_err(Error::DiscordAPICall)?;
|
||||
|
||||
for emote in SELECTION_EMOTES[0..self.get_current_page_choice_number()]
|
||||
.iter()
|
||||
|
@ -140,30 +132,24 @@ impl<'a> EmbedSelector<'a> {
|
|||
Some(a) => a
|
||||
.react(self.context, ReactionType::Unicode(emote.to_string()))
|
||||
.await
|
||||
.map_err(|_e| {
|
||||
CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to add reactions on the current selector".to_string(),
|
||||
)
|
||||
}),
|
||||
None => Err(CommandExecutionError::SelectorError(
|
||||
.map_err(Error::DiscordAPICall),
|
||||
None => Err(Error::Selector(
|
||||
"Failed to refresh the reactions of the current selector".to_string(),
|
||||
)),
|
||||
}?;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CommandExecutionError::SelectorError(
|
||||
Err(Error::Selector(
|
||||
"Tried to delete reaction from a non existent message".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_selector_end(
|
||||
&mut self,
|
||||
) -> Result<Option<HashSet<String>>, CommandExecutionError> {
|
||||
async fn wait_selector_end(&mut self) -> Result<Option<HashSet<String>>, Error> {
|
||||
let answer = match &self.embed_answer {
|
||||
Some(a) => Ok(a),
|
||||
None => Err(CommandExecutionError::SelectorError(
|
||||
None => Err(Error::Selector(
|
||||
"Tried to start collector before sending it".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
@ -174,11 +160,7 @@ impl<'a> EmbedSelector<'a> {
|
|||
.add_message_id(*answer.id.as_u64())
|
||||
.timeout(Duration::from_secs(COLLECTOR_MAX_DURATION_SEC))
|
||||
.build()
|
||||
.map_err(|_e| {
|
||||
CommandExecutionError::SelectorError(
|
||||
"Failed to build the EventCollector".to_string(),
|
||||
)
|
||||
})?;
|
||||
.map_err(|_e| Error::Selector("Failed to build the EventCollector".to_string()))?;
|
||||
|
||||
while let Some(reaction_event) = collector.next().await {
|
||||
let reaction = match *reaction_event {
|
||||
|
@ -235,11 +217,10 @@ impl<'a> EmbedSelector<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
reaction.delete(self.context).await.map_err(|_e| {
|
||||
CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to delete reaction from selector".to_string(),
|
||||
)
|
||||
})?;
|
||||
reaction
|
||||
.delete(self.context)
|
||||
.await
|
||||
.map_err(Error::DiscordAPICall)?;
|
||||
}
|
||||
}
|
||||
collector.stop();
|
||||
|
@ -251,7 +232,7 @@ impl<'a> EmbedSelector<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
async fn next_page(&mut self) -> Result<(), CommandExecutionError> {
|
||||
async fn next_page(&mut self) -> Result<(), Error> {
|
||||
if self.current_page != self.page_number {
|
||||
self.current_page += 1;
|
||||
self.refresh_embed_selection().await?;
|
||||
|
@ -259,7 +240,7 @@ impl<'a> EmbedSelector<'a> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn previous_page(&mut self) -> Result<(), CommandExecutionError> {
|
||||
async fn previous_page(&mut self) -> Result<(), Error> {
|
||||
if self.current_page != 1 {
|
||||
self.current_page -= 1;
|
||||
self.refresh_embed_selection().await?;
|
||||
|
@ -277,7 +258,7 @@ impl<'a> EmbedSelector<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
async fn refresh_embed_selection(&mut self) -> Result<(), CommandExecutionError> {
|
||||
async fn refresh_embed_selection(&mut self) -> Result<(), Error> {
|
||||
let embed_builder = EmbedMessageBuilder::new(self.context).await?;
|
||||
|
||||
let curr_choices = self.selectable
|
||||
|
@ -298,10 +279,6 @@ impl<'a> EmbedSelector<'a> {
|
|||
))
|
||||
})
|
||||
.await
|
||||
.map_err(|_e| {
|
||||
CommandExecutionError::DiscordAPICallError(
|
||||
"Failed to edit selector content".to_string(),
|
||||
)
|
||||
})
|
||||
.map_err(Error::DiscordAPICall)
|
||||
}
|
||||
}
|
||||
|
|
64
src/logs.rs
64
src/logs.rs
|
@ -1,64 +0,0 @@
|
|||
use log::LevelFilter;
|
||||
|
||||
use log4rs::{
|
||||
append::{
|
||||
console::ConsoleAppender,
|
||||
rolling_file::{
|
||||
policy::compound::{
|
||||
roll::fixed_window::FixedWindowRoller, trigger::size::SizeTrigger, CompoundPolicy,
|
||||
},
|
||||
RollingFileAppender,
|
||||
},
|
||||
},
|
||||
config::{Appender, Logger, Root},
|
||||
encode::pattern::PatternEncoder,
|
||||
Config,
|
||||
};
|
||||
|
||||
const FILE_MAX_SIZE: u64 = 30000000; //30 mB
|
||||
const LOGS_MAX_FILES: u32 = 10;
|
||||
|
||||
const LOG_FORMAT: &str = "{d(%Y-%m-%d %H:%M:%S)} | {({l}):5.5} | {f}:{L} — {m}{n}";
|
||||
const LOGS_FILE: &str = "logs/yorokobot_latest.log";
|
||||
const LOGS_ARCHIVE_FILE_PATTERN: &str = "logs/yorokobot_{}.gz";
|
||||
|
||||
/// Configure the bot logger
|
||||
pub fn init_logger() -> log4rs::Handle {
|
||||
// Rollings logs trigger
|
||||
let size_triger = SizeTrigger::new(FILE_MAX_SIZE);
|
||||
|
||||
// Rolling logs rollers
|
||||
let logs_roller = FixedWindowRoller::builder()
|
||||
.base(1)
|
||||
.build(LOGS_ARCHIVE_FILE_PATTERN, LOGS_MAX_FILES)
|
||||
.unwrap();
|
||||
|
||||
// Rolling logs policies
|
||||
let rolling_logs_policy = CompoundPolicy::new(Box::new(size_triger), Box::new(logs_roller));
|
||||
|
||||
// Appenders
|
||||
let stdout = ConsoleAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new(LOG_FORMAT)))
|
||||
.build();
|
||||
let rolling_logs = RollingFileAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new(LOG_FORMAT)))
|
||||
.build(LOGS_FILE, Box::new(rolling_logs_policy))
|
||||
.unwrap();
|
||||
|
||||
let config = Config::builder()
|
||||
.appender(Appender::builder().build("stdout", Box::new(stdout)))
|
||||
.appender(Appender::builder().build("rolling_logs", Box::new(rolling_logs)))
|
||||
// Don't print the serenity crate logs
|
||||
.logger(Logger::builder().build("serenity", LevelFilter::Error))
|
||||
// Don't print the tracing crate logs
|
||||
.logger(Logger::builder().build("tracing", LevelFilter::Error))
|
||||
.build(
|
||||
Root::builder()
|
||||
.appender("stdout")
|
||||
.appender("rolling_logs")
|
||||
.build(LevelFilter::Info),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
log4rs::init_config(config).unwrap()
|
||||
}
|
|
@ -3,23 +3,16 @@
|
|||
//!
|
||||
//! [`Serenity`]: https://github.com/serenity-rs/serenity
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(warnings)]
|
||||
|
||||
mod client;
|
||||
mod database;
|
||||
mod discord;
|
||||
mod environment;
|
||||
mod logs;
|
||||
|
||||
use client::Client;
|
||||
use logs::init_logger;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Start the logger
|
||||
let _handle = init_logger();
|
||||
|
||||
env_logger::init();
|
||||
let mut client = Client::new().await;
|
||||
|
||||
client.start().await;
|
||||
|
|
Loading…
Add table
Reference in a new issue