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
154pub struct PermissionsSnapshot {
155    user: std::collections::HashSet<String>,
156    admin: std::collections::HashSet<String>,
157    server: std::collections::HashSet<String>,
158}
159
160impl ExtensionPermissionsBuilder {
161    pub(crate) fn snapshot(&self) -> PermissionsSnapshot {
162        PermissionsSnapshot {
163            user: crate::permissions::flatten_permissions(&self.user_permissions),
164            admin: crate::permissions::flatten_permissions(&self.admin_permissions),
165            server: crate::permissions::flatten_permissions(&self.server_permissions),
166        }
167    }
168
169    pub(crate) fn contributions_since(
170        &self,
171        snapshot: &PermissionsSnapshot,
172    ) -> crate::settings::ExtensionPermissions {
173        fn added(
174            current: std::collections::HashSet<String>,
175            previous: &std::collections::HashSet<String>,
176        ) -> Vec<compact_str::CompactString> {
177            let mut added: Vec<compact_str::CompactString> = current
178                .difference(previous)
179                .map(|permission| permission.into())
180                .collect();
181            added.sort_unstable();
182
183            added
184        }
185
186        crate::settings::ExtensionPermissions {
187            user: added(
188                crate::permissions::flatten_permissions(&self.user_permissions),
189                &snapshot.user,
190            ),
191            admin: added(
192                crate::permissions::flatten_permissions(&self.admin_permissions),
193                &snapshot.admin,
194            ),
195            server: added(
196                crate::permissions::flatten_permissions(&self.server_permissions),
197                &snapshot.server,
198            ),
199        }
200    }
201
202    pub fn new(
203        user_permissions: RawPermissionMap,
204        admin_permissions: RawPermissionMap,
205        server_permissions: RawPermissionMap,
206    ) -> Self {
207        Self {
208            user_permissions,
209            admin_permissions,
210            server_permissions,
211        }
212    }
213
214    /// Adds a permission group to the user permissions.
215    pub fn add_user_permission_group(
216        mut self,
217        group_name: &'static str,
218        group: PermissionGroup,
219    ) -> Self {
220        self.user_permissions.insert(group_name, group);
221
222        self
223    }
224
225    /// Mutates a permission group in the user permissions.
226    pub fn mutate_user_permission_group(
227        mut self,
228        group_name: &'static str,
229        mutation: impl FnOnce(&mut PermissionGroup),
230    ) -> Self {
231        if let Some(group) = self.user_permissions.get_mut(group_name) {
232            mutation(group);
233        }
234
235        self
236    }
237
238    /// Adds a permission group to the admin permissions.
239    pub fn add_admin_permission_group(
240        mut self,
241        group_name: &'static str,
242        group: PermissionGroup,
243    ) -> Self {
244        self.admin_permissions.insert(group_name, group);
245
246        self
247    }
248
249    /// Mutates a permission group in the admin permissions.
250    pub fn mutate_admin_permission_group(
251        mut self,
252        group_name: &'static str,
253        mutation: impl FnOnce(&mut PermissionGroup),
254    ) -> Self {
255        if let Some(group) = self.admin_permissions.get_mut(group_name) {
256            mutation(group);
257        }
258
259        self
260    }
261
262    /// Adds a permission group to the server permissions.
263    pub fn add_server_permission_group(
264        mut self,
265        group_name: &'static str,
266        group: PermissionGroup,
267    ) -> Self {
268        self.server_permissions.insert(group_name, group);
269
270        self
271    }
272
273    /// Mutates a permission group in the server permissions.
274    pub fn mutate_server_permission_group(
275        mut self,
276        group_name: &'static str,
277        mutation: impl FnOnce(&mut PermissionGroup),
278    ) -> Self {
279        if let Some(group) = self.server_permissions.get_mut(group_name) {
280            mutation(group);
281        }
282
283        self
284    }
285}
286
287pub struct ExtensionUpdateInfo {
288    pub version: semver::Version,
289    pub changes: Vec<compact_str::CompactString>,
290}
291
292pub type ExtensionCallValue = Box<dyn std::any::Any + Send + Sync>;
293
294#[async_trait::async_trait]
295pub trait Extension: Send + Sync {
296    /// Your extension entrypoint, this runs as soon as the database is migrated and before the webserver starts
297    async fn initialize(&mut self, state: State) {}
298
299    /// Your extension cli entrypoint, this runs after the env has been parsed
300    async fn initialize_cli(
301        &mut self,
302        env: Option<&Arc<crate::env::Env>>,
303        builder: commands::CliCommandGroupBuilder,
304    ) -> commands::CliCommandGroupBuilder {
305        builder
306    }
307
308    /// Your extension routes entrypoint, this runs as soon as the database is migrated and before the webserver starts
309    async fn initialize_router(
310        &mut self,
311        state: State,
312        builder: ExtensionRouteBuilder,
313    ) -> ExtensionRouteBuilder {
314        builder
315    }
316
317    /// Your extension email templates entrypoint, this runs as soon as the database is migrated and before the webserver starts
318    async fn initialize_email_templates(
319        &mut self,
320        state: State,
321        builder: email_templates::ExtensionEmailTemplateBuilder,
322    ) -> email_templates::ExtensionEmailTemplateBuilder {
323        builder
324    }
325
326    /// Your extension background tasks entrypoint, this runs as soon as the database is migrated and before the webserver starts
327    async fn initialize_background_tasks(
328        &mut self,
329        state: State,
330        builder: background_tasks::BackgroundTaskBuilder,
331    ) -> background_tasks::BackgroundTaskBuilder {
332        builder
333    }
334
335    /// Your extension shutdown handler entrypoint, this runs as soon as the database is migrated and before the webserver starts
336    async fn initialize_shutdown_handlers(
337        &mut self,
338        state: State,
339        builder: shutdown_handlers::ShutdownHandlerBuilder,
340    ) -> shutdown_handlers::ShutdownHandlerBuilder {
341        builder
342    }
343
344    /// Your extension permissions entrypoint, this runs as soon as the database is migrated and before the webserver starts
345    async fn initialize_permissions(
346        &mut self,
347        state: State,
348        builder: ExtensionPermissionsBuilder,
349    ) -> ExtensionPermissionsBuilder {
350        builder
351    }
352
353    /// Your extension settings deserializer, this is used to deserialize your extension settings from the database
354    /// Whatever value you return in the `deserialize_boxed` method must match the trait `ExtensionSettings`, which requires
355    /// `SettingsSerializeExt` to be implemented for it. If you have no clue what this means. copy code from the docs.
356    async fn settings_deserializer(&self, state: State) -> settings::ExtensionSettingsDeserializer {
357        Arc::new(settings::EmptySettings)
358    }
359
360    /// Your extension update checker, this is used to check for updates to your extension. It runs every 12 hours and on startup.
361    /// You can return an `ExtensionUpdateInfo` struct with the new version and a list of changes, this changes list *should* be all
362    /// 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.
363    async fn check_for_updates(
364        &self,
365        state: State,
366        current_version: &semver::Version,
367    ) -> Result<Option<ExtensionUpdateInfo>, anyhow::Error> {
368        Ok(None)
369    }
370
371    /// Your extension call processor, this can be called by other extensions to interact with yours,
372    /// if the call does not apply to your extension, simply return `None` to continue the matching process.
373    ///
374    /// Optimally (if applies) make sure your calls are globally unique, for example by prepending them with your package name
375    async fn process_call(
376        &self,
377        name: &str,
378        args: &[ExtensionCallValue],
379    ) -> Option<ExtensionCallValue> {
380        None
381    }
382
383    /// Your extension call processor, this can be called by other extensions to interact with yours,
384    /// if the call does not apply to your extension, simply return `None` to continue the matching process.
385    ///
386    /// The only difference to `process_call` is that this takes an owned vec, its automatically implemented in terms of `process_call`.
387    ///
388    /// Optimally (if applies) make sure your calls are globally unique, for example by prepending them with your package name
389    async fn process_call_owned(
390        &self,
391        name: &str,
392        args: Vec<ExtensionCallValue>,
393    ) -> Option<ExtensionCallValue> {
394        self.process_call(name, &args).await
395    }
396}
397
398#[derive(ToSchema, Serialize, Clone)]
399pub struct ConstructedExtension {
400    pub metadata_toml: distr::MetadataToml,
401    pub package_name: &'static str,
402    pub description: &'static str,
403    pub authors: &'static [&'static str],
404    #[schema(value_type = String)]
405    pub version: semver::Version,
406
407    #[serde(skip)]
408    #[schema(ignore)]
409    pub extension: Arc<dyn Extension>,
410}
411
412impl Deref for ConstructedExtension {
413    type Target = Arc<dyn Extension>;
414
415    fn deref(&self) -> &Self::Target {
416        &self.extension
417    }
418}
419
420#[derive(ToSchema, Serialize, Clone)]
421pub struct PendingExtension {
422    pub metadata_toml: distr::MetadataToml,
423    pub package_name: compact_str::CompactString,
424    pub description: compact_str::CompactString,
425    pub authors: Vec<compact_str::CompactString>,
426    #[schema(value_type = String)]
427    pub version: semver::Version,
428}