2022-05-06 05:01:27 +00:00
|
|
|
#![warn(clippy::nursery, clippy::pedantic)]
|
|
|
|
|
|
|
|
use std::path::PathBuf;
|
2022-05-06 07:23:56 +00:00
|
|
|
use std::str::FromStr;
|
2022-05-06 05:01:27 +00:00
|
|
|
use std::time::Duration;
|
|
|
|
|
2022-05-09 08:04:08 +00:00
|
|
|
use anyhow::{Context, Result};
|
2022-05-06 05:01:27 +00:00
|
|
|
use clap::Parser;
|
2022-05-09 08:04:08 +00:00
|
|
|
use matrix_sdk::config::SyncSettings;
|
2022-05-09 08:37:44 +00:00
|
|
|
use matrix_sdk::room::{Joined, Room};
|
2022-05-09 08:04:08 +00:00
|
|
|
use matrix_sdk::ruma::events::room::member::StrippedRoomMemberEvent;
|
2022-05-06 05:01:27 +00:00
|
|
|
use matrix_sdk::ruma::events::room::message::{
|
2022-05-09 08:04:08 +00:00
|
|
|
MessageType, RoomMessageEventContent, SyncRoomMessageEvent, TextMessageEventContent,
|
2022-05-06 05:01:27 +00:00
|
|
|
};
|
2022-05-09 08:37:44 +00:00
|
|
|
use matrix_sdk::ruma::events::{OriginalSyncMessageLikeEvent, SyncMessageLikeEvent};
|
2022-05-09 08:04:08 +00:00
|
|
|
use matrix_sdk::ruma::OwnedUserId;
|
|
|
|
use matrix_sdk::store::CryptoStore;
|
|
|
|
use matrix_sdk::{Client, Session};
|
2022-05-06 05:01:27 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
2022-05-09 08:04:08 +00:00
|
|
|
use tracing::{error, info, Level};
|
|
|
|
use tracing_subscriber::util::SubscriberInitExt;
|
2022-05-06 05:01:27 +00:00
|
|
|
|
2022-05-09 08:37:44 +00:00
|
|
|
use commands::*;
|
|
|
|
|
|
|
|
mod commands;
|
|
|
|
|
2022-05-06 05:01:27 +00:00
|
|
|
#[derive(Parser)]
|
|
|
|
struct Args {
|
|
|
|
#[clap(subcommand)]
|
|
|
|
subcommand: Command,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Parser)]
|
|
|
|
enum Command {
|
|
|
|
Login {
|
2022-05-09 08:04:08 +00:00
|
|
|
user_id: OwnedUserId,
|
2022-05-06 05:01:27 +00:00
|
|
|
password: String,
|
|
|
|
},
|
|
|
|
Run {
|
|
|
|
#[clap(default_value = "config.toml")]
|
|
|
|
config_path: PathBuf,
|
2022-05-09 08:04:08 +00:00
|
|
|
#[clap(default_value = "crypto_data")]
|
|
|
|
crypto_store_path: PathBuf,
|
2022-05-06 05:01:27 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
struct TomlConfig {
|
|
|
|
session: Session,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::main]
|
|
|
|
async fn main() -> Result<()> {
|
2022-05-09 08:04:08 +00:00
|
|
|
tracing_subscriber::fmt()
|
|
|
|
.with_max_level(Level::INFO)
|
|
|
|
.finish()
|
|
|
|
.init();
|
2022-05-06 05:01:27 +00:00
|
|
|
let args = Args::parse();
|
|
|
|
|
|
|
|
match args.subcommand {
|
|
|
|
Command::Login { user_id, password } => handle_login(user_id, &password).await,
|
|
|
|
Command::Run {
|
|
|
|
config_path,
|
2022-05-09 08:04:08 +00:00
|
|
|
crypto_store_path,
|
|
|
|
} => handle_run(config_path, crypto_store_path).await,
|
2022-05-06 05:01:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-09 08:04:08 +00:00
|
|
|
async fn handle_login(user_id: OwnedUserId, password: &str) -> Result<()> {
|
|
|
|
let client = Client::builder().user_id(&user_id).build().await?;
|
2022-05-06 05:01:27 +00:00
|
|
|
let resp = client
|
|
|
|
.login(user_id.localpart(), password, None, None)
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
info!("Config generated. Please store this somewhere (like config.toml)!");
|
|
|
|
println!(
|
|
|
|
"\n\n{}",
|
|
|
|
toml::to_string_pretty(&TomlConfig {
|
|
|
|
session: Session {
|
|
|
|
access_token: resp.access_token,
|
|
|
|
device_id: resp.device_id,
|
|
|
|
user_id,
|
|
|
|
}
|
2022-05-09 08:23:35 +00:00
|
|
|
})?
|
2022-05-06 05:01:27 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2022-05-09 08:04:08 +00:00
|
|
|
async fn handle_run(config_path: PathBuf, crypto_store_path: PathBuf) -> Result<()> {
|
2022-05-06 05:01:27 +00:00
|
|
|
let data = std::fs::read_to_string(config_path)?;
|
2022-05-09 08:23:35 +00:00
|
|
|
let config: TomlConfig = toml::from_str(&data)?;
|
2022-05-06 05:01:27 +00:00
|
|
|
|
2022-05-09 08:04:08 +00:00
|
|
|
let client = Client::builder()
|
|
|
|
.user_id(&config.session.user_id)
|
|
|
|
.crypto_store(Box::new(CryptoStore::open_with_passphrase(
|
|
|
|
crypto_store_path,
|
|
|
|
None,
|
|
|
|
)?))
|
|
|
|
.build()
|
|
|
|
.await
|
|
|
|
.context("s")?;
|
2022-05-06 05:01:27 +00:00
|
|
|
client.restore_login(config.session.clone()).await?;
|
|
|
|
|
|
|
|
info!(user = %config.session.user_id, device_id = %config.session.device_id, "Logged in");
|
|
|
|
|
|
|
|
let sync_settings = SyncSettings::default();
|
|
|
|
|
|
|
|
// Sync once before registering events to not double-send events on boot.
|
|
|
|
client.sync_once(sync_settings.clone()).await?;
|
|
|
|
|
|
|
|
client
|
|
|
|
.register_event_handler(auto_join)
|
|
|
|
.await
|
|
|
|
.register_event_handler(on_room_message)
|
|
|
|
.await;
|
|
|
|
|
|
|
|
// Syncing is important to synchronize the client state with the server.
|
|
|
|
// This method will never return.
|
|
|
|
client.sync(sync_settings).await;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2022-05-09 08:04:08 +00:00
|
|
|
async fn auto_join(room_member: StrippedRoomMemberEvent, client: Client, room: Room) -> Result<()> {
|
|
|
|
if Some(room_member.sender) == client.user_id().await {
|
2022-05-06 05:01:27 +00:00
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Room::Invited(room) = room {
|
|
|
|
info!("Autojoining room {}", room.room_id());
|
|
|
|
let mut delay = 2;
|
|
|
|
|
|
|
|
while let Err(err) = room.accept_invitation().await {
|
|
|
|
// retry autojoin due to synapse sending invites, before the
|
|
|
|
// invited user can join for more information see
|
|
|
|
// https://github.com/matrix-org/synapse/issues/4345
|
|
|
|
error!(
|
|
|
|
room_id = ?room.room_id(),
|
|
|
|
?err,
|
|
|
|
"Failed to join room, retrying in {delay}s",
|
|
|
|
);
|
|
|
|
|
|
|
|
tokio::time::sleep(Duration::from_secs(delay)).await;
|
|
|
|
delay *= 2;
|
|
|
|
|
|
|
|
if delay > 3600 {
|
|
|
|
error!(
|
|
|
|
room_id = ?room.room_id(),
|
|
|
|
?err,
|
|
|
|
"Failed to join room. Giving up.",
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
info!(room_id = ?room.room_id(), "Successfully joined room.");
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2022-05-09 08:04:08 +00:00
|
|
|
async fn on_room_message(event: SyncRoomMessageEvent, room: Room, client: Client) -> Result<()> {
|
2022-05-06 07:28:31 +00:00
|
|
|
// Ignore messages sent from self.
|
2022-05-09 08:04:08 +00:00
|
|
|
if client.user_id().await.as_deref() == Some(event.sender()) {
|
2022-05-06 07:28:31 +00:00
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
2022-05-06 05:01:27 +00:00
|
|
|
if let Room::Joined(room) = room {
|
|
|
|
parse_message(event, room).await?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2022-05-09 08:04:08 +00:00
|
|
|
async fn parse_message(event: SyncRoomMessageEvent, room: Joined) -> Result<()> {
|
|
|
|
room.read_receipt(event.event_id()).await?;
|
|
|
|
|
|
|
|
let message = match &event {
|
|
|
|
SyncMessageLikeEvent::Original(OriginalSyncMessageLikeEvent {
|
|
|
|
content:
|
|
|
|
RoomMessageEventContent {
|
|
|
|
msgtype: MessageType::Text(TextMessageEventContent { body, .. }),
|
|
|
|
..
|
|
|
|
},
|
|
|
|
..
|
|
|
|
}) => body.clone(),
|
2022-05-06 05:01:27 +00:00
|
|
|
_ => return Ok(()),
|
|
|
|
};
|
|
|
|
|
2022-05-06 05:07:55 +00:00
|
|
|
if let Some(message) = message.strip_prefix('!') {
|
|
|
|
let (command, args) = message.split_once(' ').unwrap_or((message, ""));
|
2022-05-09 08:39:09 +00:00
|
|
|
match BotCommand::from_str(command) {
|
|
|
|
Ok(BotCommand::Source) => source(room).await?,
|
|
|
|
Ok(BotCommand::Uwu) => uwuify(room, args).await?,
|
|
|
|
Ok(BotCommand::Ping) => ping(room).await?,
|
|
|
|
Ok(BotCommand::Help) => help(room).await?,
|
2022-05-06 05:07:20 +00:00
|
|
|
_ => unsupported_command(event, room, command).await?,
|
2022-05-06 05:01:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn unsupported_command(
|
2022-05-09 08:04:08 +00:00
|
|
|
event: SyncRoomMessageEvent,
|
2022-05-06 05:01:27 +00:00
|
|
|
room: Joined,
|
|
|
|
command: &str,
|
|
|
|
) -> Result<()> {
|
2022-05-09 08:04:08 +00:00
|
|
|
let event = if let SyncRoomMessageEvent::Original(event) = event {
|
|
|
|
event.into_full_event(room.room_id().to_owned())
|
|
|
|
} else {
|
|
|
|
return Ok(());
|
|
|
|
};
|
2022-05-06 05:01:27 +00:00
|
|
|
let content =
|
2022-05-09 08:04:08 +00:00
|
|
|
RoomMessageEventContent::notice_reply_plain(format!("Unknown command `{command}`"), &event);
|
2022-05-06 05:01:27 +00:00
|
|
|
room.send(content, None).await?;
|
|
|
|
Ok(())
|
|
|
|
}
|