Skip to content

Commit

Permalink
Merge pull request #10 from lazytanuki/urgent_rooms
Browse files Browse the repository at this point in the history
feat: urgent rooms, emoji tags, JSON deserialization and header fields aliases
  • Loading branch information
arcuru authored Oct 10, 2024
2 parents a8cd2bb + 0d981f8 commit 0016d5a
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 26 deletions.
36 changes: 36 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ tokio = { version = "1", features = ["full"] }
http-body-util = "0.1"
hyper-util = { version = "0.1", features = ["full"] }
reqwest = "0.12"
url = "2.5.2"
emojis = "0.6.3"
serde_json = "1.0.128"
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,11 @@ rooms:
# e.g. `pokem The backup failed!` will send to the default room
default: "!RoomID:jackson.dev"
error: "!ErrorRoom:jackson.dev"
fullteam: "!RoomWithFullTeam:jackson.dev"
discord: "!RoomBridgedToDiscord:jackson.dev"
fullteam: "!RoomWithFullTeam:jackson.dev"
# Messages that come with the "urgent" tag sent to "fullteam" will go to "fullteam-urgent", if
# it exists, otherwise they will be sent to "fullteam" with a "@room" ping
fullteam-urgent: "!RoomWithFullTeamUrgent:jackson.dev"

# Optional, define the server to send messages to
# If configured, `pokem` will first try to query this server to send the message
Expand Down
200 changes: 177 additions & 23 deletions src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
use crate::config::*;
use crate::utils::*;

use anyhow::Context;
use clap::error::Result;
use emojis::Emoji;
use matrix_sdk::ruma::events::room::message::RoomMessageEventContent;

use matrix_sdk::ruma::events::tag::TagInfo;
use matrix_sdk::Room;

use serde::Deserialize;
use tokio::sync::RwLock;
use tracing::error;
use tracing::{debug, error};

use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr};
Expand All @@ -25,6 +29,106 @@ use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;

#[derive(Debug, Clone, Deserialize)]
struct PokeRequest {
topic: String,
title: Option<String>,
message: String,
priority: Option<u8>,
tags: Option<Vec<String>>,
}

impl PokeRequest {
/// Try to deserialize the request from JSON, otherwise build it from headers and body.
pub async fn from_request(request: Request<hyper::body::Incoming>) -> anyhow::Result<Self> {
// Try JSON deserialization
let headers = request.headers().clone();
let uri = request.uri().clone();

let body_bytes = request.collect().await?.to_bytes();
let body_str =
String::from_utf8(body_bytes.to_vec()).with_context(|| "error while decoding UTF-8")?;
let Ok(poke_request) = serde_json::from_str::<PokeRequest>(&body_str) else {
// Build from headers and body
let query_params: HashMap<String, String> = uri
.query()
.map(|v| {
url::form_urlencoded::parse(v.as_bytes())
.map(|(a, b)| (a.to_lowercase(), b.to_string()))
.collect()
})
.unwrap_or_default();
return Ok(PokeRequest {
// The uri without the leading / will be the room id
topic: uri.path().trim_start_matches('/').to_string(),
title: query_params.get("title").cloned().or_else(|| {
headers
.get("x-title")
.or_else(|| headers.get("X-Title"))
.or_else(|| headers.get("Title"))
.or_else(|| headers.get("title"))
.or_else(|| headers.get("ti"))
.or_else(|| headers.get("t"))
.and_then(|tags| tags.to_str().ok().map(String::from))
}),
message: query_params
.get("message")
.cloned()
.or_else(|| {
headers
.get("x-message")
.or_else(|| headers.get("X-Message"))
.or_else(|| headers.get("Message"))
.or_else(|| headers.get("message"))
.or_else(|| headers.get("m"))
.and_then(|msg| msg.to_str().ok().map(String::from))
})
.unwrap_or(body_str),
priority: query_params
.get("priority")
.and_then(|p| p.parse().ok())
.or_else(|| {
headers
.get("x-priority")
.or_else(|| headers.get("X-Priority"))
.or_else(|| headers.get("Priority"))
.or_else(|| headers.get("priority"))
.or_else(|| headers.get("prio"))
.or_else(|| headers.get("p"))
.and_then(|priority_header| {
priority_header.to_str().ok().map(|header_str| {
header_str.parse().unwrap_or_else(|_| {
match &header_str.to_lowercase()[..] {
"min" => 1,
"low" => 2,
"default" => 3,
"high" => 4,
"urgent" | "max" => 5,
_ => 3,
}
})
})
})
}),
tags: query_params
.get("tags")
.cloned()
.or_else(|| {
headers
.get("x-tags")
.or_else(|| headers.get("X-Tags"))
.or_else(|| headers.get("Tags"))
.or_else(|| headers.get("tag"))
.or_else(|| headers.get("ta"))
.and_then(|tags| tags.to_str().ok().map(String::from))
})
.map(|tags_str| tags_str.split(',').map(String::from).collect()),
});
};
Ok(poke_request)
}
}

