KCUGRTO54BZLPQJPUZNJGKM3TZOR2K3Z7SZN5SR7IZQAEOYWL4WQC RUZW6T62JOBNDOJPHP5BGN4DIISXEFG554WHWWSJSHBL3AWUXB6AC EISCAJ3CFVMGPWZQGFI7RZ2A7J4QJ6MN2IRZINV77QMHSESCMO3AC H7H2NJQAHUY4A5DF2IQLVB5ESJUWYOITYVCDPABMXQ5BRMAZJ7AQC 3BHGSDHUR4TLFVXR74U6BA4HZLLHDAWY3JABHZHBC6RXCZZUUVNQC VVETADYZOWCU4IYTJVLKZR2X5HWH6FUKIVLWQNQIAUG4LKTONL6QC 73ETI54RGG3MRHZ6SCFMBVJ6FU7WPY2HYEYNDFW4KK4K6CIRE4CAC YMW2RSKMODRBEHP5ZGBQQ3ZKZQCKLGUT3F6MXSI73AK446LRNFKAC 2KXFHTS3VTQWWMOUVONFJ7GYSX4DCHQ6RQ2LGV6TH5R6X7MH7RNAC RTRMLNTU262CQDXRF5YALSHUT2XXCOG6NPKFTF767PJ4Q2BYQ72QC HFHOMADVK6SIRUFUUZOQC3XGNSOETNRELZ3RDMZFY3RIBVAYCKIAC JLRU6XLFRAA7IVLF2GRU6UF5HJIKE3MCN566TIG7UQGIY7MZSCMQC Q2NINZCW6KJVDFJTS6NQ7DTSSWPPU4DG34GGZLEJPK4U6KQAJYWAC 3ZV7EZ26UNQJJM2LW22M7YONNLIQDQVMYJSKQZ5B7SXEMQ4IKHOAC pub async fn streaming(sns: megalodon::SNS,url: String,token: String,output_dest: String,logging_url: Option<String>,filter: crate::logger::Filter,tl: Option<ExtraTimeline>,) {let client = generator(sns, format!("https://{}", url), Some(token), None);if tl.is_none() {
pub async fn streaming(tl: Timeline) {let config = crate::config::CONFIG.get().unwrap();let client = generator(config.software.clone(),format!("https://{}", config.instance_url),Some(config.token.clone()),None,);if matches!(tl, Timeline::Home) {
Some(ExtraTimeline::Public) => client.public_streaming(format!("wss://{}", url)),Some(ExtraTimeline::Local) => client.local_streaming(format!("wss://{}", url)),None => client.user_streaming(format!("wss://{}", url)),
Timeline::Public => client.public_streaming(format!("wss://{}", config.instance_url)),Timeline::Local => client.local_streaming(format!("wss://{}", config.instance_url)),Timeline::Home => client.user_streaming(format!("wss://{}", config.instance_url)),
use std::ops::Deref;use kanaria::string::UCSStr;use lexical_bool::LexicalBool;use log::info;use megalodon::SNS;use streamer::ExtraTimeline;
use streamer::Timeline;
// Read options from .env filelet sns = match dotenvy::var("SOFTWARE") {Err(_) => {eprintln!("* SOFTWARE is not set; Please specify SOFTWARE to listen to.");return;}Ok(software) => match software.to_lowercase().as_str() {"pleroma" => SNS::Pleroma,"mastodon" => {eprintln!("* Software other than Pleroma is not tested!");SNS::Mastodon}"firefish" => {eprintln!("* Software other than Pleroma is not tested!");SNS::Firefish}"friendica" => {eprintln!("* Software other than Pleroma is not tested!");SNS::Friendica}unsupported => {eprintln!("* Software {} is unknown!", unsupported);return;}},};let extra_tl = match dotenvy::var("EXTRA_TIMELINE") {Err(_) => None,Ok(tl_type) => match tl_type.to_lowercase().as_str() {"public" => Some(ExtraTimeline::Public),"local" => Some(ExtraTimeline::Local),_ => {eprintln!("* EXTRA_TIMELINE is invalid. Valid values are Public or Local.");return;}},};let Ok(url) = dotenvy::var("INSTANCE_URL") else {eprintln!("* Please specify INSTANCE_URL to listen to.");
if let Err(e) = config::load_config().await {eprintln!("{}", e);
let Ok(token) = dotenvy::var("ACCESS_TOKEN") else {eprintln!("* ACCESS_TOKEN is not set. Generating...");streamer::oath(sns, url.as_str()).await;return;};
let timelines = config::TIMELINES.get().unwrap();
let logging_method = match dotenvy::var("LOGGER") {Ok(l) => l,Err(_) => {eprintln!("* LOGGER is not set. Falling back to stdout.");"stdout".to_string()}
// Home Timelinelet home_tl_handle = if timelines.home {tokio::spawn(streamer::streaming(Timeline::Home))} else {tokio::spawn(async {})
let logging_url = dotenvy::var("LOGGER_URL").ok();let is_regex: bool = if let Ok(regex) = dotenvy::var("USE_REGEX") {if let Ok(lb) = regex.parse::<LexicalBool>() {*lb.deref()} else {eprintln!("* The value of USE_REGEX doesn't match expected pattern!");return;}
// Local Timelinelet local_tl_handle = if timelines.local {tokio::spawn(streamer::streaming(Timeline::Local))
let is_case_sensitive: bool = !is_regex&& if let Ok(case_sensitive) = dotenvy::var("CASE_SENSITIVE") {if let Ok(lb) = case_sensitive.parse::<LexicalBool>() {*lb.deref()} else {eprintln!("* The value of CASE_SENSITIVE doesn't match expected pattern!");return;}} else {true};let include: Vec<String> = match dotenvy::var("INCLUDE") {Ok(include) => {if is_case_sensitive {include.split(',').map(|x| x.to_string()).collect()} else {include.split(',').map(|x| UCSStr::from_str(x).lower_case().hiragana().to_string()).collect()}}Err(_) => vec![],};let exclude: Vec<String> = match dotenvy::var("EXCLUDE") {Ok(exclude) => {if is_case_sensitive {exclude.split(',').map(|x| x.to_string()).collect()} else {exclude.split(',').map(|x| UCSStr::from_str(x).lower_case().hiragana().to_string()).collect()}}Err(_) => vec![],};let user_include: Vec<String> = match dotenvy::var("USER_INCLUDE") {Ok(include) => include.split(',').map(|x| x.to_string()).collect(),Err(_) => vec![],};let user_exclude: Vec<String> = match dotenvy::var("USER_EXCLUDE") {Ok(exclude) => exclude.split(',').map(|x| x.to_string()).collect(),Err(_) => vec![],};let Ok(filter) = logger::Filter::new(extra_tl.clone(),include,exclude,user_include,user_exclude,is_case_sensitive,is_regex,) else {eprintln!("* Invalid regex syntax!");return;};info!("{:?}", token);info!("{:?}", filter);// Extra Timelinelet extra_tl_handle = if let Some(tl) = extra_tl {tokio::spawn(streamer::streaming(sns.clone(),url.clone(),token.clone(),logging_method.clone(),logging_url.clone(),filter.clone(),Some(tl),))
// Public Timelinelet public_tl_handle = if timelines.local {tokio::spawn(streamer::streaming(Timeline::Public))
pub fn log(self, message: megalodon::entities::status::Status) -> Result<(), &'static str> {match self.dest.to_lowercase().as_str() {"stdout" => {log::debug!("{:?}", message);println!("==========");println!("Name: {} ({})",message.account.display_name, message.account.acct);println!("Content:");println!("{}",message.plain_content.unwrap_or(html2text(&message.content)));println!("URL: {}", message.uri);Ok(())}"discord" => {let Some(webhook) = self.url else {
match logger.logger_type.to_lowercase().as_str() {"stdout" => {log::debug!("{:?}", message);println!("==========");println!("Name: {} ({})",message.account.display_name, message.account.acct);println!("Content:");println!("{}",message.plain_content.unwrap_or(html2text(&message.content)));println!("URL: {}", message.uri);Ok(())}"discord" => {let Some(webhook) = logger.logger_url.clone() else {
if ureq::post(&webhook).send_json(ureq::json!({//"username": message.account.display_name,//"avatar_url": message.account.avatar,//"content": message.plain_content.unwrap_or(html2text(&message.content)),"content": message.uri,})).is_err(){Err("* Something happend executing Webhook.")} else {Ok(())}
let json = if message.visibility == StatusVisibility::Private|| message.visibility == StatusVisibility::Direct{ureq::json!({"username": message.account.display_name,"avatar_url": message.account.avatar,"content": format!("{}\nLink: {}", message.plain_content.unwrap_or(html2text(&message.content)), message.uri),})} else {ureq::json!({"content": message.uri,})};if ureq::post(&webhook).send_json(json).is_err() {Err("* Something happend executing Webhook.")} else {Ok(())
}#[derive(Clone, Debug)]pub struct Filter {extra_tl: Option<ExtraTimeline>,include: Vec<String>,exclude: Vec<String>,include_regex: Vec<Regex>,exclude_regex: Vec<Regex>,user_include: Vec<String>,user_exclude: Vec<String>,is_case_sensitive: bool,
impl Filter {pub fn new(extra_tl: Option<ExtraTimeline>,include: Vec<String>,exclude: Vec<String>,user_include: Vec<String>,user_exclude: Vec<String>,is_case_sensitive: bool,is_regex: bool,) -> Result<Filter, &'static str> {let include_plain: Vec<String>;let exclude_plain: Vec<String>;let mut include_regex: Vec<Regex>;let mut exclude_regex: Vec<Regex>;if is_regex {include_plain = vec![];exclude_plain = vec![];include_regex = vec![];exclude_regex = vec![];for i in include {let Ok(re) = Regex::new(i.as_str()) else {return Err("Invalid Regex");};include_regex.push(re);}for i in exclude {let Ok(re) = Regex::new(i.as_str()) else {return Err("Invalid Regex");};exclude_regex.push(re);}} else {include_plain = include;exclude_plain = exclude;include_regex = vec![];exclude_regex = vec![];}Ok(Filter {extra_tl,include: include_plain,exclude: exclude_plain,include_regex,exclude_regex,user_include,user_exclude,is_case_sensitive,})}}pub fn egosa(message: megalodon::entities::status::Status,settings: Filter,tl: Option<ExtraTimeline>,) -> bool {// Remove Repeats (a.k.a. Boosts)if message.reblogged.unwrap_or_default() {return false;}// Remove dupicates from Home Timelineif tl.is_none() && message.visibility == megalodon::entities::StatusVisibility::Public {match settings.extra_tl {Some(ExtraTimeline::Public) => return false,Some(ExtraTimeline::Local) => return message.account.acct.contains('@'),_ => {}};}if !settings.user_include.is_empty() && !settings.user_include.contains(&message.account.acct) {return false;}if settings.user_exclude.contains(&message.account.acct) {return false;}let content = if settings.is_case_sensitive {message.content} else {UCSStr::from_str(message.content.as_str()).lower_case().hiragana().to_string()};if !settings.include.is_empty()&& settings.include.into_iter().filter(|x| content.contains(x)).collect::<Vec<String>>().is_empty(){return false;}if !settings.exclude.into_iter().filter(|x| content.contains(x)).collect::<Vec<String>>().is_empty(){return false;}if !settings.include_regex.is_empty()&& settings.include_regex.into_iter().filter(|x| x.is_match(&content)).collect::<Vec<Regex>>().is_empty(){return false;}if !settings.exclude_regex.into_iter().filter(|x| x.is_match(&content)).collect::<Vec<Regex>>().is_empty(){return false;}true}
use crate::config::TimelineSetting;use crate::streamer::Timeline;use kanaria::string::UCSStr;use megalodon::entities::StatusVisibility;use regex::Regex;pub fn filter(message: megalodon::entities::status::Status, tl: Timeline) -> bool {let filter = crate::config::FILTER.get().unwrap();let timeline_setting = crate::config::TIMELINES.get().unwrap();// Remove Repeats (a.k.a. Boosts)if message.reblogged.unwrap_or_default() {return false;}// Remove dupicates from Home Timelineif matches!(tl, Timeline::Home) && message.visibility == StatusVisibility::Public {match timeline_setting {TimelineSetting { public: true, .. } => return false,TimelineSetting { local: true, .. } => return message.account.acct.contains('@'),_ => {}};}if !filter.user_include.is_empty() && !filter.user_include.contains(&message.account.acct) {return false;}if filter.user_exclude.contains(&message.account.acct) {return false;}let content = if filter.is_case_sensitive {message.content} else {UCSStr::from_str(message.content.as_str()).lower_case().hiragana().to_string()};if !filter.include.is_empty()&& filter.include.clone().into_iter().filter(|x| content.contains(x)).collect::<Vec<String>>().is_empty(){return false;}if !filter.exclude.clone().into_iter().filter(|x| content.contains(x)).collect::<Vec<String>>().is_empty(){return false;}if !filter.include_regex.is_empty()&& filter.include_regex.clone().into_iter().filter(|x| x.is_match(&content)).collect::<Vec<Regex>>().is_empty(){return false;}if !filter.exclude_regex.clone().into_iter().filter(|x| x.is_match(&content)).collect::<Vec<Regex>>().is_empty(){return false;}true}
use kanaria::string::UCSStr;use lexical_bool::LexicalBool;use megalodon::SNS;use regex::Regex;use std::ops::Deref;use std::sync::OnceLock;pub struct Config {pub software: SNS,pub instance_url: String,pub token: String,}impl Config {pub fn new(software: SNS, instance_url: String, token: String) -> Self {Config {software,instance_url,token,}}}#[derive(Clone, Debug)]pub struct Filter {pub include: Vec<String>,pub exclude: Vec<String>,pub include_regex: Vec<Regex>,pub exclude_regex: Vec<Regex>,pub user_include: Vec<String>,pub user_exclude: Vec<String>,pub is_case_sensitive: bool,}impl Filter {pub fn new(include: Vec<String>,exclude: Vec<String>,user_include: Vec<String>,user_exclude: Vec<String>,is_case_sensitive: bool,is_regex: bool,) -> Result<Filter, &'static str> {let include_plain: Vec<String>;let exclude_plain: Vec<String>;let mut include_regex: Vec<Regex>;let mut exclude_regex: Vec<Regex>;if is_regex {include_plain = vec![];exclude_plain = vec![];include_regex = vec![];exclude_regex = vec![];for i in include {let Ok(re) = Regex::new(i.as_str()) else {return Err("Invalid Regex");};include_regex.push(re);}for i in exclude {let Ok(re) = Regex::new(i.as_str()) else {return Err("Invalid Regex");};exclude_regex.push(re);}} else {include_plain = include;exclude_plain = exclude;include_regex = vec![];exclude_regex = vec![];}Ok(Filter {include: include_plain,exclude: exclude_plain,include_regex,exclude_regex,user_include,user_exclude,is_case_sensitive,})}}pub struct Logger {pub logger_type: String,pub logger_url: Option<String>,}impl Logger {pub fn new(logger_type: String, logger_url: Option<String>) -> Self {Logger {logger_type,logger_url,}}}pub struct TimelineSetting {pub home: bool,pub local: bool,pub public: bool,}impl TimelineSetting {pub fn new(home: bool, local: bool, public: bool) -> Self {TimelineSetting {home,local,public,}}}pub static CONFIG: OnceLock<Config> = OnceLock::new();pub static FILTER: OnceLock<Filter> = OnceLock::new();pub static LOGGER: OnceLock<Logger> = OnceLock::new();pub static TIMELINES: OnceLock<TimelineSetting> = OnceLock::new();// Read options from .env filepub async fn load_config() -> Result<(), String> {let software = match dotenvy::var("SOFTWARE") {Err(_) => {return Err("* SOFTWARE is not set; Please specify SOFTWARE to listen to.".to_string());}Ok(software) => match software.to_lowercase().as_str() {"pleroma" => SNS::Pleroma,"mastodon" => {eprintln!("* Software other than Pleroma is not tested!");SNS::Mastodon}"firefish" => {eprintln!("* Software other than Pleroma is not tested!");SNS::Firefish}"friendica" => {eprintln!("* Software other than Pleroma is not tested!");SNS::Friendica}unsupported => {return Err(format!("* Software {} is unknown!", unsupported));}},};let Ok(instance_url) = dotenvy::var("INSTANCE_URL") else {return Err("* Please specify INSTANCE_URL to listen to.".to_string());};let Ok(token) = dotenvy::var("ACCESS_TOKEN") else {eprintln!("* ACCESS_TOKEN is not set. Generating...");crate::streamer::oath(software, instance_url.as_str()).await;return Err(String::new());};let logging_method = match dotenvy::var("LOGGER") {Ok(l) => l,Err(_) => {eprintln!("* LOGGER is not set. Falling back to stdout.");"stdout".to_string()}};let logging_url = dotenvy::var("LOGGER_URL").ok();let is_regex: bool = if let Ok(regex) = dotenvy::var("USE_REGEX") {if let Ok(lb) = regex.parse::<LexicalBool>() {*lb.deref()} else {return Err("* The value of USE_REGEX doesn't match expected pattern!".to_string());}} else {false};let is_case_sensitive: bool = !is_regex&& if let Ok(case_sensitive) = dotenvy::var("CASE_SENSITIVE") {if let Ok(lb) = case_sensitive.parse::<LexicalBool>() {*lb.deref()} else {return Err("* the value of case_sensitive doesn't match expected pattern!".to_string(),);}} else {true};let timelines = match dotenvy::var("TIMELINES") {Ok(tl) => {let mut home = false;let mut local = false;let mut public = false;for tl in tl.split(',') {match tl.to_lowercase().as_str() {"home" => home = true,"local" => local = true,"public" => public = true,invalid => {eprintln!("* Timeline type {} is unkown!", invalid);}}}if !(home || local || public) {eprintln!("* No valid timeline type found. Falling back to Home...");TimelineSetting::new(true, false, false)} else {TimelineSetting::new(home, local, public)}}Err(_) => {eprintln!("* No timelines specified. Falling back to Home...");TimelineSetting::new(true, false, false)}};let include: Vec<String> = match dotenvy::var("INCLUDE") {Ok(include) => {if is_case_sensitive {include.split(',').map(|x| x.to_string()).collect()} else {include.split(',').map(|x| UCSStr::from_str(x).lower_case().hiragana().to_string()).collect()}}Err(_) => vec![],};let exclude: Vec<String> = match dotenvy::var("EXCLUDE") {Ok(exclude) => {if is_case_sensitive {exclude.split(',').map(|x| x.to_string()).collect()} else {exclude.split(',').map(|x| UCSStr::from_str(x).lower_case().hiragana().to_string()).collect()}}Err(_) => vec![],};let user_include: Vec<String> = match dotenvy::var("USER_INCLUDE") {Ok(include) => include.split(',').map(|x| x.to_string()).collect(),Err(_) => vec![],};let user_exclude: Vec<String> = match dotenvy::var("USER_EXCLUDE") {Ok(exclude) => exclude.split(',').map(|x| x.to_string()).collect(),Err(_) => vec![],};let Ok(filter) = Filter::new(include,exclude,user_include,user_exclude,is_case_sensitive,is_regex,) else {return Err("* invalid regex syntax!".to_string());};if CONFIG.set(Config::new(software, instance_url, token)).is_err(){return Err("* Failed to load config file!".to_string());}if FILTER.set(filter).is_err() {return Err("* Failed to load config file!".to_string());}if LOGGER.set(Logger::new(logging_method, logging_url)).is_err(){return Err("* Failed to load config file!".to_string());}if TIMELINES.set(timelines).is_err() {return Err("* Failed to load config file!".to_string());}Ok(())}
2. Publicが指定されている場合、HTLに加えてGTL(「すべてのネットワーク」(Pleroma)、「連合タイムライン」(Mastodon))も監視します。(GTLが使用できるサーバーのみ)3. Localが指定されている場合、HTLに加えてLTL(「公開タイムライン」(Pleroma))も監視します。(LTLが使用できるサーバーのみ)