Skip to content

Commit

Permalink
progress
Browse files Browse the repository at this point in the history
  • Loading branch information
ZephyrTFA committed Nov 7, 2024
1 parent 857571c commit 40d7230
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 5 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ Cargo.lock
# Added by cargo

/target
.env
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"editor.formatOnSave": true,
"rust-analyzer.check.command": "clippy",
}
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ version = "0.1.0"
edition = "2021"

[dependencies]
dotenv = "0.15.0"
futures = "0.3.31"
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.132"
serenity = "0.12.2"
tokio = { version = "1.41.0", features = ["macros", "rt-multi-thread"] }
2 changes: 1 addition & 1 deletion package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ in stdenv.mkDerivation {
owner = "tgstation";
repo = "dragon-bot";
rev = "${version}";
hash = "sha256-DofTPeRx7lMX2Un3OYeQ0ZiSpYfdfTp7yvYAIRRwjG8=";
hash = "sha256-DofTPeRx7lMX2Un3OaeQ0ZiSpYfdfTp7yvYAIRRwjG8=";
};

buildInputs = with pkgs; [
Expand Down
30 changes: 30 additions & 0 deletions src/audit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use serenity::{
all::{Context, EventHandler, GuildId, GuildMemberUpdateEvent, Member, User},
async_trait,
};

pub struct AuditHandler;

#[async_trait]
#[allow(unused_variables)]
impl EventHandler for AuditHandler {
async fn guild_member_addition(&self, ctx: Context, new_member: Member) {}

async fn guild_member_removal(
&self,
ctx: Context,
guild_id: GuildId,
user: User,
member_data_if_available: Option<Member>,
) {
}

async fn guild_member_update(
&self,
ctx: Context,
old_if_available: Option<Member>,
new: Option<Member>,
event: GuildMemberUpdateEvent,
) {
}
}
13 changes: 13 additions & 0 deletions src/commands/base.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use serenity::{
all::{CommandInteraction, Context, CreateCommandOption, Permissions},
async_trait,
};

#[async_trait]
pub trait Command {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn options(&self) -> Vec<CreateCommandOption>;
fn default_permission(&self) -> Permissions;
async fn execute(&self, ctx: Context, interaction: CommandInteraction);
}
95 changes: 95 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use std::process::exit;

use serenity::all::{CommandInteraction, EditInteractionResponse};
use serenity::{
all::{
Context, CreateCommand, CreateInteractionResponse, CreateInteractionResponseMessage,
EventHandler, GuildId, Interaction,
},
async_trait,
model::gateway::Ready,
};

use crate::get_guild_id;

pub mod base;

pub mod tgverify;

pub const COMMANDS: [&(dyn base::Command + Sync); 1] = [&tgverify::TgVerifyCommand];

pub struct CommandHandler;

#[async_trait]
impl EventHandler for CommandHandler {
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
let command = interaction.command();
if command.is_none() {
return;
}
let command_interaction = command.unwrap();
let name = command_interaction.data.name.clone();
println!("Received command: {}", name);

let command = COMMANDS.iter().find(|command| command.name() == name);
if command.is_none() {
return;
}

if let Err(why) = command_interaction
.create_response(
&ctx.http,
CreateInteractionResponse::Defer(CreateInteractionResponseMessage::default()),
)
.await
{
println!("Failed to acknowledge command: {:?}", why);
return;
}

let command = command.unwrap();
command.execute(ctx, command_interaction).await;
}

async fn ready(&self, ctx: Context, _ready: Ready) {
let id = get_guild_id();
if id == 0 {
println!("GUILD_ID is invalid.");
exit(1);
}

let guild = GuildId::new(id);
let mut commands = vec![];
for command in COMMANDS.iter() {
commands.push(CommandHandler::create_command(*command));
}

if let Err(why) = guild.set_commands(&ctx.http, commands).await {
println!("Failed to set application commands: {:?}", why);
exit(1);
}
println!("Commands set.");
}
}

impl CommandHandler {
fn create_command(command: &dyn base::Command) -> CreateCommand {
let mut create_command = CreateCommand::new(command.name())
.description(command.description())
.dm_permission(false);
for option in command.options() {
create_command = create_command.add_option(option);
}
create_command.default_member_permissions(command.default_permission())
}
}

pub async fn send_response(ctx: &Context, interaction: &CommandInteraction, content: &str) {
println!("Sending response: {}", content);
if let Err(why) = interaction
.edit_response(&ctx.http, EditInteractionResponse::new().content(content))
.await
{
println!("Failed to send response: {:?}", why);
}
}
39 changes: 39 additions & 0 deletions src/commands/tgverify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use serenity::{
all::{CommandInteraction, CommandOptionType, Context, CreateCommandOption, Permissions},
async_trait,
};

