backend/objects/
guild.rs

1use axum::body::Bytes;
2use diesel::{
3    ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into,
4    update,
5};
6use diesel_async::RunQueryDsl;
7use serde::Serialize;
8use tokio::task;
9use url::Url;
10use uuid::Uuid;
11
12use crate::{
13    AppState, Conn,
14    error::Error,
15    schema::{guild_members, guilds, invites},
16    utils::image_check,
17};
18
19use super::{Invite, Member, Role, load_or_empty, member::MemberBuilder};
20
21#[derive(Serialize, Queryable, Selectable, Insertable, Clone)]
22#[diesel(table_name = guilds)]
23#[diesel(check_for_backend(diesel::pg::Pg))]
24pub struct GuildBuilder {
25    uuid: Uuid,
26    name: String,
27    description: Option<String>,
28    icon: Option<String>,
29}
30
31impl GuildBuilder {
32    pub async fn build(self, conn: &mut Conn) -> Result<Guild, Error> {
33        let member_count = Member::count(conn, self.uuid).await?;
34
35        let roles = Role::fetch_all(conn, self.uuid).await?;
36
37        Ok(Guild {
38            uuid: self.uuid,
39            name: self.name,
40            description: self.description,
41            icon: self.icon.and_then(|i| i.parse().ok()),
42            roles,
43            member_count,
44        })
45    }
46}
47
48#[derive(Serialize)]
49pub struct Guild {
50    pub uuid: Uuid,
51    name: String,
52    description: Option<String>,
53    icon: Option<Url>,
54    pub roles: Vec<Role>,
55    member_count: i64,
56}
57
58impl Guild {
59    pub async fn fetch_one(conn: &mut Conn, guild_uuid: Uuid) -> Result<Self, Error> {
60        use guilds::dsl;
61        let guild_builder: GuildBuilder = dsl::guilds
62            .filter(dsl::uuid.eq(guild_uuid))
63            .select(GuildBuilder::as_select())
64            .get_result(conn)
65            .await?;
66
67        guild_builder.build(conn).await
68    }
69
70    pub async fn fetch_amount(
71        conn: &mut Conn,
72        offset: i64,
73        amount: i64,
74    ) -> Result<Vec<Self>, Error> {
75        // Fetch guild data from database
76        use guilds::dsl;
77        let guild_builders: Vec<GuildBuilder> = load_or_empty(
78            dsl::guilds
79                .select(GuildBuilder::as_select())
80                .order_by(dsl::uuid)
81                .offset(offset)
82                .limit(amount)
83                .load(conn)
84                .await,
85        )?;
86
87        let mut guilds = vec![];
88
89        for builder in guild_builders {
90            guilds.push(builder.build(conn).await?);
91        }
92
93        Ok(guilds)
94    }
95
96    pub async fn new(conn: &mut Conn, name: String, owner_uuid: Uuid) -> Result<Self, Error> {
97        let guild_uuid = Uuid::now_v7();
98
99        let guild_builder = GuildBuilder {
100            uuid: guild_uuid,
101            name: name.clone(),
102            description: None,
103            icon: None,
104        };
105
106        insert_into(guilds::table)
107            .values(guild_builder)
108            .execute(conn)
109            .await?;
110
111        let member_uuid = Uuid::now_v7();
112
113        let member = MemberBuilder {
114            uuid: member_uuid,
115            nickname: None,
116            user_uuid: owner_uuid,
117            guild_uuid,
118            is_owner: true,
119        };
120
121        insert_into(guild_members::table)
122            .values(member)
123            .execute(conn)
124            .await?;
125
126        Ok(Guild {
127            uuid: guild_uuid,
128            name,
129            description: None,
130            icon: None,
131            roles: vec![],
132            member_count: 1,
133        })
134    }
135
136    pub async fn get_invites(&self, conn: &mut Conn) -> Result<Vec<Invite>, Error> {
137        use invites::dsl;
138        let invites = load_or_empty(
139            dsl::invites
140                .filter(dsl::guild_uuid.eq(self.uuid))
141                .select(Invite::as_select())
142                .load(conn)
143                .await,
144        )?;
145
146        Ok(invites)
147    }
148
149    pub async fn create_invite(
150        &self,
151        conn: &mut Conn,
152        user_uuid: Uuid,
153        custom_id: Option<String>,
154    ) -> Result<Invite, Error> {
155        let invite_id;
156
157        if let Some(id) = custom_id {
158            invite_id = id;
159            if invite_id.len() > 32 {
160                return Err(Error::BadRequest("MAX LENGTH".to_string()));
161            }
162        } else {
163            let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
164
165            invite_id = random_string::generate(8, charset);
166        }
167
168        let invite = Invite {
169            id: invite_id,
170            user_uuid,
171            guild_uuid: self.uuid,
172        };
173
174        insert_into(invites::table)
175            .values(invite.clone())
176            .execute(conn)
177            .await?;
178
179        Ok(invite)
180    }
181
182    // FIXME: Horrible security
183    pub async fn set_icon(
184        &mut self,
185        conn: &mut Conn,
186        app_state: &AppState,
187        icon: Bytes,
188    ) -> Result<(), Error> {
189        let icon_clone = icon.clone();
190        let image_type = task::spawn_blocking(move || image_check(icon_clone)).await??;
191
192        if let Some(icon) = &self.icon {
193            let relative_url = icon.path().trim_start_matches('/');
194
195            app_state.bunny_storage.delete(relative_url).await?;
196        }
197
198        let path = format!("icons/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type);
199
200        app_state.bunny_storage.upload(path.clone(), icon).await?;
201
202        let icon_url = app_state.config.bunny.cdn_url.join(&path)?;
203
204        use guilds::dsl;
205        update(guilds::table)
206            .filter(dsl::uuid.eq(self.uuid))
207            .set(dsl::icon.eq(icon_url.as_str()))
208            .execute(conn)
209            .await?;
210
211        self.icon = Some(icon_url);
212
213        Ok(())
214    }
215}