Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
131a59b3b3 | ||
|
|
1f08706a44 | ||
|
|
e52e692889 | ||
|
|
b4619f25e7 | ||
|
|
5389031d8c | ||
|
|
e77c8157ae
|
||
|
|
cb6724baab
|
||
|
|
cbd3c0a575 | ||
|
|
e1ac2777f3 | ||
|
|
900f499932 | ||
|
|
ebcf12e46c
|
||
|
|
0d37425c32
|
+26
-13
@@ -1,27 +1,40 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v0.4.2
|
## v0.4.6 [2025-06-15]
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
## v0.4.5 [2022-11-02]
|
||||||
|
- Ignore #nobot in bio if the user is also a member
|
||||||
|
|
||||||
|
## v0.4.4 [2021-11-02]
|
||||||
|
- Fix some failing tests
|
||||||
|
- Lowercase the domain when normalizing an account
|
||||||
|
|
||||||
|
## v0.4.3 [2021-10-12]
|
||||||
|
- Fix hashtag not working in a mention
|
||||||
|
|
||||||
|
## v0.4.2 [2021-10-12]
|
||||||
- Fix URL fragment detected as hashtag
|
- Fix URL fragment detected as hashtag
|
||||||
|
|
||||||
## v0.4.1
|
## v0.4.1 [2021-10-10]
|
||||||
- "en" translation fixes
|
- "en" translation fixes
|
||||||
- add `messages.json`
|
- add `messages.json`
|
||||||
- Config files are now parsed as JSON5 = comments are allowed
|
- Config files are now parsed as JSON5 = comments are allowed
|
||||||
|
|
||||||
## v0.4.0
|
## v0.4.0 [2021-10-10]
|
||||||
- Add a translation system using the `locales` folder
|
- Add a translation system using the `locales` folder
|
||||||
- Add more trace logging
|
- Add more trace logging
|
||||||
|
|
||||||
## v0.3.0
|
## v0.3.0 [2021-10-05]
|
||||||
- Changed config/storage format to directory-based, removed shared config mutex
|
- Changed config/storage format to directory-based, removed shared config mutex
|
||||||
- Made more options configurable (timeouts, catch-up limits, etc)
|
- Made more options configurable (timeouts, catch-up limits, etc)
|
||||||
- Changed default log level to Debug, added `-q` to reduce it (opposite of `-v`)
|
- Changed default log level to Debug, added `-q` to reduce it (opposite of `-v`)
|
||||||
- Code cleaning
|
- Code cleaning
|
||||||
|
|
||||||
## v0.2.8
|
## v0.2.8 [2021-08-30]
|
||||||
- fix error processing statuses when a misskey poll has infinite run time
|
- fix error processing statuses when a misskey poll has infinite run time
|
||||||
|
|
||||||
## v0.2.7
|
## v0.2.7 [2021-08-30]
|
||||||
- Fix some wrong responses to admin commands
|
- Fix some wrong responses to admin commands
|
||||||
- Remove automatic announcements from some admin commands
|
- Remove automatic announcements from some admin commands
|
||||||
- Send no reply to unauthorized admin commands
|
- Send no reply to unauthorized admin commands
|
||||||
@@ -29,34 +42,34 @@
|
|||||||
- Add `/optout` and `/optin`
|
- Add `/optout` and `/optin`
|
||||||
- Add `#nobot` checking when using the `/boost` command
|
- Add `#nobot` checking when using the `/boost` command
|
||||||
|
|
||||||
## v0.2.6
|
## v0.2.6 [2021-08-28]
|
||||||
- Allow boosting group hashtags when they are in a reply, except when it is private/DM
|
- Allow boosting group hashtags when they are in a reply, except when it is private/DM
|
||||||
or contains actionable commands
|
or contains actionable commands
|
||||||
- `/follow` and `/unfollow` are now aliases to `/add` and `/remove` (for users and tags)
|
- `/follow` and `/unfollow` are now aliases to `/add` and `/remove` (for users and tags)
|
||||||
- Add workaround for pleroma markdown processor eating trailing hashtags
|
- Add workaround for pleroma markdown processor eating trailing hashtags
|
||||||
- Command replies are now always DM again so we don't spam timelines
|
- Command replies are now always DM again so we don't spam timelines
|
||||||
|
|
||||||
## v0.2.5
|
## v0.2.5 [2021-08-27]
|
||||||
- Add `/undo` command
|
- Add `/undo` command
|
||||||
- Fix users joining via follow not marked as members
|
- Fix users joining via follow not marked as members
|
||||||
|
|
||||||
## v0.2.4
|
## v0.2.4 [2021-08-27]
|
||||||
- make account lookup try harder
|
- make account lookup try harder
|
||||||
|
|
||||||
## v0.2.3
|
## v0.2.3 [2021-08-27]
|
||||||
- `/add user` will now try to follow even if already a member
|
- `/add user` will now try to follow even if already a member
|
||||||
|
|
||||||
## v0.2.2
|
## v0.2.2 [2021-08-27]
|
||||||
- All hashtags, server names and handles are now lowercased = case-insensitive
|
- All hashtags, server names and handles are now lowercased = case-insensitive
|
||||||
- Prevent the `-a` flag overwriting existing group in the config
|
- Prevent the `-a` flag overwriting existing group in the config
|
||||||
- Update the help text
|
- Update the help text
|
||||||
- `/i` now works in hashtag posts
|
- `/i` now works in hashtag posts
|
||||||
- `/add user` and `/remove user` now correctly follow/unfollow
|
- `/add user` and `/remove user` now correctly follow/unfollow
|
||||||
|
|
||||||
## v0.2.1
|
## v0.2.1 [2021-08-27]
|
||||||
- More reliable websocket reconnect, workaround for pleroma socket going silent
|
- More reliable websocket reconnect, workaround for pleroma socket going silent
|
||||||
|
|
||||||
## v0.2.0
|
## v0.2.0 [2021-08-27]
|
||||||
- Add hashtag boosting and back-follow/unfollow
|
- Add hashtag boosting and back-follow/unfollow
|
||||||
- Add hashtag commands
|
- Add hashtag commands
|
||||||
- Code reorganization
|
- Code reorganization
|
||||||
|
|||||||
Generated
+1339
-1406
File diff suppressed because it is too large
Load Diff
+10
-14
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fedigroups"
|
name = "fedigroups"
|
||||||
version = "0.4.2"
|
version = "0.4.6"
|
||||||
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
|
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
publish = false
|
publish = false
|
||||||
@@ -9,24 +9,20 @@ build = "build.rs"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
#elefren = { path = "../elefren22-fork" }
|
#elefren = { path = "../elefren-fork" }
|
||||||
elefren = { git = "https://git.ondrovo.com/MightyPork/elefren-fork.git", rev = "b10e5935ae32f4756b19e9ca58b78a5382f865d1" }
|
elefren = { git = "https://git.ondrovo.com/MightyPork/elefren-fork.git", rev = "8aa32c060eddb825e46f777661a93ff6f745659d" }
|
||||||
|
|
||||||
env_logger = "0.9.0"
|
env_logger = "0.11.8"
|
||||||
|
|
||||||
log = "0.4.14"
|
log = "0.4.27"
|
||||||
serde = "1"
|
serde = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
clap = "2.33.0"
|
clap = "3.2"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-stream = "0.1.7"
|
thiserror = "2.0.12"
|
||||||
thiserror = "1.0.26"
|
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
voca_rs = "1.13.0"
|
voca_rs = "1.15.2"
|
||||||
regex = "1.5.4"
|
regex = "1.11.1"
|
||||||
once_cell = "1.8.0"
|
once_cell = "1.21.3"
|
||||||
json5 = "0.4.1"
|
json5 = "0.4.1"
|
||||||
|
|
||||||
native-tls = "0.2.8"
|
|
||||||
websocket = "0.26.2"
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::process::Command;
|
|||||||
use std::str;
|
use std::str;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let desc_c = Command::new("git").args(&["describe", "--all", "--long"]).output().unwrap();
|
let desc_c = Command::new("git").args(["describe", "--all", "--long"]).output().unwrap();
|
||||||
|
|
||||||
let desc = unsafe { str::from_utf8_unchecked(&desc_c.stdout) };
|
let desc = unsafe { str::from_utf8_unchecked(&desc_c.stdout) };
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"ping_response": "pong, toto je fedigroups verze {version}"
|
|
||||||
}
|
|
||||||
+9
-1
@@ -8,6 +8,8 @@ pub enum GroupError {
|
|||||||
UserIsBanned,
|
UserIsBanned,
|
||||||
#[error("User opted out from the group")]
|
#[error("User opted out from the group")]
|
||||||
UserOptedOut,
|
UserOptedOut,
|
||||||
|
#[error("User opted out from the group using #nobot")]
|
||||||
|
UserOptedOutNobot,
|
||||||
#[error("Server could not be banned because there are admin users on it")]
|
#[error("Server could not be banned because there are admin users on it")]
|
||||||
AdminsOnServer,
|
AdminsOnServer,
|
||||||
#[error("Config error: {0}")]
|
#[error("Config error: {0}")]
|
||||||
@@ -21,7 +23,13 @@ pub enum GroupError {
|
|||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Serializer5(#[from] json5::Error),
|
Serializer5(#[from] json5::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Elefren(#[from] elefren::Error),
|
Elefren(Box<elefren::Error>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<elefren::Error> for GroupError {
|
||||||
|
fn from(e: elefren::Error) -> Self {
|
||||||
|
Self::Elefren(Box::new(e))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is for tests
|
// this is for tests
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ use crate::store::group_config::GroupConfig;
|
|||||||
use crate::store::CommonConfig;
|
use crate::store::CommonConfig;
|
||||||
use crate::tr::TranslationTable;
|
use crate::tr::TranslationTable;
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
use crate::utils::{normalize_acct, LogError};
|
use crate::utils::{normalize_acct, LogError, VisExt};
|
||||||
|
|
||||||
|
use crate::{grp_debug, grp_info, grp_warn};
|
||||||
|
|
||||||
pub struct ProcessMention<'a> {
|
pub struct ProcessMention<'a> {
|
||||||
status: Status,
|
status: Status,
|
||||||
@@ -38,7 +40,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn lookup_acct_id(&self, acct: &str, followed: bool) -> Result<Option<String>, GroupError> {
|
async fn lookup_acct_id(&self, acct: &str, followed: bool) -> Result<Option<String>, GroupError> {
|
||||||
debug!("Looking up user ID by acct: {}", acct);
|
grp_debug!(self, "Looking up user ID by acct: {}", acct);
|
||||||
|
|
||||||
match tokio::time::timeout(
|
match tokio::time::timeout(
|
||||||
Duration::from_secs(5),
|
Duration::from_secs(5),
|
||||||
@@ -48,7 +50,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
warn!("Account lookup timeout!");
|
grp_warn!(self, "Account lookup timeout!");
|
||||||
Err(GroupError::ApiTimeout)
|
Err(GroupError::ApiTimeout)
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
@@ -60,14 +62,14 @@ impl<'a> ProcessMention<'a> {
|
|||||||
// XXX limit is 1!
|
// XXX limit is 1!
|
||||||
let acct_normalized = normalize_acct(&item.acct, &self.group_acct)?;
|
let acct_normalized = normalize_acct(&item.acct, &self.group_acct)?;
|
||||||
if acct_normalized == acct {
|
if acct_normalized == acct {
|
||||||
debug!("Search done, account found: {}", item.acct);
|
grp_debug!(self, "Search done, account found: {}", item.acct);
|
||||||
return Ok(Some(item.id));
|
return Ok(Some(item.id));
|
||||||
} else {
|
} else {
|
||||||
warn!("Found wrong account: {}", item.acct);
|
grp_warn!(self, "Found wrong account: {}", item.acct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Search done, nothing found");
|
grp_debug!(self, "Search done, nothing found");
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,14 +103,14 @@ impl<'a> ProcessMention<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn follow_user_by_id(&self, id: &str) -> Result<(), GroupError> {
|
async fn follow_user_by_id(&self, id: &str) -> Result<(), GroupError> {
|
||||||
debug!("Trying to follow user #{}", id);
|
grp_debug!(self, "Trying to follow user #{}", id);
|
||||||
self.client.follow(id).await?;
|
self.client.follow(id).await?;
|
||||||
self.delay_after_post().await;
|
self.delay_after_post().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn unfollow_user_by_id(&self, id: &str) -> Result<(), GroupError> {
|
async fn unfollow_user_by_id(&self, id: &str) -> Result<(), GroupError> {
|
||||||
debug!("Trying to unfollow user #{}", id);
|
grp_debug!(self, "Trying to unfollow user #{}", id);
|
||||||
self.client.unfollow(id).await?;
|
self.client.unfollow(id).await?;
|
||||||
self.delay_after_post().await;
|
self.delay_after_post().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -119,7 +121,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
let status_acct = normalize_acct(&status.account.acct, &group_acct)?.to_string();
|
let status_acct = normalize_acct(&status.account.acct, &group_acct)?.to_string();
|
||||||
|
|
||||||
if gh.config.is_banned(&status_acct) {
|
if gh.config.is_banned(&status_acct) {
|
||||||
warn!("Status author {} is banned!", status_acct);
|
grp_warn!(gh, "Status author {} is banned!", status_acct);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,8 +145,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn reblog_status(&self) {
|
async fn reblog_status(&self) {
|
||||||
self.client.reblog(&self.status.id)
|
self.client.reblog(&self.status.id).await.log_error("Failed to reblog status");
|
||||||
.await.log_error("Failed to reblog status");
|
|
||||||
self.delay_after_post().await;
|
self.delay_after_post().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +164,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
self.handle_post_with_no_commands().await;
|
self.handle_post_with_no_commands().await;
|
||||||
} else {
|
} else {
|
||||||
if commands.contains(&StatusCommand::Ignore) {
|
if commands.contains(&StatusCommand::Ignore) {
|
||||||
debug!("Notif ignored because of ignore command");
|
grp_debug!(self, "Notif ignored because of ignore command");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +260,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
self.delay_after_post().await;
|
self.delay_after_post().await;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Can't reblog: {}", e);
|
grp_warn!(self, "Can't reblog: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,9 +268,9 @@ impl<'a> ProcessMention<'a> {
|
|||||||
|
|
||||||
if !self.replies.is_empty() {
|
if !self.replies.is_empty() {
|
||||||
let mut msg = std::mem::take(&mut self.replies);
|
let mut msg = std::mem::take(&mut self.replies);
|
||||||
debug!("r={}", msg);
|
grp_debug!(self, "r={}", msg);
|
||||||
|
|
||||||
apply_trailing_hashtag_pleroma_bug_workaround(&mut msg);
|
self.apply_trailing_hashtag_pleroma_bug_workaround(&mut msg);
|
||||||
|
|
||||||
let mention = crate::tr!(self, "mention_prefix", user = &self.status_acct);
|
let mention = crate::tr!(self, "mention_prefix", user = &self.status_acct);
|
||||||
self.send_reply_multipart(mention, msg).await?;
|
self.send_reply_multipart(mention, msg).await?;
|
||||||
@@ -277,9 +278,9 @@ impl<'a> ProcessMention<'a> {
|
|||||||
|
|
||||||
if !self.announcements.is_empty() {
|
if !self.announcements.is_empty() {
|
||||||
let mut msg = std::mem::take(&mut self.announcements);
|
let mut msg = std::mem::take(&mut self.announcements);
|
||||||
debug!("a={}", msg);
|
grp_debug!(self, "a={}", msg);
|
||||||
|
|
||||||
apply_trailing_hashtag_pleroma_bug_workaround(&mut msg);
|
self.apply_trailing_hashtag_pleroma_bug_workaround(&mut msg);
|
||||||
|
|
||||||
let msg = crate::tr!(self, "group_announcement", message = &msg);
|
let msg = crate::tr!(self, "group_announcement", message = &msg);
|
||||||
self.send_announcement_multipart(&msg).await?;
|
self.send_announcement_multipart(&msg).await?;
|
||||||
@@ -339,24 +340,45 @@ impl<'a> ProcessMention<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_post_with_no_commands(&mut self) {
|
async fn handle_post_with_no_commands(&mut self) {
|
||||||
debug!("No commands in post");
|
grp_debug!(self, "No commands in post");
|
||||||
if self.status.in_reply_to_id.is_none() {
|
|
||||||
|
if self.status.visibility.is_private() {
|
||||||
|
grp_debug!(self, "Mention is private, discard");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if self.can_write {
|
if self.can_write {
|
||||||
|
if self.status.in_reply_to_id.is_none() {
|
||||||
// Someone tagged the group in OP, boost it.
|
// Someone tagged the group in OP, boost it.
|
||||||
info!("Boosting OP mention");
|
grp_info!(self, "Boosting OP mention");
|
||||||
// tokio::time::sleep(DELAY_BEFORE_ACTION).await;
|
// tokio::time::sleep(DELAY_BEFORE_ACTION).await;
|
||||||
self.reblog_status().await;
|
self.reblog_status().await;
|
||||||
} else {
|
|
||||||
warn!("User @{} can't post to group!", self.status_acct);
|
|
||||||
}
|
|
||||||
// Otherwise, don't react
|
// Otherwise, don't react
|
||||||
} else {
|
} else {
|
||||||
debug!("Not OP, ignore mention");
|
// Check for tags
|
||||||
|
let tags = crate::command::parse_status_tags(&self.status.content);
|
||||||
|
grp_debug!(self, "Tags in mention: {:?}", tags);
|
||||||
|
|
||||||
|
for t in tags {
|
||||||
|
if self.config.is_tag_followed(&t) {
|
||||||
|
grp_info!(self, "REBLOG #{} STATUS", t);
|
||||||
|
self.client.reblog(&self.status.id).await.log_error("Failed to reblog");
|
||||||
|
self.delay_after_post().await;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
grp_debug!(self, "#{} is not a group tag", t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
grp_debug!(self, "Not OP & no tags, ignore mention");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
grp_warn!(self, "User @{} can't post to group!", self.status_acct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cmd_announce(&mut self, msg: String) {
|
async fn cmd_announce(&mut self, msg: String) {
|
||||||
info!("Sending PSA");
|
grp_info!(self, "Sending PSA");
|
||||||
self.add_announcement(msg);
|
self.add_announcement(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,7 +386,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
if self.can_write {
|
if self.can_write {
|
||||||
self.do_boost_prev_post = self.status.in_reply_to_id.is_some();
|
self.do_boost_prev_post = self.status.in_reply_to_id.is_some();
|
||||||
} else {
|
} else {
|
||||||
warn!("User @{} can't share to group!", self.status_acct);
|
grp_warn!(self, "User @{} can't share to group!", self.status_acct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,19 +420,19 @@ impl<'a> ProcessMention<'a> {
|
|||||||
// This is a post sent by the group user, likely an announcement.
|
// This is a post sent by the group user, likely an announcement.
|
||||||
// Undo here means delete it.
|
// Undo here means delete it.
|
||||||
if self.is_admin {
|
if self.is_admin {
|
||||||
info!("Deleting group post #{}", parent_status_id);
|
grp_info!(self, "Deleting group post #{}", parent_status_id);
|
||||||
self.client.delete_status(parent_status_id).await?;
|
self.client.delete_status(parent_status_id).await?;
|
||||||
self.delay_after_post().await;
|
self.delay_after_post().await;
|
||||||
} else {
|
} else {
|
||||||
warn!("Only admin can delete posts made by the group user");
|
grp_warn!(self, "Only admin can delete posts made by the group user");
|
||||||
}
|
}
|
||||||
} else if self.is_admin || parent_account_id == &self.status_user_id {
|
} else if self.is_admin || parent_account_id == &self.status_user_id {
|
||||||
info!("Un-reblogging post #{}", parent_status_id);
|
grp_info!(self, "Un-reblogging post #{}", parent_status_id);
|
||||||
// User unboosting own post boosted by accident, or admin doing it
|
// User unboosting own post boosted by accident, or admin doing it
|
||||||
self.client.unreblog(parent_status_id).await?;
|
self.client.unreblog(parent_status_id).await?;
|
||||||
self.delay_after_post().await;
|
self.delay_after_post().await;
|
||||||
} else {
|
} else {
|
||||||
warn!("Only the author and admins can undo reblogs");
|
grp_warn!(self, "Only the author and admins can undo reblogs");
|
||||||
// XXX this means when someone /b's someone else's post to a group,
|
// XXX this means when someone /b's someone else's post to a group,
|
||||||
// they then can't reverse that (only admin or the post's author can)
|
// they then can't reverse that (only admin or the post's author can)
|
||||||
}
|
}
|
||||||
@@ -436,7 +458,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
self.add_reply(crate::tr!(self, "cmd_ban_user_fail_already", user = &u));
|
self.add_reply(crate::tr!(self, "cmd_ban_user_fail_already", user = &u));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Ignore cmd, user not admin");
|
grp_warn!(self, "Ignore cmd, user not admin");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -458,7 +480,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
self.add_reply(crate::tr!(self, "cmd_unban_user_fail_already", user = &u));
|
self.add_reply(crate::tr!(self, "cmd_unban_user_fail_already", user = &u));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Ignore cmd, user not admin");
|
grp_warn!(self, "Ignore cmd, user not admin");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -478,7 +500,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
self.add_reply(crate::tr!(self, "cmd_ban_server_fail_already", server = s));
|
self.add_reply(crate::tr!(self, "cmd_ban_server_fail_already", server = s));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Ignore cmd, user not admin");
|
grp_warn!(self, "Ignore cmd, user not admin");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,7 +519,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
self.add_reply(crate::tr!(self, "cmd_unban_server_fail_already", server = s));
|
self.add_reply(crate::tr!(self, "cmd_unban_server_fail_already", server = s));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Ignore cmd, user not admin");
|
grp_warn!(self, "Ignore cmd, user not admin");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,7 +538,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Ignore cmd, user not admin");
|
grp_warn!(self, "Ignore cmd, user not admin");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -534,7 +556,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Ignore cmd, user not admin");
|
grp_warn!(self, "Ignore cmd, user not admin");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -548,7 +570,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
self.add_reply(crate::tr!(self, "cmd_add_tag_fail_already", tag = &tag));
|
self.add_reply(crate::tr!(self, "cmd_add_tag_fail_already", tag = &tag));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Ignore cmd, user not admin");
|
grp_warn!(self, "Ignore cmd, user not admin");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,7 +583,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
self.add_reply(crate::tr!(self, "cmd_remove_tag_fail_already", tag = &tag));
|
self.add_reply(crate::tr!(self, "cmd_remove_tag_fail_already", tag = &tag));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Ignore cmd, user not admin");
|
grp_warn!(self, "Ignore cmd, user not admin");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,7 +606,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
self.add_reply(crate::tr!(self, "cmd_admin_fail_already", user = &u));
|
self.add_reply(crate::tr!(self, "cmd_admin_fail_already", user = &u));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Ignore cmd, user not admin");
|
grp_warn!(self, "Ignore cmd, user not admin");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -605,7 +627,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
self.add_reply(crate::tr!(self, "cmd_unadmin_fail_already", user = &u));
|
self.add_reply(crate::tr!(self, "cmd_unadmin_fail_already", user = &u));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Ignore cmd, user not admin");
|
grp_warn!(self, "Ignore cmd, user not admin");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -619,7 +641,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
self.add_reply(crate::tr!(self, "cmd_open_resp_already"));
|
self.add_reply(crate::tr!(self, "cmd_open_resp_already"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Ignore cmd, user not admin");
|
grp_warn!(self, "Ignore cmd, user not admin");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,7 +654,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
self.add_reply(crate::tr!(self, "cmd_close_resp_already"));
|
self.add_reply(crate::tr!(self, "cmd_close_resp_already"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Ignore cmd, user not admin");
|
grp_warn!(self, "Ignore cmd, user not admin");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,7 +670,11 @@ impl<'a> ProcessMention<'a> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if self.config.is_member_only() {
|
if self.config.is_member_only() {
|
||||||
self.add_reply(crate::tr!(self, "help_group_info_closed", membership = &membership_line));
|
self.add_reply(crate::tr!(
|
||||||
|
self,
|
||||||
|
"help_group_info_closed",
|
||||||
|
membership = &membership_line
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
self.add_reply(crate::tr!(self, "help_group_info_open", membership = &membership_line));
|
self.add_reply(crate::tr!(self, "help_group_info_open", membership = &membership_line));
|
||||||
}
|
}
|
||||||
@@ -701,7 +727,7 @@ impl<'a> ProcessMention<'a> {
|
|||||||
|
|
||||||
async fn cmd_join(&mut self) {
|
async fn cmd_join(&mut self) {
|
||||||
if self.config.is_member_or_admin(&self.status_acct) {
|
if self.config.is_member_or_admin(&self.status_acct) {
|
||||||
debug!("Already member or admin, try to follow-back again");
|
grp_debug!(self, "Already member or admin, try to follow-back again");
|
||||||
// Already a member, so let's try to follow the user
|
// Already a member, so let's try to follow the user
|
||||||
// again, maybe first time it failed
|
// again, maybe first time it failed
|
||||||
self.follow_user_by_id(&self.status_user_id).await.log_error("Failed to follow");
|
self.follow_user_by_id(&self.status_user_id).await.log_error("Failed to follow");
|
||||||
@@ -747,11 +773,11 @@ impl<'a> ProcessMention<'a> {
|
|||||||
// Try to unfollow
|
// Try to unfollow
|
||||||
let account = self.client.get_account(id).await?;
|
let account = self.client.get_account(id).await?;
|
||||||
let bio = utils::strip_html(&account.note);
|
let bio = utils::strip_html(&account.note);
|
||||||
if RE_NOBOT_TAG.is_match(&bio) {
|
|
||||||
// #nobot
|
|
||||||
Err(GroupError::UserOptedOut)
|
|
||||||
} else {
|
|
||||||
let normalized = normalize_acct(&account.acct, &self.group_acct)?;
|
let normalized = normalize_acct(&account.acct, &self.group_acct)?;
|
||||||
|
if RE_NOBOT_TAG.is_match(&bio) && !self.config.is_member(&normalized) {
|
||||||
|
// #nobot in a non-member account
|
||||||
|
Err(GroupError::UserOptedOutNobot)
|
||||||
|
} else {
|
||||||
if self.config.is_banned(&normalized) {
|
if self.config.is_banned(&normalized) {
|
||||||
Err(GroupError::UserIsBanned)
|
Err(GroupError::UserIsBanned)
|
||||||
} else if self.config.is_optout(&normalized) {
|
} else if self.config.is_optout(&normalized) {
|
||||||
@@ -765,15 +791,15 @@ impl<'a> ProcessMention<'a> {
|
|||||||
async fn delay_after_post(&self) {
|
async fn delay_after_post(&self) {
|
||||||
tokio::time::sleep(Duration::from_secs_f64(self.cc.delay_after_post_s)).await;
|
tokio::time::sleep(Duration::from_secs_f64(self.cc.delay_after_post_s)).await;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_trailing_hashtag_pleroma_bug_workaround(msg: &mut String) {
|
fn apply_trailing_hashtag_pleroma_bug_workaround(&self, msg: &mut String) {
|
||||||
if crate::command::RE_HASHTAG_TRIGGERING_PLEROMA_BUG.is_match(msg) {
|
if crate::command::RE_HASHTAG_TRIGGERING_PLEROMA_BUG.is_match(msg) {
|
||||||
// if a status ends with a hashtag, pleroma will fuck it up
|
// if a status ends with a hashtag, pleroma will fuck it up
|
||||||
debug!("Adding \" .\" to fix pleroma hashtag eating bug!");
|
grp_debug!(self, "Adding \" .\" to fix pleroma hashtag eating bug!");
|
||||||
msg.push_str(" .");
|
msg.push_str(" .");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn smart_split(msg: &str, prefix: Option<String>, limit: usize) -> Vec<String> {
|
fn smart_split(msg: &str, prefix: Option<String>, limit: usize) -> Vec<String> {
|
||||||
let prefix = prefix.unwrap_or_default();
|
let prefix = prefix.unwrap_or_default();
|
||||||
@@ -785,29 +811,29 @@ fn smart_split(msg: &str, prefix: Option<String>, limit: usize) -> Vec<String> {
|
|||||||
let mut parts_to_send = vec![];
|
let mut parts_to_send = vec![];
|
||||||
let mut this_piece = prefix.clone();
|
let mut this_piece = prefix.clone();
|
||||||
for l in msg.split('\n') {
|
for l in msg.split('\n') {
|
||||||
println!("* Line: {:?}", l);
|
// println!("* Line: {:?}", l);
|
||||||
|
|
||||||
match (this_piece.len() + l.len()).cmp(&limit) {
|
match (this_piece.len() + l.len()).cmp(&limit) {
|
||||||
Ordering::Less => {
|
Ordering::Less => {
|
||||||
println!("append line");
|
// println!("append line");
|
||||||
// this line still fits comfortably
|
// this line still fits comfortably
|
||||||
this_piece.push_str(l);
|
this_piece.push_str(l);
|
||||||
this_piece.push('\n');
|
this_piece.push('\n');
|
||||||
}
|
}
|
||||||
Ordering::Equal => {
|
Ordering::Equal => {
|
||||||
println!("exactly fits within limit");
|
// println!("exactly fits within limit");
|
||||||
// this line exactly reaches the limit
|
// this line exactly reaches the limit
|
||||||
this_piece.push_str(l);
|
this_piece.push_str(l);
|
||||||
parts_to_send.push(std::mem::take(&mut this_piece).trim().to_owned());
|
parts_to_send.push(std::mem::take(&mut this_piece).trim().to_owned());
|
||||||
this_piece.push_str(&prefix);
|
this_piece.push_str(&prefix);
|
||||||
}
|
}
|
||||||
Ordering::Greater => {
|
Ordering::Greater => {
|
||||||
println!("too long to append (already {} + new {})", this_piece.len(), l.len());
|
// println!("too long to append (already {} + new {})", this_piece.len(), l.len());
|
||||||
// line too long to append
|
// line too long to append
|
||||||
if this_piece != prefix {
|
if this_piece != prefix {
|
||||||
let trimmed = this_piece.trim();
|
let trimmed = this_piece.trim();
|
||||||
if !trimmed.is_empty() {
|
if !trimmed.is_empty() {
|
||||||
println!("flush buffer: {:?}", trimmed);
|
// println!("flush buffer: {:?}", trimmed);
|
||||||
parts_to_send.push(trimmed.to_owned());
|
parts_to_send.push(trimmed.to_owned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -817,19 +843,19 @@ fn smart_split(msg: &str, prefix: Option<String>, limit: usize) -> Vec<String> {
|
|||||||
|
|
||||||
while this_piece.len() > limit {
|
while this_piece.len() > limit {
|
||||||
// line too long, try splitting at the last space, if any
|
// line too long, try splitting at the last space, if any
|
||||||
let to_send = if let Some(last_space) = (&this_piece[..=limit]).rfind(' ') {
|
let to_send = if let Some(last_space) = this_piece[..=limit].rfind(' ') {
|
||||||
println!("line split at word boundary");
|
// println!("line split at word boundary");
|
||||||
let mut p = this_piece.split_off(last_space + 1);
|
let mut p = this_piece.split_off(last_space + 1);
|
||||||
std::mem::swap(&mut p, &mut this_piece);
|
std::mem::swap(&mut p, &mut this_piece);
|
||||||
p
|
p
|
||||||
} else {
|
} else {
|
||||||
println!("line split at exact len (no word boundary found)");
|
// println!("line split at exact len (no word boundary found)");
|
||||||
let mut p = this_piece.split_off(limit);
|
let mut p = this_piece.split_off(limit);
|
||||||
std::mem::swap(&mut p, &mut this_piece);
|
std::mem::swap(&mut p, &mut this_piece);
|
||||||
p
|
p
|
||||||
};
|
};
|
||||||
let part_trimmed = to_send.trim();
|
let part_trimmed = to_send.trim();
|
||||||
println!("flush buffer: {:?}", part_trimmed);
|
// println!("flush buffer: {:?}", part_trimmed);
|
||||||
parts_to_send.push(part_trimmed.to_owned());
|
parts_to_send.push(part_trimmed.to_owned());
|
||||||
this_piece = format!("{}{}", prefix, this_piece.trim());
|
this_piece = format!("{}{}", prefix, this_piece.trim());
|
||||||
}
|
}
|
||||||
@@ -841,7 +867,7 @@ fn smart_split(msg: &str, prefix: Option<String>, limit: usize) -> Vec<String> {
|
|||||||
if this_piece != prefix {
|
if this_piece != prefix {
|
||||||
let leftover_trimmed = this_piece.trim();
|
let leftover_trimmed = this_piece.trim();
|
||||||
if !leftover_trimmed.is_empty() {
|
if !leftover_trimmed.is_empty() {
|
||||||
println!("flush buffer: {:?}", leftover_trimmed);
|
// println!("flush buffer: {:?}", leftover_trimmed);
|
||||||
parts_to_send.push(leftover_trimmed.to_owned());
|
parts_to_send.push(leftover_trimmed.to_owned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-15
@@ -34,19 +34,12 @@ pub struct GroupHandle {
|
|||||||
pub internal: GroupInternal,
|
pub internal: GroupInternal,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Default)]
|
||||||
pub struct GroupInternal {
|
pub struct GroupInternal {
|
||||||
recently_seen_notif_statuses: VecDeque<String>,
|
recently_seen_notif_statuses: VecDeque<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GroupInternal {
|
#[macro_export]
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
recently_seen_notif_statuses: VecDeque::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! grp_debug {
|
macro_rules! grp_debug {
|
||||||
($self:ident, $f:expr) => {
|
($self:ident, $f:expr) => {
|
||||||
::log::debug!(concat!("(@{}) ", $f), $self.config.get_acct());
|
::log::debug!(concat!("(@{}) ", $f), $self.config.get_acct());
|
||||||
@@ -56,6 +49,7 @@ macro_rules! grp_debug {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
macro_rules! grp_info {
|
macro_rules! grp_info {
|
||||||
($self:ident, $f:expr) => {
|
($self:ident, $f:expr) => {
|
||||||
::log::info!(concat!("(@{}) ", $f), $self.config.get_acct());
|
::log::info!(concat!("(@{}) ", $f), $self.config.get_acct());
|
||||||
@@ -65,6 +59,7 @@ macro_rules! grp_info {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
macro_rules! grp_trace {
|
macro_rules! grp_trace {
|
||||||
($self:ident, $f:expr) => {
|
($self:ident, $f:expr) => {
|
||||||
::log::trace!(concat!("(@{}) ", $f), $self.config.get_acct());
|
::log::trace!(concat!("(@{}) ", $f), $self.config.get_acct());
|
||||||
@@ -74,6 +69,7 @@ macro_rules! grp_trace {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
macro_rules! grp_warn {
|
macro_rules! grp_warn {
|
||||||
($self:ident, $f:expr) => {
|
($self:ident, $f:expr) => {
|
||||||
::log::warn!(concat!("(@{}) ", $f), $self.config.get_acct());
|
::log::warn!(concat!("(@{}) ", $f), $self.config.get_acct());
|
||||||
@@ -83,6 +79,7 @@ macro_rules! grp_warn {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
macro_rules! grp_error {
|
macro_rules! grp_error {
|
||||||
($self:ident, $f:expr) => {
|
($self:ident, $f:expr) => {
|
||||||
::log::error!(concat!("(@{}) ", $f), $self.config.get_acct());
|
::log::error!(concat!("(@{}) ", $f), $self.config.get_acct());
|
||||||
@@ -133,11 +130,11 @@ impl GroupHandle {
|
|||||||
match self.run_internal().await {
|
match self.run_internal().await {
|
||||||
Ok(()) => unreachable!(),
|
Ok(()) => unreachable!(),
|
||||||
Err(e @ GroupError::BadConfig(_)) => {
|
Err(e @ GroupError::BadConfig(_)) => {
|
||||||
grp_error!(self, "ERROR in group handler, aborting! {}", e);
|
grp_error!(self, "ERROR in group handler, aborting! {:?}", e);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
Err(other) => {
|
Err(other) => {
|
||||||
grp_error!(self, "ERROR in group handler, will restart! {}", other);
|
grp_error!(self, "ERROR in group handler, will restart! {:?}", other);
|
||||||
tokio::time::sleep(Duration::from_secs_f64(self.cc.delay_reopen_error_s)).await;
|
tokio::time::sleep(Duration::from_secs_f64(self.cc.delay_reopen_error_s)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,7 +167,7 @@ impl GroupHandle {
|
|||||||
grp_debug!(self, "No notifs missed");
|
grp_debug!(self, "No notifs missed");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
grp_error!(self, "Failed to handle missed notifs: {}", e);
|
grp_error!(self, "Failed to handle missed notifs: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,7 +181,7 @@ impl GroupHandle {
|
|||||||
grp_debug!(self, "No statuses missed");
|
grp_debug!(self, "No statuses missed");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
grp_error!(self, "Failed to handle missed statuses: {}", e);
|
grp_error!(self, "Failed to handle missed statuses: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -547,8 +544,7 @@ impl GroupHandle {
|
|||||||
|
|
||||||
self.config.set_member(notif_acct, true).log_error("Fail add a member");
|
self.config.set_member(notif_acct, true).log_error("Fail add a member");
|
||||||
|
|
||||||
crate::tr!(self, "mention_prefix", user = notif_acct)
|
crate::tr!(self, "mention_prefix", user = notif_acct) + &crate::tr!(self, "welcome_public")
|
||||||
+ &crate::tr!(self, "welcome_public")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let post = StatusBuilder::new()
|
let post = StatusBuilder::new()
|
||||||
|
|||||||
+9
-10
@@ -18,6 +18,7 @@ use crate::utils::acct_to_server;
|
|||||||
|
|
||||||
mod command;
|
mod command;
|
||||||
mod error;
|
mod error;
|
||||||
|
#[macro_use]
|
||||||
mod group_handler;
|
mod group_handler;
|
||||||
mod store;
|
mod store;
|
||||||
mod utils;
|
mod utils;
|
||||||
@@ -30,26 +31,26 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let args = clap::App::new("groups")
|
let args = clap::App::new("groups")
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("verbose")
|
Arg::with_name("verbose")
|
||||||
.short("v")
|
.short('v')
|
||||||
.multiple(true)
|
.multiple_occurrences(true)
|
||||||
.help("increase logging, can be repeated"),
|
.help("increase logging, can be repeated"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("quiet")
|
Arg::with_name("quiet")
|
||||||
.short("q")
|
.short('q')
|
||||||
.multiple(true)
|
.multiple_occurrences(true)
|
||||||
.help("decrease logging, can be repeated"),
|
.help("decrease logging, can be repeated"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("config")
|
Arg::with_name("config")
|
||||||
.short("c")
|
.short('c')
|
||||||
.long("config")
|
.long("config")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("set custom config directory, defaults to the current folder"),
|
.help("set custom config directory, defaults to the current folder"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("auth")
|
Arg::with_name("auth")
|
||||||
.short("a")
|
.short('a')
|
||||||
.long("auth")
|
.long("auth")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.value_name("HANDLE")
|
.value_name("HANDLE")
|
||||||
@@ -57,7 +58,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("reauth")
|
Arg::with_name("reauth")
|
||||||
.short("A")
|
.short('A')
|
||||||
.long("reauth")
|
.long("reauth")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.value_name("HANDLE")
|
.value_name("HANDLE")
|
||||||
@@ -75,9 +76,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let default_level = 3;
|
let default_level = 3;
|
||||||
|
|
||||||
let level = (
|
let level = (default_level as isize + args.occurrences_of("verbose") as isize
|
||||||
default_level as isize
|
|
||||||
+ args.occurrences_of("verbose") as isize
|
|
||||||
- args.occurrences_of("quiet") as isize)
|
- args.occurrences_of("quiet") as isize)
|
||||||
.clamp(0, LEVELS.len() as isize) as usize;
|
.clamp(0, LEVELS.len() as isize) as usize;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use crate::store::DEFAULT_LOCALE_NAME;
|
use crate::store::DEFAULT_LOCALE_NAME;
|
||||||
use crate::tr::TranslationTable;
|
use crate::tr::TranslationTable;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
@@ -55,7 +55,7 @@ impl CommonConfig {
|
|||||||
pub fn tr(&self, lang: &str) -> &TranslationTable {
|
pub fn tr(&self, lang: &str) -> &TranslationTable {
|
||||||
match self.tr.get(lang) {
|
match self.tr.get(lang) {
|
||||||
Some(tr) => tr,
|
Some(tr) => tr,
|
||||||
None => self.tr.get(DEFAULT_LOCALE_NAME).expect("default locale is not loaded")
|
None => self.tr.get(DEFAULT_LOCALE_NAME).expect("default locale is not loaded"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-41
@@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use elefren::AppData;
|
use elefren::AppData;
|
||||||
|
|
||||||
use crate::error::GroupError;
|
use crate::error::GroupError;
|
||||||
use crate::store::{DEFAULT_LOCALE_NAME, CommonConfig};
|
use crate::store::{CommonConfig, DEFAULT_LOCALE_NAME};
|
||||||
use crate::tr::TranslationTable;
|
use crate::tr::TranslationTable;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -25,7 +25,7 @@ struct FixedConfig {
|
|||||||
_path: PathBuf,
|
_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
struct MutableConfig {
|
struct MutableConfig {
|
||||||
/// Hashtags the group will auto-boost from it's members
|
/// Hashtags the group will auto-boost from it's members
|
||||||
@@ -48,7 +48,7 @@ struct MutableConfig {
|
|||||||
_path: PathBuf,
|
_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
struct StateConfig {
|
struct StateConfig {
|
||||||
/// Last seen notification timestamp (millis)
|
/// Last seen notification timestamp (millis)
|
||||||
@@ -94,33 +94,6 @@ impl Default for FixedConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MutableConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
group_tags: Default::default(),
|
|
||||||
admin_users: Default::default(),
|
|
||||||
member_users: Default::default(),
|
|
||||||
banned_users: Default::default(),
|
|
||||||
optout_users: Default::default(),
|
|
||||||
member_only: false,
|
|
||||||
banned_servers: Default::default(),
|
|
||||||
_dirty: false,
|
|
||||||
_path: PathBuf::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for StateConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
last_notif_ts: 0,
|
|
||||||
last_status_ts: 0,
|
|
||||||
_dirty: false,
|
|
||||||
_path: PathBuf::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_change_tracking {
|
macro_rules! impl_change_tracking {
|
||||||
($struc:ident) => {
|
($struc:ident) => {
|
||||||
impl $struc {
|
impl $struc {
|
||||||
@@ -259,7 +232,11 @@ impl GroupConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// (re)init using new authorization
|
/// (re)init using new authorization
|
||||||
pub(crate) async fn initialize_by_appdata(acct: String, appdata: AppData, group_dir: PathBuf) -> Result<(), GroupError> {
|
pub(crate) async fn initialize_by_appdata(
|
||||||
|
acct: String,
|
||||||
|
appdata: AppData,
|
||||||
|
group_dir: PathBuf,
|
||||||
|
) -> Result<(), GroupError> {
|
||||||
if !group_dir.is_dir() {
|
if !group_dir.is_dir() {
|
||||||
debug!("Creating group directory");
|
debug!("Creating group directory");
|
||||||
tokio::fs::create_dir_all(&group_dir).await?;
|
tokio::fs::create_dir_all(&group_dir).await?;
|
||||||
@@ -306,7 +283,12 @@ impl GroupConfig {
|
|||||||
/* state */
|
/* state */
|
||||||
let state = load_or_create_state_file(state_path).await?;
|
let state = load_or_create_state_file(state_path).await?;
|
||||||
|
|
||||||
let g = GroupConfig { config, control, state, _group_tr: TranslationTable::new() };
|
let g = GroupConfig {
|
||||||
|
config,
|
||||||
|
control,
|
||||||
|
state,
|
||||||
|
_group_tr: TranslationTable::new(),
|
||||||
|
};
|
||||||
g.warn_of_bad_config();
|
g.warn_of_bad_config();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -338,7 +320,12 @@ impl GroupConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let g = GroupConfig { config, control, state, _group_tr: tr };
|
let g = GroupConfig {
|
||||||
|
config,
|
||||||
|
control,
|
||||||
|
state,
|
||||||
|
_group_tr: tr,
|
||||||
|
};
|
||||||
g.warn_of_bad_config();
|
g.warn_of_bad_config();
|
||||||
Ok(g)
|
Ok(g)
|
||||||
}
|
}
|
||||||
@@ -458,7 +445,7 @@ impl GroupConfig {
|
|||||||
/// Check if the user's server is banned
|
/// Check if the user's server is banned
|
||||||
fn is_users_server_banned(&self, acct: &str) -> bool {
|
fn is_users_server_banned(&self, acct: &str) -> bool {
|
||||||
let server = acct_to_server(acct);
|
let server = acct_to_server(acct);
|
||||||
self.is_server_banned(server)
|
self.is_server_banned(&server)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn can_write(&self, acct: &str) -> bool {
|
pub(crate) fn can_write(&self, acct: &str) -> bool {
|
||||||
@@ -574,8 +561,8 @@ impl GroupConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn acct_to_server(acct: &str) -> &str {
|
fn acct_to_server(acct: &str) -> String {
|
||||||
acct.split('@').nth(1).unwrap_or_default()
|
crate::utils::acct_to_server(acct).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -588,15 +575,15 @@ mod tests {
|
|||||||
config: Default::default(),
|
config: Default::default(),
|
||||||
control: Default::default(),
|
control: Default::default(),
|
||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
_group_tr: Default::default()
|
_group_tr: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_acct_to_server() {
|
fn test_acct_to_server() {
|
||||||
assert_eq!("pikachu.rocks", acct_to_server("raichu@pikachu.rocks"));
|
assert_eq!("pikachu.rocks".to_string(), acct_to_server("raichu@pikachu.rocks"));
|
||||||
assert_eq!("pikachu.rocks", acct_to_server("m@pikachu.rocks"));
|
assert_eq!("pikachu.rocks".to_string(), acct_to_server("m@pikachu.rocks"));
|
||||||
assert_eq!("", acct_to_server("what"));
|
assert_eq!("".to_string(), acct_to_server("what"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -666,13 +653,15 @@ mod tests {
|
|||||||
assert!(!group.is_banned("piggo@piggo.space"), "user not banned by default");
|
assert!(!group.is_banned("piggo@piggo.space"), "user not banned by default");
|
||||||
|
|
||||||
group.ban_user("piggo@piggo.space", true).unwrap();
|
group.ban_user("piggo@piggo.space", true).unwrap();
|
||||||
assert!(group.is_member("piggo@piggo.space"), "still member even if banned");
|
assert!(!group.is_member("piggo@piggo.space"), "banned user is kicked");
|
||||||
assert!(group.is_banned("piggo@piggo.space"), "banned user is banned");
|
assert!(group.is_banned("piggo@piggo.space"), "banned user is banned");
|
||||||
|
|
||||||
assert!(!group.can_write("piggo@piggo.space"), "banned member can't post");
|
assert!(!group.can_write("piggo@piggo.space"), "banned member can't post");
|
||||||
|
|
||||||
// unban
|
// unban
|
||||||
group.ban_user("piggo@piggo.space", false).unwrap();
|
group.ban_user("piggo@piggo.space", false).unwrap();
|
||||||
|
assert!(!group.can_write("piggo@piggo.space"), "unbanned member is still kicked");
|
||||||
|
group.set_member("piggo@piggo.space", true).unwrap();
|
||||||
assert!(group.can_write("piggo@piggo.space"), "un-ban works");
|
assert!(group.can_write("piggo@piggo.space"), "un-ban works");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -9,9 +9,9 @@ use crate::group_handler::{GroupHandle, GroupInternal};
|
|||||||
|
|
||||||
pub mod common_config;
|
pub mod common_config;
|
||||||
pub mod group_config;
|
pub mod group_config;
|
||||||
|
use crate::tr::TranslationTable;
|
||||||
pub use common_config::CommonConfig;
|
pub use common_config::CommonConfig;
|
||||||
pub use group_config::GroupConfig;
|
pub use group_config::GroupConfig;
|
||||||
use crate::tr::TranslationTable;
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct ConfigStore {
|
pub struct ConfigStore {
|
||||||
@@ -149,7 +149,7 @@ impl ConfigStore {
|
|||||||
|
|
||||||
/// Re-auth an existing group
|
/// Re-auth an existing group
|
||||||
pub async fn reauth_group(&self, acct: &str) -> Result<(), GroupError> {
|
pub async fn reauth_group(&self, acct: &str) -> Result<(), GroupError> {
|
||||||
let group_dir = self.groups_path.join(&acct);
|
let group_dir = self.groups_path.join(acct);
|
||||||
|
|
||||||
let mut config = GroupConfig::from_dir(group_dir, &self.config).await?;
|
let mut config = GroupConfig::from_dir(group_dir, &self.config).await?;
|
||||||
|
|
||||||
@@ -203,8 +203,7 @@ impl ConfigStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for e in entries {
|
for e in entries.flatten() {
|
||||||
if let Ok(e) = e {
|
|
||||||
let path = e.path();
|
let path = e.path();
|
||||||
if path.is_file() && path.extension().unwrap_or_default().to_string_lossy() == "json" {
|
if path.is_file() && path.extension().unwrap_or_default().to_string_lossy() == "json" {
|
||||||
let filename = path.file_name().unwrap_or_default().to_string_lossy();
|
let filename = path.file_name().unwrap_or_default().to_string_lossy();
|
||||||
@@ -214,7 +213,7 @@ impl ConfigStore {
|
|||||||
Ok(f) => {
|
Ok(f) => {
|
||||||
let locale_name = path.file_stem().unwrap_or_default().to_string_lossy();
|
let locale_name = path.file_stem().unwrap_or_default().to_string_lossy();
|
||||||
self.load_locale(&locale_name, &String::from_utf8_lossy(&f), false);
|
self.load_locale(&locale_name, &String::from_utf8_lossy(&f), false);
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to read locale file {}: {}", path.display(), e);
|
error!("Failed to read locale file {}: {}", path.display(), e);
|
||||||
}
|
}
|
||||||
@@ -222,7 +221,6 @@ impl ConfigStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn load_locale(&mut self, locale_name: &str, locale_json: &str, is_default: bool) {
|
fn load_locale(&mut self, locale_name: &str, locale_json: &str, is_default: bool) {
|
||||||
if let Ok(mut tr) = json5::from_str::<TranslationTable>(locale_json) {
|
if let Ok(mut tr) = json5::from_str::<TranslationTable>(locale_json) {
|
||||||
@@ -234,10 +232,12 @@ impl ConfigStore {
|
|||||||
for (k, v) in def_tr.entries() {
|
for (k, v) in def_tr.entries() {
|
||||||
if !tr.translation_exists(k) {
|
if !tr.translation_exists(k) {
|
||||||
if self.config.validate_locales {
|
if self.config.validate_locales {
|
||||||
warn!("locale \"{}\" is missing \"{}\", default: {:?}",
|
warn!(
|
||||||
|
"locale \"{}\" is missing \"{}\", default: {:?}",
|
||||||
locale_name,
|
locale_name,
|
||||||
k,
|
k,
|
||||||
def_tr.get_translation_raw(k).unwrap());
|
def_tr.get_translation_raw(k).unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
tr.add_translation(k, v);
|
tr.add_translation(k, v);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ impl TranslationTable {
|
|||||||
}
|
}
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
None => key.to_owned()
|
None => key.to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,6 @@ mod tests {
|
|||||||
assert_eq!("xxx", tr.subs("xxx", &[]));
|
assert_eq!("xxx", tr.subs("xxx", &[]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn subs() {
|
fn subs() {
|
||||||
let mut tr = TranslationTable::new();
|
let mut tr = TranslationTable::new();
|
||||||
|
|||||||
+8
-6
@@ -19,8 +19,8 @@ impl<V, E: Error> LogError for Result<V, E> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn acct_to_server(acct: &str) -> Option<&str> {
|
pub(crate) fn acct_to_server(acct: &str) -> Option<String> {
|
||||||
acct.trim_start_matches('@').split('@').nth(1)
|
acct.trim_start_matches('@').split('@').nth(1).map(|s| s.to_lowercase())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn normalize_acct(acct: &str, group: &str) -> Result<String, GroupError> {
|
pub(crate) fn normalize_acct(acct: &str, group: &str) -> Result<String, GroupError> {
|
||||||
@@ -45,8 +45,8 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_acct_to_server() {
|
fn test_acct_to_server() {
|
||||||
assert_eq!(Some("novak"), acct_to_server("pepa@novak"));
|
assert_eq!(Some("novak".to_string()), acct_to_server("pepa@novak"));
|
||||||
assert_eq!(Some("banana.co.uk"), acct_to_server("@pepa@banana.co.uk"));
|
assert_eq!(Some("banana.co.uk".to_string()), acct_to_server("@pepa@banana.co.uk"));
|
||||||
assert_eq!(None, acct_to_server("probably_local"));
|
assert_eq!(None, acct_to_server("probably_local"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,11 +82,11 @@ mod test {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Ok("piggo@piggo.space".into()),
|
Ok("piggo@piggo.space".into()),
|
||||||
normalize_acct("piGGgo@pIggo.spaCe", "uhh")
|
normalize_acct("piGGo@pIggo.spaCe", "uhh")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Ok("piggo@banana.nana".into()),
|
Ok("piggo@banana.nana".into()),
|
||||||
normalize_acct("piGGgo", "foo@baNANA.nana")
|
normalize_acct("piGGo", "foo@baNANA.nana")
|
||||||
);
|
);
|
||||||
assert_eq!(Err(GroupError::BadConfig("_".into())), normalize_acct("piggo", "uhh"));
|
assert_eq!(Err(GroupError::BadConfig("_".into())), normalize_acct("piggo", "uhh"));
|
||||||
}
|
}
|
||||||
@@ -95,6 +95,8 @@ mod test {
|
|||||||
pub trait VisExt: Copy {
|
pub trait VisExt: Copy {
|
||||||
/// Check if is private or direct
|
/// Check if is private or direct
|
||||||
fn is_private(self) -> bool;
|
fn is_private(self) -> bool;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
fn make_unlisted(self) -> Self;
|
fn make_unlisted(self) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user