242 lines
7 KiB
Rust
242 lines
7 KiB
Rust
use anyhow::{anyhow, bail, Context};
|
|
use log::{error, info, warn};
|
|
use matrix_sdk::SyncRoom;
|
|
use matrix_sdk::{
|
|
api::r0::{session::login, sync::sync_events},
|
|
async_trait,
|
|
events::{
|
|
room::{
|
|
member::MemberEventContent,
|
|
message::{MessageEventContent, TextMessageEventContent},
|
|
},
|
|
AnyMessageEventContent, AnyToDeviceEvent, StrippedStateEvent, SyncMessageEvent,
|
|
},
|
|
EventEmitter, LoopCtrl,
|
|
};
|
|
use std::{
|
|
collections::BTreeMap,
|
|
path::PathBuf,
|
|
sync::{atomic::AtomicBool, Arc},
|
|
time::Duration,
|
|
};
|
|
use structopt::StructOpt;
|
|
use tokio::sync::RwLock;
|
|
|
|
use config::Config;
|
|
|
|
use matrix_sdk::{self, api::r0::uiaa::AuthData, identifiers::UserId, Client, SyncSettings};
|
|
use serde_json::json;
|
|
mod config;
|
|
|
|
#[derive(Debug, StructOpt)]
|
|
struct Opt {
|
|
#[structopt(
|
|
short,
|
|
long,
|
|
help = "config file to use",
|
|
default_value = "~/.config/ruff/config.toml"
|
|
)]
|
|
config: PathBuf,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> anyhow::Result<()> {
|
|
env_logger::init();
|
|
|
|
let opt = Opt::from_args();
|
|
let config = std::fs::read(&opt.config).map_err(|e| anyhow!("Error reading config: {}", e))?;
|
|
let config =
|
|
toml::from_slice::<Config>(&config).map_err(|e| anyhow!("Error parsing config: {}", e))?;
|
|
|
|
let client = Arc::new(RwLock::new(Client::new(config.homeserver_url.clone())?));
|
|
|
|
let device_name = config.device_name.as_ref().map(String::as_ref);
|
|
let login::Response { user_id, .. } = client
|
|
.read()
|
|
.await
|
|
.login(&config.user_id, &config.password, device_name, device_name)
|
|
.await?;
|
|
|
|
client
|
|
.write()
|
|
.await
|
|
.add_event_emitter(Box::new(Bot {
|
|
client: Arc::clone(&client),
|
|
}))
|
|
.await;
|
|
|
|
let initial = AtomicBool::from(true);
|
|
let initial_ref = &initial;
|
|
let client_ref = &client.read().await;
|
|
let config_ref = &config;
|
|
let user_id_ref = &user_id;
|
|
client
|
|
.read()
|
|
.await
|
|
.sync_with_callback(SyncSettings::new(), |response| async move {
|
|
if let Err(e) = on_response(&response, client_ref).await {
|
|
error!("Error processing response: {}", e);
|
|
}
|
|
|
|
let initial = initial_ref;
|
|
|
|
if initial.load(std::sync::atomic::Ordering::SeqCst) {
|
|
if let Err(e) =
|
|
on_initial_response(&response, client_ref, &user_id_ref, &config_ref.password)
|
|
.await
|
|
{
|
|
error!("Error processing initial response: {}", e);
|
|
}
|
|
|
|
initial.store(false, std::sync::atomic::Ordering::SeqCst);
|
|
}
|
|
|
|
LoopCtrl::Continue
|
|
})
|
|
.await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
struct Bot {
|
|
client: Arc<RwLock<Client>>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl EventEmitter for Bot {
|
|
async fn on_stripped_state_member(
|
|
&self,
|
|
room: SyncRoom,
|
|
room_member: &StrippedStateEvent<MemberEventContent>,
|
|
_: Option<MemberEventContent>,
|
|
) {
|
|
if room_member.state_key == self.client.read().await.user_id().await.unwrap() {
|
|
return;
|
|
}
|
|
|
|
if let SyncRoom::Invited(room) = room {
|
|
let room = room.read().await;
|
|
println!("Autojoining room {}", room.room_id);
|
|
let mut delay = 2;
|
|
|
|
while let Err(err) = self
|
|
.client
|
|
.read()
|
|
.await
|
|
.join_room_by_id(&room.room_id)
|
|
.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
|
|
warn!(
|
|
"Failed to join room {} ({:?}), retrying in {}s",
|
|
room.room_id, err, delay
|
|
);
|
|
|
|
tokio::time::delay_for(Duration::from_secs(delay)).await;
|
|
delay *= 2;
|
|
|
|
if delay > 3600 {
|
|
error!("Can't join room {} ({:?})", room.room_id, err);
|
|
break;
|
|
}
|
|
}
|
|
info!("Successfully joined room {}", room.room_id);
|
|
}
|
|
}
|
|
|
|
async fn on_room_message(&self, _room: SyncRoom, msg: &SyncMessageEvent<MessageEventContent>) {
|
|
dbg!(msg);
|
|
}
|
|
}
|
|
|
|
async fn on_initial_response(
|
|
_response: &sync_events::Response,
|
|
client: &Client,
|
|
user_id: &UserId,
|
|
password: &str,
|
|
) -> anyhow::Result<()> {
|
|
bootstrap_cross_signing(client, user_id, password).await?;
|
|
for (id, _room) in client.joined_rooms().read().await.iter() {
|
|
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::Text(
|
|
TextMessageEventContent::plain("Hello world"),
|
|
));
|
|
client.room_send(id, content, None).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn on_response(response: &sync_events::Response, client: &Client) -> anyhow::Result<()> {
|
|
for event in response
|
|
.to_device
|
|
.events
|
|
.iter()
|
|
.filter_map(|e| e.deserialize().ok())
|
|
{
|
|
match event {
|
|
AnyToDeviceEvent::KeyVerificationStart(e) => {
|
|
info!("Starting verification");
|
|
if let Some(sas) = &client.get_verification(&e.content.transaction_id).await {
|
|
if let Err(e) = sas.accept().await {
|
|
error!("Error accepting key verification request: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
AnyToDeviceEvent::KeyVerificationKey(e) => {
|
|
if let Some(sas) = &client.get_verification(&e.content.transaction_id).await {
|
|
if let Err(e) = sas.confirm().await {
|
|
error!("Error confirming key verification request: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn auth_data<'a>(user: &UserId, password: &str, session: Option<&'a str>) -> AuthData<'a> {
|
|
let mut auth_parameters = BTreeMap::new();
|
|
let identifier = json!({
|
|
"type": "m.id.user",
|
|
"user": user,
|
|
});
|
|
|
|
auth_parameters.insert("identifier".to_owned(), identifier);
|
|
auth_parameters.insert("password".to_owned(), password.to_owned().into());
|
|
|
|
AuthData::DirectRequest {
|
|
kind: "m.login.password",
|
|
auth_parameters,
|
|
session,
|
|
}
|
|
}
|
|
|
|
async fn bootstrap_cross_signing(
|
|
client: &Client,
|
|
user_id: &UserId,
|
|
password: &str,
|
|
) -> anyhow::Result<()> {
|
|
info!("bootstrapping e2e");
|
|
if let Err(e) = dbg!(client.bootstrap_cross_signing(None).await) {
|
|
warn!("couldnt bootstrap e2e without auth data");
|
|
if let Some(response) = e.uiaa_response() {
|
|
let auth_data = auth_data(&user_id, &password, response.session.as_deref());
|
|
client
|
|
.bootstrap_cross_signing(Some(auth_data))
|
|
.await
|
|
.context("Couldn't bootstrap cross signing")?;
|
|
info!("bootstrapped e2e with auth data");
|
|
} else {
|
|
bail!("Error during cross-signing bootstrap {:#?}", e);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|