Skip to content

Commit 10a197f

Browse files
committed
feat(playmatch): post external suggestions to Discord for moderation
1 parent 3d081c4 commit 10a197f

4 files changed

Lines changed: 449 additions & 100 deletions

File tree

src/command/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
mod playmatch;
1+
pub(crate) mod playmatch;
22
mod role;
33
mod util;
44

@@ -16,7 +16,7 @@ lazy_static! {
1616
.unwrap();
1717
static ref UPDATE_ROLE_ID: String =
1818
std::env::var("DISCORD_RETROREALM_UPDATE_ROLE_ID").unwrap_or_default();
19-
static ref SUGGESTION_CHANNEL_ID: u64 =
19+
pub(crate) static ref SUGGESTION_CHANNEL_ID: u64 =
2020
std::env::var("DISCORD_RETROREALM_SUGGESTION_CHANNEL_ID")
2121
.unwrap_or_default()
2222
.parse()

src/command/playmatch.rs

Lines changed: 142 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -386,19 +386,22 @@ pub async fn create_game_suggestion(
386386
let platform = game_response.platform.name.clone();
387387
let company = game_response.company.clone().map(|c| c.name);
388388
async move {
389-
handle_suggestion_message(SuggestionMessageHandleData {
390-
playmatch_client,
391-
serenity_ctx,
392-
suggestion_id: suggestion.id,
393-
owners,
394-
author_id,
395-
r#type: SuggestionType::Game,
396-
provider: provider_meta,
397-
name: game_name,
398-
platform: Some(platform),
399-
company,
400-
comment: suggestion.comment,
401-
})
389+
handle_suggestion_message(
390+
SuggestionMessageHandleData {
391+
playmatch_client,
392+
serenity_ctx,
393+
suggestion_id: suggestion.id,
394+
owners,
395+
submitter: SuggestionSubmitter::DiscordUser(author_id),
396+
r#type: SuggestionType::Game,
397+
provider: provider_meta,
398+
name: game_name,
399+
platform: Some(platform),
400+
company,
401+
comment: suggestion.comment,
402+
},
403+
None,
404+
)
402405
.await
403406
}
404407
});
@@ -472,19 +475,22 @@ pub async fn create_company_suggestion(
472475
let author_id = ctx.author().id;
473476
let company_name = name.clone();
474477
async move {
475-
handle_suggestion_message(SuggestionMessageHandleData {
476-
playmatch_client,
477-
serenity_ctx,
478-
suggestion_id: suggestion.id,
479-
owners,
480-
author_id,
481-
r#type: SuggestionType::Company,
482-
provider: provider_meta,
483-
name: company_name,
484-
platform: None,
485-
company: None,
486-
comment: suggestion.comment,
487-
})
478+
handle_suggestion_message(
479+
SuggestionMessageHandleData {
480+
playmatch_client,
481+
serenity_ctx,
482+
suggestion_id: suggestion.id,
483+
owners,
484+
submitter: SuggestionSubmitter::DiscordUser(author_id),
485+
r#type: SuggestionType::Company,
486+
provider: provider_meta,
487+
name: company_name,
488+
platform: None,
489+
company: None,
490+
comment: suggestion.comment,
491+
},
492+
None,
493+
)
488494
.await
489495
}
490496
});
@@ -581,19 +587,22 @@ pub async fn create_platform_suggestion(
581587
let platform_name = name.clone();
582588
let company = platform.company_name.clone();
583589
async move {
584-
handle_suggestion_message(SuggestionMessageHandleData {
585-
playmatch_client,
586-
serenity_ctx,
587-
suggestion_id: suggestion.id,
588-
owners,
589-
author_id,
590-
r#type: SuggestionType::Platform,
591-
provider: provider_meta,
592-
name: platform_name,
593-
platform: None,
594-
company,
595-
comment: suggestion.comment,
596-
})
590+
handle_suggestion_message(
591+
SuggestionMessageHandleData {
592+
playmatch_client,
593+
serenity_ctx,
594+
suggestion_id: suggestion.id,
595+
owners,
596+
submitter: SuggestionSubmitter::DiscordUser(author_id),
597+
r#type: SuggestionType::Platform,
598+
provider: provider_meta,
599+
name: platform_name,
600+
platform: None,
601+
company,
602+
comment: suggestion.comment,
603+
},
604+
None,
605+
)
597606
.await
598607
}
599608
});
@@ -838,27 +847,38 @@ impl PlaymatchUserCtx {
838847
}
839848
}
840849

