diff --git a/Cargo.lock b/Cargo.lock index b8b4622..978f948 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -434,6 +434,7 @@ dependencies = [ "chrono", "envconfig", "futures", + "gql_client", "hex", "infer", "lapin", @@ -1983,6 +1984,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "gql_client" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57f4862d3e5cd4ffe8df03fa2137e620e33578bbc1895e6f8f569630e17b1db" +dependencies = [ + "log", + "reqwest 0.11.27", + "serde", + "serde_json", +] + [[package]] name = "graphql-parser" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index bf90d56..e82b83b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ bytes = "1.6.0" once_cell = "1.19.0" sha1 = "0.10.6" hex = "0.4.3" +gql_client = "1.0.7" [profile.release] lto = true diff --git a/README.md b/README.md index 1d6650e..70beba3 100644 --- a/README.md +++ b/README.md @@ -6,4 +6,5 @@ There is probably a lot of better ways to do many of the things I've done here, - Spotify History and Now Playing API / Queue Messages - File and Screenshot Uploads +- Github pinned repositories - Blog System for Personal Site \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c1a7829..415c125 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,7 +1,7 @@ generator client { provider = "cargo prisma" output = "../src/connectivity/prisma.rs" - module_path = "crate::connectivity::prisma" + module_path = "connectivity::prisma" } datasource db { diff --git a/src/config.rs b/src/config.rs index 3c7e2b4..d861a51 100644 --- a/src/config.rs +++ b/src/config.rs @@ -61,4 +61,7 @@ pub struct Config { #[envconfig(from = "S3_FILES_ALIAS", default = "files.dstn.to")] pub s3_files_alias: String, + + #[envconfig(from = "GITHUB_PAT", default = "")] + pub github_pat: String, } diff --git a/src/main.rs b/src/main.rs index ad5062d..3f9c5f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,6 +94,7 @@ async fn main() -> Result<(), Box> { .service(services::base::health) .service(services::uploads::factory::uploads_factory()) .service(services::spotify::factory::spotify_factory()) + .service(services::github::factory::github_factory()) .service(services::blog::factory::blog_factory()), ) }) diff --git a/src/modules/spotify.rs b/src/modules/spotify.rs index b687a78..474d26d 100644 --- a/src/modules/spotify.rs +++ b/src/modules/spotify.rs @@ -103,6 +103,10 @@ pub(crate) async fn fetch_spotify_current(data: web::Data) { Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()); let current_playing = Arc::clone(¤t_clone); + if current_playing.progress.unwrap() < 10000 { + return; + } + match prisma .spotify_history() .find_first(vec![spotify_history::id::equals( @@ -118,29 +122,13 @@ pub(crate) async fn fetch_spotify_current(data: web::Data) { let date_minus_length = (date.timestamp() * 1000) - latest.length as i64; - if current_playing.progress.unwrap() < 10000 { - return; - } - if date_minus_length >= listened_date { store_history(prisma, current_playing).await; } } - None => { - if current_playing.progress.unwrap() < 10000 { - return; - } - - store_history(prisma, current_playing).await; - } + None => store_history(prisma, current_playing).await, }, - Err(_) => { - if current_playing.progress.unwrap() < 10000 { - return; - } - - store_history(prisma, current_playing).await; - } + Err(_) => store_history(prisma, current_playing).await, } } } else { diff --git a/src/services/blog/posts.rs b/src/services/blog/posts.rs index 0ecf0fb..8e792ae 100644 --- a/src/services/blog/posts.rs +++ b/src/services/blog/posts.rs @@ -34,7 +34,7 @@ async fn get_posts( .find_many(vec![blog_posts::visibility::equals("public".to_string())]) .take(query.limit.unwrap_or(25)) .skip(query.offset.unwrap_or(0)) - .order_by(blog_posts::published_at::order(Direction::Asc)) + .order_by(blog_posts::published_at::order(Direction::Desc)) .exec() .await; @@ -81,7 +81,7 @@ async fn get_all_posts( .find_many(vec![]) .take(query.limit.unwrap_or(25)) .skip(query.offset.unwrap_or(0)) - .order_by(blog_posts::created_at::order(Direction::Asc)) + .order_by(blog_posts::created_at::order(Direction::Desc)) .exec() .await; diff --git a/src/services/github/factory.rs b/src/services/github/factory.rs new file mode 100644 index 0000000..0b3c7d3 --- /dev/null +++ b/src/services/github/factory.rs @@ -0,0 +1,7 @@ +use actix_web::{web, Scope}; + +use crate::services; + +pub fn github_factory() -> Scope { + web::scope("/github").service(services::github::routes::github_pinned) +} diff --git a/src/services/github/mod.rs b/src/services/github/mod.rs new file mode 100644 index 0000000..2527156 --- /dev/null +++ b/src/services/github/mod.rs @@ -0,0 +1,2 @@ +pub mod factory; +pub mod routes; diff --git a/src/services/github/routes.rs b/src/services/github/routes.rs new file mode 100644 index 0000000..dfdf90e --- /dev/null +++ b/src/services/github/routes.rs @@ -0,0 +1,99 @@ +use std::collections::HashMap; + +use actix_web::{get, http::Error, web, HttpResponse}; +use envconfig::Envconfig; +use gql_client::Client; +use redis::{aio::ConnectionManager, AsyncCommands, RedisError}; +use serde_json::json; + +use crate::{config::Config, structs::github::Data, ServerState}; + +#[get("/pinned")] +async fn github_pinned( + state: web::Data, +) -> Result { + let query = " + query GithubUserPins { + user(login: \"dustinrouillard\") { + pinnedItems(first: 6, types: [REPOSITORY]) { + totalCount + edges { + node { + ... on Repository { + owner { + login + } + name + description + stargazerCount + forkCount + primaryLanguage { + name + color + } + pushedAt + url + } + } + } + } + } + } + "; + + let valkey = &mut state.valkey.clone(); + + let cached = redis::cmd("GET") + .arg("cache/github/pinned") + .query_async::(&mut valkey.cm) + .await; + + let response: Vec = if let Err(_) = cached { + let config = Config::init_from_env().unwrap(); + + let mut headers = HashMap::new(); + headers.insert("user-agent", "rest.dstn.to/2.0".to_string()); + headers + .insert("authorization", format!("token {}", config.github_pat)); + + let client = + Client::new_with_headers("https://api.github.com/graphql", headers); + + let data = client.query::(query).await.unwrap(); + + let response = data + .unwrap() + .user + .pinned_items + .edges + .iter() + .map(|edge| { + json!({ + "owner": edge.node.owner.login, + "name": edge.node.name, + "description": edge.node.description, + "stars": edge.node.stargazer_count, + "forks": edge.node.fork_count, + "language": edge.node.primary_language, + "pushed_at": edge.node.pushed_at, + "url": edge.node.url, + }) + }) + .collect(); + + let _: Result = valkey + .cm + .set_ex("cache/github/pinned", json!(response).to_string(), 1800) + .await; + + response + } else { + serde_json::from_str(&cached.unwrap()).unwrap() + }; + + Ok( + HttpResponse::Ok() + .insert_header(("Content-Type", "application/json")) + .body(json!({"repositories": response}).to_string()), + ) +} diff --git a/src/services/mod.rs b/src/services/mod.rs index bba0000..fd7671b 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,4 +1,5 @@ pub mod base; pub mod blog; +pub mod github; pub mod spotify; pub mod uploads; diff --git a/src/services/spotify/factory.rs b/src/services/spotify/factory.rs index f767580..b0c0a52 100644 --- a/src/services/spotify/factory.rs +++ b/src/services/spotify/factory.rs @@ -4,6 +4,7 @@ use crate::services; pub fn spotify_factory() -> Scope { web::scope("/spotify") + .service(services::spotify::routes::recent_listens) .service(services::spotify::routes::current) .service(services::spotify::routes::authorize) .service(services::spotify::routes::setup) diff --git a/src/services/spotify/routes.rs b/src/services/spotify/routes.rs index c939d37..b9276f7 100644 --- a/src/services/spotify/routes.rs +++ b/src/services/spotify/routes.rs @@ -1,13 +1,15 @@ use actix_web::{get, http::Error, web, HttpResponse}; use envconfig::Envconfig; +use prisma_client_rust::Direction; use redis::{aio::ConnectionManager, AsyncCommands}; use serde::{Deserialize, Serialize}; use serde_json::json; use crate::{ config::Config, + connectivity::prisma::spotify_history, services::spotify::helpers, - structs::spotify::{AuthorizationData, SpotifyTokens}, + structs::spotify::{AuthorizationData, RecentSongQuery, SpotifyTokens}, ServerState, }; @@ -36,6 +38,49 @@ async fn current( ) } +#[get("/recents")] +async fn recent_listens( + state: web::Data, + query: Option>, +) -> Result { + let prisma = &mut &state.prisma; + + let query: web::Query = query + .unwrap_or(actix_web::web::Query(RecentSongQuery { limit: Some(10) })); + + let recents = prisma + .spotify_history() + .find_many(vec![]) + .order_by(spotify_history::listened_at::order(Direction::Desc)) + .take(query.limit.unwrap_or(10)) + .include(spotify_history::include!({ spotify_devices: select { id name r#type } })) + .exec() + .await; + + let recents: Vec = recents + .unwrap() + .into_iter() + .map(|recent| { + json!({ + "id": recent.id, + "type": recent.r#type, + "name": recent.name, + "artists": recent.artists, + "length": recent.length, + "image": recent.image, + "device": recent.spotify_devices, + "listened_at": recent.listened_at + }) + }) + .collect(); + + Ok( + HttpResponse::Ok() + .insert_header(("Content-Type", "application/json")) + .body(json!({"recents": recents}).to_string()), + ) +} + #[get("/authorize")] async fn authorize( data: web::Data, diff --git a/src/structs/github.rs b/src/structs/github.rs new file mode 100644 index 0000000..88c8217 --- /dev/null +++ b/src/structs/github.rs @@ -0,0 +1,48 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Data { + pub user: User, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct User { + pub pinned_items: PinnedItems, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PinnedItems { + pub total_count: i64, + pub edges: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Edge { + pub node: Node, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Node { + pub owner: Owner, + pub name: String, + pub description: String, + pub stargazer_count: i64, + pub fork_count: i64, + pub primary_language: Option, + pub pushed_at: String, + pub url: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Owner { + pub login: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct PrimaryLanguage { + pub name: String, + pub color: String, +} diff --git a/src/structs/mod.rs b/src/structs/mod.rs index 10ca48c..dc4b9fa 100644 --- a/src/structs/mod.rs +++ b/src/structs/mod.rs @@ -1,3 +1,4 @@ pub mod blog; +pub mod github; pub mod spotify; pub mod uploads; diff --git a/src/structs/spotify.rs b/src/structs/spotify.rs index fafcef0..8cdfb8f 100644 --- a/src/structs/spotify.rs +++ b/src/structs/spotify.rs @@ -2,6 +2,11 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; extern crate serde_json; +#[derive(Deserialize, Debug)] +pub struct RecentSongQuery { + pub limit: Option, +} + #[derive(Debug, Serialize, Deserialize)] pub struct PlayerState { pub device: Device,