From afa23f9daebf554d5c5ff5a87a08269fe7878bec Mon Sep 17 00:00:00 2001 From: Daniel Olsen Date: Sun, 20 Oct 2024 03:14:35 +0200 Subject: [PATCH] Add Create functionality --- Cargo.lock | 22 ++-- src/ctf/add.rs | 85 +++++++++++++++ src/ctf/common.rs | 174 +++++++++++++++++++++++++++++++ src/ctf/create.rs | 143 +++++-------------------- src/ctf/events/my_room_member.rs | 8 ++ src/ctf/mod.rs | 8 ++ src/ctf/reactions.rs | 18 ++++ src/main.rs | 3 + 8 files changed, 332 insertions(+), 129 deletions(-) create mode 100644 src/ctf/add.rs create mode 100644 src/ctf/common.rs create mode 100644 src/ctf/reactions.rs diff --git a/Cargo.lock b/Cargo.lock index 474aa2c..1b5fb40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -226,9 +226,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backoff" @@ -370,9 +370,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.21" +version = "1.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" +checksum = "9540e661f81799159abee814118cc139a2004b3a3aa3ea37724a1b66530b90e0" dependencies = [ "shlex", ] @@ -2103,9 +2103,9 @@ checksum = "072cfe5b1d2dcd38d20e18f85e9c9978b6cc08f0b373e9f1fff1541335622974" [[package]] name = "redox_syscall" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62871f2d65009c0256aed1b9cfeeb8ac272833c404e13d53d400cd0dad7a2ac0" +checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" dependencies = [ "bitflags 2.6.0", ] @@ -2498,9 +2498,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -2832,7 +2832,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.19", + "winnow 0.6.20", ] [[package]] @@ -3331,9 +3331,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/src/ctf/add.rs b/src/ctf/add.rs new file mode 100644 index 0000000..926c68b --- /dev/null +++ b/src/ctf/add.rs @@ -0,0 +1,85 @@ +use lazy_regex::regex_captures; + +use matrix_sdk::ruma::events::room::message::{ + MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent, +}; +use matrix_sdk::{Client, Room, RoomState}; + +use super::common::*; +use super::events::my_room_member::FiskenType; + +pub async fn add(event: OriginalSyncRoomMessageEvent, room: Room, client: Client) { + if room.state() != RoomState::Joined { + return; + } + + let MessageType::Text(text_content) = &event.content.msgtype else { + return; + }; + + if !text_content.body.starts_with("!add") { + return; + } + + let (_, name) = regex_captures!(r#"^!add (.+)$"#, &text_content.body,).unwrap(); + let name = name.to_owned(); + if name == "" { + let content = RoomMessageEventContent::text_plain("command: !add "); + room.send(content).await.unwrap(); + } + + tokio::spawn(actually_add(name, event, room, client)); +} + +async fn actually_add( + name: String, + event: OriginalSyncRoomMessageEvent, + room: Room, + client: Client, +) { + let member = match get_room_membership(&room, &client).await { + Ok(m) => m, + Err(e) => { + println!("{}: {e}", room.room_id()); + return; + } + }; + + if member.fisken_type != Some(FiskenType::Command) { + return; + } + + let main = match member.parent.and_then(|rid| client.get_room(&rid)) { + Some(r) => r, + None => { + println!("{}: couldn't get main room", room.room_id()); + return; + } + }; + + let task = match create_task(&name, &main, client.clone()).await { + Ok(t) => t, + Err(e) => { + println!("{}: couldn't create task room: {e}", room.room_id()); + return; + } + }; + + let _ = task.invite_user_by_id(&event.sender).await; + + let reactors = get_reactors( + client.clone(), + &room, + member.fisken_command_info.unwrap(), + "🔔".to_string(), + ) + .await; +} + +pub async fn create_task(name: &str, parent: &Room, client: Client) -> anyhow::Result { + let task = create_room(name, parent, client.clone(), None).await?; + + set_space_child(&client, parent, task.room_id(), Some("01".into()), true).await?; + + Ok(task) +} diff --git a/src/ctf/common.rs b/src/ctf/common.rs new file mode 100644 index 0000000..c05730e --- /dev/null +++ b/src/ctf/common.rs @@ -0,0 +1,174 @@ +use anyhow::{bail, Result}; +use js_int::Int; +use tokio::time::{sleep, Duration}; + +use super::events::my_room_member::RoomMemberEventContent as myRoomMemberEventContent; +use matrix_sdk::ruma::api::client::relations::get_relating_events_with_rel_type_and_event_type; +use matrix_sdk::ruma::api::client::room::create_room::v3::Request as CreateRoomRequest; +use matrix_sdk::{ + deserialized_responses::{RawSyncOrStrippedState, SyncOrStrippedState}, + ruma::{ + api::client::{room::create_room::v3::CreationContent, state::send_state_event}, + events::{ + reaction::ReactionEvent, + relation::RelationType, + room::{ + join_rules::{AllowRule, JoinRule, Restricted, RoomJoinRulesEventContent}, + power_levels::RoomPowerLevelsEventContent, + }, + space::child::SpaceChildEventContent, + InitialStateEvent, SyncStateEvent, TimelineEventType, + }, + room::RoomType, + serde::Raw, + OwnedEventId, OwnedUserId, RoomId, + }, + Client, Room, +}; + +pub async fn create_room( + name: &str, + parent: &Room, + client: Client, + room_type: Option, +) -> anyhow::Result { + println!("Creating room: {name}"); + // Create a room + let mut creation_content = CreationContent::new(); + creation_content.room_type = room_type; + + // Allow members of the room where !create was issued to join the space + let restriction = Restricted::new(vec![AllowRule::room_membership( + parent.room_id().to_owned(), + )]); + let join_rule = JoinRule::Restricted(restriction); + let join_rule_content = RoomJoinRulesEventContent::new(join_rule); + let join_rule_state = InitialStateEvent::::new(join_rule_content); + + // Copy PLs from the parent room + let mut delay = 2; + let mut parent_pl = loop { + let maybe_parent_pl = parent + .get_state_event_static::() + .await?; + + if let Some(raw) = maybe_parent_pl { + let pl = match raw.deserialize()? { + SyncOrStrippedState::Sync(SyncStateEvent::Original(ev)) => ev.content, + _ => todo!(), + }; + + break pl; + } + + println!("Couldn't get PLs, waiting {delay}: {:?}", maybe_parent_pl); + sleep(Duration::from_secs(delay)).await; + delay *= 2; + + if delay > 3600 { + bail!("Couldn't get power-level event from parent-room"); + } + }; + + // Also make the bot Admin in the space + parent_pl.users.insert( + client.user_id().unwrap().to_owned(), + Int::new_saturating(100), + ); + let mut plec = RoomPowerLevelsEventContent::new(); + plec.users = parent_pl.users; + + // Actually create the room + let mut request = CreateRoomRequest::new(); + request.creation_content = Some(Raw::new(&creation_content).unwrap()); + request.initial_state = vec![join_rule_state.to_raw_any()]; + request.name = Some(name.to_string()); + request.power_level_content_override = Some(Raw::new(&plec).unwrap()); + + let room = client.create_room(request).await?; + + // Get the pfp and display name of the bot from the !create room + let parent_member = get_room_membership(&parent, &client).await?; + + // Get member event to edit + let mut new_member_event = get_room_membership(&room, &client).await?; + + new_member_event.avatar_url = parent_member.avatar_url; + new_member_event.displayname = parent_member.displayname; + + room.send_state_event_for_key(client.user_id().unwrap(), new_member_event) + .await?; + + return Ok(room); +} + +pub async fn get_room_membership(room: &Room, client: &Client) -> Result { + room.sync_up().await; + let member = match room + .get_state_event_static_for_key::( + &client.user_id().unwrap().to_owned(), + ) + .await? + { + Some(RawSyncOrStrippedState::Sync(r)) => match r.deserialize()? { + SyncStateEvent::Original(e) => e.content, + _ => todo!(), + }, + _ => todo!(), + }; + + Ok(member) +} + +pub async fn set_space_child( + client: &Client, + parent: &Room, + child: &RoomId, + order: Option, + suggested: bool, +) -> Result { + let mut space_child_event = + SpaceChildEventContent::new(vec![client.user_id().unwrap().server_name().to_owned()]); + space_child_event.order = order; + space_child_event.suggested = suggested; + + Ok(parent + .send_state_event_for_key(child, space_child_event) + .await?) +} + +pub async fn get_reactors( + client: Client, + room: &Room, + message: OwnedEventId, + key: String, +) -> Result> { + let mut request = get_relating_events_with_rel_type_and_event_type::v1::Request::new( + room.room_id().to_owned(), + message.to_owned(), + RelationType::Annotation, + TimelineEventType::Reaction, + ); + + let mut response = client.send(request.clone(), None).await?; + let mut reactors = Vec::::new(); + loop { + { + for raw in response.chunk { + let reaction = raw.deserialize_as::()?; + let event = reaction.as_original().unwrap().to_owned(); + if event.content.relates_to.key == key { + reactors.push(event.sender); + } + } + + if response.next_batch.is_none() { + break; + } + } + request.from = response.next_batch; + response = client.send(request.clone(), None).await?; + } + + Ok(reactors) +} diff --git a/src/ctf/create.rs b/src/ctf/create.rs index fbe9373..a0b9cce 100644 --- a/src/ctf/create.rs +++ b/src/ctf/create.rs @@ -1,26 +1,15 @@ -use anyhow::bail; use anyhow::Result; use lazy_regex::regex_captures; -use tokio::time::{sleep, Duration}; -use matrix_sdk::ruma::events::space::child::SpaceChildEventContent; use matrix_sdk::deserialized_responses::RawSyncOrStrippedState; -use matrix_sdk::deserialized_responses::SyncOrStrippedState; -use matrix_sdk::ruma::api::client::room::create_room::v3::CreationContent; -use matrix_sdk::ruma::api::client::room::create_room::v3::Request as CreateRoomRequest; -use matrix_sdk::ruma::events::room::join_rules::AllowRule; -use matrix_sdk::ruma::events::room::join_rules::JoinRule; -use matrix_sdk::ruma::events::room::join_rules::Restricted; -use matrix_sdk::ruma::events::room::join_rules::RoomJoinRulesEventContent; +use matrix_sdk::ruma::events::reaction::ReactionEventContent; +use matrix_sdk::ruma::events::relation::Annotation; use matrix_sdk::ruma::events::room::message::{ MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent, }; -use matrix_sdk::ruma::events::room::power_levels::RoomPowerLevelsEventContent; -use matrix_sdk::ruma::events::InitialStateEvent; + use matrix_sdk::ruma::events::SyncStateEvent; use matrix_sdk::ruma::room::RoomType; -use matrix_sdk::ruma::serde::Raw; -use matrix_sdk::ruma::Int; use matrix_sdk::{Client, Room, RoomState}; use super::events; @@ -77,107 +66,23 @@ pub async fn actually_create( room.send(content).await.unwrap(); } -async fn create_room( - name: &str, - parent: &Room, - client: Client, - room_type: Option, -) -> anyhow::Result { - println!("Creating room: {name}"); - // Create a room - let mut creation_content = CreationContent::new(); - creation_content.room_type = room_type; - - // Allow members of the room where !create was issued to join the space - let restriction = Restricted::new(vec![AllowRule::room_membership( - parent.room_id().to_owned(), - )]); - let join_rule = JoinRule::Restricted(restriction); - let join_rule_content = RoomJoinRulesEventContent::new(join_rule); - let join_rule_state = InitialStateEvent::::new(join_rule_content); - - // Copy PLs from the parent room - let mut delay = 2; - let mut parent_pl = loop { - let maybe_parent_pl = parent - .get_state_event_static::() - .await?; - - if let Some(raw) = maybe_parent_pl { - let pl = match raw.deserialize()? { - SyncOrStrippedState::Sync(SyncStateEvent::Original(ev)) => ev.content, - _ => todo!(), - }; - - break pl; - } - - println!("Couldn't get PLs, waiting {delay}: {:?}", maybe_parent_pl); - sleep(Duration::from_secs(delay)).await; - delay *= 2; - - if delay > 3600 { - bail!("Couldn't get power-level event from parent-room"); - } - }; - - // Also make the bot Admin in the space - parent_pl.users.insert( - client.user_id().unwrap().to_owned(), - Int::new_saturating(100), - ); - let mut plec = RoomPowerLevelsEventContent::new(); - plec.users = parent_pl.users; - - // Actually create the room - let mut request = CreateRoomRequest::new(); - request.creation_content = Some(Raw::new(&creation_content).unwrap()); - request.initial_state = vec![join_rule_state.to_raw_any()]; - request.name = Some(name.to_string()); - request.power_level_content_override = Some(Raw::new(&plec).unwrap()); - - let room = client.create_room(request).await?; - - // Get the pfp and display name of the bot from the !create room - let parent_member = match parent - .get_state_event_static_for_key::( - &client.user_id().unwrap().to_owned(), - ) - .await? - { - Some(RawSyncOrStrippedState::Sync(r)) => match r.deserialize()? { - SyncStateEvent::Original(e) => e.content, - _ => todo!(), - }, - _ => todo!(), - }; - - // Get member event to edit - let mut new_member_event = match parent - .get_state_event_static_for_key::( - &client.user_id().unwrap().to_owned(), - ) - .await? - { - Some(RawSyncOrStrippedState::Sync(r)) => match r.deserialize()? { - SyncStateEvent::Original(e) => e.content, - _ => todo!(), - }, - _ => todo!(), - }; - - new_member_event.avatar_url = parent_member.avatar_url; - new_member_event.displayname = parent_member.displayname; - - room.send_state_event_for_key(client.user_id().unwrap(), new_member_event) - .await?; - - return Ok(room); -} +use super::common::*; async fn create_command(name: &str, parent: &Room, client: Client) -> Result { let command_room = create_room(name, parent, client.clone(), None).await?; + // Inform about the command_room + let info_content = RoomMessageEventContent::text_plain(format!( + "This is the command room, you can issue commands like !add to make a new room for a challenge, \ + or !archive to archive the whole space.\n\ + React to this message with a bell to automatically get invited to new challenge-rooms" + )); + let info_event = command_room.send(info_content).await?.event_id; + + let react_annotation = Annotation::new(info_event.clone(), "🔔".into()); + let react_content = ReactionEventContent::new(react_annotation); + command_room.send(react_content).await?; + // Get settings from room that issued !create let parent_member = match parent .get_state_event_static_for_key::( @@ -208,6 +113,7 @@ async fn create_command(name: &str, parent: &Room, client: Client) -> Result Result main.sync_up().await; let command_room = create_command(&format!("🐠 - {name}"), &main, client.clone()).await?; - let mut space_child_command = - SpaceChildEventContent::new(vec![client.user_id().unwrap().server_name().to_owned()]); - space_child_command.order = Some("00".into()); - space_child_command.suggested = true; - - main.send_state_event_for_key(command_room.room_id(), space_child_command) - .await?; + let _ = set_space_child( + &client, + &main, + command_room.room_id(), + Some("00".into()), + true, + ) + .await; Ok(main) } diff --git a/src/ctf/events/my_room_member.rs b/src/ctf/events/my_room_member.rs index ca9beda..331b907 100644 --- a/src/ctf/events/my_room_member.rs +++ b/src/ctf/events/my_room_member.rs @@ -3,6 +3,7 @@ // License: MIT use matrix_sdk::ruma::events::macros::EventContent; +use matrix_sdk::ruma::OwnedEventId; use matrix_sdk::ruma::OwnedMxcUri; use matrix_sdk::ruma::OwnedRoomId; use matrix_sdk::ruma::OwnedUserId; @@ -119,6 +120,12 @@ pub struct RoomMemberEventContent { skip_serializing_if = "Option::is_none" )] pub fisken_state: Option, + + #[serde( + rename = "xyz.dandellion.fisken.command_info", + skip_serializing_if = "Option::is_none" + )] + pub fisken_command_info: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -158,6 +165,7 @@ impl RoomMemberEventContent { archival_space: None, active_space: None, fisken_state: None, + fisken_command_info: None, } } } diff --git a/src/ctf/mod.rs b/src/ctf/mod.rs index 508e3fe..97f100c 100644 --- a/src/ctf/mod.rs +++ b/src/ctf/mod.rs @@ -1,4 +1,12 @@ mod create; pub use create::*; +mod add; +pub use add::*; + +mod reactions; +pub use reactions::*; + +mod common; + mod events; diff --git a/src/ctf/reactions.rs b/src/ctf/reactions.rs new file mode 100644 index 0000000..c0db326 --- /dev/null +++ b/src/ctf/reactions.rs @@ -0,0 +1,18 @@ +use matrix_sdk::ruma::events::reaction::OriginalSyncReactionEvent; +use matrix_sdk::{Client, Room, RoomState}; + +pub async fn reactions(event: OriginalSyncReactionEvent, room: Room, client: Client) { + if room.state() != RoomState::Joined { + return; + } + + if event.sender == client.user_id().unwrap() { + return; + } + + tokio::spawn(actually_reactions(event, room, client)); +} + +async fn actually_reactions(event: OriginalSyncReactionEvent, room: Room, client: Client) { + return; +} diff --git a/src/main.rs b/src/main.rs index 0d4a7e5..715f0e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,6 +56,9 @@ async fn main() -> anyhow::Result<()> { client.add_event_handler(invite::on_stripped_state_member); client.add_event_handler(ctf::create); + client.add_event_handler(ctf::add); + + client.add_event_handler(ctf::reactions); // let sync_settings = SyncSettings::new().full_state(true); let sync_settings = SyncSettings::new();