2021-06-17 17:32:31 +02:00
|
|
|
use anyhow::{anyhow, bail, Context};
|
2021-06-18 23:15:19 +02:00
|
|
|
use jm_client_core::JMClient;
|
2021-06-17 17:32:31 +02:00
|
|
|
use log::{error, info, warn};
|
|
|
|
use matrix_sdk::{
|
2021-06-18 23:15:19 +02:00
|
|
|
api::r0::session::login,
|
2021-06-17 17:32:31 +02:00
|
|
|
async_trait,
|
2021-06-18 23:15:19 +02:00
|
|
|
deserialized_responses::SyncResponse,
|
2021-06-17 17:32:31 +02:00
|
|
|
events::{
|
|
|
|
room::{
|
|
|
|
member::MemberEventContent,
|
2021-06-18 23:15:19 +02:00
|
|
|
message::{MessageEventContent, MessageType, TextMessageEventContent},
|
2021-06-17 17:32:31 +02:00
|
|
|
},
|
2021-06-18 23:15:19 +02:00
|
|
|
AnyToDeviceEvent, StrippedStateEvent, SyncMessageEvent,
|
2021-06-17 17:32:31 +02:00
|
|
|
},
|
2021-06-18 23:15:19 +02:00
|
|
|
room::Room,
|
|
|
|
verification::Verification,
|
|
|
|
EventHandler, LoopCtrl,
|
2021-06-17 17:32:31 +02:00
|
|
|
};
|
2021-06-18 23:15:19 +02:00
|
|
|
use rand::{rngs::StdRng, SeedableRng};
|
|
|
|
use sled::Db;
|
2021-01-12 00:44:51 +01:00
|
|
|
use std::{
|
2021-06-17 17:32:31 +02:00
|
|
|
collections::BTreeMap,
|
|
|
|
path::PathBuf,
|
2021-06-18 23:15:19 +02:00
|
|
|
sync::{
|
|
|
|
atomic::{AtomicBool, AtomicU32},
|
|
|
|
Arc,
|
|
|
|
},
|
2021-06-17 17:32:31 +02:00
|
|
|
time::Duration,
|
2021-01-12 00:44:51 +01:00
|
|
|
};
|
|
|
|
use structopt::StructOpt;
|
2021-06-18 23:15:19 +02:00
|
|
|
use tokio::sync::Mutex;
|
2021-06-17 17:32:31 +02:00
|
|
|
use tokio::sync::RwLock;
|
2021-01-12 00:44:51 +01:00
|
|
|
|
2021-06-17 17:32:31 +02:00
|
|
|
use config::Config;
|
2021-01-12 00:44:51 +01:00
|
|
|
|
2021-06-17 17:32:31 +02:00
|
|
|
use matrix_sdk::{self, api::r0::uiaa::AuthData, identifiers::UserId, Client, SyncSettings};
|
|
|
|
use serde_json::json;
|
|
|
|
mod config;
|
2021-06-18 23:15:19 +02:00
|
|
|
mod meme;
|
|
|
|
mod responder;
|
2021-06-17 17:32:31 +02:00
|
|
|
|
|
|
|
#[derive(Debug, StructOpt)]
|
|
|
|
struct Opt {
|
2021-01-12 00:44:51 +01:00
|
|
|
#[structopt(
|
|
|
|
short,
|
2021-06-17 17:32:31 +02:00
|
|
|
long,
|
|
|
|
help = "config file to use",
|
|
|
|
default_value = "~/.config/ruff/config.toml"
|
2021-01-12 00:44:51 +01:00
|
|
|
)]
|
2021-06-17 17:32:31 +02:00
|
|
|
config: PathBuf,
|
2021-01-12 00:44:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::main]
|
2021-06-17 17:32:31 +02:00
|
|
|
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))?;
|
2021-06-18 23:15:19 +02:00
|
|
|
let config = Arc::new(config);
|
2021-06-17 17:32:31 +02:00
|
|
|
|
|
|
|
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
|
2021-06-18 23:15:19 +02:00
|
|
|
.set_event_handler(Box::new(Bot {
|
2021-06-17 17:32:31 +02:00
|
|
|
client: Arc::clone(&client),
|
2021-06-18 23:15:19 +02:00
|
|
|
jm_client: RwLock::new(JMClient::new()),
|
|
|
|
memecache: sled::open(config.store_path.join("memecache"))
|
|
|
|
.map_err(|e| anyhow!("error opening memecache: {}", e))?,
|
|
|
|
config: Arc::clone(&config),
|
|
|
|
meme_count: AtomicU32::new(0),
|
|
|
|
rng: Mutex::new(StdRng::from_rng(rand::thread_rng())?),
|
2021-06-17 17:32:31 +02:00
|
|
|
}))
|
|
|
|
.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);
|
|
|
|
}
|
2021-01-12 00:44:51 +01:00
|
|
|
|
2021-06-17 17:32:31 +02:00
|
|
|
let initial = initial_ref;
|
2021-01-12 00:44:51 +01:00
|
|
|
|
2021-06-17 17:32:31 +02:00
|
|
|
if initial.load(std::sync::atomic::Ordering::SeqCst) {
|
|
|
|
if let Err(e) =
|
2021-06-18 23:15:19 +02:00
|
|
|
on_initial_response(client_ref, &user_id_ref, &config_ref.password).await
|
2021-06-17 17:32:31 +02:00
|
|
|
{
|
|
|
|
error!("Error processing initial response: {}", e);
|
|
|
|
}
|
2021-01-12 00:44:51 +01:00
|
|
|
|
2021-06-17 17:32:31 +02:00
|
|
|
initial.store(false, std::sync::atomic::Ordering::SeqCst);
|
2021-01-12 00:44:51 +01:00
|
|
|
}
|
|
|
|
|
2021-06-17 17:32:31 +02:00
|
|
|
LoopCtrl::Continue
|
|
|
|
})
|
|
|
|
.await;
|
2021-01-12 00:44:51 +01:00
|
|
|
|
2021-06-17 17:32:31 +02:00
|
|
|
Ok(())
|
2021-01-12 00:44:51 +01:00
|
|
|
}
|
|
|
|
|
2021-06-18 23:15:19 +02:00
|
|
|
pub struct Bot {
|
2021-06-17 17:32:31 +02:00
|
|
|
client: Arc<RwLock<Client>>,
|
2021-06-18 23:15:19 +02:00
|
|
|
jm_client: RwLock<JMClient>,
|
|
|
|
memecache: Db,
|
|
|
|
config: Arc<Config>,
|
|
|
|
/// used to keep track of how many memes have been sent.
|
|
|
|
/// this is reset once the threshold set in the config has been reached, and the JMClient cache
|
|
|
|
/// is cleared.
|
|
|
|
meme_count: AtomicU32,
|
|
|
|
rng: Mutex<StdRng>,
|
2021-01-12 00:44:51 +01:00
|
|
|
}
|
|
|
|
|
2021-06-17 17:32:31 +02:00
|
|
|
#[async_trait]
|
2021-06-18 23:15:19 +02:00
|
|
|
impl EventHandler for Bot {
|
2021-06-17 17:32:31 +02:00
|
|
|
async fn on_stripped_state_member(
|
|
|
|
&self,
|
2021-06-18 23:15:19 +02:00
|
|
|
room: Room,
|
2021-06-17 17:32:31 +02:00
|
|
|
room_member: &StrippedStateEvent<MemberEventContent>,
|
|
|
|
_: Option<MemberEventContent>,
|
|
|
|
) {
|
|
|
|
if room_member.state_key == self.client.read().await.user_id().await.unwrap() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-06-18 23:15:19 +02:00
|
|
|
if let Room::Invited(room) = room {
|
|
|
|
println!("Autojoining room {}", room.room_id());
|
2021-06-17 17:32:31 +02:00
|
|
|
let mut delay = 2;
|
|
|
|
|
|
|
|
while let Err(err) = self
|
|
|
|
.client
|
|
|
|
.read()
|
|
|
|
.await
|
2021-06-18 23:15:19 +02:00
|
|
|
.join_room_by_id(&room.room_id())
|
2021-06-17 17:32:31 +02:00
|
|
|
.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",
|
2021-06-18 23:15:19 +02:00
|
|
|
room.room_id(),
|
|
|
|
err,
|
|
|
|
delay
|
2021-06-17 17:32:31 +02:00
|
|
|
);
|
|
|
|
|
2021-06-18 23:15:19 +02:00
|
|
|
tokio::time::sleep(Duration::from_secs(delay)).await;
|
2021-06-17 17:32:31 +02:00
|
|
|
delay *= 2;
|
|
|
|
|
|
|
|
if delay > 3600 {
|
2021-06-18 23:15:19 +02:00
|
|
|
error!("Can't join room {} ({:?})", room.room_id(), err);
|
2021-06-17 17:32:31 +02:00
|
|
|
break;
|
2021-01-12 00:44:51 +01:00
|
|
|
}
|
|
|
|
}
|
2021-06-18 23:15:19 +02:00
|
|
|
|
|
|
|
info!("Successfully joined room {}", room.room_id());
|
2021-06-17 17:32:31 +02:00
|
|
|
}
|
2021-01-12 00:44:51 +01:00
|
|
|
}
|
|
|
|
|
2021-06-18 23:15:19 +02:00
|
|
|
async fn on_room_message(&self, room: Room, msg: &SyncMessageEvent<MessageEventContent>) {
|
|
|
|
if self
|
|
|
|
.client
|
|
|
|
.read()
|
|
|
|
.await
|
|
|
|
.user_id()
|
|
|
|
.await
|
|
|
|
.map(|u| u == msg.sender)
|
|
|
|
.unwrap_or(true)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let SyncMessageEvent {
|
|
|
|
content:
|
|
|
|
MessageEventContent {
|
|
|
|
msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
|
|
|
|
..
|
|
|
|
},
|
|
|
|
..
|
|
|
|
} = msg
|
|
|
|
{
|
|
|
|
if let Err(e) = responder::on_msg(msg_body, room, self).await {
|
|
|
|
error!("Responder error: {}", e);
|
|
|
|
}
|
|
|
|
}
|
2021-06-17 17:32:31 +02:00
|
|
|
}
|
2021-01-12 00:44:51 +01:00
|
|
|
}
|
|
|
|
|
2021-06-17 17:32:31 +02:00
|
|
|
async fn on_initial_response(
|
|
|
|
client: &Client,
|
|
|
|
user_id: &UserId,
|
|
|
|
password: &str,
|
|
|
|
) -> anyhow::Result<()> {
|
|
|
|
bootstrap_cross_signing(client, user_id, password).await?;
|
2021-01-12 00:44:51 +01:00
|
|
|
|
2021-06-17 17:32:31 +02:00
|
|
|
Ok(())
|
2021-01-12 00:44:51 +01:00
|
|
|
}
|
|
|
|
|
2021-06-18 23:15:19 +02:00
|
|
|
async fn on_response(response: &SyncResponse, client: &Client) -> anyhow::Result<()> {
|
2021-06-17 17:32:31 +02:00
|
|
|
for event in response
|
|
|
|
.to_device
|
|
|
|
.events
|
|
|
|
.iter()
|
|
|
|
.filter_map(|e| e.deserialize().ok())
|
|
|
|
{
|
|
|
|
match event {
|
|
|
|
AnyToDeviceEvent::KeyVerificationStart(e) => {
|
|
|
|
info!("Starting verification");
|
2021-06-18 23:15:19 +02:00
|
|
|
if let Some(Verification::SasV1(sas)) = &client
|
|
|
|
.get_verification(&e.sender, &e.content.transaction_id)
|
|
|
|
.await
|
|
|
|
{
|
2021-06-17 17:32:31 +02:00
|
|
|
if let Err(e) = sas.accept().await {
|
|
|
|
error!("Error accepting key verification request: {}", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-01-12 00:44:51 +01:00
|
|
|
|
2021-06-17 17:32:31 +02:00
|
|
|
AnyToDeviceEvent::KeyVerificationKey(e) => {
|
2021-06-18 23:15:19 +02:00
|
|
|
if let Some(Verification::SasV1(sas)) = &client
|
|
|
|
.get_verification(&e.sender, &e.content.transaction_id)
|
|
|
|
.await
|
|
|
|
{
|
2021-06-17 17:32:31 +02:00
|
|
|
if let Err(e) = sas.confirm().await {
|
|
|
|
error!("Error confirming key verification request: {}", e);
|
2021-01-12 00:44:51 +01:00
|
|
|
}
|
2021-06-17 17:32:31 +02:00
|
|
|
}
|
2021-01-12 00:44:51 +01:00
|
|
|
}
|
2021-06-17 17:32:31 +02:00
|
|
|
|
|
|
|
_ => {}
|
2021-01-12 00:44:51 +01:00
|
|
|
}
|
|
|
|
}
|
2021-06-17 17:32:31 +02:00
|
|
|
|
|
|
|
Ok(())
|
2021-01-12 00:44:51 +01:00
|
|
|
}
|
|
|
|
|
2021-06-17 17:32:31 +02:00
|
|
|
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,
|
2021-01-12 00:44:51 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-17 17:32:31 +02:00
|
|
|
async fn bootstrap_cross_signing(
|
|
|
|
client: &Client,
|
|
|
|
user_id: &UserId,
|
|
|
|
password: &str,
|
|
|
|
) -> anyhow::Result<()> {
|
|
|
|
info!("bootstrapping e2e");
|
2021-06-18 23:15:19 +02:00
|
|
|
if let Err(e) = client.bootstrap_cross_signing(None).await {
|
2021-06-17 17:32:31 +02:00
|
|
|
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")?;
|
2021-06-18 23:15:19 +02:00
|
|
|
info!("bootstrapped e2e with auth data");
|
2021-06-17 17:32:31 +02:00
|
|
|
} else {
|
|
|
|
bail!("Error during cross-signing bootstrap {:#?}", e);
|
|
|
|
}
|
2021-01-12 00:44:51 +01:00
|
|
|
}
|
|
|
|
|
2021-06-17 17:32:31 +02:00
|
|
|
Ok(())
|
|
|
|
}
|