Compare commits

..

No commits in common. "6d8af9b60a7dd3a8b8754fff422359d81ffff0c3" and "71e0f92708396555a147b2393f54195d2321b349" have entirely different histories.

19 changed files with 459 additions and 1091 deletions

1
.envrc
View File

@ -1 +0,0 @@
use nix

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteTargetsManager">
<targets>
<target name="rust:latest" type="docker" uuid="abc6565f-f912-434a-ae04-68ceee732c3a">
<config>
<option name="targetPlatform">
<TargetPlatform />
</option>
<option name="buildNotPull" value="false" />
<option name="pullImageConfig">
<PullImageConfig>
<option name="tagToPull" value="rust:latest" />
</PullImageConfig>
</option>
</config>
<ContributedStateBase type="RsLanguageRuntime">
<config>
<option name="cargoPath" value="/usr/local/cargo/bin/cargo" />
<option name="cargoVersion" value="1.75.0 (1d8b05cdd 2023-11-20)" />
<option name="rustcPath" value="/usr/local/cargo/bin/rustc" />
<option name="rustcVersion" value="1.75.0 (82e1608df 2023-12-21)" />
</config>
</ContributedStateBase>
</target>
</targets>
</component>
</project>

10
.idea/sqldialects.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/migrations/2023-09-20-230922_extend userinfo to include XP/up.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/migrations/2023-09-26-215927_add custom responses table/up.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/migrations/2023-11-20-091909_add constant values to each rank track for XP generation control/down.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/migrations/2023-11-20-091909_add constant values to each rank track for XP generation control/up.sql" dialect="PostgreSQL" />
<file url="PROJECT" dialect="SQLite" />
</component>
</project>

1309
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "badgey" name = "badgey"
version = "4.0.0" version = "3.1.1"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -20,17 +20,4 @@ manifold = { git = "https://code.orbiter-radio.uk/discord/manifold.git" }
poise = { version = "0.5.*", features = [ "cache" ] } poise = { version = "0.5.*", features = [ "cache" ] }
rand = { version = "0.8.5", features = [ "small_rng" ] } rand = { version = "0.8.5", features = [ "small_rng" ] }
regex = "1.9.5" regex = "1.9.5"
rust-i18n = "3.1.2"
tokio = { version = "1.16.1", features = ["sync", "macros", "rt-multi-thread"] } tokio = { version = "1.16.1", features = ["sync", "macros", "rt-multi-thread"] }
[package.metadata.i18n]
# The available locales for your application, default: ["en"].
available-locales = ["en", "de"]
# The default locale, default: "en".
default-locale = "en"
# Path for your translations YAML file, default: "locales".
# This config for let `cargo i18n` command line tool know where to find your translations.
# You must keep this path same as the one you pass to method `rust_i18n::i18n!`.
load-path = "config/locales"

View File