use crate::commands::send_response;

use super::base::Command;

pub struct TgVerifyCommand;

#[async_trait]
impl Command for TgVerifyCommand {
fn name(&self) -> &str {
"tgverify"
}

fn description(&self) -> &str {
"Verify your BYOND account."
}

fn options(&self) -> Vec<CreateCommandOption> {
vec![CreateCommandOption::new(
CommandOptionType::String,
"token",
"The token you received from the game server.",
)
.required(true)]
}

async fn execute(&self, ctx: Context, interaction: CommandInteraction) {
println!("TODO: Implement tgverify command");
send_response(&ctx, &interaction, "TODO").await;
}

fn default_permission(&self) -> serenity::all::Permissions {
Permissions::empty()
}
}
40 changes: 40 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use std::collections::HashMap;

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct Config {
guild_id: u64,
values: HashMap<String, String>,
}

impl Config {
pub fn new(guild_id: u64) -> Self {
Self {
guild_id,
values: HashMap::new(),
}
}

pub fn save(&self) {
let path = std::env::var("CONFIG_PATH").unwrap();
let path = std::path::PathBuf::from(path);
let path = path.join(format!("{}.json", self.guild_id));
let data = serde_json::to_string_pretty(&self).unwrap();
std::fs::write(&path, data).unwrap();
}

pub fn get(&self, key: &str) -> Option<&str> {
self.values.get(key).map(|s| s.as_str())
}

pub fn set(&mut self, key: &str, value: &str) {
self.values.insert(key.to_string(), value.to_string());
}
}

impl Drop for Config {
fn drop(&mut self) {
self.save();
}
}
93 changes: 89 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,92 @@
use std::thread;
use std::{
collections::HashMap,
path::PathBuf,
process::exit,
sync::{Arc, LazyLock, Mutex},
};

fn main() {
loop {
thread::park();
use config::Config;
use serenity::all::GatewayIntents;

pub mod audit;
pub mod commands;
pub mod config;

#[tokio::main]
async fn main() {
dotenv::dotenv().ok();
ensure_env_vars();

let client = serenity::Client::builder(
get_discord_token(),
GatewayIntents::GUILD_MESSAGES
| GatewayIntents::GUILD_MESSAGE_REACTIONS
| GatewayIntents::GUILD_MEMBERS
| GatewayIntents::MESSAGE_CONTENT,
)
.event_handler(commands::CommandHandler)
.event_handler(audit::AuditHandler)
.await;
if client.is_err() {
println!("Failed to create client: {:?}", client.err().unwrap());
exit(1);
}
let mut client = client.unwrap();

let invite_url = format!(
"https://discord.com/api/oauth2/authorize?client_id={}&permissions=8&scope=bot%20applications.commands",
client.http.get_current_application_info().await.unwrap().id
);

println!("Invite URL: {}", invite_url);
println!("Starting client...");
if let Err(why) = client.start().await {
println!("Client error: {:?}", why);
}
println!("Client closed.");
}

const DISCORD_TOKEN: &str = "DISCORD_TOKEN";
const GUILD_ID: &str = "GUILD_ID";

pub fn ensure_env_vars() {
if std::env::var(DISCORD_TOKEN).is_err() {
println!("{} is not set", DISCORD_TOKEN);
exit(1);
}
if std::env::var(GUILD_ID).is_err() {
println!("{} is not set", GUILD_ID);
exit(1);
}
if std::env::var("CONFIG_PATH").is_err() {
println!("CONFIG_PATH is not set");
exit(1);
}
}

pub fn get_discord_token() -> String {
std::env::var(DISCORD_TOKEN).unwrap()
}

pub fn get_guild_id() -> u64 {
std::env::var(GUILD_ID).unwrap().parse().unwrap()
}

pub fn get_config(guild: u64) -> Arc<Config> {
static CONFIGS: LazyLock<Mutex<HashMap<u64, Arc<Config>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));

let mut configs = CONFIGS.lock().expect("failed to lock CONFIGS");
configs.entry(guild).or_insert_with(|| {
let path = PathBuf::from(std::env::var("CONFIG_PATH").unwrap());
let path = path.join(format!("{}.json", guild));
if !path.exists() {
let config = Config::new(guild);
config.save();
}
let data = std::fs::read_to_string(path).unwrap();
Arc::new(serde_json::from_str(&data).unwrap())
});

configs.get(&guild).unwrap().clone()
}

0 comments on commit 40d7230

Please sign in to comment.