added matrix support, added matrix-sdk dependency
build / docker (push) Has been cancelled

This commit is contained in:
2026-02-22 17:59:57 -05:00
parent afea8a09fe
commit 598870689d
11 changed files with 2189 additions and 107 deletions
+6 -6
View File
@@ -1,12 +1,12 @@
use serenity::builder::CreateCommand;
use serenity::model::application::ResolvedOption;
pub async fn run(options: &[ResolvedOption<'_>]) -> String {
r#"
A Discord bot that rolls limitless dice, randomly!
Copyright 2025, all rights reserved
"#
.to_string()
pub fn run_core() -> String {
"A bot that rolls limitless dice, randomly!\nCopyright 2025, all rights reserved".to_string()
}
pub async fn run(_options: &[ResolvedOption<'_>]) -> String {
run_core()
}
pub fn register() -> CreateCommand {
+14 -11
View File
@@ -3,24 +3,27 @@ use crate::util::{junk, random::RandomGen, validate};
use serenity::builder::{CreateCommand, CreateCommandOption};
use serenity::model::application::{CommandOptionType, ResolvedOption, ResolvedValue};
pub async fn run(options: &[ResolvedOption<'_>]) -> String {
// check if options array is empty first
if options.is_empty() {
let mut rng = RandomGen::new();
return rng.range_random_from_one(999).to_string();
pub fn run_core(input: Option<&str>) -> String {
let mut rng = RandomGen::new();
match input {
None => rng.range_random_from_one(999).to_string(),
Some(s) => match validate::parse_str_into_num::<i32>(s.trim()) {
Some(n) => rng.range_random_from_one(n).to_string(),
None => junk::get_random_insult(),
},
}
}
// options exist, process first option
pub async fn run(options: &[ResolvedOption<'_>]) -> String {
if options.is_empty() {
return run_core(None);
}
if let Some(ResolvedOption {
value: ResolvedValue::String(input),
..
}) = options.first()
{
let mut rng = RandomGen::new();
return match validate::parse_str_into_num::<i32>(input.trim()) {
Some(n) => rng.range_random_from_one(n).to_string(),
None => return junk::get_random_insult(),
};
run_core(Some(input))
} else {
junk::get_random_insult()
}
+21 -13
View File
@@ -6,25 +6,33 @@ use crate::{
use serenity::builder::{CreateCommand, CreateCommandOption};
use serenity::model::application::{CommandOptionType, ResolvedOption, ResolvedValue};
pub fn run_core(input: Option<&str>) -> String {
let Some(input) = input else {
return junk::get_random_insult();
};
let split = match input.split('d').nth(1) {
Some(s) => s,
None => return junk::get_random_insult(),
};
let die_num = match validate::parse_str_into_num::<i32>(split.trim()) {
Some(d) => d,
None => return junk::get_random_insult(),
};
match dietype::DieType::from_sides(die_num) {
Some(_) => {}
None => return junk::get_random_insult(),
};
let mut rng = random::RandomGen::new();
rng.range_random_from_one(die_num).to_string()
}
pub async fn run(options: &[ResolvedOption<'_>]) -> String {
if let Some(ResolvedOption {
value: ResolvedValue::String(input),
..
}) = options.first()
{
let split = input.split("d").nth(1).unwrap();
let die_num = match validate::parse_str_into_num::<i32>(split.trim()) {
Some(d) => d,
None => return junk::get_random_insult(),
};
let _die_type = match dietype::DieType::from_sides(die_num) {
Some(d) => d.to_sides(),
None => return junk::get_random_insult(),
};
let mut rng = random::RandomGen::new();
let result = rng.range_random_from_one(die_num).to_string();
return format!("{result}");
run_core(Some(input))
} else {
junk::get_random_insult()
}
+76
View File
@@ -0,0 +1,76 @@
use std::env;
use serenity::async_trait;
use serenity::builder::{CreateInteractionResponse, CreateInteractionResponseMessage};
use serenity::model::application::Interaction;
use serenity::model::gateway::Ready;
use serenity::model::id::GuildId;
use serenity::prelude::*;
use crate::commands;
struct Handler;
#[async_trait]
impl EventHandler for Handler {
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
if let Interaction::Command(command) = interaction {
let content = match command.data.name.as_str() {
"roll" => Some(commands::roll::run(&command.data.options()).await),
"random" => Some(commands::random::run(&command.data.options()).await),
"about" => Some(commands::about::run(&command.data.options()).await),
_ => Some("not implemented".to_string()),
};
if let Some(content) = content {
let data = CreateInteractionResponseMessage::new().content(content);
let builder = CreateInteractionResponse::Message(data);
if let Err(why) = command.create_response(&ctx.http, builder).await {
println!("Cannot respond to slash command: {why}");
}
}
}
}
async fn ready(&self, ctx: Context, ready: Ready) {
println!("{} is connected!", ready.user.name);
let guild_id = GuildId::new(
env::var("GUILD_ID")
.expect("Expected GUILD_ID in environment")
.parse()
.expect("GUILD_ID must be an integer"),
);
let cmds = guild_id
.set_commands(
&ctx.http,
vec![
commands::roll::register(),
commands::random::register(),
commands::about::register(),
],
)
.await;
match cmds {
Ok(c) => println!("Registered {} commands!", c.len()),
Err(e) => println!("Error registering commands! Reason: {e}"),
}
println!("{} is ready to rock and roll!", ready.user.name);
}
}
pub async fn run() {
let token = env::var("DISCORD_TOKEN").expect("Expected DISCORD_TOKEN in environment");
let intents = GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MESSAGES;
let mut client = Client::builder(token, intents)
.event_handler(Handler)
.await
.expect("Error creating Discord client");
if let Err(why) = client.start().await {
println!("Discord client error: {why:?}");
}
}
+24 -72
View File
@@ -1,86 +1,38 @@
mod commands;
mod config;
mod discord;
mod matrix;
mod types;
mod util;
use std::env;
use serenity::async_trait;
use serenity::builder::{CreateInteractionResponse, CreateInteractionResponseMessage};
use serenity::model::application::Interaction;
use serenity::model::gateway::Ready;
use serenity::model::id::GuildId;
use serenity::prelude::*;
struct Handler;
#[async_trait]
impl EventHandler for Handler {
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
if let Interaction::Command(command) = interaction {
let content = match command.data.name.as_str() {
"roll" => Some(commands::roll::run(&command.data.options()).await),
"random" => Some(commands::random::run(&command.data.options()).await),
"about" => Some(commands::about::run(&command.data.options()).await),
_ => Some("not implemented :(".to_string()),
};
if let Some(content) = content {
let data = CreateInteractionResponseMessage::new().content(content);
let builder = CreateInteractionResponse::Message(data);
if let Err(why) = command.create_response(&ctx.http, builder).await {
println!("Cannot respond to slash command: {why}");
}
}
}
}
async fn ready(&self, ctx: Context, ready: Ready) {
println!("{} is connected!", ready.user.name);
let guild_id = GuildId::new(
env::var("GUILD_ID")
.expect("Expected GUILD_ID in environment")
.parse()
.expect("GUILD_ID must be an integer"),
);
let commands = guild_id
.set_commands(
&ctx.http,
vec![
commands::roll::register(),
commands::random::register(),
commands::about::register(),
],
)
.await;
match commands {
Ok(c) => println!("Registered {} commands!", c.len()),
Err(e) => println!("Error registering commands! Reason: {e}"),
}
println!("{} is ready to rock and roll!", ready.user.name);
}
}
#[tokio::main]
async fn main() {
// setting up configuration
let _ = config::config();
// setting up the discord bot
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
let has_discord = env::var("DISCORD_TOKEN").is_ok();
let has_matrix = env::var("MATRIX_HOMESERVER").is_ok()
&& env::var("MATRIX_USERNAME").is_ok()
&& env::var("MATRIX_PASSWORD").is_ok();
// setting up the discord client
let intents = GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MESSAGES;
let mut client = Client::builder(token, intents)
.event_handler(Handler)
.await
.expect("Error creating client");
if let Err(why) = client.start().await {
println!("Client error: {why:?}");
match (has_discord, has_matrix) {
(true, true) => {
println!("Both Discord and Matrix credentials found; defaulting to Discord.");
discord::run().await;
}
(true, false) => {
discord::run().await;
}
(false, true) => {
matrix::run().await;
}
(false, false) => {
eprintln!(
"No credentials found. Set DISCORD_TOKEN or \
MATRIX_HOMESERVER + MATRIX_USERNAME + MATRIX_PASSWORD."
);
std::process::exit(1);
}
}
}
+114
View File
@@ -0,0 +1,114 @@
use std::env;
use matrix_sdk::{
Client, Room, RoomState,
config::SyncSettings,
ruma::events::room::{
member::{MembershipState, StrippedRoomMemberEvent},
message::{
AddMentions, ForwardThread, MessageType, OriginalSyncRoomMessageEvent,
RoomMessageEventContent,
},
},
};
use crate::commands;
async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) {
if room.state() != RoomState::Joined {
return;
}
let MessageType::Text(text_content) = event.content.msgtype.to_owned() else {
return;
};
let body = text_content.body.trim();
if !body.starts_with('!') {
return;
}
let mut parts = body.splitn(3, ' ');
let command = parts.next().unwrap_or("");
let arg = parts.next();
let response = match command {
"!roll" => commands::roll::run_core(arg),
"!random" => commands::random::run_core(arg),
"!about" => commands::about::run_core(),
_ => return,
};
let reply = RoomMessageEventContent::text_plain(response).make_reply_to(
&event.into_full_event(room.room_id().to_owned()),
ForwardThread::No,
AddMentions::Yes,
);
match room.send(reply).await {
Ok(_) => (),
Err(e) => {
println!("Failed to send message: {e}");
}
}
}
async fn on_stripped_member(event: StrippedRoomMemberEvent, client: Client, room: Room) {
let user_id = match client.user_id() {
Some(u) => u,
None => {
println!("Irregular Matrix ID");
return;
}
};
if event.state_key != user_id {
return;
}
if event.content.membership != MembershipState::Invite {
return;
}
if let Err(e) = room.join().await {
println!("Failed to join room: {e}");
}
}
pub async fn run() {
let homeserver =
env::var("MATRIX_HOMESERVER").expect("Expected MATRIX_HOMESERVER in environment");
let username = env::var("MATRIX_USERNAME").expect("Expected MATRIX_USERNAME in environment");
let password = env::var("MATRIX_PASSWORD").expect("Expected MATRIX_PASSWORD in environment");
let client = Client::builder()
.homeserver_url(&homeserver)
.build()
.await
.expect("Failed to build Matrix client");
client
.matrix_auth()
.login_username(&username, &password)
.initial_device_display_name("caitsith")
.send()
.await
.expect("Failed to log in to Matrix");
println!("Logged in to Matrix as {username}");
// Advance past existing messages before registering handlers so the bot
// doesn't reply to messages that ever existed in a given room.
let sync_response = client
.sync_once(SyncSettings::default())
.await
.expect("Matrix initial sync error");
client.add_event_handler(on_room_message);
client.add_event_handler(on_stripped_member);
client
.sync(SyncSettings::default().token(sync_response.next_batch))
.await
.expect("Matrix sync error");
}