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 file
let 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 Timeline
let 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 Timeline
let 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 Timeline
let 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 Timeline
let 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 Timeline
if 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 Timeline
if 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 file
pub 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が使用できるサーバーのみ)