tetratto_core/database/
ads.rs

1use crate::model::{
2    auth::{User, UserWarning},
3    economy::{CoinTransfer, CoinTransferMethod, CoinTransferSource, UserAd, UserAdSize},
4    permissions::FinePermission,
5    Error, Result,
6};
7use crate::{auto_method, DataManager};
8use oiseau::{cache::Cache, execute, get, params, query_row, query_rows, PostgresRow};
9use tetratto_shared::unix_epoch_timestamp;
10
11impl DataManager {
12    /// Get a [`UserAd`] from an SQL row.
13    pub(crate) fn get_ad_from_row(x: &PostgresRow) -> UserAd {
14        UserAd {
15            id: get!(x->0(i64)) as usize,
16            created: get!(x->1(i64)) as usize,
17            owner: get!(x->2(i64)) as usize,
18            upload_id: get!(x->3(i64)) as usize,
19            target: get!(x->4(String)),
20            last_charge_time: get!(x->5(i64)) as usize,
21            is_running: get!(x->6(i32)) as i8 == 1,
22            size: serde_json::from_str(&get!(x->7(String))).unwrap(),
23        }
24    }
25
26    auto_method!(get_ad_by_id(usize as i64)@get_ad_from_row -> "SELECT * FROM ads WHERE id = $1" --name="ad" --returns=UserAd --cache-key-tmpl="atto.ad:{}");
27
28    /// Get all ads by user.
29    ///
30    /// # Arguments
31    /// * `id` - the ID of the user to fetch ads for
32    /// * `batch` - the limit of items in each page
33    /// * `page` - the page number
34    pub async fn get_ads_by_user(
35        &self,
36        id: usize,
37        batch: usize,
38        page: usize,
39    ) -> Result<Vec<UserAd>> {
40        let conn = match self.0.connect().await {
41            Ok(c) => c,
42            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
43        };
44
45        let res = query_rows!(
46            &conn,
47            "SELECT * FROM ads WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
48            &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
49            |x| { Self::get_ad_from_row(x) }
50        );
51
52        if res.is_err() {
53            return Err(Error::GeneralNotFound("ad".to_string()));
54        }
55
56        Ok(res.unwrap())
57    }
58
59    /// Disable all ads by the given user.
60    ///
61    /// # Arguments
62    /// * `id` - the ID of the user to kill ads from
63    pub async fn stop_all_ads_by_user(&self, id: usize) -> Result<Vec<UserAd>> {
64        let conn = match self.0.connect().await {
65            Ok(c) => c,
66            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
67        };
68
69        let res = query_rows!(
70            &conn,
71            "UPDATE ads SET is_running = 0 WHERE owner = $1",
72            &[&(id as i64)],
73            |x| { Self::get_ad_from_row(x) }
74        );
75
76        if let Err(e) = res {
77            return Err(Error::DatabaseError(e.to_string()));
78        }
79
80        Ok(res.unwrap())
81    }
82
83    /// Create a new ad in the database.
84    ///
85    /// # Arguments
86    /// * `data` - a mock [`UserAd`] object to insert
87    pub async fn create_ad(&self, data: UserAd) -> Result<UserAd> {
88        // check values
89        if data.target.len() < 2 {
90            return Err(Error::DataTooShort("description".to_string()));
91        } else if data.target.len() > 256 {
92            return Err(Error::DataTooLong("description".to_string()));
93        }
94
95        // charge for first day
96        if data.is_running {
97            self.create_transfer(
98                &mut CoinTransfer::new(
99                    data.owner,
100                    self.0.0.system_user,
101                    Self::AD_RUN_CHARGE,
102                    CoinTransferMethod::Transfer,
103                    CoinTransferSource::AdCharge,
104                ),
105                true,
106            )
107            .await?;
108        }
109
110        // ...
111        let conn = match self.0.connect().await {
112            Ok(c) => c,
113            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
114        };
115
116        let res = execute!(
117            &conn,
118            "INSERT INTO ads VALUES (DEFAULT, $1, $2, $3, $4, $5, $6, $7)",
119            params![
120                &(data.created as i64),
121                &(data.owner as i64),
122                &(data.upload_id as i64),
123                &data.target,
124                &(data.last_charge_time as i64),
125                &if data.is_running { 1 } else { 0 },
126                &serde_json::to_string(&data.size).unwrap()
127            ]
128        );
129
130        if let Err(e) = res {
131            return Err(Error::DatabaseError(e.to_string()));
132        }
133
134        Ok(data)
135    }
136
137    pub async fn delete_ad(&self, id: usize, user: &User) -> Result<()> {
138        let ad = self.get_ad_by_id(id).await?;
139
140        // check user permission
141        if user.id != ad.owner && !user.permissions.check(FinePermission::MANAGE_USERS) {
142            return Err(Error::NotAllowed);
143        }
144
145        // remove upload
146        self.delete_upload(ad.upload_id).await?;
147
148        // ...
149        let conn = match self.0.connect().await {
150            Ok(c) => c,
151            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
152        };
153
154        let res = execute!(&conn, "DELETE FROM ads WHERE id = $1", &[&(id as i64)]);
155
156        if let Err(e) = res {
157            return Err(Error::DatabaseError(e.to_string()));
158        }
159
160        // ...
161        self.0.1.remove(format!("atto.ad:{}", id)).await;
162        Ok(())
163    }
164
165    /// Pull a random running ad.
166    pub async fn random_ad(&self, size: UserAdSize) -> Result<UserAd> {
167        let conn = match self.0.connect().await {
168            Ok(c) => c,
169            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
170        };
171
172        let res = query_row!(
173            &conn,
174            "SELECT * FROM ads WHERE is_running = 1 AND size = $1 ORDER BY RANDOM() DESC LIMIT 1",
175            &[&serde_json::to_string(&size).unwrap()],
176            |x| { Ok(Self::get_ad_from_row(x)) }
177        );
178
179        if res.is_err() {
180            return Err(Error::GeneralNotFound("ad".to_string()));
181        }
182
183        Ok(res.unwrap())
184    }
185
186    const MINIMUM_DELTA_FOR_CHARGE: usize = 604_800_000; // 7 days
187    /// The amount charged to a [`UserAd`] owner each day the ad is running (and is pulled from the pool).
188    pub const AD_RUN_CHARGE: i32 = 25;
189    /// The amount charged to a [`UserAd`] owner each time the ad is clicked.
190    pub const AD_CLICK_CHARGE: i32 = 2;
191
192    /// Get a random ad and check if the ad owner needs to be charged for this period.
193    pub async fn random_ad_charged(&self, size: UserAdSize) -> Result<UserAd> {
194        let ad = self.random_ad(size).await?;
195
196        let now = unix_epoch_timestamp();
197        let delta = now - ad.last_charge_time;
198
199        if delta >= Self::MINIMUM_DELTA_FOR_CHARGE {
200            if let Err(e) = self
201                .create_transfer(
202                    &mut CoinTransfer::new(
203                        ad.owner,
204                        self.0.0.system_user,
205                        Self::AD_RUN_CHARGE,
206                        CoinTransferMethod::Transfer,
207                        CoinTransferSource::AdCharge,
208                    ),
209                    true,
210                )
211                .await
212            {
213                // boo user cannot afford to keep running their ads
214                self.stop_all_ads_by_user(ad.owner).await?;
215                return Err(e);
216            };
217
218            self.update_ad_last_charge_time(ad.id, now as i64).await?;
219        }
220
221        Ok(ad)
222    }
223
224    /// Handle a click on an ad from the given host.
225    ///
226    /// Hosts are just the ID of the user that is embedding the ad on their page.
227    pub async fn ad_click(&self, host: usize, ad: usize, user: Option<User>) -> Result<String> {
228        let ad = self.get_ad_by_id(ad).await?;
229
230        if let Some(ref ua) = user {
231            if ua.id == host {
232                self.create_user_warning(
233                    UserWarning::new(
234                        ua.id,
235                        self.0.0.system_user,
236                        "Automated warning: do not click on ads on your own site! This incident has been reported.".to_string()
237                    )
238                ).await?;
239
240                return Ok(ad.target);
241            }
242        }
243
244        // create click transfer
245        if let Err(e) = self
246            .create_transfer(
247                &mut CoinTransfer::new(
248                    ad.owner,
249                    host,
250                    Self::AD_CLICK_CHARGE,
251                    CoinTransferMethod::Transfer,
252                    CoinTransferSource::AdClick,
253                ),
254                true,
255            )
256            .await
257        {
258            self.stop_all_ads_by_user(ad.owner).await?;
259            return Err(e);
260        }
261
262        // return
263        Ok(ad.target)
264    }
265
266    auto_method!(update_ad_is_running(i32)@get_ad_by_id:FinePermission::MANAGE_USERS; -> "UPDATE ads SET is_running = $1 WHERE id = $2" --cache-key-tmpl="atto.ad:{}");
267    auto_method!(update_ad_last_charge_time(i64) -> "UPDATE ads SET last_charge_time = $1 WHERE id = $2" --cache-key-tmpl="atto.ad:{}");
268}