From 7673dbdde9627ecd356c1580420c595da84ca663 Mon Sep 17 00:00:00 2001 From: End Date: Thu, 2 Apr 2026 15:30:25 -0700 Subject: [PATCH] feat(sync): add /sync from-pk command to import members from PluralKit --- src/commands/mod.rs | 19 ++++++--- src/commands/sync.rs | 98 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 5 deletions(-) create mode 100755 src/commands/sync.rs diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 6e929b5..52d84b2 100755 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -9,6 +9,7 @@ use std::sync::Arc; mod alias; mod member; +mod sync; mod system; mod trigger; @@ -20,6 +21,7 @@ use slack_morphism::prelude::*; use tracing::{Level, debug, error, trace}; use member::Member; +use sync::Sync; use system::System; use trigger::Trigger; @@ -36,6 +38,8 @@ enum Command { Triggers(Trigger), #[clap(subcommand)] Aliases(Alias), + #[clap(subcommand)] + Sync(Sync), /// Provides an explanation of this bot. Explain, } @@ -65,6 +69,10 @@ impl Command { .run(event, state) .await .change_context(CommandError::Aliases), + Self::Sync(sync) => sync + .run(event, client, state) + .await + .change_context(CommandError::Sync), Self::Explain => Ok(Self::explain()), } } @@ -73,13 +81,13 @@ impl Command { SlackCommandEventResponse::new( SlackMessageContent::new().with_text( indoc::indoc! {r#" - Slack System Bot is a bot that can replace user-sent messages under a "pseudo-account" of a systems member profile using custom display information. + Plura is a bot that can replace user-sent messages under a "pseudo-account" of a systems member profile using custom display information. This is useful for multiple people sharing one body (aka. systems), people who wish to role-play as different characters without having multiple Slack profiles, or anyone else who may want to post messages under a different identity from the same Slack account. Due to Slack's limitations, these messages will show up with the [APP] tag - however, they are not apps/bots. You can use message actions to find who the message was sent by. - If you wish to use the bot yourself, you can start with `/system help` and `/members help`. + If you wish to use the bot yourself, you can start with `/system help` and `/members help`. Other commands: `/triggers help`, `/aliases help`, `/sync help`. "#}.into(), ), ).with_response_type(SlackMessageResponseType::InChannel) @@ -96,9 +104,10 @@ enum CommandError { System, /// Error running the aliases command Aliases, + /// Error running the sync command + Sync, } -// TO-DO: figure out error handling #[tracing::instrument(skip(environment, event))] pub async fn process_command_event( Extension(environment): Extension>, @@ -110,7 +119,7 @@ pub async fn process_command_event( match command_event_callback(event, client, state).await { Ok(response) => Json(response), Err(e) => { - error!(error = ?e, "Error processing command event"); + tracing::error!(error = ?e, "Error processing command event"); Json(SlackCommandEventResponse::new( SlackMessageContent::new() .with_text("Error processing command! Logged to developers".into()), @@ -150,7 +159,7 @@ async fn command_event_callback( error!(error = ?e, "Error running command"); Ok(SlackCommandEventResponse::new( SlackMessageContent::new().with_text( - "Error running command! TODO: show error info on slack".into(), + format!("Error running command: {e}").into(), ), )) } diff --git a/src/commands/sync.rs b/src/commands/sync.rs new file mode 100755 index 0000000..7970380 --- /dev/null +++ b/src/commands/sync.rs @@ -0,0 +1,98 @@ +use std::sync::Arc; + +use error_stack::{Result, ResultExt}; +use reqwest::Client; +use serde::Deserialize; +use slack_morphism::prelude::*; + +use crate::{ + fetch_system, + models::{member, user}, +}; + +#[derive(thiserror::Error, displaydoc::Display, Debug)] +pub enum CommandError { + /// Error calling the database + Sqlx, + /// Error calling the PluralKit API + PluralKit, +} + +#[derive(clap::Subcommand, Debug)] +pub enum Sync { + /// Import members from PluralKit. Run in a DM to keep your token private. + FromPk { + /// Your PluralKit token (from pluralkit.me/settings) + token: String, + }, +} + +#[derive(Deserialize, Debug)] +struct PkMember { + name: String, + display_name: Option, + avatar_url: Option, + pronouns: Option, +} + +impl Sync { + #[tracing::instrument(skip_all)] + pub async fn run( + self, + event: SlackCommandEvent, + _client: Arc, + state: SlackClientEventsUserState, + ) -> Result { + let states = state.read().await; + let user_state = states.get_user_state::().unwrap(); + + match self { + Self::FromPk { token } => { + fetch_system!(event, user_state => system_id); + + let http = Client::new(); + let pk_members = http + .get("https://api.pluralkit.me/v2/systems/@me/members") + .header("User-Agent", "Plura/0.1 (https://github.com/Suya1671/plura)") + .header("Authorization", token.trim()) + .send() + .await + .change_context(CommandError::PluralKit) + .attach_printable("Failed to reach PluralKit API")? + .error_for_status() + .change_context(CommandError::PluralKit) + .attach_printable("PluralKit API returned an error — is your token correct?")? + .json::>() + .await + .change_context(CommandError::PluralKit) + .attach_printable("Failed to parse PluralKit API response")?; + + let count = pk_members.len(); + + for pk_member in pk_members { + let display_name = pk_member + .display_name + .unwrap_or_else(|| pk_member.name.clone()); + member::View { + full_name: pk_member.name, + display_name, + profile_picture_url: pk_member.avatar_url, + pronouns: pk_member.pronouns, + title: None, + name_pronunciation: None, + name_recording_url: None, + } + .add(system_id, &user_state.db) + .await + .change_context(CommandError::Sqlx)?; + } + + Ok(SlackCommandEventResponse::new( + SlackMessageContent::new().with_text( + format!("Imported {count} member(s) from PluralKit!").into(), + ), + )) + } + } + } +}