about summary refs log tree commit diff
path: root/fun/paroxysm/src/keyword.rs
diff options
context:
space:
mode:
Diffstat (limited to 'fun/paroxysm/src/keyword.rs')
-rw-r--r--fun/paroxysm/src/keyword.rs220
1 files changed, 220 insertions, 0 deletions
diff --git a/fun/paroxysm/src/keyword.rs b/fun/paroxysm/src/keyword.rs
new file mode 100644
index 000000000000..1b2b6ce592a0
--- /dev/null
+++ b/fun/paroxysm/src/keyword.rs
@@ -0,0 +1,220 @@
+use crate::models::{Entry, Keyword, NewEntry, NewKeyword};
+use diesel::pg::PgConnection;
+use diesel::prelude::*;
+use failure::format_err;
+use failure::Error;
+use std::borrow::Cow;
+
+/// Maximum number of times we'll follow a `see: ` pointer.
+const RECURSION_LIMIT: usize = 5;
+
+pub struct KeywordDetails {
+    pub keyword: Keyword,
+    pub entries: Vec<Entry>,
+}
+
+impl KeywordDetails {
+    pub fn learn(&mut self, nick: &str, text: &str, dbc: &PgConnection) -> Result<usize, Error> {
+        let now = ::chrono::Utc::now().naive_utc();
+        let ins = NewEntry {
+            keyword_id: self.keyword.id,
+            idx: (self.entries.len() + 1) as _,
+            text,
+            creation_ts: now,
+            created_by: nick,
+        };
+        let new = {
+            use crate::schema::entries;
+            ::diesel::insert_into(entries::table)
+                .values(ins)
+                .get_result(dbc)?
+        };
+        self.entries.push(new);
+        Ok(self.entries.len())
+    }
+
+    pub fn process_moves(&mut self, moves: &[(i32, i32)], dbc: &PgConnection) -> Result<(), Error> {
+        for (oid, new_idx) in moves {
+            {
+                use crate::schema::entries::dsl::*;
+                ::diesel::update(entries.filter(id.eq(oid)))
+                    .set(idx.eq(new_idx))
+                    .execute(dbc)?;
+            }
+        }
+        self.entries = Self::get_entries(self.keyword.id, dbc)?;
+        Ok(())
+    }
+
+    pub fn swap(&mut self, idx_a: usize, idx_b: usize, dbc: &PgConnection) -> Result<(), Error> {
+        let mut moves = vec![];
+        for ent in self.entries.iter() {
+            if ent.idx == idx_a as i32 {
+                moves.push((ent.id, idx_b as i32));
+            }
+            if ent.idx == idx_b as i32 {
+                moves.push((ent.id, idx_a as i32));
+            }
+        }
+        if moves.len() != 2 {
+            Err(format_err!("Invalid swap operation."))?;
+        }
+        self.process_moves(&moves, dbc)?;
+        Ok(())
+    }
+
+    pub fn update(&mut self, idx: usize, val: &str, dbc: &PgConnection) -> Result<(), Error> {
+        let ent = self
+            .entries
+            .get_mut(idx.saturating_sub(1))
+            .ok_or(format_err!("No such element to update."))?;
+        {
+            use crate::schema::entries::dsl::*;
+            ::diesel::update(entries.filter(id.eq(ent.id)))
+                .set(text.eq(val))
+                .execute(dbc)?;
+        }
+        ent.text = val.to_string();
+        Ok(())
+    }
+
+    pub fn delete(&mut self, idx: usize, dbc: &PgConnection) -> Result<(), Error> {
+        // step 1: delete the element
+        {
+            let ent = self
+                .entries
+                .get(idx.saturating_sub(1))
+                .ok_or(format_err!("No such element to delete."))?;
+            {
+                use crate::schema::entries::dsl::*;
+                ::diesel::delete(entries.filter(id.eq(ent.id))).execute(dbc)?;
+            }
+        }
+        // step 2: move all the elements in front of it back one
+        let mut moves = vec![];
+        for ent in self.entries.iter() {
+            if idx > ent.idx as _ {
+                moves.push((ent.id, ent.idx.saturating_sub(1)));
+            }
+        }
+        self.process_moves(&moves, dbc)?;
+        Ok(())
+    }
+
+    pub fn add_zwsp_to_name(name: &str) -> Option<String> {
+        let second_index = name.char_indices().nth(1).map(|(i, _)| i)?;
+        let (start, end) = name.split_at(second_index);
+        Some(format!("{}​{}", start, end))
+    }
+
+    pub fn format_entry(&self, idx: usize) -> Option<String> {
+        self.format_entry_colours(idx, true)
+    }
+
+    pub fn format_entry_colours(&self, idx: usize, with_colours: bool) -> Option<String> {
+        if let Some(ent) = self.entries.get(idx.saturating_sub(1)) {
+            let gen_clr = if self.keyword.chan == "*" && with_colours {
+                "\x0307"
+            } else {
+                ""
+            };
+            let zwsp_name = Self::add_zwsp_to_name(&self.keyword.name)
+                .unwrap_or_else(|| self.keyword.name.clone());
+            Some(format!(
+                "{}{}{name}{}[{idx}/{total}]{}: {text} {}[{date}]{}",
+                if with_colours { "\x02" } else { "" },
+                gen_clr,
+                if with_colours { "\x0f\x0315" } else { "" },
+                if with_colours { "\x0f" } else { "" },
+                if with_colours { "\x0f\x0314" } else { "" },
+                if with_colours { "\x0f" } else { "" },
+                name = zwsp_name,
+                idx = idx,
+                total = self.entries.len(),
+                text = ent.text,
+                date = ent.creation_ts.date()
+            ))
+        } else {
+            None
+        }
+    }
+
+    pub fn get_or_create(word: &str, c: &str, dbc: &PgConnection) -> Result<Self, Error> {
+        if let Some(ret) = Self::get(word, c, dbc)? {
+            Ok(ret)
+        } else {
+            Ok(Self::create(word, c, dbc)?)
+        }
+    }
+
+    pub fn create(word: &str, c: &str, dbc: &PgConnection) -> Result<Self, Error> {
+        let val = NewKeyword {
+            name: word,
+            chan: c,
+        };
+        let ret: Keyword = {
+            use crate::schema::keywords;
+            ::diesel::insert_into(keywords::table)
+                .values(val)
+                .get_result(dbc)?
+        };
+        Ok(KeywordDetails {
+            keyword: ret,
+            entries: vec![],
+        })
+    }
+
+    fn get_entries(kid: i32, dbc: &PgConnection) -> Result<Vec<Entry>, Error> {
+        let entries: Vec<Entry> = {
+            use crate::schema::entries::dsl::*;
+            entries
+                .filter(keyword_id.eq(kid))
+                .order_by(idx.asc())
+                .load(dbc)?
+        };
+        Ok(entries)
+    }
+
+    fn get_inner<'a, T: Into<Cow<'a, str>>>(
+        word: T,
+        c: &str,
+        dbc: &PgConnection,
+        recursion_count: usize,
+    ) -> Result<Option<Self>, Error> {
+        let word = word.into();
+        let keyword: Option<Keyword> = {
+            use crate::schema::keywords::dsl::*;
+            keywords
+                .filter(name.ilike(word).and(chan.eq(c).or(chan.eq("*"))))
+                .first(dbc)
+                .optional()?
+        };
+        if let Some(k) = keyword {
+            let entries = Self::get_entries(k.id, dbc)?;
+            if let Some(e0) = entries.get(0) {
+                if e0.text.starts_with("see: ") {
+                    if recursion_count > RECURSION_LIMIT {
+                        // Oh dear.
+                        Err(format_err!("Halt. You're having a bit too much fun."))?
+                    }
+                    let new_word = e0.text.replace("see: ", "");
+                    return Self::get_inner(new_word, c, dbc, recursion_count + 1);
+                }
+            }
+            Ok(Some(KeywordDetails {
+                keyword: k,
+                entries,
+            }))
+        } else {
+            Ok(None)
+        }
+    }
+
+    pub fn get<'a, T: Into<Cow<'a, str>>>(
+        word: T,
+        c: &str,
+        dbc: &PgConnection,
+    ) -> Result<Option<Self>, Error> {
+        Self::get_inner(word, c, dbc, 0)
+    }
+}