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. 69
      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
## 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 commands
- Code reorganization

4
Cargo.lock generated

@ -276,7 +276,7 @@ checksum = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e"
[[package]]
name = "elefren"
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 = [
"chrono",
"doc-comment",
@ -328,7 +328,7 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]]
name = "fedigroups"
version = "0.2.1"
version = "0.2.2"
dependencies = [
"anyhow",
"clap",

@ -1,6 +1,6 @@
[package]
name = "fedigroups"
version = "0.2.1"
version = "0.2.2"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018"
publish = false
@ -10,7 +10,7 @@ build = "build.rs"
[dependencies]
#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"

@ -116,7 +116,7 @@ Any user (member in member-only groups) can post to the group by mentioning the
### 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.

@ -134,7 +134,7 @@ pub fn parse_status_tags(content: &str) -> Vec<String> {
let mut tags = vec![];
for c in RE_A_HASHTAG.captures_iter(&content) {
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);
debug!("Stripped tags: {}", content);
if !content.contains('/') && !content.contains('\\') {
// No slash = no command
return vec![];
}
// short-circuiting commands
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.trim_start_matches('@');
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.trim_start_matches('@');
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) {
if let Some(s) = c.get(1) {
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) {
if let Some(s) = c.get(1) {
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.trim_start_matches('@');
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.trim_start_matches('@');
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) {
let s = s.as_str();
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) {
let s = s.as_str();
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.trim_start_matches('@');
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.trim_start_matches('@');
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())
}
Ok(Ok(res)) => {
debug!("Result: {:#?}", res);
if let Some(item) = res.accounts.into_iter().next() {
debug!("Search done, account found");
let acct_normalized = normalize_acct(&item.acct, &self.group_acct)?;
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 {
debug!("Search done, nothing found");
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?;
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?;
Ok(())
}
@ -373,13 +383,15 @@ impl<'a> ProcessMention<'a> {
match self.config.set_member(&u, true) {
Ok(_) => {
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");
}
Err(e) => {
self.add_reply(format!("Failed to add user {} to group: {}", u, e));
}
}
} else {
debug!("User was already a member");
}
} else {
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. ");
}
if self.config.can_write(&self.status_acct) {
if self.is_admin {
self.add_reply("*You are an admin.*");
} else {
} else if self.config.is_member(&self.status_acct) {
self.add_reply("*You are a member.*");
}
} else {
if self.config.is_member_only() {
} else if self.config.is_member_only() {
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\
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\
\n\
**Supported commands:**\n\
`/boost, /b` - boost the replied-to post into the group\n\
`/ignore, /i` - make the group completely ignore the post\n\
`/ping` - check that the service is alive\n\
`/boost`, `/b` - boost the replied-to post into the group\n\
`/ignore`, `/i` - make the group ignore the post\n\
`/ping` - check the service is alive\n\
`/tags` - show group hashtags\n\
`/join` - join the group\n\
`/leave` - leave the group");
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 {
self.add_reply("`/members, /who` - show group admins");
self.add_reply("`/members`, `/who` - show group admins");
}
if self.is_admin {
self.add_reply("\n\
**Admin commands:**\n\
`/add user` - add a member (use e-mail style address)\n\
`/kick, /remove user` - kick a member\n\
`/ban x` - ban a user or a server\n\
`/remove user` - remove a member\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\
`/op, /admin user` - grant admin rights\n\
`/deop, /deadmin user` - revoke admin rights\n\
`/admin user` - grant admin rights\n\
`/deadmin user` - revoke admin rights\n\
`/opengroup` - make member-only\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
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.unfollow_user(&self.status_user_id).await
self.unfollow_user_by_id(&self.status_user_id).await
.log_error("Failed to unfollow");
}
}
@ -585,7 +596,7 @@ impl<'a> ProcessMention<'a> {
debug!("Already member or admin, try to follow-back again");
// Already a member, so let's try to follow the user
// 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");
} else {
// Not a member yet
@ -598,7 +609,7 @@ impl<'a> ProcessMention<'a> {
self.append_admin_list_to_reply();
} else {
// 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");
// 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> {
// Try to unfollow
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(())
}

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

@ -82,7 +82,13 @@ async fn main() -> anyhow::Result<()> {
.await?;
if let Some(handle) = args.value_of("auth") {
let handle = handle.to_lowercase();
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) {
let g = store
.auth_new_group(NewGroupOptions {

@ -152,6 +152,10 @@ impl ConfigStore {
.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> {
let c = self.data.read().await;
@ -191,10 +195,7 @@ impl ConfigStore {
}
fn make_scopes() -> Scopes {
Scopes::read(scopes::Read::Accounts)
| Scopes::read(scopes::Read::Notifications)
| Scopes::read(scopes::Read::Statuses)
| Scopes::read(scopes::Read::Follows)
Scopes::read_all()
| Scopes::write(scopes::Write::Statuses)
| Scopes::write(scopes::Write::Media)
| Scopes::write(scopes::Write::Follows)

@ -1,4 +1,3 @@
use std::borrow::Cow;
use std::error::Error;
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)
}
pub(crate) fn normalize_acct<'a, 'g>(acct: &'a str, group: &'g str) -> Result<Cow<'a, str>, GroupError> {
let acct = acct.trim_start_matches('@');
if acct_to_server(acct).is_some() {
pub(crate) fn normalize_acct(acct: &str, group: &str) -> Result<String, GroupError> {
let acct = acct.trim_start_matches('@').to_lowercase();
if acct_to_server(&acct).is_some() {
// already has server
Ok(Cow::Borrowed(acct))
Ok(acct)
} else if let Some(gs) = acct_to_server(group) {
// attach server from the group actor
Ok(Cow::Owned(format!("{}@{}", acct, gs)))
Ok(format!("{}@{}", acct, gs))
} else {
Err(GroupError::BadConfig(
format!("Group acct {} is missing server!", group).into(),
@ -81,6 +80,14 @@ mod test {
Ok("piggo@piggo.space".into()),
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"));
}
}

Loading…
Cancel
Save