Initial commit

main
Amanda Graven 2021-06-11 23:49:07 +02:00
parent 929984ff0c
commit 45212da497
Signed by: amanda
GPG Key ID: 45C461CDC9286390
4 changed files with 223 additions and 0 deletions

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "reminder-bot"
version = "0.1.0"
authors = ["Amanda Graven <amanda@amandag.net>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk", rev = "dbf8cf2" }
ms-converter = "1.4"
#serde = { version = "1.0", features = ["derive"] }
rpassword = "5.0"
serde_json = "1.0"
tokio = { version = "1.0", features = ["macros", "time", "rt-multi-thread"]}

7
rustfmt.toml Normal file
View File

@ -0,0 +1,7 @@
hard_tabs=true
max_width = 100
comment_width = 80
wrap_comments = true
imports_granularity = "Crate"
use_small_heuristics = "Max"
group_imports = "StdExternalCrate"

5
session.sample.json Normal file
View File

@ -0,0 +1,5 @@
{
"access_token": "asdf1234",
"user_id": "@alice:matrix.org",
"device_id": "ABCDEFGHIJ"
}

196
src/main.rs Normal file
View File

@ -0,0 +1,196 @@
use std::{convert::TryFrom, fs::File};
use matrix_sdk::{
events::{
reaction::{ReactionEventContent, Relation as ReactionRelation},
room::{
member::MemberEventContent,
message::{MessageEvent, MessageEventContent, MessageType},
},
AnyMessageEventContent, AnyStrippedStateEvent, AnySyncMessageEvent, AnySyncRoomEvent,
StrippedStateEvent,
},
identifiers::{RoomId, UserId},
Client, ClientConfig, LoopCtrl, Session, SyncSettings,
};
#[tokio::main]
async fn main() {
if let Some("login") = std::env::args().nth(1).as_deref() {
println!("Performing interactive login");
let path = std::env::var("SESSION_FILE").unwrap_or_else(|_| String::from("session.json"));
let file = std::fs::File::create(&path).expect("Can't open session file");
let session = match login().await {
Ok(session) => session,
Err(e) => {
eprintln!("Login failed: {}", e);
return;
}
};
serde_json::to_writer_pretty(file, &session).unwrap();
println!("Interactive login completed");
return;
}
let client = match restore_login().await {
Ok(client) => client,
Err(e) => {
eprintln!("Login failed: {}", e);
return;
}
};
sync(client).await
}
async fn client(user_id: UserId) -> matrix_sdk::Result<Client> {
match std::env::var_os("STORAGE_DIR") {
Some(dir) => {
Client::new_from_user_id_with_config(user_id, ClientConfig::new().store_path(&dir))
.await
}
None => Client::new_from_user_id(user_id).await,
}
}
/// Log in interactively via the TTY.
async fn login() -> Result<Session, Box<dyn std::error::Error>> {
println!("Enter username (e.g. @alice:matrix.org):");
let mut user = String::new();
std::io::stdin().read_line(&mut user)?;
let user = UserId::try_from(user.trim())?;
println!("Enter password:");
let password = rpassword::read_password_from_tty(None)?;
let client = client(user.clone()).await?;
let response = client.login(user.as_str(), password.trim(), None, Some("Reminder bot")).await?;
let session = Session {
access_token: response.access_token,
user_id: response.user_id,
device_id: response.device_id,
};
Ok(session)
}
/// Restores a login from a session file. It gets the file path from the `SESSION_PATH` environment
/// variable if set, and defaults to looking for `session.json` in the current directory otherwise.
async fn restore_login() -> Result<Client, Box<dyn std::error::Error>> {
let path = std::env::var("SESSION_FILE").unwrap_or_else(|_| String::from("session.json"));
let file = File::open(&path)?;
let session: Session = serde_json::from_reader(file)?;
let client = client(session.user_id.clone()).await?;
client.restore_login(session).await?;
Ok(client)
}
async fn sync(client: Client) {
client
.sync_with_callback(SyncSettings::new(), |response| {
let client = client.clone();
async move {
for (room_id, room) in response.rooms.invite {
for event in room.invite_state.events {
let event = match event.deserialize() {
Ok(event) => event,
Err(_) => continue,
};
if let AnyStrippedStateEvent::RoomMember(member) = event {
handle_invite(client.clone(), &room_id, member).await
};
}
}
for (room_id, room) in response.rooms.join {
for event in room.timeline.events {
// skip if serialization failed
let event = match event.event.deserialize() {
Ok(event) => event,
Err(_) => continue,
};
// Skip if not a text message, otherwise extract the text event
if let AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(
message,
)) = event
{
handle_message(client.clone(), message.into_full_event(room_id.clone()))
.await
};
}
}
LoopCtrl::Continue
}
})
.await;
}
async fn handle_invite(
client: Client,
room_id: &RoomId,
member: StrippedStateEvent<MemberEventContent>,
) {
// Check that it's an invite, and that it's for our own mxid
if member.state_key != client.user_id().await.unwrap() {
return;
}
let invited = match client.get_invited_room(room_id) {
Some(invited) => invited,
None => return,
};
// Retry the accept until an house passes because synapse is silly and sends the invite before
// it's ready for receiving a join.
let mut delay = std::time::Duration::from_secs(2);
while invited.accept_invitation().await.is_err() {
tokio::time::sleep(delay).await;
delay *= 2;
if delay.as_secs() > 3600 {
return;
}
}
}
async fn handle_message(client: Client, message: MessageEvent) {
let text = match message.content.msgtype {
MessageType::Text(ref text) => text,
_ => return,
};
// Strip the command prefix, skip if it isn't there
let time = match text.body.strip_prefix("!remindme ") {
Some(time) => time,
None => return,
};
// Get the room so we can send messages.
let room = client.get_joined_room(&message.room_id).unwrap();
// Parse the duration, send message if that fails.
let duration = match ms_converter::ms_into_time(time) {
Ok(time) => time,
Err(_) => {
let _ = room
.send(
AnyMessageEventContent::RoomMessage(MessageEventContent::notice_reply_plain(
"Sorry! I don't understand when you want me to remind you.",
&message,
)),
None,
)
.await;
return;
}
};
// Send a reaction so the sender knows their message was received
let _ = room
.send(
AnyMessageEventContent::Reaction(ReactionEventContent::new(ReactionRelation::new(
message.event_id.clone(),
String::from(""),
))),
None,
)
.await;
tokio::task::spawn(async move {
tokio::time::sleep(duration).await;
// contruct the reminder message
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::notice_reply_plain(
"Here's your reminder!",
&message,
));
let _ = room.send(content, None).await;
});
}