180 lines
6 KiB
Rust
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(())
|
|
}
|