pikadick/
config.rs

1use anyhow::Context;
2use camino::{
3    Utf8Path,
4    Utf8PathBuf,
5};
6use serde::{
7    Deserialize,
8    Serialize,
9};
10use serenity::{
11    model::prelude::GuildId,
12    utils::validate_token,
13};
14use std::{
15    borrow::Cow,
16    collections::HashMap,
17};
18
19fn default_prefix() -> String {
20    "p!".to_string()
21}
22
23/// The bot config
24#[derive(Deserialize, Debug)]
25pub struct Config {
26    /// The discord token
27    pub token: String,
28
29    /// The application id
30    pub application_id: u64,
31
32    /// Prefix for the bot
33    #[serde(default = "default_prefix")]
34    pub prefix: String,
35
36    /// Status config
37    pub status: Option<StatusConfig>,
38
39    /// Data dir
40    pub data_dir: Utf8PathBuf,
41
42    /// The test guild
43    pub test_guild: Option<GuildId>,
44
45    /// FML config
46    pub fml: FmlConfig,
47
48    /// DeviantArt config
49    pub deviantart: DeviantArtConfig,
50
51    /// SauceNao config
52    pub sauce_nao: SauceNaoConfig,
53
54    /// Open AI config
55    #[serde(rename = "open-ai")]
56    pub open_ai: OpenAiConfig,
57
58    /// The log config
59    #[serde(default)]
60    pub log: LogConfig,
61
62    /// Unknown extra data
63    #[serde(flatten)]
64    pub extra: HashMap<String, toml::Value>,
65}
66
67/// FML config
68#[derive(Deserialize, Debug)]
69pub struct FmlConfig {
70    /// FML API key
71    pub key: String,
72}
73
74/// Deviant Config
75#[derive(Deserialize, Debug)]
76pub struct DeviantArtConfig {
77    /// Username
78    pub username: String,
79
80    /// Password
81    pub password: String,
82}
83
84/// SauceNao Config
85#[derive(Deserialize, Debug)]
86pub struct SauceNaoConfig {
87    /// The api key
88    pub api_key: String,
89
90    /// Unknown extra data
91    #[serde(flatten)]
92    pub extra: HashMap<String, toml::Value>,
93}
94
95/// Open AI Config
96#[derive(Deserialize, Debug)]
97pub struct OpenAiConfig {
98    /// The api key
99    #[serde(rename = "api-key")]
100    pub api_key: String,
101
102    /// Unknown extra data
103    #[serde(flatten)]
104    pub extra: HashMap<String, toml::Value>,
105}
106
107/// Log Config
108#[derive(Deserialize, Debug)]
109pub struct LogConfig {
110    /// The logging directives.
111    #[serde(default = "LogConfig::default_directives")]
112    pub directives: Vec<String>,
113
114    /// Whether to use opentelemetry
115    #[serde(default, rename = "opentelemetry")]
116    pub opentelemetry: bool,
117
118    /// The OTLP endpoint
119    pub endpoint: Option<String>,
120
121    /// Headers
122    #[serde(default)]
123    pub headers: HashMap<String, String>,
124}
125
126impl LogConfig {
127    /// If logging directives not given, choose some defaults.
128    fn default_directives() -> Vec<String> {
129        // Only enable pikadick since serenity likes puking in the logs during connection failures
130        // serenity's framework section seems ok as well
131        vec![
132            "pikadick=info".to_string(),
133            "serenity::framework::standard=info".to_string(),
134        ]
135    }
136}
137
138impl Default for LogConfig {
139    fn default() -> Self {
140        Self {
141            directives: Self::default_directives(),
142
143            opentelemetry: false,
144            endpoint: None,
145            headers: HashMap::new(),
146        }
147    }
148}
149
150impl Config {
151    /// Shortcut for getting the status name
152    pub fn status_name(&self) -> Option<&str> {
153        self.status.as_ref().map(|s| s.name.as_str())
154    }
155
156    /// Shortcut for getting the status url
157    pub fn status_url(&self) -> Option<&str> {
158        self.status.as_ref().and_then(|s| s.url.as_deref())
159    }
160
161    /// Shortcut for getting the status type
162    pub fn status_type(&self) -> Option<ActivityKind> {
163        self.status.as_ref().and_then(|s| s.kind)
164    }
165
166    /// The log file dir
167    pub fn log_file_dir(&self) -> Utf8PathBuf {
168        self.data_dir.join("logs")
169    }
170
171    /// The cache dir
172    pub fn cache_dir(&self) -> Utf8PathBuf {
173        self.data_dir.join("cache")
174    }
175
176    /// Load a config from a path
177    pub fn load_from_path<P>(path: P) -> anyhow::Result<Self>
178    where
179        P: AsRef<Utf8Path>,
180    {
181        let path = path.as_ref();
182        std::fs::read_to_string(path)
183            .with_context(|| format!("failed to read config from '{}'", path))
184            .and_then(|b| Self::load_from_str(&b))
185    }
186
187    /// Load a config from a str
188    pub fn load_from_str(s: &str) -> anyhow::Result<Self> {
189        toml::from_str(s).context("failed to parse config")
190    }
191
192    /// Validate a config
193    pub fn validate(&mut self) -> Vec<ValidationMessage> {
194        let mut errors = Vec::with_capacity(3);
195
196        if let Err(_e) = validate_token(&self.token) {
197            errors.push(ValidationMessage {
198                severity: Severity::Error,
199                error: ValidationError::InvalidToken,
200            });
201        }
202
203        if let Some(config) = &self.status {
204            if let (Some(ActivityKind::Streaming), None) = (config.kind, &config.url) {
205                errors.push(ValidationMessage {
206                    severity: Severity::Error,
207                    error: ValidationError::MissingStreamUrl,
208                });
209            }
210
211            if let (None, _) = (config.kind, &config.url) {
212                errors.push(ValidationMessage {
213                    severity: Severity::Warn,
214                    error: ValidationError::MissingStatusType,
215                });
216            }
217        }
218
219        errors
220    }
221}
222
223#[derive(Deserialize, Debug)]
224pub struct StatusConfig {
225    #[serde(rename = "type")]
226    #[serde(default)]
227    kind: Option<ActivityKind>,
228    name: String,
229    url: Option<String>,
230
231    #[serde(flatten)]
232    pub extra: HashMap<String, toml::Value>,
233}
234
235#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Deserialize, Serialize, Default)]
236pub enum ActivityKind {
237    Listening,
238    #[default]
239    Playing,
240    Streaming,
241}
242
243#[derive(Debug)]
244pub struct ValidationMessage {
245    severity: Severity,
246    error: ValidationError,
247}
248
249impl ValidationMessage {
250    pub fn severity(&self) -> Severity {
251        self.severity
252    }
253
254    pub fn error(&self) -> &ValidationError {
255        &self.error
256    }
257}
258
259/// Validation Errors
260#[derive(Debug, thiserror::Error)]
261pub enum ValidationError {
262    #[error("invalid token")]
263    InvalidToken,
264    #[error("missing status type")]
265    MissingStatusType,
266    #[error("missing stream url type")]
267    MissingStreamUrl,
268
269    #[error("{0}")]
270    Generic(Cow<'static, str>),
271}
272
273#[derive(Copy, Clone, Debug)]
274pub enum Severity {
275    Warn,
276    Error,
277}