/ src / main.rs
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  }