1use std::collections::HashMap;
2
3use chrono::{NaiveDateTime, Utc};
4use diesel::{
5 ExpressionMethods, QueryDsl, RunQueryDsl, Selectable,
6 backend::Backend,
7 deserialize::{self, FromSql, FromSqlRow},
8 expression::AsExpression,
9 mysql::Mysql,
10 prelude::{Insertable, Queryable},
11 serialize::{self, IsNull, Output, ToSql},
12 sql_types::Text,
13};
14use log::warn;
15use serde::{Deserialize, Serialize};
16
17use crate::{
18 endpoints::interactions::{InteractionReturnType, LeaderBoardPlayers, ZombieTree},
19 establish_connection,
20 player::{Player, PlayerRole, PlayerState},
21 schema::interactions,
22};
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, AsExpression, FromSqlRow)]
26#[diesel(sql_type = Text)]
27pub enum InteractionType {
28 Hunt,
29 Feed,
30 Cure,
31 Corpsify,
32 Starve,
33 ReviveToHuman,
34 ReviveToZombie,
35 Other,
36}
37
38impl ToSql<Text, Mysql> for InteractionType {
39 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Mysql>) -> serialize::Result {
41 match self {
42 InteractionType::Hunt => <str as ToSql<Text, Mysql>>::to_sql("Hunt", out)?,
43 InteractionType::Feed => <str as ToSql<Text, Mysql>>::to_sql("Feed", out)?,
44 InteractionType::Cure => <str as ToSql<Text, Mysql>>::to_sql("Cure", out)?,
45 InteractionType::Corpsify => <str as ToSql<Text, Mysql>>::to_sql("Corpsify", out)?,
46 InteractionType::Starve => <str as ToSql<Text, Mysql>>::to_sql("Starve", out)?,
47 InteractionType::ReviveToHuman => {
48 <str as ToSql<Text, Mysql>>::to_sql("ReviveToHuman", out)?
49 }
50 InteractionType::ReviveToZombie => {
51 <str as ToSql<Text, Mysql>>::to_sql("ReviveToZombie", out)?
52 }
53 InteractionType::Other => <str as ToSql<Text, Mysql>>::to_sql("Other", out)?,
54 };
55 Ok(IsNull::No)
56 }
57}
58
59impl FromSql<Text, Mysql> for InteractionType {
60 fn from_sql<'a>(string: <Mysql as Backend>::RawValue<'a>) -> deserialize::Result<Self> {
62 match str::from_utf8(string.as_bytes())? {
63 "Hunt" => Ok(InteractionType::Hunt),
64 "Feed" => Ok(InteractionType::Feed),
65 "Cure" => Ok(InteractionType::Cure),
66 "Corpsify" => Ok(InteractionType::Corpsify),
67 "Starve" => Ok(InteractionType::Starve),
68 "ReviveToHuman" => Ok(InteractionType::ReviveToHuman),
69 "ReviveToZombie" => Ok(InteractionType::ReviveToZombie),
70 "Other" => Ok(InteractionType::Other),
71 _ => Err(format!("Invalid InteractionType string value").into()),
72 }
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, Selectable, Queryable, Insertable)]
80#[diesel(table_name = crate::schema::interactions)]
81#[diesel(check_for_backend(diesel::mysql::Mysql))]
82#[diesel(belongs_to(Player))]
83pub struct Interaction {
84 pub id: String,
85 pub interaction_type: InteractionType,
86 pub player: String,
87 pub target: Option<String>,
88 pub date: Option<NaiveDateTime>,
89}
90
91impl Interaction {
92 pub fn new(interaction_type: InteractionType, player: String, target: Option<String>) -> Self {
107 Interaction {
108 id: uuid::Uuid::new_v4().to_string(),
109 interaction_type,
110 player,
111 target,
112 date: Some(Utc::now().naive_local()),
113 }
114 }
115
116 pub async fn save_hunt_interaction(
132 player: &Player,
133 target: &Player,
134 feed1: Option<&Player>,
135 feed2: Option<&Player>,
136 ) -> Result<(), String> {
137 let connection = &mut establish_connection();
138 let hunt = Interaction::new(
139 InteractionType::Hunt,
140 player.id.clone(),
141 Some(target.id.clone()),
142 );
143
144 diesel::insert_into(interactions::table)
145 .values(&hunt)
146 .execute(connection)
147 .map_err(|e| format!("Failed to save hunt interaction: {}", e))?;
148
149 if let Some(feed1) = feed1 {
150 let feed1_interaction = Interaction::new(
151 InteractionType::Feed,
152 player.id.clone(),
153 Some(feed1.id.clone()),
154 );
155
156 diesel::insert_into(interactions::table)
157 .values(&feed1_interaction)
158 .execute(connection)
159 .map_err(|e| format!("Failed to save feed interaction: {}", e))?;
160 }
161
162 if let Some(feed2) = feed2 {
163 let feed2_interaction = Interaction::new(
164 InteractionType::Feed,
165 player.id.clone(),
166 Some(feed2.id.clone()),
167 );
168
169 diesel::insert_into(interactions::table)
170 .values(&feed2_interaction)
171 .execute(connection)
172 .map_err(|e| format!("Failed to save feed interaction: {}", e))?;
173 }
174
175 Ok(())
176 }
177
178 pub async fn get_newest_interactions() -> Result<Vec<Interaction>, String> {
183 let connection = &mut establish_connection();
184
185 let result = interactions::table
186 .order(interactions::id.desc())
187 .limit(1)
188 .load::<Interaction>(connection);
189
190 match result {
191 Ok(interactions) => Ok(interactions),
192 Err(err) => Err(err.to_string()),
193 }
194 }
195
196 pub async fn get_all_interactions() -> Result<Vec<InteractionReturnType>, String> {
203 let connection = &mut establish_connection();
204
205 let result = interactions::table
206 .load::<Interaction>(connection)
207 .map_err(|e| e.to_string())
208 .expect("Failed to load interactions");
209
210 let mut interactions = Vec::new();
211 for interaction in result {
212 match Player::get_player(interaction.player.clone()).await {
213 Ok(Some(player)) => {
214 let player_alias = if player.state == PlayerState::Zombie0 {
215 String::from("Z0")
216 } else {
217 player.alias
218 };
219 let target_alias = if let Some(target_id) = interaction.target {
220 match Player::get_player(target_id.clone()).await {
221 Ok(Some(target)) => Some(target.alias),
222 Ok(None) => {
223 warn!("Target not found: {}", target_id);
224 None
225 }
226 Err(e) => return Err(e),
227 }
228 } else {
229 None
230 };
231 interactions.push(InteractionReturnType {
232 player_alias,
233 target_alias,
234 interaction_type: interaction.interaction_type,
235 date: interaction.date,
236 });
237 }
238 Ok(None) => warn!("Player not found: {}", interaction.player),
239 Err(e) => return Err(e),
240 }
241 }
242
243 Ok(interactions)
244 }
245
246 pub async fn get_hunt_interactions() -> Result<Vec<Interaction>, String> {
253 let connection = &mut establish_connection();
254
255 let result = interactions::table
256 .filter(interactions::interaction_type.eq(InteractionType::Hunt))
257 .load::<Interaction>(connection)
258 .map_err(|e| e.to_string());
259
260 result
261 }
262
263 pub async fn get_leaderboard(is_zombie: bool) -> Result<Vec<LeaderBoardPlayers>, String> {
272 let hunt_interactions = match Interaction::get_hunt_interactions().await {
273 Ok(vec) => vec,
274 Err(e) => return Err(e),
275 };
276
277 let mut kill_list: HashMap<String, Vec<String>> = HashMap::new();
278 for interaction in &hunt_interactions {
279 if let Some(user_id) = interaction.target.clone() {
280 if let Ok(Some(target)) = Player::get_player(user_id.clone()).await {
281 kill_list
282 .entry(interaction.player.clone())
283 .or_insert_with(Vec::new)
284 .push(target.alias.clone());
285 }
286 }
287 }
288
289 let mut leaderboard: Vec<LeaderBoardPlayers> = Vec::new();
290 for (user_id, kills) in kill_list.iter() {
291 match Player::get_player(user_id.clone()).await {
292 Ok(Some(player)) => {
293 match player.state {
294 PlayerState::Zombie0 => {
295 if let Some(existing) = leaderboard
296 .iter_mut()
297 .find(|p| p.state == PlayerState::Zombie0)
298 {
299 existing.kills.extend(kills.clone());
300 } else {
301 leaderboard.push(LeaderBoardPlayers {
302 alias: String::from("Z0"),
303 kills: kills.clone(),
304 next_feeding: None,
305 state: player.state,
306 role: player.role,
307 });
308 }
309 }
310 _ => leaderboard.push(LeaderBoardPlayers {
311 alias: player.alias,
312 kills: kills.clone(),
313 next_feeding: if is_zombie { player.next_feeding } else { None },
314 state: player.state,
315 role: player.role,
316 }),
317 };
318 }
319 Ok(None) => warn!("Player not found: {}", user_id),
320 Err(e) => return Err(e),
321 }
322 }
323
324 leaderboard.sort_by(|a, b| b.kills.len().cmp(&a.kills.len()));
325
326 Ok(leaderboard)
327 }
328
329 pub async fn get_zombie_tree()
339 -> Result<(Vec<ZombieTree>, HashMap<String, (PlayerState, PlayerRole)>), String> {
340 let hunt_interactions = match Interaction::get_hunt_interactions().await {
341 Ok(vec) => vec,
342 Err(e) => return Err(e),
343 };
344
345 let mut kill_list: HashMap<String, Vec<(NaiveDateTime, String)>> = HashMap::new();
346 for interaction in &hunt_interactions {
347 if let Some(user_id) = interaction.target.clone() {
348 kill_list
349 .entry(interaction.player.clone())
350 .or_insert_with(Vec::new)
351 .push((interaction.date.unwrap_or_default(), user_id.clone()));
352 }
353 }
354
355 let mut zombie_tree = Vec::<ZombieTree>::new();
356 let mut simple_players: HashMap<String, (PlayerState, PlayerRole)> = HashMap::new();
357 for (user_id, kill_vec) in kill_list.iter() {
358 if let Ok(Some(hunter)) = Player::get_player(user_id.clone()).await {
359 let alias = match hunter.state.clone() {
360 PlayerState::Zombie0 => String::from("Z0"),
361 _ => hunter.alias,
362 };
363 for (date, kill) in kill_vec.iter() {
364 if let Ok(Some(target)) = Player::get_player(kill.clone()).await {
365 zombie_tree.push(ZombieTree {
366 hunter_alias: alias.clone(),
367 target_alias: target.alias.clone(),
368 date: *date,
369 });
370 simple_players
371 .entry(alias.clone())
372 .or_insert((hunter.state.clone(), hunter.role.clone()));
373 simple_players
374 .entry(target.alias.clone())
375 .or_insert((target.state.clone(), target.role.clone()));
376 };
377 }
378 }
379 }
380
381 Ok((zombie_tree, simple_players))
382 }
383
384 pub async fn delete(id: String) -> Result<(), String> {
392 let connection = &mut establish_connection();
393
394 let result =
395 diesel::delete(interactions::table.filter(interactions::id.eq(id))).execute(connection);
396
397 match result {
398 Ok(_) => Ok(()),
399 Err(err) => Err(err.to_string()),
400 }
401 }
402}