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#[derive(Deserialize, Debug)]
25pub struct Config {
26 pub token: String,
28
29 pub application_id: u64,
31
32 #[serde(default = "default_prefix")]
34 pub prefix: String,
35
36 pub status: Option<StatusConfig>,
38
39 pub data_dir: Utf8PathBuf,
41
42 pub test_guild: Option<GuildId>,
44
45 pub fml: FmlConfig,
47
48 pub deviantart: DeviantArtConfig,
50
51 pub sauce_nao: SauceNaoConfig,
53
54 #[serde(rename = "open-ai")]
56 pub open_ai: OpenAiConfig,
57
58 #[serde(default)]
60 pub log: LogConfig,
61
62 #[serde(flatten)]
64 pub extra: HashMap<String, toml::Value>,
65}
66
67#[derive(Deserialize, Debug)]
69pub struct FmlConfig {
70 pub key: String,
72}
73
74#[derive(Deserialize, Debug)]
76pub struct DeviantArtConfig {
77 pub username: String,
79
80 pub password: String,
82}
83
84#[derive(Deserialize, Debug)]
86pub struct SauceNaoConfig {
87 pub api_key: String,
89
90 #[serde(flatten)]
92 pub extra: HashMap<String, toml::Value>,
93}
94
95#[derive(Deserialize, Debug)]
97pub struct OpenAiConfig {
98 #[serde(rename = "api-key")]
100 pub api_key: String,
101
102 #[serde(flatten)]
104 pub extra: HashMap<String, toml::Value>,
105}
106
107#[derive(Deserialize, Debug)]
109pub struct LogConfig {
110 #[serde(default = "LogConfig::default_directives")]
112 pub directives: Vec<String>,
113
114 #[serde(default, rename = "opentelemetry")]
116 pub opentelemetry: bool,
117
118 pub endpoint: Option<String>,
120
121 #[serde(default)]
123 pub headers: HashMap<String, String>,
124}
125
126impl LogConfig {
127 fn default_directives() -> Vec<String> {
129 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 pub fn status_name(&self) -> Option<&str> {
153 self.status.as_ref().map(|s| s.name.as_str())
154 }
155
156 pub fn status_url(&self) -> Option<&str> {
158 self.status.as_ref().and_then(|s| s.url.as_deref())
159 }
160
161 pub fn status_type(&self) -> Option<ActivityKind> {
163 self.status.as_ref().and_then(|s| s.kind)
164 }
165
166 pub fn log_file_dir(&self) -> Utf8PathBuf {
168 self.data_dir.join("logs")
169 }
170
171 pub fn cache_dir(&self) -> Utf8PathBuf {
173 self.data_dir.join("cache")
174 }
175
176 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 pub fn load_from_str(s: &str) -> anyhow::Result<Self> {
189 toml::from_str(s).context("failed to parse config")
190 }
191
192 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#[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}