diff --git a/migrations/migrations/definitions/_initial.json b/migrations/migrations/definitions/_initial.json index eaf9381..2dcc369 100644 --- a/migrations/migrations/definitions/_initial.json +++ b/migrations/migrations/definitions/_initial.json @@ -1 +1 @@ -{"schemas":"DEFINE TABLE OVERWRITE activity_preference SCHEMAFULL;\n\nDEFINE FIELD OVERWRITE login ON activity_preference TYPE bool DEFAULT false;\nDEFINE FIELD OVERWRITE add_influence ON activity_preference TYPE bool DEFAULT true;\nDEFINE FIELD OVERWRITE remove_influence ON activity_preference TYPE bool DEFAULT false;\nDEFINE FIELD OVERWRITE add_user_beatmap ON activity_preference TYPE bool DEFAULT true;\nDEFINE FIELD OVERWRITE remove_user_beatmap ON activity_preference TYPE bool DEFAULT false;\nDEFINE FIELD OVERWRITE add_influence_beatmap ON activity_preference TYPE bool DEFAULT true;\nDEFINE FIELD OVERWRITE remove_influence_beatmap ON activity_preference TYPE bool DEFAULT false;\nDEFINE FIELD OVERWRITE edit_influence_description ON activity_preference TYPE bool DEFAULT true;\nDEFINE FIELD OVERWRITE edit_influence_type ON activity_preference TYPE bool DEFAULT true;\nDEFINE FIELD OVERWRITE edit_bio ON activity_preference TYPE bool DEFAULT true;\n\nDEFINE FUNCTION OVERWRITE fn::id_or_null($value: any) -> any {\n IF $value.is_none() {\n RETURN NONE;\n }\n ELSE {\n RETURN meta::id($value);\n };\n} PERMISSIONS FULL;\n\nDEFINE FUNCTION OVERWRITE fn::add_possible_nulls($var1: any, $var2: any) -> any {\n IF type::is::none($var1) AND type::is::none($var2) {\n RETURN NONE;\n }\n ELSE {\n RETURN $var1 + $var2;\n };\n} PERMISSIONS FULL;\n\nDEFINE TABLE OVERWRITE influenced_by SCHEMAFULL TYPE RELATION IN user OUT user ENFORCED;\n\nDEFINE FIELD OVERWRITE influence_type on influenced_by TYPE int DEFAULT 1;\nDEFINE FIELD OVERWRITE description ON influenced_by TYPE string DEFAULT \"\";\nDEFINE FIELD OVERWRITE beatmaps ON influenced_by TYPE set DEFAULT [];\nDEFINE FIELD OVERWRITE updated_at ON influenced_by type datetime VALUE time::now();\nDEFINE FIELD OVERWRITE created_at ON influenced_by type datetime VALUE time::now() READONLY;\n\n// COUNTLESS HOURS LOST BECAUSE I USED VALUE INSTEAD OF DEFAULT\nDEFINE FIELD OVERWRITE order on influenced_by TYPE int \nDEFAULT (UPSERT increment:order SET increment+=1 RETURN increment).at(0).values().at(0);\n\nDEFINE INDEX OVERWRITE unique_in_out ON TABLE influenced_by COLUMNS in, out UNIQUE;\n\nDEFINE TABLE OVERWRITE script_migration SCHEMAFULL\n PERMISSIONS\n FOR select FULL\n FOR create, update, delete NONE;\n\nDEFINE FIELD OVERWRITE script_name ON script_migration TYPE string;\nDEFINE FIELD OVERWRITE executed_at ON script_migration TYPE datetime VALUE time::now() READONLY;\nDEFINE TABLE OVERWRITE user SCHEMAFULL;\n\nDEFINE FIELD OVERWRITE username ON user TYPE string;\nDEFINE FIELD OVERWRITE avatar_url ON user TYPE string;\nDEFINE FIELD OVERWRITE bio ON user TYPE string DEFAULT \"\";\nDEFINE FIELD OVERWRITE ranked_mapper ON user TYPE bool DEFAULT false;\nDEFINE FIELD OVERWRITE authenticated ON user TYPE bool DEFAULT false;\nDEFINE FIELD OVERWRITE beatmaps ON user TYPE set DEFAULT [];\nDEFINE FIELD OVERWRITE updated_at ON user type datetime VALUE time::now();\nDEFINE FIELD OVERWRITE created_at ON user type datetime VALUE time::now() READONLY;\nDEFINE FIELD OVERWRITE country_name ON user TYPE string;\nDEFINE FIELD OVERWRITE country_code ON user TYPE string;\nDEFINE FIELD OVERWRITE groups ON user FLEXIBLE TYPE array;\nDEFINE FIELD OVERWRITE previous_usernames ON user TYPE array;\nDEFINE FIELD OVERWRITE ranked_and_approved_beatmapset_count ON user TYPE int;\nDEFINE FIELD OVERWRITE ranked_beatmapset_count ON user TYPE int;\nDEFINE FIELD OVERWRITE nominated_beatmapset_count ON user TYPE int;\nDEFINE FIELD OVERWRITE guest_beatmapset_count ON user TYPE int;\nDEFINE FIELD OVERWRITE loved_beatmapset_count ON user TYPE int;\nDEFINE FIELD OVERWRITE graveyard_beatmapset_count ON user TYPE int;\nDEFINE FIELD OVERWRITE pending_beatmapset_count ON user TYPE int;\nDEFINE FIELD OVERWRITE activity_preference ON user TYPE option>;\n\nDEFINE INDEX OVERWRITE country_name_index ON TABLE user COLUMNS country_name;\n","events":"DEFINE EVENT OVERWRITE add_influence ON TABLE influenced_by\nWHEN \n $session.tk.ID == \"backend\" AND $event == \"CREATE\"\nTHEN (\n CREATE activity \n SET user = $after.in, \n created_at = time::now(),\n event_type = \"ADD_INFLUENCE\", \n influence = {\n id: $after.id,\n out: $after.out,\n }\n);\n\nDEFINE EVENT OVERWRITE add_influence_beatmap ON TABLE influenced_by\nWHEN\n $session.tk.ID == \"backend\"\n AND $event == \"UPDATE\"\n AND array::len($after.beatmaps) > array::len($before.beatmaps)\nTHEN (\n CREATE activity \n SET user = $after.in,\n created_at = time::now(),\n event_type = \"ADD_INFLUENCE_BEATMAP\", \n beatmap = array::complement($after.beatmaps, $before.beatmaps).at(0),\n influence = {\n id: $after.id,\n out: $after.out,\n }\n);\n\nDEFINE EVENT OVERWRITE add_user_beatmap ON TABLE user \nWHEN \n $session.tk.ID == \"backend\" \n AND $event == \"UPDATE\" \n AND array::len($after.beatmaps) > array::len($before.beatmaps) \nTHEN (\n CREATE activity \n SET user = $after.id,\n created_at = time::now(),\n event_type = \"ADD_USER_BEATMAP\", \n beatmap = array::complement($after.beatmaps, $before.beatmaps).at(0)\n);\n\n// edit_bio logs when creating user, so added before != null\nDEFINE EVENT OVERWRITE edit_bio ON TABLE user \nWHEN \n $session.tk.ID == \"backend\"\n AND $event == \"UPDATE\"\n AND $before.bio != $after.bio\nTHEN (\n CREATE activity \n SET user = $after.id, \n created_at = time::now(), \n event_type = \"EDIT_BIO\", \n bio = $after.bio\n);\n\nDEFINE EVENT OVERWRITE edit_influence_description ON TABLE influenced_by\nWHEN\n $session.tk.ID == \"backend\"\n AND $event == \"UPDATE\"\n AND $before.description != $after.description\nTHEN (\n CREATE activity \n SET user = $after.in, \n created_at = time::now(), \n event_type = \"EDIT_INFLUENCE_DESC\", \n description = $after.description,\n influence = {\n id: $after.id,\n out: $after.out,\n }\n);\n\nDEFINE EVENT OVERWRITE edit_influence_type ON TABLE influenced_by \nWHEN \n $session.tk.ID == \"backend\"\n AND $event == \"UPDATE\"\n AND $before.influence_type != $after.influence_type \nTHEN (\n CREATE activity \n SET user = $after.in, \n created_at = time::now(), \n event_type = \"EDIT_INFLUENCE_TYPE\", \n influence_type= $after.influence_type,\n influence = {\n id: $after.id,\n out: $after.out,\n }\n);\n\nDEFINE EVENT OVERWRITE remove_influence ON TABLE influenced_by\nWHEN \n $session.tk.ID == \"backend\" AND $event == \"DELETE\"\nTHEN (\n CREATE activity \n SET user = $before.in, \n created_at = time::now(),\n event_type = \"REMOVE_INFLUENCE\", \n influence = $before\n);\n\nDEFINE EVENT OVERWRITE remove_influence_beatmap ON TABLE influenced_by\nWHEN\n $session.tk.ID == \"backend\"\n AND $event == \"UPDATE\"\n AND array::len($before.beatmaps) > array::len($after.beatmaps)\nTHEN (\n CREATE activity \n SET user = $after.in, \n created_at = time::now(),\n event_type = \"REMOVE_INFLUENCE_BEATMAP\", \n beatmap = array::complement($before.beatmaps, $after.beatmaps).at(0),\n influence = {\n id: $after.id,\n out: $after.out,\n }\n);\n\nDEFINE EVENT OVERWRITE remove_user_beatmap ON TABLE user \nWHEN \n $session.tk.ID == \"backend\"\n AND $event == \"UPDATE\"\n AND array::len($before.beatmaps) > array::len($after.beatmaps)\nTHEN (\n CREATE activity \n SET user = $after.id, \n created_at = time::now(),\n event_type = \"REMOVE_USER_BEATMAP\", \n beatmap = array::complement($before.beatmaps, $after.beatmaps).at(0)\n);\n"} \ No newline at end of file +{"schemas":"DEFINE FUNCTION OVERWRITE fn::id_or_null($value: any) -> any {\n IF $value.is_none() {\n RETURN NONE;\n }\n ELSE {\n RETURN meta::id($value);\n };\n} PERMISSIONS FULL;\n\nDEFINE FUNCTION OVERWRITE fn::add_possible_nulls($var1: any, $var2: any) -> any {\n IF type::is::none($var1) AND type::is::none($var2) {\n RETURN NONE;\n }\n ELSE {\n RETURN $var1 + $var2;\n };\n} PERMISSIONS FULL;\n\nDEFINE TABLE OVERWRITE influenced_by SCHEMAFULL TYPE RELATION IN user OUT user ENFORCED;\n\nDEFINE FIELD OVERWRITE influence_type on influenced_by TYPE int DEFAULT 1;\nDEFINE FIELD OVERWRITE description ON influenced_by TYPE string DEFAULT \"\";\nDEFINE FIELD OVERWRITE beatmaps ON influenced_by TYPE set DEFAULT [];\nDEFINE FIELD OVERWRITE updated_at ON influenced_by type datetime VALUE time::now();\nDEFINE FIELD OVERWRITE created_at ON influenced_by type datetime VALUE time::now() READONLY;\n\n// COUNTLESS HOURS LOST BECAUSE I USED VALUE INSTEAD OF DEFAULT\nDEFINE FIELD OVERWRITE order on influenced_by TYPE int \nDEFAULT (UPSERT increment:order SET increment+=1 RETURN increment).at(0).values().at(0);\n\nDEFINE INDEX OVERWRITE unique_in_out ON TABLE influenced_by COLUMNS in, out UNIQUE;\n\nDEFINE TABLE OVERWRITE script_migration SCHEMAFULL\n PERMISSIONS\n FOR select FULL\n FOR create, update, delete NONE;\n\nDEFINE FIELD OVERWRITE script_name ON script_migration TYPE string;\nDEFINE FIELD OVERWRITE executed_at ON script_migration TYPE datetime VALUE time::now() READONLY;\nDEFINE TABLE OVERWRITE user SCHEMAFULL;\n\nDEFINE FIELD OVERWRITE username ON user TYPE string;\nDEFINE FIELD OVERWRITE avatar_url ON user TYPE string;\nDEFINE FIELD OVERWRITE bio ON user TYPE string DEFAULT \"\";\nDEFINE FIELD OVERWRITE ranked_mapper ON user TYPE bool DEFAULT false;\nDEFINE FIELD OVERWRITE authenticated ON user TYPE bool DEFAULT false;\nDEFINE FIELD OVERWRITE beatmaps ON user TYPE set DEFAULT [];\nDEFINE FIELD OVERWRITE updated_at ON user type datetime VALUE time::now();\nDEFINE FIELD OVERWRITE created_at ON user type datetime VALUE time::now() READONLY;\nDEFINE FIELD OVERWRITE country_name ON user TYPE string;\nDEFINE FIELD OVERWRITE country_code ON user TYPE string;\nDEFINE FIELD OVERWRITE groups ON user FLEXIBLE TYPE array;\nDEFINE FIELD OVERWRITE previous_usernames ON user TYPE array;\nDEFINE FIELD OVERWRITE ranked_and_approved_beatmapset_count ON user TYPE int;\nDEFINE FIELD OVERWRITE ranked_beatmapset_count ON user TYPE int;\nDEFINE FIELD OVERWRITE nominated_beatmapset_count ON user TYPE int;\nDEFINE FIELD OVERWRITE guest_beatmapset_count ON user TYPE int;\nDEFINE FIELD OVERWRITE loved_beatmapset_count ON user TYPE int;\nDEFINE FIELD OVERWRITE graveyard_beatmapset_count ON user TYPE int;\nDEFINE FIELD OVERWRITE pending_beatmapset_count ON user TYPE int;\n\nDEFINE FIELD OVERWRITE activity_preferences ON user FLEXIBLE TYPE object \nDEFAULT{\n add_influence: true,\n add_influence_beatmap: true,\n add_user_beatmap: true,\n edit_bio: true,\n edit_influence_description: true,\n edit_influence_type: true,\n login: false,\n remove_influence: false,\n remove_influence_beatmap: false,\n remove_user_beatmap: false,\n};\n","events":"DEFINE EVENT OVERWRITE add_influence ON TABLE influenced_by\nWHEN \n $session.tk.ID == \"backend\" AND $event == \"CREATE\"\nTHEN (\n CREATE activity \n SET user = $after.in, \n created_at = time::now(),\n event_type = \"ADD_INFLUENCE\", \n influence = {\n id: $after.id,\n out: $after.out,\n }\n);\n\nDEFINE EVENT OVERWRITE add_influence_beatmap ON TABLE influenced_by\nWHEN\n $session.tk.ID == \"backend\"\n AND $event == \"UPDATE\"\n AND array::len($after.beatmaps) > array::len($before.beatmaps)\nTHEN (\n CREATE activity \n SET user = $after.in,\n created_at = time::now(),\n event_type = \"ADD_INFLUENCE_BEATMAP\", \n beatmap = array::complement($after.beatmaps, $before.beatmaps).at(0),\n influence = {\n id: $after.id,\n out: $after.out,\n }\n);\n\nDEFINE EVENT OVERWRITE add_user_beatmap ON TABLE user \nWHEN \n $session.tk.ID == \"backend\" \n AND $event == \"UPDATE\" \n AND array::len($after.beatmaps) > array::len($before.beatmaps) \nTHEN (\n CREATE activity \n SET user = $after.id,\n created_at = time::now(),\n event_type = \"ADD_USER_BEATMAP\", \n beatmap = array::complement($after.beatmaps, $before.beatmaps).at(0)\n);\n\n// edit_bio logs when creating user, so added before != null\nDEFINE EVENT OVERWRITE edit_bio ON TABLE user \nWHEN \n $session.tk.ID == \"backend\"\n AND $event == \"UPDATE\"\n AND $before.bio != $after.bio\nTHEN (\n CREATE activity \n SET user = $after.id, \n created_at = time::now(), \n event_type = \"EDIT_BIO\", \n bio = $after.bio\n);\n\nDEFINE EVENT OVERWRITE edit_influence_description ON TABLE influenced_by\nWHEN\n $session.tk.ID == \"backend\"\n AND $event == \"UPDATE\"\n AND $before.description != $after.description\nTHEN (\n CREATE activity \n SET user = $after.in, \n created_at = time::now(), \n event_type = \"EDIT_INFLUENCE_DESC\", \n description = $after.description,\n influence = {\n id: $after.id,\n out: $after.out,\n }\n);\n\nDEFINE EVENT OVERWRITE edit_influence_type ON TABLE influenced_by \nWHEN \n $session.tk.ID == \"backend\"\n AND $event == \"UPDATE\"\n AND $before.influence_type != $after.influence_type \nTHEN (\n CREATE activity \n SET user = $after.in, \n created_at = time::now(), \n event_type = \"EDIT_INFLUENCE_TYPE\", \n influence_type= $after.influence_type,\n influence = {\n id: $after.id,\n out: $after.out,\n }\n);\n\nDEFINE EVENT OVERWRITE remove_influence ON TABLE influenced_by\nWHEN \n $session.tk.ID == \"backend\" AND $event == \"DELETE\"\nTHEN (\n CREATE activity \n SET user = $before.in, \n created_at = time::now(),\n event_type = \"REMOVE_INFLUENCE\", \n influence = $before\n);\n\nDEFINE EVENT OVERWRITE remove_influence_beatmap ON TABLE influenced_by\nWHEN\n $session.tk.ID == \"backend\"\n AND $event == \"UPDATE\"\n AND array::len($before.beatmaps) > array::len($after.beatmaps)\nTHEN (\n CREATE activity \n SET user = $after.in, \n created_at = time::now(),\n event_type = \"REMOVE_INFLUENCE_BEATMAP\", \n beatmap = array::complement($before.beatmaps, $after.beatmaps).at(0),\n influence = {\n id: $after.id,\n out: $after.out,\n }\n);\n\nDEFINE EVENT OVERWRITE remove_user_beatmap ON TABLE user \nWHEN \n $session.tk.ID == \"backend\"\n AND $event == \"UPDATE\"\n AND array::len($before.beatmaps) > array::len($after.beatmaps)\nTHEN (\n CREATE activity \n SET user = $after.id, \n created_at = time::now(),\n event_type = \"REMOVE_USER_BEATMAP\", \n beatmap = array::complement($before.beatmaps, $after.beatmaps).at(0)\n);\n"} \ No newline at end of file diff --git a/migrations/schemas/activity_preference.surql b/migrations/schemas/activity_preference.surql deleted file mode 100644 index 33965d8..0000000 --- a/migrations/schemas/activity_preference.surql +++ /dev/null @@ -1,12 +0,0 @@ -DEFINE TABLE OVERWRITE activity_preference SCHEMAFULL; - -DEFINE FIELD OVERWRITE login ON activity_preference TYPE bool DEFAULT false; -DEFINE FIELD OVERWRITE add_influence ON activity_preference TYPE bool DEFAULT true; -DEFINE FIELD OVERWRITE remove_influence ON activity_preference TYPE bool DEFAULT false; -DEFINE FIELD OVERWRITE add_user_beatmap ON activity_preference TYPE bool DEFAULT true; -DEFINE FIELD OVERWRITE remove_user_beatmap ON activity_preference TYPE bool DEFAULT false; -DEFINE FIELD OVERWRITE add_influence_beatmap ON activity_preference TYPE bool DEFAULT true; -DEFINE FIELD OVERWRITE remove_influence_beatmap ON activity_preference TYPE bool DEFAULT false; -DEFINE FIELD OVERWRITE edit_influence_description ON activity_preference TYPE bool DEFAULT true; -DEFINE FIELD OVERWRITE edit_influence_type ON activity_preference TYPE bool DEFAULT true; -DEFINE FIELD OVERWRITE edit_bio ON activity_preference TYPE bool DEFAULT true; diff --git a/migrations/schemas/user.surql b/migrations/schemas/user.surql index d3d039d..c5b82a2 100644 --- a/migrations/schemas/user.surql +++ b/migrations/schemas/user.surql @@ -19,6 +19,17 @@ DEFINE FIELD OVERWRITE guest_beatmapset_count ON user TYPE int; DEFINE FIELD OVERWRITE loved_beatmapset_count ON user TYPE int; DEFINE FIELD OVERWRITE graveyard_beatmapset_count ON user TYPE int; DEFINE FIELD OVERWRITE pending_beatmapset_count ON user TYPE int; -DEFINE FIELD OVERWRITE activity_preference ON user TYPE option>; -DEFINE INDEX OVERWRITE country_name_index ON TABLE user COLUMNS country_name; +DEFINE FIELD OVERWRITE activity_preferences ON user FLEXIBLE TYPE object +DEFAULT{ + add_influence: true, + add_influence_beatmap: true, + add_user_beatmap: true, + edit_bio: true, + edit_influence_description: true, + edit_influence_type: true, + login: false, + remove_influence: false, + remove_influence_beatmap: false, + remove_user_beatmap: false, +}; diff --git a/src/daily_update.rs b/src/daily_update.rs new file mode 100644 index 0000000..0a7be36 --- /dev/null +++ b/src/daily_update.rs @@ -0,0 +1,54 @@ +use std::{sync::Arc, time::Duration}; + +use crate::{ + database::DatabaseClient, osu_api::credentials_grant::CredentialsGrantClient, retry::Retryable, +}; + +pub async fn update_once( + client: Arc, + database: Arc, + users_to_update: Vec, + wait_duration: Duration, +) { + let mut interval = tokio::time::interval(wait_duration); + for user_id in users_to_update { + interval.tick().await; + let Ok(user) = client.get_user_osu(user_id).await else { + tracing::error!( + "Failed to request {} from osu! API for daily update", + user_id + ); + continue; + }; + let Ok(_) = database.upsert_user(user).await else { + tracing::error!( + "Failed to insert user {} to database for daily update", + user_id + ); + continue; + }; + tracing::debug!("Requested and inserted user {} for daily update", user_id); + } +} + +pub async fn update_routine( + client: Arc, + mut database: Arc, + initial_sleep_time: Duration, +) { + tokio::time::sleep(initial_sleep_time).await; + let mut interval = tokio::time::interval(Duration::from_secs(60 * 60 * 24)); + loop { + interval.tick().await; + let users_to_update: Vec = database + .retry_until_success(60, "Failed to fetch users for daily update") + .await; + update_once( + client.clone(), + database.clone(), + users_to_update, + Duration::from_secs(15), + ) + .await; + } +} diff --git a/src/database/activity.rs b/src/database/activity.rs index e11caeb..eb4df21 100644 --- a/src/database/activity.rs +++ b/src/database/activity.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use async_trait::async_trait; + use surrealdb::{method::QueryStream, Notification}; use crate::{error::AppError, handlers::activity::Activity, retry::Retryable}; @@ -83,9 +85,8 @@ impl DatabaseClient { } } -impl Retryable for Arc { - type Value = QueryStream>; - type Err = AppError; +#[async_trait] +impl Retryable>, AppError> for Arc { async fn retry(&mut self) -> Result>, AppError> { self.start_activity_stream().await } diff --git a/src/database/graph_vizualizer.rs b/src/database/graph_vizualizer.rs index d573b4b..8803f28 100644 --- a/src/database/graph_vizualizer.rs +++ b/src/database/graph_vizualizer.rs @@ -17,6 +17,7 @@ pub struct GraphUser { pub struct GraphInfluence { source: u32, target: u32, + influence_type: u8, } impl DatabaseClient { @@ -41,7 +42,7 @@ impl DatabaseClient { pub async fn get_influences_for_graph(&self) -> Result, AppError> { let graph_influences: Vec = self .db - .query("SELECT meta::id(in) AS source, meta::id(out) AS target FROM influenced_by;") + .query("SELECT meta::id(in) AS source, meta::id(out), influence_type AS target FROM influenced_by;") .await? .take(0)?; Ok(graph_influences) diff --git a/src/database/user.rs b/src/database/user.rs index b9e01d2..1a5ae56 100644 --- a/src/database/user.rs +++ b/src/database/user.rs @@ -1,3 +1,6 @@ +use std::sync::Arc; + +use async_trait::async_trait; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use surrealdb::sql::Thing; @@ -5,6 +8,7 @@ use surrealdb::sql::Thing; use crate::{ error::AppError, osu_api::{BeatmapEnum, Group, OsuBeatmapSmall, UserOsu}, + retry::Retryable, }; use super::{numerical_thing, DatabaseClient}; @@ -88,7 +92,14 @@ impl From for UserSmall { } } } -#[derive(Serialize, Deserialize, JsonSchema)] + +/// Needed to get return type from activities +#[derive(Serialize, Deserialize)] +pub struct ActivityPreferenceWrapper { + pub activity_preferences: ActivityPreferences, +} + +#[derive(Serialize, Deserialize, JsonSchema, Debug)] pub struct ActivityPreferences { pub add_influence: bool, pub add_influence_beatmap: bool, @@ -119,12 +130,13 @@ impl Default for ActivityPreferences { } } +#[derive(Deserialize, Debug)] +pub struct DbUserId { + pub id: u32, +} + impl DatabaseClient { - pub async fn upsert_user( - &self, - user_details: UserOsu, - authenticated: bool, - ) -> Result<(), AppError> { + pub async fn upsert_user(&self, user_details: UserOsu) -> Result<(), AppError> { let ranked_mapper = user_details.is_ranked_mapper(); self.db .query( @@ -133,7 +145,6 @@ impl DatabaseClient { SET username = $username, avatar_url = $avatar_url, - authenticated = $authenticated, ranked_mapper = $ranked_maps, country_code = $country_code, country_name = $country_name, @@ -151,7 +162,6 @@ impl DatabaseClient { .bind(("thing", numerical_thing("user", user_details.id))) .bind(("username", user_details.username)) .bind(("avatar_url", user_details.avatar_url)) - .bind(("authenticated", authenticated.then_some(true))) .bind(("ranked_maps", ranked_mapper)) .bind(("country_code", user_details.country.code)) .bind(("country_name", user_details.country.name)) @@ -189,6 +199,14 @@ impl DatabaseClient { Ok(()) } + pub async fn set_authenticated(&self, user_id: u32) -> Result<(), AppError> { + self.db + .query("UPDATE $thing SET authenticated = true") + .bind(("thing", numerical_thing("user", user_id))) + .await?; + Ok(()) + } + fn single_user_return_string(&self) -> &str { " meta::id(id) as id, @@ -336,61 +354,18 @@ impl DatabaseClient { user_id: u32, preferences: ActivityPreferences, ) -> Result { - let query = r#" - IF $thing.activity_preference != NONE { - RETURN UPDATE ONLY $thing.activity_preference SET - add_influence = $add_influence, - add_influence_beatmap = $add_influence_beatmap, - add_user_beatmap = $add_user_beatmap, - edit_bio = $edit_bio, - edit_influence_description = $edit_influence_description, - edit_influence_type = $edit_influence_type, - login = $login, - remove_influence = $remove_influence, - remove_influence_beatmap = $remove_influence_beatmap, - remove_user_beatmap = $remove_user_beatmap; - } ELSE { - LET $created_preference = (CREATE ONLY activity_preference SET - add_influence = $add_influence, - add_influence_beatmap = $add_influence_beatmap, - add_user_beatmap = $add_user_beatmap, - edit_bio = $edit_bio, - edit_influence_description = $edit_influence_description, - edit_influence_type = $edit_influence_type, - login = $login, - remove_influence = $remove_influence, - remove_influence_beatmap = $remove_influence_beatmap, - remove_user_beatmap = $remove_user_beatmap - ); - UPDATE ONLY $thing SET activity_preference = $created_preference.id; - RETURN $created_preference; - }; - "#; - - let preferences: Option = self + let preference_wrapper: Option = self .db - .query(query) + .query( + "UPDATE $thing SET activity_preferences = $preferences RETURN activity_preferences", + ) .bind(("thing", numerical_thing("user", user_id))) - .bind(("add_influence", preferences.add_influence)) - .bind(("add_influence_beatmap", preferences.add_influence_beatmap)) - .bind(("add_user_beatmap", preferences.add_user_beatmap)) - .bind(("edit_bio", preferences.edit_bio)) - .bind(( - "edit_influence_description", - preferences.edit_influence_description, - )) - .bind(("edit_influence_type", preferences.edit_influence_type)) - .bind(("login", preferences.login)) - .bind(("remove_influence", preferences.remove_influence)) - .bind(( - "remove_influence_beatmap", - preferences.remove_influence_beatmap, - )) - .bind(("remove_user_beatmap", preferences.remove_user_beatmap)) + .bind(("preferences", preferences)) .await? .take(0)?; - preferences.ok_or(AppError::ActivityPreferencesQuery) + let preference_wrapper = preference_wrapper.ok_or(AppError::ActivityPreferencesQuery)?; + Ok(preference_wrapper.activity_preferences) } /// Returns an [`ActivityPreferences`] directly if the data is in DB. @@ -399,12 +374,31 @@ impl DatabaseClient { &self, user_id: u32, ) -> Result { - let preferences: Option = self + let preference_wrapper: Option = self .db - .query("SELECT * FROM $thing.activity_preference") + .query("SELECT activity_preferences FROM ONLY $thing") .bind(("thing", numerical_thing("user", user_id))) .await? .take(0)?; - Ok(preferences.unwrap_or_default()) + let preference_wrapper = preference_wrapper.ok_or(AppError::MissingUser(user_id))?; + Ok(preference_wrapper.activity_preferences) + } + + pub async fn get_users_to_update(&self) -> Result, AppError> { + let ids: Vec = self + .db + .query("SELECT meta::id(id) as id FROM user WHERE updated_at + 1s < time::now()") + .await? + .take(0)?; + + let ids = ids.into_iter().map(|db_id| db_id.id).collect(); + Ok(ids) + } +} + +#[async_trait] +impl Retryable, AppError> for Arc { + async fn retry(&mut self) -> Result, AppError> { + self.get_users_to_update().await } } diff --git a/src/error.rs b/src/error.rs index ad78984..a5249c6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,5 @@ +use std::num::ParseIntError; + use aide::OperationIo; use axum::{http::StatusCode, response::IntoResponse, Json}; use serde::Serialize; @@ -68,6 +70,9 @@ pub enum AppError { #[error("Error in activity preferences query")] ActivityPreferencesQuery, + + #[error("Parse int: {0}")] + ParseInt(#[from] ParseIntError), } #[derive(Serialize)] @@ -97,7 +102,7 @@ impl IntoResponse for AppError { AppError::MissingTokenCookie | AppError::JwtVerification | AppError::WrongAdminPassword => StatusCode::UNAUTHORIZED, - AppError::MissingLayerJson | AppError::StringTooLong => { + AppError::MissingLayerJson | AppError::StringTooLong | AppError::ParseInt(_) => { StatusCode::UNPROCESSABLE_ENTITY } AppError::MissingInfluence | AppError::MissingUser(_) | Self::NonExistingMap(_) => { diff --git a/src/handlers/activity.rs b/src/handlers/activity.rs index 186234c..8ae07c2 100644 --- a/src/handlers/activity.rs +++ b/src/handlers/activity.rs @@ -15,7 +15,7 @@ use axum::{ use futures::{SinkExt, StreamExt}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use surrealdb::{sql::Datetime, Action}; +use surrealdb::{method::QueryStream, sql::Datetime, Action, Notification}; use tokio::sync::{ broadcast::{self, Receiver, Sender}, Mutex, @@ -348,7 +348,7 @@ impl ActivityTracker { } async fn start_loop(self: Arc, mut db: Arc) -> Result<(), AppError> { - let mut stream = db + let mut stream: QueryStream> = db .retry_until_success(60, "Failed to start activity stream") .await; let broadcast_sender = self.activity_broadcaster.clone(); diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index b993361..59f8a49 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -70,10 +70,14 @@ pub async fn osu_oauth2_redirect( .parse() .unwrap(), ); + + // TODO: maybe fix authorized thing to be in the same query later? + let osu_user_id = osu_user.id; try_join!( - state.db.add_login_activity(osu_user.id), - state.db.upsert_user(osu_user, true) + state.db.add_login_activity(osu_user_id), + state.db.upsert_user(osu_user) )?; + state.db.set_authenticated(osu_user_id).await?; Ok(redirect_response) } diff --git a/src/handlers/influence.rs b/src/handlers/influence.rs index 2962a14..e500d98 100644 --- a/src/handlers/influence.rs +++ b/src/handlers/influence.rs @@ -27,22 +27,26 @@ pub struct Description { } /// `InfluenceCreationOptions` type. Optional fields to override defaults +/// TODO: for the love of god, let's fix this rename later #[derive(Deserialize, JsonSchema)] pub struct InfluenceCreationOptions { pub influence_type: Option, pub description: Option, pub beatmaps: Option>, + #[serde(rename = "userId")] + pub user_id: String, } pub async fn add_influence( - Path(influenced_to): Path, Extension(auth_data): Extension, State(state): State>, Json(options): Json, ) -> Result, AppError> { + let influenced_to = options.user_id.parse::()?; + let target_user = state .request - .get_user_osu(&auth_data.osu_token, influenced_to.value) + .get_user_osu(&auth_data.osu_token, influenced_to) .await?; if let Some(influence_beatmaps) = &options.beatmaps { @@ -55,10 +59,10 @@ pub async fn add_influence( } let (_, mut influence) = try_join!( - state.db.upsert_user(target_user, false), + state.db.upsert_user(target_user), state .db - .add_influence_relation(auth_data.user_id, influenced_to.value, options) + .add_influence_relation(auth_data.user_id, influenced_to, options) )?; swap_beatmaps( diff --git a/src/lib.rs b/src/lib.rs index 4c7f35f..0991866 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ use osu_api::credentials_grant::CredentialsGrantClient; use osu_api::request::Requester; pub mod custom_cache; +pub mod daily_update; pub mod database; pub mod documentation; pub mod error; @@ -36,11 +37,11 @@ pub struct AppState { } impl AppState { - pub async fn new(request: Arc, db: Arc) -> Arc { - let credentials_grant_client = CredentialsGrantClient::new(request.clone()) - .await - .expect("Failed to initialize credentials grant client"); - + pub async fn new( + request: Arc, + credentials_grant_client: Arc, + db: Arc, + ) -> Arc { let cached_combined_requester = CombinedRequester::new(request.clone(), "https://osu.ppy.sh"); @@ -94,7 +95,7 @@ pub fn routes(state: Arc) -> ApiRouter> { get_with(handlers::osu_search::osu_user_search, |op| op.tag("Search")), ) .api_route( - "/influence/:influenced_to", + "/influence", post_with(handlers::influence::add_influence, |op| op.tag("Influence")), ) .api_route( diff --git a/src/main.rs b/src/main.rs index 19f1526..2e9d6d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::{net::SocketAddr, sync::Arc}; +use std::{net::SocketAddr, sync::Arc, time::Duration}; use aide::{axum::ApiRouter, openapi::OpenApi}; use axum::{ @@ -8,7 +8,10 @@ use axum::{ }; use axum_swagger_ui::swagger_ui; use mapper_influences_backend_rs::{ - database::DatabaseClient, osu_api::request::OsuApiRequestClient, routes, AppState, + daily_update::update_routine, + database::{user::ActivityPreferences, DatabaseClient}, + osu_api::{credentials_grant::CredentialsGrantClient, request::OsuApiRequestClient}, + routes, AppState, }; use tower_http::{compression::CompressionLayer, cors::CorsLayer, trace::TraceLayer}; use tracing::info; @@ -23,14 +26,23 @@ async fn main() { .with_span_events(FmtSpan::CLOSE) .init(); - // initializing client wrappers and state let url = std::env::var("SURREAL_URL").expect("Missing SURREAL_URL environment variable"); let db = DatabaseClient::new(&url) .await .expect("failed to initialize db connection"); let request = Arc::new(OsuApiRequestClient::new(10)); - let state = AppState::new(request, db).await; + let credentials_grant_client = CredentialsGrantClient::new(request.clone()) + .await + .expect("Failed to initialize credentials grant client"); + + let state = AppState::new(request, credentials_grant_client.clone(), db.clone()).await; + + tokio::spawn(update_routine( + credentials_grant_client, + db.clone(), + Duration::from_secs(120), + )); aide::gen::on_error(|error| { println!("{error}"); diff --git a/src/osu_api/request.rs b/src/osu_api/request.rs index 41f01ad..8dd69e4 100644 --- a/src/osu_api/request.rs +++ b/src/osu_api/request.rs @@ -182,9 +182,8 @@ impl Requester for OsuApiRequestClient { } } -impl Retryable for Arc { - type Value = OsuAuthToken; - type Err = AppError; +#[async_trait] +impl Retryable for Arc { async fn retry(&mut self) -> Result { self.get_client_credentials_token().await } diff --git a/src/retry.rs b/src/retry.rs index 7051468..c2b61f7 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -1,46 +1,34 @@ +use async_trait::async_trait; use std::{error::Error, time::Duration}; -use futures::Future; - -// TODO: use asycn_traits now that you imported it anyway? -pub trait Retryable { - type Value: Send; - type Err: Error + Send; - fn retry(&mut self) -> impl Future> + Send; - fn retry_until_success( - &mut self, - longest_cooldown: u32, - message: &str, - ) -> impl Future + Send - where - Self: Send, - { - async move { - let mut cooldown_fibo_last = 0; - let mut cooldown = 1; - let mut attempt = 1; - loop { - match self.retry().await { - Ok(value) => { - return value; - } - Err(error) => { - tracing::error!( - "{}. Trying to reconnect. Attempt {}, Cooldown {} secs. full error: {}", - message, - attempt, - cooldown, - error - ); - let fibo_temp = cooldown; - cooldown += cooldown_fibo_last; - if cooldown > longest_cooldown { - cooldown = longest_cooldown; - } - cooldown_fibo_last = fibo_temp; - attempt += 1; - tokio::time::sleep(Duration::from_secs(cooldown.into())).await; +#[async_trait] +pub trait Retryable: Send { + async fn retry(&mut self) -> Result; + async fn retry_until_success(&mut self, longest_cooldown: u32, message: &str) -> Value { + let mut cooldown_fibo_last = 0; + let mut cooldown = 1; + let mut attempt = 1; + loop { + match self.retry().await { + Ok(value) => { + return value; + } + Err(error) => { + tracing::error!( + "{}. Trying to reconnect. Attempt {}, Cooldown {} secs. full error: {}", + message, + attempt, + cooldown, + error + ); + let fibo_temp = cooldown; + cooldown += cooldown_fibo_last; + if cooldown > longest_cooldown { + cooldown = longest_cooldown; } + cooldown_fibo_last = fibo_temp; + attempt += 1; + tokio::time::sleep(Duration::from_secs(cooldown.into())).await; } } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 02845fc..2baa333 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -7,7 +7,10 @@ use axum::{ }; use axum_test::TestServer; use mapper_influences_backend_rs::{ - database::DatabaseClient, handlers, osu_api::request::OsuApiRequestClient, AppState, + database::DatabaseClient, + handlers, + osu_api::{credentials_grant::CredentialsGrantClient, request::OsuApiRequestClient}, + AppState, }; use osu_test_client::OsuApiTestClient; use surrealdb_migrations::MigrationRunner; @@ -130,8 +133,11 @@ pub async fn init_test_env( let working_request_client = Arc::new(OsuApiRequestClient::new(10)); let test_request_client = OsuApiTestClient::new(working_request_client.clone(), label); + let credentials_grant_client = CredentialsGrantClient::new(test_request_client.clone()) + .await + .expect("Failed to initialize credentials grant client"); - let state = AppState::new(test_request_client.clone(), db).await; + let state = AppState::new(test_request_client.clone(), credentials_grant_client, db).await; // Requesting peppy to add in our initial database let test_initial_user = state @@ -139,7 +145,7 @@ pub async fn init_test_env( .get_user_osu(2) .await .unwrap(); - state.db.upsert_user(test_initial_user, true).await.unwrap(); + state.db.upsert_user(test_initial_user).await.unwrap(); let routes = test_routes(state.clone()).with_state(state); let test_server = TestServer::new(routes).expect("failed to initialize test server");