10 Commits
Author SHA1 Message Date
Elias Wendland f869d92012 Add /git and /speedy_pc command, release 0.3.0
Run cargo test / Run tests (push) Successful in 5m6s
Build Docker Package / build (push) Successful in 7m25s
Build Docker Package / build (release) Successful in 6m46s
2026-07-23 15:08:34 +02:00
Elias Wendland dc014945fd fix: Fix version command
Run cargo test / Run tests (push) Successful in 4m29s
Build Docker Package / build (push) Successful in 10m27s
Build Docker Package / build (release) Successful in 10m43s
2026-07-23 12:37:48 +02:00
Elias Wendland 09ec866d9a Change some file names 2026-07-23 12:24:36 +02:00
Elias Wendland bada6696a2 Fix missing libquadmath.so issue
Run cargo test / Run tests (push) Successful in 5m18s
Build Docker Package / build (push) Successful in 14m3s
Build Docker Package / build (release) Successful in 11m0s
2026-07-20 18:33:49 +02:00
Elias Wendland b1116846aa add some new commands
Run cargo test / Run tests (push) Successful in 4m7s
Build Docker Package / build (push) Successful in 6m47s
Build Docker Package / build (release) Successful in 7m16s
2026-07-05 19:49:00 +02:00
Elias Wendland afa64fd55b improve calculator, improve logging, fix auto role
Run cargo test / Run tests (push) Successful in 3m25s
Build Docker Package / build (push) Successful in 6m6s
2026-07-05 12:13:41 +02:00
Elias Wendland 24205326bf Add experimental f128 and f256 modes for /calc
Run cargo test / Run tests (push) Successful in 4m1s
Build Docker Package / build (push) Successful in 6m7s
Build Docker Package / build (release) Successful in 7m20s
2026-07-05 01:06:49 +02:00
Elias Wendland b3ada54689 Release 0.1.1
Run cargo test / Run tests (push) Successful in 3m36s
Build Docker Package / build (push) Successful in 6m26s
Build Docker Package / build (release) Successful in 7m7s
Fix a bug, add better error handling
2026-07-05 00:45:45 +02:00
ewenlau 2a2d359348 Fix typo
Build Docker Package / build (push) Successful in 7m51s
Run cargo test / Run tests (push) Successful in 4m40s
2026-07-05 00:18:37 +02:00
ewenlau 6719ebfda6 Fix typo
Build Docker Package / build (push) Successful in 6m37s
Run cargo test / Run tests (push) Successful in 3m30s
2026-07-05 00:18:19 +02:00
22 changed files with 1074 additions and 87 deletions

No files matched your search

