10 Commits
Author SHA1 Message Date
adevries17 e49f27c0df Update dependencies 2026-07-21 12:30:12 -05:00
adevries17 da84226a45 add mentions support 2026-05-12 14:01:32 -05:00
adevries17 a9b1115269 update matrix sdk to 0.17.0 2026-05-09 08:41:27 -05:00
adevries17 d3fac99ecf add debug messages 2026-05-09 08:40:58 -05:00
adevries17 bade2a1284 change the status type to the enum 2026-05-08 16:36:02 -05:00
adevries17 a8674ef0ad update packages 2026-05-08 16:35:29 -05:00
adevries17 a18eb2a41f update dependencies 2026-04-22 14:16:51 -05:00
adevries17 1b999c6f87 Fix formatting in config.yaml comments 2026-04-22 14:15:27 -05:00
adevries17 de23493572 Update status field to be optional
The status field in MagbotConfig is now an Option<MagbotStatus> instead
of a direct MagbotStatus value, allowing it to be None when not
configured.
```
2026-03-24 13:19:52 -05:00
adevries17 3b5a4a8cb2 WIP: track state between commands 2026-01-31 12:37:38 -06:00
8 changed files with 1039 additions and 883 deletions
+22
View File
@@ -0,0 +1,22 @@
// Project-local debug tasks
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[
{
"label": "Build & Debug magbot-rs",
"build": {
"command": "cargo",
"args": ["build"],
},
"program": "$ZED_WORKTREE_ROOT/target/debug/magbot-rs",
// sourceLanguages is required for CodeLLDB (not GDB) when using Rust
"sourceLanguages": ["rust"],
"request": "launch",
"adapter": "CodeLLDB",
"env": {
"HOMESERVER_URL": "https://matrix.org",
},
},
]
Generated
+886 -856
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,11 +1,11 @@
username: "magbot"
password: "zC1b9qCKT8Cv0D"
room_id: "!ajyOBbqoCIbBhEqNPU:matrix.org" # Magbot Testing Space
room_id: "!ajyOBbqoCIbBhEqNPU:matrix.org" # Magbot Testing Space
# room_id: "!HXiyYVEZOMRtkyvarN:jupiterbroadcasting.com" # Da Lunch
# room_id: "!gJYEKNllaubNlNkFIj:jupiterbroadcasting.com" # The Main Chat
lookback_seconds: 86400 # 24 hours
lookback_seconds: 86400 # 24 hours
lookback_limit: 400
dump_mode: True
dbName: ""
vote_host: "localhost" # Hostname where voting interface is accessible (e.g., "localhost", "example.com", "vote.mysite.xyz")
vote_activity_hours: 6672 # Hours a user must have been active within to be eligible to vote
vote_host: "localhost" # Hostname where voting interface is accessible (e.g., "localhost", "example.com", "vote.mysite.xyz")
vote_activity_hours: 6672 # Hours a user must have been active within to be eligible to vote
+2 -1
View File
@@ -15,10 +15,11 @@ path = "src/bin/main.rs"
[dependencies]
anyhow = "1.0.100"
libsqlite3-sys = { version = "0.35.0", features = ["bundled"] }
matrix-sdk = "0.16.0"
matrix-sdk = "0.17.0"
serde = { version = "1.0.228", features = ["derive"] }
serde_yml = "0.0.12"
tokio = { version = "1.48.0", features = ["rt-multi-thread", "macros"] }
tracing = "0.1.41"
tracing-subscriber = "0.3.22"
[dev-dependencies]
+2 -1
View File
@@ -6,7 +6,8 @@ use libmagbot::config::MagbotConfig;
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
let config = MagbotConfig::from_env()?;
let file_str = libmagbot::config::read_file()?;
let config = MagbotConfig::from_str(&file_str)?;
let Ok(homeserver_fqdn) = env::var("HOMESERVER_URL") else {
eprintln!("Set the homeserver fqdn as the `HOMESERVER_URL` environment variable");
std::process::exit(1)
+56 -12
View File
@@ -1,10 +1,14 @@
use matrix_sdk::{
Room,
event_handler::Ctx,
ruma::events::{
reaction::ReactionEventContent,
relation::Annotation,
room::message::{OriginalSyncRoomMessageEvent, RoomMessageEventContent},
ruma::{
UserId,
events::{
Mentions,
reaction::ReactionEventContent,
relation::Annotation,
room::message::{OriginalSyncRoomMessageEvent, RoomMessageEventContent},
},
},
};
@@ -24,16 +28,23 @@ pub async fn start(
room: &Room,
mut ctx: Ctx<MagbotContext>,
) -> anyhow::Result<()> {
tracing::debug!("Executing !start command");
let user = event.sender.to_string();
let ids = vec![UserId::parse(user.clone()).unwrap()];
tracing::debug!("Command invoked by: {}", user);
// set to listening
match ctx.0.status {
MagbotStatus::Running => {
tracing::info!("Bot status already Running, not changing state");
let mentions = Mentions::with_user_ids(ids);
let msg = RoomMessageEventContent::text_plain(format!(
"{user} Magbot is already listening for titles!"
));
))
.add_mentions(mentions);
room.send(msg).await?;
}
MagbotStatus::Sleeping => {
tracing::info!("Changing bot status from Sleeping to Running");
ctx.0.status = MagbotStatus::Running;
let msg = RoomMessageEventContent::text_plain(format!(
"{user} Magbot is now listening for titles!"
@@ -50,8 +61,12 @@ pub async fn stop(
room: &Room,
mut ctx: Ctx<MagbotContext>,
) -> anyhow::Result<()> {
tracing::debug!("Executing !stop command");
let user = event.sender.to_string();
let ids = vec![UserId::parse(user.clone()).unwrap()];
tracing::debug!("Command invoked by: {}", user);
tracing::debug!("Clearing {} saved titles", ctx.0.titles.len());
// clear out saved titles
ctx.0.titles.clear();
@@ -59,9 +74,11 @@ pub async fn stop(
ctx.0.status = MagbotStatus::Sleeping;
// send message that magbot is no longer listening
let mentions = Mentions::with_user_ids(ids);
let msg = RoomMessageEventContent::text_plain(format!(
"{user} Magbot is no longer listening for titles!"
));
))
.add_mentions(mentions);
room.send(msg).await?;
Ok(())
@@ -73,12 +90,18 @@ pub async fn help(
room: &Room,
ctx: Ctx<MagbotContext>,
) -> anyhow::Result<()> {
tracing::debug!("Executing !help command");
let user = event.sender.to_string();
let ids = vec![UserId::parse(user.clone()).unwrap()];
tracing::debug!("Command invoked by: {}", user);
tracing::debug!("Sending help message with current status: {}", ctx.0.status);
// send message with available commands
let mentions = Mentions::with_user_ids(ids);
let msg = RoomMessageEventContent::text_plain(format!(
"{user} available commands are:\n{HELP_MSG}\nBot current status: {}",
ctx.0.status
));
))
.add_mentions(mentions);
room.send(msg).await?;
Ok(())
}
@@ -89,9 +112,12 @@ pub async fn suggest(
room: &Room,
mut ctx: Ctx<MagbotContext>,
) -> anyhow::Result<()> {
// save the title into memory with author
tracing::debug!("Executing !suggest command");
let user = event.sender.to_string();
tracing::debug!("Command invoked by: {}", user);
match ctx.0.status {
MagbotStatus::Running => {
tracing::info!("Processing title suggestion");
// grab the index of the first <space> character
let start_index = event
.content
@@ -100,8 +126,13 @@ pub async fn suggest(
.position(|c| c == ' ')
.unwrap_or(8); // start at 8 if no space since '!suggest' is 8 chars
let suggestion_str = &event.content.body()[start_index + 1..];
tracing::debug!("Extracted suggestion: {}", suggestion_str);
let suggestion = TitleSuggestion::new(event.sender.as_ref(), suggestion_str);
ctx.0.titles.push(suggestion);
tracing::info!(
"Title suggestion saved, total titles: {}",
ctx.0.titles.len()
);
let reaction = ReactionEventContent::new(Annotation::new(
event.event_id.clone(),
"👍".to_string(),
@@ -109,7 +140,7 @@ pub async fn suggest(
room.send(reaction).await?;
}
MagbotStatus::Sleeping => {
let msg = RoomMessageEventContent::text_plain("{user} Magbot is not running");
let msg = RoomMessageEventContent::text_plain(format!("{user} Magbot is not running"));
room.send(msg).await?;
}
}
@@ -123,22 +154,29 @@ pub async fn get_titles(
room: &Room,
mut ctx: Ctx<MagbotContext>,
) -> anyhow::Result<()> {
tracing::debug!("Executing !gettitles command");
let user = event.sender.to_string();
tracing::debug!("Command invoked by: {}", user);
tracing::debug!("Retrieving {} titles", ctx.0.titles.len());
// sends all the titles with an initial reaction to be used for voting
let user = event.sender.to_string();
match ctx.0.status {
MagbotStatus::Running => {
for title in ctx.0.titles {
let id = vec![UserId::parse(&title.user).unwrap()];
let mentions = Mentions::with_user_ids(id);
let msg = RoomMessageEventContent::text_plain(format!(
"{} : {}",
title.user, title.title
));
))
.add_mentions(mentions);
let response = room.send(msg).await?;
let reaction = ReactionEventContent::new(Annotation::new(
response.event_id.clone(),
response.response.event_id.clone(),
"👍".to_string(),
));
room.send(reaction).await?;
ctx.0.message_ids.push(response.event_id);
ctx.0.message_ids.push(response.response.event_id);
}
}
MagbotStatus::Sleeping => {
@@ -155,6 +193,9 @@ pub async fn count_votes(
room: &Room,
ctx: Ctx<MagbotContext>,
) -> anyhow::Result<()> {
tracing::debug!("Executing !countvotes command");
tracing::debug!("Command invoked by: {}", event.sender);
tracing::debug!("Processing {} title messages", ctx.0.message_ids.len());
// count the reactions of the title messages
// send message with winning title
todo!()
@@ -165,6 +206,8 @@ pub async fn not_implemented(
event: &OriginalSyncRoomMessageEvent,
room: &Room,
) -> anyhow::Result<()> {
tracing::debug!("Executing !not_implemented command");
tracing::debug!("Command invoked by: {}", event.sender);
// get '!' to the next '<space>' char and output as a command
let end_index = event
.content
@@ -173,6 +216,7 @@ pub async fn not_implemented(
.position(|c| c == ' ')
.unwrap_or(event.content.body().chars().count());
let att_cmd = &event.content.body()[..end_index];
tracing::debug!("Not implemented command: {}", att_cmd);
let content =
RoomMessageEventContent::text_plain(format!("The command `{att_cmd}` is not implemented"));
room.send(content).await?;
+51 -7
View File
@@ -26,15 +26,59 @@ pub struct MagbotConfig {
pub username: String,
pub password: String,
pub room_id: String,
pub status: MagbotStatus,
// has to be option since getting from configuration file will fail without the field
pub status: Option<MagbotStatus>,
}
impl MagbotConfig {
pub fn from_env() -> anyhow::Result<Self> {
let mut file = File::open("config.yaml").unwrap_or(File::open("config.yaml")?);
let mut file_str = String::new();
file.read_to_string(&mut file_str)?;
let config: MagbotConfig = serde_yml::from_str(&file_str)?;
pub fn from_str(input: &str) -> anyhow::Result<Self> {
let mut config: MagbotConfig = serde_yml::from_str(input)?;
config.status = Some(MagbotStatus::default());
Ok(config)
}
}
impl Default for MagbotConfig {
fn default() -> Self {
Self {
username: String::new(),
password: String::new(),
room_id: String::new(),
status: Some(MagbotStatus::default()),
}
}
}
pub fn read_file() -> anyhow::Result<String> {
let mut file = File::open("config.yml").unwrap_or(File::open("config.yaml")?);
let mut file_str = String::new();
file.read_to_string(&mut file_str)?;
Ok(file_str)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_config() {
let username = "magbot";
let password = "password";
let room_id = "!ajyOBbqoCIbBhEqNPU:matrix.org";
let _lookpack_sec = 86400;
let _lookpack_limit = 400;
let _dump_mode = true;
let _db_name = "";
let _vote_activity_hrs = 6672;
let str = r##"username: "magbot"
password: "password"
room_id: "!asdf:asdf.org"
"##;
let parsed = MagbotConfig::from_str(str).unwrap();
assert_eq!(username, parsed.username);
assert_eq!(password, parsed.password);
assert_eq!(room_id, parsed.room_id);
}
}
+16 -2
View File
@@ -57,49 +57,63 @@ async fn on_room_message(
return Ok(());
};
match &text_content.body {
let body = &text_content.body;
tracing::debug!("Received message: {}", body);
tracing::debug!("Message sender: {}", event.sender);
match body {
// start listening for titles
s if s.to_lowercase().starts_with("!showstart") => {
tracing::info!("Processing command: !showstart");
commands::start(&event, &room, ctx).await?;
}
// stop listening for titles
s if s.to_lowercase().starts_with("!showstop") => {
tracing::info!("Processing command: !showstop");
commands::stop(&event, &room, ctx).await?;
}
// shows available commands
s if s.to_lowercase().starts_with("!help") => {
tracing::info!("Processing command: !help");
commands::help(&event, &room, ctx).await?;
}
// submit a title suggestion
s if s.to_lowercase().starts_with("!suggest") => {
tracing::info!("Processing command: !suggest");
tracing::debug!("Suggestion text: {}", s);
commands::suggest(&event, &room, ctx).await?;
}
// retrieve list of titles
s if s.to_lowercase().starts_with("!gettitles") => {
tracing::info!("Processing command: !gettitles");
commands::get_titles(&event, &room, ctx).await?;
}
// count upvotes on posted suggestions
s if s.to_lowercase().starts_with("!countvotes") => {
tracing::info!("Processing command: !countvotes");
commands::count_votes(&event, &room, ctx).await?;
}
// generate chaotic text using Ollama
s if s.to_lowercase().starts_with("!mayhem") => {
tracing::info!("Processing command: !mayhem");
commands::not_implemented(&event, &room).await?;
}
// output the voting URL where users can cast votes
s if s.to_lowercase().starts_with("!voteurl") => {
tracing::info!("Processing command: !voteurl");
commands::not_implemented(&event, &room).await?;
}
// message is not a command; ie just a regular message
_ => {
tracing::debug!("Message not a recognized command, ignoring");
return Ok(());
}
};
@@ -111,7 +125,7 @@ pub async fn login_and_sync(
username: String,
password: String,
) -> anyhow::Result<()> {
let context = MagbotContext::default();
let mut context = MagbotContext::default();
// Note that when encryption is enabled, you should use a persistent store to be
// able to restore the session with a working encryption setup.