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 CONFIG
let 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 LOGGER
let 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 file
let 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 options
if 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![],
};
// Setting
let 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 options
let config = CONFIG.get_or_init(|| config);
info!("{:?}", config);
[instance]
software = 'Pleroma' # Software name
url = 'pleroma.social' # Instance URL
token = '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' # インスタンスのURL
token = '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