Hexbear is the engine that powers Chapochat. It is a customization of the Lemmy project.
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.

427 lines
13 KiB

2 years ago
  1. #[macro_use]
  2. pub extern crate lazy_static;
  3. pub extern crate actix_web;
  4. pub extern crate anyhow;
  5. pub extern crate comrak;
  6. pub extern crate lettre;
  7. pub extern crate lettre_email;
  8. pub extern crate openssl;
  9. pub extern crate rand;
  10. pub extern crate regex;
  11. pub extern crate serde_json;
  12. pub extern crate url;
  13. pub mod settings;
  14. use crate::settings::Settings;
  15. use actix_web::dev::ConnectionInfo;
  16. use chrono::{DateTime, FixedOffset, Local, NaiveDateTime};
  17. use itertools::Itertools;
  18. use lettre::{
  19. smtp::{
  20. authentication::{Credentials, Mechanism},
  21. extension::ClientId,
  22. ConnectionReuseParameters,
  23. },
  24. ClientSecurity, SmtpClient, Transport,
  25. };
  26. use lettre_email::Email;
  27. use openssl::{pkey::PKey, rsa::Rsa};
  28. use rand::{distributions::Alphanumeric, thread_rng, Rng};
  29. use regex::{Regex, RegexBuilder};
  30. use std::{
  31. io::{Error, ErrorKind},
  32. net::{IpAddr, SocketAddr},
  33. };
  34. use url::Url;
  35. pub type ConnectionId = usize;
  36. pub type PostId = i32;
  37. pub type CommunityId = i32;
  38. pub type UserId = i32;
  39. pub type IPAddr = String;
  40. #[macro_export]
  41. macro_rules! location_info {
  42. () => {
  43. format!(
  44. "None value at {}:{}, column {}",
  45. file!(),
  46. line!(),
  47. column!()
  48. )
  49. };
  50. }
  51. #[derive(Debug)]
  52. pub struct LemmyError {
  53. inner: anyhow::Error,
  54. }
  55. impl<T> From<T> for LemmyError
  56. where
  57. T: Into<anyhow::Error>,
  58. {
  59. fn from(t: T) -> Self {
  60. LemmyError { inner: t.into() }
  61. }
  62. }
  63. impl std::fmt::Display for LemmyError {
  64. fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
  65. self.inner.fmt(f)
  66. }
  67. }
  68. impl actix_web::error::ResponseError for LemmyError {}
  69. pub fn naive_from_unix(time: i64) -> NaiveDateTime {
  70. NaiveDateTime::from_timestamp(time, 0)
  71. }
  72. pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
  73. let now = Local::now();
  74. DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
  75. }
  76. pub fn is_email_regex(test: &str) -> bool {
  77. EMAIL_REGEX.is_match(test)
  78. }
  79. pub fn num_md_images(test: &str) -> i32 {
  80. MD_IMAGE_REGEX.find_iter(test).count() as i32
  81. }
  82. pub fn remove_slurs(test: &str) -> String {
  83. SLUR_REGEX.replace_all(test, "*removed*").to_string()
  84. }
  85. pub fn remove_pii(test: &str) -> String {
  86. PII_REGEX.replace_all(test, "*removed*").to_string()
  87. //TODO: add other pii filters.
  88. }
  89. pub fn pii_check(test: &str) -> Result<(), Vec<&str>> {
  90. let mut matches: Vec<&str> = PII_REGEX.find_iter(test).map(|mat| mat.as_str()).collect();
  91. matches.sort_unstable();
  92. matches.dedup();
  93. if matches.is_empty() {
  94. Ok(())
  95. } else {
  96. Err(matches)
  97. }
  98. }
  99. pub fn slur_check(test: &str) -> Result<(), Vec<&str>> {
  100. let mut matches: Vec<&str> = SLUR_REGEX.find_iter(test).map(|mat| mat.as_str()).collect();
  101. // Unique
  102. matches.sort_unstable();
  103. matches.dedup();
  104. if matches.is_empty() {
  105. Ok(())
  106. } else {
  107. Err(matches)
  108. }
  109. }
  110. pub fn slurs_vec_to_str(slurs: Vec<&str>) -> String {
  111. let start = "No slurs - ";
  112. let combined = &slurs.join(", ");
  113. [start, combined].concat()
  114. }
  115. pub fn pii_vec_to_str(pii: Vec<&str>) -> String {
  116. let start = "No personally identifiable information - ";
  117. let combined = &pii.join(", ");
  118. [start, combined].concat()
  119. }
  120. pub fn generate_random_string() -> Result<String, anyhow::Error> {
  121. Ok(String::from_utf8(thread_rng().sample_iter(&Alphanumeric).take(30).collect())?)
  122. }
  123. pub fn send_email(
  124. subject: &str,
  125. to_email: &str,
  126. to_username: &str,
  127. html: &str,
  128. ) -> Result<(), String> {
  129. let email_config = Settings::get().email.ok_or("no_email_setup")?;
  130. let email = Email::builder()
  131. .to((to_email, to_username))
  132. .from(email_config.smtp_from_address.to_owned())
  133. .subject(subject)
  134. .html(html)
  135. .build()
  136. .unwrap();
  137. let mailer = if email_config.use_tls {
  138. SmtpClient::new_simple(&email_config.smtp_server).unwrap()
  139. } else {
  140. SmtpClient::new(&email_config.smtp_server, ClientSecurity::None).unwrap()
  141. }
  142. .hello_name(ClientId::Domain(Settings::get().hostname))
  143. .smtp_utf8(true)
  144. .authentication_mechanism(Mechanism::Plain)
  145. .connection_reuse(ConnectionReuseParameters::ReuseUnlimited);
  146. let mailer = if let (Some(login), Some(password)) =
  147. (&email_config.smtp_login, &email_config.smtp_password)
  148. {
  149. mailer.credentials(Credentials::new(login.to_owned(), password.to_owned()))
  150. } else {
  151. mailer
  152. };
  153. let mut transport = mailer.transport();
  154. let result = transport.send(email.into());
  155. transport.close();
  156. match result {
  157. Ok(_) => Ok(()),
  158. Err(e) => Err(e.to_string()),
  159. }
  160. }
  161. pub fn markdown_to_html(text: &str) -> String {
  162. comrak::markdown_to_html(text, &comrak::ComrakOptions::default())
  163. }
  164. // TODO nothing is done with community / group webfingers yet, so just ignore those for now
  165. #[derive(Clone, PartialEq, Eq, Hash)]
  166. pub struct MentionData {
  167. pub name: String,
  168. pub domain: String,
  169. }
  170. impl MentionData {
  171. pub fn is_local(&self) -> bool {
  172. self.domain.is_empty() || Settings::get().hostname.eq(&self.domain)
  173. }
  174. pub fn full_name(&self) -> String {
  175. let domain = if self.domain.is_empty() {
  176. Settings::get().hostname
  177. } else {
  178. self.domain.clone()
  179. };
  180. format!("@{}@{}", &self.name, &domain)
  181. }
  182. }
  183. pub fn scrape_text_for_mentions(text: &str) -> Vec<MentionData> {
  184. let mut out: Vec<MentionData> = Vec::new();
  185. for caps in MENTIONS_REGEX.captures_iter(text) {
  186. out.push(MentionData {
  187. name: caps["name"].to_string(),
  188. domain: caps.name("domain").map_or("", |m| m.as_str()).to_string(),
  189. });
  190. }
  191. out.into_iter().unique().collect()
  192. }
  193. pub fn is_valid_username(name: &str) -> bool {
  194. VALID_USERNAME_REGEX.is_match(name)
  195. }
  196. // Can't do a regex here, reverse lookarounds not supported
  197. pub fn is_valid_preferred_username(preferred_username: &str) -> bool {
  198. !preferred_username.starts_with('@')
  199. && preferred_username.len() >= 3
  200. && preferred_username.len() <= 20
  201. }
  202. pub fn is_valid_community_name(name: &str) -> bool {
  203. VALID_COMMUNITY_NAME_REGEX.is_match(name)
  204. }
  205. pub fn is_valid_post_title(title: &str) -> bool {
  206. VALID_POST_TITLE_REGEX.is_match(title)
  207. }
  208. #[cfg(test)]
  209. mod tests {
  210. use crate::{
  211. is_valid_community_name, is_valid_post_title, is_valid_preferred_username, is_valid_username,
  212. remove_slurs, scrape_text_for_mentions, slur_check, slurs_vec_to_str,
  213. };
  214. #[test]
  215. fn test_mentions_regex() {
  216. let text = "Just read a great blog post by [@[email protected]](/u/test). And another by [email protected] . Another [@[email protected]:8540](/u/fish)";
  217. let mentions = scrape_text_for_mentions(text);
  218. assert_eq!(mentions[0].name, "tedu".to_string());
  219. assert_eq!(mentions[0].domain, "honk.teduangst.com".to_string());
  220. assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string());
  221. }
  222. #[test]
  223. fn test_valid_register_username() {
  224. assert!(is_valid_username("Hello_98"));
  225. assert!(is_valid_username("ten"));
  226. assert!(!is_valid_username("Hello-98"));
  227. assert!(!is_valid_username("a"));
  228. assert!(!is_valid_username(""));
  229. }
  230. #[test]
  231. fn test_valid_preferred_username() {
  232. assert!(is_valid_preferred_username("hello @there"));
  233. assert!(!is_valid_preferred_username("@hello there"));
  234. }
  235. #[test]
  236. fn test_valid_community_name() {
  237. assert!(is_valid_community_name("example"));
  238. assert!(is_valid_community_name("example_community"));
  239. assert!(!is_valid_community_name("Example"));
  240. assert!(!is_valid_community_name("Ex"));
  241. assert!(!is_valid_community_name(""));
  242. }
  243. #[test]
  244. fn test_valid_post_title() {
  245. assert!(is_valid_post_title("Post Title"));
  246. assert!(is_valid_post_title(" POST TITLE 😃😃😃😃😃"));
  247. assert!(!is_valid_post_title("\n \n \n \n ")); // tabs/spaces/newlines
  248. }
  249. #[test]
  250. fn test_slur_filter() {
  251. let test =
  252. "coons test dindu ladyboy tranny retardeds. Capitalized Niggerz. This is a bunch of other safe text.";
  253. let slur_free = "No slurs here";
  254. assert_eq!(
  255. remove_slurs(&test),
  256. "*removed* test *removed* *removed* *removed* *removed*. Capitalized *removed*. This is a bunch of other safe text."
  257. .to_string()
  258. );
  259. let has_slurs_vec = vec![
  260. "Niggerz",
  261. "coons",
  262. "dindu",
  263. "ladyboy",
  264. "retardeds",
  265. "tranny",
  266. ];
  267. let has_slurs_err_str = "No slurs - Niggerz, coons, dindu, ladyboy, retardeds, tranny";
  268. assert_eq!(slur_check(test), Err(has_slurs_vec));
  269. assert_eq!(slur_check(slur_free), Ok(()));
  270. if let Err(slur_vec) = slur_check(test) {
  271. assert_eq!(&slurs_vec_to_str(slur_vec), has_slurs_err_str);
  272. }
  273. }
  274. // These helped with testing
  275. // #[test]
  276. // fn test_send_email() {
  277. // let result = send_email("not a subject", "[email protected]", "ur user", "<h1>HI there</h1>");
  278. // assert!(result.is_ok());
  279. // }
  280. }
  281. lazy_static! {
  282. static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-][email protected][a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
  283. static ref PII_REGEX: Regex = Regex::new(r"(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}").unwrap();
  284. static ref MD_IMAGE_REGEX: Regex = Regex::new(r"!\[[\s\S]*?\]\([\s\S]*?\)").unwrap();
  285. static ref SLUR_REGEX: Regex = RegexBuilder::new(r"([^\p{P}\s]*?(f(a|4)g(got|g)?){1,}|maricos?|(n(i|1)gg((a|er)?(s|z)?)){1,}|(nig){2,}|dindu(s?){1,}|mudslime?s?|(k(i|y|1|l)kes?){1,}|(mongoloids?){1,}|(towel\s*heads?){1,}|\bspi(c|k)s?\b|(spi(c|k)s){2,}|\bchinks?|(ch(i|1)nks?){1,}|(n(i|1|l)glets?){1,}|be(a|@|4)ners?|\bjaps?\b|(japs){2,}|\bcoons?\b|(coons?){2,}|(jungle\s*bunn(y|ies?)){1,}|(jigg?aboo?s?){1,}|\bpakis?\b|(pakis?){2,}|(rag\s*heads?){1,}|(gooks?){1,}|(cuntboy?){1,}|(feminazis?){1,}|\btr(a|@)nn?(y|(i|1|l)es?|ers?)|(tr(a|@)nn?(y|(i|1|l)es?|ers?)){1,}|(l(a|@|4)dyboy(s?)){1,}|ret(a|4)rd(s|ed)?|(hymie){1,}|(porch\s?monkey){1,}|(zh(y|i)d(ovka)?){1,}|\bching\s?chong\b|(ching\s?chong\s?){1,}|(chong\s?ching\s?){1,}|(hefem(a|@|4)le){1,}|(dickgirl){1,}|(hermie){1,}|(\babb?o\b)|(abb?o){2,}|(boong){1,}|(cunts?){1,}|(shem(en|an|ales?))|(chinam(an|en))|(redsk(i|l)ns?)|(slanteyes?)|(degen(s|era(tes?|cy)))|(curry\s?munchers?))").case_insensitive(true).build().unwrap();
  286. static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
  287. // TODO keep this old one, it didn't work with port well tho
  288. // static ref MENTIONS_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)").unwrap();
  289. static ref MENTIONS_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)(@(?P<domain>[a-zA-Z0-9._:-]+))?").unwrap();
  290. static ref VALID_USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]{3,20}$").unwrap();
  291. static ref VALID_COMMUNITY_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_]{3,20}$").unwrap();
  292. static ref VALID_POST_TITLE_REGEX: Regex = Regex::new(r".*\S.*").unwrap();
  293. pub static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!(
  294. "^group:([a-z0-9_]{{3, 20}})@{}$",
  295. Settings::get().hostname
  296. ))
  297. .unwrap();
  298. pub static ref WEBFINGER_USER_REGEX: Regex = Regex::new(&format!(
  299. "^acct:([a-z0-9_]{{3, 20}})@{}$",
  300. Settings::get().hostname
  301. ))
  302. .unwrap();
  303. pub static ref CACHE_CONTROL_IMAGE_REGEX: Regex =
  304. Regex::new("^(image/.+)$").unwrap();
  305. pub static ref CACHE_CONTROL_APPLICATION_REGEX: Regex =
  306. Regex::new("^(text/.+)|(application/javascript)$").unwrap();
  307. }
  308. pub struct Keypair {
  309. pub private_key: String,
  310. pub public_key: String,
  311. }
  312. /// Generate the asymmetric keypair for ActivityPub HTTP signatures.
  313. pub fn generate_actor_keypair() -> Result<Keypair, Error> {
  314. let rsa = Rsa::generate(2048)?;
  315. let pkey = PKey::from_rsa(rsa)?;
  316. let public_key = pkey.public_key_to_pem()?;
  317. let private_key = pkey.private_key_to_pem_pkcs8()?;
  318. let key_to_string = |key| match String::from_utf8(key) {
  319. Ok(s) => Ok(s),
  320. Err(e) => Err(Error::new(
  321. ErrorKind::Other,
  322. format!("Failed converting key to string: {}", e),
  323. )),
  324. };
  325. Ok(Keypair {
  326. private_key: key_to_string(private_key)?,
  327. public_key: key_to_string(public_key)?,
  328. })
  329. }
  330. pub enum EndpointType {
  331. Community,
  332. User,
  333. Post,
  334. Comment,
  335. PrivateMessage,
  336. }
  337. pub fn get_apub_protocol_string() -> &'static str {
  338. if Settings::get().federation.tls_enabled {
  339. "https"
  340. } else {
  341. "http"
  342. }
  343. }
  344. /// Generates the ActivityPub ID for a given object type and ID.
  345. pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
  346. let point = match endpoint_type {
  347. EndpointType::Community => "c",
  348. EndpointType::User => "u",
  349. EndpointType::Post => "post",
  350. EndpointType::Comment => "comment",
  351. EndpointType::PrivateMessage => "private_message",
  352. };
  353. Url::parse(&format!(
  354. "{}://{}/{}/{}",
  355. get_apub_protocol_string(),
  356. Settings::get().hostname,
  357. point,
  358. name
  359. ))
  360. .unwrap()
  361. }
  362. pub fn get_ip(conn_info: &ConnectionInfo) -> String {
  363. conn_info
  364. .realip_remote_addr()
  365. .and_then(|real_ip| {
  366. real_ip
  367. .parse::<IpAddr>()
  368. .or_else(|_| real_ip.parse::<SocketAddr>().map(|sa| sa.ip()))
  369. .ok()
  370. })
  371. .map_or("0.0.0.0".to_string(), |ip_addr| ip_addr.to_string())
  372. }