Skip to content
This repository has been archived by the owner on Oct 6, 2024. It is now read-only.

Commit

Permalink
improve oauth stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
katsumi143 committed Jul 20, 2024
1 parent b374b37 commit 8a830d5
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 70 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ chrono = { version = "0.4.38", features = ["serde"] }
dashmap = { version = "6.0.1", features = ["inline"] }
sqlx = { version = "0.7.4", features = ["uuid", "json", "chrono", "macros", "postgres", "tls-native-tls", "rust_decimal", "runtime-tokio"] }
tokio = { version = "1.38.0", features = ["full"] }
rand = "0.8.5"
reqwest = { version = "0.12.5", features = ["json"] }
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.120"
Expand Down
1 change: 1 addition & 0 deletions crates/mellow/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mellow_macros.path = "../mellow_macros"
mellow_models.path = "../mellow_models"
mellow_util.path = "../mellow_util"
once_cell = "1.19.0"
rand.workspace = true
reqwest.workspace = true
sha2 = "0.10.8"
serde.workspace = true
Expand Down
56 changes: 36 additions & 20 deletions crates/mellow/src/commands/syncing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,32 +58,48 @@ pub async fn sync_with_token(guild_id: Id<GuildMarker>, user_id: HakuId<HakuUser
};
let website_token = create_website_token(user_id)
.await?;
DISCORD_INTERACTION_CLIENT
.update_response(interaction_token)
.content(Some(&format!("{}{}\n[<:gear_fill:1224667889592700950> Your Server Preferences <:external_link:1225472071417729065>](<https://hakumi.cafe/mellow/server/{}/user_settings?mt={website_token}>) • [<:personraisedhand:1219234152709095424> Get Support](<https://discord.com/invite/rs3r4dQu9P>)", if result.profile_changed {
format!("## <:check2circle:1219235152580837419> {determiner} server profile has been updated.\n{}```diff\n{}```",
if has_assigned_role && has_retracted_role {
format!("{pronoun} been assigned and retracted roles, ...equality! o(>ω<)o")
} else if has_assigned_role {
format!("{pronoun} been assigned new roles, {}",
if is_forced { "yippee!" } else { "hold them dearly to your heart! ♡(>ᴗ•)" }
let content = format!("{}\n[<:gear_fill:1224667889592700950> Your Server Preferences <:external_link:1225472071417729065>](<https://hakumi.cafe/mellow/server/{guild_id}/user_settings?mt={website_token}>) • [<:personraisedhand:1219234152709095424> Get Support](<https://discord.com/invite/rs3r4dQu9P>)",
if result.profile_changed {
format!("## {}\n```diff\n{}```",
if result.issues.is_empty() {
format!("<:check2circle:1219235152580837419> {determiner} server profile has been updated.\n{}",
if has_assigned_role && has_retracted_role {
format!("{pronoun} been assigned and retracted roles, ...equality! o(>ω<)o")
} else if has_assigned_role {
format!("{pronoun} been assigned new roles, {}",
if is_forced { "yippee!" } else { "hold them dearly to your heart! ♡(>ᴗ•)" }
)
} else {
format!("{pronoun} been retracted some roles, that's either a good thing, or a bad thing! ┐(︶▽︶)┌")
}
)
} else {
format!("{pronoun} been retracted some roles, that's either a good thing, or a bad thing! ┐(︶▽︶)┌")
format!("There was an issue while syncing your profile.\n{}",
SyncingIssue::format_many(&result.issues, guild_id, user_id, &website_token)
.await?
)
},
result.role_changes.iter().map(|x| match x.kind {
RoleChangeKind::Added => format!("+ {}", x.display_name),
RoleChangeKind::Removed => format!("- {}", x.display_name)
}).collect::<Vec<String>>().join("\n")
result.role_changes
.iter()
.map(|x| match x.kind {
RoleChangeKind::Added => format!("+ {}", x.display_name),
RoleChangeKind::Removed => format!("- {}", x.display_name)
})
.collect::<Vec<String>>()
.join("\n")
)
} else {
format!("## <:mellow_squircled:1225413361777508393> {determiner} server profile is already up to par!\nAccording to my simulated brain, there's nothing to change here, {contraction} all set!\nIf you were expecting a *different* result, you may need to try again in a few minutes, apologies!\n")
}, if !result.issues.is_empty() {
format!("\n### There were issues with your syncing request\n{}\n",
SyncingIssue::format_many(&result.issues, guild_id, &website_token)
} else if !result.issues.is_empty() {
format!("## There was an issue while syncing your profile.\n{}\n",
SyncingIssue::format_many(&result.issues, guild_id, user_id, &website_token)
.await?
)
} else { "".into() }, guild_id)))
} else {
format!("## <:mellow_squircled:1225413361777508393> {determiner} server profile is already up to par!\nAccording to my simulated brain, there's nothing to change here, {contraction} all set!\nIf you were expecting a *different* result, you may need to try again in a few minutes, apologies!\n")
}
);
DISCORD_INTERACTION_CLIENT
.update_response(interaction_token)
.content(Some(&content))
.await?;

let mut server_logs: Vec<ServerLog> = vec![];
Expand Down
3 changes: 3 additions & 0 deletions crates/mellow/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ pub enum Error {

#[error("Model: {0}")]
Model(#[from] mellow_models::Error),

#[error("There is no patreon campaign connected")]
PatreonCampaignNotConnected,

#[error("Reqwest Error: {0}")]
Reqwest(#[from] reqwest::Error),
Expand Down
41 changes: 33 additions & 8 deletions crates/mellow/src/syncing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ use mellow_util::{
marker::{ ConnectionMarker, DocumentMarker, SyncActionMarker, UserMarker as HakuUserMarker },
HakuId
},
DISCORD_CLIENT
DISCORD_CLIENT,
PG_POOL
};
use rand::{ distributions::Alphanumeric, Rng };
use serde::{ Serialize, Deserialize };
use std::collections::HashMap;
use std::{
collections::HashMap,
pin::Pin
};
use twilight_http::request::AuditLogReason;
use twilight_model::id::{
marker::{ GuildMarker, RoleMarker, UserMarker },
Expand Down Expand Up @@ -87,21 +92,41 @@ pub enum SyncingIssue {
}

impl SyncingIssue {
pub async fn format_many(items: &[Self], guild_id: Id<GuildMarker>, website_token: &str) -> Result<String> {
pub async fn format_many(items: &[Self], guild_id: Id<GuildMarker>, user_id: HakuId<HakuUserMarker>, website_token: &str) -> Result<String> {
let mut strings = Vec::with_capacity(items.len());
for item in items {
strings.push(item.display(guild_id, website_token).await?);
strings.push(item.display(guild_id, user_id, website_token).await?);
}

Ok(strings.join("\n"))
}

pub async fn display(&self, guild_id: Id<GuildMarker>, website_token: &str) -> Result<String> {
pub async fn display(&self, guild_id: Id<GuildMarker>, user_id: HakuId<HakuUserMarker>, website_token: &str) -> Result<String> {
Ok(match self {
Self::MissingConnections =>
format!("You haven't given this server access to all connections yet, fix that [here](<https://hakumi.cafe/mellow/server/{guild_id}/user_settings?mt={website_token}>)!"),
Self::MissingOAuthAuthorisation(connection_kind) =>
format!("Missing authorisation for your {connection_kind:?} connection.")
Self::MissingOAuthAuthorisation(connection_kind) => {
let token: String = rand::thread_rng()
.sample_iter(Alphanumeric)
.take(24)
.map(char::from)
.collect();
sqlx::query!(
"
INSERT INTO mellow_connection_requests (server_id, user_id, token)
VALUES ($1, $2, $3)
ON CONFLICT (user_id)
DO UPDATE SET token = $3
",
guild_id.get() as i64,
user_id.value,
&token
)
.execute(&*Pin::static_ref(&PG_POOL).await)
.await?;

format!("Your {connection_kind:?} connection was invalidated, please [reconnect it](<https://www.patreon.com/oauth2/authorize?client_id=BaKp_8PIeBxx0cfJoEEaVxVQMxD3c_IUFS_qCSu5gNFnXLL5c4Qw4YMPtgMJG-n9&redirect_uri=https%3A%2F%2Flocal-api-new.hakumi.cafe%2Fv1%2Fconnection_callback%2F4&scope=identity%20identity.memberships&response_type=code&state=m1-{token}>).")
}
})
}
}
Expand Down Expand Up @@ -212,7 +237,7 @@ pub async fn get_connection_metadata(guild_id: Id<GuildMarker>, user_ids: &Vec<H
}
}
}
} else {
} else if !issues.iter().any(|x| matches!(x, SyncingIssue::MissingOAuthAuthorisation(ConnectionKind::Patreon))) {
issues.push(SyncingIssue::MissingOAuthAuthorisation(ConnectionKind::Patreon));
}
}
Expand Down
14 changes: 3 additions & 11 deletions crates/mellow/src/visual_scripting/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use twilight_model::id::{
use crate::{
server::logging::send_logs,
syncing::{ SyncingInitiator, sync_single_user },
Result
Error, Result
};

pub mod action_tracker;
Expand Down Expand Up @@ -131,19 +131,11 @@ pub async fn process_document(document: DocumentModel, variables: Variable) -> A
},
ElementKind::GetLinkedPatreonCampaign => {
let guild_id = variables.read().await.get("guild_id").cast_id();
let oauth_authorisations = CACHE
.mellow
.server_oauth_authorisations(guild_id)
.await?;
let patreon_authorisation = CACHE
.mellow
.oauth_authorisation(oauth_authorisations.into_iter().next().unwrap())
.unwrap()
.clone();
let campaign = CACHE
.patreon
.campaign(patreon_authorisation)
.campaign(guild_id)
.await?
.ok_or(Error::PatreonCampaignNotConnected)?
.clone();
variables
.write()
Expand Down
13 changes: 4 additions & 9 deletions crates/mellow_cache/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use mellow_models::hakumi::user::connection::ConnectionKind;

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Model: {0}")]
Expand All @@ -14,17 +12,14 @@ pub enum Error {
#[error("Serde JSON: {0}")]
SerdeJson(#[from] serde_json::Error),

#[error("OAuth authorisation refresh failed")]
OAuthAuthorisationRefresh,

#[error("SIMD JSON: {0}")]
SimdJson(#[from] simd_json::Error),

#[error("SQLx: {0}")]
Sqlx(#[from] sqlx::Error),

#[error("Your {0:?} connection appears to be invalid, please reconnect it [here](<https://hakumi.cafe/settings/account/connections>).")]
UserConnectionInvalid(ConnectionKind),

#[error("Failed to refresh user connection")]
UserConnectionRefresh
Sqlx(#[from] sqlx::Error)
}

pub type Result<T> = core::result::Result<T, Error>;
2 changes: 1 addition & 1 deletion crates/mellow_cache/src/mellow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use crate::{ CACHE, Result };
#[derive(Default)]
pub struct MellowCache {
commands: DashMap<Id<CommandMarker>, CommandModel>,
oauth_authorisations: DashMap<u64, OAuthAuthorisationModel>,
pub oauth_authorisations: DashMap<u64, OAuthAuthorisationModel>,
pub servers: DashMap<Id<GuildMarker>, ServerModel>,
server_oauth_authorisations: DashMap<Id<GuildMarker>, DashSet<u64>>,
server_sync_actions: DashMap<Id<GuildMarker>, DashSet<HakuId<SyncActionMarker>>>,
Expand Down
94 changes: 74 additions & 20 deletions crates/mellow_cache/src/patreon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,9 @@ use dashmap::{
mapref::one::Ref,
DashMap
};
use mellow_models::{
hakumi::{
user::connection::ConnectionKind,
OAuthAuthorisationModel
},
patreon::{
campaign::{ GetCampaign, Tier },
CampaignModel, UserIdentityModel
}
use mellow_models::patreon::{
campaign::{ GetCampaign, Tier },
CampaignModel, UserIdentityModel
};
use mellow_util::{
hakuid::{
Expand All @@ -28,6 +22,10 @@ use std::{
collections::HashMap,
pin::Pin
};
use twilight_model::id::{
marker::GuildMarker,
Id
};

use crate::{ CACHE, Error, Result };

Expand All @@ -44,23 +42,78 @@ pub struct PatreonRefreshResult {

#[derive(Default)]
pub struct PatreonCache {
campaigns: DashMap<String, CampaignModel>,
campaigns: DashMap<Id<GuildMarker>, CampaignModel>,
user_identities: DashMap<HakuId<ConnectionMarker>, UserIdentityModel>
}

impl PatreonCache {
pub async fn campaign(&self, oauth_authorisation: OAuthAuthorisationModel) -> Result<Ref<'_, String, CampaignModel>> {
let access_token = oauth_authorisation.access_token;
Ok(match self.campaigns.get(&access_token) {
Some(model) => model,
None => {
pub async fn campaign(&self, guild_id: Id<GuildMarker>) -> Result<Option<Ref<'_, Id<GuildMarker>, CampaignModel>>> {
Ok(match self.campaigns.get(&guild_id) {
Some(model) => Some(model),
None => if
let Some(authorisation_id) = CACHE
.mellow
.server_oauth_authorisations(guild_id)
.await?
.into_iter()
.next() &&
let Some(authorisation) = CACHE.mellow.oauth_authorisation(authorisation_id)
{
let auth_header = if Utc::now() > authorisation.expires_at {
let response = HTTP
.post("https://www.patreon.com/api/oauth2/token")
.form(&HashMap::from([
("client_id", PATREON_CLIENT_ID),
("client_secret", PATREON_CLIENT_SECRET),
("grant_type", "refresh_token"),
("refresh_token", &authorisation.refresh_token)
]))
.send()
.await?;
let status_code = response.status();
if status_code.is_success() {
let result: PatreonRefreshResult = response
.json()
.await?;
let expires_at = Utc::now().checked_add_signed(TimeDelta::seconds(result.expires_in)).unwrap();
sqlx::query!(
"
UPDATE user_connection_oauth_authorisations
SET expires_at = $2, token_type = $3, access_token = $4, refresh_token = $5
WHERE id = $1
",
authorisation.id as i64,
expires_at,
result.token_type,
result.access_token,
result.refresh_token
)
.execute(&*Pin::static_ref(&PG_POOL).await)
.await?;

let auth_header = format!("{} {}", result.token_type, result.access_token);
if let Some(mut authorisation) = CACHE.mellow.oauth_authorisations.get_mut(&authorisation_id) {
authorisation.expires_at = expires_at;
authorisation.token_type = result.token_type;
authorisation.access_token = result.access_token;
authorisation.refresh_token = result.refresh_token;
}

auth_header
} else if status_code == StatusCode::UNAUTHORIZED {
return Ok(None);
} else {
return Err(Error::OAuthAuthorisationRefresh);
}
} else { format!("{} {}", authorisation.token_type, authorisation.access_token) };

let campaign: GetCampaign = get_json("https://www.patreon.com/api/oauth2/v2/campaigns?include=tiers&fields%5Btier%5D=patron_count")
.header("authorization", format!("{} {}", oauth_authorisation.token_type, access_token))
.header("authorization", auth_header)
.await?;
let Some(included) = campaign.included else {
return Err(Error::ModelNotFound);
};
self.campaigns.entry(access_token)
Some(self.campaigns.entry(guild_id)
.insert(CampaignModel {
tiers: included
.into_iter()
Expand All @@ -70,7 +123,8 @@ impl PatreonCache {
.collect()
})
.downgrade()
}
)
} else { None }
})
}

Expand Down Expand Up @@ -129,9 +183,9 @@ impl PatreonCache {
authorisation = authorisation2.clone();
}
} else if status_code == StatusCode::UNAUTHORIZED {
return Err(Error::UserConnectionInvalid(ConnectionKind::Patreon));
return Ok(None);
} else {
return Err(Error::UserConnectionRefresh);
return Err(Error::OAuthAuthorisationRefresh);
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/mellow_util/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async-once-cell.workspace = true
chrono.workspace = true
http = "1.1.0"
once_cell.workspace = true
rand = "0.8.5"
rand.workspace = true
reqwest.workspace = true
serde.workspace = true
sqlx.workspace = true
Expand Down

0 comments on commit 8a830d5

Please sign in to comment.