use std::collections::BTreeMap;
use regex::Regex;
type TwtxtErr<T> = std::result::Result<T, ErrorKind>;
#[derive(Debug)]
pub enum ErrorKind {
Metadata,
Keyword,
Regex,
}
impl std::fmt::Display for ErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let kind = match self {
ErrorKind::Metadata => "Metadata",
ErrorKind::Keyword => "Keyword",
ErrorKind::Regex => "Regex",
};
write!(f, "{}", kind)
}
}
impl std::error::Error for ErrorKind {}
pub fn metadata(twtxt: &str, keyword: &str) -> TwtxtErr<String> {
if !twtxt.contains("== Metadata ==") && !twtxt.contains(keyword) {
return Err(ErrorKind::Metadata);
}
let regex_string = format!("{} = (.*)", keyword);
let regex = if let Ok(val) = Regex::new(®ex_string) {
val
} else {
return Err(ErrorKind::Regex);
};
let matched = if let Some(val) = regex.captures(twtxt) {
val
} else {
return Err(ErrorKind::Keyword);
};
let keyword_match = if let Some(val) = matched.get(1) {
val.as_str()
} else {
return Err(ErrorKind::Keyword);
};
Ok(keyword_match.to_string())
}
pub fn statuses(twtxt: &str) -> Option<BTreeMap<String, String>> {
let mut map = BTreeMap::new();
let lines = twtxt.split('\n').collect::<Vec<&str>>();
lines.iter().for_each(|line| {
if line.starts_with('#') || line.len() < 2 || !line.contains('\t') {
return;
}
let status = line.split('\t').collect::<Vec<&str>>();
let datestamp = status[0];
map.insert(datestamp.into(), status[1].into());
});
if map.is_empty() {
return None;
}
Some(map)
}
pub fn mentions(twtxt: &str) -> Option<BTreeMap<String, String>> {
let statuses = if let Some(val) = statuses(&twtxt) {
val
} else {
return None;
};
let mut map = BTreeMap::new();
statuses.iter().for_each(|(k, v)| {
if !v.contains("@<") {
return;
}
let regex = Regex::new(r"[@<].*[>]+").unwrap();
let out = if let Some(val) = regex.captures(v) {
match val.get(0) {
Some(n) => n.as_str(),
_ => return,
}
} else {
return;
};
let mention = out.to_string();
map.insert(k.to_string(), mention);
});
if map.is_empty() {
return None;
}
Some(map)
}
pub fn mention_to_nickname(line: &str) -> Option<String> {
let regex = Regex::new(r"[@<].*[>]+").unwrap();
let mention = if let Some(val) = regex.captures(line) {
match val.get(0) {
Some(n) => n.as_str(),
_ => return None,
}
} else {
return None;
};
let mention_trimmed = mention[2..mention.len() - 1].to_string();
let mention_split = mention_trimmed.split(' ').collect::<Vec<&str>>();
Some(mention_split[0].into())
}
pub fn tags(twtxt: &str) -> Option<BTreeMap<String, String>> {
let statuses = if let Some(val) = statuses(&twtxt) {
val
} else {
return None;
};
let mut map = BTreeMap::new();
statuses.iter().for_each(|(k, v)| {
if !v.contains('#') {
return;
}
let regex = Regex::new(r"(^|\s)#[^\s]+").unwrap();
let tag: Vec<(String, String)> = regex
.find_iter(v)
.map(|ding| (k.clone(), ding.as_str().to_string()))
.collect();
let tags: Vec<(String, String)> = tag
.iter()
.map(|(k, v)| {
let v = v
.chars()
.map(|c| {
if c.is_whitespace() {
return "".into();
}
c.to_string()
})
.collect::<String>();
(k.clone(), v)
})
.collect();
let mut tag_group = String::new();
tags.iter().for_each(|(_, v)| {
tag_group.push_str(v);
tag_group.push_str(" ");
});
map.insert(k.to_string(), tag_group[..tag_group.len() - 1].to_string());
});
if map.is_empty() {
return None;
}
Some(map)
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_URL: &str = "https://gbmor.dev/twtxt.txt";
#[test]
fn turn_mentions_to_nick() {
let twtxt = "2019.09.09\tHey @<gbmor https://gbmor.dev/twtxt.txt>!";
let mention = mention_to_nickname(twtxt).unwrap();
assert_eq!("gbmor", mention);
}
#[test]
fn get_tags() {
let tag_map = tags("test\t#test").unwrap();
assert!("#test" == &tag_map["test"]);
let tag_map = tags("test\tsome other #test here").unwrap();
assert!("#test" == &tag_map["test"]);
let tag_map = tags("test\tsome other #test").unwrap();
assert!("#test" == &tag_map["test"]);
let tag_map = tags("test\tsome #test goes #here").unwrap();
assert!("#test #here" == &tag_map["test"]);
}
#[test]
#[should_panic]
fn bad_regex() {
metadata("SOME DATA", "<#*#@(&$(%)@$)>").unwrap();
}
#[test]
#[should_panic]
fn no_matches() {
metadata("SOME = DATA", "nick").unwrap();
}
#[test]
fn get_mentions() {
let twtxt = crate::pull_twtxt(TEST_URL).unwrap();
let mention_map = mentions(&twtxt).unwrap();
assert!(mention_map.len() > 1);
}
#[test]
fn get_username() {
let res = crate::pull_twtxt(TEST_URL).unwrap();
let user = metadata(&res, "nick").unwrap();
assert_eq!("gbmor", user);
}
#[test]
fn get_url() {
let res = crate::pull_twtxt(TEST_URL).unwrap();
let url = metadata(&res, "url").unwrap();
assert_eq!(TEST_URL, url);
}
#[test]
fn get_status_map() {
let twtxt = crate::pull_twtxt(TEST_URL).unwrap();
let res = statuses(&twtxt).unwrap();
assert!(res.len() > 1);
}
#[test]
#[should_panic]
fn parse_bad_twtxt() {
metadata("SOMETHING GOES HERE", "url").unwrap();
}
#[test]
#[should_panic]
fn get_bad_statuses() {
statuses("").unwrap();
}
}