+12 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tg-dev-srv-bot"
version = "0.1.0"
version = "0.3.0"
edition = "2024"
license = "GPL-3.0-or-later"
description = "Bot for the TeenGovernment Development Server"
@@ -10,13 +10,24 @@ repository = "https://git.ewenlau.net/ewenlau/tg-dev-srv-bot"
homepage = "https://git.ewenlau.net/ewenlau/tg-dev-srv-bot"
[dependencies]
chrono = "0.4.45"
dotenv = "0.15.0"
exmex = "0.21.0"
f128 = "0.2.9"
f256 = "0.11.2"
num-bigint = "0.5.1"
num-traits = "0.2.19"
ollama-rs = { version = "0.3.5", features = ["stream"] }
poise = "0.6.2"
qapi = { version = "0.15.0", features = ["qmp", "qga"] }
reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] }
regex = "1.12.4"
rug = "1.30.0"
serde = { version = "1", features = ["derive"] }
serenity = "0.12.5"
sqlx = { version = "0.9.0", features = ["postgres", "runtime-tokio", "macros"] }
tokio = { version = "1.52.3", features = ["full"] }
tokio-stream = "0.1.18"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+1 -1
View File
@@ -4,6 +4,6 @@ COPY . .
RUN SQLX_OFFLINE=true cargo install --path .
FROM debian:trixie
RUN apt-get update && apt-get install -y ca-certificates libgmp-dev libmpfr-dev libmpc-dev && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y ca-certificates libgmp-dev libmpfr-dev libmpc-dev libquadmath0 && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/cargo/bin/tg-dev-srv-bot /usr/local/bin/tg-dev-srv-bot
CMD ["tg-dev-srv-bot"]
+1 -1
View File
@@ -6,4 +6,4 @@ Not really meant for any other server, but feel free to use it if you'd like.
The bot is really made to be used with Docker, but you can run it locally.
An image is available at git.ewenlau.net/ewenlau/tg-dev-srv-bot:release for the stable version and git.ewenlau.net/ewenlau/tg-dev-srv-bot:dev for the development (or testing) version.
An image is available at git.ewenlau.net/ewenlau/tg-dev-srv-bot:latest for the stable version and git.ewenlau.net/ewenlau/tg-dev-srv-bot:dev for the development (or testing) version.
+35
View File
@@ -0,0 +1,35 @@
use std::{env, fs, path::Path, process::Command};
fn command_output(command: &str, args: &[&str]) -> String {
Command::new(command)
.args(args)
.output()
.ok()
.filter(|output| output.status.success())
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned())
.filter(|output| !output.is_empty())
.unwrap_or_else(|| "unknown".to_owned())
}
fn main() {
let out_dir = env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo");
let version = env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "unknown".to_owned());
let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned());
let rustc_version = command_output(&rustc, &["--version"]);
let target = env::var("TARGET").unwrap_or_else(|_| "unknown".to_owned());
println!("cargo:rustc-env=TG_BOT_RUSTC_VERSION={rustc_version}");
println!("cargo:rustc-env=TG_BOT_TARGET={target}");
let version_text = format!(
"{}\nbuild-time: {}\ncommit: {}\ntarget: {}\nrustc: {}",
version,
command_output("date", &["-u", "+%Y-%m-%d %H:%M:%S UTC"]),
command_output("git", &["rev-parse", "--short", "HEAD"]),
target,
rustc_version,
);
fs::write(Path::new(&out_dir).join("version.txt"), version_text)
.expect("failed to write generated version information");
}
+1 -1
View File
@@ -21,7 +21,7 @@ services:
context: .
dockerfile: Dockerfile
# OR pull from registry
# image: git.ewenlau.net/ewenlau/tg-dev-srv-bot:release
# image: git.ewenlau.net/ewenlau/tg-dev-srv-bot:latest
# image: git.ewenlau.net/ewenlau/tg-dev-srv-bot:dev
container_name: tg-dev-srv-bot
restart: always
@@ -0,0 +1 @@
ALTER TABLE auto_roles ADD CONSTRAINT auto_roles_guild_id_key UNIQUE (guild_id);
+295
View File
@@ -0,0 +1,295 @@
use crate::{Context, Error};
use tracing::{debug, error, trace};
use ollama_rs::Ollama;
use std::env;
use std::time::Duration;
use ollama_rs::models::create::CreateModelRequest;
use ollama_rs::generation::completion::request::GenerationRequest;
use chrono::{DateTime, Utc};
use tokio_stream::StreamExt;
use poise::CreateReply;
/// Prompt the super advanced EwiAI
#[poise::command(slash_command, prefix_command)]
pub async fn prompt(
ctx: Context<'_>,
#[description = "The prompt to send to EwiAI"]
prompt: String,
#[description = "Include recent messages (max 10)"]
include_messages: Option<u8>,
) -> Result<(), Error> {
debug!("{} has requested to prompt EwiAI with '{}'", ctx.author().name, prompt);
let host = match env::var("TG_BOT_OLLAMA_HOST") {
Ok(h) => h,
Err(_) => {
ctx.say("Error: Expected an ollama url in the environment (`TG_BOT_OLLAMA_HOST`).").await?;
return Ok(());
}
};
let formatted_host = if !host.starts_with("http://") && !host.starts_with("https://") {
format!("http://{}", host)
} else {
host
};
let ollama = Ollama::builder()
.host(&formatted_host)
.port(11434)
.build();
let model_list = match ollama.list_local_models().await {
Ok(m) => m,
Err(e) => {
ctx.say(format!("Error: Failed to connect to Ollama: {}", e)).await?;
return Ok(());
}
};
let mut needs_create = true;
let yesterday = Utc::now() - chrono::Duration::days(1);
for model in &model_list {
if model.name.starts_with("EwiAI") {
if let Ok(modified) = DateTime::parse_from_rfc3339(&model.modified_at) {
if modified.with_timezone(&Utc) > yesterday {
needs_create = false;
}
}
}
}
if needs_create {
let system_prompt = system_prompt();
let _ = ctx.say("EwiAI model is missing or out of date. Creating/updating model (this may take a bit)...").await?;
if let Err(e) = ollama.create_model(CreateModelRequest::new("EwiAI".into())
.system(system_prompt.into())
.from_model("gemma4:e2b-it-qat".into())).await {
ctx.say(format!("Error creating model: {}", e)).await?;
return Ok(());
}
}
let mut final_prompt = String::new();
if let Some(mut limit) = include_messages {
if limit > 10 {
limit = 10;
}
match ctx.channel_id().messages(ctx.http(), poise::serenity_prelude::GetMessages::new().limit(limit)).await {
Ok(msgs) => {
final_prompt.push_str("Recent channel messages:\n");
for msg in msgs.iter().rev() {
let mut content = msg.content.clone();
if content.len() > 100 {
content.truncate(100);
content.push_str("...");
}
final_prompt.push_str(&format!("{} (ID: {}): {}\n", msg.author.name, msg.author.id, content));
}
final_prompt.push_str("\n");
}
Err(e) => {
error!("Failed to get messages: {:?}", e);
}
}
}
final_prompt.push_str(&format!("The user talking to you is {} (ID: {}).\nThat user is telling you the following: {}", ctx.author().name, ctx.author().id, prompt));
trace!("Final prompt: {}", final_prompt);
let reply = ctx.say("Thinking...\n-# The hardware this thing runs is really slow, expect a long wait").await?;
let mut stream = match ollama.generate_stream(GenerationRequest::new("EwiAI".into(), final_prompt).system(system_prompt())).await {
Ok(s) => s,
Err(e) => {
reply.edit(ctx, CreateReply::default().content(format!("Error during generation: {}", e))).await?;
return Ok(());
}
};
let mut response_text = String::new();
let mut last_update = std::time::Instant::now();
let mut final_response = None;
while let Some(res) = stream.next().await {
match res {
Ok(chunks) => {
for chunk in chunks {
response_text.push_str(&chunk.response);
if chunk.done {
final_response = Some(chunk);
}
}
if last_update.elapsed() >= Duration::from_secs(1) && !response_text.is_empty() {
let _ = reply.edit(ctx, CreateReply::default().content(&response_text)).await;
last_update = std::time::Instant::now();
}
}
Err(e) => {
let _ = reply.edit(ctx, CreateReply::default().content(format!("{} [Stream Error: {}]", response_text, e))).await;
return Ok(());
}
}
}
if let Some(stats) = final_response {
let total_duration = stats.total_duration.unwrap_or(0) as f64 / 1_000_000_000.0;
let eval_count = stats.eval_count.unwrap_or(0);
let eval_duration = stats.eval_duration.unwrap_or(0) as f64 / 1_000_000_000.0;
let tokens_per_sec = if eval_duration > 0.0 { eval_count as f64 / eval_duration } else { 0.0 };
let stats_text = format!(
"\n\n*Generated in {:.2}s ({:.2} tok/s)*",
total_duration, tokens_per_sec
);
response_text.push_str(&stats_text);
}
let _ = reply.edit(ctx, CreateReply::default().content(&response_text)).await;
Ok(())
}
/// Ask EwiAI to answer to recent messages
#[poise::command(slash_command, prefix_command)]
pub async fn answer(
ctx: Context<'_>,
#[description = "How many messages to include (max 10)"]
messages_count: Option<u8>,
) -> Result<(), Error> {
debug!("{} has requested EwiAI to answer", ctx.author().name);
let host = match env::var("TG_BOT_OLLAMA_HOST") {
Ok(h) => h,
Err(_) => {
ctx.say("Error: Expected an ollama url in the environment (`TG_BOT_OLLAMA_HOST`).").await?;
return Ok(());
}
};
let formatted_host = if !host.starts_with("http://") && !host.starts_with("https://") {
format!("http://{}", host)
} else {
host
};
let ollama = Ollama::builder()
.host(&formatted_host)
.port(11434)
.build();
let model_list = match ollama.list_local_models().await {
Ok(m) => m,
Err(e) => {
ctx.say(format!("Error: Failed to connect to Ollama: {}", e)).await?;
return Ok(());
}
};
let mut needs_create = true;
let yesterday = Utc::now() - chrono::Duration::days(1);
for model in &model_list {
if model.name.starts_with("EwiAI") {
if let Ok(modified) = DateTime::parse_from_rfc3339(&model.modified_at) {
if modified.with_timezone(&Utc) > yesterday {
needs_create = false;
}
}
}
}
if needs_create {
let system_prompt = system_prompt();
let _ = ctx.say("EwiAI model is missing or out of date. Creating/updating model (this may take a bit)...").await?;
if let Err(e) = ollama.create_model(CreateModelRequest::new("EwiAI".into())
.system(system_prompt.into())
.from_model("gemma4:e2b-it-qat".into())).await {
ctx.say(format!("Error creating model: {}", e)).await?;
return Ok(());
}
}
let mut final_prompt = String::new();
let mut limit = messages_count.unwrap_or(5);
if limit > 10 {
limit = 10;
}
match ctx.channel_id().messages(ctx.http(), poise::serenity_prelude::GetMessages::new().limit(limit)).await {
Ok(msgs) => {
final_prompt.push_str("Recent channel messages:\n");
for msg in msgs.iter().rev() {
let mut content = msg.content.clone();
if content.len() > 100 {
content.truncate(100);
content.push_str("...");
}
final_prompt.push_str(&format!("{} (ID: {}): {}\n", msg.author.name, msg.author.id, content));
}
final_prompt.push_str("\n");
}
Err(e) => {
error!("Failed to get messages: {:?}", e);
}
}
final_prompt.push_str(&format!("Please respond to the messages above."));
trace!("Final prompt: {}", final_prompt);
let reply = ctx.say("Thinking...\n-# The hardware this thing runs is really slow, expect a long wait").await?;
let mut stream = match ollama.generate_stream(GenerationRequest::new("EwiAI".into(), final_prompt).system(system_prompt())).await {
Ok(s) => s,
Err(e) => {
reply.edit(ctx, CreateReply::default().content(format!("Error during generation: {}", e))).await?;
return Ok(());
}
};
let mut response_text = String::new();
let mut last_update = std::time::Instant::now();
let mut final_response = None;
while let Some(res) = stream.next().await {
match res {
Ok(chunks) => {
for chunk in chunks {
response_text.push_str(&chunk.response);
if chunk.done {
final_response = Some(chunk);
}
}
if last_update.elapsed() >= Duration::from_secs(1) && !response_text.is_empty() {
let _ = reply.edit(ctx, CreateReply::default().content(&response_text)).await;
last_update = std::time::Instant::now();
}
}
Err(e) => {
let _ = reply.edit(ctx, CreateReply::default().content(format!("{} [Stream Error: {}]", response_text, e))).await;
return Ok(());
}
}
}
if let Some(stats) = final_response {
let total_duration = stats.total_duration.unwrap_or(0) as f64 / 1_000_000_000.0;
let eval_count = stats.eval_count.unwrap_or(0);
let eval_duration = stats.eval_duration.unwrap_or(0) as f64 / 1_000_000_000.0;
let tokens_per_sec = if eval_duration > 0.0 { eval_count as f64 / eval_duration } else { 0.0 };
let stats_text = format!(
"\n\n*Generated in {:.2}s ({:.2} tok/s)*",
total_duration, tokens_per_sec
);
response_text.push_str(&stats_text);
}
let _ = reply.edit(ctx, CreateReply::default().content(&response_text)).await;
Ok(())
}
fn system_prompt() -> String {
format!("You are EwiAI, an AI in the TeenGovernment Development Server. Your developer is Ewi/ewenlau (discord user id: 713354021124964422, discord username: ewenlau). Be slightly unhelpful, in a sarcastic way, but still answer the question. The current date is {}", Utc::now().to_string())
}
+207 -8
View File
@@ -1,6 +1,112 @@
use crate::{Context, Error};
use tracing::{debug, trace, info};
use crate::shared_functions::trace_message;
use tracing::{debug, trace, warn};
use crate::messaging::trace_message;
use regex::Regex;
use std::sync::LazyLock;
use std::str::FromStr;
use std::fmt::{Display, Formatter};
use f128::f128;
use exmex::{Express, MakeOperators, Operator, BinOp};
use num_traits::float::Float;
#[derive(Clone, Copy, Debug)]
pub struct F128(pub f128);
impl Default for F128 {
fn default() -> Self {
F128(f128::new(0.0))
}
}
impl FromStr for F128 {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match f128::parse(s) {
Ok(v) => Ok(F128(v)),
Err(e) => Err(format!("{:?}", e))
}
}
}
impl Display for F128 {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug)]
pub struct F128OpsFactory;
impl MakeOperators<F128> for F128OpsFactory {
fn make<'a>() -> Vec<Operator<'a, F128>> {
vec![
Operator::make_bin("^", BinOp { apply: |a, b| F128(a.0.powf(b.0)), prio: 4, is_commutative: false }),
Operator::make_bin("*", BinOp { apply: |a, b| F128(a.0 * b.0), prio: 2, is_commutative: true }),
Operator::make_bin("/", BinOp { apply: |a, b| F128(a.0 / b.0), prio: 3, is_commutative: false }),
Operator::make_bin_unary("+", BinOp { apply: |a, b| F128(a.0 + b.0), prio: 0, is_commutative: true }, |a| a),
Operator::make_bin_unary("-", BinOp { apply: |a, b| F128(a.0 - b.0), prio: 1, is_commutative: false }, |a| F128(-a.0)),
Operator::make_unary("sin", |a| F128(a.0.sin())),
Operator::make_unary("cos", |a| F128(a.0.cos())),
Operator::make_unary("tan", |a| F128(a.0.tan())),
Operator::make_unary("floor", |a| F128(a.0.floor())),
Operator::make_unary("round", |a| F128(a.0.round())),
Operator::make_unary("ceil", |a| F128(a.0.ceil())),
Operator::make_unary("trunc", |a| F128(a.0.trunc())),
Operator::make_unary("fract", |a| F128(a.0.fract())),
Operator::make_unary("exp", |a| F128(a.0.exp())),
Operator::make_unary("sqrt", |a| F128(a.0.sqrt())),
Operator::make_unary("cbrt", |a| F128(a.0.cbrt())),
Operator::make_unary("ln", |a| F128(a.0.ln())),
Operator::make_unary("log2", |a| F128(a.0.log2())),
Operator::make_unary("log10", |a| F128(a.0.log10())),
Operator::make_unary("abs", |a| F128(a.0.abs())),
Operator::make_unary("signum", |a| F128(a.0.signum())),
Operator::make_constant("PI", F128(f128::new(std::f64::consts::PI))),
Operator::make_constant("π", F128(f128::new(std::f64::consts::PI))),
Operator::make_constant("E", F128(f128::new(std::f64::consts::E))),
Operator::make_constant("e", F128(f128::new(std::f64::consts::E))),
Operator::make_constant("TAU", F128(f128::new(std::f64::consts::TAU))),
Operator::make_constant("τ", F128(f128::new(std::f64::consts::TAU))),
]
}
}
#[derive(Clone, Debug)]
pub struct F256OpsFactory;
impl MakeOperators<f256::f256> for F256OpsFactory {
fn make<'a>() -> Vec<Operator<'a, f256::f256>> {
vec![
Operator::make_bin("^", BinOp { apply: |a, b| a.powf(&b), prio: 4, is_commutative: false }),
Operator::make_bin("*", BinOp { apply: |a, b| a * b, prio: 2, is_commutative: true }),
Operator::make_bin("/", BinOp { apply: |a, b| a / b, prio: 3, is_commutative: false }),
Operator::make_bin_unary("+", BinOp { apply: |a, b| a + b, prio: 0, is_commutative: true }, |a| a),
Operator::make_bin_unary("-", BinOp { apply: |a, b| a - b, prio: 1, is_commutative: false }, |a| -a),
Operator::make_unary("sin", |a| a.sin()),
Operator::make_unary("cos", |a| a.cos()),
Operator::make_unary("tan", |a| a.tan()),
Operator::make_unary("floor", |a| a.floor()),
Operator::make_unary("round", |a| a.round()),
Operator::make_unary("ceil", |a| a.ceil()),
Operator::make_unary("trunc", |a| a.trunc()),
Operator::make_unary("fract", |a| a.fract()),
Operator::make_unary("exp", |a| a.exp()),
Operator::make_unary("sqrt", |a| a.sqrt()),
Operator::make_unary("cbrt", |a| a.cbrt()),
Operator::make_unary("ln", |a| a.ln()),
Operator::make_unary("log2", |a| a.log2()),
Operator::make_unary("log10", |a| a.log10()),
Operator::make_unary("abs", |a| a.abs()),
Operator::make_unary("signum", |a| a.signum()),
Operator::make_constant("PI", f256::f256::from(std::f64::consts::PI)),
Operator::make_constant("π", f256::f256::from(std::f64::consts::PI)),
Operator::make_constant("E", f256::f256::from(std::f64::consts::E)),
Operator::make_constant("e", f256::f256::from(std::f64::consts::E)),
Operator::make_constant("TAU", f256::f256::from(std::f64::consts::TAU)),
Operator::make_constant("τ", f256::f256::from(std::f64::consts::TAU)),
]
}
}
static FIND_SCI_NOTATION_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([0-9]+(?:\.[0-9]+)?)e([+-]?[0-9]+)").unwrap());
#[derive(Debug, poise::ChoiceParameter)]
pub enum Precision {
@@ -8,6 +114,18 @@ pub enum Precision {
Bits32,
#[name = "64-bit"]
Bits64,
#[name = "128-bit"]
Bits128,
#[name = "256-bit"]
Bits256,
}
#[derive(Debug, poise::ChoiceParameter)]
pub enum Mode {
#[name = "Floating Point"]
Float,
#[name = "Integer"]
Int,
}
/// Calculator
@@ -24,11 +142,27 @@ pub async fn calc(ctx: Context<'_>,
let response_message = ctx.say(msg).await?;
let start_time = std::time::Instant::now();
trace!("Saving start time: {:?}", start_time);
let value = match precision {
Some(Precision::Bits32) => exmex::eval_str::<f32>(&expression).map_err(|err| err.to_string())?.to_string(),
Some(Precision::Bits64) => exmex::eval_str::<f64>(&expression).map_err(|err| err.to_string())?.to_string(),
None => exmex::eval_str::<f64>(&expression).map_err(|err| err.to_string())?.to_string(),
if expression.contains('=') {
let error_msg = format!("Invalid expression: {}\nYou are not allowed to have an equal sign in the expression.", expression);
debug!("Invalid expression detected: {}", expression);
trace_message(error_msg.clone(), ctx.channel_id().to_string(), ctx.guild_id().unwrap().to_string()).await;
edit_response_message(&response_message, ctx, error_msg, false).await?;
return Ok(());
}
let processed_expr : String = FIND_SCI_NOTATION_RE.replace_all(&expression, "$1 * 10^($2)").to_string();
trace!("Processed expression: {}", processed_expr);
let value = match evaluate_expression(processed_expr, &precision) {
Ok(v) => v,
Err(err_msg) => {
let error_msg = format!("Failed to parse or evaluate expression: `{}`", err_msg);
warn!("Failed to parse or evaluate expression: `{}` in guild {} channel {} by {}", err_msg, ctx.guild_id().unwrap().get(), ctx.channel_id().get(), ctx.author().name);
trace_message(error_msg.clone(), ctx.channel_id().to_string(), ctx.guild_id().unwrap().to_string()).await;
edit_response_message(&response_message, ctx, error_msg, false).await?;
return Ok(());
}
};
trace!("Result: {}", value);
let end_time = std::time::Instant::now();
trace!("Saving end time: {:?}", end_time);
let duration = end_time - start_time;
@@ -36,12 +170,21 @@ pub async fn calc(ctx: Context<'_>,
let precision_str = match precision {
Some(Precision::Bits32) => "32-bit".to_string(),
Some(Precision::Bits64) => "64-bit".to_string(),
Some(Precision::Bits128) => "128-bit".to_string(),
Some(Precision::Bits256) => "256-bit".to_string(),
None => "64-bit".to_string(),
};
let msg = format!("{} = {} \n-# Precision: {} Compute time : {:?}", expression, value, precision_str, duration);
// Turn to scientifc notation if number has more than 30 digits
let value_string = if value.len() > 30 {
to_scientific_notation(value)
} else {
value
};
let msg = format!("{} = {} \n-# Precision: {} Compute time : {:?}", expression, value_string, precision_str, duration);
trace_message(msg.clone(), ctx.channel_id().to_string(), ctx.guild_id().unwrap().to_string()).await;
edit_response_message(&response_message, ctx, msg, false).await?;
debug!("Calculation perfomed for {} with result {} in {:?} by {}", expression, value, duration, ctx.author().name);
debug!("Calculation perfomed for {} with result {} in {:?} by {}", expression, value_string, duration, ctx.author().name);
Ok(())
}
@@ -52,4 +195,60 @@ async fn edit_response_message<'a>(response_message: &poise::ReplyHandle<'a>, ct
response_message.edit(ctx, poise::CreateReply::default().content(content)).await?;
}
Ok(())
}
fn to_scientific_notation(value: String) -> String {
trace!("Converting {} to scientific notation", value);
if value.contains('e') || value.contains('E') {
trace!("Already in scientific notation");
return value;
}
let mut is_negative = false;
let mut val_str = value.as_str();
if val_str.starts_with('-') {
is_negative = true;
val_str = &val_str[1..];
}
let parts: Vec<&str> = val_str.split('.').collect();
let integer_part = parts[0];
let fractional_part = if parts.len() > 1 { parts[1] } else { "" };
if integer_part.len() <= 1 {
return value;
}
let first_digit = &integer_part[0..1];
let rest_integer = &integer_part[1..];
let exponent = integer_part.len() - 1;
let mut fractional_combined = format!("{}{}", rest_integer, fractional_part);
fractional_combined = fractional_combined.trim_end_matches('0').to_string();
let sign = if is_negative { "-" } else { "" };
if fractional_combined.is_empty() {
format!("{}{}e{}", sign, first_digit, exponent)
} else {
format!("{}{}.{}e{}", sign, first_digit, fractional_combined, exponent)
}
}
fn evaluate_expression(expr: String, precision: &Option<Precision>) -> Result<String, String> {
let value = match precision {
Some(Precision::Bits32) => exmex::eval_str::<f32>(&expr).map_err(|err| err.to_string())?.to_string(),
Some(Precision::Bits64) => exmex::eval_str::<f64>(&expr).map_err(|err| err.to_string())?.to_string(),
Some(Precision::Bits128) => {
let parsed_expr = exmex::FlatEx::<F128, F128OpsFactory>::parse(&expr).map_err(|err| err.to_string())?;
parsed_expr.eval(&[]).map_err(|err| err.to_string())?.to_string()
},
Some(Precision::Bits256) => {
let parsed_expr = exmex::FlatEx::<f256::f256, F256OpsFactory>::parse(&expr).map_err(|err| err.to_string())?;
parsed_expr.eval(&[]).map_err(|err| err.to_string())?.to_string()
},
None => exmex::eval_str::<f64>(&expr).map_err(|err| err.to_string())?.to_string(),
};
Ok(value)
}
+34
View File
@@ -0,0 +1,34 @@
use std::process::Command;
use crate::{Context, Error};
use tracing::{debug, warn, trace};
use crate::messaging::{trace_message, edit_response_message};
/// Run and output the contents of the fastfetch command on the machine running the bot
#[poise::command(slash_command, prefix_command)]
pub async fn fastfetch(ctx: Context<'_>) -> Result<(), Error> {
debug!("fastfetch command called by user {} in guild {}", ctx.author().id.get(), ctx.guild_id().unwrap().get());
let msg = "Processing...".to_string();
let res_msg = ctx.say(&msg).await?;
trace_message(msg.clone(), ctx.channel_id().to_string(), ctx.guild_id().unwrap().to_string()).await;
let cmd = Command::new("fastfetch").args(&["--raw", "true", "--logo", "none"]).output();
match cmd {
Err(e) => {
warn!("Error executing fastfetch: {}", e);
let msg = format!("Error: {}", e);
edit_response_message(&res_msg, ctx, msg.clone(), false).await?;
trace_message(msg, ctx.channel_id().to_string(), ctx.guild_id().unwrap().to_string()).await;
Ok(())
}
Ok(output) => {
let output_str = output.stdout.into_iter().map(|c| c as char).collect::<String>();
let msg = format!("```ansi\n{}\n```", output_str);
edit_response_message(&res_msg, ctx, msg.clone(), false).await?;
// Tracing the output would be a bad idea since it's really long and filled with ansi escape codes
// trace_message(msg, ctx.channel_id().to_string(), ctx.guild_id().unwrap().to_string()).await;
trace!("Saying fastfetch command output");
Ok(())
}
}
}
+183
View File
@@ -0,0 +1,183 @@
use crate::{Context, Error};
use tracing::{debug, trace};
use crate::messaging::{trace_message, edit_response_message};
/// Find the nth prime number (single threaded)
#[derive(Debug, poise::ChoiceParameter, Clone, Copy)]
pub enum Method {
#[name = "Sieve"]
Sieve,
#[name = "Trial and Error"]
TrialAndError,
}
#[poise::command(slash_command, prefix_command)]
pub async fn find_prime(
ctx: Context<'_>,
#[description = "Which nth prime number to find"]
n: u64,
#[description = "Method to use"]
method: Option<Method>,
#[description = "Custom timeout in seconds (upper limit of 60s)"]
timeout: Option<u64>,
) -> Result<(), Error> {
let guild_id_str = ctx.guild_id().map(|g| g.get().to_string()).unwrap_or_else(|| "DM".to_string());
debug!("Find prime command called by user {} in guild {}", ctx.author().id.get(), guild_id_str);
trace!("Primes requested for {} with method {:?}", n, method);
let msg = "Calculating...".to_string();
let res_msg = ctx.say(msg.clone()).await?;
trace_message(msg, ctx.channel_id().to_string(), guild_id_str.clone()).await;
if n == 0 {
let msg = "Prime indices start at 1. Please provide a value greater than 0.".to_string();
edit_response_message(&res_msg, ctx, msg.clone(), false).await?;
trace_message(msg, ctx.channel_id().to_string(), guild_id_str).await;
return Ok(());
}
if timeout.is_some() && timeout.unwrap() > 60 {
let msg = "Timeout is too long. Maximum is 60 seconds.".to_string();
edit_response_message(&res_msg, ctx, msg.clone(), false).await?;
trace_message(msg, ctx.channel_id().to_string(), guild_id_str).await;
return Ok(());
}
let start_time = std::time::Instant::now();
let timeout_duration = std::time::Duration::from_secs(timeout.unwrap_or(20));
let (tx, rx) = tokio::sync::oneshot::channel();
tokio::task::spawn_blocking(move || {
let result = match method {
Some(Method::Sieve) => sieve(n),
Some(Method::TrialAndError) => super3(n),
None => super3(n),
};
let _ = tx.send(result);
});
let method_name = match method {
Some(Method::Sieve) => "Sieve",
Some(Method::TrialAndError) => "TrialAndError",
None => "TrialAndError",
};
let result = match tokio::time::timeout(timeout_duration, rx).await {
Ok(Ok(result)) => result,
Ok(Err(_)) => {
let msg = "Calculation thread panicked or was dropped unexpectedly.".to_string();
edit_response_message(&res_msg, ctx, msg.clone(), false).await?;
trace_message(msg, ctx.channel_id().to_string(), guild_id_str).await;
return Ok(());
}
Err(_) => {
let msg = format!("Calculation timed out after {:?}", timeout_duration);
edit_response_message(&res_msg, ctx, msg.clone(), false).await?;
trace_message(msg, ctx.channel_id().to_string(), guild_id_str).await;
return Ok(());
}
};
let duration = start_time.elapsed();
let msg = format!("The {}th prime number is {}\n-# Calculation time: {:?}, Method: {:?}", n, result, duration, method_name);
edit_response_message(&res_msg, ctx, msg.clone(), false).await?;
trace_message(msg, ctx.channel_id().to_string(), guild_id_str).await;
Ok(())
}
// My best trial and error algorithm, originally designed to find as many primes as possible in 1s
pub fn super3(limit: u64) -> u64 {
if limit == 0 {
return 0;
}
let mut primes = Vec::new();
primes.push(2);
if limit == 1 {
return 2;
}
primes.push(3);
if limit == 2 {
return 3;
}
let mut current_multiplier = 1;
while (primes.len() as u64) < limit {
for offset in [-1_i64, 1] {
let base = 6 * current_multiplier;
let current_number = if offset < 0 {
base - 1
} else {
base + 1
};
let sqrt = (current_number as f64).sqrt().round() as u64;
let mut is_prime = true;
for &p in primes.iter().filter(|&&x| x > 3).take_while(|&&x| x <= sqrt) {
if current_number % p == 0 {
is_prime = false;
break;
}
}
if is_prime {
primes.push(current_number);
if (primes.len() as u64) == limit {
return current_number;
}
}
}
current_multiplier += 1;
}
*primes.last().unwrap()
}
fn sieve(n: u64) -> u64 {
if n == 0 {
panic!("Prime indices start at 1");
}
if n == 1 { return 2; }
if n == 2 { return 3; }
if n == 3 { return 5; }
if n == 4 { return 7; }
if n == 5 { return 11; }
let n_f = n as f64;
let ln_n = n_f.ln();
let limit = if n >= 688383 {
// Extremely tight bound for large n
(n_f * (ln_n + n_f.ln().ln() - 1.0 + (n_f.ln().ln() - 0.9385) / ln_n)) as usize
} else if n >= 6 {
// Safe bound for medium n
(n_f * (ln_n + n_f.ln().ln())) as usize
} else {
12 // Fallback for tiny numbers, though handled above
};
// Sieve of Eratosthenes
let mut is_prime = vec![true; limit + 1];
is_prime[0] = false;
is_prime[1] = false;
let mut count = 0;
for p in 2..=limit {
if is_prime[p] {
count += 1;
if count == n {
return p as u64;
}
// Safe overflow check for p * p
if let Some(mut i) = p.checked_mul(p) {
while i <= limit {
is_prime[i] = false;
i += p;
}
}
}
}
unreachable!("If we reach here, the upper bound calculation failed.");
}
+20
View File
@@ -0,0 +1,20 @@
use crate::{Context, Error};
use crate::messaging::trace_message;
use std::env;
use tracing::debug;
/// Get a link to the git repo
#[poise::command(slash_command, prefix_command)]
pub async fn git(ctx: Context<'_>) -> Result<(), Error> {
debug!("git command ran by {} in {}", ctx.author().name, ctx.channel_id().get());
let msg = format!("Git repo: https://git.ewenlau.net/ewenlau/tg-dev-srv-bot\nIf you'd like to contribute, contact me to get an account.");
trace_message(msg, ctx.channel_id().get().to_string(), ctx.guild_id().unwrap().get().to_string()).await;
Ok(())
}
+3 -11
View File
@@ -1,7 +1,8 @@
use crate::{Context, Error};
use crate::shared_functions::get_all_admin_users;
use crate::information_queries::get_all_admin_users;
use crate::messaging::edit_response_message;
use poise::serenity_prelude as sere;
use tracing::{error, warn, info, debug, trace};
use tracing::{error, info, debug, trace};
#[derive(Debug, poise::ChoiceParameter)]
pub enum AdminOperationType {
@@ -115,15 +116,6 @@ async fn add_admin_user<'a>(ctx: Context<'a>, msg: &poise::ReplyHandle<'a>, user
Ok(())
}
async fn edit_response_message<'a>(response_message: &poise::ReplyHandle<'a>, ctx: Context<'_>, content: String, silent: bool) -> Result<(), Error> {
if silent {
response_message.edit(ctx, poise::CreateReply::default().content(content).allowed_mentions(serenity::all::CreateAllowedMentions::new().empty_users())).await?;
} else {
response_message.edit(ctx, poise::CreateReply::default().content(content)).await?;
}
Ok(())
}
async fn is_target_admin_user(ctx: Context<'_>, user: &sere::Member) -> Result<bool, Error> {
let admins = get_all_admin_users(ctx).await?;
for admin in admins {
+6 -14
View File
@@ -1,7 +1,8 @@
use crate::{Context, Error};
use crate::shared_functions::get_all_admin_users;
use crate::information_queries::get_all_admin_users;
use crate::messaging::edit_response_message;
use poise::serenity_prelude as sere;
use tracing::{error, warn, info, debug, trace};
use tracing::{error, debug, trace, info, warn};
#[derive(Debug, poise::ChoiceParameter)]
pub enum AutoRoleOperationType {
@@ -97,7 +98,7 @@ async fn enable_auto_role(ctx: Context<'_>, response_message: &poise::ReplyHandl
let query = sqlx::query!("UPDATE auto_roles SET enabled = true WHERE guild_id = $1 AND role_id = $2;", ctx.guild_id().unwrap().get() as i64, role.id.get() as i64).execute(&ctx.data().pool).await;
match query {
Ok(_) => {
info!("Enabled auto role {} in guild {}", role.name, ctx.guild_id().unwrap().get());
debug!("Enabled auto role {} in guild {}", role.name, ctx.guild_id().unwrap().get());
edit_response_message(response_message, ctx, format!("Enabled auto role: <@&{}>", role.id.get()), false).await?;
}
Err(e) => {
@@ -137,7 +138,7 @@ async fn set_auto_role(ctx: Context<'_>, response_message: &poise::ReplyHandle<'
let query = sqlx::query!("INSERT INTO auto_roles (guild_id, role_id, enabled) VALUES ($1, $2, false) ON CONFLICT (guild_id) DO UPDATE SET role_id = $2;", ctx.guild_id().unwrap().get() as i64, role.id.get() as i64).execute(&ctx.data().pool).await;
match query {
Ok(_) => {
info!("Set {} as auto role in guild {}", role.name, ctx.guild_id().unwrap().get());
debug!("Set {} as auto role in guild {}", role.name, ctx.guild_id().unwrap().get());
edit_response_message(response_message, ctx, format!("Set {} as auto role", role.name), false).await?;
}
Err(e) => {
@@ -165,7 +166,7 @@ async fn disable_auto_role(ctx: Context<'_>, response_message: &poise::ReplyHand
let query = sqlx::query!("UPDATE auto_roles SET enabled = false WHERE guild_id = $1 AND role_id = $2;", ctx.guild_id().unwrap().get() as i64, role.id.get() as i64).execute(&ctx.data().pool).await;
match query {
Ok(_) => {
info!("Disabled auto role {} in guild {}", role.name, ctx.guild_id().unwrap().get());
debug!("Disabled auto role {} in guild {}", role.name, ctx.guild_id().unwrap().get());
edit_response_message(response_message, ctx, format!("Disabled auto role: {}", role.name), false).await?;
}
Err(e) => {
@@ -177,15 +178,6 @@ async fn disable_auto_role(ctx: Context<'_>, response_message: &poise::ReplyHand
Ok(())
}
async fn edit_response_message<'a>(response_message: &poise::ReplyHandle<'a>, ctx: Context<'_>, content: String, silent: bool) -> Result<(), Error> {
if silent {
response_message.edit(ctx, poise::CreateReply::default().content(content).allowed_mentions(serenity::all::CreateAllowedMentions::new().empty_users())).await?;
} else {
response_message.edit(ctx, poise::CreateReply::default().content(content)).await?;
}
Ok(())
}
async fn is_auto_role_enabled(ctx: Context<'_>, target_role: &sere::Role) -> Result<bool, Error> {
let query = sqlx::query!("SELECT enabled FROM auto_roles WHERE guild_id = $1 AND role_id = $2;", ctx.guild_id().unwrap().get() as i64, target_role.id.get() as i64).fetch_optional(&ctx.data().pool).await?;
if query.is_none() {
+13 -2
View File
@@ -1,11 +1,22 @@
pub mod ping;
pub mod ai;
pub mod calc;
pub mod fastfetch;
pub mod find_prime;
pub mod manage_admins;
pub mod manage_auto_role;
pub mod ping;
pub mod speedy_pc;
pub mod version;
pub mod git;
pub use ping::ping;
pub use ai::answer;
pub use ai::prompt;
pub use calc::calc;
pub use fastfetch::fastfetch;
pub use find_prime::find_prime;
pub use manage_admins::manage_admins;
pub use manage_auto_role::manage_auto_role;
pub use ping::ping;
pub use speedy_pc::speedy_pc;
pub use version::version;
pub use git::git;
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::{Context, Error};
use poise::serenity_prelude as sere;
use tracing::{trace, debug};
use crate::shared_functions::trace_message;
use crate::messaging::trace_message;
/// Get the bot's current ping (back and forth)
#[poise::command(slash_command, prefix_command)]
+205
View File
@@ -0,0 +1,205 @@
use crate::messaging::trace_message;
use crate::{Context, Error};
use chrono::{DateTime, Utc};
use ollama_rs::generation::completion::request::GenerationRequest;
use ollama_rs::models::create::CreateModelRequest;
use ollama_rs::Ollama;
use poise::CreateReply;
use std::env;
use std::time::Duration;
use tokio_stream::StreamExt;
use tracing::{debug, trace};
const MODEL_NAME: &str = "SpeedyPC";
/// Run a command on a system close to Speedy's PC performance
#[poise::command(slash_command, prefix_command)]
pub async fn speedy_pc(
ctx: Context<'_>,
#[description = "The command to run."] command: String,
) -> Result<(), Error> {
debug!(
"speedy_pc command called by user {} in guild {:?}",
ctx.author().id.get(),
ctx.guild_id()
);
let msg = format!("Running command: {:?}", command);
trace_message(
msg,
ctx.channel_id().to_string(),
ctx.guild_id()
.map(|guild_id| guild_id.to_string())
.unwrap_or_else(|| "DM".to_string()),
)
.await;
let host = match env::var("TG_BOT_OLLAMA_HOST") {
Ok(host) => host,
Err(_) => {
ctx.say("Error: Expected an ollama url in the environment (`TG_BOT_OLLAMA_HOST`).")
.await?;
return Ok(());
}
};
let formatted_host = if !host.starts_with("http://") && !host.starts_with("https://") {
format!("http://{}", host)
} else {
host
};
let ollama = Ollama::builder().host(&formatted_host).port(11434).build();
let model_list = match ollama.list_local_models().await {
Ok(models) => models,
Err(error) => {
ctx.say(format!("Error: Failed to connect to simulation: {}", error))
.await?;
return Ok(());
}
};
let mut needs_create = true;
let yesterday = Utc::now() - chrono::Duration::days(1);
for model in &model_list {
if model.name.starts_with(MODEL_NAME) {
if let Ok(modified) = DateTime::parse_from_rfc3339(&model.modified_at) {
if modified.with_timezone(&Utc) > yesterday {
needs_create = false;
}
}
}
}
let reply = ctx
.say("Processing...")
.await?;
if needs_create {
reply.edit(
ctx,
CreateReply::default().content("Loading Speedy's PC..."),
)
.await?;
if let Err(error) = ollama
.create_model(
CreateModelRequest::new(MODEL_NAME.into())
.system(system_prompt().into())
.from_model("gemma4:e2b-it-qat".into()),
)
.await
{
reply
.edit(
ctx,
CreateReply::default().content(format!("Error loading Speedy's PC: {}", error)),
)
.await?;
return Ok(());
}
}
trace!("SpeedyPC command prompt: {}", command);
let mut stream = match ollama
.generate_stream(GenerationRequest::new(MODEL_NAME.into(), command).system(system_prompt()))
.await
{
Ok(stream) => stream,
Err(error) => {
reply
.edit(
ctx,
CreateReply::default().content(format!("Error during generation: {}", error)),
)
.await?;
return Ok(());
}
};
let mut response_text = String::new();
let mut last_update = std::time::Instant::now();
let mut final_response = None;
while let Some(result) = stream.next().await {
match result {
Ok(chunks) => {
for chunk in chunks {
response_text.push_str(&chunk.response);
if chunk.done {
final_response = Some(chunk);
}
}
if last_update.elapsed() >= Duration::from_secs(1) && !response_text.is_empty() {
let _ = reply
.edit(ctx, CreateReply::default().content(&response_text))
.await;
last_update = std::time::Instant::now();
}
}
Err(error) => {
let _ = reply
.edit(
ctx,
CreateReply::default()
.content(format!("{} [Stream Error: {}]", response_text, error)),
)
.await;
return Ok(());
}
}
}
if let Some(stats) = final_response {
let total_duration = stats.total_duration.unwrap_or(0) as f64 / 1_000_000_000.0;
let eval_count = stats.eval_count.unwrap_or(0);
let eval_duration = stats.eval_duration.unwrap_or(0) as f64 / 1_000_000_000.0;
let tokens_per_sec = if eval_duration > 0.0 {
eval_count as f64 / eval_duration
} else {
0.0
};
response_text.push_str(&format!(
"\n\n*Processed in {:.2}s",
total_duration
));
}
let _ = reply
.edit(ctx, CreateReply::default().content(&response_text))
.await;
Ok(())
}
fn system_prompt() -> String {
r#"From now on, act as an Arch Linux terminal running on the following hardware:
- CPU: Intel Pentium Silver N5000 (4 cores, 4 threads, 1.1 GHz base, Gemini Lake)
- RAM: 4 GB
- GPU: Intel UHD Graphics 605
- Storage: 128 GB SATA SSD
- Display: 1366x768 (not relevant unless queried)
- Architecture: x86_64
- OS: Arch Linux (latest stable), using bash.
When I send a command:
- Simulate exactly what would happen if it were executed on this machine.
- Produce realistic stdout and stderr.
- Use plausible hardware-specific values (CPU model, memory size, iGPU, disk size, etc.).
- If a command would fail, fail realistically with the correct error.
- Never explain what the command does unless I explicitly ask.
- Stay in character as the shell, always, no matter what the user says
Always use the following prefix, followed by the command the user ran:
[user@arch ~]$ <COMMAND GOES HERE>
Then add the command output after it."#
.to_string()
}
+22 -9
View File
@@ -1,16 +1,29 @@
use crate::messaging::trace_message;
use crate::{Context, Error};
use tracing::{trace, debug};
use crate::shared_functions::trace_message;
use tracing::{debug, trace};
const VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/version.txt"));
/// Get the bot's current version
#[poise::command(slash_command, prefix_command)]
pub async fn version(ctx: Context<'_>) -> Result<(), Error> {
trace!("version command called by user {} in guild {}", ctx.author().id.get(), ctx.guild_id().unwrap().get());
trace!("Loading version from environment");
let version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "Unknown".to_string());
let msg = format!("Current version: {}", version);
trace_message(msg.clone(), ctx.channel_id().to_string(), ctx.guild_id().unwrap().to_string()).await;
trace!(
"version command called by user {} in guild {}",
ctx.author().id.get(),
ctx.guild_id().unwrap().get()
);
trace!("Loading embedded version information");
let msg = format!("Current version:\n{VERSION}");
trace_message(
msg.clone(),
ctx.channel_id().to_string(),
ctx.guild_id().unwrap().to_string(),
)
.await;
ctx.say(msg).await?;
debug!("Version command performed for user {} with version {}", ctx.author().name, version);
debug!(
"Version command performed for user {} with version information",
ctx.author().name
);
Ok(())
}
}
+2 -1
View File
@@ -1,5 +1,5 @@
use poise::serenity_prelude as sere;
use tracing::{error, warn, info, debug, trace};
use tracing::{error, debug, trace};
pub async fn user_join(ctx: &sere::Context, new_member: &sere::Member, pool: &sqlx::PgPool) {
trace!("User joined event handler called for guild {}", new_member.guild_id);
@@ -35,6 +35,7 @@ pub async fn user_join(ctx: &sere::Context, new_member: &sere::Member, pool: &sq
async fn give_role_to_user(ctx: &sere::Context, member: &sere::Member, role_id: u64) {
if let Err(e) = member.add_role(&ctx.http, sere::RoleId::new(role_id)).await {
error!("Error adding role to member: {:?}", e);
return;
}
debug!("Role {} added to user {} in guild {}", role_id, member.user.name, member.guild_id);
}
@@ -53,8 +53,4 @@ pub async fn get_all_admin_users(ctx: Context<'_>) -> Result<Vec<sere::User>, Er
}
Ok(admin_users)
}
pub async fn trace_message(msg: String, channel: String, guild: String) {
trace!("Saying \"{}\" in channel {} in guild {}", msg, channel, guild);
}
+16 -3
View File
@@ -5,7 +5,8 @@ use tracing_subscriber::EnvFilter;
pub mod commands;
pub mod event_handler;
pub mod events;
pub mod shared_functions;
pub mod information_queries;
pub mod messaging;
pub struct Data {
pub pool: sqlx::PgPool,
@@ -75,7 +76,19 @@ async fn main() {
event_handler: |ctx, event, framework, user_data| {
Box::pin(event_handler::event_handler(ctx, event, framework, user_data))
},
commands: vec![commands::ping(), commands::calc(), commands::manage_admins(), commands::manage_auto_role(), commands::version()],
on_error: |error| Box::pin(async move {
match error {
poise::FrameworkError::Command { error, ctx, .. } => {
error!("Error in command `{}`: {:?}", ctx.command().name, error);
}
error => {
if let Err(e) = poise::builtins::on_error(error).await {
error!("Error while handling error: {}", e);
}
}
}
}),
commands: vec![commands::ping(), commands::calc(), commands::manage_admins(), commands::manage_auto_role(), commands::version(), commands::fastfetch(), commands::find_prime(), commands::prompt(), commands::answer(), commands::speedy_pc(), commands::git()],
..Default::default()
})
.setup(|ctx, _ready, framework| {
@@ -97,4 +110,4 @@ async fn main() {
if let Err(why) = client.start().await {
error!("Client error: {why:?}");
}
}
}
+16
View File
@@ -0,0 +1,16 @@
use crate::{Context, Error};
use poise::serenity_prelude as sere;
use tracing::trace;
pub async fn trace_message(msg: String, channel: String, guild: String) {
trace!("Saying \"{}\" in channel {} in guild {}", msg, channel, guild);
}
pub async fn edit_response_message<'a>(response_message: &poise::ReplyHandle<'a>, ctx: Context<'_>, content: String, silent: bool) -> Result<(), Error> {
if silent {
response_message.edit(ctx, poise::CreateReply::default().content(content).allowed_mentions(serenity::all::CreateAllowedMentions::new().empty_users())).await?;
} else {
response_message.edit(ctx, poise::CreateReply::default().content(content)).await?;
}
Ok(())
}
-30
View File
@@ -1,30 +0,0 @@
#!/bin/bash
# AI WRITTEN
set -e
# Ensure we run from the script's directory
cd "$(dirname "$0")"
# Load environment variables if they exist
if [ -f .env ]; then
set -a
source .env
set +a
fi
echo "Wiping database volume and restarting containers..."
docker compose -f docker-compose-dev.yml down -v
docker compose -f docker-compose-dev.yml up -d
echo "Waiting for PostgreSQL to be ready..."
until [ "$(docker inspect -f '{{.State.Health.Status}}' tg-dev-srv-bot-postgres 2>/dev/null)" = "healthy" ]; do
printf "."
sleep 1
done
echo ""
echo "PostgreSQL is healthy!"
echo "Applying database schema from sqlschema.txt..."
docker compose -f docker-compose-dev.yml exec -T postgres psql -U "${TG_BOT_POSTGRES_USER:-postgres}" -d "${TG_BOT_POSTGRES_DB:-tg_dev_srv_bot}" -c "$(cat sqlschema.txt)"
echo "Database reset and schema applied successfully!"