Rework BlueMap claiming and expand ranks guilds chat and warps
This commit is contained in:
1 parent
434ed5338a
commit
6a2944a88e
68 files changed
+1453
-2500
No files matched your search
@@ -2,7 +2,8 @@
|
||||
//! sync, an invite link for launchers, and optional webhook announcements.
|
||||
//!
|
||||
//! Everything here is opt-in and needs the bot token and server ID saved in
|
||||
//! Settings. Sync only ever *adds* membership, in the direction the admin picks.
|
||||
//! Settings. Group mappings only add membership in the chosen direction;
|
||||
//! managed level-title roles are replaced when a player's rank changes.
|
||||
|
||||
use crate::auth::{AdminUser, AuthUser};
|
||||
use crate::error::{AppError, AppResult};
|
||||
@@ -22,6 +23,8 @@ const API: &str = "https://discord.com/api/v10";
|
||||
pub struct DiscordSettings {
|
||||
/// `off`, `discord_to_panel`, `panel_to_discord` or `both`.
|
||||
pub role_sync: String,
|
||||
/// Discord role used until the first mapped rank title is earned.
|
||||
pub base_role_id: String,
|
||||
/// Shown to players in the launcher.
|
||||
pub invite_url: String,
|
||||
/// Announcements go here. Empty turns them off.
|
||||
@@ -33,7 +36,7 @@ pub struct DiscordSettings {
|
||||
|
||||
impl Default for DiscordSettings {
|
||||
fn default() -> Self {
|
||||
Self { role_sync: "off".into(), invite_url: String::new(), webhook_url: String::new(), notify_achievements: true, notify_guilds: true, notify_members: false }
|
||||
Self { role_sync: "off".into(), base_role_id: String::new(), invite_url: String::new(), webhook_url: String::new(), notify_achievements: true, notify_guilds: true, notify_members: false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +57,7 @@ pub fn valid_invite(url: &str) -> bool {
|
||||
|
||||
fn view(s: &DiscordSettings) -> Value {
|
||||
json!({
|
||||
"role_sync": s.role_sync, "invite_url": s.invite_url,
|
||||
"role_sync": s.role_sync, "base_role_id": s.base_role_id, "invite_url": s.invite_url,
|
||||
"webhook_set": !s.webhook_url.is_empty(),
|
||||
"notify_achievements": s.notify_achievements, "notify_guilds": s.notify_guilds, "notify_members": s.notify_members,
|
||||
})
|
||||
@@ -72,6 +75,8 @@ pub async fn get_settings(_: AdminUser, State(state): State<AppState>) -> AppRes
|
||||
#[derive(Deserialize)]
|
||||
pub struct SettingsInput {
|
||||
role_sync: String,
|
||||
#[serde(default)]
|
||||
base_role_id: String,
|
||||
invite_url: String,
|
||||
/// Omitted or empty keeps the saved webhook; `"-"` clears it.
|
||||
#[serde(default)]
|
||||
@@ -85,6 +90,9 @@ pub async fn put_settings(_: AdminUser, State(state): State<AppState>, Json(i):
|
||||
if !["off", "discord_to_panel", "panel_to_discord", "both"].contains(&i.role_sync.as_str()) {
|
||||
return Err(AppError::bad_request("unknown role sync mode"));
|
||||
}
|
||||
if !i.base_role_id.is_empty() && (!i.base_role_id.bytes().all(|b| b.is_ascii_digit()) || i.base_role_id.len() > 24) {
|
||||
return Err(AppError::bad_request("Base Discord role ID must contain digits only"));
|
||||
}
|
||||
let old = load(&state).await?;
|
||||
let webhook = match i.webhook_url.trim() {
|
||||
"" => old.webhook_url.clone(),
|
||||
@@ -100,6 +108,7 @@ pub async fn put_settings(_: AdminUser, State(state): State<AppState>, Json(i):
|
||||
}
|
||||
let next = DiscordSettings {
|
||||
role_sync: i.role_sync,
|
||||
base_role_id: i.base_role_id,
|
||||
invite_url: invite,
|
||||
webhook_url: webhook,
|
||||
notify_achievements: i.notify_achievements,
|
||||
@@ -197,6 +206,7 @@ pub struct SyncReport {
|
||||
pub not_in_server: u32,
|
||||
pub groups_added: u32,
|
||||
pub roles_added: u32,
|
||||
pub roles_removed: u32,
|
||||
pub failed: u32,
|
||||
}
|
||||
|
||||
@@ -212,11 +222,13 @@ pub async fn sync_all(state: &AppState) -> AppResult<SyncReport> {
|
||||
return Err(AppError::bad_request("save a Discord bot token and server ID in Settings first"));
|
||||
}
|
||||
let mapping: Vec<(i64, String)> = sqlx::query_as("SELECT id, discord_role FROM groups WHERE discord_role <> ''").fetch_all(&state.db).await?;
|
||||
if mapping.is_empty() {
|
||||
let rank_roles: Vec<(i64, String)> = sqlx::query_as("SELECT level_req, json_extract(reward_data, '$.discord_role_id') FROM level_rewards WHERE level_type='global' AND reward_type='title' AND json_extract(reward_data, '$.discord_role_id') IS NOT NULL AND json_extract(reward_data, '$.discord_role_id') <> '' ORDER BY level_req DESC")
|
||||
.fetch_all(&state.db).await?;
|
||||
if mapping.is_empty() && rank_roles.is_empty() && settings.base_role_id.is_empty() {
|
||||
return Ok(report);
|
||||
}
|
||||
let linked: Vec<(i64, String)> = sqlx::query_as("SELECT user_id, provider_id FROM account_connections WHERE provider = 'discord'").fetch_all(&state.db).await?;
|
||||
for (user_id, discord_id) in linked {
|
||||
let linked: Vec<(i64, String, String)> = sqlx::query_as("SELECT c.user_id, c.provider_id, u.uuid FROM account_connections c JOIN users u ON u.id=c.user_id WHERE c.provider = 'discord'").fetch_all(&state.db).await?;
|
||||
for (user_id, discord_id, uuid) in linked {
|
||||
report.checked += 1;
|
||||
let resp = bot_request(state, reqwest::Method::GET, format!("{API}/guilds/{}/members/{discord_id}", c.discord_guild_id), &c.discord_bot_token).send().await;
|
||||
let resp = match resp {
|
||||
@@ -235,6 +247,7 @@ pub async fn sync_all(state: &AppState) -> AppResult<SyncReport> {
|
||||
report.failed += 1;
|
||||
break;
|
||||
}
|
||||
if !resp.status().is_success() { report.failed += 1; continue; }
|
||||
let Ok(member) = resp.json::<Value>().await else {
|
||||
report.failed += 1;
|
||||
continue;
|
||||
@@ -242,11 +255,17 @@ pub async fn sync_all(state: &AppState) -> AppResult<SyncReport> {
|
||||
let roles: Vec<String> = member["roles"].as_array().map(|a| a.iter().filter_map(|r| r.as_str().map(String::from)).collect()).unwrap_or_default();
|
||||
let groups: Vec<i64> = sqlx::query_scalar("SELECT group_id FROM user_groups WHERE user_id = ?").bind(user_id).fetch_all(&state.db).await?;
|
||||
let p = plan(&settings.role_sync, &mapping, &roles, &groups);
|
||||
let xp: i64 = sqlx::query_scalar("SELECT global_xp FROM user_levels WHERE uuid=?").bind(&uuid).fetch_optional(&state.db).await?.unwrap_or(0);
|
||||
let level = crate::progression::level_from_xp(xp).0;
|
||||
let chosen = rank_roles.iter().find(|(required,_)| *required <= level).map(|(_,role)| role.clone())
|
||||
.or_else(|| (!settings.base_role_id.is_empty()).then(|| settings.base_role_id.clone()));
|
||||
for g in p.add_groups {
|
||||
sqlx::query("INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?, ?)").bind(user_id).bind(g).execute(&state.db).await?;
|
||||
report.groups_added += 1;
|
||||
}
|
||||
for role in p.add_roles {
|
||||
// A level title takes precedence over mapped panel groups on Discord.
|
||||
let wanted_roles = if let Some(role) = &chosen { if roles.contains(role) { Vec::new() } else { vec![role.clone()] } } else { p.add_roles };
|
||||
for role in wanted_roles {
|
||||
let done = bot_request(state, reqwest::Method::PUT, format!("{API}/guilds/{}/members/{discord_id}/roles/{role}", c.discord_guild_id), &c.discord_bot_token)
|
||||
.header("Content-Length", "0")
|
||||
.send()
|
||||
@@ -257,6 +276,16 @@ pub async fn sync_all(state: &AppState) -> AppResult<SyncReport> {
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
||||
}
|
||||
if let Some(chosen) = chosen {
|
||||
let mut managed: Vec<String> = rank_roles.iter().map(|(_, role)| role.clone()).collect();
|
||||
if !settings.base_role_id.is_empty() { managed.push(settings.base_role_id.clone()); }
|
||||
managed.sort(); managed.dedup();
|
||||
for role in managed.into_iter().filter(|r| r != &chosen && roles.contains(r)) {
|
||||
let done = bot_request(state, reqwest::Method::DELETE, format!("{API}/guilds/{}/members/{discord_id}/roles/{role}", c.discord_guild_id), &c.discord_bot_token).send().await;
|
||||
match done { Ok(r) if r.status().is_success() => report.roles_removed += 1, _ => report.failed += 1 }
|
||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(120)).await;
|
||||
}
|
||||
Ok(report)
|
||||
|
||||
Reference in new issue
Block a user