Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚧 Add integrations crate #175

Merged
merged 8 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ target/
*.min.js
*.map
*.tsbuildinfo
.env
docker-compose.yaml

# rust
Expand Down
63 changes: 42 additions & 21 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 crates/integrations/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DUMMY_DISCORD_WEBHOOK_URL=
DUMMY_TG_TOKEN=
DUMMY_TG_CHAT_ID=
14 changes: 14 additions & 0 deletions crates/integrations/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "integrations"
version = "0.1.0"
edition = "2021"

[dependencies]
async-trait = "0.1.72"
reqwest = { version = "0.11.18", features = [ "json" ] }
serde_json = "1.0.85"
thiserror = "1.0.37"

[dev-dependencies]
tokio = { version = "1.32.0", features = [ "full" ] }
dotenv = "0.15.0"
7 changes: 7 additions & 0 deletions crates/integrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Integrations

A rust crate providing wrappers around various notification providers. e.g. Discord, Telegram, etc. The Notifier trait defines the functions used to make API requests.

## Testing

Create a .env file similar to the template provided and add your Discord webhook url and/or Telegram token and chat ID.
28 changes: 28 additions & 0 deletions crates/integrations/src/google_books_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
pub struct GoogleBooksClient {
pub api_key: String,
pub client: reqwest::Client,
}

impl GoogleBooksClient {
pub fn new(api_key: String) -> Self {
let client = reqwest::Client::new();
Self { api_key, client }
}

pub async fn get_book_by_isbn(
&self,
isbn: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let url = format!(
"https://www.googleapis.com/books/v1/volumes?q=isbn:{}&key={}",
isbn, self.api_key
);
let response = self.client.get(&url).send().await?;
let text = response.text().await?;
println!("Response text: {text}");
// let response = response.json::<GoogleBooksResponse>().await?;
// let book = response.into();
// Ok(book)
unimplemented!()
}
}
5 changes: 5 additions & 0 deletions crates/integrations/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod google_books_client;
mod notifier;

pub use google_books_client::GoogleBooksClient;
pub use notifier::{DiscordClient, Notifier, TelegramClient};
105 changes: 105 additions & 0 deletions crates/integrations/src/notifier/discord_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use serde_json::json;

use super::{
error::{NotifierError, NotifierResult},
Notifier, NotifierEvent, FAVICON_URL, NOTIFIER_ID,
};

pub struct DiscordClient {
pub webhook_url: String,
pub client: reqwest::Client,
}

impl DiscordClient {
pub fn new(webhook_url: String) -> Self {
let client = reqwest::Client::new();
Self {
webhook_url,
client,
}
}
}

//https://core.telegram.org/bots/api#message

#[async_trait::async_trait]
impl Notifier for DiscordClient {
fn payload_from_event(event: NotifierEvent) -> NotifierResult<serde_json::Value> {
let payload = match event {
NotifierEvent::ScanCompleted {
books_added,
library_name,
} => json!({
"username" : NOTIFIER_ID,
"avatar_url" : FAVICON_URL,
"embeds" : [{
"title" : "Scan Completed!",
"description": format!("{books_added} books added to {library_name}"),
"color" : 13605239,
}]
}),
};
Ok(payload)
}

async fn send_message(&self, event: NotifierEvent) -> NotifierResult<()> {
let body = Self::payload_from_event(event)?;
let response = self
.client
.post(&self.webhook_url)
.json(&body)
.send()
.await?;
if !response.status().is_success() {
let errmsg = response
.text()
.await
.unwrap_or_else(|_| "sendMessage failed".to_string());
Err(NotifierError::RequestFailed(errmsg))
} else {
Ok(())
}
}
}

// https://birdie0.github.io/discord-webhooks-guide/structure/embed/color.html

#[cfg(test)]
mod tests {
use super::*;
use dotenv::dotenv;

fn get_debug_client() -> DiscordClient {
dotenv().ok();
let webhook_url = std::env::var("DUMMY_DISCORD_WEBHOOK_URL")
.expect("Failed to load webhook URL");
DiscordClient::new(webhook_url)
}

#[tokio::test]
async fn test_send_message() {
let client = get_debug_client();
let event = NotifierEvent::ScanCompleted {
books_added: 50,
library_name: String::from("test_library"),
};
let response = client.send_message(event).await;
assert!(response.is_ok());
}

#[test]
fn test_scan_completed() {
let event = NotifierEvent::ScanCompleted {
books_added: 5,
library_name: String::from("test_library"),
};
let response = DiscordClient::payload_from_event(event).unwrap();
assert!(response.is_object());
let embeds = response["embeds"].to_owned();
assert!(embeds.is_array());
assert_eq!(
embeds[0]["description"],
String::from("5 books added to test_library")
);
}
}
11 changes: 11 additions & 0 deletions crates/integrations/src/notifier/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
pub type NotifierResult<T> = Result<T, NotifierError>;

#[derive(Debug, thiserror::Error)]
pub enum NotifierError {
#[error("Request failed with error: {0}")]
ReqwestError(#[from] reqwest::Error),
#[error("{0}")]
Unimplemented(String),
#[error("Request was unsucessful")]
RequestFailed(String),
}
24 changes: 24 additions & 0 deletions crates/integrations/src/notifier/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
pub enum NotifierEvent {
ScanCompleted {
books_added: u64,
library_name: String,
},
}

impl NotifierEvent {
pub fn into_message(self) -> String {
match self {
NotifierEvent::ScanCompleted {
books_added,
library_name,
} => {
let is_plural = books_added == 0 || books_added > 1;
let book_or_books = if is_plural { "books" } else { "book" };
format!(
"{} {} added to {}",
books_added, book_or_books, library_name
)
},
}
}
}
20 changes: 20 additions & 0 deletions crates/integrations/src/notifier/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
mod discord_client;
mod error;
mod event;
mod telegram_client;

pub use discord_client::DiscordClient;
pub use event::NotifierEvent;
pub use telegram_client::TelegramClient;

use self::error::NotifierResult;

pub const NOTIFIER_ID: &str = "Stump Notifier";
pub const FAVICON_URL: &str = "https://stumpapp.dev/favicon.png";

#[async_trait::async_trait]
pub trait Notifier {
// TODO: MessageConfig struct? So we can style according to NotifierEvent?
fn payload_from_event(event: NotifierEvent) -> NotifierResult<serde_json::Value>;
async fn send_message(&self, event: NotifierEvent) -> NotifierResult<()>;
}
Loading
Loading