use crate::model::{
auth::{User, UserWarning},
economy::{CoinTransfer, CoinTransferMethod, CoinTransferSource, UserAd, UserAdSize},
permissions::FinePermission,
Error, Result,
};
use crate::{auto_method, DataManager};
use oiseau::{cache::Cache, execute, get, params, query_row, query_rows, PostgresRow};
use tetratto_shared::unix_epoch_timestamp;
impl DataManager {
pub(crate) fn get_ad_from_row(x: &PostgresRow) -> UserAd {
UserAd {
id: get!(x->0(i64)) as usize,
created: get!(x->1(i64)) as usize,
owner: get!(x->2(i64)) as usize,
upload_id: get!(x->3(i64)) as usize,
target: get!(x->4(String)),
last_charge_time: get!(x->5(i64)) as usize,
is_running: get!(x->6(i32)) as i8 == 1,
size: serde_json::from_str(&get!(x->7(String))).unwrap(),
}
}
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:{}");
pub async fn get_ads_by_user(
&self,
id: usize,
batch: usize,
page: usize,
) -> Result<Vec<UserAd>> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT * FROM ads WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
&[&(id as i64), &(batch as i64), &((page * batch) as i64)],
|x| { Self::get_ad_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("ad".to_string()));
}
Ok(res.unwrap())
}
pub async fn stop_all_ads_by_user(&self, id: usize) -> Result<Vec<UserAd>> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"UPDATE ads SET is_running = 0 WHERE owner = $1",
&[&(id as i64)],
|x| { Self::get_ad_from_row(x) }
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(res.unwrap())
}
pub async fn create_ad(&self, data: UserAd) -> Result<UserAd> {
if data.target.len() < 2 {
return Err(Error::DataTooShort("description".to_string()));
} else if data.target.len() > 256 {
return Err(Error::DataTooLong("description".to_string()));
}
if data.is_running {
self.create_transfer(
&mut CoinTransfer::new(
data.owner,
self.0.0.system_user,
Self::AD_RUN_CHARGE,
CoinTransferMethod::Transfer,
CoinTransferSource::AdCharge,
),
true,
)
.await?;
}
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(
&conn,
"INSERT INTO ads VALUES (DEFAULT, $1, $2, $3, $4, $5, $6, $7)",
params![
&(data.created as i64),
&(data.owner as i64),
&(data.upload_id as i64),
&data.target,
&(data.last_charge_time as i64),
&if data.is_running { 1 } else { 0 },
&serde_json::to_string(&data.size).unwrap()
]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(data)
}
pub async fn delete_ad(&self, id: usize, user: &User) -> Result<()> {
let ad = self.get_ad_by_id(id).await?;
if user.id != ad.owner && !user.permissions.check(FinePermission::MANAGE_USERS) {
return Err(Error::NotAllowed);
}
if let Err(e) = self.2.delete_upload(ad.upload_id).await {
return Err(Error::MiscError(e.to_string()));
}
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(&conn, "DELETE FROM ads WHERE id = $1", &[&(id as i64)]);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
self.0.1.remove(format!("atto.ad:{}", id)).await;
Ok(())
}
pub async fn random_ad(&self, size: UserAdSize) -> Result<UserAd> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_row!(
&conn,
"SELECT * FROM ads WHERE is_running = 1 AND size = $1 ORDER BY RANDOM() DESC LIMIT 1",
&[&serde_json::to_string(&size).unwrap()],
|x| { Ok(Self::get_ad_from_row(x)) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("ad".to_string()));
}
Ok(res.unwrap())
}
const MINIMUM_DELTA_FOR_CHARGE: usize = 604_800_000; pub const AD_RUN_CHARGE: i32 = 25;
pub const AD_CLICK_CHARGE: i32 = 2;
pub async fn random_ad_charged(&self, size: UserAdSize) -> Result<UserAd> {
let ad = self.random_ad(size).await?;
let now = unix_epoch_timestamp();
let delta = now - ad.last_charge_time;
if delta >= Self::MINIMUM_DELTA_FOR_CHARGE {
if let Err(e) = self
.create_transfer(
&mut CoinTransfer::new(
ad.owner,
self.0.0.system_user,
Self::AD_RUN_CHARGE,
CoinTransferMethod::Transfer,
CoinTransferSource::AdCharge,
),
true,
)
.await
{
self.stop_all_ads_by_user(ad.owner).await?;
return Err(e);
};
self.update_ad_last_charge_time(ad.id, now as i64).await?;
}
Ok(ad)
}
pub async fn ad_click(&self, host: usize, ad: usize, user: Option<User>) -> Result<String> {
let ad = self.get_ad_by_id(ad).await?;
if let Some(ref ua) = user {
if ua.id == host {
self.create_user_warning(
UserWarning::new(
ua.id,
self.0.0.system_user,
"Automated warning: do not click on ads on your own site! This incident has been reported.".to_string()
)
).await?;
return Ok(ad.target);
}
}
if let Err(e) = self
.create_transfer(
&mut CoinTransfer::new(
ad.owner,
host,
Self::AD_CLICK_CHARGE,
CoinTransferMethod::Transfer,
CoinTransferSource::AdClick,
),
true,
)
.await
{
self.stop_all_ads_by_user(ad.owner).await?;
return Err(e);
}
Ok(ad.target)
}
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:{}");
auto_method!(update_ad_last_charge_time(i64) -> "UPDATE ads SET last_charge_time = $1 WHERE id = $2" --cache-key-tmpl="atto.ad:{}");
}