1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
use serde::{Serialize, Deserialize};
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
use super::auth::User;
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ProductFulfillmentMethod {
/// Automatically send a letter to the customer with the specified content.
AutoMail(String),
/// Manually send a letter to the customer with the specified content.
///
/// This will leave the [`CoinTransfer`] pending until you send this mail.
ManualMail,
/// A CSS snippet which can be applied to user profiles.
///
/// Only supporters can create products like this.
ProfileStyle,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct ProductUploads {
/// Promotional thumbnails shown on the product page.
///
/// Maximum of 4 with a maximum upload size of 2 MiB.
#[serde(default)]
pub thumbnails: Vec<usize>,
/// Reward given to users through active configurations after they purchase the product.
//
// Maximum upload size of 4 MiB.
#[serde(default)]
pub reward: usize,
}
impl Default for ProductUploads {
fn default() -> Self {
Self {
thumbnails: Vec::new(),
reward: 0,
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Product {
pub id: usize,
pub created: usize,
pub owner: usize,
pub title: String,
pub description: String,
/// How this product will be delivered.
pub method: ProductFulfillmentMethod,
/// If this product is actually for sale.
pub on_sale: bool,
/// The price of this product.
pub price: i32,
/// The number of times this product can be purchased.
///
/// A negative stock means the product has unlimited stock.
pub stock: i32,
/// If this product is limited to one purchase per person.
#[serde(default)]
pub single_use: bool,
/// Data for this product. Only used by snippets.
#[serde(default)]
pub data: String,
/// Uploads for this product.
#[serde(default)]
pub uploads: ProductUploads,
}
impl Product {
/// Create a new [`Product`].
pub fn new(owner: usize, title: String, description: String) -> Self {
Self {
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
created: unix_epoch_timestamp(),
owner,
title,
description,
method: ProductFulfillmentMethod::ManualMail,
on_sale: false,
price: 0,
stock: 0,
single_use: true,
data: String::new(),
uploads: ProductUploads::default(),
}
}
}
#[derive(Serialize, Deserialize)]
pub enum CoinTransferMethod {
Transfer,
/// A [`Product`] purchase with the product's ID.
Purchase(usize),
}
#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub enum CoinTransferSource {
/// An unknown source, such as a transfer request.
General,
/// A product sale.
Sale,
/// A purchase of coins through Stripe.
Purchase,
/// A refund of coins.
Refund,
/// The charge for keeping an ad running.
AdCharge,
/// Gained coins from a click on an ad on your site.
AdClick,
}
#[derive(Serialize, Deserialize)]
pub struct CoinTransfer {
pub id: usize,
pub created: usize,
pub sender: usize,
pub receiver: usize,
pub amount: i32,
pub is_pending: bool,
pub method: CoinTransferMethod,
pub source: CoinTransferSource,
}
impl CoinTransfer {
/// Create a new [`CoinTransfer`].
pub fn new(
sender: usize,
receiver: usize,
amount: i32,
method: CoinTransferMethod,
source: CoinTransferSource,
) -> Self {
Self {
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
created: unix_epoch_timestamp(),
sender,
receiver,
amount,
is_pending: false,
method,
source,
}
}
/// Apply the effects of this transaction onto the sender and receiver balances.
///
/// # Returns
/// `(sender bankrupt, receiver bankrupt)`
pub fn apply(&self, sender: &mut User, receiver: &mut User) -> (bool, bool) {
sender.coins -= self.amount;
receiver.coins += self.amount;
(sender.coins < 0, receiver.coins < 0)
}
}
/// <https://en.wikipedia.org/wiki/Web_banner#Standard_sizes>
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum UserAdSize {
/// 970x250
Billboard,
/// 720x90
Leaderboard,
/// 160x600
Skyscraper,
/// 300x250
MediumRectangle,
/// 320x50 - mobile only
MobileLeaderboard,
}
impl Default for UserAdSize {
fn default() -> Self {
Self::MediumRectangle
}
}
impl UserAdSize {
/// Get the dimensions of the size in CSS pixels.
pub fn dimensions(&self) -> (u16, u16) {
match self {
Self::Billboard => (970, 250),
Self::Leaderboard => (720, 90),
Self::Skyscraper => (160, 600),
Self::MediumRectangle => (300, 250),
Self::MobileLeaderboard => (320, 50),
}
}
}
#[derive(Serialize, Deserialize)]
pub struct UserAd {
pub id: usize,
pub created: usize,
pub owner: usize,
pub upload_id: usize,
pub target: String,
/// The time that the owner was last charged for keeping this ad up.
///
/// Ads cost 50 coins per day of running.
pub last_charge_time: usize,
pub is_running: bool,
pub size: UserAdSize,
}
impl UserAd {
/// Create a new [`UserAd`].
pub fn new(owner: usize, upload_id: usize, target: String, size: UserAdSize) -> Self {
let created = unix_epoch_timestamp();
Self {
id: 0, // will be overwritten by postgres
created,
owner,
upload_id,
target,
last_charge_time: 0,
is_running: false,
size,
}
}
}