manicito/manicito

254 lines
8.1 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 doesnt 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 dont 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 posts 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 posts 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-posts body about the given tagged-posts 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 gettexts envsubst. Safe!
# This will evaluate a strings 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) arent 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"