Skip to main content

shared/models/
announcement.rs

1use crate::{
2    models::{InsertQueryBuilder, UpdateQueryBuilder},
3    prelude::*,
4};
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7use sqlx::{Row, postgres::PgRow, prelude::Type};
8use std::{
9    collections::BTreeMap,
10    hash::Hash,
11    sync::{Arc, LazyLock},
12};
13use utoipa::ToSchema;
14
15#[derive(ToSchema, Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone, Copy)]
16#[serde(rename_all = "snake_case")]
17#[sqlx(type_name = "announcement_type", rename_all = "SCREAMING_SNAKE_CASE")]
18pub enum AnnouncementType {
19    Info,
20    Success,
21    Warning,
22    Error,
23}
24
25pub fn validate_title_translations(
26    title_translations: &BTreeMap<compact_str::CompactString, compact_str::CompactString>,
27    _context: &(),
28) -> Result<(), garde::Error> {
29    if title_translations.len() > 512 {
30        return Err(garde::Error::new("cannot have more than 512 entries"));
31    }
32
33    for (lang, translation) in title_translations {
34        if lang.len() < 2 || lang.len() > 15 {
35            return Err(garde::Error::new(format!(
36                "language code '{}' must be between 2 and 15 characters",
37                lang
38            )));
39        }
40        if translation.is_empty() || translation.len() > 255 {
41            return Err(garde::Error::new(format!(
42                "translation for language '{}' must be between 1 and 255 characters",
43                lang
44            )));
45        }
46    }
47
48    Ok(())
49}
50
51pub fn validate_content_translations(
52    content_translations: &BTreeMap<compact_str::CompactString, compact_str::CompactString>,
53    _context: &(),
54) -> Result<(), garde::Error> {
55    if content_translations.len() > 512 {
56        return Err(garde::Error::new("cannot have more than 512 entries"));
57    }
58
59    for (lang, translation) in content_translations {
60        if lang.len() < 2 || lang.len() > 15 {
61            return Err(garde::Error::new(format!(
62                "language code '{}' must be between 2 and 15 characters",
63                lang
64            )));
65        }
66        if translation.is_empty() || translation.len() > 2048 {
67            return Err(garde::Error::new(format!(
68                "translation for language '{}' must be between 1 and 2048 characters",
69                lang
70            )));
71        }
72    }
73
74    Ok(())
75}
76
77#[derive(Serialize, Deserialize, Clone)]
78pub struct Announcement {
79    pub uuid: uuid::Uuid,
80
81    pub r#type: AnnouncementType,
82    pub enabled: bool,
83    pub enabled_start: Option<chrono::NaiveDateTime>,
84    pub enabled_end: Option<chrono::NaiveDateTime>,
85    pub dismissible: bool,
86    pub dismissible_end: Option<chrono::NaiveDateTime>,
87
88    pub title: compact_str::CompactString,
89    pub title_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
90    pub content: compact_str::CompactString,
91    pub content_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
92
93    pub locations: Vec<uuid::Uuid>,
94    pub nodes: Vec<uuid::Uuid>,
95    pub backup_configurations: Vec<uuid::Uuid>,
96    pub eggs: Vec<uuid::Uuid>,
97
98    pub created: chrono::NaiveDateTime,
99
100    extension_data: super::ModelExtensionData,
101}
102
103impl BaseModel for Announcement {
104    const NAME: &'static str = "announcement";
105
106    fn get_extension_list() -> &'static super::ModelExtensionList {
107        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
108            LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
109
110        &EXTENSIONS
111    }
112
113    fn get_extension_data(&self) -> &super::ModelExtensionData {
114        &self.extension_data
115    }
116
117    #[inline]
118    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
119        let prefix = prefix.unwrap_or_default();
120
121        BTreeMap::from([
122            (
123                "announcements.uuid",
124                compact_str::format_compact!("{prefix}uuid"),
125            ),
126            (
127                "announcements.type",
128                compact_str::format_compact!("{prefix}type"),
129            ),
130            (
131                "announcements.enabled",
132                compact_str::format_compact!("{prefix}enabled"),
133            ),
134            (
135                "announcements.enabled_start",
136                compact_str::format_compact!("{prefix}enabled_start"),
137            ),
138            (
139                "announcements.enabled_end",
140                compact_str::format_compact!("{prefix}enabled_end"),
141            ),
142            (
143                "announcements.dismissible",
144                compact_str::format_compact!("{prefix}dismissible"),
145            ),
146            (
147                "announcements.dismissible_end",
148                compact_str::format_compact!("{prefix}dismissible_end"),
149            ),
150            (
151                "announcements.title",
152                compact_str::format_compact!("{prefix}title"),
153            ),
154            (
155                "announcements.title_translations",
156                compact_str::format_compact!("{prefix}title_translations"),
157            ),
158            (
159                "announcements.content",
160                compact_str::format_compact!("{prefix}content"),
161            ),
162            (
163                "announcements.content_translations",
164                compact_str::format_compact!("{prefix}content_translations"),
165            ),
166            (
167                "announcements.locations",
168                compact_str::format_compact!("{prefix}locations"),
169            ),
170            (
171                "announcements.nodes",
172                compact_str::format_compact!("{prefix}nodes"),
173            ),
174            (
175                "announcements.backup_configurations",
176                compact_str::format_compact!("{prefix}backup_configurations"),
177            ),
178            (
179                "announcements.eggs",
180                compact_str::format_compact!("{prefix}eggs"),
181            ),
182            (
183                "announcements.created",
184                compact_str::format_compact!("{prefix}created"),
185            ),
186        ])
187    }
188
189    #[inline]
190    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
191        let prefix = prefix.unwrap_or_default();
192
193        Ok(Self {
194            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
195            r#type: row.try_get(compact_str::format_compact!("{prefix}type").as_str())?,
196            enabled: row.try_get(compact_str::format_compact!("{prefix}enabled").as_str())?,
197            enabled_start: row
198                .try_get(compact_str::format_compact!("{prefix}enabled_start").as_str())?,
199            enabled_end: row
200                .try_get(compact_str::format_compact!("{prefix}enabled_end").as_str())?,
201            dismissible: row
202                .try_get(compact_str::format_compact!("{prefix}dismissible").as_str())?,
203            dismissible_end: row
204                .try_get(compact_str::format_compact!("{prefix}dismissible_end").as_str())?,
205            title: row.try_get(compact_str::format_compact!("{prefix}title").as_str())?,
206            title_translations: serde_json::from_value(
207                row.try_get(compact_str::format_compact!("{prefix}title_translations").as_str())?,
208            )?,
209            content: row.try_get(compact_str::format_compact!("{prefix}content").as_str())?,
210            content_translations: serde_json::from_value(
211                row.try_get(compact_str::format_compact!("{prefix}content_translations").as_str())?,
212            )?,
213            locations: row.try_get(compact_str::format_compact!("{prefix}locations").as_str())?,
214            nodes: row.try_get(compact_str::format_compact!("{prefix}nodes").as_str())?,
215            backup_configurations: row
216                .try_get(compact_str::format_compact!("{prefix}backup_configurations").as_str())?,
217            eggs: row.try_get(compact_str::format_compact!("{prefix}eggs").as_str())?,
218            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
219            extension_data: Self::map_extensions(prefix, row)?,
220        })
221    }
222
223    fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
224        vec![
225            compact_str::format_compact!("{}::{}", Self::NAME, self.uuid),
226            compact_str::CompactString::const_new("announcements::active"),
227        ]
228    }
229}
230
231impl Announcement {
232    pub async fn all_with_pagination(
233        database: &crate::database::Database,
234        page: i64,
235        per_page: i64,
236        search: Option<&str>,
237    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
238        super::PaginatedQueryBuilder::new(
239            Self::columns_sql(None),
240            format!(
241                r#"
242                FROM announcements
243                WHERE {search}
244                "#,
245                search = super::search_sql(1, &["announcements.title"], &["announcements.uuid"]),
246            ),
247            "announcements.created, announcements.uuid",
248        )
249        .bind(search)
250        .fetch(database, page, per_page)
251        .await
252    }
253
254    pub async fn all_by_active(
255        database: &crate::database::Database,
256    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
257        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
258            r#"
259            SELECT {}
260            FROM announcements
261            WHERE announcements.enabled = true
262                AND (announcements.enabled_start IS NULL OR announcements.enabled_start <= NOW())
263                AND (announcements.enabled_end IS NULL OR announcements.enabled_end >= NOW())
264                AND array_length(announcements.locations, 1) IS NULL
265                AND array_length(announcements.nodes, 1) IS NULL
266                AND array_length(announcements.backup_configurations, 1) IS NULL
267                AND array_length(announcements.eggs, 1) IS NULL
268            ORDER BY announcements.created, announcements.uuid
269            "#,
270            Self::columns_sql(None)
271        )))
272        .fetch_all(database.read())
273        .await?;
274
275        rows.into_iter()
276            .map(|row| Self::map(None, &row))
277            .try_collect_vec()
278    }
279
280    pub async fn all_by_active_server(
281        database: &crate::database::Database,
282        server: &super::server::Server,
283    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
284        let node = server.node.fetch_cached(database).await?;
285
286        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
287            r#"
288            SELECT {}
289            FROM announcements
290            WHERE announcements.enabled = true
291                AND (announcements.enabled_start IS NULL OR announcements.enabled_start <= NOW())
292                AND (announcements.enabled_end IS NULL OR announcements.enabled_end >= NOW())
293                AND (
294                    array_length(announcements.locations, 1) IS NOT NULL
295                    OR array_length(announcements.nodes, 1) IS NOT NULL
296                    OR array_length(announcements.backup_configurations, 1) IS NOT NULL
297                    OR array_length(announcements.eggs, 1) IS NOT NULL
298                )
299                AND (
300                    (announcements.locations IS NULL OR $1 = ANY(announcements.locations))
301                    OR (announcements.nodes IS NULL OR $2 = ANY(announcements.nodes))
302                    OR ($3 IS NOT NULL AND (announcements.backup_configurations IS NULL OR $3 = ANY(announcements.backup_configurations)))
303                    OR (announcements.eggs IS NULL OR $4 = ANY(announcements.eggs))
304                )
305            ORDER BY announcements.created, announcements.uuid
306            "#,
307            Self::columns_sql(None)
308        )))
309        .bind(node.location.uuid)
310        .bind(node.uuid)
311        .bind(server.backup_configuration.as_ref().map_or_else(
312            || {
313                node.backup_configuration.as_ref().map_or_else(
314                    || node.location.backup_configuration.as_ref().map(|b| b.uuid),
315                    |b| Some(b.uuid),
316                )
317            },
318            |b| Some(b.uuid),
319        ))
320        .bind(server.egg.uuid)
321        .fetch_all(database.read())
322        .await?;
323
324        rows.into_iter()
325            .map(|row| Self::map(None, &row))
326            .try_collect_vec()
327    }
328
329    pub async fn cleanup_uuid_arrays(
330        database: &crate::database::Database,
331    ) -> Result<u64, crate::database::DatabaseError> {
332        let result = sqlx::query(
333            "UPDATE announcements
334            SET
335                locations = CASE
336                    WHEN EXISTS (
337                        SELECT 1 FROM unnest(locations) AS u
338                        WHERE NOT EXISTS (SELECT 1 FROM locations WHERE uuid = u)
339                    )
340                    THEN COALESCE(
341                        (SELECT array_agg(u) FROM unnest(locations) AS u
342                        WHERE EXISTS (SELECT 1 FROM locations WHERE uuid = u)),
343                        '{}'::uuid[]
344                    )
345                    ELSE locations
346                END,
347                nodes = CASE
348                    WHEN EXISTS (
349                        SELECT 1 FROM unnest(nodes) AS u
350                        WHERE NOT EXISTS (SELECT 1 FROM nodes WHERE uuid = u)
351                    )
352                    THEN COALESCE(
353                        (SELECT array_agg(u) FROM unnest(nodes) AS u
354                        WHERE EXISTS (SELECT 1 FROM nodes WHERE uuid = u)),
355                        '{}'::uuid[]
356                    )
357                    ELSE nodes
358                END,
359                backup_configurations = CASE
360                    WHEN EXISTS (
361                        SELECT 1 FROM unnest(backup_configurations) AS u
362                        WHERE NOT EXISTS (SELECT 1 FROM backup_configurations WHERE uuid = u)
363                    )
364                    THEN COALESCE(
365                        (SELECT array_agg(u) FROM unnest(backup_configurations) AS u
366                        WHERE EXISTS (SELECT 1 FROM backup_configurations WHERE uuid = u)),
367                        '{}'::uuid[]
368                    )
369                    ELSE backup_configurations
370                END,
371                eggs = CASE
372                    WHEN EXISTS (
373                        SELECT 1 FROM unnest(eggs) AS u
374                        WHERE NOT EXISTS (SELECT 1 FROM nest_eggs WHERE uuid = u)
375                    )
376                    THEN COALESCE(
377                        (SELECT array_agg(u) FROM unnest(eggs) AS u
378                        WHERE EXISTS (SELECT 1 FROM nest_eggs WHERE uuid = u)),
379                        '{}'::uuid[]
380                    )
381                    ELSE eggs
382                END
383            WHERE
384                EXISTS (SELECT 1 FROM unnest(locations) AS u WHERE NOT EXISTS (SELECT 1 FROM locations WHERE uuid = u))
385                OR EXISTS (SELECT 1 FROM unnest(nodes) AS u WHERE NOT EXISTS (SELECT 1 FROM nodes WHERE uuid = u))
386                OR EXISTS (SELECT 1 FROM unnest(backup_configurations) AS u WHERE NOT EXISTS (SELECT 1 FROM backup_configurations WHERE uuid = u))
387                OR EXISTS (SELECT 1 FROM unnest(eggs) AS u WHERE NOT EXISTS (SELECT 1 FROM nest_eggs WHERE uuid = u))",
388        )
389        .execute(database.write())
390        .await?;
391
392        Ok(result.rows_affected())
393    }
394}
395
396#[async_trait::async_trait]
397impl IntoAdminApiObject for Announcement {
398    type AdminApiObject = AdminApiAnnouncement;
399    type ExtraArgs<'a> = ();
400
401    async fn into_admin_api_object<'a>(
402        mut self,
403        state: &crate::State,
404        _args: Self::ExtraArgs<'a>,
405    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
406        let api_object = AdminApiAnnouncement::init_hooks(&self, state).await?;
407
408        let api_object = finish_extendible!(
409            AdminApiAnnouncement {
410                uuid: self.uuid,
411                r#type: self.r#type,
412                enabled: self.enabled,
413                enabled_start: self.enabled_start.map(|dt| dt.and_utc()),
414                enabled_end: self.enabled_end.map(|dt| dt.and_utc()),
415                dismissible: self.dismissible,
416                dismissible_end: self.dismissible_end.map(|dt| dt.and_utc()),
417                title: self.title,
418                title_translations: self.title_translations,
419                content: self.content,
420                content_translations: self.content_translations,
421                locations: self.locations,
422                nodes: self.nodes,
423                backup_configurations: self.backup_configurations,
424                eggs: self.eggs,
425                created: self.created.and_utc(),
426            },
427            api_object,
428            state
429        )?;
430
431        Ok(api_object)
432    }
433}
434
435#[async_trait::async_trait]
436impl IntoApiObject for Announcement {
437    type ApiObject = ApiAnnouncement;
438    type ExtraArgs<'a> = ();
439
440    async fn into_api_object<'a>(
441        self,
442        state: &crate::State,
443        _args: Self::ExtraArgs<'a>,
444    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
445        let api_object = ApiAnnouncement::init_hooks(&self, state).await?;
446
447        let api_object = finish_extendible!(
448            ApiAnnouncement {
449                uuid: self.uuid,
450                r#type: self.r#type,
451                dismissible: self.dismissible,
452                dismissible_end: self.dismissible_end.map(|dt| dt.and_utc()),
453                title: self.title,
454                title_translations: self.title_translations,
455                content: self.content,
456                content_translations: self.content_translations,
457            },
458            api_object,
459            state
460        )?;
461
462        Ok(api_object)
463    }
464}
465
466#[async_trait::async_trait]
467impl ByUuid for Announcement {
468    async fn by_uuid(
469        database: &crate::database::Database,
470        uuid: uuid::Uuid,
471    ) -> Result<Self, crate::database::DatabaseError> {
472        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
473            r#"
474            SELECT {}
475            FROM announcements
476            WHERE announcements.uuid = $1
477            "#,
478            Self::columns_sql(None)
479        )))
480        .bind(uuid)
481        .fetch_one(database.read())
482        .await?;
483
484        Self::map(None, &row)
485    }
486
487    async fn by_uuid_with_transaction(
488        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
489        uuid: uuid::Uuid,
490    ) -> Result<Self, crate::database::DatabaseError> {
491        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
492            r#"
493            SELECT {}
494            FROM announcements
495            WHERE announcements.uuid = $1
496            "#,
497            Self::columns_sql(None)
498        )))
499        .bind(uuid)
500        .fetch_one(&mut **transaction)
501        .await?;
502
503        Self::map(None, &row)
504    }
505}
506
507#[derive(ToSchema, Deserialize, Validate)]
508pub struct CreateAnnouncementOptions {
509    #[garde(skip)]
510    pub r#type: AnnouncementType,
511
512    #[garde(skip)]
513    pub enabled: bool,
514    #[garde(skip)]
515    pub enabled_start: Option<chrono::DateTime<chrono::Utc>>,
516    #[garde(skip)]
517    pub enabled_end: Option<chrono::DateTime<chrono::Utc>>,
518    #[garde(skip)]
519    pub dismissible: bool,
520    #[garde(skip)]
521    pub dismissible_end: Option<chrono::DateTime<chrono::Utc>>,
522
523    #[garde(length(chars, min = 1, max = 255))]
524    #[schema(min_length = 1, max_length = 255)]
525    pub title: compact_str::CompactString,
526    #[garde(custom(validate_title_translations))]
527    pub title_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
528    #[garde(length(chars, min = 1, max = 2048))]
529    #[schema(min_length = 1, max_length = 2048)]
530    pub content: compact_str::CompactString,
531    #[garde(custom(validate_content_translations))]
532    pub content_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
533
534    #[garde(length(max = 100))]
535    #[schema(max_length = 100)]
536    pub locations: Vec<uuid::Uuid>,
537    #[garde(length(max = 100))]
538    #[schema(max_length = 100)]
539    pub nodes: Vec<uuid::Uuid>,
540    #[garde(length(max = 100))]
541    #[schema(max_length = 100)]
542    pub backup_configurations: Vec<uuid::Uuid>,
543    #[garde(length(max = 100))]
544    #[schema(max_length = 100)]
545    pub eggs: Vec<uuid::Uuid>,
546}
547
548#[async_trait::async_trait]
549impl CreatableModel for Announcement {
550    type CreateOptions<'a> = CreateAnnouncementOptions;
551    type CreateResult = Self;
552
553    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
554        static CREATE_LISTENERS: LazyLock<CreateListenerList<Announcement>> =
555            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
556
557        &CREATE_LISTENERS
558    }
559
560    async fn create_with_transaction(
561        state: &crate::State,
562        mut options: Self::CreateOptions<'_>,
563        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
564    ) -> Result<Self, crate::database::DatabaseError> {
565        options.validate()?;
566
567        let mut query_builder = InsertQueryBuilder::new("announcements");
568
569        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
570
571        query_builder
572            .set("type", options.r#type)
573            .set("enabled", options.enabled)
574            .set("enabled_start", options.enabled_start)
575            .set("enabled_end", options.enabled_end)
576            .set("dismissible", options.dismissible)
577            .set("dismissible_end", options.dismissible_end)
578            .set("title", &options.title)
579            .set(
580                "title_translations",
581                serde_json::to_value(&options.title_translations)?,
582            )
583            .set("content", &options.content)
584            .set(
585                "content_translations",
586                serde_json::to_value(&options.content_translations)?,
587            )
588            .set("locations", &options.locations)
589            .set("nodes", &options.nodes)
590            .set("backup_configurations", &options.backup_configurations)
591            .set("eggs", &options.eggs);
592
593        let row = query_builder
594            .returning(&Self::columns_sql(None))
595            .fetch_one(&mut **transaction)
596            .await?;
597        let mut announcement = Self::map(None, &row)?;
598
599        Self::run_after_create_handlers(&mut announcement, &options, state, transaction).await?;
600
601        Ok(announcement)
602    }
603}
604
605#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
606pub struct UpdateAnnouncementOptions {
607    #[garde(skip)]
608    pub r#type: Option<AnnouncementType>,
609
610    #[garde(skip)]
611    pub enabled: Option<bool>,
612    #[garde(skip)]
613    #[serde(default, with = "::serde_with::rust::double_option")]
614    pub enabled_start: Option<Option<chrono::DateTime<chrono::Utc>>>,
615    #[garde(skip)]
616    #[serde(default, with = "::serde_with::rust::double_option")]
617    pub enabled_end: Option<Option<chrono::DateTime<chrono::Utc>>>,
618    #[garde(skip)]
619    pub dismissible: Option<bool>,
620    #[garde(skip)]
621    #[serde(default, with = "::serde_with::rust::double_option")]
622    pub dismissible_end: Option<Option<chrono::DateTime<chrono::Utc>>>,
623
624    #[garde(length(chars, min = 1, max = 255))]
625    #[schema(min_length = 1, max_length = 255)]
626    pub title: Option<compact_str::CompactString>,
627    #[garde(inner(custom(validate_title_translations)))]
628    pub title_translations:
629        Option<BTreeMap<compact_str::CompactString, compact_str::CompactString>>,
630    #[garde(length(chars, min = 1, max = 2048))]
631    #[schema(min_length = 1, max_length = 2048)]
632    pub content: Option<compact_str::CompactString>,
633    #[garde(inner(custom(validate_content_translations)))]
634    pub content_translations:
635        Option<BTreeMap<compact_str::CompactString, compact_str::CompactString>>,
636
637    #[garde(length(max = 100))]
638    #[schema(max_length = 100)]
639    pub locations: Option<Vec<uuid::Uuid>>,
640    #[garde(length(max = 100))]
641    #[schema(max_length = 100)]
642    pub nodes: Option<Vec<uuid::Uuid>>,
643    #[garde(length(max = 100))]
644    #[schema(max_length = 100)]
645    pub backup_configurations: Option<Vec<uuid::Uuid>>,
646    #[garde(length(max = 100))]
647    #[schema(max_length = 100)]
648    pub eggs: Option<Vec<uuid::Uuid>>,
649}
650
651#[async_trait::async_trait]
652impl UpdatableModel for Announcement {
653    type UpdateOptions = UpdateAnnouncementOptions;
654
655    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
656        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<Announcement>> =
657            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
658
659        &UPDATE_LISTENERS
660    }
661
662    async fn update_with_transaction(
663        &mut self,
664        state: &crate::State,
665        mut options: Self::UpdateOptions,
666        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
667    ) -> Result<(), crate::database::DatabaseError> {
668        options.validate()?;
669
670        let mut query_builder = UpdateQueryBuilder::new("announcements");
671
672        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
673            .await?;
674
675        query_builder
676            .set("type", options.r#type)
677            .set("enabled", options.enabled)
678            .set(
679                "enabled_start",
680                options
681                    .enabled_start
682                    .as_ref()
683                    .map(|dt| dt.as_ref().map(|dt| dt.naive_utc())),
684            )
685            .set(
686                "enabled_end",
687                options
688                    .enabled_end
689                    .as_ref()
690                    .map(|dt| dt.as_ref().map(|dt| dt.naive_utc())),
691            )
692            .set("dismissible", options.dismissible)
693            .set(
694                "dismissible_end",
695                options
696                    .dismissible_end
697                    .as_ref()
698                    .map(|dt| dt.as_ref().map(|dt| dt.naive_utc())),
699            )
700            .set("title", options.title.as_ref())
701            .set(
702                "title_translations",
703                options
704                    .title_translations
705                    .as_ref()
706                    .map(serde_json::to_value)
707                    .transpose()?,
708            )
709            .set("content", options.content.as_ref())
710            .set(
711                "content_translations",
712                options
713                    .content_translations
714                    .as_ref()
715                    .map(serde_json::to_value)
716                    .transpose()?,
717            )
718            .set("locations", options.locations.as_ref())
719            .set("nodes", options.nodes.as_ref())
720            .set(
721                "backup_configurations",
722                options.backup_configurations.as_ref(),
723            )
724            .set("eggs", options.eggs.as_ref())
725            .where_eq("uuid", self.uuid);
726
727        query_builder.execute(&mut **transaction).await?;
728
729        if let Some(r#type) = options.r#type {
730            self.r#type = r#type;
731        }
732        if let Some(enabled) = options.enabled {
733            self.enabled = enabled;
734        }
735        if let Some(enabled_start) = options.enabled_start {
736            self.enabled_start = enabled_start.map(|dt| dt.naive_utc());
737        }
738        if let Some(enabled_end) = options.enabled_end {
739            self.enabled_end = enabled_end.map(|dt| dt.naive_utc());
740        }
741        if let Some(dismissible) = options.dismissible {
742            self.dismissible = dismissible;
743        }
744        if let Some(dismissible_end) = options.dismissible_end {
745            self.dismissible_end = dismissible_end.map(|dt| dt.naive_utc());
746        }
747        if let Some(title) = options.title {
748            self.title = title;
749        }
750        if let Some(title_translations) = options.title_translations {
751            self.title_translations = title_translations;
752        }
753        if let Some(content) = options.content {
754            self.content = content;
755        }
756        if let Some(content_translations) = options.content_translations {
757            self.content_translations = content_translations;
758        }
759        if let Some(locations) = options.locations {
760            self.locations = locations;
761        }
762        if let Some(nodes) = options.nodes {
763            self.nodes = nodes;
764        }
765        if let Some(backup_configurations) = options.backup_configurations {
766            self.backup_configurations = backup_configurations;
767        }
768        if let Some(eggs) = options.eggs {
769            self.eggs = eggs;
770        }
771
772        self.run_after_update_handlers(state, transaction).await?;
773
774        Ok(())
775    }
776}
777
778#[async_trait::async_trait]
779impl DeletableModel for Announcement {
780    type DeleteOptions = ();
781
782    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
783        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<Announcement>> =
784            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
785
786        &DELETE_LISTENERS
787    }
788
789    async fn delete_with_transaction(
790        &self,
791        state: &crate::State,
792        options: Self::DeleteOptions,
793        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
794    ) -> Result<(), anyhow::Error> {
795        self.run_delete_handlers(&options, state, transaction)
796            .await?;
797
798        sqlx::query(
799            r#"
800            DELETE FROM announcements
801            WHERE announcements.uuid = $1
802            "#,
803        )
804        .bind(self.uuid)
805        .execute(&mut **transaction)
806        .await?;
807
808        self.run_after_delete_handlers(&options, state, transaction)
809            .await?;
810
811        Ok(())
812    }
813}
814
815#[derive(Validate)]
816pub struct DuplicateAnnouncementOptions {}
817
818#[async_trait::async_trait]
819impl DuplicableModel for Announcement {
820    type DuplicateOptions<'a> = DuplicateAnnouncementOptions;
821    type DuplicateResult = Self;
822
823    fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
824        static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<Announcement>> =
825            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
826
827        &DUPLICATE_LISTENERS
828    }
829
830    async fn duplicate_with_transaction(
831        &self,
832        state: &crate::State,
833        options: Self::DuplicateOptions<'_>,
834        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
835    ) -> Result<Self, crate::database::DatabaseError> {
836        options.validate()?;
837
838        self.run_duplicate_handlers(&options, state, transaction)
839            .await?;
840
841        let mut query_builder = InsertQueryBuilder::new("announcements");
842
843        query_builder
844            .set("type", self.r#type)
845            .set("enabled", self.enabled)
846            .set("enabled_start", self.enabled_start)
847            .set("enabled_end", self.enabled_end)
848            .set("dismissible", self.dismissible)
849            .set("dismissible_end", self.dismissible_end)
850            .set("title", &self.title)
851            .set(
852                "title_translations",
853                serde_json::to_value(&self.title_translations)?,
854            )
855            .set("content", &self.content)
856            .set(
857                "content_translations",
858                serde_json::to_value(&self.content_translations)?,
859            )
860            .set("locations", &self.locations)
861            .set("nodes", &self.nodes)
862            .set("backup_configurations", &self.backup_configurations)
863            .set("eggs", &self.eggs);
864
865        let row = query_builder
866            .returning(&Self::columns_sql(None))
867            .fetch_one(&mut **transaction)
868            .await?;
869        let mut announcement = Self::map(None, &row)?;
870
871        self.run_after_duplicate_handlers(&mut announcement, &options, state, transaction)
872            .await?;
873
874        Ok(announcement)
875    }
876}
877
878#[schema_extension_derive::extendible]
879#[init_args(Announcement, crate::State)]
880#[hook_args(crate::State)]
881#[derive(ToSchema, Serialize)]
882#[schema(title = "AdminAnnouncement")]
883pub struct AdminApiAnnouncement {
884    pub uuid: uuid::Uuid,
885
886    pub r#type: AnnouncementType,
887    pub enabled: bool,
888    pub enabled_start: Option<chrono::DateTime<chrono::Utc>>,
889    pub enabled_end: Option<chrono::DateTime<chrono::Utc>>,
890    pub dismissible: bool,
891    pub dismissible_end: Option<chrono::DateTime<chrono::Utc>>,
892
893    pub title: compact_str::CompactString,
894    pub title_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
895    pub content: compact_str::CompactString,
896    pub content_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
897
898    pub locations: Vec<uuid::Uuid>,
899    pub nodes: Vec<uuid::Uuid>,
900    pub backup_configurations: Vec<uuid::Uuid>,
901    pub eggs: Vec<uuid::Uuid>,
902
903    pub created: chrono::DateTime<chrono::Utc>,
904}
905
906#[schema_extension_derive::extendible]
907#[init_args(Announcement, crate::State)]
908#[hook_args(crate::State)]
909#[derive(ToSchema, Serialize)]
910#[schema(title = "ApiAnnouncement")]
911pub struct ApiAnnouncement {
912    pub uuid: uuid::Uuid,
913
914    pub r#type: AnnouncementType,
915    pub dismissible: bool,
916    pub dismissible_end: Option<chrono::DateTime<chrono::Utc>>,
917
918    pub title: compact_str::CompactString,
919    pub title_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
920    pub content: compact_str::CompactString,
921    pub content_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
922}