Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@ jobs:
DISCORD_CHANNEL_ID: ${{ secrets.DISCORD_CHANNEL_ID }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_TESTER_TOKEN: ${{ secrets.DISCORD_TESTER_TOKEN }}
DISCORD_USER_ID: ${{ secrets.DISCORD_USER_ID }}
VOTING_EMOJIS: ${{ secrets.VOTING_EMOJIS }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ docker/.data

test-locally.sh
run-locally.sh
.idea
vote_backups
6 changes: 4 additions & 2 deletions src/agenda_cultural/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use lazy_static::lazy_static;
use reqwest::{Client, Response};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use reqwest_retry::policies::ExponentialBackoff;
use reqwest_retry::Jitter::Bounded;
use reqwest_retry::RetryTransientMiddleware;
use scraper::{Html, Selector};
use std::cmp::Ordering;
Expand All @@ -25,8 +26,9 @@ lazy_static! {
static ref REST_CLIENT: ClientWithMiddleware = ClientBuilder::new(Client::new())
.with(RetryTransientMiddleware::new_with_policy(
ExponentialBackoff::builder()
.retry_bounds(Duration::from_millis(50), Duration::from_millis(500))
.build_with_total_retry_duration_and_max_retries(Duration::from_secs(3))
.jitter(Bounded)
.retry_bounds(Duration::from_millis(50), Duration::from_millis(1000))
.build_with_total_retry_duration_and_max_retries(Duration::from_secs(10))
))
.build();
static ref EVENT_ID_SELECTOR: Selector = Selector::parse(&format!(
Expand Down
46 changes: 28 additions & 18 deletions src/discord/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use serenity::all::ReactionType::{Custom, Unicode};
use serenity::all::{
AutoArchiveDuration, ChannelType, Colour, CreateEmbedAuthor, CreateThread, CurrentUser,
EditThread, Embed, GatewayIntents, GetMessages, GuildChannel, Message, MessageId,
MessageReaction, PartialGuild, PrivateChannel, ReactionType, User,
MessageReaction, PartialGuild, PrivateChannel, ReactionType, User, UserId,
};
use serenity::builder::{CreateEmbed, CreateMessage, EditMessage};
use serenity::cache::Settings;
Expand Down Expand Up @@ -181,7 +181,10 @@ impl DiscordAPI {
.await
}

#[instrument(skip(self, message), fields(event = %message.embeds.first().map(|embed| embed.url.clone().unwrap()).unwrap_or_default()
#[instrument(
skip(self, message),
fields(event = %message.embeds.first()
.map(|embed| embed.url.clone().unwrap()).unwrap_or_default()
))]
pub async fn add_reaction_to_message(&self, message: &Message, emoji_char: char) {
let react_result = message
Expand All @@ -197,7 +200,10 @@ impl DiscordAPI {
}
}

#[instrument(skip(self, message), fields(event = %message.embeds.first().map(|embed| embed.url.clone().unwrap()).unwrap_or_default()
#[instrument(
skip(self, message),
fields(event = %message.embeds.first()
.map(|embed| embed.url.clone().unwrap()).unwrap_or_default()
))]
pub async fn tag_save_for_later_reactions(&self, message: &mut Message, emoji_char: char) {
let save_for_later_reaction = ReactionType::from(emoji_char);
Expand Down Expand Up @@ -269,26 +275,29 @@ impl DiscordAPI {
.expect("Failed to edit message!");
}

#[instrument(skip(self, event_message, vote_emojis), fields(event = %event_message.embeds.first().map(|embed| embed.url.clone().unwrap()).unwrap_or_default()
#[instrument(skip_all,
fields(event = %event_message.embeds.first()
.map(|embed| embed.url.clone().unwrap()).unwrap_or_default()
))]
pub async fn send_privately_users_review(
&self,
event_message: &Message,
vote_emojis: &[EmojiConfig; 5],
) {
) -> Vec<UserId> {
let mut users_with_reviews = Vec::new();
let mut event_embed = event_message.embeds.first().cloned().unwrap();
let event_url = event_embed.url.clone();

if event_url.is_none() {
warn!("Event has no URL!");
return;
return users_with_reviews;
}

let users_votes = self.get_user_votes(event_message, vote_emojis).await;

if users_votes.is_empty() {
trace!("No user has voted on this message");
return;
return users_with_reviews;
}

let event_url = event_url.unwrap();
Expand All @@ -297,10 +306,15 @@ impl DiscordAPI {

for (vote, users) in users_votes.iter().enumerate() {
for user in users.iter().filter(|user| !user.bot) {
if !users_with_reviews.contains(&user.id) {
users_with_reviews.push(user.id);
}
self.send_user_review(user, &event_url, event_embed.clone(), vote_emojis, vote)
.await;
}
}

users_with_reviews
}

async fn send_user_review(
Expand Down Expand Up @@ -452,17 +466,13 @@ impl DiscordAPI {
description: Option<String>,
) -> CreateEmbed {
match &comment {
None => CreateEmbed::from(event_embed).description(format!(
"{}\n**Voto:** {}",
description.unwrap(),
vote_emoji
)),
Some(comment) => CreateEmbed::from(event_embed).description(format!(
"{}\n**Voto:** {}\n**Comentários:** {}",
description.unwrap(),
vote_emoji,
comment.content
)),
None => CreateEmbed::from(event_embed)
.description(description.unwrap())
.field("Voto", vote_emoji.to_string(), true),
Some(comment) => CreateEmbed::from(event_embed)
.description(description.unwrap())
.field("Voto", vote_emoji.to_string(), true)
.field("Comentários", &comment.content, true),
}
}

Expand Down
128 changes: 128 additions & 0 deletions src/discord/backup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use crate::discord::api::DiscordAPI;
use serde::Serialize;
use serenity::all::{GetMessages, Message, MessageType, UserId};
use tracing::{error, info, instrument};

#[instrument(skip(discord))]
pub async fn backup_user_votes(discord: &DiscordAPI, user_id: UserId) -> Option<Vec<VoteRecord>> {
let dm_channel = user_id.create_dm_channel(&discord.client.http).await;

if let Err(err) = dm_channel {
error!(
"Failed to create DM channel! Error: {}",
err
);
return None;
}

let messages = dm_channel
.unwrap()
.messages(&discord.client.http, GetMessages::new())
.await;

if let Err(err) = messages {
error!(
"Failed to get messages from DM channel! Error: {}",
err
);
return None;
}

let messages: Vec<VoteRecord> = messages
.unwrap()
.iter()
.filter_map(|message| extract_vote(discord, user_id, message))
.collect();

info!("Found {} votes", messages.len());

Some(messages)
}

#[instrument(skip_all)]
fn extract_vote(discord: &DiscordAPI, user_id: UserId, message: &Message) -> Option<VoteRecord> {
if message.author.id != discord.own_user.id
|| message.kind != MessageType::Regular
|| message.embeds.is_empty()
{
return None;
}

let embed = &message.embeds[0];
let description = embed.description.clone();

if description.is_none() {
error!("No description on event!");
return None;
}

let description = description.unwrap();
let embed_fields = &embed.fields;
let user_vote = match embed_fields
.iter()
.find(|field| field.name == "Voto")
.cloned()
{
Some(vote) => {
let comments = embed_fields
.iter()
.find(|field| field.name == "Comentários")
.map(|comment_field| comment_field.value.clone());

UserVote {
vote: vote.value,
comments,
}
}
None => {
// Fallback for embed-less reviews (backwards compatibility)
let vote = description
.lines()
.find(|line| line.starts_with("**Voto:** "))
.map(|line| line.replace("**Voto:** ", "").trim().to_string());
let comments = description
.lines()
.find(|line| line.starts_with("**Comentários:** "))
.map(|line| line.replace("**Comentários:** ", "").trim().to_string());

if vote.is_none() {
error!("No vote found in description on an embed-less review!");
return None;
}

UserVote {
vote: vote.unwrap(),
comments,
}
}
};

Some(VoteRecord {
user_id,
title: embed.title.clone().unwrap_or_else(|| {
error!("No title on event");
"No Title".to_string()
}),
url: embed.url.clone().unwrap_or_else(|| {
error!("No URL on event");
"No URL".to_string()
}),
description,
user_vote,
})
}

#[derive(Serialize, Debug)]
pub struct VoteRecord {
pub user_id: UserId,
pub title: String,
pub url: String,
pub description: String,
pub user_vote: UserVote,
}

#[derive(Serialize, Debug)]
pub struct UserVote {
pub vote: String,
pub comments: Option<String>,
}
1 change: 1 addition & 0 deletions src/discord/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod api;
pub mod backup;
Loading