MT5UYGH2CAJLYJ2C2FTT3N3YUHPWHTP26DNANVN62GLWELC73JWAC TCO7NV2XR7ZZT4LBEIZTLHC3NTCK7NOIDOS4WY3GVI2NRVYDSAYAC PNR77HFOQCCM3RMKGO42LWUDQ5OZLYSJ4OXCVAAXN4VOPTRLKINAC MKQ6KPQC76OY6AJ4ICDGPOZUEH244PKAHDQ7JUWHVJ7WNIOSF56QC 574I6K5HTOQLE5AJ54D7E7NE2R46ESVRXGSD2IN64VZS6TVNTUEAC AYYVER3MF24JA7RQOWPCQK7IXFDFQFGRQO6LC6O32FOLJFTMVJPQC UZPHCOWN2JISRBGCFGADNEFIHEXEOL4P3GO4QCDVR5GVTVSJPJCQC SLH34DY5K6YTQCYAZXA7QQF3UCZQ4LGIZR4KZFQIYFT4NRNANM4AC YMW2RSKMODRBEHP5ZGBQQ3ZKZQCKLGUT3F6MXSI73AK446LRNFKAC KCUGRTO54BZLPQJPUZNJGKM3TZOR2K3Z7SZN5SR7IZQAEOYWL4WQC 4GKYK7JDUKAOG2D2EXGNV2SMRGL3FOTG3ADLKQKM6476GATQ4BAAC BWHQDWSQESMWN2VLS5TVYNFMCQB2PEZJWMNPBCUUA47KLBPKNI6AC RTRMLNTU262CQDXRF5YALSHUT2XXCOG6NPKFTF767PJ4Q2BYQ72QC JLRU6XLFRAA7IVLF2GRU6UF5HJIKE3MCN566TIG7UQGIY7MZSCMQC 2KXFHTS3VTQWWMOUVONFJ7GYSX4DCHQ6RQ2LGV6TH5R6X7MH7RNAC VVETADYZOWCU4IYTJVLKZR2X5HWH6FUKIVLWQNQIAUG4LKTONL6QC 3ZV7EZ26UNQJJM2LW22M7YONNLIQDQVMYJSKQZ5B7SXEMQ4IKHOAC HJACI3FHGMBIF4AEVNNI2FBAZUYLNTIQUQXVXCAZZA7DX3FWBJSQC YUGNHUTWBANE6XQ3ZTFI5NG6YDP4Z3LHC7K3VCEJN5A4VH46FCDAC CHLYUISQ6NYFVYU5Z5KUQVPALXJBL5IWK342UD5KF44YGXWOAL5AC H7H2NJQAHUY4A5DF2IQLVB5ESJUWYOITYVCDPABMXQ5BRMAZJ7AQC match logger.logger_type {LoggerType::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(())}LoggerType::Discord => {let Some(webhook) = logger.logger_url.clone() else {return Err("* Please set Webhook URL to LOGGER_URL.");};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!("{}\n=====\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(())}
if logger.stdout.enable {let message = message.clone();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);}if logger.discord.enable {let message = message.clone();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!("{}\n=====\nLink: <{}>", message.plain_content.unwrap_or(html2text(&message.content)), message.uri),})} else {ureq::json!({"content": message.uri,})};if ureq::post(&logger.discord.webhook).send_json(json).is_err() {return Err("* Something happend executing Webhook.");
UCSStr::from_str(message.content.as_str()).lower_case().hiragana().to_string()
(UCSStr::from_str(message.content.as_str()).lower_case().hiragana().to_string(),filter.include.clone().into_iter().map(|x| UCSStr::from_str(&x).lower_case().hiragana().to_string()).collect(),filter.exclude.into_iter().map(|x| UCSStr::from_str(&x).lower_case().hiragana().to_string()).collect(),)
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()
if filter.use_regex {if !include.is_empty()&& include.into_iter().map(|x| Regex::new(&x).unwrap()) // We can use unwrap() here as we have already checked they're all valid regex..filter(|x| x.is_match(&content)).collect::<Vec<Regex>>().is_empty(){return false;}if !exclude
{return false;}if !filter.exclude_regex.clone().into_iter().filter(|x| x.is_match(&content)).collect::<Vec<Regex>>().is_empty(){return false;
{return false;}} else {if !include.is_empty()&& include.into_iter().filter(|x| content.contains(x)).collect::<Vec<String>>().is_empty(){return false;}if !exclude.into_iter().filter(|x| content.contains(x)).collect::<Vec<String>>().is_empty(){return false;}
impl Config {pub async fn new(software_name: String,instance_url: String,token: String,) -> Result<Config, String> {let software = match software_name.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));}};if token.is_empty() {eprintln!("* ACCESS_TOKEN is not set. Generating...");crate::streamer::oath(software, instance_url.as_str()).await;return Err(String::new());
impl Default for TimelineSetting {fn default() -> Self {Self {home: true,local: false,public: false,
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![];
impl Default for FilterSetting {fn default() -> Self {Self {include: vec![],exclude: vec![],user_include: vec![],user_exclude: vec![],case_sensitive: true,use_regex: false,
#[derive(Debug)]pub struct Logger {pub logger_type: LoggerType,pub logger_url: Option<String>,
#[derive(Debug, Default, Deserialize)]#[serde(default)]pub struct LoggerSetting {pub stdout: Stdout,pub discord: Discord,
impl Logger {pub fn new(logger_name: String, logger_url: Option<String>) -> Logger {let logger_type = match logger_name.to_lowercase().as_str() {"stdout" => LoggerType::Stdout,"discord" => LoggerType::Discord,_ => {eprintln!("* LOGGER is not set. Falling back to stdout.");LoggerType::Stdout}};Logger {logger_type,logger_url,}}
#[derive(Debug, Deserialize)]#[serde(default)]pub struct Stdout {pub enable: bool,
// Parse CONFIGlet Ok(software) = dotenvy::var("SOFTWARE") else {return Err("* SOFTWARE is not set; Please specify SOFTWARE to listen to.".to_string());};let Ok(instance_url) = dotenvy::var("INSTANCE_URL") else {return Err("* Please specify INSTANCE_URL to listen to.".to_string());};let token = dotenvy::var("ACCESS_TOKEN").unwrap_or_default();// Parse LOGGERlet logging_method = dotenvy::var("LOGGER").unwrap_or_default();let logging_url = dotenvy::var("LOGGER_URL").ok();// Parse FILTER// TODO?: Parse in new() function instead of here?let is_regex: bool = if let Ok(regex) = dotenvy::var("USE_REGEX") {if let Ok(lb) = regex.parse::<LexicalBool>() {*lb.deref()
// Read options from config.toml filelet Ok(toml) = fs::read_to_string("config.toml") else {return Err(if Path::new(".env").exists() {"* Obsolete .env config file found. Please migrate to config.toml."
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 config: Config = match toml::from_str(&toml) {Ok(c) => c,Err(e) => return Err(format!("* Failed to load config.toml: {:?}", e.message())),
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()
// Validate optionsif config.timelines.home && config.instance.token.is_none() {eprintln!("* timelines.home is set, but instance.token is empty. Generating a token...");crate::streamer::oath(config.instance.software, &config.instance.url).await;return Err(String::new());}if config.filter.use_regex {for exp in config.filter.include.iter() {if Regex::new(exp).is_err() {return Err("* filter.include contains a invalid regex.".to_string());
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()
for exp in config.filter.exclude.iter() {if Regex::new(exp).is_err() {return Err("* filter.exclude contains a invalid regex.".to_string());
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![],};// Settinglet config = Config::new(software, instance_url, token).await?;let _config = CONFIG.get_or_init(|| config);let filter = Filter::new(include,exclude,user_include,user_exclude,is_case_sensitive,is_regex,)?;let _filter = FILTER.get_or_init(|| filter);
}if config.logger.discord.enable && config.logger.discord.webhook.is_empty() {return Err("* logger.discord.enable is set, but logger.discord.webhook is empty.".to_string(),);}
let logger = Logger::new(logging_method, logging_url);let _logger = LOGGER.get_or_init(|| logger);let _timelines = TIMELINES.get_or_init(|| timelines);info!("{:?}", _config);info!("{:?}", _filter);info!("{:?}", _logger);info!("{:?}", _timelines);
// Store optionslet config = CONFIG.get_or_init(|| config);info!("{:?}", config);
[instance]software = 'Pleroma' # Software nameurl = 'pleroma.social' # Instance URLtoken = 'xxxxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxx' # Access token (Optional when timelines.home is false)[timelines]home = true # Whether to watch Home Timeline (Default: true)local = false # Whether to watch Local Timeline (a.k.a. Public Timeline in Pleroma) (Default: false)public = false # Whether to watch Public Timeline (a.k.a. Known Network in Pleroma, or Federated Timeline in Mastodon) (Default: false)[filter]case_sensitive = true # (Default: true)use_regex = false # (Default: false)include = [] # Words to include (Everything when empty) (Default: empty)exclude = [] # Words to exclude (Default: empty)user_include = [] # Authors to include (Everyone when empty) (Default: empty)user_exclude = [] # Authors to exclude (Default: empty)[logger.stdout]enable = true # Whether to write hit log to Standard Output (Default: true)[logger.discord]enable = false # Whether to Write hit log to Discord (Default: false)webhook = 'https://discord.com/api/webhooks/0000000000000000000/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxx-xxxxxxxxxxxxxxxxxx' # Webhook URL
`.env`ファイルを作って以下の情報を書きこめばOKです。環境変数でも問題ありませんが衝突防止の点から`.env`が好ましいでしょう。`USER_*`でのユーザーの書式は`@hoge@example.tld`ではなく`hoge@example.tld`なので注意してください。なお、ローカルのユーザーの場合は`@example.tld`すら不要で`hoge`のみです。
`config.toml`ファイルを作って以下の情報を書きこめばOKです。`user_*`でのユーザーの書式は`@hoge@example.tld`ではなく`hoge@example.tld`なので注意してください。なお、ローカルのユーザーの場合は`@example.tld`すら不要で`hoge`のみです。
```SOFTWARE=ソフトウェア名(例:Pleroma)INSTANCE_URL=インスタンスのURL(例:pleroma.social)ACCESS_TOKEN=アクセストークン(わからなければ空にしておくと生成してくれます、設定は手動)LOGGER=ヒットした投稿の出力先(現状stdoutとDiscordにのみ対応)LOGGER_URL=DiscordのWebhook URL(LOGGERがDiscordの場合のみ)TIMELINES=監視対象にするタイムライン(Home、PublicまたはLocalから複数選択可)CASE_SENSITIVE=大文字/小文字、ひらがな/カタカナを区別する(true/false、デフォルト:true)USE_REGEX=有効時、INCLUDEとEXCLUDEは正規表現として扱われます(true/false、デフォルト:false)INCLUDE=ヒットさせたい単語(カンマ区切り、空の場合全てにヒットします)EXCLUDE=ヒットさせたくない単語(カンマ区切り)USER_INCLUDE=ヒットさせたいユーザー(カンマ区切り、空の場合全ユーザーにヒットします)USER_EXCLUDE=ヒットさせたくないユーザー(カンマ区切り、自分の投稿を除外したいときなど)
```toml[instance]software = 'Pleroma' # ソフトウェア名url = 'pleroma.social' # インスタンスのURLtoken = 'xxxxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxx' # アクセストークン(timelines.homeがtrueの時のみ必須、わからなければ空にしておくと生成してくれます、設定は手動)[timelines]home = true # ホームタイムラインを監視するかどうか (デフォルト: true)local = false # ローカルタイムラインを監視するかどうか (デフォルト: false)public = false # グローバルタイムラインを監視するかどうか (デフォルト: false)[filter]case_sensitive = true # include, excludeで大文字/小文字、ひらがな/カタカナを区別するかどうか (デフォルト: true)use_regex = false # include, excludeを正規表現として扱うかどうか (デフォルト: false)include = [] # ヒットさせたい単語(空の場合全てにヒットします)exclude = [] # 除外したい単語user_include = [] # ヒットさせたいユーザー(空の場合全ユーザーにヒットします)user_exclude = [] # ヒットさせたくないユーザー(自分の投稿を除外したいときなど)[logger.stdout]enable = true # ヒットログを標準出力に書き込むかどうか (デフォルト: true)[logger.discord]enable = false # ヒットログをDiscordに送信するかどうか (デフォルト: false)webhook = 'https://discord.com/api/webhooks/0000000000000000000/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxx-xxxxxxxxxxxxxxxxxx' # WebhookのURL
name = "native-tls"version = "0.2.11"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"dependencies = ["lazy_static","libc","log","openssl","openssl-probe","openssl-sys","schannel","security-framework","security-framework-sys","tempfile",][[package]]
name = "rustls"version = "0.22.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "fe6b63262c9fcac8659abfaa96cac103d28166d3ff3eaf8f412e19f3ae9e5a48"dependencies = ["log","ring","rustls-pki-types","rustls-webpki 0.102.1","subtle","zeroize",][[package]]
name = "tempfile"version = "3.8.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"dependencies = ["cfg-if","fastrand","redox_syscall","rustix","windows-sys 0.48.0",][[package]]
][[package]]name = "toml"version = "0.8.8"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"dependencies = ["serde","serde_spanned","toml_datetime","toml_edit",][[package]]name = "toml_datetime"version = "0.6.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"dependencies = ["serde",][[package]]name = "toml_edit"version = "0.21.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"dependencies = ["indexmap","serde","serde_spanned","toml_datetime","winnow",
[[package]]name = "zeroize"version = "1.7.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
config.toml