main.rs
1 // Goober Bot, Discord bot 2 // Copyright (C) 2025 Valentine Briese 3 // 4 // This program is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Affero General Public License as published 6 // by the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // This program is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <https://www.gnu.org/licenses/>. 16 17 // TODO: make general refinements to existing codebase 18 // TODO: add birthday announcements system 19 // TODO: replace mentions of specific commands with actual formatted command 20 // mentions when https://github.com/serenity-rs/poise/issues/235 is 21 // resolved 22 23 use activity::start_activity_loop; 24 use analytics::analytics; 25 use commands::CustomData; 26 use config::config; 27 use monetary::has_early_access; 28 use poise::{ 29 Framework, FrameworkOptions, 30 serenity_prelude::{ClientBuilder, GatewayIntents}, 31 }; 32 use poise_error::{anyhow::Context as _, on_error}; 33 use shared::Data; 34 use shuttle_runtime::{CustomError, SecretStore}; 35 use shuttle_serenity::ShuttleSerenity; 36 use shuttle_shared_db::SerdeJsonOperator; 37 use tracing::{error, info}; 38 39 #[shuttle_runtime::main] 40 async fn main( 41 #[shuttle_runtime::Secrets] secret_store: SecretStore, 42 #[shuttle_shared_db::Postgres] op: SerdeJsonOperator, 43 ) -> ShuttleSerenity { 44 tracing_subscriber::fmt() 45 // If making a new crate, make sure to add it here. 46 .with_env_filter( 47 "goober_bot=debug,\ 48 activity=debug,\ 49 analytics=debug,\ 50 command_anon=debug,\ 51 command_debug=debug,\ 52 command_rock_paper_scissors=debug,\ 53 command_silly=debug,\ 54 command_strike=debug,\ 55 command_timestamp=debug,\ 56 command_updates=debug,\ 57 command_updates_proc_macro=debug,\ 58 command_vote=debug,\ 59 commands=debug,\ 60 commands_shared=debug,\ 61 config=debug,\ 62 database=debug,\ 63 emoji=debug,\ 64 monetary=debug,\ 65 shared=debug,\ 66 info", 67 ) 68 .without_time() 69 .init(); 70 71 #[cfg(not(debug_assertions))] 72 let topgg_client = { 73 let topgg_token = secret_store 74 .get("TOPGG_TOKEN") 75 .context("`TOPGG_TOKEN` was not found")?; 76 77 topgg::Client::new(topgg_token) 78 }; 79 let client_builder = { 80 let discord_token = secret_store 81 .get("DISCORD_TOKEN") 82 .context("`DISCORD_TOKEN` was not found")?; 83 84 ClientBuilder::new(discord_token, GatewayIntents::GUILDS) 85 }; 86 #[cfg(not(debug_assertions))] 87 let autoposter = { 88 use std::time::Duration; 89 90 use topgg::Autoposter; 91 92 info!("Bot will post stats to Top.gg"); 93 94 Autoposter::serenity(&topgg_client, Duration::from_secs(1800)) 95 }; 96 #[cfg(not(debug_assertions))] 97 let client_builder = client_builder.event_handler_arc(autoposter.handler()); 98 let mut commands = vec![ 99 analytics(), 100 commands::anon(), 101 commands::arrest(), 102 commands::bap(), 103 commands::bite(), 104 commands::blow_up(), 105 commands::boop(), 106 commands::carry(), 107 commands::debug(), 108 commands::defenestrate(), 109 commands::gnaw(), 110 commands::hamburger(), 111 commands::hug(), 112 commands::jumpscare(), 113 commands::kiss(), 114 commands::meow(), 115 commands::murder(), 116 commands::pat(), 117 commands::poke(), 118 commands::revive(), 119 commands::rock_paper_scissors(), 120 commands::slap(), 121 commands::strike(), 122 commands::tickle(), 123 commands::timestamp(), 124 commands::updates(), 125 #[cfg(not(debug_assertions))] 126 commands::vote(), 127 config(), 128 ]; 129 130 for command in commands.iter_mut() { 131 if let Some(custom_data) = command.custom_data.downcast_ref::<CustomData>() { 132 if custom_data.early_access { 133 command.checks.push(|ctx| Box::pin(has_early_access(ctx))); 134 } 135 } 136 } 137 138 let framework = Framework::builder() 139 .options(FrameworkOptions { 140 commands, 141 on_error, 142 pre_command: |ctx| { 143 Box::pin(async move { 144 if let Err(err) = analytics::increment(ctx).await { 145 error!("An error occurred whilst performing analytics: {err:#?}"); 146 } 147 148 info!( 149 "{} invoked `{}`", 150 ctx.author().name, 151 ctx.invocation_string(), 152 ); 153 }) 154 }, 155 post_command: |ctx| { 156 Box::pin(async move { 157 info!( 158 "{}'s `{}` invocation finished successfully", 159 ctx.author().name, 160 ctx.invocation_string(), 161 ); 162 }) 163 }, 164 ..Default::default() 165 }) 166 .setup(|ctx, _ready, framework| { 167 Box::pin(async move { 168 start_activity_loop(ctx.clone()); 169 poise::builtins::register_globally(ctx, &framework.options().commands).await?; 170 info!("Commands registered"); 171 172 Ok(Data { 173 op, 174 #[cfg(not(debug_assertions))] 175 topgg_client, 176 #[cfg(not(debug_assertions))] 177 _autoposter: autoposter, 178 }) 179 }) 180 }) 181 .build(); 182 let client_builder = client_builder.framework(framework); 183 let client = client_builder.await.map_err(CustomError::new)?; 184 185 Ok(client.into()) 186 }