use std::any::Any;
use botkit_core::action::AnyChatActionSender;
use botkit_core::{ContextData, OptionValue};
use matrix_sdk::Room;
use matrix_sdk::ruma::events::reaction::OriginalSyncReactionEvent;
use matrix_sdk::ruma::events::room::message::{MessageType, OriginalSyncRoomMessageEvent};
use crate::action::MatrixActionSender;
use crate::client::MatrixClient;
pub fn reaction_button_id(emoji: &str) -> String {
format!("reaction:{emoji}")
}
pub struct MatrixContextData {
room: Room,
client: MatrixClient,
room_id: String,
user_id: String,
user_name: String,
command_name: Option<String>,
command_args: Option<String>,
button_id: Option<String>,
message_content: Option<String>,
}
impl MatrixContextData {
pub fn from_message(
event: &OriginalSyncRoomMessageEvent,
room: Room,
client: MatrixClient,
command_prefix: &str,
) -> Self {
let room_id = room.room_id().to_string();
let user_id = event.sender.to_string();
let user_name = event.sender.localpart().to_string();
let message_content = match &event.content.msgtype {
MessageType::Text(text) => Some(text.body.clone()),
_ => None,
};
let (command_name, command_args) =
Self::parse_command(message_content.as_deref(), command_prefix);
Self {
room,
client,
room_id,
user_id,
user_name,
command_name,
command_args,
button_id: None,
message_content,
}
}
pub fn from_reaction(
event: &OriginalSyncReactionEvent,
room: Room,
client: MatrixClient,
) -> Self {
let room_id = room.room_id().to_string();
let user_id = event.sender.to_string();
let user_name = event.sender.localpart().to_string();
let button_id = Some(reaction_button_id(&event.content.relates_to.key));
Self {
room,
client,
room_id,
user_id,
user_name,
command_name: None,
command_args: None,
button_id,
message_content: None,
}
}
fn parse_command(text: Option<&str>, prefix: &str) -> (Option<String>, Option<String>) {
if prefix.is_empty() {
return (None, None);
}
let Some(rest) = text.and_then(|t| t.strip_prefix(prefix)) else {
return (None, None);
};
let mut parts = rest.splitn(2, char::is_whitespace);
let name = parts.next().unwrap_or_default();
if name.is_empty() {
return (None, None);
}
let args = parts.next().map(str::trim).filter(|a| !a.is_empty());
(Some(name.to_string()), args.map(str::to_string))
}
pub fn room(&self) -> &Room {
&self.room
}
pub fn client(&self) -> &MatrixClient {
&self.client
}
pub fn command(&self) -> Option<&str> {
self.command_name.as_deref()
}
pub fn message_text(&self) -> Option<&str> {
self.message_content.as_deref()
}
}
impl ContextData for MatrixContextData {
fn channel_id(&self) -> &str {
&self.room_id
}
fn user_id(&self) -> &str {
&self.user_id
}
fn user_name(&self) -> &str {
&self.user_name
}
fn command_name(&self) -> Option<&str> {
self.command_name.as_deref()
}
fn command_args(&self) -> Option<&str> {
self.command_args.as_deref()
}
fn option(&self, _name: &str) -> Option<OptionValue> {
None
}
fn button_id(&self) -> Option<&str> {
self.button_id.as_deref()
}
fn message_content(&self) -> Option<&str> {
self.message_content.as_deref()
}
fn as_any(&self) -> &dyn Any {
self
}
fn action_sender(&self) -> Option<AnyChatActionSender> {
Some(AnyChatActionSender::new(MatrixActionSender::new(
self.room.clone(),
)))
}
}
#[cfg(test)]
mod tests {
use super::MatrixContextData;
fn parse(text: &str, prefix: &str) -> (Option<String>, Option<String>) {
MatrixContextData::parse_command(Some(text), prefix)
}
#[test]
fn parses_a_command_with_arguments() {
let (name, args) = parse("!greet world and beyond", "!");
assert_eq!(name.as_deref(), Some("greet"));
assert_eq!(args.as_deref(), Some("world and beyond"));
}
#[test]
fn parses_a_bare_command() {
assert_eq!(parse("!ping", "!"), (Some("ping".into()), None));
}
#[test]
fn trailing_whitespace_is_not_an_argument() {
assert_eq!(parse("!ping ", "!"), (Some("ping".into()), None));
}
#[test]
fn multi_character_prefixes_work() {
assert_eq!(
parse(">>ping now", ">>"),
(Some("ping".into()), Some("now".into()))
);
}
#[test]
fn messages_without_the_prefix_are_not_commands() {
assert_eq!(parse("ping", "!"), (None, None));
assert_eq!(parse("hey !ping", "!"), (None, None));
}
#[test]
fn a_bare_prefix_is_not_a_command() {
assert_eq!(parse("!", "!"), (None, None));
assert_eq!(parse("! ping", "!"), (None, None));
}
#[test]
fn an_empty_prefix_does_not_make_everything_a_command() {
assert_eq!(parse("hello", ""), (None, None));
}
#[test]
fn non_ascii_prefixes_split_on_character_boundaries() {
assert_eq!(
parse("🤖ping now", "🤖"),
(Some("ping".into()), Some("now".into()))
);
}
#[test]
fn absent_text_is_not_a_command() {
assert_eq!(MatrixContextData::parse_command(None, "!"), (None, None));
}
}