feat(sync): add /sync from-pk command to import members from PluralKit

This commit is contained in:
End 2026-04-02 15:30:25 -07:00
parent 555dafc0af
commit 7673dbdde9
No known key found for this signature in database
2 changed files with 112 additions and 5 deletions

View file

@ -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<Arc<SlackHyperListenerEnvironment>>,
@ -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(),
),
))
}

98
src/commands/sync.rs Executable file
View file

@ -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<String>,
avatar_url: Option<String>,
pronouns: Option<String>,
}
impl Sync {
#[tracing::instrument(skip_all)]
pub async fn run(
self,
event: SlackCommandEvent,
_client: Arc<SlackHyperClient>,
state: SlackClientEventsUserState,
) -> Result<SlackCommandEventResponse, CommandError> {
let states = state.read().await;
let user_state = states.get_user_state::<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::<Vec<PkMember>>()
.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(),
),
))
}
}
}
}