Skip to content

Commit 9c07b1e

Browse files
committed
Feature/discord service (#24)
* Add serenity Add serenity as a dependency and use some extra features and the native TLS backend * Implement Discord Service - Implement Discord Service - Add discordTimeout config variable - Add additional logger setup to mute unimportant messages introduced by serenity
1 parent b48e218 commit 9c07b1e

File tree

6 files changed

+189
-10
lines changed

6 files changed

+189
-10
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ humantime = "2.1.0"
1818
log = { version = "0.4.20", features = ["serde"] }
1919
serde = { version = "1.0.193", features = ["derive"] }
2020
serde_json = "1.0.108"
21+
serenity = { version = "0.12.0", default-features=false, features = ["builder", "cache", "collector", "client", "framework", "gateway", "http", "model", "standard_framework", "utils", "voice", "default_native_tls", "tokio_task_builder", "unstable_discord_api", "simd_json", "temp_cache", "chrono", "interactions_endpoint"] }
2122
sqlx = { version = "0.7.3", features = ["runtime-tokio", "any", "postgres", "mysql", "sqlite", "tls-native-tls", "migrate", "macros", "uuid", "chrono", "json"] }
2223
thiserror = "1.0.52"
2324
tokio = { version = "1.35.1", features = ["full"] }

src/config.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::{
44
fmt::{Display, Formatter},
55
fs, io,
66
path::PathBuf,
7+
time::Duration,
78
};
89
use thiserror::Error;
910

@@ -37,16 +38,23 @@ fn discord_token_default() -> String {
3738
String::from("Please provide a token")
3839
}
3940

41+
fn discord_timeout_default() -> Duration {
42+
Duration::from_secs(10)
43+
}
44+
4045
#[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize, Clone)]
4146
pub struct Config {
4247
#[serde(rename = "discordToken", default = "discord_token_default")]
4348
pub discord_token: String,
49+
#[serde(rename = "discordTimeout", default = "discord_timeout_default")]
50+
pub discord_timeout: Duration,
4451
}
4552

4653
impl Default for Config {
4754
fn default() -> Self {
4855
Config {
4956
discord_token: discord_token_default(),
57+
discord_timeout: discord_timeout_default(),
5058
}
5159
}
5260
}

src/log.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ pub fn setup() -> Result<(), SetLoggerError> {
3333
))
3434
})
3535
.level(get_min_log_level())
36+
.level_for("serenity", LevelFilter::Warn)
37+
.level_for("hyper", LevelFilter::Warn)
38+
.level_for("tracing", LevelFilter::Warn)
39+
.level_for("reqwest", LevelFilter::Warn)
40+
.level_for("tungstenite", LevelFilter::Warn)
3641
.chain(io::stdout())
3742
.apply()?;
3843

src/main.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
use ::log::{error, warn};
2-
use lum::{bot::Bot, config::ConfigHandler, log, service::Service};
2+
use lum::{
3+
bot::Bot,
4+
config::{Config, ConfigHandler},
5+
log,
6+
service::{discord::DiscordService, Service},
7+
};
38

49
const BOT_NAME: &str = "Lum";
510

@@ -12,7 +17,7 @@ async fn main() {
1217
}
1318

