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