1pub mod api;
2pub mod auth;
3pub mod db;
4pub mod env;
5pub mod executor;
6mod rate_limiter;
7pub mod rpc;
8pub mod seed;
9
10#[cfg(test)]
11mod tests;
12
13use anyhow::anyhow;
14use aws_config::{BehaviorVersion, Region};
15use axum::{http::StatusCode, response::IntoResponse};
16use db::{ChannelId, Database};
17use executor::Executor;
18pub use rate_limiter::*;
19use serde::Deserialize;
20use std::{path::PathBuf, sync::Arc};
21use util::ResultExt;
22
23pub type Result<T, E = Error> = std::result::Result<T, E>;
24
25pub enum Error {
26 Http(StatusCode, String),
27 Database(sea_orm::error::DbErr),
28 Internal(anyhow::Error),
29}
30
31impl From<anyhow::Error> for Error {
32 fn from(error: anyhow::Error) -> Self {
33 Self::Internal(error)
34 }
35}
36
37impl From<sea_orm::error::DbErr> for Error {
38 fn from(error: sea_orm::error::DbErr) -> Self {
39 Self::Database(error)
40 }
41}
42
43impl From<axum::Error> for Error {
44 fn from(error: axum::Error) -> Self {
45 Self::Internal(error.into())
46 }
47}
48
49impl From<axum::http::Error> for Error {
50 fn from(error: axum::http::Error) -> Self {
51 Self::Internal(error.into())
52 }
53}
54
55impl From<serde_json::Error> for Error {
56 fn from(error: serde_json::Error) -> Self {
57 Self::Internal(error.into())
58 }
59}
60
61impl IntoResponse for Error {
62 fn into_response(self) -> axum::response::Response {
63 match self {
64 Error::Http(code, message) => {
65 log::error!("HTTP error {}: {}", code, &message);
66 (code, message).into_response()
67 }
68 Error::Database(error) => {
69 log::error!(
70 "HTTP error {}: {:?}",
71 StatusCode::INTERNAL_SERVER_ERROR,
72 &error
73 );
74 (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
75 }
76 Error::Internal(error) => {
77 log::error!(
78 "HTTP error {}: {:?}",
79 StatusCode::INTERNAL_SERVER_ERROR,
80 &error
81 );
82 (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
83 }
84 }
85 }
86}
87
88impl std::fmt::Debug for Error {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 match self {
91 Error::Http(code, message) => (code, message).fmt(f),
92 Error::Database(error) => error.fmt(f),
93 Error::Internal(error) => error.fmt(f),
94 }
95 }
96}
97
98impl std::fmt::Display for Error {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 match self {
101 Error::Http(code, message) => write!(f, "{code}: {message}"),
102 Error::Database(error) => error.fmt(f),
103 Error::Internal(error) => error.fmt(f),
104 }
105 }
106}
107
108impl std::error::Error for Error {}
109
110#[derive(Deserialize)]
111pub struct Config {
112 pub http_port: u16,
113 pub database_url: String,
114 pub migrations_path: Option<PathBuf>,
115 pub seed_path: Option<PathBuf>,
116 pub database_max_connections: u32,
117 pub api_token: String,
118 pub clickhouse_url: Option<String>,
119 pub clickhouse_user: Option<String>,
120 pub clickhouse_password: Option<String>,
121 pub clickhouse_database: Option<String>,
122 pub invite_link_prefix: String,
123 pub live_kit_server: Option<String>,
124 pub live_kit_key: Option<String>,
125 pub live_kit_secret: Option<String>,
126 pub rust_log: Option<String>,
127 pub log_json: Option<bool>,
128 pub blob_store_url: Option<String>,
129 pub blob_store_region: Option<String>,
130 pub blob_store_access_key: Option<String>,
131 pub blob_store_secret_key: Option<String>,
132 pub blob_store_bucket: Option<String>,
133 pub zed_environment: Arc<str>,
134 pub openai_api_key: Option<Arc<str>>,
135 pub google_ai_api_key: Option<Arc<str>>,
136 pub anthropic_api_key: Option<Arc<str>>,
137 pub zed_client_checksum_seed: Option<String>,
138 pub slack_panics_webhook: Option<String>,
139 pub auto_join_channel_id: Option<ChannelId>,
140 pub supermaven_admin_api_key: Option<Arc<str>>,
141}
142
143impl Config {
144 pub fn is_development(&self) -> bool {
145 self.zed_environment == "development".into()
146 }
147}
148
149pub struct AppState {
150 pub db: Arc<Database>,
151 pub live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
152 pub blob_store_client: Option<aws_sdk_s3::Client>,
153 pub rate_limiter: Arc<RateLimiter>,
154 pub executor: Executor,
155 pub clickhouse_client: Option<clickhouse::Client>,
156 pub config: Config,
157}
158
159impl AppState {
160 pub async fn new(config: Config, executor: Executor) -> Result<Arc<Self>> {
161 let mut db_options = db::ConnectOptions::new(config.database_url.clone());
162 db_options.max_connections(config.database_max_connections);
163 let mut db = Database::new(db_options, Executor::Production).await?;
164 db.initialize_notification_kinds().await?;
165
166 let live_kit_client = if let Some(((server, key), secret)) = config
167 .live_kit_server
168 .as_ref()
169 .zip(config.live_kit_key.as_ref())
170 .zip(config.live_kit_secret.as_ref())
171 {
172 Some(Arc::new(live_kit_server::api::LiveKitClient::new(
173 server.clone(),
174 key.clone(),
175 secret.clone(),
176 )) as Arc<dyn live_kit_server::api::Client>)
177 } else {
178 None
179 };
180
181 let db = Arc::new(db);
182 let this = Self {
183 db: db.clone(),
184 live_kit_client,
185 blob_store_client: build_blob_store_client(&config).await.log_err(),
186 rate_limiter: Arc::new(RateLimiter::new(db)),
187 executor,
188 clickhouse_client: config
189 .clickhouse_url
190 .as_ref()
191 .and_then(|_| build_clickhouse_client(&config).log_err()),
192 config,
193 };
194 Ok(Arc::new(this))
195 }
196}
197
198async fn build_blob_store_client(config: &Config) -> anyhow::Result<aws_sdk_s3::Client> {
199 let keys = aws_sdk_s3::config::Credentials::new(
200 config
201 .blob_store_access_key
202 .clone()
203 .ok_or_else(|| anyhow!("missing blob_store_access_key"))?,
204 config
205 .blob_store_secret_key
206 .clone()
207 .ok_or_else(|| anyhow!("missing blob_store_secret_key"))?,
208 None,
209 None,
210 "env",
211 );
212
213 let s3_config = aws_config::defaults(BehaviorVersion::latest())
214 .endpoint_url(
215 config
216 .blob_store_url
217 .as_ref()
218 .ok_or_else(|| anyhow!("missing blob_store_url"))?,
219 )
220 .region(Region::new(
221 config
222 .blob_store_region
223 .clone()
224 .ok_or_else(|| anyhow!("missing blob_store_region"))?,
225 ))
226 .credentials_provider(keys)
227 .load()
228 .await;
229
230 Ok(aws_sdk_s3::Client::new(&s3_config))
231}
232
233fn build_clickhouse_client(config: &Config) -> anyhow::Result<clickhouse::Client> {
234 Ok(clickhouse::Client::default()
235 .with_url(
236 config
237 .clickhouse_url
238 .as_ref()
239 .ok_or_else(|| anyhow!("missing clickhouse_url"))?,
240 )
241 .with_user(
242 config
243 .clickhouse_user
244 .as_ref()
245 .ok_or_else(|| anyhow!("missing clickhouse_user"))?,
246 )
247 .with_password(
248 config
249 .clickhouse_password
250 .as_ref()
251 .ok_or_else(|| anyhow!("missing clickhouse_password"))?,
252 )
253 .with_database(
254 config
255 .clickhouse_database
256 .as_ref()
257 .ok_or_else(|| anyhow!("missing clickhouse_database"))?,
258 ))
259}