backend/objects/
guild.rs

1use actix_web::web::BytesMut;
2use diesel::{
3    ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into,
4    update,
5};
6use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager};
7use serde::Serialize;
8use tokio::task;
9use url::Url;
10use uuid::Uuid;
11
12use crate::{
13    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        pool: &deadpool::managed::Pool<
72            AsyncDieselConnectionManager<diesel_async::AsyncPgConnection>,
73            Conn,
74        >,
75        offset: i64,
76        amount: i64,
77    ) -> Result<Vec<Self>, Error> {
78        // Fetch guild data from database
79        let mut conn = pool.get().await?;
80
81        use guilds::dsl;
82        let guild_builders: Vec<GuildBuilder> = load_or_empty(
83            dsl::guilds
84                .select(GuildBuilder::as_select())
85                .order_by(dsl::uuid)
86                .offset(offset)
87                .limit(amount)
88                .load(&mut conn)
89                .await,
90        )?;
91
92        // Process each guild concurrently
93        let guild_futures = guild_builders.iter().map(async move |g| {
94            let mut conn = pool.get().await?;
95            g.clone().build(&mut conn).await
96        });
97
98        // Execute all futures concurrently and collect results
99        futures::future::try_join_all(guild_futures).await
100    }
101
102    pub async fn new(conn: &mut Conn, name: String, owner_uuid: Uuid) -> Result<Self, Error> {
103        let guild_uuid = Uuid::now_v7();
104
105        let guild_builder = GuildBuilder {
106            uuid: guild_uuid,
107            name: name.clone(),
108            description: None,
109            icon: None,
110        };
111
112        insert_into(guilds::table)
113            .values(guild_builder)
114            .execute(conn)
115            .await?;
116
117        let member_uuid = Uuid::now_v7();
118
119        let member = MemberBuilder {
120            uuid: member_uuid,
121            nickname: None,
122            user_uuid: owner_uuid,
123            guild_uuid,
124            is_owner: true,
125        };
126
127        insert_into(guild_members::table)
128            .values(member)
129            .execute(conn)
130            .await?;
131
132        Ok(Guild {
133            uuid: guild_uuid,
134            name,
135            description: None,
136            icon: None,
137            roles: vec![],
138            member_count: 1,
139        })
140    }
141
142    pub async fn get_invites(&self, conn: &mut Conn) -> Result<Vec<Invite>, Error> {
143        use invites::dsl;
144        let invites = load_or_empty(
145            dsl::invites
146                .filter(dsl::guild_uuid.eq(self.uuid))
147                .select(Invite::as_select())
148                .load(conn)
149                .await,
150        )?;
151
152        Ok(invites)
153    }
154
155    pub async fn create_invite(
156        &self,
157        conn: &mut Conn,
158        user_uuid: Uuid,
159        custom_id: Option<String>,
160    ) -> Result<Invite, Error> {
161        let invite_id;
162
163        if let Some(id) = custom_id {
164            invite_id = id;
165            if invite_id.len() > 32 {
166                return Err(Error::BadRequest("MAX LENGTH".to_string()));
167            }
168        } else {
169            let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
170
171            invite_id = random_string::generate(8, charset);
172        }
173
174        let invite = Invite {
175            id: invite_id,
176            user_uuid,
177            guild_uuid: self.uuid,
178        };
179
180        insert_into(invites::table)
181            .values(invite.clone())
182            .execute(conn)
183            .await?;
184
185        Ok(invite)
186    }
187
188    // FIXME: Horrible security
189    pub async fn set_icon(
190        &mut self,
191        bunny_cdn: &bunny_api_tokio::Client,
192        conn: &mut Conn,
193        cdn_url: Url,
194        icon: BytesMut,
195    ) -> Result<(), Error> {
196        let icon_clone = icon.clone();
197        let image_type = task::spawn_blocking(move || image_check(icon_clone)).await??;
198
199        if let Some(icon) = &self.icon {
200            let relative_url = icon.path().trim_start_matches('/');
201
202            bunny_cdn.storage.delete(relative_url).await?;
203        }
204
205        let path = format!("icons/{}/icon.{}", self.uuid, image_type);
206
207        bunny_cdn.storage.upload(path.clone(), icon.into()).await?;
208
209        let icon_url = cdn_url.join(&path)?;
210
211        use guilds::dsl;
212        update(guilds::table)
213            .filter(dsl::uuid.eq(self.uuid))
214            .set(dsl::icon.eq(icon_url.as_str()))
215            .execute(conn)
216            .await?;
217
218        self.icon = Some(icon_url);
219
220        Ok(())
221    }
222}