frogbot/src/lib.rs

180 lines
6 KiB
Rust

//! A multi-purpose bot for Matrix
#![deny(missing_docs)]
pub mod embeds;
use log::warn;
use matrix_sdk::{
config::SyncSettings,
RoomState,
room::Room,
ruma::{
api::client::uiaa, events::room::member::StrippedRoomMemberEvent, OwnedDeviceId,
OwnedRoomId,
},
Client, ClientBuildError,
};
use serde::{Deserialize, Serialize};
/// Represents the entries in the configuration file.
#[derive(Serialize, Deserialize, Debug)]
pub struct Config {
/// Your Homeserver URL (e.g. "matrix.yourdomain.com")
pub homeserver: String,
/// The Bot User's Username (e.g. "frogbot")
pub username: String,
/// The Display Name of the Bot (e.g. "Frogbot 🐸")
pub display_name: String,
/// The Password to the Bot User (e.g. "hunter2")
pub password: String,
/// A List of All the Rooms to Join (e.g. ["!myid:matrix.yourdomain.com"] )
pub room_ids: Vec<OwnedRoomId>,
}
impl Config {
/// Loads a config file for frogbot to use.
pub fn load(config_file: &str) -> Config {
let config_file =
std::fs::read_to_string(config_file).expect("Failed to read config file.");
toml::from_str(&config_file).expect("Failed to parse TOML config.")
}
/// Returns a new frogbot client using the [`Config`].
pub async fn create_client(&self) -> Result<Client, ClientBuildError> {
Client::builder()
.homeserver_url(&self.homeserver)
.handle_refresh_tokens()
.build()
.await
}
}
/// Deletes all old encryption devices.
///
/// We don't want to end up with a ton of encryption devices that aren't active.
/// This function removes all the old ones while preserving the current device.
///
/// # Panics
///
/// This function will panic if it cannot get a device ID from the current client.
pub async fn delete_old_encryption_devices(client: &Client, config: &Config) -> anyhow::Result<()> {
warn!("Deleting old encryption devices");
let current_device_id = client.device_id().expect("Failed to get device ID");
let old_devices: Vec<OwnedDeviceId> = client
.devices()
.await?
.devices
.iter()
.filter(|d| d.device_id != current_device_id)
.map(|d| d.device_id.to_owned())
.collect();
// Deleting these devices needs "user interaction" or something, so we just send password again
// and it works :D
if let Err(e) = client.delete_devices(&old_devices, None).await {
if let Some(info) = e.as_uiaa_response() {
let mut password = uiaa::Password::new(
uiaa::UserIdentifier::UserIdOrLocalpart(config.username.clone()),
config.password.clone(),
);
password.session = info.session.clone();
client
.delete_devices(&old_devices, Some(uiaa::AuthData::Password(password)))
.await?;
}
}
warn!("Finished deleting old encryption devices");
Ok(())
}
/// Rejects invites that are waiting to be processed.
///
/// The bot will reject invites to spaces and DMs, as well as invites to any rooms it wasn't
/// configured to explicitly join, while accepting invites to any rooms it was configured to join.
pub async fn reject_stale_invites(client: &Client, config: &Config) {
warn!("Checking invites");
for room in client.invited_rooms() {
let room_name = room.name().unwrap_or_default();
if !room.is_space()
&& !room.is_direct().await.expect("Failed to check if room is DM")
&& config.room_ids.iter().any(|r| *r == room.room_id())
{
warn!("Got invite to room: '{}'", room_name);
room.join()
.await
.expect("Failed to accept invite");
warn!("Joined room: '{}'!", room_name);
} else {
warn!("Rejecting invite to room: '{}'", room_name);
room.leave().await.unwrap_or_default();
}
}
warn!("Finished checking old invites");
}
/// Run frogbot
///
/// Starts the bot and starts listening for events
///
/// # Panics
///
/// This function will panic in the following scenarios:
/// - If it cannot create a client using the current [`Config`].
/// - If the bot can't log into it's account.
/// - If the initial event sync fails.
pub async fn run(config: Config) -> anyhow::Result<()> {
let client = &config
.create_client()
.await
.expect("There was a problem creating frogbot's client.");
// Attempt to log into the server
client
.matrix_auth()
.login_username(&config.username, &config.password)
.initial_device_display_name(&config.display_name)
.send()
.await
.expect("frogbot couldn't log into it's account.");
// Set the bot account's display name according to config
client
.account()
.set_display_name(Some(&config.display_name))
.await?;
warn!("Logged in successfully!");
warn!(
"server: '{}', username: '{}', display name: '{}'",
&config.homeserver, &config.username, &config.display_name
);
// sync client once so we get latest events to work on before we continue
client
.sync_once(SyncSettings::default())
.await
.expect("Failed the initial event sync.");
delete_old_encryption_devices(client, &config).await?;
reject_stale_invites(client, &config).await;
// Add handler to log new room invites as they're recieved
client.add_event_handler(|ev: StrippedRoomMemberEvent, room: Room| async move {
if room.state() == RoomState::Invited {
warn!(
"Got invite to room: '{}' sent by '{}'",
room.name().unwrap_or_default(),
ev.sender
);
};
});
// Add handler to detect and create embeds for HTTP links in chat
client.add_event_handler(embeds::embed_handler);
// Now keep on syncing forever. `sync()` will use the latest sync token automatically.
warn!("Starting sync loop");
client.sync(SyncSettings::default()).await?;
Ok(())
}