Upgrade to Matrix SDK latest (Store Rewrite) and Tokio 1.0

This upgrade introduces a handful of breaking changes in the Rust
Matrix SDK.
 - Some types have disappeared and changed name.
 - Some functions are no longer async.
 - Room display name now has a Result type instead of just returning
   the value.
 - Client state store has breaking changes (not really a big deal).

This required introduction of a new type to store room information
that we are interested in on the context struct. This new RoomContext
is required mostly due to unit tests, because it is no longer possible
to instantiate the Room type in the Matrix SDK.
This commit is contained in:
projectmoon 2021-01-29 22:35:07 +00:00
parent a4cdad4936
commit 1b0003ff1b
15 changed files with 551 additions and 1053 deletions

1309
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -37,5 +37,5 @@ version = "1"
features = ['derive'] features = ['derive']
[dependencies.tokio] [dependencies.tokio]
version = "0.2" version = "1.0"
features = [ "full" ] features = [ "full" ]

View File

@ -1,18 +1,8 @@
use chronicle_dicebot::commands; use chronicle_dicebot::commands;
use chronicle_dicebot::context::Context; use chronicle_dicebot::context::{Context, RoomContext};
use chronicle_dicebot::db::Database; use chronicle_dicebot::db::Database;
use chronicle_dicebot::error::BotError; use chronicle_dicebot::error::BotError;
use matrix_sdk::{ use matrix_sdk::identifiers::room_id;
identifiers::{room_id, user_id},
Room,
};
fn dummy_room() -> Room {
Room::new(
&room_id!("!fakeroomid:example.com"),
&user_id!("@fakeuserid:example.com"),
)
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), BotError> { async fn main() -> Result<(), BotError> {
@ -26,7 +16,10 @@ async fn main() -> Result<(), BotError> {
db: Database::new_temp()?, db: Database::new_temp()?,
matrix_client: &matrix_sdk::Client::new("http://example.com") matrix_client: &matrix_sdk::Client::new("http://example.com")
.expect("Could not create matrix client"), .expect("Could not create matrix client"),
room: &dummy_room(), room: RoomContext {
id: &room_id!("!fakeroomid:example.com"),
display_name: "fake room",
},
username: "@localuser:example.com", username: "@localuser:example.com",
message_body: &input, message_body: &input,
}; };

View File

@ -1,6 +1,6 @@
use crate::commands::execute_command; use crate::commands::execute_command;
use crate::config::*; use crate::config::*;
use crate::context::Context; use crate::context::{Context, RoomContext};
use crate::db::Database; use crate::db::Database;
use crate::error::BotError; use crate::error::BotError;
use crate::state::DiceBotState; use crate::state::DiceBotState;
@ -13,7 +13,7 @@ use matrix_sdk::{
room::message::{MessageEventContent, NoticeMessageEventContent}, room::message::{MessageEventContent, NoticeMessageEventContent},
AnyMessageEventContent, AnyMessageEventContent,
}, },
Client, ClientConfig, JsonStore, Room, SyncSettings, Client, ClientConfig, JoinedRoom, SyncSettings,
}; };
//use matrix_sdk_common_macros::async_trait; //use matrix_sdk_common_macros::async_trait;
use std::clone::Clone; use std::clone::Clone;
@ -48,8 +48,8 @@ fn cache_dir() -> Result<PathBuf, BotError> {
/// Creates the matrix client. /// Creates the matrix client.
fn create_client(config: &Config) -> Result<Client, BotError> { fn create_client(config: &Config) -> Result<Client, BotError> {
let cache_dir = cache_dir()?; let cache_dir = cache_dir()?;
let store = JsonStore::open(&cache_dir)?; //let store = JsonStore::open(&cache_dir)?;
let client_config = ClientConfig::new().state_store(Box::new(store)); let client_config = ClientConfig::new().store_path(cache_dir);
let homeserver_url = Url::parse(&config.matrix_homeserver())?; let homeserver_url = Url::parse(&config.matrix_homeserver())?;
Ok(Client::new_with_config(homeserver_url, client_config)?) Ok(Client::new_with_config(homeserver_url, client_config)?)
@ -90,7 +90,7 @@ impl DiceBot {
let password = &self.config.matrix_password(); let password = &self.config.matrix_password();
//TODO provide a device id from config. //TODO provide a device id from config.
let mut client = self.client.clone(); let client = self.client.clone();
client client
.login(username, password, None, Some("matrix dice bot")) .login(username, password, None, Some("matrix dice bot"))
.await?; .await?;
@ -118,9 +118,9 @@ impl DiceBot {
Ok(()) Ok(())
} }
async fn execute_commands(&self, room: &Room, sender_username: &str, msg_body: &str) { async fn execute_commands(&self, room: &JoinedRoom, sender_username: &str, msg_body: &str) {
let room_name = room.display_name().clone(); let room_name = room.display_name().await.ok().unwrap_or_default();
let room_id = room.room_id.clone(); let room_id = room.room_id().clone();
let mut results = Vec::with_capacity(msg_body.lines().count()); let mut results = Vec::with_capacity(msg_body.lines().count());
@ -130,7 +130,7 @@ impl DiceBot {
let ctx = Context { let ctx = Context {
db: self.db.clone(), db: self.db.clone(),
matrix_client: &self.client, matrix_client: &self.client,
room: room, room: RoomContext::new_with_name(&room, &room_name),
username: &sender_username, username: &sender_username,
message_body: &command, message_body: &command,
}; };

View File

@ -11,7 +11,8 @@ use matrix_sdk::{
room::message::{MessageEventContent, TextMessageEventContent}, room::message::{MessageEventContent, TextMessageEventContent},
StrippedStateEvent, SyncMessageEvent, SyncStateEvent, StrippedStateEvent, SyncMessageEvent, SyncStateEvent,
}, },
EventEmitter, SyncRoom, identifiers::RoomId,
EventEmitter, RoomState,
}; };
use std::clone::Clone; use std::clone::Clone;
use std::ops::Sub; use std::ops::Sub;
@ -92,17 +93,37 @@ fn should_process_event(db: &Database, room_id: &str, event_id: &str) -> bool {
}) })
} }
/// Convert room state object to the room ID and display name, if
/// possible. We only care about the room if it is a joined or left
/// room.
async fn convert_room_state(state: &RoomState) -> Option<(&RoomId, String)> {
match state {
RoomState::Joined(room) => Some((
room.room_id(),
room.display_name().await.ok().unwrap_or_default(),
)),
RoomState::Left(room) => Some((
room.room_id(),
room.display_name().await.ok().unwrap_or_default(),
)),
_ => None,
}
}
/// This event emitter listens for messages with dice rolling commands. /// This event emitter listens for messages with dice rolling commands.
/// Originally adapted from the matrix-rust-sdk examples. /// Originally adapted from the matrix-rust-sdk examples.
#[async_trait] #[async_trait]
impl EventEmitter for DiceBot { impl EventEmitter for DiceBot {
async fn on_room_member(&self, room: SyncRoom, event: &SyncStateEvent<MemberEventContent>) { async fn on_room_member(&self, state: RoomState, event: &SyncStateEvent<MemberEventContent>) {
if let SyncRoom::Joined(room) | SyncRoom::Left(room) = room { let (room_id, room_display_name) = match convert_room_state(&state).await {
//Clone to avoid holding lock. Some((room_id, room_display_name)) => (room_id, room_display_name),
let room = room.read().await.clone(); _ => return,
let (room_id, username) = (room.room_id.as_str(), &event.state_key); };
if !should_process_event(&self.db, room_id, event.event_id.as_str()) { let room_id_str = room_id.as_str();
let username = &event.state_key;
if !should_process_event(&self.db, room_id_str, event.event_id.as_str()) {
return; return;
} }
@ -121,16 +142,23 @@ impl EventEmitter for DiceBot {
let result = if event_affects_us && !adding_user { let result = if event_affects_us && !adding_user {
info!("Clearing all information for room ID {}", room_id); info!("Clearing all information for room ID {}", room_id);
self.db.rooms.clear_info(room_id) self.db.rooms.clear_info(room_id_str)
} else if event_affects_us && adding_user { } else if event_affects_us && adding_user {
info!("Joined room {}; recording room information", room_id); info!("Joined room {}; recording room information", room_id);
record_room_information(&self.client, &self.db, &room, &event.state_key).await record_room_information(
&self.client,
&self.db,
&room_id,
&room_display_name,
&event.state_key,
)
.await
} else if !event_affects_us && adding_user { } else if !event_affects_us && adding_user {
info!("Adding user {} to room ID {}", username, room_id); info!("Adding user {} to room ID {}", username, room_id);
self.db.rooms.add_user_to_room(username, room_id) self.db.rooms.add_user_to_room(username, room_id_str)
} else if !event_affects_us && !adding_user { } else if !event_affects_us && !adding_user {
info!("Removing user {} from room ID {}", username, room_id); info!("Removing user {} from room ID {}", username, room_id);
self.db.rooms.remove_user_from_room(username, room_id) self.db.rooms.remove_user_from_room(username, room_id_str)
} else { } else {
debug!("Ignoring a room member event: {:#?}", event); debug!("Ignoring a room member event: {:#?}", event);
Ok(()) Ok(())
@ -142,36 +170,35 @@ impl EventEmitter for DiceBot {
debug!("Successfully processed room member update."); debug!("Successfully processed room member update.");
} }
} }
}
async fn on_stripped_state_member( async fn on_stripped_state_member(
&self, &self,
room: SyncRoom, state: RoomState,
event: &StrippedStateEvent<MemberEventContent>, event: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>, _: Option<MemberEventContent>,
) { ) {
if let SyncRoom::Invited(room) = room { if let RoomState::Invited(room) = state {
if let Some(user_id) = self.client.user_id().await { if let Some(user_id) = self.client.user_id().await {
if event.state_key != user_id { if event.state_key != user_id {
return; return;
} }
} }
//Clone to avoid holding lock. info!("Autojoining room {}", room.display_name().await);
let room = room.read().await.clone();
info!("Autojoining room {}", room.display_name());
if let Err(e) = self.client.join_room_by_id(&room.room_id).await { if let Err(e) = self.client.join_room_by_id(&room.room_id()).await {
warn!("Could not join room: {}", e.to_string()) warn!("Could not join room: {}", e.to_string())
} }
} }
} }
async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) { async fn on_room_message(
if let SyncRoom::Joined(room) = room { &self,
//Clone to avoid holding lock. state: RoomState,
let room = room.read().await.clone(); event: &SyncMessageEvent<MessageEventContent>,
let room_id = room.room_id.as_str(); ) {
if let RoomState::Joined(room) = state {
let room_id = room.room_id().as_str();
if !should_process_event(&self.db, room_id, event.event_id.as_str()) { if !should_process_event(&self.db, room_id, event.event_id.as_str()) {
return; return;
} }

View File

@ -326,12 +326,13 @@ mod tests {
use super::*; use super::*;
use crate::db::Database; use crate::db::Database;
/// Create dummy room instance. macro_rules! dummy_room {
fn dummy_room() -> matrix_sdk::Room { () => {
matrix_sdk::Room::new( crate::context::RoomContext {
&matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"), id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
&matrix_sdk::identifiers::user_id!("@fakeuserid:example.com"), display_name: "displayname",
) }
};
} }
///Instead of being random, generate a series of numbers we have complete ///Instead of being random, generate a series of numbers we have complete
@ -475,7 +476,7 @@ mod tests {
let ctx = Context { let ctx = Context {
db: db, db: db,
matrix_client: &matrix_sdk::Client::new("http://example.com").unwrap(), matrix_client: &matrix_sdk::Client::new("http://example.com").unwrap(),
room: &dummy_room(), room: dummy_room!(),
username: "username", username: "username",
message_body: "message", message_body: "message",
}; };
@ -506,7 +507,7 @@ mod tests {
let ctx = Context { let ctx = Context {
db: db, db: db,
matrix_client: &matrix_sdk::Client::new("http://example.com").unwrap(), matrix_client: &matrix_sdk::Client::new("http://example.com").unwrap(),
room: &dummy_room(), room: dummy_room!(),
username: "username", username: "username",
message_body: "message", message_body: "message",
}; };
@ -536,12 +537,12 @@ mod tests {
let ctx = Context { let ctx = Context {
db: db.clone(), db: db.clone(),
matrix_client: &matrix_sdk::Client::new("http://example.com").unwrap(), matrix_client: &matrix_sdk::Client::new("http://example.com").unwrap(),
room: &dummy_room(), room: dummy_room!(),
username: "username", username: "username",
message_body: "message", message_body: "message",
}; };
let user_and_room = UserAndRoom(&ctx.username, &ctx.room.room_id.as_str()); let user_and_room = UserAndRoom(&ctx.username, &ctx.room.id.as_str());
db.variables db.variables
.set_user_variable(&user_and_room, "myvariable", 10) .set_user_variable(&user_and_room, "myvariable", 10)

View File

@ -78,12 +78,13 @@ pub async fn execute_command(ctx: &Context<'_>) -> CommandResult {
mod tests { mod tests {
use super::*; use super::*;
/// Create a dummy room instance. macro_rules! dummy_room {
fn dummy_room() -> matrix_sdk::Room { () => {
matrix_sdk::Room::new( crate::context::RoomContext {
&matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"), id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
&matrix_sdk::identifiers::user_id!("@fakeuserid:example.com"), display_name: "displayname",
) }
};
} }
#[tokio::test] #[tokio::test]
@ -92,7 +93,7 @@ mod tests {
let ctx = Context { let ctx = Context {
db: db, db: db,
matrix_client: &matrix_sdk::Client::new("http://example.com").unwrap(), matrix_client: &matrix_sdk::Client::new("http://example.com").unwrap(),
room: &dummy_room(), room: dummy_room!(),
username: "myusername", username: "myusername",
message_body: "!notacommand", message_body: "!notacommand",
}; };

View File

@ -19,8 +19,14 @@ impl Command for ResyncCommand {
let our_username: Option<UserId> = ctx.matrix_client.user_id().await; let our_username: Option<UserId> = ctx.matrix_client.user_id().await;
let our_username: &str = our_username.as_ref().map_or("", UserId::as_str); let our_username: &str = our_username.as_ref().map_or("", UserId::as_str);
let result: ResyncResult = let result: ResyncResult = record_room_information(
record_room_information(ctx.matrix_client, &ctx.db, &ctx.room, our_username).await; ctx.matrix_client,
&ctx.db,
ctx.room.id,
&ctx.room.display_name,
our_username,
)
.await;
let (plain, html) = match result { let (plain, html) = match result {
Ok(()) => { Ok(()) => {

View File

@ -13,7 +13,7 @@ impl Command for GetAllVariablesCommand {
} }
async fn execute(&self, ctx: &Context<'_>) -> Execution { async fn execute(&self, ctx: &Context<'_>) -> Execution {
let key = UserAndRoom(&ctx.username, &ctx.room.room_id.as_str()); let key = UserAndRoom(&ctx.username, &ctx.room.id.as_str());
let result = ctx.db.variables.get_user_variables(&key); let result = ctx.db.variables.get_user_variables(&key);
let value = match result { let value = match result {
@ -48,7 +48,7 @@ impl Command for GetVariableCommand {
async fn execute(&self, ctx: &Context<'_>) -> Execution { async fn execute(&self, ctx: &Context<'_>) -> Execution {
let name = &self.0; let name = &self.0;
let key = UserAndRoom(&ctx.username, &ctx.room.room_id.as_str()); let key = UserAndRoom(&ctx.username, &ctx.room.id.as_str());
let result = ctx.db.variables.get_user_variable(&key, name); let result = ctx.db.variables.get_user_variable(&key, name);
let value = match result { let value = match result {
@ -74,7 +74,7 @@ impl Command for SetVariableCommand {
async fn execute(&self, ctx: &Context<'_>) -> Execution { async fn execute(&self, ctx: &Context<'_>) -> Execution {
let name = &self.0; let name = &self.0;
let value = self.1; let value = self.1;
let key = UserAndRoom(&ctx.username, ctx.room.room_id.as_str()); let key = UserAndRoom(&ctx.username, ctx.room.id.as_str());
let result = ctx.db.variables.set_user_variable(&key, name, value); let result = ctx.db.variables.set_user_variable(&key, name, value);
let content = match result { let content = match result {
@ -98,7 +98,7 @@ impl Command for DeleteVariableCommand {
async fn execute(&self, ctx: &Context<'_>) -> Execution { async fn execute(&self, ctx: &Context<'_>) -> Execution {
let name = &self.0; let name = &self.0;
let key = UserAndRoom(&ctx.username, ctx.room.room_id.as_str()); let key = UserAndRoom(&ctx.username, ctx.room.id.as_str());
let result = ctx.db.variables.delete_user_variable(&key, name); let result = ctx.db.variables.delete_user_variable(&key, name);
let value = match result { let value = match result {

View File

@ -1,6 +1,7 @@
use crate::db::Database; use crate::db::Database;
use matrix_sdk::identifiers::RoomId;
use matrix_sdk::Client; use matrix_sdk::Client;
use matrix_sdk::Room; use matrix_sdk::JoinedRoom;
/// A context carried through the system providing access to things /// A context carried through the system providing access to things
/// like the database. /// like the database.
@ -8,7 +9,22 @@ use matrix_sdk::Room;
pub struct Context<'a> { pub struct Context<'a> {
pub db: Database, pub db: Database,
pub matrix_client: &'a Client, pub matrix_client: &'a Client,
pub room: &'a Room, pub room: RoomContext<'a>,
pub username: &'a str, pub username: &'a str,
pub message_body: &'a str, pub message_body: &'a str,
} }
#[derive(Clone)]
pub struct RoomContext<'a> {
pub id: &'a RoomId,
pub display_name: &'a str,
}
impl RoomContext<'_> {
pub fn new_with_name<'a>(room: &'a JoinedRoom, display_name: &'a str) -> RoomContext<'a> {
RoomContext {
id: room.room_id(),
display_name,
}
}
}

View File

@ -343,12 +343,13 @@ mod tests {
use crate::db::Database; use crate::db::Database;
use crate::parser::{Amount, Element, Operator}; use crate::parser::{Amount, Element, Operator};
/// Create a dummy room instance. macro_rules! dummy_room {
fn dummy_room() -> matrix_sdk::Room { () => {
matrix_sdk::Room::new( crate::context::RoomContext {
&matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"), id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
&matrix_sdk::identifiers::user_id!("@fakeuserid:example.com"), display_name: "displayname",
) }
};
} }
/// Generate a series of numbers manually for testing. For this /// Generate a series of numbers manually for testing. For this
@ -391,7 +392,7 @@ mod tests {
let ctx = Context { let ctx = Context {
db: db, db: db,
matrix_client: &matrix_sdk::Client::new("https://example.com").unwrap(), matrix_client: &matrix_sdk::Client::new("https://example.com").unwrap(),
room: &dummy_room(), room: dummy_room!(),
username: "username", username: "username",
message_body: "message", message_body: "message",
}; };

View File

@ -9,7 +9,7 @@ use futures::stream::{self, StreamExt, TryStreamExt};
//New hotness //New hotness
pub async fn calculate_dice_amount(amounts: &[Amount], ctx: &Context<'_>) -> Result<i32, BotError> { pub async fn calculate_dice_amount(amounts: &[Amount], ctx: &Context<'_>) -> Result<i32, BotError> {
let stream = stream::iter(amounts); let stream = stream::iter(amounts);
let key = UserAndRoom(&ctx.username, ctx.room.room_id.as_str()); let key = UserAndRoom(&ctx.username, ctx.room.id.as_str());
let variables = &ctx.db.variables.get_user_variables(&key)?; let variables = &ctx.db.variables.get_user_variables(&key)?;
use DiceRollingError::VariableNotFound; use DiceRollingError::VariableNotFound;

View File

@ -27,6 +27,9 @@ pub enum BotError {
#[error("could not parse URL")] #[error("could not parse URL")]
UrlParseError(#[from] url::ParseError), UrlParseError(#[from] url::ParseError),
#[error("error in matrix state store: {0}")]
MatrixStateStoreError(#[from] matrix_sdk::StoreError),
#[error("uncategorized matrix SDK error")] #[error("uncategorized matrix SDK error")]
MatrixError(#[from] matrix_sdk::Error), MatrixError(#[from] matrix_sdk::Error),

View File

@ -1,21 +1,22 @@
use crate::db::errors::DataError; use crate::db::errors::DataError;
use crate::matrix; use crate::matrix;
use crate::models::RoomInfo; use crate::models::RoomInfo;
use matrix_sdk::{self, Client, Room}; use matrix_sdk::{self, identifiers::RoomId, Client};
/// Record the information about a room, including users in it. /// Record the information about a room, including users in it.
pub async fn record_room_information( pub async fn record_room_information(
client: &Client, client: &Client,
db: &crate::db::Database, db: &crate::db::Database,
room: &Room, room_id: &RoomId,
room_display_name: &str,
our_username: &str, our_username: &str,
) -> Result<(), DataError> { ) -> Result<(), DataError> {
let room_id_str = room.room_id.as_str(); let room_id_str = room_id.as_str();
let usernames = matrix::get_users_in_room(&client, &room.room_id).await; let usernames = matrix::get_users_in_room(&client, &room_id).await;
let info = RoomInfo { let info = RoomInfo {
room_id: room_id_str.to_owned(), room_id: room_id_str.to_owned(),
room_name: room.display_name(), room_name: room_display_name.to_owned(),
}; };
// TODO this and the username adding should be one whole // TODO this and the username adding should be one whole

View File

@ -1,13 +1,21 @@
use matrix_sdk::{identifiers::RoomId, Client, Room}; use matrix_sdk::{identifiers::RoomId, Client};
/// Retrieve a list of users in a given room. /// Retrieve a list of users in a given room.
pub async fn get_users_in_room(client: &Client, room_id: &RoomId) -> Vec<String> { pub async fn get_users_in_room(client: &Client, room_id: &RoomId) -> Vec<String> {
if let Some(joined_room) = client.get_joined_room(room_id).await { if let Some(joined_room) = client.get_joined_room(room_id) {
let joined_room: Room = joined_room.read().await.clone();
joined_room joined_room
.joined_members .joined_members()
.keys() .await
.map(|user_id| format!("@{}:{}", user_id.localpart(), user_id.server_name())) .ok()
.unwrap_or_default()
.into_iter()
.map(|member| {
format!(
"@{}:{}",
member.user_id().localpart(),
member.user_id().server_name()
)
})
.collect() .collect()
} else { } else {
vec![] vec![]