Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Premium #148

Merged
merged 21 commits into from
Jan 17, 2023
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
732 changes: 707 additions & 25 deletions sqlx-data.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/cache/events/guild.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ impl UpdateCache for GuildCreate {
.collect();

let guild = CachedGuild {
name: self.name.clone(),
emojis: self.emojis.iter().map(|e| (e.id, e.animated)).collect(),
channels,
active_thread_parents: self
Expand Down
1 change: 1 addition & 0 deletions src/cache/models/guild.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use twilight_model::id::{
use super::{channel::CachedChannel, member::CachedMember};

pub struct CachedGuild {
pub name: String,
/// all custom emojis mapped to whether they are animated
pub emojis: HashMap<Id<EmojiMarker>, bool>,
/// all textable channels except for threads
Expand Down
29 changes: 22 additions & 7 deletions src/client/bot.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use std::fmt::{Debug, Write};
use std::{
fmt::{Debug, Write},
sync::Arc,
};

use futures::Future;
use snafu::ErrorCompat;
use sqlx::PgPool;
use tokio::sync::RwLock;
Expand Down Expand Up @@ -71,12 +75,14 @@ impl StarboardBot {
.expect("failed to run migrations");

// load autostar channels
let asc: Vec<_> = sqlx::query!("SELECT channel_id FROM autostar_channels")
.fetch_all(&pool)
.await?
.into_iter()
.map(|rec| Id::<ChannelMarker>::new(rec.channel_id as u64))
.collect();
let asc: Vec<_> = sqlx::query!(
"SELECT DISTINCT channel_id FROM autostar_channels WHERE premium_locked=false"
)
.fetch_all(&pool)
.await?
.into_iter()
.map(|rec| Id::<ChannelMarker>::new(rec.channel_id as u64))
.collect();

let mut map = dashmap::DashSet::new();
map.extend(asc);
Expand Down Expand Up @@ -140,4 +146,13 @@ impl StarboardBot {
}
}
}

pub async fn catch_future_errors<T, E: Into<StarboardError>>(
bot: Arc<StarboardBot>,
future: impl Future<Output = Result<T, E>>,
) {
if let Err(err) = future.await {
bot.handle_error(&err.into()).await;
}
}
}
5 changes: 4 additions & 1 deletion src/client/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use tokio::{
use twilight_gateway::cluster::Events;

use crate::{
client::bot::StarboardBot, core::posroles::loop_update_posroles, events::handle_event,
client::bot::StarboardBot,
core::{posroles::loop_update_posroles, premium::expire::loop_expire_premium},
events::handle_event,
};

use super::cooldowns::Cooldowns;
Expand Down Expand Up @@ -46,6 +48,7 @@ pub async fn run(mut events: Events, bot: StarboardBot) {

// start background tasks
tokio::spawn(loop_update_posroles(bot.clone()));
tokio::spawn(loop_expire_premium(bot.clone()));

// handle events
while let Some((shard_id, event)) = events.next().await {
Expand Down
8 changes: 8 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ pub const BOT_COLOR: u32 = 0xFFE19C;

pub const YEAR_SECONDS: i64 = 31_557_600;
pub const MONTH_SECONDS: i64 = 2_630_016;
pub const MONTH_DAYS: u64 = 31;

pub const CREDITS_PER_MONTH: u64 = 3;

// Links
pub const INVITE_URL: &str = "https://discord.com/api/oauth2/authorize?client_id=700796664276844612&permissions=805661760&scope=bot%20applications.commands";
Expand All @@ -20,6 +23,7 @@ pub const REVIEW_URL: &str = "https://top.gg/bot/700796664276844612#reviews";

// Tasks
pub const UPDATE_PRS_DELAY: Duration = Duration::from_secs(60 * 60);
pub const CHECK_EXPIRED_PREMIUM: Duration = Duration::from_secs(60 * 60);

// Cache size
pub const MAX_MESSAGES: u32 = 10_000;
Expand All @@ -39,7 +43,9 @@ pub const MAX_MAX_CHARS: i16 = 5_000;
pub const MAX_MIN_CHARS: i16 = 5_000;

pub const MAX_ASC_EMOJIS: usize = 3;
pub const MAX_PREM_ASC_EMOJIS: usize = 10;
pub const MAX_AUTOSTAR: i64 = 3;
pub const MAX_PREM_AUTOSTAR: i64 = 10;

// Starboard Validation
pub const MIN_REQUIRED: i16 = 1;
Expand All @@ -57,7 +63,9 @@ pub const MAX_NEWER_THAN: i64 = YEAR_SECONDS * 50;
pub const MAX_OLDER_THAN: i64 = YEAR_SECONDS * 50;

pub const MAX_VOTE_EMOJIS: usize = 3;
pub const MAX_PREM_VOTE_EMOJIS: usize = 10;
pub const MAX_STARBOARDS: i64 = 3;
pub const MAX_PREM_STARBOARDS: i64 = 10;

// Override Validation
pub const MAX_CHANNELS_PER_OVERRIDE: usize = 100;
Expand Down
1 change: 1 addition & 0 deletions src/core/autostar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub async fn handle(

// Fetch the autostar channels
let asc = AutoStarChannel::list_by_channel(&bot.pool, autostar_channel_id.get_i64()).await?;
let asc: Vec<_> = asc.into_iter().filter(|a| !a.premium_locked).collect();

// If none, remove the channel id from the cache
if asc.is_empty() {
Expand Down
67 changes: 42 additions & 25 deletions src/core/embedder/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use twilight_model::id::{marker::MessageMarker, Id};
use crate::{
cache::models::{message::CachedMessage, user::CachedUser},
client::bot::StarboardBot,
core::starboard::{config::StarboardConfig, webhooks::get_valid_webhook},
core::{
premium::is_premium::is_guild_premium,
starboard::{config::StarboardConfig, webhooks::get_valid_webhook},
},
database::{Message as DbMessage, Starboard},
errors::StarboardResult,
utils::{get_status::get_status, id_as_i64::GetI64, into_id::IntoId},
Expand Down Expand Up @@ -33,18 +36,25 @@ impl Embedder<'_, '_> {
&self,
bot: &StarboardBot,
) -> StarboardResult<twilight_model::channel::Message> {
let built = match self.build(false, self.config.resolved.use_webhook) {
let guild_id = self.config.starboard.guild_id.into_id();
let sb_channel_id = self.config.starboard.channel_id.into_id();

let is_prem = is_guild_premium(bot, self.config.starboard.guild_id).await?;

let built = match self.build(false, self.config.resolved.use_webhook && !is_prem) {
BuiltStarboardEmbed::Full(built) => built,
BuiltStarboardEmbed::Partial(_) => panic!("Tried to send an unbuildable message."),
};
let (attachments, errors) = built.upload_attachments.as_attachments(bot).await;

for e in errors {
bot.handle_error(&e).await;
}

let guild_id = self.config.starboard.guild_id.into_id();
let sb_channel_id = self.config.starboard.channel_id.into_id();
let attachments = if is_prem {
let (attachments, errors) = built.upload_attachments.as_attachments(bot).await;
for e in errors {
bot.handle_error(&e).await;
}
Some(attachments)
} else {
None
};

let forum_post_name = if bot.cache.is_channel_forum(guild_id, sb_channel_id) {
let name = &built.embeds[0].author.as_ref().unwrap().name;
Expand Down Expand Up @@ -76,8 +86,11 @@ impl Embedder<'_, '_> {
.http
.execute_webhook(wh.id, wh.token.as_ref().unwrap())
.content(&built.top_content)?
.embeds(&built.embeds)?
.attachments(&attachments)?;
.embeds(&built.embeds)?;

if let Some(attachments) = &attachments {
ret = ret.attachments(attachments)?;
}

if parent != sb_channel_id {
ret = ret.thread_id(sb_channel_id);
Expand Down Expand Up @@ -106,28 +119,30 @@ impl Embedder<'_, '_> {
}

if let Some(name) = forum_post_name {
let ret = bot
let mut ret = bot
.http
.create_forum_thread(sb_channel_id, &name)
.message()
.content(&built.top_content)?
.embeds(&built.embeds)?
.attachments(&attachments)?
.await?
.model()
.await?;
.embeds(&built.embeds)?;

if let Some(attachments) = &attachments {
ret = ret.attachments(attachments)?;
};

Ok(ret.message)
Ok(ret.await?.model().await?.message)
} else {
Ok(bot
let mut ret = bot
.http
.create_message(self.config.starboard.channel_id.into_id())
.content(&built.top_content)?
.embeds(&built.embeds)?
.attachments(&attachments)?
.await?
.model()
.await?)
.embeds(&built.embeds)?;

if let Some(attachments) = &attachments {
ret = ret.attachments(attachments)?;
}

Ok(ret.await?.model().await?)
}
}

Expand Down Expand Up @@ -169,7 +184,9 @@ impl Embedder<'_, '_> {
(None, false)
};

match self.build(force_partial, wh.is_some()) {
let is_prem = is_guild_premium(bot, self.config.starboard.guild_id).await?;

match self.build(force_partial, wh.is_some() && !is_prem) {
BuiltStarboardEmbed::Full(built) => {
if let Some(wh) = wh {
let mut ud = bot
Expand Down
1 change: 1 addition & 0 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod emoji;
pub mod has_image;
pub mod permroles;
pub mod posroles;
pub mod premium;
pub mod starboard;
pub mod stats;
pub mod xproles;
39 changes: 35 additions & 4 deletions src/core/posroles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use crate::{
utils::{id_as_i64::GetI64, into_id::IntoId},
};

use super::premium::is_premium::is_guild_premium;

pub struct GuildPRUpdateResult {
pub removed_roles: i32,
pub added_roles: i32,
Expand All @@ -28,16 +30,45 @@ pub async fn loop_update_posroles(bot: Arc<StarboardBot>) {
.fetch_all(&bot.pool)
.await;

let Ok(guilds) = guilds else {
sentry::capture_message("Updating posroles failed due to query.", sentry::Level::Error);
continue;
let guilds = match guilds {
Ok(guilds) => guilds,
Err(err) => {
bot.handle_error(&err.into()).await;
continue;
}
};

let mut tasks = Vec::new();
for guild in guilds {
tokio::spawn(update_posroles_for_guild(
let is_prem = match is_guild_premium(&bot, guild.guild_id).await {
Ok(is_prem) => is_prem,
Err(why) => {
bot.handle_error(&why).await;
continue;
}
};
if !is_prem {
continue;
}
let task = tokio::spawn(update_posroles_for_guild(
bot.clone(),
guild.guild_id.into_id(),
));
tasks.push(task);
}

for t in tasks {
let ret = t.await;
let ret = match ret {
Ok(ret) => ret,
Err(err) => {
bot.handle_error(&err.into()).await;
continue;
}
};
if let Err(err) = ret {
bot.handle_error(&err.into()).await;
}
}
}
}
Expand Down
63 changes: 63 additions & 0 deletions src/core/premium/expire.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use futures::TryStreamExt;
use std::sync::Arc;

use crate::{
client::bot::StarboardBot, constants, core::premium::locks::refresh_premium_locks,
errors::StarboardResult,
};

use super::{
is_premium::is_guild_premium,
redeem::{redeem_premium, RedeemPremiumResult},
};

pub async fn loop_expire_premium(bot: Arc<StarboardBot>) {
loop {
tokio::time::sleep(constants::CHECK_EXPIRED_PREMIUM).await;

if let Err(err) = check_expired_premium(bot.clone()).await {
bot.handle_error(&err).await;
}
}
}

async fn check_expired_premium(bot: Arc<StarboardBot>) -> StarboardResult<()> {
let expired_guilds = sqlx::query!(
"UPDATE guilds SET premium_end=null WHERE premium_end IS NOT NULL AND premium_end < $1
RETURNING guild_id",
chrono::Utc::now(),
)
.fetch_all(&bot.pool)
.await?;

for guild in expired_guilds {
tokio::spawn(StarboardBot::catch_future_errors(
bot.clone(),
process_expired_guild(bot.clone(), guild.guild_id),
));
}

Ok(())
}

async fn process_expired_guild(bot: Arc<StarboardBot>, guild_id: i64) -> StarboardResult<()> {
let mut stream = sqlx::query!(
"SELECT user_id FROM members WHERE autoredeem_enabled=true AND guild_id=$1",
guild_id
)
.fetch(&bot.pool);

while let Some(member) = stream.try_next().await? {
let ret = redeem_premium(&bot, member.user_id, guild_id, 1, Some(None)).await?;

match ret {
RedeemPremiumResult::Ok => break, // successfully added premium
RedeemPremiumResult::StateMismatch => break, // the server has premium now
RedeemPremiumResult::TooFewCredits => (), // try another member
}
}

refresh_premium_locks(&bot, guild_id, is_guild_premium(&bot, guild_id).await?).await?;

Ok(())
}
9 changes: 9 additions & 0 deletions src/core/premium/is_premium.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use crate::{client::bot::StarboardBot, database::Guild, errors::StarboardResult};

pub async fn is_guild_premium(bot: &StarboardBot, guild_id: i64) -> StarboardResult<bool> {
if let Some(guild) = Guild::get(&bot.pool, guild_id).await? {
Ok(guild.premium_end.is_some())
} else {
Ok(false)
}
}
Loading