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
use serde::{Deserialize, Serialize};
use crate::{Client, Code, RoliError};
use reqwest::header;
const MARKET_ACTIVITY_URL: &str = "https://www.rolimons.com/api/activity";
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RecentSalesResponse {
success: bool,
activities: Vec<Vec<Code>>,
activities_count: u64,
}
/// Details of the sale of a limited item.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
pub struct Sale {
/// The Roblox id of the item that was sold.
pub item_id: u64,
/// The rap of the item before the sale.
pub old_rap: u64,
/// The rap of the item after the sale.
pub new_rap: u64,
/// The price the item was sold at.
pub sale_price: u64,
/// The Rolimons id of the sale. Used in the url <https://www.rolimons.com/itemsale/{sale_id}>.
pub sale_id: u64,
/// The unix timestamp of the sale.
/// This is likely when the sale was detected by Rolimons.
pub timestamp: u64,
}
impl Sale {
fn from_raw(codes: Vec<Code>) -> Result<Self, RoliError> {
// Follows form of
// [
// 1679978239, timestamp
// 1, unknown
// 327318670, item id
// 4272, old rap, will be below 0 if no rap
// 4314, current rap
// 4991002 sale id, used in <https://www.rolimons.com/itemsale/4991002>
// ],
if codes.len() != 6 {
return Err(RoliError::MalformedResponse);
}
// It doesn't seem like the value will ever not be 1.
// However, as this look like the code somewhat corresponds to the type of activity,
// if the value is not 1 then we return a malformed response.
let activity_type = codes[1].to_i64()? as u64;
if activity_type != 1 {
return Err(RoliError::MalformedResponse);
}
let timestamp = codes[0].to_i64()? as u64;
let item_id = codes[2].to_i64()? as u64;
let old_rap = codes[3].to_i64()? as u64;
let new_rap = codes[4].to_i64()? as u64;
let sale_price = calculate_sale_price(old_rap, new_rap);
let sale_id = codes[5].to_i64()? as u64;
Ok(Self {
item_id,
old_rap,
new_rap,
sale_price,
timestamp,
sale_id,
})
}
}
impl Client {
/// A wrapper for the market activity page.
///
/// Provides information on the most recent limited sales.
///
/// On the Rolimons deals page, this api is polled roughly every 3 seconds.
///
/// Does not require authentication.
///
/// # Example
/// ```no_run
/// # use std::error::Error;
/// #
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn Error>> {
/// let client = roli::ClientBuilder::new().build();
/// let sales = client.recent_sales().await?;
/// #
/// # Ok(())
/// # }
/// ```
pub async fn recent_sales(&self) -> Result<Vec<Sale>, RoliError> {
let request_result = self
.reqwest_client
.get(MARKET_ACTIVITY_URL)
.header(header::USER_AGENT, crate::USER_AGENT)
.send()
.await;
match request_result {
Ok(response) => {
let status_code = response.status().as_u16();
match status_code {
200 => {
let raw = match response.json::<RecentSalesResponse>().await {
Ok(raw) => raw,
Err(_) => return Err(RoliError::MalformedResponse),
};
if !raw.success {
return Err(RoliError::RequestReturnedUnsuccessful);
}
let mut sales = Vec::new();
for activity in raw.activities {
let sale = Sale::from_raw(activity)?;
sales.push(sale);
}
Ok(sales)
}
429 => Err(RoliError::TooManyRequests),
500 => Err(RoliError::InternalServerError),
_ => Err(RoliError::UnidentifiedStatusCode(status_code)),
}
}
Err(e) => Err(RoliError::ReqwestError(e)),
}
}
}
fn calculate_sale_price(old_rap: u64, new_rap: u64) -> u64 {
// Formula from https://devforum.roblox.com/t/rap-change-calculator/1971776
// I can do basic algebra!
// If the rap was originally 0, the new rap is the sale price.
if old_rap == 0 {
return new_rap;
}
let change = new_rap as i64 - old_rap as i64;
let price = 10 * change + old_rap as i64;
price as u64
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_calculate_sale_price() {
let old_rap = 4272;
let new_rap = 4314;
let price = calculate_sale_price(old_rap, new_rap);
assert_eq!(price, 4692);
}
}