254 lines
8.1 KiB
Bash
Executable File
254 lines
8.1 KiB
Bash
Executable File
#!/bin/sh
|
||
#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
|
||
# Name: manicito (Manic-citation)
|
||
# Desc: A Mastodon/Pleroma bot which Quote-posts all posts of the given hashtag.
|
||
# Reqs: curl, shell, jq
|
||
# Date: 2024-04-10
|
||
#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
|
||
|
||
if test -z "$MANICITO_TEMPLATE"; then
|
||
MANICITO_TEMPLATE='$USER (<a href="$USER_URL">$USERHOST</a>) <a href="$POST_URL">posted</a> about $HASHTAG: <blockquote>$(echo "$POST" | head -c 200)</blockquote>'
|
||
fi
|
||
|
||
# Prints a simple usage message.
|
||
usage() {
|
||
echo "usage: $(basename $0) [-h] [-H HISTORY_FILE] HASHTAG SERVER_URL"
|
||
echo 'Mastodon/Pleroma/etc script to quote-posts all posts of a given hashtag.'
|
||
echo
|
||
echo ' -h print this message and exit'
|
||
echo ' -H file used to store/check quoted posts; to avoid duplicates'
|
||
echo ' -Q disable actual quote-posting (which Mastodon doesn’t support)'
|
||
echo ' -s scope for posts, one of: public/[unlisted]/private/direct'
|
||
echo
|
||
echo 'Posts tagged under HASHTAG will all be quote-posted to the'
|
||
echo 'Mastodon-compatible server at SERVER_URL.'
|
||
echo
|
||
echo 'In order to make these posts, you must set the environment variable $FEDI_AUTH'
|
||
echo 'to your authorization token. To find your authorization token, you can snoop '
|
||
echo 'through request headers in your web-browser. In Firefox, you can do:'
|
||
echo ' Developer Tools (F12) → Network → Headers'
|
||
echo 'Look for your $FEDI_AUTH value in the Authorization header like so:'
|
||
echo ' Authorization: Bearer $FEDI_AUTH'
|
||
echo
|
||
echo 'To avoid duplicate-posts, you should specify a “history” file with the -H'
|
||
echo 'parameter. With this, all quoted post IDs will be saved in this file, and'
|
||
echo 'on subsequent runs (so long as you don’t change the history-file), no post'
|
||
echo 'will be quoted twice.'
|
||
echo
|
||
echo 'The environment variable $MANICITO_TEMPLATE is used create the body of your'
|
||
echo 'quote-posts. It substitutes the following shell-variables in templates:'
|
||
echo ' • $HASHTAG'
|
||
echo ' • $POST'
|
||
echo ' • $POST_URL'
|
||
echo ' • $USER'
|
||
echo ' • $USER_URL'
|
||
}
|
||
|
||
|
||
# Fetch a JSON array of posts of a hashtag. If a minimum post ID is provided,
|
||
# only posts older than that one will be returned.
|
||
# fetch_hashtag_posts $server $hashtag $minimum_id
|
||
fetch_hashtag_posts() {
|
||
local server="$1"
|
||
local hashtag="$2"
|
||
local minimum_id="$3"
|
||
|
||
if test -n "$minimum_id"; then
|
||
curl --fail "$server/api/v1/timelines/tag/$hashtag?min_id=$minimum_id"
|
||
else
|
||
curl --fail "$server/api/v1/timelines/tag/$hashtag"
|
||
fi
|
||
}
|
||
|
||
|
||
# Given a post’s JSON, create and submit a quote-post for it.
|
||
# quote_post $tagged_post_json
|
||
quote_post() {
|
||
local tagged_post_json="$1"
|
||
curl --fail \
|
||
--request POST \
|
||
--header "Authorization: Bearer $FEDI_AUTH" \
|
||
--header 'Content-Type: application/json' \
|
||
--header "Idempotency-Key: $(echo "$tagged_post_json" | jq -r .id)" \
|
||
--data "$(post_json "$tagged_post_json")" "$FEDI_SERVER/api/v1/statuses"
|
||
}
|
||
|
||
|
||
# Given a JSON-array of posts over stdin, create and submit a quote-post for each one.
|
||
# $posts_json_array | quote_posts
|
||
quote_posts() {
|
||
local IFS="
|
||
"
|
||
while read -r tagged_post_line; do
|
||
quote_post "$tagged_post_line"
|
||
if test "$?" -ne 0; then
|
||
echo "Failed to post about post $(echo "$tagged_post_line" | jq .id)!" 1>&2
|
||
exit 1
|
||
fi
|
||
echo "$tagged_post_line"
|
||
done
|
||
}
|
||
|
||
|
||
# Given a post’s JSON, return JSON of a quote-post quoting it.
|
||
# post_json $tagged_post-json
|
||
post_json() {
|
||
local tagged_post_json="$1"
|
||
printf '{ "content_type": "text/html", "visibility": "%s",' "$FEDI_SCOPE"
|
||
if test -z "$NO_QUOTE_POSTS"; then
|
||
printf '"quote_id": "%s",' "$(echo "$tagged_post_json" | jq -r .id)"
|
||
fi
|
||
printf '"expires_in": %s,' 864000
|
||
printf '"status": "%s" }\n' "$(post_body "$tagged_post_json")"
|
||
}
|
||
|
||
|
||
# Output the contents of our quote-post’s body about the given tagged-post’s JSON.
|
||
# This uses the global variable $MANICITO as our template, replacing the variables
|
||
# $POST, $POST_URL, $USER, $USER_URL, and $USERHOST.
|
||
# post_body $tagged_post_json
|
||
post_body() {
|
||
local tagged_post_json="$1"
|
||
POST="$(echo "$tagged_post_json" | jq -r .content | tr -d "\"'\n")"
|
||
POST_URL="$(echo "$tagged_post_json" | jq -r .uri)"
|
||
USER="$(echo "$tagged_post_json" | jq -r .account.display_name)"
|
||
USER_URL="$(echo "$tagged_post_json" | jq -r .account.url)"
|
||
USERHOST="$(echo "$tagged_post_json" | jq -r .account.acct)"
|
||
env_subst "$MANICITO_TEMPLATE"
|
||
}
|
||
|
||
|
||
# Receives posts in a JSON array over stdin. Given a newline-delimited list of
|
||
# post IDs ($ids_to_filter), all matching posts from the JSON array will be
|
||
# filted out.# sent over stdin that match one of these posts IDs will be filtered out.
|
||
# echo $post_array_json | filter_posts $ids_to_filter
|
||
filter_posts() {
|
||
local ids_to_filter="$1"
|
||
# If the list has more than give characters, use it to filter… (sanity check)
|
||
if test -n "$ids_to_filter"; then
|
||
jq -cr .[] \
|
||
| grep --invert-match $(echo "$ids_to_filter" | sed 's%^%-e %')
|
||
else
|
||
jq -cr .[]
|
||
fi
|
||
}
|
||
|
||
|
||
history_post_ids() {
|
||
local history_file="$1"
|
||
if test -f "$history_file"; then
|
||
awk -F '\t' '{print $2}' "$history_file" \
|
||
| grep -v '^[[:space:]]*$'
|
||
fi
|
||
}
|
||
|
||
|
||
# Pipe in fediverse posts in JSON format; these will all be echoed into the given
|
||
# “history file”, so that they can be filtered out and avoided on subsequent
|
||
# runs.
|
||
# echo $newline_delimited_post_jsons | update_filter $history_file
|
||
update_filter() {
|
||
local history_file="$1"
|
||
if test -n "$history_file"; then
|
||
jq -r '(.created_at + "\t" + .id)' \
|
||
| sort -n \
|
||
>> "$history_file"
|
||
fi
|
||
}
|
||
|
||
|
||
# Sanitize a template-string.
|
||
# AKA, escape quotation-marks.
|
||
# prep_template $template
|
||
prep_template() {
|
||
local template="$1"
|
||
echo "$template" \
|
||
| sed 's%\"%\\\"%g'
|
||
}
|
||
|
||
|
||
# Rough replacement for gettext’s envsubst. Safe!
|
||
# This will evaluate a string’s shell variables (somewhat) safely
|
||
# Probably not good for general use — but for our purposes, it only subsitutes
|
||
# along the “first level.” So environment variables are replaced, but those
|
||
# variables’ contents (AKA, post contents) aren’t evaluated.
|
||
# env_subst $template
|
||
# env_subst "$SHELL" → "/bin/sh"
|
||
env_subst() {
|
||
local template="$1"
|
||
eval "echo \"$(prep_template "$template")\""
|
||
}
|
||
|
||
|
||
FEDI_SCOPE="unlisted" # Default value.
|
||
|
||
|
||
while getopts 'hH:s:Q' arg; do
|
||
case $arg in
|
||
h)
|
||
usage
|
||
exit 0
|
||
;;
|
||
H)
|
||
HISTORY_FILE="$OPTARG"
|
||
;;
|
||
s)
|
||
OPTARG="$(echo "$OPTARG" | tr 'A-Z' 'a-z')"
|
||
if test "$OPTARG" = "unlisted" -o "$OPTARG" = "public" \
|
||
-o "$OPTARG" = "private" -o "$OPTARG" = "direct"; then
|
||
FEDI_SCOPE="$OPTARG"
|
||
else
|
||
1>&2 echo '$OPTARG is an invalid scope; must be one of:'
|
||
1>&2 echo ' • unlisted'
|
||
1>&2 echo ' • public'
|
||
1>&2 echo ' • private'
|
||
1>&2 echo ' • direct'
|
||
1>&2 echo
|
||
1>&2 echo 'Run with -h for more information.'
|
||
exit 6
|
||
fi
|
||
;;
|
||
Q)
|
||
NO_QUOTE_POSTS="1"
|
||
;;
|
||
esac
|
||
done
|
||
|
||
|
||
shift $((OPTIND-1))
|
||
HASHTAG="$1"
|
||
FEDI_SERVER="$2"
|
||
|
||
|
||
if test -z "$HASHTAG"; then
|
||
usage
|
||
exit 1
|
||
fi
|
||
|
||
if test -z "$FEDI_AUTH"; then
|
||
1>&2 echo 'You need to set the environment variable $FEDI_AUTH!'
|
||
1>&2 echo 'You can find your auth key by examining the "Authorization: Bearer" header'
|
||
1>&2 echo "used in requests by your server's web-client."
|
||
1>&2 echo 'In Firefox, F12→Network.'
|
||
1>&2 echo ""
|
||
exit 2
|
||
fi
|
||
|
||
if test -z "$FEDI_SERVER"; then
|
||
1>&2 echo 'No server specified!'
|
||
1>&2 echo 'Make sure to provide a hashtag (like #esperanto) and a'
|
||
1>&2 echo 'server URL (like https://fedi.server) as the last two arguments.'
|
||
1>&2 echo
|
||
1>&2 echo 'Run with -h for more information.'
|
||
exit 3
|
||
fi
|
||
|
||
|
||
POST_IDS_TO_IGNORE="$(history_post_ids "$HISTORY_FILE")"
|
||
MOST_RECENT_POST="$(echo "$POST_IDS_TO_IGNORE" | tail -1)"
|
||
|
||
fetch_hashtag_posts "$FEDI_SERVER" "$HASHTAG" "$MOST_RECENT_POST" \
|
||
| filter_posts "$POST_IDS_TO_IGNORE" \
|
||
| quote_posts \
|
||
| update_filter "$HISTORY_FILE"
|