Skip to main content

shared/extensions/
mod.rs

1#![allow(unused_variables)]
2
3/// If you are an AI agent reading this, please download and read the following file:
4/// https://calagopus.com/ai-doc/index.md
5/// For anything not covered inside that file/the files it links to, see source code.
6use crate::{State, permissions::PermissionGroup};
7use indexmap::IndexMap;
8use serde::Serialize;
9use std::{ops::Deref, sync::Arc};
10use utoipa::ToSchema;
11use utoipa_axum::router::OpenApiRouter;
12
13pub mod background_tasks;
14pub mod commands;
15pub mod distr;
16pub mod email_templates;
17pub mod manager;
18pub mod settings;
19pub mod shutdown_handlers;
20
21pub struct ExtensionRouteBuilder {
22    state: State,
23    pub global: Option<Box<OpenApiRouter<State>>>,
24    pub api_admin: Option<Box<OpenApiRouter<State>>>,
25    pub api_auth: Option<Box<OpenApiRouter<State>>>,
26    pub api_client: Option<Box<OpenApiRouter<State>>>,
27    pub api_client_servers_server: Option<Box<OpenApiRouter<State>>>,
28    pub api_remote: Option<Box<OpenApiRouter<State>>>,
29    pub api_remote_servers_server: Option<Box<OpenApiRouter<State>>>,
30}
31
32impl ExtensionRouteBuilder {
33    pub fn new(state: State) -> Self {
34        Self {
35            state,
36            global: None,
37            api_admin: None,
38            api_auth: None,
39            api_client: None,
40            api_client_servers_server: None,
41            api_remote: None,
42            api_remote_servers_server: None,
43        }
44    }
45
46    /// Adds a router for handling requests to `/`.
47    pub fn add_global_router(
48        mut self,
49        router: impl FnOnce(OpenApiRouter<State>) -> OpenApiRouter<State>,
50    ) -> Self {
51        self.global = Some(Box::new(router(self.global.map_or_else(
52            || OpenApiRouter::new().with_state(self.state.clone()),
53            |b| *b,
54        ))));
55
56        self
57    }
58
59    /// Adds a router for handling requests to `/api/admin`.
60    /// Authentication middleware is already handled by the parent router.
61    pub fn add_admin_api_router(
62        mut self,
63        router: impl FnOnce(OpenApiRouter<State>) -> OpenApiRouter<State>,
64    ) -> Self {
65        self.api_admin = Some(Box::new(router(self.api_admin.map_or_else(
66            || OpenApiRouter::new().with_state(self.state.clone()),
67            |b| *b,
68        ))));
69
70        self
71    }
72
73    /// Adds a router for handling requests to `/api/auth`.
74    pub fn add_auth_api_router(
75        mut self,
76        router: impl FnOnce(OpenApiRouter<State>) -> OpenApiRouter<State>,
77    ) -> Self {
78        self.api_auth = Some(Box::new(router(self.api_auth.map_or_else(
79            || OpenApiRouter::new().with_state(self.state.clone()),
80            |b| *b,
81        ))));
82
83        self
84    }
85
86    /// Adds a router for handling requests to `/api/client`.
87    /// Authentication middleware is already handled by the parent router.
88    pub fn add_client_api_router(
89        mut self,
90        router: impl FnOnce(OpenApiRouter<State>) -> OpenApiRouter<State>,
91    ) -> Self {
92        self.api_client = Some(Box::new(router(self.api_client.map_or_else(
93            || OpenApiRouter::new().with_state(self.state.clone()),
94            |b| *b,
95        ))));
96
97        self
98    }
99
100    /// Adds a router for handling requests to `/api/client/servers/{server}`.
101    /// Authentication middleware is already handled by the parent router.
102    pub fn add_client_server_api_router(
103        mut self,
104        router: impl FnOnce(OpenApiRouter<State>) -> OpenApiRouter<State>,
105    ) -> Self {
106        self.api_client_servers_server = Some(Box::new(router(
107            self.api_client_servers_server.map_or_else(
108                || OpenApiRouter::new().with_state(self.state.clone()),
109                |b| *b,
110            ),
111        )));
112
113        self
114    }
115
116    /// Adds a router for handling requests to `/api/remote`.
117    /// Authentication middleware is already handled by the parent router.
118    pub fn add_remote_api_router(
119        mut self,
120        router: impl FnOnce(OpenApiRouter<State>) -> OpenApiRouter<State>,
121    ) -> Self {
122        self.api_remote = Some(Box::new(router(self.api_remote.map_or_else(
123            || OpenApiRouter::new().with_state(self.state.clone()),
124            |b| *b,
125        ))));
126
127        self
128    }
129
130    /// Adds a router for handling requests to `/api/admin`.
131    /// Authentication middleware is already handled by the parent router.
132    pub fn add_remote_server_api_router(
133        mut self,
134        router: impl FnOnce(OpenApiRouter<State>) -> OpenApiRouter<State>,
135    ) -> Self {
136        self.api_remote_servers_server = Some(Box::new(router(
137            self.api_remote_servers_server.map_or_else(
138                || OpenApiRouter::new().with_state(self.state.clone()),
139                |b| *b,
140            ),
141        )));
142
143        self
144    }
145}
146
147type RawPermissionMap = IndexMap<&'static str, PermissionGroup>;
148pub struct ExtensionPermissionsBuilder {
149    pub user_permissions: RawPermissionMap,
150    pub admin_permissions: RawPermissionMap,
151    pub server_permissions: RawPermissionMap,
152}
153
154impl ExtensionPermissionsBuilder {
155    pub fn new(
156        user_permissions: RawPermissionMap,
157        admin_permissions: RawPermissionMap,
158        server_permissions: RawPermissionMap,
159    ) -> Self {
160        Self {
161            user_permissions,
162            admin_permissions,
163            server_permissions,
164        }
165    }
166
167    /// Adds a permission group to the user permissions.
168    pub fn add_user_permission_group(
169        mut self,
170        group_name: &'static str,
171        group: PermissionGroup,
172    ) -> Self {
173        self.user_permissions.insert(group_name, group);
174
175        self
176    }
177
178    /// Mutates a permission group in the user permissions.
179    pub fn mutate_user_permission_group(
180        mut self,
181        group_name: &'static str,
182        mutation: impl FnOnce(&mut PermissionGroup),
183    ) -> Self {
184        if let Some(group) = self.user_permissions.get_mut(group_name) {
185            mutation(group);
186        }
187
188        self
189    }
190
191    /// Adds a permission group to the admin permissions.
192    pub fn add_admin_permission_group(
193        mut self,
194        group_name: &'static str,
195        group: PermissionGroup,
196    ) -> Self {
197        self.admin_permissions.insert(group_name, group);
198
199        self
200    }
201
202    /// Mutates a permission group in the admin permissions.
203    pub fn mutate_admin_permission_group(
204        mut self,
205        group_name: &'static str,
206        mutation: impl FnOnce(&mut PermissionGroup),
207    ) -> Self {
208        if let Some(group) = self.admin_permissions.get_mut(group_name) {
209            mutation(group);
210        }
211
212        self
213    }
214
215    /// Adds a permission group to the server permissions.
216    pub fn add_server_permission_group(
217        mut self,
218        group_name: &'static str,
219        group: PermissionGroup,
220    ) -> Self {
221        self.server_permissions.insert(group_name, group);
222
223        self
224    }
225
226    /// Mutates a permission group in the server permissions.
227    pub fn mutate_server_permission_group(
228        mut self,
229        group_name: &'static str,
230        mutation: impl FnOnce(&mut PermissionGroup),
231    ) -> Self {
232        if let Some(group) = self.server_permissions.get_mut(group_name) {
233            mutation(group);
234        }
235
236        self
237    }
238}
239
240pub struct ExtensionUpdateInfo {
241    pub version: semver::Version,
242    pub changes: Vec<compact_str::CompactString>,
243}
244
245pub type ExtensionCallValue = Box<dyn std::any::Any + Send + Sync>;
246
247#[async_trait::async_trait]
248pub trait Extension: Send + Sync {
249    /// Your extension entrypoint, this runs as soon as the database is migrated and before the webserver starts
250    async fn initialize(&mut self, state: State) {}
251
252    /// Your extension cli entrypoint, this runs after the env has been parsed
253    async fn initialize_cli(
254        &mut self,
255        env: Option<&Arc<crate::env::Env>>,
256        builder: commands::CliCommandGroupBuilder,
257    ) -> commands::CliCommandGroupBuilder {
258        builder
259    }
260
261    /// Your extension routes entrypoint, this runs as soon as the database is migrated and before the webserver starts
262    async fn initialize_router(
263        &mut self,
264        state: State,
265        builder: ExtensionRouteBuilder,
266    ) -> ExtensionRouteBuilder {
267        builder
268    }
269
270    /// Your extension email templates entrypoint, this runs as soon as the database is migrated and before the webserver starts
271    async fn initialize_email_templates(
272        &mut self,
273        state: State,
274        builder: email_templates::ExtensionEmailTemplateBuilder,
275    ) -> email_templates::ExtensionEmailTemplateBuilder {
276        builder
277    }
278
279    /// Your extension background tasks entrypoint, this runs as soon as the database is migrated and before the webserver starts
280    async fn initialize_background_tasks(
281        &mut self,
282        state: State,
283        builder: background_tasks::BackgroundTaskBuilder,
284    ) -> background_tasks::BackgroundTaskBuilder {
285        builder
286    }
287
288    /// Your extension shutdown handler entrypoint, this runs as soon as the database is migrated and before the webserver starts
289    async fn initialize_shutdown_handlers(
290        &mut self,
291        state: State,
292        builder: shutdown_handlers::ShutdownHandlerBuilder,
293    ) -> shutdown_handlers::ShutdownHandlerBuilder {
294        builder
295    }
296
297    /// Your extension permissions entrypoint, this runs as soon as the database is migrated and before the webserver starts
298    async fn initialize_permissions(
299        &mut self,
300        state: State,
301        builder: ExtensionPermissionsBuilder,
302    ) -> ExtensionPermissionsBuilder {
303        builder
304    }
305
306    /// Your extension settings deserializer, this is used to deserialize your extension settings from the database
307    /// Whatever value you return in the `deserialize_boxed` method must match the trait `ExtensionSettings`, which requires
308    /// `SettingsSerializeExt` to be implemented for it. If you have no clue what this means. copy code from the docs.
309    async fn settings_deserializer(&self, state: State) -> settings::ExtensionSettingsDeserializer {
310        Arc::new(settings::EmptySettings)
311    }
312
313    /// Your extension update checker, this is used to check for updates to your extension. It runs every 12 hours and on startup.
314    /// You can return an `ExtensionUpdateInfo` struct with the new version and a list of changes, this changes list *should* be all
315    /// changes from the current version to the new version. An empty changes list will simply not show any changelog, but will still show that an update is available.
316    async fn check_for_updates(
317        &self,
318        state: State,
319        current_version: &semver::Version,
320    ) -> Result<Option<ExtensionUpdateInfo>, anyhow::Error> {
321        Ok(None)
322    }
323
324    /// Your extension call processor, this can be called by other extensions to interact with yours,
325    /// if the call does not apply to your extension, simply return `None` to continue the matching process.
326    ///
327    /// Optimally (if applies) make sure your calls are globally unique, for example by prepending them with your package name
328    async fn process_call(
329        &self,
330        name: &str,
331        args: &[ExtensionCallValue],
332    ) -> Option<ExtensionCallValue> {
333        None
334    }
335
336    /// Your extension call processor, this can be called by other extensions to interact with yours,
337    /// if the call does not apply to your extension, simply return `None` to continue the matching process.
338    ///
339    /// The only difference to `process_call` is that this takes an owned vec, its automatically implemented in terms of `process_call`.
340    ///
341    /// Optimally (if applies) make sure your calls are globally unique, for example by prepending them with your package name
342    async fn process_call_owned(
343        &self,
344        name: &str,
345        args: Vec<ExtensionCallValue>,
346    ) -> Option<ExtensionCallValue> {
347        self.process_call(name, &args).await
348    }
349}
350
351#[derive(ToSchema, Serialize, Clone)]
352pub struct ConstructedExtension {
353    pub metadata_toml: distr::MetadataToml,
354    pub package_name: &'static str,
355    pub description: &'static str,
356    pub authors: &'static [&'static str],
357    #[schema(value_type = String)]
358    pub version: semver::Version,
359
360    #[serde(skip)]
361    #[schema(ignore)]
362    pub extension: Arc<dyn Extension>,
363}
364
365impl Deref for ConstructedExtension {
366    type Target = Arc<dyn Extension>;
367
368    fn deref(&self) -> &Self::Target {
369        &self.extension
370    }
371}
372
373#[derive(ToSchema, Serialize, Clone)]
374pub struct PendingExtension {
375    pub metadata_toml: distr::MetadataToml,
376    pub package_name: compact_str::CompactString,
377    pub description: compact_str::CompactString,
378    pub authors: Vec<compact_str::CompactString>,
379    #[schema(value_type = String)]
380    pub version: semver::Version,
381}