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

feat(utils): poll command #40

Merged
merged 3 commits into from
Dec 15, 2022
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
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