/// Run in daemon mode
/// This binds to a port and listens for incoming requests, and sends them to the Matrix room
pub async fn daemon(
Expand Down Expand Up @@ -87,13 +191,19 @@ pub async fn daemon(
args.next(); // Ignore the "poke"
let room_id = args.next().unwrap_or_default();
let message = args.collect::<Vec<&str>>().join(" ");
error!("Room: {:?}, Message: {:?}", room_id, message);
debug!("Room: {:?}, Message: {:?}", room_id, message);

// Get a copy of the bot
let bot = GLOBAL_BOT.lock().unwrap().as_ref().unwrap().clone();

if let Err(e) =
ping_room(&bot, room_id, &reqwest::header::HeaderMap::new(), &message).await
if let Err(e) = ping_room(
&bot,
room_id,
&reqwest::header::HeaderMap::new(),
&message,
false,
)
.await
{
error!("Failed to send message: {:?}", e);
if can_message_room(&room).await {
Expand Down Expand Up @@ -277,26 +387,67 @@ Current values:\n- block: {}{}",
async fn daemon_poke(
request: Request<hyper::body::Incoming>,
rooms: Arc<RwLock<Option<HashMap<String, String>>>>,
) -> Result<Response<Full<Bytes>>, hyper::Error> {
// The uri without the leading / will be the room id
let room_id = request.uri().path().trim_start_matches('/').to_string();
) -> anyhow::Result<Response<Full<Bytes>>> {
let headers = request.headers().clone();
let is_get = request.method() == hyper::Method::GET;
let mut poke_request = PokeRequest::from_request(request).await?;

// The room_id may be URI encoded
let mut room_id = match urlencoding::decode(&room_id) {
let mut room_id = match urlencoding::decode(&poke_request.topic) {
Ok(room) => room.to_string(),
Err(_) => room_id,
Err(_) => poke_request.topic,
};

// If the room is a room name in the config, we'll transform it to the room id
room_id = if let Some(room_id) = &rooms.read().await.as_ref().and_then(|r| r.get(&room_id)) {
room_id.to_string()
} else {
room_id
};
let urgent = poke_request.priority.is_some_and(|p| p > 3);

let headers = request.headers().clone();
// Add title
if let Some(title) = poke_request.title {
poke_request.message = format!("**{title}**\n\n{}", poke_request.message);
}

// Add emojis
if let Some(tags) = poke_request.tags {
let emojis_vec: Vec<&'static Emoji> = tags
.iter()
.filter_map(|shortcode| emojis::get_by_shortcode(shortcode.as_str()))
.collect();
let emojis_str = emojis_vec
.iter()
.map(|e| e.to_string())
.collect::<Vec<String>>()
.join("");
if !emojis_str.is_empty() {
poke_request.message = format!("{emojis_str} {}", poke_request.message);
}
}

// If the room is a room name in the config, we'll transform it to the room id.
// If the message is urgent and <room_name>-urgent exists, it will got there, otherwise
// we mention the entire @room.
let mut mention_room = false;
room_id = match &rooms.read().await.as_ref().and_then(|r| {
if urgent {
r.get(&format!("{}-urgent", room_id)).or_else(|| {
// No urgent room found, pinging @room
mention_room = true;
r.get(&room_id)
})
} else {
r.get(&room_id)
}
}) {
Some(room_id) => room_id.to_string(),
_ => {
// No urgent room found, pinging @room
if urgent {
mention_room = true;
}
room_id
}
};

// If it's a GET request, we'll serve a WebUI
if request.method() == hyper::Method::GET {
if is_get {
// Create the webpage with the room id filled in
let page = r#"
<!DOCTYPE html>
Expand Down Expand Up @@ -397,16 +548,19 @@ async fn daemon_poke(
.body(Full::new(Bytes::from(page)))
.unwrap());
}
// The request body will be the message
// Transform the body into a string
let body_bytes = request.collect().await?.to_bytes();
let message = String::from_utf8(body_bytes.to_vec()).unwrap();
error!("Room: {:?}, Message: {:?}", room_id, message);

// Get a copy of the bot
let bot = GLOBAL_BOT.lock().unwrap().as_ref().unwrap().clone();

if let Err(e) = ping_room(&bot, &room_id, &headers, &message).await {
if let Err(e) = ping_room(
&bot,
&room_id,
&headers,
&poke_request.message,
mention_room,
)
.await
{
error!("Failed to send message: {:?}", e);
return Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ async fn main() -> anyhow::Result<()> {
let bot = connect(matrix).await?;
GLOBAL_BOT.lock().unwrap().replace(bot.clone());
// Ping the room
return ping_room(&bot, &room, &headers, &messages.join(" ")).await;
return ping_room(&bot, &room, &headers, &messages.join(" "), false).await;
}

return Err(anyhow::anyhow!("Unable to send message"));
Expand Down
7 changes: 6 additions & 1 deletion src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use headjack::*;
use matrix_sdk::ruma::events::room::message::RoomMessageEventContent;

use matrix_sdk::ruma::events::tag::TagInfo;
use matrix_sdk::ruma::events::Mentions;
use matrix_sdk::{Room, RoomMemberships, RoomState};

use tracing::{error, info};
Expand Down Expand Up @@ -173,6 +174,7 @@ pub async fn ping_room(
room_id: &str,
headers: &HeaderMap,
message: &str,
mention_room: bool,
) -> anyhow::Result<()> {
let r = get_room_from_name(bot, room_id).await;
if r.is_none() {
Expand Down Expand Up @@ -206,7 +208,10 @@ pub async fn ping_room(
}

// Get the message formatting
let msg = format_message(headers, &msg);
let mut msg = format_message(headers, &msg);
if mention_room {
msg = msg.add_mentions(Mentions::with_room_mention());
}

if can_message_room(&r).await {
if let Err(e) = r.send(msg).await {
Expand Down

0 comments on commit 0016d5a

Please sign in to comment.