initial commit
This commit is contained in:
commit
b2e6bb1dfa
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
/target
|
||||
.direnv
|
||||
/logs
|
||||
config.json
|
||||
config.toml
|
||||
config/
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT * FROM custom_commands WHERE command = $1;",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "command",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "response",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "90608f616dfbc17de3ccfe5a30b3ad7dc75ad438de55755aefe3b9a7e46f5c0c"
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT command FROM custom_commands WHERE command LIKE $1;",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "command",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ab9481dff0717ae2d901ffd4a03b742d80c687f8d29ec6e86cc0f0e066f4be82"
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO custom_commands (command, response) VALUES (LOWER($1), $2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d8866cf840461fd954a5345cce31b1419c2075ca1c411ba52c6057b9e35fab2a"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM custom_commands WHERE command = $1;",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f3b61b3e21833c2bb158febb1df80b46af882d1391552389f771893a304f5981"
|
||||
}
|
3515
Cargo.lock
generated
Normal file
3515
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
24
Cargo.toml
Normal file
24
Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "blizzpacksbot"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
color-eyre = "0.6.5"
|
||||
config = "0.15.11"
|
||||
poise = "0.6.1"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres"] }
|
||||
tokio = { version = "1.45.1", features = [
|
||||
"tracing",
|
||||
"macros",
|
||||
"rt-multi-thread",
|
||||
] }
|
||||
tracing = "0.1.41"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
|
||||
[features]
|
||||
default = ["cache"]
|
||||
cache = ["poise/cache"]
|
169
flake.lock
Normal file
169
flake.lock
Normal file
|
@ -0,0 +1,169 @@
|
|||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1745454774,
|
||||
"narHash": "sha256-oLvmxOnsEKGtwczxp/CwhrfmQUG2ym24OMWowcoRhH8=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "efd36682371678e2b6da3f108fdb5c613b3ec598",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1748821116,
|
||||
"narHash": "sha256-F82+gS044J1APL0n4hH50GYdPRv/5JWm34oCJYmVKdE=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "49f0870db23e8c1ca0b5259734a02cd9e1e371a1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1748929857,
|
||||
"narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1748740939,
|
||||
"narHash": "sha256-rQaysilft1aVMwF14xIdGS3sj1yHlI6oKQNBRTF40cc=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "656a64127e9d791a334452c6b6606d17539476e2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1745234285,
|
||||
"narHash": "sha256-GfpyMzxwkfgRVN0cTGQSkTC0OHhEkv3Jf6Tcjm//qZ0=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c11863f1e964833214b767f4a369c6e6a7aba141",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1747958103,
|
||||
"narHash": "sha256-qmmFCrfBwSHoWw7cVK4Aj+fns+c54EBP8cGqp/yK410=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "fe51d34885f7b5e3e7b59572796e1bcb427eccb1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-flake": "rust-flake",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
}
|
||||
},
|
||||
"rust-flake": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"rust-overlay": "rust-overlay"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1745474556,
|
||||
"narHash": "sha256-ZOK3isVPOsJoAXw7dWnWq31elONt2xF1Q9THLfhKA7w=",
|
||||
"owner": "juspay",
|
||||
"repo": "rust-flake",
|
||||
"rev": "f69408a404f09afe0d85be88eddff07a054c2397",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "juspay",
|
||||
"repo": "rust-flake",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"rust-flake",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1745462120,
|
||||
"narHash": "sha256-TbVjPOl+Cg5vZ7TIn1KpQ8SOfHKD6OEgu84b6YSCfKE=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "79d3acd1a7e67fb9315fa5c5556eb6adf93dc2da",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1749194973,
|
||||
"narHash": "sha256-eEy8cuS0mZ2j/r/FE0/LYBSBcIs/MKOIVakwHVuqTfk=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "a05be418a1af1198ca0f63facb13c985db4cb3c5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
57
flake.nix
Normal file
57
flake.nix
Normal file
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
description = "Description for the project";
|
||||
|
||||
inputs = {
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
rust-flake.url = "github:juspay/rust-flake";
|
||||
treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs@{ flake-parts, ... }:
|
||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
imports = [
|
||||
inputs.rust-flake.flakeModules.default
|
||||
inputs.rust-flake.flakeModules.nixpkgs
|
||||
inputs.treefmt-nix.flakeModule
|
||||
];
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
"aarch64-darwin"
|
||||
"x86_64-darwin"
|
||||
];
|
||||
perSystem =
|
||||
{
|
||||
pkgs,
|
||||
self',
|
||||
...
|
||||
}:
|
||||
{
|
||||
# Per-system attributes can be defined here. The self' and inputs'
|
||||
# module parameters provide easy access to attributes of the same
|
||||
# system.
|
||||
|
||||
# Equivalent to inputs'.nixpkgs.legacyPackages.hello;
|
||||
packages.default = pkgs.hello;
|
||||
treefmt.programs = {
|
||||
nixfmt.enable = true;
|
||||
deadnix.enable = true;
|
||||
statix.enable = true;
|
||||
rustfmt.enable = true;
|
||||
};
|
||||
devShells.default = pkgs.mkShell {
|
||||
inputsFrom = [ self'.devShells.rust ];
|
||||
buildInputs = with pkgs; [ sqlx-cli postgresql libpq ];
|
||||
DATABASE_URL="postgres://twitchbot@noproxy.groundcrafter.de/twitchbot";
|
||||
};
|
||||
};
|
||||
flake = {
|
||||
# The usual flake attributes can be defined here, including system-
|
||||
# agnostic ones like nixosModule and system-enumerating ones, although
|
||||
# those are more easily expressed in perSystem.
|
||||
|
||||
};
|
||||
};
|
||||
}
|
5
migrations/20250609201443_custom_commands.sql
Normal file
5
migrations/20250609201443_custom_commands.sql
Normal file
|
@ -0,0 +1,5 @@
|
|||
CREATE TABLE IF NOT EXISTS custom_commands (
|
||||
id SERIAL PRIMARY KEY,
|
||||
command TEXT UNIQUE NOT NULL,
|
||||
response TEXT NOT NULL
|
||||
);
|
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "nightly"
|
17
src/builtins.rs
Normal file
17
src/builtins.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
use crate::{Context, Error};
|
||||
|
||||
#[cfg(feature = "cache")]
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
pub async fn servers(ctx: Context<'_>) -> Result<(), Error> {
|
||||
poise::builtins::servers(ctx).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
pub async fn help(ctx: Context<'_>, command: Option<String>) -> Result<(), Error> {
|
||||
let configuration = poise::builtins::HelpConfiguration {
|
||||
..Default::default()
|
||||
};
|
||||
poise::builtins::help(ctx, command.as_deref(), configuration).await?;
|
||||
Ok(())
|
||||
}
|
105
src/commands/admin.rs
Normal file
105
src/commands/admin.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use color_eyre::{Section, eyre};
|
||||
use poise::serenity_prelude::{
|
||||
ChannelType, CreateChannel, PermissionOverwrite, PermissionOverwriteType, Permissions, RoleId,
|
||||
};
|
||||
|
||||
use crate::Context;
|
||||
|
||||
async fn has_mod_role(ctx: Context<'_>) -> eyre::Result<bool> {
|
||||
let mod_role = ctx.data().settings.roles.moderator;
|
||||
match ctx.author_member().await {
|
||||
Some(member) => match member.roles(ctx) {
|
||||
Some(roles) => Ok(roles.iter().any(|role| role.id.get() == mod_role)),
|
||||
None => Ok(false),
|
||||
},
|
||||
None => Err(eyre::eyre!("no member")),
|
||||
}
|
||||
}
|
||||
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
check = "has_mod_role",
|
||||
guild_only,
|
||||
required_bot_permissions = "MANAGE_CHANNELS|MANAGE_ROLES"
|
||||
)]
|
||||
pub async fn new_modpack(
|
||||
ctx: Context<'_>,
|
||||
#[description = "modpack name"] name: String,
|
||||
) -> eyre::Result<()> {
|
||||
let Some(guild) = ctx.guild().map(|g| g.clone()) else {
|
||||
ctx.reply("Diser Befehl muss in einem Server ausgeführt werden.")
|
||||
.await?;
|
||||
return Err(eyre::eyre!("new_modpack is a guild-only command."));
|
||||
};
|
||||
let base_perms = Vec::new();
|
||||
let parent_channel = guild
|
||||
.create_channel(
|
||||
ctx,
|
||||
CreateChannel::new(name)
|
||||
.kind(ChannelType::Category)
|
||||
.permissions(base_perms.clone()),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| eyre::eyre!(e).wrap_err("Error creating category channel"))?;
|
||||
guild
|
||||
.create_channel(
|
||||
ctx,
|
||||
CreateChannel::new("announcements")
|
||||
.kind(
|
||||
if guild.features.contains(&"COMMUNITY".to_string())
|
||||
|| guild.features.contains(&"NEWS".to_string())
|
||||
{
|
||||
ChannelType::News
|
||||
} else {
|
||||
ChannelType::Text
|
||||
},
|
||||
)
|
||||
.permissions(vec![PermissionOverwrite {
|
||||
allow: Permissions::empty(),
|
||||
deny: Permissions::SEND_MESSAGES,
|
||||
kind: PermissionOverwriteType::Role(RoleId::new(guild.id.into())),
|
||||
}])
|
||||
.category(&parent_channel),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| eyre::eyre!(e).wrap_err("Error creating announcements channel"))?;
|
||||
guild
|
||||
.create_channel(
|
||||
ctx,
|
||||
CreateChannel::new("bug-reports")
|
||||
.kind(ChannelType::Text)
|
||||
.category(&parent_channel),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| eyre::Report::msg("Error creating bug-reports channel").error(e))?;
|
||||
guild
|
||||
.create_channel(
|
||||
ctx,
|
||||
CreateChannel::new("suggestions")
|
||||
.kind(ChannelType::Text)
|
||||
.category(&parent_channel),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| eyre::eyre!(e).wrap_err("Creating suggestions channel"))?;
|
||||
guild
|
||||
.create_channel(
|
||||
ctx,
|
||||
CreateChannel::new("main")
|
||||
.kind(ChannelType::Text)
|
||||
.category(&parent_channel),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| eyre::eyre!(e).wrap_err("Creating main channel"))?;
|
||||
guild
|
||||
.create_channel(
|
||||
ctx,
|
||||
CreateChannel::new("Voice")
|
||||
.kind(ChannelType::Voice)
|
||||
.category(&parent_channel),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| eyre::eyre!(e).wrap_err("Creating Voice channel"))?;
|
||||
|
||||
ctx.reply("Modpack Kategorie und Kanäle erstellt.").await?;
|
||||
Ok(())
|
||||
}
|
140
src/commands/custom_commands.rs
Normal file
140
src/commands/custom_commands.rs
Normal file
|
@ -0,0 +1,140 @@
|
|||
use color_eyre::eyre::{self, eyre};
|
||||
use poise::serenity_prelude::{self, Message};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{Context, Data};
|
||||
|
||||
pub async fn execute(
|
||||
ctx: &serenity_prelude::Context,
|
||||
data: &Data,
|
||||
msg: &Message,
|
||||
command_name: String,
|
||||
) -> eyre::Result<()> {
|
||||
let full_command_name = format!("!{}", command_name.to_lowercase());
|
||||
let db = &data.db;
|
||||
let query = sqlx::query!(
|
||||
"SELECT * FROM custom_commands WHERE command = $1;",
|
||||
command_name
|
||||
);
|
||||
match query.fetch_optional(db).await {
|
||||
Ok(Some(response)) => match msg.reply(ctx, response.response).await {
|
||||
Ok(_) => {
|
||||
info!("replied to customcommand response");
|
||||
}
|
||||
Err(why) => {
|
||||
error!("Failed to send customcommand response: {why}");
|
||||
}
|
||||
},
|
||||
Ok(None) => {
|
||||
info!("Could not find customcommand {full_command_name}");
|
||||
}
|
||||
Err(why) => {
|
||||
return Err(eyre!("Failed to fetch customcommand from database: {why}"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
pub async fn add(ctx: Context<'_>, command_name: String, response: String) -> eyre::Result<()> {
|
||||
match sqlx::query!(
|
||||
r#"INSERT INTO custom_commands (command, response) VALUES (LOWER($1), $2)"#,
|
||||
command_name,
|
||||
response
|
||||
)
|
||||
.execute(&ctx.data().db)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
if result.rows_affected() > 0 {
|
||||
ctx.reply(format!(
|
||||
"Successfully added command `{}{command_name}`.",
|
||||
ctx.data().settings.prefix
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Err(why) => return Err(why.into()),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn autocomplete_commandname(ctx: Context<'_>, partial: &str) -> Vec<String> {
|
||||
let arg = format!("%{}%", partial.to_lowercase());
|
||||
let query = sqlx::query!(
|
||||
"SELECT command FROM custom_commands WHERE command LIKE $1;",
|
||||
arg
|
||||
);
|
||||
query
|
||||
.fetch_all(&ctx.data().db)
|
||||
.await
|
||||
.unwrap_or(vec![])
|
||||
.iter()
|
||||
.map(|e| e.command.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
pub async fn del(
|
||||
ctx: Context<'_>,
|
||||
#[autocomplete = "autocomplete_commandname"] command_name: String,
|
||||
) -> eyre::Result<()> {
|
||||
let query = sqlx::query!(
|
||||
r#"DELETE FROM custom_commands WHERE command = $1;"#,
|
||||
command_name
|
||||
);
|
||||
match query.execute(&ctx.data().db).await {
|
||||
Ok(result) => {
|
||||
if result.rows_affected() == 1 {
|
||||
ctx.reply(format!(
|
||||
"Der Befehl {}{command_name} wurde gelöscht.",
|
||||
ctx.data().settings.prefix
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Err(why) => {
|
||||
ctx.reply("Ein Fehler ist aufgetreten.").await?;
|
||||
return Err(why.into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn edit(
|
||||
ctx: Context<'_>,
|
||||
#[autocomplete = "autocomplete_commandname"] command_name: String,
|
||||
response: String,
|
||||
) -> eyre::Result<()> {
|
||||
let query = sqlx::query!(
|
||||
r#"SELECT * FROM custom_commands WHERE command = $1;"#,
|
||||
command_name
|
||||
);
|
||||
match query.fetch_optional(&ctx.data().db).await {
|
||||
Ok(Some(result)) => {
|
||||
let update_query = sqlx::query!(
|
||||
"UPDATE custom_commands SET response = $2 WHERE id = $1;",
|
||||
result.id,
|
||||
response
|
||||
);
|
||||
match update_query.execute(&ctx.data().db).await {
|
||||
Ok(_) => {
|
||||
ctx.reply(format!("Customcommand {command_name} wurde aktualisiert."))
|
||||
.await?;
|
||||
}
|
||||
Err(why) => {
|
||||
ctx.reply(format!("Beim aktualisieren des Customcommands {command_name} ist ein Fehler aufgetreten.")).await?;
|
||||
return Err(why.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
ctx.reply(format!("there is no customcommand {command_name}"))
|
||||
.await?;
|
||||
}
|
||||
Err(why) => return Err(why.into()),
|
||||
}
|
||||
Ok(())
|
||||
}
|
2
src/commands/mod.rs
Normal file
2
src/commands/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod admin;
|
||||
pub mod custom_commands;
|
166
src/event_handler.rs
Normal file
166
src/event_handler.rs
Normal file
|
@ -0,0 +1,166 @@
|
|||
use color_eyre::eyre;
|
||||
use poise::serenity_prelude::{
|
||||
self as serenity, Channel, ChannelId, Colour, CreateEmbed, CreateEmbedAuthor,
|
||||
CreateEmbedFooter, CreateMessage,
|
||||
};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::Data;
|
||||
|
||||
async fn get_parent_channel_name(ctx: &serenity::Context, channel: &Channel) -> String {
|
||||
match channel
|
||||
.clone()
|
||||
.guild()
|
||||
.and_then(|channel| channel.parent_id)
|
||||
.map(|parent_id| parent_id.name(&ctx))
|
||||
{
|
||||
Some(p) => p.await.map(|e| e + " - ").unwrap_or(String::new()),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn deleted_log(
|
||||
ctx: &serenity::Context,
|
||||
channel_id: &ChannelId,
|
||||
deleted_message_id: &serenity::MessageId,
|
||||
data: &Data,
|
||||
) {
|
||||
let embed = if let Some(msg) = ctx
|
||||
.cache
|
||||
.message(channel_id, deleted_message_id)
|
||||
.map(|m| m.clone())
|
||||
{
|
||||
let embed_color = message_member_color(ctx, &msg);
|
||||
let message_channel = msg.channel(&ctx);
|
||||
let channel_name = get_channel_name(ctx, message_channel).await;
|
||||
CreateEmbed::new()
|
||||
.author(
|
||||
CreateEmbedAuthor::new(msg.author.tag()).icon_url(
|
||||
msg.author
|
||||
.avatar_url()
|
||||
.unwrap_or_else(|| msg.author.default_avatar_url()),
|
||||
),
|
||||
)
|
||||
.description(msg.content_safe(ctx))
|
||||
.color(embed_color.await)
|
||||
.footer(CreateEmbedFooter::new(channel_name))
|
||||
} else {
|
||||
CreateEmbed::new().title("Deleted").description(format!(
|
||||
"Could not get message details. ({deleted_message_id})"
|
||||
))
|
||||
};
|
||||
let res = ChannelId::new(data.settings.channels.log)
|
||||
.send_message(ctx, CreateMessage::new().add_embed(embed));
|
||||
match res.await {
|
||||
Ok(_m) => info!("Successfully sent deleted log for {deleted_message_id}"),
|
||||
Err(why) => error!("Failed to send a message to deleted log: {why}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn message_member_color(ctx: &serenity::Context, msg: &serenity::Message) -> Colour {
|
||||
if let Ok(mem) = msg.member(&ctx).await {
|
||||
if let Some(colour) = mem.colour(ctx) {
|
||||
return colour;
|
||||
}
|
||||
}
|
||||
Colour::default()
|
||||
}
|
||||
|
||||
async fn get_channel_name(
|
||||
ctx: &serenity::Context,
|
||||
message_channel: impl Future<Output = Result<Channel, serenity::Error>>,
|
||||
) -> String {
|
||||
match message_channel.await {
|
||||
Ok(c) => {
|
||||
let parent_name = get_parent_channel_name(ctx, &c).await;
|
||||
parent_name + &c.guild().map_or(String::new(), |gc| gc.name().to_string())
|
||||
}
|
||||
Err(_) => "<unknown-channel>".to_owned(),
|
||||
}
|
||||
}
|
||||
pub async fn event_handler(
|
||||
ctx: &serenity::Context,
|
||||
event: &serenity::FullEvent,
|
||||
data: &Data,
|
||||
) -> eyre::Result<()> {
|
||||
use serenity::FullEvent;
|
||||
match event {
|
||||
FullEvent::MessageDelete {
|
||||
channel_id,
|
||||
deleted_message_id,
|
||||
guild_id: _,
|
||||
} => {
|
||||
Box::pin(deleted_log(ctx, channel_id, deleted_message_id, data)).await;
|
||||
}
|
||||
FullEvent::Message { .. }
|
||||
| FullEvent::MessageDeleteBulk { .. }
|
||||
| FullEvent::CacheReady { .. }
|
||||
| FullEvent::ShardsReady { .. }
|
||||
| FullEvent::ChannelCreate { .. }
|
||||
| FullEvent::CategoryCreate { .. }
|
||||
| FullEvent::CategoryDelete { .. }
|
||||
| FullEvent::ChannelDelete { .. }
|
||||
| FullEvent::ChannelPinsUpdate { .. }
|
||||
| FullEvent::ChannelUpdate { .. }
|
||||
| FullEvent::GuildAuditLogEntryCreate { .. }
|
||||
| FullEvent::GuildBanRemoval { .. }
|
||||
| FullEvent::GuildCreate { .. }
|
||||
| FullEvent::GuildDelete { .. }
|
||||
| FullEvent::GuildEmojisUpdate { .. }
|
||||
| FullEvent::GuildIntegrationsUpdate { .. }
|
||||
| FullEvent::GuildMemberAddition { .. }
|
||||
| FullEvent::GuildMemberRemoval { .. }
|
||||
| FullEvent::GuildMemberUpdate { .. }
|
||||
| FullEvent::GuildMembersChunk { .. }
|
||||
| FullEvent::GuildRoleCreate { .. }
|
||||
| FullEvent::GuildRoleDelete { .. }
|
||||
| FullEvent::GuildRoleUpdate { .. }
|
||||
| FullEvent::GuildStickersUpdate { .. }
|
||||
| FullEvent::GuildUpdate { .. }
|
||||
| FullEvent::InviteCreate { .. }
|
||||
| FullEvent::InviteDelete { .. }
|
||||
| FullEvent::MessageUpdate { .. }
|
||||
| FullEvent::ReactionAdd { .. }
|
||||
| FullEvent::ReactionRemove { .. }
|
||||
| FullEvent::ReactionRemoveAll { .. }
|
||||
| FullEvent::ReactionRemoveEmoji { .. }
|
||||
| FullEvent::Ready { .. }
|
||||
| FullEvent::PresenceUpdate { .. }
|
||||
| FullEvent::Resume { .. }
|
||||
| FullEvent::ShardStageUpdate { .. }
|
||||
| FullEvent::TypingStart { .. }
|
||||
| FullEvent::UserUpdate { .. }
|
||||
| FullEvent::VoiceServerUpdate { .. }
|
||||
| FullEvent::VoiceStateUpdate { .. }
|
||||
| FullEvent::VoiceChannelStatusUpdate { .. }
|
||||
| FullEvent::WebhookUpdate { .. }
|
||||
| FullEvent::InteractionCreate { .. }
|
||||
| FullEvent::IntegrationCreate { .. }
|
||||
| FullEvent::IntegrationUpdate { .. }
|
||||
| FullEvent::IntegrationDelete { .. }
|
||||
| FullEvent::StageInstanceCreate { .. }
|
||||
| FullEvent::StageInstanceUpdate { .. }
|
||||
| FullEvent::StageInstanceDelete { .. }
|
||||
| FullEvent::ThreadCreate { .. }
|
||||
| FullEvent::ThreadUpdate { .. }
|
||||
| FullEvent::ThreadDelete { .. }
|
||||
| FullEvent::ThreadListSync { .. }
|
||||
| FullEvent::ThreadMemberUpdate { .. }
|
||||
| FullEvent::ThreadMembersUpdate { .. }
|
||||
| FullEvent::GuildScheduledEventCreate { .. }
|
||||
| FullEvent::GuildScheduledEventUpdate { .. }
|
||||
| FullEvent::GuildScheduledEventDelete { .. }
|
||||
| FullEvent::GuildScheduledEventUserAdd { .. }
|
||||
| FullEvent::GuildScheduledEventUserRemove { .. }
|
||||
| FullEvent::EntitlementCreate { .. }
|
||||
| FullEvent::EntitlementUpdate { .. }
|
||||
| FullEvent::EntitlementDelete { .. }
|
||||
| FullEvent::MessagePollVoteAdd { .. }
|
||||
| FullEvent::MessagePollVoteRemove { .. }
|
||||
| FullEvent::Ratelimit { .. } => {}
|
||||
e => {
|
||||
warn!("Encountered an unknown event: {}", e.snake_case_name());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
41
src/logging.rs
Normal file
41
src/logging.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
pub fn init() {
|
||||
let file_appender = RollingFileAppender::builder()
|
||||
.rotation(Rotation::DAILY)
|
||||
.max_log_files(7)
|
||||
.filename_suffix("log")
|
||||
.build("logs")
|
||||
.expect("failed to construct rolling file appender");
|
||||
let console_layer = fmt::layer()
|
||||
.with_target(true)
|
||||
.with_level(true)
|
||||
.with_ansi(true)
|
||||
.with_line_number(true)
|
||||
.with_file(true)
|
||||
.with_span_events(fmt::format::FmtSpan::CLOSE)
|
||||
.pretty();
|
||||
|
||||
let file_layer = fmt::layer()
|
||||
.with_writer(file_appender)
|
||||
.with_ansi(false)
|
||||
.with_target(true)
|
||||
.with_level(true)
|
||||
.with_line_number(true)
|
||||
.with_file(true)
|
||||
.with_span_events(fmt::format::FmtSpan::CLOSE)
|
||||
.compact();
|
||||
let crate_name = env!("CARGO_PKG_NAME").replace('-', "_");
|
||||
let filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::ERROR.into())
|
||||
.parse(format!("{},{}={}", "error", crate_name, LevelFilter::INFO))
|
||||
.expect("Failed to build EnvFilter");
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(console_layer)
|
||||
.with(file_layer)
|
||||
.init();
|
||||
}
|
131
src/main.rs
Normal file
131
src/main.rs
Normal file
|
@ -0,0 +1,131 @@
|
|||
mod builtins;
|
||||
mod commands;
|
||||
mod event_handler;
|
||||
mod logging;
|
||||
mod settings;
|
||||
|
||||
use color_eyre::eyre;
|
||||
use poise::serenity_prelude as serenity;
|
||||
use serenity::cache::Settings as CacheSettings;
|
||||
use sqlx::PgPool;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::{commands::custom_commands, event_handler::event_handler, settings::Settings};
|
||||
|
||||
type Error = eyre::ErrReport;
|
||||
type Context<'a> = poise::Context<'a, Data, Error>;
|
||||
#[derive(Debug)]
|
||||
pub struct Data {
|
||||
pub settings: Settings,
|
||||
pub db: PgPool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> eyre::Result<()> {
|
||||
logging::init();
|
||||
let settings = Settings::load()?;
|
||||
|
||||
debug!("parsed settings: {settings:?}");
|
||||
let db = PgPool::connect(&settings.database_url).await?;
|
||||
sqlx::migrate!().run(&db).await?;
|
||||
let settings_clone = settings.clone();
|
||||
let framework = build_framework(settings.clone(), db);
|
||||
|
||||
let token = std::env::var("DISCORD_TOKEN").unwrap_or(settings_clone.token);
|
||||
let intents = serenity::GatewayIntents::non_privileged()
|
||||
| serenity::GatewayIntents::MESSAGE_CONTENT
|
||||
| serenity::GatewayIntents::GUILD_MESSAGES
|
||||
| serenity::GatewayIntents::GUILD_MESSAGE_REACTIONS
|
||||
| serenity::GatewayIntents::GUILDS;
|
||||
|
||||
let mut cache_settings = CacheSettings::default();
|
||||
cache_settings.max_messages = 1000;
|
||||
let mut client = serenity::ClientBuilder::new(token, intents)
|
||||
.cache_settings(cache_settings)
|
||||
.framework(framework)
|
||||
.await?;
|
||||
|
||||
client.start().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn build_framework(settings: Settings, db: PgPool) -> poise::Framework<Data, eyre::ErrReport> {
|
||||
poise::Framework::builder()
|
||||
.options(poise::FrameworkOptions {
|
||||
commands: vec![
|
||||
#[cfg(feature = "cache")]
|
||||
builtins::servers(),
|
||||
builtins::help(),
|
||||
commands::custom_commands::add(),
|
||||
commands::custom_commands::del(),
|
||||
commands::custom_commands::edit(),
|
||||
commands::admin::new_modpack(),
|
||||
],
|
||||
prefix_options: poise::PrefixFrameworkOptions {
|
||||
prefix: Some(settings.prefix.clone()),
|
||||
|
||||
..Default::default()
|
||||
},
|
||||
event_handler: |ctx, event, _framework, data| {
|
||||
Box::pin(async move { Box::pin(event_handler(ctx, event, data)).await })
|
||||
},
|
||||
on_error: |error| {
|
||||
Box::pin(async move {
|
||||
match error {
|
||||
poise::FrameworkError::ArgumentParse { error, .. } => {
|
||||
if let Some(error) = error.downcast_ref::<serenity::RoleParseError>() {
|
||||
error!("Found a RoleParseError: {error:?}");
|
||||
} else {
|
||||
error!("Not a RoleParseError :(");
|
||||
}
|
||||
}
|
||||
poise::FrameworkError::Command { error, ctx, .. } => {
|
||||
error!(
|
||||
"An error occurred executing command {}: {:?}",
|
||||
ctx.command().name,
|
||||
error
|
||||
);
|
||||
poise::builtins::on_error(poise::FrameworkError::new_command(
|
||||
ctx, error,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
poise::FrameworkError::UnknownCommand {
|
||||
ctx,
|
||||
msg,
|
||||
prefix,
|
||||
msg_content,
|
||||
framework,
|
||||
..
|
||||
} => {
|
||||
// TODO: add error handling
|
||||
let _ = custom_commands::execute(
|
||||
ctx,
|
||||
framework.user_data,
|
||||
msg,
|
||||
msg_content.replace(prefix, ""),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
other => {
|
||||
error!("An error occurred.");
|
||||
poise::builtins::on_error(other).await.unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.setup(move |ctx, ready, framework| {
|
||||
Box::pin(async move {
|
||||
info!("[Ready] {} connected", ready.user.display_name());
|
||||
let main_guild = ctx.http.get_guild(settings.guild.into()).await;
|
||||
if let Err(why) = main_guild {
|
||||
error!("Failed to fetch main guild: {why}");
|
||||
}
|
||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||
Ok(Data { settings, db })
|
||||
})
|
||||
})
|
||||
.build()
|
||||
}
|
38
src/settings.rs
Normal file
38
src/settings.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use config::Config;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub token: String,
|
||||
pub channels: ChannelConfig,
|
||||
pub roles: RoleConfig,
|
||||
pub database_url: String,
|
||||
pub prefix: String,
|
||||
pub guild: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RoleConfig {
|
||||
pub moderator: u64,
|
||||
pub trial_mod: u64,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChannelConfig {
|
||||
pub command: u64,
|
||||
pub languages: u64,
|
||||
pub verify: u64,
|
||||
pub log: u64,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn load() -> Result<Self, config::ConfigError> {
|
||||
Config::builder()
|
||||
.add_source(config::File::with_name("config/config.json").required(false))
|
||||
.add_source(config::File::with_name("config/config.toml").required(false))
|
||||
.add_source(config::File::with_name("./config.json").required(false))
|
||||
.add_source(config::File::with_name("./config.toml").required(false))
|
||||
.add_source(config::Environment::default())
|
||||
.build()?
|
||||
.try_deserialize()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue