readme, some fixes

master v0.2.2
Ondřej Hruška 3 years ago
parent 2b84e5eeb0
commit 957f0dbb3b
Signed by untrusted user: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 11
      CHANGELOG.md
  2. 4
      Cargo.lock
  3. 4
      Cargo.toml
  4. 2
      README.md
  5. 27
      src/command.rs
  6. 81
      src/group_handler/handle_mention.rs
  7. 14
      src/group_handler/mod.rs
  8. 6
      src/main.rs
  9. 9
      src/store/mod.rs
  10. 19
      src/utils.rs

@ -1,7 +1,16 @@
# Changelog # Changelog
## v0.2 ## v0.2.2
- All hashtags, server names and handles are now lowercased = case-insensitive
- Prevent the `-a` flag overwriting existing group in the config
- Update the help text
- `/i` now works in hashtag posts
- `/add user` and `/remove user` now correctly follow/unfollow
## v0.2.1
- More reliable websocket reconnect, workaround for pleroma socket going silent
## v0.2.0
- Add hashtag boosting and back-follow/unfollow - Add hashtag boosting and back-follow/unfollow
- Add hashtag commands - Add hashtag commands
- Code reorganization - Code reorganization

4
Cargo.lock generated

@ -276,7 +276,7 @@ checksum = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e"
[[package]] [[package]]
name = "elefren" name = "elefren"
version = "0.22.0" version = "0.22.0"
source = "git+https://git.ondrovo.com/MightyPork/elefren-fork.git?rev=54a0e55#54a0e55964784368864f36580c5630f730bf72dc" source = "git+https://git.ondrovo.com/MightyPork/elefren-fork.git?rev=a0ebb46#a0ebb46542ede2d235ca6094135a6d6d01d0ecb8"
dependencies = [ dependencies = [
"chrono", "chrono",
"doc-comment", "doc-comment",
@ -328,7 +328,7 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]] [[package]]
name = "fedigroups" name = "fedigroups"
version = "0.2.1" version = "0.2.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",

@ -1,6 +1,6 @@
[package] [package]
name = "fedigroups" name = "fedigroups"
version = "0.2.1" version = "0.2.2"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"] authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018" edition = "2018"
publish = false publish = false
@ -10,7 +10,7 @@ build = "build.rs"
[dependencies] [dependencies]
#elefren = { path = "../elefren22-fork" } #elefren = { path = "../elefren22-fork" }
elefren = { git = "https://git.ondrovo.com/MightyPork/elefren-fork.git", rev = "54a0e55" } elefren = { git = "https://git.ondrovo.com/MightyPork/elefren-fork.git", rev = "a0ebb46" }
env_logger = "0.9.0" env_logger = "0.9.0"

@ -116,7 +116,7 @@ Any user (member in member-only groups) can post to the group by mentioning the
### Group hashtags ### Group hashtags
Admins can add hashtags to the group config (`/add #hashtag`, remove the same way: `/remove #hashtag`). Admins can add hashtags to the group config (`/add #hashtag`, remove the same way: `/remove #hashtag`). Hashtags are case-insensitive.
When a *group member* posts one of the group hashtags, the group will reblog it. This is a nicer way to share posts, you don't have to mention the group user at all. When a *group member* posts one of the group hashtags, the group will reblog it. This is a nicer way to share posts, you don't have to mention the group user at all.

@ -134,7 +134,7 @@ pub fn parse_status_tags(content: &str) -> Vec<String> {
let mut tags = vec![]; let mut tags = vec![];
for c in RE_A_HASHTAG.captures_iter(&content) { for c in RE_A_HASHTAG.captures_iter(&content) {
if let Some(s) = c.get(1) { if let Some(s) = c.get(1) {
tags.push(s.as_str().to_string()) tags.push(s.as_str().to_lowercase())
} }
} }
@ -150,6 +150,11 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
let content = voca_rs::strip::strip_tags(&content); let content = voca_rs::strip::strip_tags(&content);
debug!("Stripped tags: {}", content); debug!("Stripped tags: {}", content);
if !content.contains('/') && !content.contains('\\') {
// No slash = no command
return vec![];
}
// short-circuiting commands // short-circuiting commands
if RE_IGNORE.is_match(&content) { if RE_IGNORE.is_match(&content) {
@ -221,7 +226,7 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
let s = s.as_str(); let s = s.as_str();
let s = s.trim_start_matches('@'); let s = s.trim_start_matches('@');
debug!("BAN USER: {}", s); debug!("BAN USER: {}", s);
commands.push(StatusCommand::BanUser(s.to_owned())); commands.push(StatusCommand::BanUser(s.to_lowercase()));
} }
} }
@ -230,21 +235,21 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
let s = s.as_str(); let s = s.as_str();
let s = s.trim_start_matches('@'); let s = s.trim_start_matches('@');
debug!("UNBAN USER: {}", s); debug!("UNBAN USER: {}", s);
commands.push(StatusCommand::UnbanUser(s.to_owned())); commands.push(StatusCommand::UnbanUser(s.to_lowercase()));
} }
} }
for c in RE_BAN_SERVER.captures_iter(&content) { for c in RE_BAN_SERVER.captures_iter(&content) {
if let Some(s) = c.get(1) { if let Some(s) = c.get(1) {
debug!("BAN SERVER: {}", s.as_str()); debug!("BAN SERVER: {}", s.as_str());
commands.push(StatusCommand::BanServer(s.as_str().to_owned())); commands.push(StatusCommand::BanServer(s.as_str().to_lowercase()));
} }
} }
for c in RE_UNBAN_SERVER.captures_iter(&content) { for c in RE_UNBAN_SERVER.captures_iter(&content) {
if let Some(s) = c.get(1) { if let Some(s) = c.get(1) {
debug!("UNBAN SERVER: {}", s.as_str()); debug!("UNBAN SERVER: {}", s.as_str());
commands.push(StatusCommand::UnbanServer(s.as_str().to_owned())); commands.push(StatusCommand::UnbanServer(s.as_str().to_lowercase()));
} }
} }
@ -253,7 +258,7 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
let s = s.as_str(); let s = s.as_str();
let s = s.trim_start_matches('@'); let s = s.trim_start_matches('@');
debug!("ADD MEMBER: {}", s); debug!("ADD MEMBER: {}", s);
commands.push(StatusCommand::AddMember(s.to_owned())); commands.push(StatusCommand::AddMember(s.to_lowercase()));
} }
} }
@ -262,7 +267,7 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
let s = s.as_str(); let s = s.as_str();
let s = s.trim_start_matches('@'); let s = s.trim_start_matches('@');
debug!("REMOVE USER: {}", s); debug!("REMOVE USER: {}", s);
commands.push(StatusCommand::RemoveMember(s.to_owned())); commands.push(StatusCommand::RemoveMember(s.to_lowercase()));
} }
} }
@ -270,7 +275,7 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
if let Some(s) = c.get(1) { if let Some(s) = c.get(1) {
let s = s.as_str(); let s = s.as_str();
debug!("ADD TAG: {}", s); debug!("ADD TAG: {}", s);
commands.push(StatusCommand::AddTag(s.to_owned())); commands.push(StatusCommand::AddTag(s.to_lowercase()));
} }
} }
@ -278,7 +283,7 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
if let Some(s) = c.get(1) { if let Some(s) = c.get(1) {
let s = s.as_str(); let s = s.as_str();
debug!("REMOVE TAG: {}", s); debug!("REMOVE TAG: {}", s);
commands.push(StatusCommand::RemoveTag(s.to_owned())); commands.push(StatusCommand::RemoveTag(s.to_lowercase()));
} }
} }
@ -287,7 +292,7 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
let s = s.as_str(); let s = s.as_str();
let s = s.trim_start_matches('@'); let s = s.trim_start_matches('@');
debug!("ADD ADMIN: {}", s); debug!("ADD ADMIN: {}", s);
commands.push(StatusCommand::GrantAdmin(s.to_owned())); commands.push(StatusCommand::GrantAdmin(s.to_lowercase()));
} }
} }
@ -296,7 +301,7 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
let s = s.as_str(); let s = s.as_str();
let s = s.trim_start_matches('@'); let s = s.trim_start_matches('@');
debug!("REMOVE ADMIN: {}", s); debug!("REMOVE ADMIN: {}", s);
commands.push(StatusCommand::RemoveAdmin(s.to_owned())); commands.push(StatusCommand::RemoveAdmin(s.to_lowercase()));
} }
} }

@ -46,9 +46,17 @@ impl<'a> ProcessMention<'a> {
Err(e.into()) Err(e.into())
} }
Ok(Ok(res)) => { Ok(Ok(res)) => {
debug!("Result: {:#?}", res);
if let Some(item) = res.accounts.into_iter().next() { if let Some(item) = res.accounts.into_iter().next() {
debug!("Search done, account found"); let acct_normalized = normalize_acct(&item.acct, &self.group_acct)?;
Ok(Some(item.id)) if acct_normalized == acct {
debug!("Search done, account found: {}", item.acct);
Ok(Some(item.id))
} else {
warn!("Search done but found wrong account: {}", item.acct);
Ok(None)
}
} else { } else {
debug!("Search done, nothing found"); debug!("Search done, nothing found");
Ok(None) Ok(None)
@ -80,12 +88,14 @@ impl<'a> ProcessMention<'a> {
} }
} }
async fn follow_user(&self, id: &str) -> Result<(), GroupError> { async fn follow_user_by_id(&self, id: &str) -> Result<(), GroupError> {
debug!("Trying to follow user #{}", id);
self.client.follow(id).await?; self.client.follow(id).await?;
Ok(()) Ok(())
} }
async fn unfollow_user(&self, id: &str) -> Result<(), GroupError> { async fn unfollow_user_by_id(&self, id: &str) -> Result<(), GroupError> {
debug!("Trying to unfollow user #{}", id);
self.client.unfollow(id).await?; self.client.unfollow(id).await?;
Ok(()) Ok(())
} }
@ -373,13 +383,15 @@ impl<'a> ProcessMention<'a> {
match self.config.set_member(&u, true) { match self.config.set_member(&u, true) {
Ok(_) => { Ok(_) => {
self.add_reply(format!("User {} added to the group!", u)); self.add_reply(format!("User {} added to the group!", u));
self.follow_user(&self.status_user_id) self.follow_by_acct(&u)
.await.log_error("Failed to follow"); .await.log_error("Failed to follow");
} }
Err(e) => { Err(e) => {
self.add_reply(format!("Failed to add user {} to group: {}", u, e)); self.add_reply(format!("Failed to add user {} to group: {}", u, e));
} }
} }
} else {
debug!("User was already a member");
} }
} else { } else {
self.add_reply("Only admins can manage members"); self.add_reply("Only admins can manage members");
@ -505,49 +517,48 @@ impl<'a> ProcessMention<'a> {
self.add_reply("This is a public-access group. "); self.add_reply("This is a public-access group. ");
} }
if self.config.can_write(&self.status_acct) { if self.is_admin {
if self.is_admin { self.add_reply("*You are an admin.*");
self.add_reply("*You are an admin.*"); } else if self.config.is_member(&self.status_acct) {
} else { self.add_reply("*You are a member.*");
self.add_reply("*You are a member.*"); } else if self.config.is_member_only() {
} self.add_reply("*You are not a member, ask one of the admins to add you.*");
} else { } else {
if self.config.is_member_only() { self.add_reply("*You are not a member, follow or use /join to join the group.*");
self.add_reply("*You are not a member, ask one of the admins to add you.*");
} else {
self.add_reply("*You are not a member, follow or use /join to join the group.*");
}
} }
self.add_reply("\n\ self.add_reply("\n\
To share an original post, mention the group user.\n\ To share a post, mention the group user or use one of the group hashtags. \
Replies and mentions with commands won't be shared.\n\ Replies and mentions with commands won't be shared.\n\
\n\ \n\
**Supported commands:**\n\ **Supported commands:**\n\
`/boost, /b` - boost the replied-to post into the group\n\ `/boost`, `/b` - boost the replied-to post into the group\n\
`/ignore, /i` - make the group completely ignore the post\n\ `/ignore`, `/i` - make the group ignore the post\n\
`/ping` - check that the service is alive\n\ `/ping` - check the service is alive\n\
`/tags` - show group hashtags\n\
`/join` - join the group\n\ `/join` - join the group\n\
`/leave` - leave the group"); `/leave` - leave the group");
if self.config.is_member_only() { if self.config.is_member_only() {
self.add_reply("`/members, /who` - show group members / admins"); self.add_reply("`/members`, `/who` - show group members / admins");
} else { } else {
self.add_reply("`/members, /who` - show group admins"); self.add_reply("`/members`, `/who` - show group admins");
} }
if self.is_admin { if self.is_admin {
self.add_reply("\n\ self.add_reply("\n\
**Admin commands:**\n\ **Admin commands:**\n\
`/add user` - add a member (use e-mail style address)\n\ `/add user` - add a member (use e-mail style address)\n\
`/kick, /remove user` - kick a member\n\ `/remove user` - remove a member\n\
`/ban x` - ban a user or a server\n\ `/add #hashtag` - add a group hashtag\n\
`/remove #hashtag` - remove a group hashtag\n\
`/ban x` - ban a user or server\n\
`/unban x` - lift a ban\n\ `/unban x` - lift a ban\n\
`/op, /admin user` - grant admin rights\n\ `/admin user` - grant admin rights\n\
`/deop, /deadmin user` - revoke admin rights\n\ `/deadmin user` - revoke admin rights\n\
`/opengroup` - make member-only\n\ `/opengroup` - make member-only\n\
`/closegroup` - make public-access\n\ `/closegroup` - make public-access\n\
`/announce x` - make a public announcement from the rest of the status"); `/announce x` - make a public announcement from the rest of the status (without formatting)");
} }
} }
@ -575,7 +586,7 @@ impl<'a> ProcessMention<'a> {
// admin can leave but that's a bad idea // admin can leave but that's a bad idea
let _ = self.config.set_member(&self.status_acct, false); let _ = self.config.set_member(&self.status_acct, false);
self.add_reply("You're no longer a group member. Unfollow the group user to stop receiving group messages."); self.add_reply("You're no longer a group member. Unfollow the group user to stop receiving group messages.");
self.unfollow_user(&self.status_user_id).await self.unfollow_user_by_id(&self.status_user_id).await
.log_error("Failed to unfollow"); .log_error("Failed to unfollow");
} }
} }
@ -585,7 +596,7 @@ impl<'a> ProcessMention<'a> {
debug!("Already member or admin, try to follow-back again"); debug!("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(&self.status_user_id).await self.follow_user_by_id(&self.status_user_id).await
.log_error("Failed to follow"); .log_error("Failed to follow");
} else { } else {
// Not a member yet // Not a member yet
@ -598,7 +609,7 @@ impl<'a> ProcessMention<'a> {
self.append_admin_list_to_reply(); self.append_admin_list_to_reply();
} else { } else {
// Open access, try to follow back // Open access, try to follow back
self.follow_user(&self.status_user_id).await self.follow_user_by_id(&self.status_user_id).await
.log_error("Failed to follow"); .log_error("Failed to follow");
// This only fails if the user is banned, but that is filtered above // This only fails if the user is banned, but that is filtered above
@ -618,7 +629,15 @@ impl<'a> ProcessMention<'a> {
async fn unfollow_by_acct(&self, acct: &str) -> Result<(), GroupError> { async fn unfollow_by_acct(&self, acct: &str) -> Result<(), GroupError> {
// Try to unfollow // Try to unfollow
if let Ok(Some(id)) = self.lookup_acct_id(acct, true).await { if let Ok(Some(id)) = self.lookup_acct_id(acct, true).await {
self.unfollow_user(&id).await?; self.unfollow_user_by_id(&id).await?;
}
Ok(())
}
async fn follow_by_acct(&self, acct: &str) -> Result<(), GroupError> {
// Try to unfollow
if let Ok(Some(id)) = self.lookup_acct_id(acct, false).await {
self.follow_user_by_id(&id).await?;
} }
Ok(()) Ok(())
} }

@ -17,6 +17,7 @@ use crate::error::GroupError;
use crate::store::ConfigStore; use crate::store::ConfigStore;
use crate::store::data::GroupConfig; use crate::store::data::GroupConfig;
use crate::utils::{LogError, normalize_acct, VisExt}; use crate::utils::{LogError, normalize_acct, VisExt};
use crate::command::StatusCommand;
mod handle_mention; mod handle_mention;
@ -241,7 +242,7 @@ impl GroupHandle {
Ok(()) Ok(())
} }
/// Handle a non-mention status /// Handle a non-mention status for tags
async fn handle_status(&mut self, s: Status) -> Result<(), GroupError> { async fn handle_status(&mut self, s: Status) -> Result<(), GroupError> {
debug!("Handling status #{}", s.id); debug!("Handling status #{}", s.id);
let ts = s.timestamp_millis(); let ts = s.timestamp_millis();
@ -252,11 +253,22 @@ impl GroupHandle {
return Ok(()); return Ok(());
} }
if s.in_reply_to_id.is_some() {
debug!("Status is a reply, discard");
return Ok(());
}
if !s.content.contains('#') { if !s.content.contains('#') {
debug!("No tags in status, discard"); debug!("No tags in status, discard");
return Ok(()); return Ok(());
} }
let commands = crate::command::parse_slash_commands(&s.content);
if commands.contains(&StatusCommand::Ignore) {
debug!("Post has IGNORE command, discard");
return Ok(());
}
let group_user = self.config.get_acct(); let group_user = self.config.get_acct();
let status_user = normalize_acct(&s.account.acct, group_user)?; let status_user = normalize_acct(&s.account.acct, group_user)?;

@ -82,7 +82,13 @@ async fn main() -> anyhow::Result<()> {
.await?; .await?;
if let Some(handle) = args.value_of("auth") { if let Some(handle) = args.value_of("auth") {
let handle = handle.to_lowercase();
let acct = handle.trim_start_matches('@'); let acct = handle.trim_start_matches('@');
if store.group_exists(acct).await {
anyhow::bail!("Group already exists in config!");
}
if let Some(server) = acct_to_server(acct) { if let Some(server) = acct_to_server(acct) {
let g = store let g = store
.auth_new_group(NewGroupOptions { .auth_new_group(NewGroupOptions {

@ -152,6 +152,10 @@ impl ConfigStore {
.collect() .collect()
} }
pub async fn group_exists(&self, acct : &str) -> bool {
self.data.read().await.groups.contains_key(acct)
}
/* /*
pub(crate) async fn get_group_config(&self, group: &str) -> Option<GroupConfig> { pub(crate) async fn get_group_config(&self, group: &str) -> Option<GroupConfig> {
let c = self.data.read().await; let c = self.data.read().await;
@ -191,10 +195,7 @@ impl ConfigStore {
} }
fn make_scopes() -> Scopes { fn make_scopes() -> Scopes {
Scopes::read(scopes::Read::Accounts) Scopes::read_all()
| Scopes::read(scopes::Read::Notifications)
| Scopes::read(scopes::Read::Statuses)
| Scopes::read(scopes::Read::Follows)
| Scopes::write(scopes::Write::Statuses) | Scopes::write(scopes::Write::Statuses)
| Scopes::write(scopes::Write::Media) | Scopes::write(scopes::Write::Media)
| Scopes::write(scopes::Write::Follows) | Scopes::write(scopes::Write::Follows)

@ -1,4 +1,3 @@
use std::borrow::Cow;
use std::error::Error; use std::error::Error;
use elefren::status_builder::Visibility; use elefren::status_builder::Visibility;
@ -24,14 +23,14 @@ pub(crate) fn acct_to_server(acct: &str) -> Option<&str> {
acct.trim_start_matches('@').split('@').nth(1) acct.trim_start_matches('@').split('@').nth(1)
} }
pub(crate) fn normalize_acct<'a, 'g>(acct: &'a str, group: &'g str) -> Result<Cow<'a, str>, GroupError> { pub(crate) fn normalize_acct(acct: &str, group: &str) -> Result<String, GroupError> {
let acct = acct.trim_start_matches('@'); let acct = acct.trim_start_matches('@').to_lowercase();
if acct_to_server(acct).is_some() { if acct_to_server(&acct).is_some() {
// already has server // already has server
Ok(Cow::Borrowed(acct)) Ok(acct)
} else if let Some(gs) = acct_to_server(group) { } else if let Some(gs) = acct_to_server(group) {
// attach server from the group actor // attach server from the group actor
Ok(Cow::Owned(format!("{}@{}", acct, gs))) Ok(format!("{}@{}", acct, gs))
} else { } else {
Err(GroupError::BadConfig( Err(GroupError::BadConfig(
format!("Group acct {} is missing server!", group).into(), format!("Group acct {} is missing server!", group).into(),
@ -81,6 +80,14 @@ mod test {
Ok("piggo@piggo.space".into()), Ok("piggo@piggo.space".into()),
normalize_acct("piggo@piggo.space", "uhh") normalize_acct("piggo@piggo.space", "uhh")
); );
assert_eq!(
Ok("piggo@piggo.space".into()),
normalize_acct("piGGgo@pIggo.spaCe", "uhh")
);
assert_eq!(
Ok("piggo@banana.nana".into()),
normalize_acct("piGGgo", "foo@baNANA.nana")
);
assert_eq!(Err(GroupError::BadConfig("_".into())), normalize_acct("piggo", "uhh")); assert_eq!(Err(GroupError::BadConfig("_".into())), normalize_acct("piggo", "uhh"));
} }
} }

Loading…
Cancel
Save