@ -11,7 +11,7 @@
"channels": { "channels": {
"log": 648260641626390528 "log": 648260641626390528
}, },
"responses_file_path": "txt/responses", "responses_file_path": "txt/responses.txt",
"services": { "services": {
"weather": { "weather": {
"source_uri": "https://api.weatherapi.com/v1", "source_uri": "https://api.weatherapi.com/v1",

View File

@ -1,69 +0,0 @@
_version: 2
commands:
custom_response:
added:
de: "Benutzerdefinierte Antwort erfolgreich hinzugefügt"
en: "Custom response successfully added"
error:
de: "Benutzerdefinierte Antwort NICHT hinzugefügt, ein Fehler ist aufgetreten. Darüber sollten Sie sich wahrscheinlich bei Xyon beschweren. Der Fehler war %{error}"
en: "Custom response NOT added, an error happened. You should probably complain to Xyon about that. The error was %{error}"
marked_deleted:
de: "Benutzerdefinierte Antwort mit der ID %{id} erfolgreich als gelöscht markiert."
en: "Successfully marked custom response with ID %{response_id} as deleted."
marked_undeleted:
de: "Nicht gelöschte benutzerdefinierte Antwort mit der ID: %{response_id}"
en: "Undeleted custom response with ID: %{response_id}"
undelete_error_not_deleted:
de: "Die benutzerdefinierte Antwort mit der ID %{response_id} kann nicht wiederhergestellt werden, da sie nicht von vornherein gelöscht wurde."
en: "Can't undelete custom response with ID %{response_id} because it was not deleted in the first place."
list_user:
de: "Ich habe die folgenden benutzerdefinierten Antworten für %{user} gefunden: %{response_list}"
en: "I found the following custom responses for %{user}: %{response_list}"
ranks:
track_switch:
de: "Ich habe dich auf die %{new_track}-Leiter umgestellt. Du bist jetzt ein %{new_rank}. Ich hoffe, das hat sich gelohnt."
en: "I have switched you onto the %{new_track} rank track. You're now a %{new_rank}. I hope this was worth it."
failure:
de: "Sie befinden sich bereits auf der %(rank_track)-Leiter."
en: "You're already on the %(rank_track) rank track."
rank_freeze:
not_found:
de: "Diesen Rang konnte ich nicht finden um ihn dir zu setzen. Wahrscheinlich musst Du die Karriere-Leiter wechseln, oder es existiert einfach nicht."
en: "I couldn't find that rank as a rank I can give you. You might need to switch tracks, or it might just not exist."
success:
de: "Du bist nun für immer %{rank}. Viel Glück Harry!"
en: "Okay. I've frozen your rank at %{rank}. Enjoy!"
rank_unfreeze:
success:
de: "Dein Rang wurde enteist. Janeway wird das nicht erfreuen."
en: "Okay. I've unfrozen your rank. Good luck out there."
failure:
de: "Dein Rang war nicht eingefroren."
en: "Negative, your rank wasn't frozen."
rank:
current_rank:
de: "Du bist derzeit %{current_rank}. Du brauchst %{next_level_xp} mehr XP, um das nächste Level zu erreichen"
en: "You're currently %{current_rank}. You need %{next_level_xp} more XP to get to the next level"
max_rank:
de: "%{response}. Du bist bereits ganz oben angekommen! - Du kannst nicht weiter befördert werden!"
en: "%{response}. There are no more ranks for you - you can't be promoted any further!"
and_rank:
de: "%{response} und Rang!"
en: "%{response} and rank!"
and_rank_different_level:
de: "%{response} und du brauchst %{next_rank_xp} mehr, um den nächsten Rang zu erreichen. Viel Glück!"
en: "%{response} and you need %{next_rank_xp} more to get to the next rank. Good luck!"
models:
xp:
cooldown:
de: "Du hast eine Abklingzeit. Versuchen Sie es in %{timeuntil} Sekunden erneut."
en: "You are on cooldown. Try again in %{timeuntil} seconds."
unranked:
de: ohne Rangliste
en: unranked
misc:
version_string:
de: "Badgey Bot Version %{ver} erstellt um %{time} aus der Revision %{rev}"
en: "Badgey Bot version %{ver} built at %{time} from revision %{rev}"

View File

@ -1,8 +0,0 @@
{ pkgs ? import <nixpkgs> {}}:
pkgs.mkShell {
buildInputs = with pkgs; [
openssl
postgresql
pkg-config
];
}

View File

@ -11,8 +11,8 @@ async fn add_custom_response(ctx: ManifoldContext<'_>, target: serenity::User, t
let db = &ctx.data().database; let db = &ctx.data().database;
match CustomResponseInserter::new(trigger, response, target, ctx.author().clone()).insert(db) { match CustomResponseInserter::new(trigger, response, target, ctx.author().clone()).insert(db) {
Ok(_) => ctx.reply(t!("commands.custom_response.added")).await?, Ok(_) => ctx.reply("Custom response successfully added").await?,
Err(e) => ctx.reply(t!("commands.custom_response.added.error", error = e)).await?, Err(e) => ctx.reply(format!("Custom response NOT added, an error happened. You should probably complain to Xyon about that. The error was {}", e)).await?,
}; };
Ok(()) Ok(())
@ -27,7 +27,7 @@ async fn remove_custom_response(ctx: ManifoldContext<'_>, id: i32) -> ManifoldRe
response.deleted = Some(true); response.deleted = Some(true);
response.insert(db)?; response.insert(db)?;
ctx.reply(t!("commands.custom_response.marked_deleted", response_id=id)).await?; ctx.reply(format!("Successfully marked custom response with ID {} as deleted.", id)).await?;
Ok(()) Ok(())
} }
@ -41,9 +41,9 @@ async fn undelete_custom_response(ctx: ManifoldContext<'_>, id: i32) -> Manifold
if let Some(_) = response.deleted { if let Some(_) = response.deleted {
response.deleted = None; response.deleted = None;
response.insert(db)?; response.insert(db)?;
ctx.reply(t!("commands.custom_response.marked_undeleted", response_id=id)).await?; ctx.reply(format!("Undeleted custom response with ID: {}", &id)).await?;
} else { } else {
ctx.reply(t!("commands.custom_response.undelete_error_not_deleted", response_id=id)).await?; ctx.reply(format!("Can't undelete custom response with ID {} because it was not deleted in the first place.", &id)).await?;
} }
Ok(()) Ok(())
@ -63,7 +63,7 @@ async fn list_custom_responses_for_user(ctx: ManifoldContext<'_>, target: sereni
answer.push_str("\nNone found"); answer.push_str("\nNone found");
} }
ctx.reply(t!("commands.custom_response.list_user", user = &target.mention(), response_list = answer)).await?; ctx.reply(format!("I found the following custom responses for {}: {}", &target.mention(), answer)).await?;
Ok(()) Ok(())
} }

View File

@ -4,13 +4,11 @@ use poise::Command;
pub mod custom_responses; pub mod custom_responses;
pub mod ranks; pub mod ranks;
pub mod moderation;
pub fn collect_commands() -> Vec<Command<ManifoldData, ManifoldError>> { pub fn collect_commands() -> Vec<Command<ManifoldData, ManifoldError>> {
commands().into_iter() commands().into_iter()
.chain(custom_responses::commands()) .chain(custom_responses::commands())
.chain(ranks::commands()) .chain(ranks::commands())
.chain(moderation::commands())
.collect() .collect()
} }

View File

@ -1,27 +0,0 @@
use manifold::error::{ManifoldError, ManifoldResult};
use manifold::{ManifoldContext, ManifoldData};
use poise::serenity_prelude as serenity;
#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
async fn void_user(_ctx: ManifoldContext<'_>, _target: serenity::User) -> ManifoldResult<()> {
Ok(())
}
#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
async fn airlock_user(_ctx: ManifoldContext<'_>, _target: serenity::User) -> ManifoldResult<()> {
Ok(())
}
#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
async fn unvoid_user(_ctx: ManifoldContext<'_>, _target: serenity::User) -> ManifoldResult<()> {
Ok(())
}
#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
async fn record_chronicle(_ctx: ManifoldContext<'_>, _target: serenity::User) -> ManifoldResult<()> {
Ok(())
}
pub fn commands() -> [poise::Command<ManifoldData, ManifoldError>; 4] {
[void_user(), unvoid_user(), airlock_user(), record_chronicle()]
}

View File

@ -26,11 +26,11 @@ async fn switch_rank_track(ctx: ManifoldContext<'_>, #[description = "Track to s
xp.rank_track = parsed_track.track_id; xp.rank_track = parsed_track.track_id;
ctx.reply(t!("commands.ranks.track_switch", new_track = parsed_track.track_name, new_rank = new_level_rank.rank_name)).await?; ctx.reply(format!("I have switched you onto the {} rank track. You're now a {}. I hope this was worth it.", parsed_track.track_name, new_level_rank.rank_name)).await?;
xp.rank_track_last_changed = Some(chrono::Utc::now().timestamp()); xp.rank_track_last_changed = Some(chrono::Utc::now().timestamp());
xp.insert(db)?; xp.insert(db)?;
} else { } else {
ctx.reply(t!("commands.ranks.track_switch.failure", rank_track = parsed_track.track_name)).await?; ctx.reply(format!("You're already on the {} rank track.", parsed_track.track_name)).await?;
} }
Ok(()) Ok(())
@ -51,7 +51,7 @@ async fn freeze_rank(ctx: ManifoldContext<'_>, #[rest] #[description = "Rank to
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
error!("Rank lookup error: {}", e); error!("Rank lookup error: {}", e);
ctx.reply(t!("commands.ranks.rank_freeze.not_found")).await?; ctx.reply(format!("I couldn't find that rank as a rank I can give you. You might need to switch tracks, or it might just not exist.")).await?;
Err(e)? Err(e)?
} }
}; };
@ -67,7 +67,7 @@ async fn freeze_rank(ctx: ManifoldContext<'_>, #[rest] #[description = "Rank to
if let Some(mut member) = ctx.author_member().await { if let Some(mut member) = ctx.author_member().await {
member.to_mut().add_role(ctx, frozen_rank.role_id as u64).await?; member.to_mut().add_role(ctx, frozen_rank.role_id as u64).await?;
ctx.reply(t!("commands.ranks.rank_freeze.success", rank = frozen_rank.rank_name)).await?; ctx.reply(format!("Okay. I've frozen your rank at {}. Enjoy!", frozen_rank.rank_name)).await?;
} }
} else { } else {
@ -78,9 +78,9 @@ async fn freeze_rank(ctx: ManifoldContext<'_>, #[rest] #[description = "Rank to
} }
xp.freeze_rank = None; xp.freeze_rank = None;
ctx.reply(t!("commands.ranks.rank_unfreeze.success")).await?; ctx.reply(format!("Okay. I've unfrozen your rank. Good luck out there.")).await?;
} else { } else {
ctx.reply(t!("commands.ranks.rank_unfreeze.failure")).await?; ctx.reply(format!("Negative, your rank wasn't frozen.")).await?;
} }
} }
@ -105,13 +105,13 @@ async fn rank(ctx: ManifoldContext<'_>) -> ManifoldResult<()> {
let next_level_xp = xp.get_xp_to_next_level(&db); let next_level_xp = xp.get_xp_to_next_level(&db);
let next_rank_xp = xp.get_xp_to_next_rank(&db).unwrap_or(0); let next_rank_xp = xp.get_xp_to_next_rank(&db).unwrap_or(0);
let mut response = t!("commands.ranks.rank.current_rank", current_rank = RoleId::from(current_rank.role_id as u64).mention(), next_level_xp = next_level_xp); let mut response = format!("You're currently {}. You need {} more XP to get to the next level", RoleId::from(current_rank.role_id as u64).mention(), next_level_xp);
if next_rank_xp == 0 { if next_rank_xp == 0 {
response = t!("commands.ranks.rank.max_rank", reponse = response); response = format!("{}. There are no more ranks for you - you can't be promoted any further!", response);
} else if next_rank_xp == next_level_xp { } else if next_rank_xp == next_level_xp {
response = t!("commands.ranks.rank.and_rank", response = response); response = format!("{} and rank!", response);
} else { } else {
response = t!("commands.ranks.rank.and_rank_different_level", response = response, next_rank_xp = next_rank_xp); response = format!("{} and you need {} more to rank up. Good luck!", response, next_rank_xp);
} }
ctx.reply(response).await?; ctx.reply(response).await?;

View File

@ -1,6 +1,6 @@
use built::chrono; use built::chrono;
use manifold::error::{ManifoldError, ManifoldResult}; use manifold::error::{ManifoldError, ManifoldResult};
use manifold::events::EventHandler; use manifold::events::{Handler, EventHandler};
use manifold::ManifoldData; use manifold::ManifoldData;
use poise::{async_trait, FrameworkContext, Event}; use poise::{async_trait, FrameworkContext, Event};
use poise::serenity_prelude::{Context, Mentionable, Message, RoleId}; use poise::serenity_prelude::{Context, Mentionable, Message, RoleId};
@ -18,6 +18,8 @@ pub struct BadgeyHandler {
#[async_trait] #[async_trait]
impl EventHandler for BadgeyHandler { impl EventHandler for BadgeyHandler {
async fn listen(ctx: &Context, framework_ctx: FrameworkContext<'_, ManifoldData, ManifoldError>, event: &Event<'_>) -> ManifoldResult<()> { async fn listen(ctx: &Context, framework_ctx: FrameworkContext<'_, ManifoldData, ManifoldError>, event: &Event<'_>) -> ManifoldResult<()> {
Handler::listen(ctx, framework_ctx, event).await?;
match event { match event {
Event::Message { new_message } => BadgeyHandler::message(&ctx, &framework_ctx, new_message).await, Event::Message { new_message } => BadgeyHandler::message(&ctx, &framework_ctx, new_message).await,
_ => Ok(()) _ => Ok(())

View File

@ -14,10 +14,8 @@ pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
#[tokio::main] #[tokio::main]
pub async fn run(arguments: ArgMatches) { pub async fn run(arguments: ArgMatches) {
rust_i18n::set_locale(arguments.get_one::<String>("locale").unwrap());
let git_info: String = built_info::GIT_VERSION.unwrap_or("unknown").to_string(); let git_info: String = built_info::GIT_VERSION.unwrap_or("unknown").to_string();
let version_string = t!("misc.version_string", ver=built_info::PKG_VERSION, time=built_info::BUILT_TIME_UTC, rev=git_info).to_string(); let version_string = format!("Badgey Bot version {ver} built at {time} from revision {rev}", ver=built_info::PKG_VERSION, time=built_info::BUILT_TIME_UTC, rev=git_info);
let client = match manifold::prepare_client::<BadgeyHandler>(arguments, GatewayIntents::all(), commands::collect_commands(), version_string, MIGRATIONS).await { let client = match manifold::prepare_client::<BadgeyHandler>(arguments, GatewayIntents::all(), commands::collect_commands(), version_string, MIGRATIONS).await {
Ok(c) => c, Ok(c) => c,

View File

@ -1,7 +1,7 @@
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use built::chrono::{DateTime, Utc}; use built::chrono::{NaiveDateTime, Utc};
use diesel::prelude::*; use diesel::prelude::*;
use diesel::insert_into; use diesel::{insert_into, sql_function};
use regex::Regex; use regex::Regex;
use poise::serenity_prelude as serenity; use poise::serenity_prelude as serenity;
@ -10,7 +10,7 @@ use manifold::error::{ManifoldError, ManifoldResult};
use manifold::models::user::UserInfo; use manifold::models::user::UserInfo;
use manifold::schema::userinfo; use manifold::schema::userinfo;
define_sql_function!(fn random() -> Text); sql_function!(fn random() -> Text);
use crate::badgey::schema::*; use crate::badgey::schema::*;
@ -112,6 +112,6 @@ impl CustomResponse {
impl Display for CustomResponse { impl Display for CustomResponse {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "ID: {}, trigger: {}, response: {}, added by: {}, added on: {}, deleted: {}", self.id, self.trigger, self.response, serenity::UserId::from(self.added_by as u64), DateTime::from_timestamp(self.added_on, 0).unwrap(), self.deleted.unwrap_or(false)) write!(f, "ID: {}, trigger: {}, response: {}, added by: {}, added on: {}, deleted: {}", self.id, self.trigger, self.response, serenity::UserId::from(self.added_by as u64), NaiveDateTime::from_timestamp_opt(self.added_on, 0).unwrap(), self.deleted.unwrap_or(false))
} }
} }

View File

@ -107,7 +107,7 @@ impl Xp {
if let Some(v) = value { if let Some(v) = value {
if v > &(chrono::Utc::now().timestamp() - timeout) { if v > &(chrono::Utc::now().timestamp() - timeout) {
let timeuntil = v - (chrono::Utc::now().timestamp() - timeout); let timeuntil = v - (chrono::Utc::now().timestamp() - timeout);
Err(t!("models.xp.cooldown", timeuntil = timeuntil))?; Err(format!("You are on cooldown. Try again in {} seconds.", timeuntil))?;
} }
} }
@ -121,7 +121,7 @@ impl Rank {
role_id: 0, role_id: 0,
required_level: 0, required_level: 0,
rank_track: 0, rank_track: 0,
rank_name: t!("models.xp.unranked").to_string(), rank_name: "unranked".to_string(),
} }
} }

View File

@ -1,5 +1,4 @@
#[macro_use] extern crate log; #[macro_use] extern crate log;
#[macro_use] extern crate rust_i18n;
pub mod badgey; pub mod badgey;
@ -10,8 +9,6 @@ pub mod built_info {
include!(concat!(env!("OUT_DIR"), "/built.rs")); include!(concat!(env!("OUT_DIR"), "/built.rs"));
} }
i18n!("config/locales");
fn main() { fn main() {
env_logger::init(); env_logger::init();
@ -36,10 +33,6 @@ fn main() {
.value_name("ENV") .value_name("ENV")
.default_value("Production") .default_value("Production")
.help("Bot environment to use. Determines which config settings are read. Defaults to Production.")) .help("Bot environment to use. Determines which config settings are read. Defaults to Production."))
.arg(Arg::new("locale")
.short('l')
.default_value("en")
.help("Bot locale to run in, en and de currently supported"))
.arg(Arg::new("make-config") .arg(Arg::new("make-config")
.short('M') .short('M')
.long("make-config") .long("make-config")

View File

@ -1,20 +0,0 @@
[bot startup]
**B A D G E Y** Programminitialisierung. Laden von Zielen aus dem Speicher.
Badgey lebt wieder! Nichts kann Badgey erlegen! Badgey ist unzerstörbar!
Retikulierende Splines....
[help footer]
Badgey möchte, dass du weißt, dass er nichts als Liebe für dich empfindet. Noch.
Badgeys geduld ist grenzenlos! Er wünscht Ihnen nichts als viel Glück auf Ihrer Entdeckungsreise.
Badgey möchte wissen, warum du nach seinen Daten fragst.
Badgey hat diese Anfrage über seine innersten Strukturen aufgezeichnet. Badgey wird sich erinnern.
Badgey weiß, wo du schläfst, weißt du. Er wollte nur, dass du dich daran erinnerst.
[weather card footer]
Badgey Weather wurde Ihnen in Zusammenarbeit mit AstroGlide für den reibungslosesten Betrieb zur Verfügung gestellt, den Sie jemals durchführen werden.This weather broadcast and all which preceded it are entirely fictional. Badgey takes no responsibility for any harm caused by believing otherwise.
Badgey Weather, stolz angetrieben von einem Rasenmähermotor und einem 93-jährigen Veteranen, der einfach nicht aufgeben will.
Der Badgey Weather Service arbeitet mit Ihren Spenden und den Tränen der Faschisten.
Wetter von Badgey. Nicht die Berichterstattung. Das tatsächliche Wetter. Ich kann tun, was ich will.3
Dieser Bericht wird sich innerhalb von 5 sekunden selbst zerstören.
Alle im obigen Wetterbericht enthaltenen Informationen sind nicht für den menschlichen Verzehr geeignet. Bei Kontakt mit den Augen konsultieren Sie bitte einen Arzt.
Ich glaube, Ihr Warpkern ist undicht.
Badgey versuchte, diesen Wetterbericht von der Sternenflotte absegnen zu lassen, aber die Sternenflotte leugnet jede Kenntnis von der Existenz von Badgey, was Badgey traurig machte.
[end]