Skip to content

Commit

Permalink
add <> support, improve whitespace handling and update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
untitaker committed Oct 13, 2024
1 parent 3933637 commit 45a49f0
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 53 deletions.
51 changes: 31 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,46 @@ maintain. So what if those lists updated themselves?

## How to use

Create a list with any of the following names. The bot will then start populating it.
Create these lists to get started:

* `#last_status_at>1d` -- contains all users which haven't posted in a **day** or more.
* `#last_status_at>1w` -- contains all users which haven't posted in a **week** or more.
* `#last_status_at>1m` -- contains all users which haven't posted in a **month** or more.
* `#mutuals` -- contains all users who you follow and who also follow you.
* `#last_status_at>1d & last_status_at<4d` -- contains all users who haven't
posted yesterday, but sometime within the past three days.
* `#last_status_at>3d` -- contains all users who haven't posted in over three
days.

Variations such as `2d`, `3d`, `8m` are permitted.
Then, in the client of your choice, add those lists as columns or tabs, so you
can easily switch between home timeline and alternative timelines. In Mastodon
web they are already tabs, in [Phanpy](https://phanpy.social/) I recommend the
column layout, in [Tusky](https://github.com/tuskyapp/Tusky/) you can add them
as tabs as well.

Then, head over to [list-bot.woodland.cafe](https://list-bot.woodland.cafe/)
and sign in with your Mastodon account. Click sync, and the bot should start
adding users to the list (asynchronously).

The bot has been successfully tested on GoToSocial as well.

## Syntax reference

* `#last_status_at` supports days (`1d`), weeks (`1w`), months (`1m`). It does
not support numbers larger than 999 (`9999m` is invalid)
* `#last_status_at` supports operators `<` and `>`. Other operators may be
added if it's useful, but so far it doesn't seem that it would be.
* `#mutuals` takes no arguments of any kind.
* Clauses can be chained with `&`. Other operators or parenthesis are not
supported.

List names do not have to match exactly, they only have to end with the
specified string. For example, it is permitted to name a list `My best friends
#mutuals`, so that your preferred list name is shown while the
"machine-readable configuration" is still there. There can currently however
only be one `#` in the name.

List clauses can be composed:

* `#last_status_at>1d & mutuals` -- contains all mutuals who haven't posted in a day or more.

## Using the bot as a service

This bot is available as a webservice at
[list-bot.woodland.cafe](https://list-bot.woodland.cafe/). Sign in with
Mastodon and get started.

## Using the bot from your own machine
## Self-hosting

Create an empty list with the name `#last_status_at>1w`. The program will recognize it
by its name, and overwrite its contents with users who haven't posted in a week.
list-bot comes as a CLI to put into crontab, and as a webservice. For
single-user purposes, it's probably easier to run it from the CLI.

Go to Development in your Mastodon account, and create a new access token.

Expand All @@ -54,8 +65,8 @@ Then, run:
RUST_LOG=info cargo run run-once --host=mastodon.social --token=...
```

Your list is now populated with new accounts. Run this program periodically to
update it (this both adds and removes accounts).
Your lists are now populated with new accounts. Run this program periodically
to update it (this both adds and removes accounts).

## Caveats

Expand Down
93 changes: 60 additions & 33 deletions src/list_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const UPDATE_CHUNK_SIZE: usize = 250;

#[derive(Debug, Clone, Eq, PartialEq)]
enum ListManagerTerm {
LastStatus(Days),
LastStatus { is_gt: bool, days: Days },
Mutuals,
}

Expand All @@ -26,7 +26,7 @@ impl FromStr for ListManagerTerms {

let whitespace = || sym(b' ').repeat(0..).discard();
let duration_days =
(one_of(b"123456789").repeat(1..2) + one_of(b"dwm")).map(|(number, unit)| {
(one_of(b"123456789").repeat(1..3) + one_of(b"dwm")).map(|(number, unit)| {
let unit = match unit {
b'd' => 1,
b'w' => 7,
Expand All @@ -38,11 +38,17 @@ impl FromStr for ListManagerTerms {
Days::new(number * unit)
});

let last_status_at =
seq(b"last_status_at>") * duration_days.map(ListManagerTerm::LastStatus);
let last_status_at = seq(b"last_status_at")
* whitespace()
* (one_of(b"<>") + whitespace() * duration_days).map(|(op, days)| {
ListManagerTerm::LastStatus {
is_gt: op == b'>',
days,
}
});
let mutuals = seq(b"mutuals").map(|_| ListManagerTerm::Mutuals);
let term = last_status_at | mutuals;
let and_symbol = whitespace() + sym(b'&').repeat(1..) + whitespace();
let and_symbol = whitespace() * sym(b'&').repeat(1..) * whitespace();
let and_term = list(term, and_symbol).convert(|terms| match terms.len() {
0 => Err("empty filter"),
_ => Ok(ListManagerTerms(terms)),
Expand Down Expand Up @@ -78,18 +84,26 @@ impl ListManager {

for term in &self.terms.0 {
let term_result = match term {
ListManagerTerm::LastStatus(days) => api_cache
.get_follows(client)
.await?
.iter()
.filter(|account| {
account
.last_status_at
.map_or(true, |x| x < Local::now().date_naive() - *days)
})
.map(|account| account.id.clone())
.collect(),

ListManagerTerm::LastStatus { is_gt, days } => {
let is_gt = *is_gt;
let cutoff = Local::now().date_naive() - *days;
api_cache
.get_follows(client)
.await?
.iter()
.filter(|account| {
let Some(last_status_at) = account.last_status_at else {
return is_gt;
};
if is_gt {
last_status_at < cutoff
} else {
last_status_at > cutoff
}
})
.map(|account| account.id.clone())
.collect()
}
ListManagerTerm::Mutuals => {
let follows = api_cache.get_follows(client).await?;
let follow_ids = follows.iter().map(|account| account.id.clone()).collect();
Expand Down Expand Up @@ -230,27 +244,31 @@ fn parsing() {
);
assert_eq!(
ListManagerTerms::from_str("#last_status_at>2d"),
Ok(ListManagerTerms(vec![ListManagerTerm::LastStatus(
Days::new(2)
)]))
Ok(ListManagerTerms(vec![ListManagerTerm::LastStatus {
is_gt: true,
days: Days::new(2)
}]))
);
assert_eq!(
ListManagerTerms::from_str("#last_status_at>1w"),
Ok(ListManagerTerms(vec![ListManagerTerm::LastStatus(
Days::new(7)
)]))
Ok(ListManagerTerms(vec![ListManagerTerm::LastStatus {
is_gt: true,
days: Days::new(7)
}]))
);
assert_eq!(
ListManagerTerms::from_str("#last_status_at>1m"),
Ok(ListManagerTerms(vec![ListManagerTerm::LastStatus(
Days::new(30)
)]))
ListManagerTerms::from_str("#last_status_at > 1m"),
Ok(ListManagerTerms(vec![ListManagerTerm::LastStatus {
is_gt: true,
days: Days::new(30)
}]))
);
assert_eq!(
ListManagerTerms::from_str("hello #last_status_at>1m"),
Ok(ListManagerTerms(vec![ListManagerTerm::LastStatus(
Days::new(30)
)]))
Ok(ListManagerTerms(vec![ListManagerTerm::LastStatus {
is_gt: true,
days: Days::new(30)
}]))
);
}

Expand All @@ -260,22 +278,31 @@ fn parsing_and() {
assert_eq!(
ListManagerTerms::from_str("hello #last_status_at>1m&mutuals"),
Ok(ListManagerTerms(vec![
ListManagerTerm::LastStatus(Days::new(30)),
ListManagerTerm::LastStatus {
is_gt: true,
days: Days::new(30)
},
ListManagerTerm::Mutuals
]))
);
assert_eq!(
ListManagerTerms::from_str("hello #last_status_at>1m & mutuals"),
Ok(ListManagerTerms(vec![
ListManagerTerm::LastStatus(Days::new(30)),
ListManagerTerm::LastStatus {
is_gt: true,
days: Days::new(30)
},
ListManagerTerm::Mutuals
]))
);

assert_eq!(
ListManagerTerms::from_str("hello #last_status_at>1m && mutuals"),
Ok(ListManagerTerms(vec![
ListManagerTerm::LastStatus(Days::new(30)),
ListManagerTerm::LastStatus {
is_gt: true,
days: Days::new(30)
},
ListManagerTerm::Mutuals
]))
);
Expand Down

0 comments on commit 45a49f0

Please sign in to comment.