Productivity bot for Discord
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

245 lines
8.5KB

  1. extern crate serenity;
  2. use serenity::{
  3. model::channel::Message,
  4. prelude::*,
  5. utils::MessageBuilder,
  6. };
  7. extern crate serde_json;
  8. extern crate regex;
  9. use regex::{Regex, RegexBuilder, Match};
  10. use core::option::Option;
  11. use std::collections::HashMap;
  12. use std::string::String;
  13. use std::fs::{File, OpenOptions};
  14. use std::io::BufReader;
  15. use std::vec::Vec;
  16. use crate::traits::Command;
  17. const MACRO_PATH: &str = "macros.json";
  18. pub struct CmdMacro {
  19. format: Regex,
  20. help_format: Regex,
  21. macros: HashMap<u64,HashMap<String, String>>,
  22. }
  23. impl Command for CmdMacro {
  24. fn execute(& mut self, ctx: &Context, msg: &Message) {
  25. let id: u64;
  26. match msg.guild_id {
  27. Some(g) => id = *g.as_u64(),
  28. None => id = *msg.channel_id.as_u64(),
  29. }
  30. if let Some(parsed) = self.format.captures(&msg.content) {
  31. if let Some(op) = parsed.get(1) {
  32. let mut response = MessageBuilder::new();
  33. if op.as_str().to_string().to_lowercase() == "add" {
  34. if self.add(&id, parsed.get(2), parsed.get(3)) {
  35. response.push("Successfully added macro.");
  36. self.save_json();
  37. } else {
  38. response.push("Missing macro argument or macro already exists.");
  39. }
  40. } else if op.as_str().to_string().to_lowercase() == "remove" {
  41. if self.remove(&id, parsed.get(2)) {
  42. response.push("Successfully removed macro.");
  43. self.save_json();
  44. } else {
  45. response.push("Macro does not exist.");
  46. }
  47. } else if op.as_str().to_string().to_lowercase() == "list" {
  48. response.push(self.list(&id));
  49. } else if op.as_str().to_string().to_lowercase() == "help" {
  50. self.help(ctx, msg);
  51. return;
  52. } else {
  53. let mut macro_msg = self.get(&id, &op.as_str().to_string());
  54. if msg.content.chars().count() > op.end() {
  55. // there's some parameters for the macro
  56. macro_msg = parse_macro(&macro_msg, &msg.content[op.end()+1..]);
  57. }
  58. response.push(&macro_msg);
  59. }
  60. if let Err(why) = msg.channel_id.say(&ctx.http, &response.build()) {
  61. println!("Failed to send macro message {:?}", why);
  62. }
  63. return;
  64. }
  65. }
  66. let response = MessageBuilder::new()
  67. .push("Unexpected failure.")
  68. .build();
  69. if let Err(why) = msg.channel_id.say(&ctx.http, &response) {
  70. println!("Failed to send macro failure message {:?}", why);
  71. }
  72. }
  73. fn valid(&self, _ctx: &Context, msg: &Message) -> bool {
  74. return self.format.is_match(&msg.content) && !msg.author.bot;
  75. }
  76. fn help(&self, ctx: &Context, msg: &Message) {
  77. let mut response = MessageBuilder::new();
  78. response.push("**Use text shortcuts to generate macro text.**\n!macro (short text)\n!macro list\n!macro add (shortcut) (macro)\n!macro remove (shortcut)".to_string());
  79. if let Err(why) = msg.channel_id.say(&ctx.http, &response.build()) {
  80. println!("Failed to send macro help message {:?}", why);
  81. }
  82. }
  83. fn valid_help(&self, _ctx: &Context, msg: &Message) -> bool {
  84. return self.help_format.is_match(&msg.content);
  85. }
  86. }
  87. impl CmdMacro {
  88. pub fn new() -> CmdMacro{
  89. // load macro map from JSON
  90. let file: File = OpenOptions::new()
  91. .read(true)
  92. .write(true)
  93. .create(true)
  94. .open(MACRO_PATH)
  95. .unwrap();
  96. let reader = BufReader::new(file);
  97. let macros: HashMap<u64, HashMap<String, String>> = serde_json::from_reader(reader).unwrap_or(HashMap::<u64, HashMap<String, String>>::new());
  98. return CmdMacro {
  99. format:
  100. RegexBuilder::new(r#"^!macro\s+([A-Za-z0-9]+|"[^\s]+"|'[^\s]+')(?:\s+([A-Za-z0-9]+|"[^\s]+"|'[^\s]+'))?(?:\s+(.+))?"#)
  101. .multi_line(true)
  102. .dot_matches_new_line(true)
  103. .case_insensitive(true)
  104. .build()
  105. .unwrap(),
  106. help_format:
  107. RegexBuilder::new(r#"^!help\s*macro$"#)
  108. .case_insensitive(true)
  109. .build()
  110. .unwrap(),
  111. macros: macros,
  112. };
  113. }
  114. fn add<'t>(&mut self, id: &u64, key: Option<Match<'t>>, value: Option<Match<'t>>) -> bool {
  115. let map: &mut HashMap<String, String>;
  116. match self.macros.get_mut(id) {
  117. Some(m) => map = m,
  118. None => {
  119. self.macros.insert(*id, HashMap::new());
  120. map = self.macros.get_mut(id).unwrap();
  121. },
  122. }
  123. let rk; let rv;
  124. if let Some(k) = key {
  125. rk = k.as_str().trim_matches('\"').trim_matches('\'').to_string();
  126. if map.get(&rk).is_some() {
  127. return false;
  128. }
  129. } else {return false;}
  130. if let Some(v) = value {
  131. rv = v.as_str().to_string();
  132. } else {return false;}
  133. map.insert(rk, rv);
  134. return true;
  135. }
  136. fn remove<'t>(&mut self, id: &u64, key: Option<Match<'t>>) -> bool {
  137. let map: &mut HashMap<String, String>;
  138. match self.macros.get_mut(id) {
  139. Some(m) => map = m,
  140. None => {
  141. self.macros.insert(*id, HashMap::new());
  142. map = self.macros.get_mut(id).unwrap();
  143. },
  144. }
  145. match key {
  146. Some(k) => return map.remove(&k.as_str().trim_matches('\"').trim_matches('\'').to_string()).is_some(),
  147. None => return false,
  148. }
  149. }
  150. fn get(&mut self, id: &u64, key: &String) -> String {
  151. let map: &HashMap<String, String>;
  152. match self.macros.get_mut(id) {
  153. Some(m) => map = m,
  154. None => {
  155. self.macros.insert(*id, HashMap::new());
  156. map = self.macros.get(id).unwrap();
  157. },
  158. }
  159. if let Some(s) = map.get(&key.to_owned()) {
  160. return s.to_string();
  161. }
  162. return "Invalid macro".to_string();
  163. }
  164. fn list(&mut self, id: &u64) -> String {
  165. let map: &HashMap<String, String>;
  166. match self.macros.get_mut(id) {
  167. Some(m) => map = m,
  168. None => {
  169. self.macros.insert(*id, HashMap::new());
  170. map = self.macros.get(id).unwrap();
  171. },
  172. }
  173. let mut resp = "Available macros:\n`".to_owned();
  174. for k in map.keys() {
  175. resp += k;
  176. resp += ", ";
  177. }
  178. if resp == "Available macros:\n`" {
  179. resp += "(None)`";
  180. } else {
  181. resp = resp[0..resp.chars().count()-2].to_string()+"`";
  182. }
  183. return resp.to_string();
  184. }
  185. fn save_json(&self) {
  186. let file = OpenOptions::new()
  187. .write(true)
  188. .append(false)
  189. .truncate(true)
  190. .open(MACRO_PATH)
  191. .unwrap();
  192. match file.sync_all() {
  193. Err(why) => println!("File sync failed ({:?})", why),
  194. Ok(_) => (),
  195. }
  196. //let writer = BufWriter::new(file.unwrap());
  197. match serde_json::to_writer_pretty(file, &self.macros) {
  198. Err(why) => println!("Macro saving failed ({:?})", why),
  199. Ok(_) => (),
  200. }
  201. }
  202. }
  203. fn parse_macro<'a, 'b>(macro_pre: &'a str, macro_params: &'b str) -> String {
  204. if macro_params.chars().count() == 0 {
  205. return macro_pre.to_string();
  206. }
  207. let re = RegexBuilder::new(r#"^\s*([A-Za-z0-9]+|".*"|'.*')"#)
  208. .multi_line(true)
  209. .dot_matches_new_line(true)
  210. .case_insensitive(true)
  211. .build()
  212. .unwrap();
  213. let mut parsed_params = Vec::<String>::new();
  214. let mut params_str = macro_params.to_string();
  215. // move over macro_params like a tape until out of matches
  216. while let Some(re_capture) = re.captures(&params_str) {
  217. let re_match = re_capture.get(1).unwrap();
  218. parsed_params.push(re_match.as_str().trim_matches('\"').trim_matches('\'').to_string());
  219. params_str = params_str[re_match.end()..].to_string();
  220. }
  221. let mut macro_post = macro_pre.to_string();
  222. // replace {#} format in macro with given param
  223. for i in 0..parsed_params.len() {
  224. macro_post = macro_post.replace(&("{".to_owned()+&i.to_string()+"}"), parsed_params.get(i).unwrap());
  225. }
  226. return macro_post;
  227. }