Initial commit
This commit is contained in:
parent
929984ff0c
commit
45212da497
15
Cargo.toml
Normal file
15
Cargo.toml
Normal 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
7
rustfmt.toml
Normal 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
5
session.sample.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"access_token": "asdf1234",
|
||||||
|
"user_id": "@alice:matrix.org",
|
||||||
|
"device_id": "ABCDEFGHIJ"
|
||||||
|
}
|
196
src/main.rs
Normal file
196
src/main.rs
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue