1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
|
use std::{env, error::Error, path::PathBuf, sync::Arc};
use brain::Brain;
use confindent::Confindent;
use database::Database;
use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType};
use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt};
use twilight_http::Client as HttpClient;
use twilight_model::{application::interaction::InteractionData, id::Id};
use crate::command::Commands;
mod brain;
mod command;
mod database;
mod import;
mod util;
const PROD_APP_ID: u64 = 1363055126264283136;
#[allow(dead_code)]
const DEV_APP_ID: u64 = 1363494986699771984;
const APP_ID: u64 = PROD_APP_ID;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
//dotenv::dotenv().ok();
let conf_path = if !PathBuf::from("/etc/leaberblord.conf").exists() {
"leaberblord.conf"
} else {
"/etc/leaberblord.conf"
};
let conf = Confindent::from_file(conf_path).unwrap();
let db_dir = conf
.child_owned("Database")
.unwrap_or("/var/leaberblord/leaberblord.sqlite".to_string());
let db = Database::new(db_dir);
db.create_tables();
// Look for an `import` CLI-thing so we know if we should impor and emboard
// database from local
let arg = std::env::args().nth(1);
match arg.as_deref() {
Some("import") => {
let Some(file) = std::env::args().nth(2) else {
eprintln!("import subcommand requires a file path argument to read data from.");
return Ok(());
};
let data = std::fs::read_to_string(file).unwrap();
import::import(&db, data);
println!("Import finished!");
return Ok(());
}
_ => (),
}
// Finally, setup and start the bot itself
let TwilightStuff {
mut shard,
http,
cache,
} = setup_twilight(&conf).await;
let brain = Arc::new(Brain::new(db, http, cache));
while let Some(item) = shard.next_event(EventTypeFlags::all()).await {
let Ok(event) = item else {
eprintln!("error receiving event");
continue;
};
brain.update_cache(&event);
tokio::spawn(handle_event(event, Arc::clone(&brain)));
}
Ok(())
}
/// A more proper container than a tuple for everything we get from [setup_twilight]
struct TwilightStuff {
shard: Shard,
http: HttpClient,
cache: DefaultInMemoryCache,
}
/// Configure enough to get off the ground with twilight.
/// - Get the `DISCORD_TOKEN` from the environment or `DiscordToken` from the conf file, with that preference
/// - Setup ourself as Shard 1 and take the `GUILD_MESSAGES` intent
/// - Register with the API and create our `InMemoryCache`
/// - Register all of our commands
async fn setup_twilight(conf: &Confindent) -> TwilightStuff {
let token = env::var("DISCORD_TOKEN")
.ok()
.unwrap_or_else(|| conf.child_owned("DiscordToken").unwrap());
let shard = Shard::new(ShardId::ONE, token.clone(), Intents::GUILD_MESSAGES);
let http = HttpClient::new(token);
let cache = DefaultInMemoryCache::builder()
.resource_types(ResourceType::GUILD)
.resource_types(ResourceType::MESSAGE)
.resource_types(ResourceType::MEMBER)
.build();
let iclient = http.interaction(Id::new(APP_ID));
let commands_vec = command::build().await;
iclient.set_global_commands(&commands_vec).await.unwrap();
println!("Set commands!");
let cmd_ap_global = iclient
.global_commands()
.await
.unwrap()
.models()
.await
.unwrap();
for cmd in cmd_ap_global {
println!(
"global command- [{}] {}: {}",
cmd.id
.map(|c| c.get().to_string())
.unwrap_or("-".to_owned()),
cmd.name,
cmd.description
);
}
TwilightStuff { shard, http, cache }
}
/// Handle all of our events. This is our main work function and what main
/// tends to branch into, but we don't loop in the function itself.
async fn handle_event(event: Event, brain: Arc<Brain>) -> Result<(), Box<dyn Error + Send + Sync>> {
match event {
Event::InteractionCreate(create) => {
// Don't allow direct message commanding
if create.is_dm() {
brain
.interaction_respond(&create, fail!("leaderboards not supported in DMs"))
.await;
return Ok(());
}
let data = create.data.as_ref().unwrap();
if let InteractionData::ApplicationCommand(cmd) = data {
let guild = create.guild.as_ref().unwrap().id.unwrap();
let command = match cmd.name.as_str() {
"about" => Commands::About,
"leaderboard" => Commands::Leaderboard,
"points" => Commands::Points,
"revise" => Commands::Revise,
"permission" => Commands::Permission,
"import" => Commands::Import,
_ => panic!("'{}' is not a command", cmd.name),
};
command::handler(brain, guild, &create, command, cmd).await;
}
}
Event::GatewayClose(close) => {
println!("GatewayClose - {close:?}")
}
_ => (),
}
Ok(())
}
|