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 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 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 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 pub async fn create_ad(&self, data: UserAd) -> Result<UserAd> {
88 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 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 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 if user.id != ad.owner && !user.permissions.check(FinePermission::MANAGE_USERS) {
142 return Err(Error::NotAllowed);
143 }
144
145 self.delete_upload(ad.upload_id).await?;
147
148 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 self.0.1.remove(format!("atto.ad:{}", id)).await;
162 Ok(())
163 }
164
165 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; pub const AD_RUN_CHARGE: i32 = 25;
189 pub const AD_CLICK_CHARGE: i32 = 2;
191
192 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 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 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 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 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}