Skip to main content

dink_osrs/
lib.rs

1use std::collections::HashMap;
2
3// https://github.com/pajlads/DinkPlugin/blob/master/docs/json-examples.md
4// Structs for the Dink Plugin Message types
5/*
6  {
7    "content": "Text message as set by the user",
8    "extra": {},
9    "type": "NOTIFICATION_TYPE",
10    "playerName": "your rsn",
11    "accountType": "NORMAL | IRONMAN | HARDCORE_IRONMAN",
12    "seasonalWorld": "true | false",
13    "dinkAccountHash": "abcdefghijklmnopqrstuvwxyz1234abcdefghijklmnopqrstuvwxyz",
14    "embeds": []
15  }
16*/
17
18#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
19#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
20pub enum NotificationType {
21    Death,
22    Collection,
23    Level,
24    XpMilestone,
25    Loot,
26    Slayer,
27    Quest,
28    Clue,
29    KillCount,
30    CombatAchievement,
31    AchievementDiary,
32    Pet,
33    Speedrun,
34    BarbarianAssaultGamble,
35    PlayerKill,
36    GroupStorage,
37    GrandExchange,
38    Trade,
39    LeaguesArea,
40    LeaguesMastery,
41    LeaguesRelic,
42    LeaguesTask,
43    Chat,
44    ExternalPlugin,
45    Login,
46    Logout,
47    ToaUnique,
48}
49
50#[derive(Debug, Clone, PartialEq)]
51pub struct Notification {
52    pub content: String,
53    pub extra: HashMap<String, String>,
54    pub notification_type: NotificationType,
55    pub player_name: String,
56    pub account_type: String,
57    pub seasonal_world: String,
58    pub dink_account_hash: String,
59    // pub embeds: Vec<Embed>, ignore for now
60}
61
62impl Notification {
63    /// Creates a short message summarizing the notification
64    pub fn message(&self) -> String {
65        match &self.notification_type {
66            NotificationType::Death => {
67                let value_lost = self
68                    .extra
69                    .get("valueLost")
70                    .map(|s| s.as_str())
71                    .unwrap_or("0");
72                let is_pvp = self
73                    .extra
74                    .get("isPvp")
75                    .map(|s| s.as_str())
76                    .unwrap_or("false");
77                if is_pvp == "true" {
78                    format!("{} died in PvP, lost {} gp", self.player_name, value_lost)
79                } else {
80                    format!("{} died, lost {} gp", self.player_name, value_lost)
81                }
82            }
83            NotificationType::Collection => {
84                let item_name = self
85                    .extra
86                    .get("itemName")
87                    .map(|s| s.as_str())
88                    .unwrap_or("Unknown item");
89                let price = self.extra.get("price").map(|s| s.as_str()).unwrap_or("0");
90                format!(
91                    "{} added {} to collection ({} gp)",
92                    self.player_name, item_name, price
93                )
94            }
95            NotificationType::Level => {
96                let skills: Vec<String> = self
97                    .extra
98                    .get("levelledSkills")
99                    .map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
100                    .unwrap_or_default();
101                if skills.is_empty() {
102                    format!("{} leveled up", self.player_name)
103                } else {
104                    format!("{} leveled up: {}", self.player_name, skills.join(", "))
105                }
106            }
107            NotificationType::XpMilestone => {
108                let milestone = self
109                    .extra
110                    .get("milestoneAchieved")
111                    .map(|s| {
112                        s.split(',')
113                            .map(|s| s.trim().to_string())
114                            .collect::<Vec<String>>()
115                            .join(", ")
116                    })
117                    .unwrap_or_else(|| "skills".to_string());
118                format!("{} hit XP milestone in {}", self.player_name, milestone)
119            }
120            NotificationType::Loot => {
121                let source = self
122                    .extra
123                    .get("source")
124                    .map(|s| s.as_str())
125                    .unwrap_or("Unknown source");
126                let items_count = self
127                    .extra
128                    .get("items")
129                    .map(|s| s.split(',').count())
130                    .unwrap_or(1);
131                format!(
132                    "{} looted {} items from {}",
133                    self.player_name, items_count, source
134                )
135            }
136            NotificationType::Slayer => {
137                let task = self
138                    .extra
139                    .get("slayerTask")
140                    .map(|s| s.as_str())
141                    .unwrap_or("Unknown task");
142                let points = self
143                    .extra
144                    .get("slayerPoints")
145                    .map(|s| s.as_str())
146                    .unwrap_or("0");
147                format!(
148                    "{} completed {} slayer task (+{} points)",
149                    self.player_name, task, points
150                )
151            }
152            NotificationType::Quest => {
153                let quest = self
154                    .extra
155                    .get("questName")
156                    .map(|s| s.as_str())
157                    .unwrap_or("Unknown quest");
158                format!("{} completed quest: {}", self.player_name, quest)
159            }
160            NotificationType::Clue => {
161                let clue_type = self
162                    .extra
163                    .get("clueType")
164                    .map(|s| s.as_str())
165                    .unwrap_or("Unknown");
166                let count = self
167                    .extra
168                    .get("numberCompleted")
169                    .map(|s| s.as_str())
170                    .unwrap_or("0");
171                format!(
172                    "{} completed {} clue (#{})",
173                    self.player_name, clue_type, count
174                )
175            }
176            NotificationType::KillCount => {
177                let boss = self
178                    .extra
179                    .get("boss")
180                    .map(|s| s.as_str())
181                    .unwrap_or("Unknown boss");
182                let count = self.extra.get("count").map(|s| s.as_str()).unwrap_or("0");
183                format!("{} defeated {} (count: {})", self.player_name, boss, count)
184            }
185            NotificationType::CombatAchievement => {
186                let task = self
187                    .extra
188                    .get("task")
189                    .map(|s| s.as_str())
190                    .unwrap_or("Unknown task");
191                let tier = self
192                    .extra
193                    .get("tier")
194                    .map(|s| s.as_str())
195                    .unwrap_or("Unknown tier");
196                format!(
197                    "{} completed {} combat task: {}",
198                    self.player_name, tier, task
199                )
200            }
201            NotificationType::AchievementDiary => {
202                let area = self
203                    .extra
204                    .get("area")
205                    .map(|s| s.as_str())
206                    .unwrap_or("Unknown area");
207                let difficulty = self
208                    .extra
209                    .get("difficulty")
210                    .map(|s| s.as_str())
211                    .unwrap_or("Unknown");
212                format!(
213                    "{} completed {} {} diary",
214                    self.player_name, difficulty, area
215                )
216            }
217            NotificationType::Pet => {
218                let pet_name = self
219                    .extra
220                    .get("petName")
221                    .map(|s| s.as_str())
222                    .unwrap_or("Unknown pet");
223                format!("{} got pet: {}", self.player_name, pet_name)
224            }
225            NotificationType::Speedrun => {
226                let quest = self
227                    .extra
228                    .get("questName")
229                    .map(|s| s.as_str())
230                    .unwrap_or("Unknown quest");
231                let time = self
232                    .extra
233                    .get("currentTime")
234                    .map(|s| s.as_str())
235                    .unwrap_or("Unknown time");
236                format!("{} speedran {} in {}", self.player_name, quest, time)
237            }
238            NotificationType::BarbarianAssaultGamble => {
239                let count = self
240                    .extra
241                    .get("gambleCount")
242                    .map(|s| s.as_str())
243                    .unwrap_or("0");
244                format!("{} reached {} high gambles", self.player_name, count)
245            }
246            NotificationType::PlayerKill => {
247                let victim = self
248                    .extra
249                    .get("victimName")
250                    .map(|s| s.as_str())
251                    .unwrap_or("Unknown");
252                format!("{} PK'd {}", self.player_name, victim)
253            }
254            NotificationType::GroupStorage => {
255                let _group = self
256                    .extra
257                    .get("groupName")
258                    .map(|s| s.as_str())
259                    .unwrap_or("Unknown group");
260                let net_value = self
261                    .extra
262                    .get("netValue")
263                    .map(|s| s.as_str())
264                    .unwrap_or("0");
265                format!(
266                    "{} group storage activity ({} gp net)",
267                    self.player_name, net_value
268                )
269            }
270            NotificationType::GrandExchange => {
271                let status = self
272                    .extra
273                    .get("status")
274                    .map(|s| s.as_str())
275                    .unwrap_or("Unknown");
276                let item_name = self
277                    .extra
278                    .get("item")
279                    .and_then(|s| s.split(',').next())
280                    .unwrap_or("Unknown item");
281                format!("{} {} on GE: {}", self.player_name, status, item_name)
282            }
283            NotificationType::Trade => {
284                let counterparty = self
285                    .extra
286                    .get("counterparty")
287                    .map(|s| s.as_str())
288                    .unwrap_or("Unknown");
289                format!("{} traded with {}", self.player_name, counterparty)
290            }
291            NotificationType::LeaguesArea => {
292                let area = self
293                    .extra
294                    .get("area")
295                    .map(|s| s.as_str())
296                    .unwrap_or("Unknown area");
297                format!("{} selected region: {}", self.player_name, area)
298            }
299            NotificationType::LeaguesMastery => {
300                let mastery = self
301                    .extra
302                    .get("masteryType")
303                    .map(|s| s.as_str())
304                    .unwrap_or("Unknown");
305                format!("{} unlocked {} mastery", self.player_name, mastery)
306            }
307            NotificationType::LeaguesRelic => {
308                let relic = self
309                    .extra
310                    .get("relic")
311                    .map(|s| s.as_str())
312                    .unwrap_or("Unknown relic");
313                let tier = self
314                    .extra
315                    .get("tier")
316                    .map(|s| s.as_str())
317                    .unwrap_or("Unknown");
318                format!(
319                    "{} unlocked Tier {} relic: {}",
320                    self.player_name, tier, relic
321                )
322            }
323            NotificationType::LeaguesTask => {
324                let task = self
325                    .extra
326                    .get("taskName")
327                    .map(|s| s.as_str())
328                    .unwrap_or("Unknown task");
329                let difficulty = self
330                    .extra
331                    .get("difficulty")
332                    .map(|s| s.as_str())
333                    .unwrap_or("Unknown");
334                format!(
335                    "{} completed {} task: {}",
336                    self.player_name, difficulty, task
337                )
338            }
339            NotificationType::Chat => {
340                let message_type = self
341                    .extra
342                    .get("type")
343                    .map(|s| s.as_str())
344                    .unwrap_or("Unknown");
345                format!("{} received {} message", self.player_name, message_type)
346            }
347            NotificationType::ExternalPlugin => {
348                let plugin = self
349                    .extra
350                    .get("sourcePlugin")
351                    .map(|s| s.as_str())
352                    .unwrap_or("Unknown plugin");
353                format!(
354                    "{} external plugin notification from {}",
355                    self.player_name, plugin
356                )
357            }
358            NotificationType::Login => {
359                let world = self
360                    .extra
361                    .get("world")
362                    .map(|s| s.as_str())
363                    .unwrap_or("Unknown");
364                format!("{} logged into World {}", self.player_name, world)
365            }
366            NotificationType::Logout => {
367                format!("{} logged out", self.player_name)
368            }
369            NotificationType::ToaUnique => {
370                let raid_level = self
371                    .extra
372                    .get("raidLevels")
373                    .map(|s| s.as_str())
374                    .unwrap_or("Unknown");
375                format!(
376                    "{} rolled purple from TOA (level {})",
377                    self.player_name, raid_level
378                )
379            }
380        }
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    #[test]
389    fn test_death_notification_message() {
390        let mut extra = HashMap::new();
391        extra.insert("valueLost".to_string(), "500".to_string());
392        extra.insert("isPvp".to_string(), "false".to_string());
393        extra.insert("killerName".to_string(), "Giant Spider".to_string());
394
395        let notification = Notification {
396            content: "PlayerName has died...".to_string(),
397            extra,
398            notification_type: NotificationType::Death,
399            player_name: "PlayerName".to_string(),
400            account_type: "NORMAL".to_string(),
401            seasonal_world: "false".to_string(),
402            dink_account_hash: "test_hash".to_string(),
403        };
404
405        let message = notification.message();
406        assert_eq!(message, "PlayerName died, lost 500 gp");
407    }
408
409    #[test]
410    fn test_death_pvp_notification_message() {
411        let mut extra = HashMap::new();
412        extra.insert("valueLost".to_string(), "1000".to_string());
413        extra.insert("isPvp".to_string(), "true".to_string());
414        extra.insert("killerName".to_string(), "PKerName".to_string());
415
416        let notification = Notification {
417            content: "PlayerName has just been PKed by PKerName for 1000 gp...".to_string(),
418            extra,
419            notification_type: NotificationType::Death,
420            player_name: "PlayerName".to_string(),
421            account_type: "NORMAL".to_string(),
422            seasonal_world: "false".to_string(),
423            dink_account_hash: "test_hash".to_string(),
424        };
425
426        let message = notification.message();
427        assert_eq!(message, "PlayerName died in PvP, lost 1000 gp");
428    }
429
430    #[test]
431    fn test_collection_notification_message() {
432        let mut extra = HashMap::new();
433        extra.insert("itemName".to_string(), "Zamorak chaps".to_string());
434        extra.insert("itemId".to_string(), "10372".to_string());
435        extra.insert("price".to_string(), "500812".to_string());
436        extra.insert("completedEntries".to_string(), "420".to_string());
437        extra.insert("totalEntries".to_string(), "1443".to_string());
438        extra.insert("currentRank".to_string(), "IRON".to_string());
439
440        let notification = Notification {
441            content: "PlayerName has added Zamorak chaps to their collection".to_string(),
442            extra,
443            notification_type: NotificationType::Collection,
444            player_name: "PlayerName".to_string(),
445            account_type: "NORMAL".to_string(),
446            seasonal_world: "false".to_string(),
447            dink_account_hash: "test_hash".to_string(),
448        };
449
450        let message = notification.message();
451        assert_eq!(
452            message,
453            "PlayerName added Zamorak chaps to collection (500812 gp)"
454        );
455    }
456
457    #[test]
458    fn test_loot_valuable_drop_notification_message() {
459        let mut extra = HashMap::new();
460        extra.insert("source".to_string(), "Tombs of Amascut".to_string());
461        extra.insert(
462            "items".to_string(),
463            "Tumeken's shadow, Osmumten's fang, Lightbearer".to_string(),
464        );
465        extra.insert("category".to_string(), "EVENT".to_string());
466        extra.insert("killCount".to_string(), "60".to_string());
467        extra.insert("rarestProbability".to_string(), "0.001".to_string());
468
469        let notification = Notification {
470            content: "PlayerName has looted: Tumeken's shadow, Osmumten's fang, Lightbearer\nFrom: Tombs of Amascut".to_string(),
471            extra,
472            notification_type: NotificationType::Loot,
473            player_name: "PlayerName".to_string(),
474            account_type: "NORMAL".to_string(),
475            seasonal_world: "false".to_string(),
476            dink_account_hash: "test_hash".to_string(),
477        };
478
479        let message = notification.message();
480        assert_eq!(message, "PlayerName looted 3 items from Tombs of Amascut");
481    }
482
483    #[test]
484    fn test_loot_single_item_notification_message() {
485        let mut extra = HashMap::new();
486        extra.insert("source".to_string(), "Giant Mole".to_string());
487        extra.insert("items".to_string(), "Mole skin".to_string());
488        extra.insert("category".to_string(), "NPC".to_string());
489        extra.insert("npcId".to_string(), "3340".to_string());
490
491        let notification = Notification {
492            content: "PlayerName has looted: Mole skin\nFrom: Giant Mole".to_string(),
493            extra,
494            notification_type: NotificationType::Loot,
495            player_name: "PlayerName".to_string(),
496            account_type: "NORMAL".to_string(),
497            seasonal_world: "false".to_string(),
498            dink_account_hash: "test_hash".to_string(),
499        };
500
501        let message = notification.message();
502        assert_eq!(message, "PlayerName looted 1 items from Giant Mole");
503    }
504
505    #[test]
506    fn test_notification_with_missing_extra_data() {
507        let extra = HashMap::new();
508
509        let notification = Notification {
510            content: "PlayerName has died...".to_string(),
511            extra,
512            notification_type: NotificationType::Death,
513            player_name: "PlayerName".to_string(),
514            account_type: "NORMAL".to_string(),
515            seasonal_world: "false".to_string(),
516            dink_account_hash: "test_hash".to_string(),
517        };
518
519        let message = notification.message();
520        assert_eq!(message, "PlayerName died, lost 0 gp");
521    }
522}