Compare commits

...

3 Commits

Author SHA1 Message Date
Xyon 68731984c1
Add i18n effort to permit bot strings to be translated 2024-08-26 19:57:34 +01:00
Xyon 9d1c124ba2
Add existing changes from worktree with build fix 2024-08-26 13:46:05 +01:00
Xyon 72cfe7b17b
Nixos nonsense 2024-01-15 07:59:53 +00:00
19 changed files with 1099 additions and 469 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use nix

28
.idea/remote-targets.xml Normal file
View File

@ -0,0 +1,28 @@
<?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>

View File

@ -1,10 +0,0 @@
<?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>

1326
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,9 +15,22 @@ diesel = { version = "2.1.0", features = ["postgres", "r2d2", "chrono"] }
diesel_migrations = "2.1.0" diesel_migrations = "2.1.0"
env_logger = "0.10.0" env_logger = "0.10.0"
log = "0.4.20" log = "0.4.20"
manifold = { git = "https://code.orbiter-radio.uk/discord/manifold.git" } # manifold = { git = "https://code.orbiter-radio.uk/discord/manifold.git" }
# manifold = { path = "/home/xyon/Workspace/manifold/" } manifold = { path = "/home/xyon/Workspace/manifold/" }
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.txt", "responses_file_path": "txt/responses",
"services": { "services": {
"weather": { "weather": {
"source_uri": "https://api.weatherapi.com/v1", "source_uri": "https://api.weatherapi.com/v1",

69
config/locales/badgey.yml Normal file
View File

@ -0,0 +1,69 @@
_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}-Rangleiste 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)-Rangleiste."
en: "You're already on the %(rank_track) rank track."
rank_freeze:
not_found:
de: ""
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: ""
en: "Okay. I've frozen your rank at %{rank}. Enjoy!"
rank_unfreeze:
success:
de: ""
en: Okay. I've unfrozen your rank. Good luck out there.
failure:
de: ""
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}. Es gibt keine Ränge mehr für dich - 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}"

8
shell.nix Normal file
View File

@ -0,0 +1,8 @@
{ 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("Custom response successfully added").await?, Ok(_) => ctx.reply(t!("commands.custom_response.added")).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?, Err(e) => ctx.reply(t!("commands.custom_response.added.error", error = 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(format!("Successfully marked custom response with ID {} as deleted.", id)).await?; ctx.reply(t!("commands.custom_response.marked_deleted", response_id=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(format!("Undeleted custom response with ID: {}", &id)).await?; ctx.reply(t!("commands.custom_response.marked_undeleted", response_id=id)).await?;
} else { } else {
ctx.reply(format!("Can't undelete custom response with ID {} because it was not deleted in the first place.", &id)).await?; ctx.reply(t!("commands.custom_response.undelete_error_not_deleted", response_id=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(format!("I found the following custom responses for {}: {}", &target.mention(), answer)).await?; ctx.reply(t!("commands.custom_response.list_user", user = &target.mention(), response_list = answer)).await?;
Ok(()) Ok(())
} }

View File

@ -4,6 +4,7 @@ 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()

View File

@ -0,0 +1,27 @@
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(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?; ctx.reply(t!("commands.ranks.track_switch", new_track = parsed_track.track_name, new_rank = 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(format!("You're already on the {} rank track.", parsed_track.track_name)).await?; ctx.reply(t!("commands.ranks.track_switch.failure", 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(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?; ctx.reply(t!("commands.ranks.rank_freeze.not_found")).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(format!("Okay. I've frozen your rank at {}. Enjoy!", frozen_rank.rank_name)).await?; ctx.reply(t!("commands.ranks.rank_freeze.success", rank = 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(format!("Okay. I've unfrozen your rank. Good luck out there.")).await?; ctx.reply(t!("commands.ranks.rank_unfreeze.success")).await?;
} else { } else {
ctx.reply(format!("Negative, your rank wasn't frozen.")).await?; ctx.reply(t!("commands.ranks.rank_unfreeze.failure")).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 = 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); 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);
if next_rank_xp == 0 { if next_rank_xp == 0 {
response = format!("{}. There are no more ranks for you - you can't be promoted any further!", response); response = t!("commands.ranks.rank.max_rank", reponse = response);
} else if next_rank_xp == next_level_xp { } else if next_rank_xp == next_level_xp {
response = format!("{} and rank!", response); response = t!("commands.ranks.rank.and_rank", response = response);
} else { } else {
response = format!("{} and you need {} more to rank up. Good luck!", response, next_rank_xp); response = t!("commands.ranks.rank.and_rank_different_level", response = response, next_rank_xp = 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::{Handler, EventHandler}; use manifold::events::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,8 +18,6 @@ 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,8 +14,10 @@ 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 = 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 version_string = t!("misc.version_string", ver=built_info::PKG_VERSION, time=built_info::BUILT_TIME_UTC, rev=git_info).to_string();
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::{NaiveDateTime, Utc}; use built::chrono::{DateTime, Utc};
use diesel::prelude::*; use diesel::prelude::*;
use diesel::{insert_into, sql_function}; use diesel::insert_into;
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;
sql_function!(fn random() -> Text); define_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), NaiveDateTime::from_timestamp_opt(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), DateTime::from_timestamp(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(format!("You are on cooldown. Try again in {} seconds.", timeuntil))?; Err(t!("models.xp.cooldown", timeuntil = 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: "unranked".to_string(), rank_name: t!("models.xp.unranked").to_string(),
} }
} }

View File

@ -1,4 +1,5 @@
#[macro_use] extern crate log; #[macro_use] extern crate log;
#[macro_use] extern crate rust_i18n;
pub mod badgey; pub mod badgey;
@ -9,6 +10,8 @@ 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();
@ -33,6 +36,10 @@ 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")

20
txt/responses.de.txt Normal file
View File

@ -0,0 +1,20 @@
[bot startup]
**B A D G E Y** Programminitialisierung. Laden von Zielen aus dem Speicher.
Badgey lebt wieder! Nichts kann Badgey töten! Badgey ist unzerstörbar!
Retikulierende Splines....
[help footer]
Badgey möchte, dass du weißt, dass er nichts als Liebe für dich empfindet. Heute.
Badgeys Geduld ist grenzenlos! Er wünscht Ihnen nichts als viel Glück auf Ihrer Entdeckungsreise.
Badgey möchte wissen, warum du nach seinen Interna fragst.
Badgey hat diese Bitte um Auskunft ü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 Ungeimpften.
Wetter von Badgey. Nicht die Berichterstattung. Das tatsächliche Wetter. Ich kann tun, was ich will.3
Dieser Bericht zerstört sich innerhalb von fünf Sekunden selbst.
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 oder Nichtexistenz von Badgey, was Badgey traurig machte.
[end]