Skip to content
This repository has been archived by the owner on Aug 7, 2024. It is now read-only.

Commit

Permalink
feat(utils): poll command (#40)
Browse files Browse the repository at this point in the history
Co-authored-by: Ax333l <main@axelen.xyz>
  • Loading branch information
oSumAtrIX and Axelen123 authored Dec 15, 2022
1 parent 9064320 commit 3be6b46
Show file tree
Hide file tree
Showing 14 changed files with 354 additions and 34 deletions.
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -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=''
4 changes: 1 addition & 3 deletions configuration.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@
}
},
"description": "Introduce new threads with a message.",
"minItems": 1,
"uniqueItems": true
},
"message_responses": {
Expand Down Expand Up @@ -138,8 +137,7 @@
"items": {
"type": "integer"
},
"uniqueItems": true,
"minItems": 1
"uniqueItems": true
},
"match": {
"$ref": "#/$defs/regex",
Expand Down
62 changes: 62 additions & 0 deletions src/api/client.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod client;
pub mod model;
mod routing;
6 changes: 6 additions & 0 deletions src/api/model/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use serde::Deserialize;

#[derive(Deserialize)]
pub struct Authentication {
pub access_token: String,
}
1 change: 1 addition & 0 deletions src/api/model/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod auth;
28 changes: 28 additions & 0 deletions src/api/routing.rs
Original file line number Diff line number Diff line change
@@ -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),
}
}
}
58 changes: 52 additions & 6 deletions src/commands/misc.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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();

Expand Down Expand Up @@ -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(())
}
17 changes: 16 additions & 1 deletion src/db/model.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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)
Expand Down
53 changes: 53 additions & 0 deletions src/events/interaction.rs
Original file line number Diff line number Diff line change
@@ -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
}
52 changes: 31 additions & 21 deletions src/events/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand All @@ -83,20 +89,24 @@ impl serenity::EventHandler for Handler<Arc<RwLock<Data>>> {
.await;
}

async fn thread_create(&self, ctx: serenity::Context, thread: serenity::GuildChannel) {
thread_create::thread_create(&ctx, &thread).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 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 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 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 thread_create(&self, ctx: serenity::Context, thread: serenity::GuildChannel) {
thread_create::thread_create(&ctx, &thread).await;
}
}
Loading

0 comments on commit 3be6b46

Please sign in to comment.