mirror of
https://github.com/revanced/revanced-discord-bot.git
synced 2025-05-16 21:07:06 +02:00
feat(utils): poll
command (#40)
Co-authored-by: Ax333l <main@axelen.xyz>
This commit is contained in:
parent
90643206b1
commit
3be6b4693c
@ -1,4 +1,11 @@
|
||||
# The Discord authorization token for the bot, requires the MESSAGE_CONTENT intent
|
||||
DISCORD_AUTHORIZATION_TOKEN=
|
||||
# The connection string to the MongoDB database
|
||||
MONGODB_URI=''
|
||||
MONGODB_URI=''
|
||||
|
||||
# The api server for the poll command
|
||||
API_SERVER=''
|
||||
# The client id for the api
|
||||
API_CLIENT_ID=''
|
||||
# The client secret for the api
|
||||
API_CLIENT_SECRET=''
|
||||
|
@ -63,7 +63,6 @@
|
||||
}
|
||||
},
|
||||
"description": "Introduce new threads with a message.",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
},
|
||||
"message_responses": {
|
||||
@ -138,8 +137,7 @@
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"minItems": 1
|
||||
"uniqueItems": true
|
||||
},
|
||||
"match": {
|
||||
"$ref": "#/$defs/regex",
|
||||
|
62
src/api/client.rs
Normal file
62
src/api/client.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use reqwest::header::HeaderMap;
|
||||
use reqwest::Client;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use super::model::auth::Authentication;
|
||||
|
||||
use super::routing::Endpoint;
|
||||
|
||||
pub struct Api {
|
||||
pub client: Client,
|
||||
pub server: reqwest::Url,
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
}
|
||||
|
||||
struct RequestInfo<'a> {
|
||||
headers: Option<HeaderMap>,
|
||||
route: Endpoint<'a>,
|
||||
}
|
||||
|
||||
impl Api {
|
||||
pub fn new(server: reqwest::Url, client_id: String, client_secret: String) -> Self {
|
||||
let client = Client::builder()
|
||||
.build()
|
||||
.expect("Cannot build reqwest::Client");
|
||||
|
||||
Api {
|
||||
client,
|
||||
server,
|
||||
client_id,
|
||||
client_secret,
|
||||
}
|
||||
}
|
||||
|
||||
async fn fire<T: DeserializeOwned>(&self, request_info: &RequestInfo<'_>) -> Result<T, reqwest::Error> {
|
||||
let client = &self.client;
|
||||
let mut req = request_info.route.to_request(&self.server);
|
||||
|
||||
if let Some(headers) = &request_info.headers {
|
||||
*req.headers_mut() = headers.clone();
|
||||
}
|
||||
|
||||
client.execute(req).await?.json::<T>().await
|
||||
}
|
||||
|
||||
pub async fn authenticate(
|
||||
&self,
|
||||
discord_id_hash: &str,
|
||||
) -> Result<Authentication, reqwest::Error> {
|
||||
let route = Endpoint::Authenticate {
|
||||
id: &self.client_id,
|
||||
secret: &self.client_secret,
|
||||
discord_id_hash,
|
||||
};
|
||||
self
|
||||
.fire(&RequestInfo {
|
||||
headers: None,
|
||||
route,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
3
src/api/mod.rs
Normal file
3
src/api/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod client;
|
||||
pub mod model;
|
||||
mod routing;
|
6
src/api/model/auth.rs
Normal file
6
src/api/model/auth.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Authentication {
|
||||
pub access_token: String,
|
||||
}
|
1
src/api/model/mod.rs
Normal file
1
src/api/model/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod auth;
|
28
src/api/routing.rs
Normal file
28
src/api/routing.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use reqwest::{Body, Method, Request};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Endpoint<'a> {
|
||||
Authenticate {
|
||||
id: &'a str,
|
||||
secret: &'a str,
|
||||
discord_id_hash: &'a str,
|
||||
},
|
||||
}
|
||||
|
||||
macro_rules! route {
|
||||
($self:ident, $server:ident, $endpoint:literal, $method:ident) => {{
|
||||
let mut req = Request::new(Method::$method, $server.join($endpoint).unwrap());
|
||||
*req.body_mut() = Some(Body::from(serde_json::to_vec($self).unwrap()));
|
||||
req
|
||||
}};
|
||||
}
|
||||
|
||||
impl Endpoint<'_> {
|
||||
pub fn to_request(&self, server: &reqwest::Url) -> Request {
|
||||
match self {
|
||||
Self::Authenticate { .. } => route!(self, server, "/auth/", POST),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
use poise::serenity_prelude::{self as serenity, MessageId};
|
||||
use chrono::Utc;
|
||||
use poise::serenity_prelude::{self as serenity, MessageId, ReactionType};
|
||||
use poise::ReplyHandle;
|
||||
|
||||
use crate::{Context, Error};
|
||||
@ -10,6 +11,13 @@ pub async fn reply(
|
||||
#[description = "The message id to reply to"] reply_message: Option<String>,
|
||||
#[description = "The message to send"] message: String,
|
||||
) -> Result<(), Error> {
|
||||
async fn send_ephermal<'a>(
|
||||
ctx: &Context<'a>,
|
||||
content: &str,
|
||||
) -> Result<ReplyHandle<'a>, serenity::Error> {
|
||||
ctx.send(|f| f.ephemeral(true).content(content)).await
|
||||
}
|
||||
|
||||
let http = &ctx.discord().http;
|
||||
let channel = &ctx.channel_id();
|
||||
|
||||
@ -38,9 +46,47 @@ pub async fn reply(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_ephermal<'a>(
|
||||
ctx: &Context<'a>,
|
||||
content: &str,
|
||||
) -> Result<ReplyHandle<'a>, serenity::Error> {
|
||||
ctx.send(|f| f.ephemeral(true).content(content)).await
|
||||
/// Start a poll.
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn poll(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The id of the poll"] id: u64,
|
||||
#[description = "The poll message"] message: String,
|
||||
#[description = "The poll title"] title: String,
|
||||
#[description = "The minumum server age in days to allow members to poll"] age: u16,
|
||||
) -> Result<(), Error> {
|
||||
let data = ctx.data().read().await;
|
||||
let configuration = &data.configuration;
|
||||
let embed_color = configuration.general.embed_color;
|
||||
|
||||
ctx.send(|m| {
|
||||
m.embed(|e| {
|
||||
let guild = &ctx.guild().unwrap();
|
||||
if let Some(url) = guild.icon_url() {
|
||||
e.thumbnail(url.clone()).footer(|f| {
|
||||
f.icon_url(url).text(format!(
|
||||
"{} • {}",
|
||||
guild.name,
|
||||
Utc::today().format("%Y/%m/%d")
|
||||
))
|
||||
})
|
||||
} else {
|
||||
e
|
||||
}
|
||||
.title(title)
|
||||
.description(message)
|
||||
.color(embed_color)
|
||||
})
|
||||
.components(|c| {
|
||||
c.create_action_row(|r| {
|
||||
r.create_button(|b| {
|
||||
b.label("Vote")
|
||||
.emoji(ReactionType::Unicode("🗳️".to_string()))
|
||||
.custom_id(format!("poll:{}:{}", id, age))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use bson::Document;
|
||||
use poise::serenity_prelude::{PermissionOverwrite};
|
||||
use poise::serenity_prelude::PermissionOverwrite;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with_macros::skip_serializing_none;
|
||||
|
||||
@ -23,6 +23,21 @@ pub struct LockedChannel {
|
||||
pub overwrites: Option<Vec<PermissionOverwrite>>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
pub struct Poll {
|
||||
pub author: Option<PollAuthor>,
|
||||
pub image_url: Option<String>,
|
||||
pub votes: Option<u16>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
pub struct PollAuthor {
|
||||
pub name: Option<String>,
|
||||
pub id: Option<u64>,
|
||||
}
|
||||
|
||||
impl From<Muted> for Document {
|
||||
fn from(muted: Muted) -> Self {
|
||||
to_document(&muted)
|
||||
|
53
src/events/interaction.rs
Normal file
53
src/events/interaction.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use poise::serenity_prelude::{
|
||||
ComponentType,
|
||||
MessageComponentInteraction,
|
||||
MessageComponentInteractionData,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::utils;
|
||||
pub async fn interaction_create(
|
||||
ctx: &serenity::Context,
|
||||
interaction: &serenity::Interaction,
|
||||
) -> Result<(), crate::serenity::SerenityError> {
|
||||
if let serenity::Interaction::MessageComponent(MessageComponentInteraction {
|
||||
data:
|
||||
MessageComponentInteractionData {
|
||||
component_type: ComponentType::Button,
|
||||
custom_id,
|
||||
..
|
||||
},
|
||||
..
|
||||
}) = interaction
|
||||
{
|
||||
if custom_id.starts_with("poll") {
|
||||
handle_poll(ctx, interaction, custom_id).await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_poll(
|
||||
ctx: &serenity::Context,
|
||||
interaction: &serenity::Interaction,
|
||||
custom_id: &str,
|
||||
) -> Result<(), crate::serenity::SerenityError> {
|
||||
fn parse<T>(str: &str) -> T
|
||||
where
|
||||
<T as std::str::FromStr>::Err: std::fmt::Debug,
|
||||
T: std::str::FromStr,
|
||||
{
|
||||
str.parse::<T>().unwrap()
|
||||
}
|
||||
|
||||
let poll: Vec<_> = custom_id.split(':').collect::<Vec<_>>();
|
||||
|
||||
let poll_id = parse::<u64>(poll[1]);
|
||||
let min_age = parse::<i64>(poll[2]);
|
||||
|
||||
let min_join_date = serenity::Timestamp::from(Utc::now() - Duration::days(min_age));
|
||||
|
||||
utils::poll::handle_poll(ctx, interaction, poll_id, min_join_date).await
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use poise::serenity_prelude::{self as serenity, Mutex, RwLock, ShardManager, UserId};
|
||||
use tracing::log::error;
|
||||
|
||||
use crate::{Data, Error};
|
||||
|
||||
mod guild_member_addition;
|
||||
mod guild_member_update;
|
||||
mod interaction;
|
||||
mod message_create;
|
||||
mod ready;
|
||||
mod thread_create;
|
||||
@ -46,10 +48,21 @@ impl<T: Send + Sync> Handler<T> {
|
||||
// Manually dispatch events from serenity to poise
|
||||
#[serenity::async_trait]
|
||||
impl serenity::EventHandler for Handler<Arc<RwLock<Data>>> {
|
||||
async fn ready(&self, ctx: serenity::Context, ready: serenity::Ready) {
|
||||
*self.bot_id.write().await = Some(ready.user.id);
|
||||
async fn guild_member_addition(
|
||||
&self,
|
||||
ctx: serenity::Context,
|
||||
mut new_member: serenity::Member,
|
||||
) {
|
||||
guild_member_addition::guild_member_addition(&ctx, &mut new_member).await;
|
||||
}
|
||||
|
||||
ready::load_muted_members(&ctx, &ready).await;
|
||||
async fn guild_member_update(
|
||||
&self,
|
||||
ctx: serenity::Context,
|
||||
old_if_available: Option<serenity::Member>,
|
||||
new: serenity::Member,
|
||||
) {
|
||||
guild_member_update::guild_member_update(&ctx, &old_if_available, &new).await;
|
||||
}
|
||||
|
||||
async fn message(&self, ctx: serenity::Context, new_message: serenity::Message) {
|
||||
@ -61,13 +74,6 @@ impl serenity::EventHandler for Handler<Arc<RwLock<Data>>> {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn interaction_create(&self, ctx: serenity::Context, interaction: serenity::Interaction) {
|
||||
self.dispatch_poise_event(&ctx, &poise::Event::InteractionCreate {
|
||||
interaction,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn message_update(
|
||||
&self,
|
||||
ctx: serenity::Context,
|
||||
@ -83,20 +89,24 @@ impl serenity::EventHandler for Handler<Arc<RwLock<Data>>> {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn ready(&self, ctx: serenity::Context, ready: serenity::Ready) {
|
||||
*self.bot_id.write().await = Some(ready.user.id);
|
||||
|
||||
ready::load_muted_members(&ctx, &ready).await;
|
||||
}
|
||||
|
||||
async fn interaction_create(&self, ctx: serenity::Context, interaction: serenity::Interaction) {
|
||||
if let Err(e) = interaction::interaction_create(&ctx, &interaction).await {
|
||||
error!("Failed to handle interaction: {:?}.", e);
|
||||
}
|
||||
|
||||
self.dispatch_poise_event(&ctx, &poise::Event::InteractionCreate {
|
||||
interaction,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn thread_create(&self, ctx: serenity::Context, thread: serenity::GuildChannel) {
|
||||
thread_create::thread_create(&ctx, &thread).await;
|
||||
}
|
||||
|
||||
async fn guild_member_addition(&self, ctx: serenity::Context, mut new_member: serenity::Member) {
|
||||
guild_member_addition::guild_member_addition(&ctx, &mut new_member).await;
|
||||
}
|
||||
|
||||
async fn guild_member_update(
|
||||
&self,
|
||||
ctx: serenity::Context,
|
||||
old_if_available: Option<serenity::Member>,
|
||||
new: serenity::Member,
|
||||
) {
|
||||
guild_member_update::guild_member_update(&ctx, &old_if_available, &new).await;
|
||||
}
|
||||
}
|
||||
|
12
src/main.rs
12
src/main.rs
@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
|
||||
use api::client::Api;
|
||||
use commands::{configuration, misc, moderation};
|
||||
use db::database::Database;
|
||||
use events::Handler;
|
||||
@ -12,6 +13,7 @@ use utils::bot::load_configuration;
|
||||
|
||||
use crate::model::application::Configuration;
|
||||
|
||||
mod api;
|
||||
mod commands;
|
||||
mod db;
|
||||
mod events;
|
||||
@ -30,6 +32,7 @@ pub struct Data {
|
||||
configuration: Configuration,
|
||||
database: Arc<Database>,
|
||||
pending_unmutes: HashMap<u64, JoinHandle<Option<Error>>>,
|
||||
api: Api,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@ -53,6 +56,7 @@ async fn main() {
|
||||
moderation::lock(),
|
||||
moderation::unlock(),
|
||||
misc::reply(),
|
||||
misc::poll(),
|
||||
];
|
||||
poise::set_qualified_names(&mut commands);
|
||||
|
||||
@ -79,6 +83,14 @@ async fn main() {
|
||||
.unwrap(),
|
||||
),
|
||||
pending_unmutes: HashMap::new(),
|
||||
api: Api::new(
|
||||
reqwest::Url::parse(
|
||||
&env::var("API_SERVER").expect("API_SERVER environment variable not set"),
|
||||
)
|
||||
.expect("Invalid API_SERVER"),
|
||||
env::var("API_CLIENT_ID").expect("API_CLIENT_ID environment variable not set"),
|
||||
env::var("API_CLIENT_SECRET").expect("API_CLIENT_SECRET environment variable not set"),
|
||||
),
|
||||
}));
|
||||
|
||||
let handler = Arc::new(Handler::new(
|
||||
|
@ -1,8 +1,9 @@
|
||||
use poise::serenity_prelude::{self as serenity, Member, RoleId};
|
||||
|
||||
pub mod autorespond;
|
||||
pub mod bot;
|
||||
pub mod decancer;
|
||||
pub mod embed;
|
||||
pub mod media_channel;
|
||||
pub mod moderation;
|
||||
pub mod autorespond;
|
||||
pub mod media_channel;
|
||||
pub mod poll;
|
||||
|
78
src/utils/poll.rs
Normal file
78
src/utils/poll.rs
Normal file
@ -0,0 +1,78 @@
|
||||
use poise::serenity_prelude::{ButtonStyle, ReactionType, Timestamp};
|
||||
|
||||
use tracing::log::{error, info, trace};
|
||||
|
||||
use super::bot::get_data_lock;
|
||||
use super::*;
|
||||
|
||||
pub async fn handle_poll(
|
||||
ctx: &serenity::Context,
|
||||
interaction: &serenity::Interaction,
|
||||
poll_id: u64,
|
||||
min_join_date: Timestamp,
|
||||
) -> Result<(), crate::serenity::SerenityError> {
|
||||
trace!("Handling poll: {}.", poll_id);
|
||||
|
||||
let data = get_data_lock(ctx).await;
|
||||
let data = data.read().await;
|
||||
|
||||
let component = &interaction.clone().message_component().unwrap();
|
||||
|
||||
let member = component.member.as_ref().unwrap();
|
||||
|
||||
let eligible = member.joined_at.unwrap() <= min_join_date;
|
||||
let auth_token = if eligible {
|
||||
let result = data
|
||||
.api
|
||||
.authenticate(&member.user.id.to_string())
|
||||
.await
|
||||
.map(|auth| auth.access_token);
|
||||
|
||||
if let Err(ref e) = result {
|
||||
error!("API Request error: {}", e)
|
||||
}
|
||||
result.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
component
|
||||
.create_interaction_response(&ctx.http, |r| {
|
||||
r.interaction_response_data(|m| {
|
||||
if let Some(token) = auth_token.as_deref() {
|
||||
let url = format!("https://revanced.app/polling#{}", token);
|
||||
m.components(|c| {
|
||||
c.create_action_row(|r| {
|
||||
r.create_button(|b| {
|
||||
b.label("Vote")
|
||||
.emoji(ReactionType::Unicode("🗳️".to_string()))
|
||||
.style(ButtonStyle::Link)
|
||||
.url(&url)
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
m
|
||||
}
|
||||
.ephemeral(true)
|
||||
.embed(|e| {
|
||||
if auth_token.is_some() {
|
||||
e.title("Cast your vote")
|
||||
.description("You can now vote on the poll.")
|
||||
} else if !eligible {
|
||||
info!("Member {} failed to vote.", member.display_name());
|
||||
e.title("You can not vote")
|
||||
.description("You are not eligible to vote on this poll.")
|
||||
} else {
|
||||
e.title("Error")
|
||||
.description("An error has occured. Please try again later.")
|
||||
}
|
||||
.color(data.configuration.general.embed_color)
|
||||
.thumbnail(member.user.face())
|
||||
})
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user