1419
let config_handler = ConfigHandler::new(BOT_NAME.to_lowercase().as_str());
15-
let _config = match config_handler.load_config() {
20+
let config = match config_handler.load_config() {
1621
Ok(config) => config,
1722
Err(err) => {
1823
error!(
@@ -25,7 +30,7 @@ async fn main() {
2530
};
2631

2732
let bot = Bot::builder(BOT_NAME)
28-
.with_services(initialize_services())
33+
.with_services(initialize_services(&config))
2934
.build();
3035

3136
lum::run(bot).await;
@@ -40,9 +45,12 @@ fn setup_logger() {
4045
}
4146
}
4247

43-
fn initialize_services() -> Vec<Box<dyn Service>> {
48+
fn initialize_services(config: &Config) -> Vec<Box<dyn Service>> {
4449
//TODO: Add services
4550
//...
4651

47-
vec![]
52+
let discord_service =
53+
DiscordService::new(config.discord_token.as_str(), config.discord_timeout);
54+
55+
vec![Box::new(discord_service)]
4856
}

src/service.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use std::{
1111
};
1212
use tokio::sync::Mutex;
1313

14+
pub mod discord;
15+
1416
pub type PinnedBoxedFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
1517

1618
pub type PinnedBoxedFutureResult<'a, T> =
@@ -174,12 +176,12 @@ pub trait Service: ServiceInternals {
174176

175177
match self.start().await {
176178
Ok(()) => {
177-
self.info().set_status(Status::Started).await;
178179
info!("Started service: {}", self.info().name);
180+
self.info().set_status(Status::Started).await;
179181
}
180182
Err(error) => {
183+
error!("Failed to start service {}: {}", self.info().name, error);
181184
self.info().set_status(Status::FailedStarting(error)).await;
182-
error!("Failed to start service: {}", self.info().name);
183185
}
184186
}
185187
})
@@ -189,7 +191,7 @@ pub trait Service: ServiceInternals {
189191
Box::pin(async move {
190192
let mut status = self.info().status.lock().await;
191193

192-
if matches!(&*status, Status::Started) {
194+
if !matches!(&*status, Status::Started) {
193195
warn!(
194196
"Tried to stop service {} while it was in state {}. Ignoring stop request.",
195197
self.info().name,
@@ -203,12 +205,12 @@ pub trait Service: ServiceInternals {
203205

204206
match ServiceInternals::stop(self).await {
205207
Ok(()) => {
206-
self.info().set_status(Status::Stopped).await;
207208
info!("Stopped service: {}", self.info().name);
209+
self.info().set_status(Status::Stopped).await;
208210
}
209211
Err(error) => {
212+
error!("Failed to stop service {}: {}", self.info().name, error);
210213
self.info().set_status(Status::FailedStopping(error)).await;
211-
error!("Failed to stop service: {}", self.info().name);
212214
}
213215
}
214216
})

src/service/discord.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
use super::{PinnedBoxedFutureResult, Priority, Service, ServiceInfo, ServiceInternals};
2+
use log::info;
3+
use serenity::{
4+
all::{GatewayIntents, Ready},
5+
async_trait,
6+
client::{self, Cache, Context},
7+
framework::{standard::Configuration, StandardFramework},
8+
gateway::{ShardManager, VoiceGatewayManager},
9+
http::Http,
10+
prelude::TypeMap,
11+
Client, Error,
12+
};
13+
use std::{sync::Arc, time::Duration};
14+
use tokio::{
15+
sync::{Mutex, Notify, RwLock},
16+
task::JoinHandle,
17+
time::timeout,
18+
};
19+
20+
pub struct DiscordService {
21+
info: ServiceInfo,
22+
discord_token: String,
23+
connection_timeout: Duration,
24+
pub client: Arc<Mutex<Option<Ready>>>,
25+
client_handle: Option<JoinHandle<Result<(), Error>>>,
26+
pub cache: Option<Arc<Cache>>,
27+
pub data: Option<Arc<RwLock<TypeMap>>>,
28+
pub http: Option<Arc<Http>>,
29+
pub shard_manager: Option<Arc<ShardManager>>,
30+
pub voice_manager: Option<Arc<dyn VoiceGatewayManager>>,
31+
pub ws_url: Option<Arc<Mutex<String>>>,
32+
}
33+
34+
impl DiscordService {
35+
pub fn new(discord_token: &str, connection_timeout: Duration) -> Self {
36+
Self {
37+
info: ServiceInfo::new("lum_builtin_discord", "Discord", Priority::Essential),
38+
discord_token: discord_token.to_string(),
39+
connection_timeout,
40+
client: Arc::new(Mutex::new(None)),
41+
client_handle: None,
42+
cache: None,
43+
data: None,
44+
http: None,
45+
shard_manager: None,
46+
voice_manager: None,
47+
ws_url: None,
48+
}
49+
}
50+
}
51+
52+
impl ServiceInternals for DiscordService {
53+
fn start(&mut self) -> PinnedBoxedFutureResult<'_, ()> {
54+
Box::pin(async move {
55+
let framework = StandardFramework::new();
56+
framework.configure(Configuration::new().prefix("!"));
57+
58+
let client_ready_notify = Arc::new(Notify::new());
59+
60+
let mut client = Client::builder(self.discord_token.as_str(), GatewayIntents::all())
61+
.framework(framework)
62+
.event_handler(EventHandler::new(
63+
Arc::clone(&self.client),
64+
Arc::clone(&client_ready_notify),
65+
))
66+
.await?;
67+
68+
self.cache = Some(Arc::clone(&client.cache));
69+
self.data = Some(Arc::clone(&client.data));
70+
self.http = Some(Arc::clone(&client.http));
71+
self.shard_manager = Some(Arc::clone(&client.shard_manager));
72+
if let Some(shard_manager) = &self.shard_manager {
73+
self.shard_manager = Some(Arc::clone(shard_manager));
74+
}
75+
if let Some(voice_manager) = &self.voice_manager {
76+
self.voice_manager = Some(Arc::clone(voice_manager));
77+
}
78+
self.ws_url = Some(Arc::clone(&client.ws_url));
79+
80+
info!("Connecting to Discord");
81+
let client_handle = tokio::spawn(async move { client.start().await });
82+
83+
if timeout(self.connection_timeout, client_ready_notify.notified())
84+
.await
85+
.is_err()
86+
{
87+
client_handle.abort();
88+
89+
return Err(format!(
90+
"Discord client failed to connect within {} seconds",
91+
self.connection_timeout.as_secs()
92+
)
93+
.into());
94+
}
95+
96+
if client_handle.is_finished() {
97+
client_handle.await??;
98+
return Err("Discord client stopped unexpectedly and with no error".into());
99+
}
100+
101+
self.client_handle = Some(client_handle);
102+
Ok(())
103+
})
104+
}
105+
106+
fn stop(&mut self) -> PinnedBoxedFutureResult<'_, ()> {
107+
Box::pin(async move {
108+
if let Some(handle) = self.client_handle.take() {
109+
info!("Waiting for Discord client to stop...");
110+
handle.abort();
111+
112+
let result = match handle.await {
113+
Ok(result) => result,
114+
Err(_) => {
115+
info!("Discord client stopped");
116+
return Ok(());
117+
}
118+
};
119+
120+
result?;
121+
}
122+
123+
Ok(())
124+
})
125+
}
126+
}
127+
128+
impl Service for DiscordService {
129+
fn info(&self) -> &ServiceInfo {
130+
&self.info
131+
}
132+
}
133+
134+
struct EventHandler {
135+
client: Arc<Mutex<Option<Ready>>>,
136+
ready_notify: Arc<Notify>,
137+
}
138+
139+
impl EventHandler {
140+
pub fn new(client: Arc<Mutex<Option<Ready>>>, ready_notify: Arc<Notify>) -> Self {
141+
Self {
142+
client,
143+
ready_notify,
144+
}
145+
}
146+
}
147+
148+
#[async_trait]
149+
impl client::EventHandler for EventHandler {
150+
async fn ready(&self, _ctx: Context, data_about_bot: Ready) {
151+
info!("Connected to Discord as {}", data_about_bot.user.tag());
152+
*self.client.lock().await = Some(data_about_bot);
153+
self.ready_notify.notify_one();
154+
}
155+
}

0 commit comments

Comments
 (0)