841-
enum SuggestionType {
850+
pub(crate) enum SuggestionType {
842851
Platform,
843852
Company,
844853
Game,
845854
}
846855

847-
struct SuggestionMessageHandleData {
848-
playmatch_client: Arc<playmatch_client::Client>,
849-
serenity_ctx: Context,
850-
suggestion_id: Uuid,
851-
owners: HashSet<UserId>,
852-
author_id: UserId,
853-
r#type: SuggestionType,
854-
provider: MetadataProvider,
855-
name: String,
856-
platform: Option<String>,
857-
company: Option<String>,
858-
comment: Option<String>,
856+
pub(crate) enum SuggestionSubmitter {
857+
DiscordUser(UserId),
858+
External { source: String },
859+
}
860+
861+
pub(crate) struct SuggestionMessageHandleData {
862+
pub playmatch_client: Arc<playmatch_client::Client>,
863+
pub serenity_ctx: Context,
864+
pub suggestion_id: Uuid,
865+
pub owners: HashSet<UserId>,
866+
pub submitter: SuggestionSubmitter,
867+
pub r#type: SuggestionType,
868+
pub provider: MetadataProvider,
869+
pub name: String,
870+
pub platform: Option<String>,
871+
pub company: Option<String>,
872+
pub comment: Option<String>,
859873
}
860874

861-
async fn handle_suggestion_message(data: SuggestionMessageHandleData) -> CommandResult {
875+
/// Posts the staff card (if `existing_message_id` is None) and then waits on the Approve/Decline
876+
/// buttons. Called inline from the suggest commands (with `None`) and from the external-suggestion
877+
/// poller (with `None` for new posts and `Some(id)` to re-attach to a pre-restart message).
878+
pub(crate) async fn handle_suggestion_message(
879+
data: SuggestionMessageHandleData,
880+
existing_message_id: Option<serenity::all::MessageId>,
881+
) -> CommandResult {
862882
let http: &Http = data.serenity_ctx.http.as_ref();
863883
let cache: Arc<Cache> = data.serenity_ctx.cache.clone();
864884

@@ -880,7 +900,13 @@ async fn handle_suggestion_message(data: SuggestionMessageHandleData) -> Command
880900
.send()
881901
.await?;
882902

883-
let author = http.get_user(data.author_id).await?;
903+
let (author_label, dm_target): (String, Option<UserId>) = match &data.submitter {
904+
SuggestionSubmitter::DiscordUser(id) => {
905+
let user = http.get_user(*id).await?;
906+
(format!("<@{}> ({})", user.id, user.name), Some(user.id))
907+
}
908+
SuggestionSubmitter::External { source } => (format!("External ({source})"), None),
909+
};
884910

885911
let display_type = match data.r#type {
886912
SuggestionType::Platform => "Platform",
@@ -909,10 +935,7 @@ async fn handle_suggestion_message(data: SuggestionMessageHandleData) -> Command
909935
let provider_label = display_name(data.provider);
910936

911937
let build_card = |status: Status, heading: String| -> Card<'static> {
912-
let mut card = Card::new(status, heading).row(
913-
"Suggested by",
914-
format!("<@{}> ({})", author.id, author.name),
915-
);
938+
let mut card = Card::new(status, heading).row("Suggested by", author_label.clone());
916939
card = card.row(display_type.to_string(), data.name.clone());
917940
if let Some(platform) = data.platform.clone() {
918941
card = card.row("Platform", platform);
@@ -931,34 +954,41 @@ async fn handle_suggestion_message(data: SuggestionMessageHandleData) -> Command
931954
card
932955
};
933956

934-
let mut staff_card = build_card(
935-
Status::Info,
936-
format!("New {display_type} Metadata Suggestion"),
937-
);
938-
if let Some(url) = provider_page_url.clone() {
939-
staff_card = staff_card.link(CreateButton::new_link(url).label(format!("View on {provider_label}")));
940-
}
941-
staff_card = staff_card
942-
.link(
943-
CreateButton::new("approve")
944-
.label("Approve")
945-
.style(ButtonStyle::Success),
946-
)
947-
.link(
948-
CreateButton::new("decline")
949-
.label("Decline")
950-
.style(ButtonStyle::Danger),
951-
)
952-
.footer("Only bot owners can approve or decline.");
957+
let message_id = match existing_message_id {
958+
Some(id) => id,
959+
None => {
960+
let mut staff_card = build_card(
961+
Status::Info,
962+
format!("New {display_type} Metadata Suggestion"),
963+
);
964+
if let Some(url) = provider_page_url.clone() {
965+
staff_card =
966+
staff_card.link(CreateButton::new_link(url).label(format!("View on {provider_label}")));
967+
}
968+
staff_card = staff_card
969+
.link(
970+
CreateButton::new(format!("approve:{}", data.suggestion_id))
971+
.label("Approve")
972+
.style(ButtonStyle::Success),
973+
)
974+
.link(
975+
CreateButton::new(format!("decline:{}", data.suggestion_id))
976+
.label("Decline")
977+
.style(ButtonStyle::Danger),
978+
)
979+
.footer("Only bot owners can approve or decline.");
953980

954-
let message = channel_id
955-
.widen()
956-
.send_message(http, staff_card.into_message())
957-
.await?;
981+
let message = channel_id
982+
.widen()
983+
.send_message(http, staff_card.into_message())
984+
.await?;
985+
message.id
986+
}
987+
};
958988

959989
let owners = data.owners.clone();
960990
let interaction_opt = ComponentInteractionCollector::new(&data.serenity_ctx)
961-
.message_id(message.id)
991+
.message_id(message_id)
962992
.timeout(Duration::from_secs(7 * 24 * 60 * 60))
963993
.filter(move |i| owners.contains(&i.user.id))
964994
.await;
@@ -970,15 +1000,22 @@ async fn handle_suggestion_message(data: SuggestionMessageHandleData) -> Command
9701000
let edit = EditMessage::new()
9711001
.flags(MessageFlags::IS_COMPONENTS_V2)
9721002
.components(vec![CreateComponent::Container(expired.into_container())]);
973-
if let Err(e) = message.clone().edit(http, edit).await {
1003+
if let Err(e) = channel_id.widen().edit_message(http, message_id, edit).await {
9741004
error!("failed to edit expired suggestion message: {e}");
9751005
}
9761006
return Ok(());
9771007
};
9781008

9791009
let staff_id = interaction.user.id;
9801010

981-
match interaction.data.custom_id.as_str() {
1011+
let action = interaction
1012+
.data
1013+
.custom_id
1014+
.split(':')
1015+
.next()
1016+
.unwrap_or("");
1017+
1018+
match action {
9821019
"approve" => {
9831020
let updated = data
9841021
.playmatch_client
@@ -1010,14 +1047,16 @@ async fn handle_suggestion_message(data: SuggestionMessageHandleData) -> Command
10101047
)
10111048
.await?;
10121049

1013-
let mut dm = Card::new(Status::Success, "Suggestion Approved")
1014-
.row(display_type.to_string(), data.name.clone());
1015-
if matches!(data.r#type, SuggestionType::Game) {
1016-
dm = dm.row("ROMs updated", updated.updated.to_string());
1017-
}
1018-
dm = dm.text("Thanks for contributing.");
1019-
if let Err(e) = author.id.dm(http, dm.into_message()).await {
1020-
error!("failed to DM submitter on approval: {e}");
1050+
if let Some(dm_id) = dm_target {
1051+
let mut dm = Card::new(Status::Success, "Suggestion Approved")
1052+
.row(display_type.to_string(), data.name.clone());
1053+
if matches!(data.r#type, SuggestionType::Game) {
1054+
dm = dm.row("ROMs updated", updated.updated.to_string());
1055+
}
1056+
dm = dm.text("Thanks for contributing.");
1057+
if let Err(e) = dm_id.dm(http, dm.into_message()).await {
1058+
error!("failed to DM submitter on approval: {e}");
1059+
}
10211060
}
10221061
}
10231062
"decline" => {
@@ -1047,15 +1086,20 @@ async fn handle_suggestion_message(data: SuggestionMessageHandleData) -> Command
10471086
)
10481087
.await?;
10491088

1050-
let dm = Card::new(Status::Error, "Suggestion Declined")
1051-
.row(display_type.to_string(), data.name.clone())
1052-
.text("If you'd like context, reach out to the Playmatch team.");
1053-
if let Err(e) = author.id.dm(http, dm.into_message()).await {
1054-
error!("failed to DM submitter on decline: {e}");
1089+
if let Some(dm_id) = dm_target {
1090+
let dm = Card::new(Status::Error, "Suggestion Declined")
1091+
.row(display_type.to_string(), data.name.clone())
1092+
.text("If you'd like context, reach out to the Playmatch team.");
1093+
if let Err(e) = dm_id.dm(http, dm.into_message()).await {
1094+
error!("failed to DM submitter on decline: {e}");
1095+
}
10551096
}
10561097
}
10571098
_ => {
1058-
warn!("Unexpected button interaction");
1099+
warn!(
1100+
"Unexpected button interaction custom_id: {}",
1101+
interaction.data.custom_id
1102+
);
10591103
}
10601104
}
10611105

src/events/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
use crate::abstraction::activity_data::FromStringTuple;
2+
use crate::abstraction::command::CommandData;
23
use lazy_static::lazy_static;
34
use log::{debug, info};
45
use serenity::all::{ActivityData, Context, EventHandler, FullEvent};
56
use serenity::async_trait;
67
use std::env;
8+
use std::sync::atomic::{AtomicBool, Ordering};
9+
10+
pub mod suggestion_poller;
711

812
pub struct Handler;
913

@@ -12,6 +16,8 @@ lazy_static! {
1216
static ref DISCORD_STATUS_NAME: String = env::var("DISCORD_STATUS_NAME").unwrap_or_default();
1317
}
1418

19+
static POLLER_STARTED: AtomicBool = AtomicBool::new(false);
20+
1521
#[async_trait]
1622
impl EventHandler for Handler {
1723
async fn dispatch(&self, ctx: &Context, event: &FullEvent) {
@@ -25,6 +31,14 @@ impl EventHandler for Handler {
2531
&DISCORD_STATUS_NAME,
2632
)));
2733
}
34+
35+
if !POLLER_STARTED.swap(true, Ordering::SeqCst) {
36+
let ctx = ctx.clone();
37+
let data = ctx.data::<CommandData>();
38+
tokio::spawn(async move {
39+
suggestion_poller::run(ctx, data).await;
40+
});
41+
}
2842
}
2943
FullEvent::Resume { .. } => {
3044
debug!("Resumed");

0 commit comments

Comments
 (0)