fedi2html/fedi2html.sh

269 lines
7.9 KiB
Bash
Executable File
Raw 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: fedi2html.sh
# Desc:
# Reqs:
# Date:
#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
POST_TEMPLATE='
<article class="comment">
<a class="user" href="$ACCOUNT_URL">
<img class="avatar" src="$ACCOUNT_AVATAR">
<strong class="username">$ACCOUNT_NAME</strong>
<em class="useraddress">$ACCOUNT_ID</em>
</a>
<a class="address" href="$POST_URL">
<time title="$POST_DATE">$(date --date=$POST_DATE)</time>
</a>
<section>
$POST_CONTENT
</section>
<div class="attachments">
$POST_ATTACHMENTS
</div>
<div class="responses">
$POST_RESPONSES
</div>
</article>
'
ATTACH_TEMPLATE='
<a href="$ATTACH_URL">
<div class="attachment">
<strong>$ATTACH_NAME</strong>
$ATTACH_DESC
</div>
</a>'
ATTACH_IMAGE_TEMPLATE='
<figure>
<a href="$ATTACH_URL">
<img class="attachment" src="$ATTACH_URL" alt="$ATTACH_DESC" title="$ATTACH_DESC">
</a>
<figcaption>
<a href="$ATTACH_URL"><b>$ATTACH_NAME</b></a><br>
$(if test $(echo "$ATTACH_DESC" | wc -c) -gt 52; then
echo $ATTACH_DESC | head -c 53 | sed 's%[[:space:]]*$%%'
echo …
else
echo $ATTACH_DESC
fi)
</figcaption>
</figure>
'
EMOJI_TEMPLATE='<img class="emoji" src="$EMOJI_URL" alt="$EMOJI_SHORTCODE" title="$EMOJI_SHORTCODE">'
# Given a notes JSON, render it as HTML.
# The most important part of the script!
# render_post $post_data $context_data $tree_level
render_post() {
local post_data="$1"
local responses_data="$2"
local POST_TREE_LEVEL="$3"
local ACCOUNT_URL="$(echo "$post_data" | jq -r .account.url)"
local ACCOUNT_ID="$(echo "$post_data" | jq -r .account.fqn)"
local ACCOUNT_NAME="$(echo "$post_data" | jq -r .account.display_name | replace_emojis "$(echo "$post_data" | jq -r '.account')")"
local ACCOUNT_AVATAR="$(echo "$post_data" | jq -r .account.avatar)"
local POST_URL="$(echo "$post_data" | jq -r .url)"
local POST_DATE="$(echo "$post_data" | jq -r .created_at)"
local POST_CONTENT="$(echo "$post_data" | jq -r .content | replace_emojis "$post_data")"
local POST_ATTACHMENTS="$(media_attachments "$post_data")"
local POST_RESPONSES="$(render_responses "$post_data" "$responses_data" "$POST_TREE_LEVEL")"
env_subst "$POST_TEMPLATE"
}
# Render a posts responses one-by-one and recursively.
# Each branch of the response tree will be rendered completely before proceeding
# to the next.
# render_responses $post_data $context_data $tree_level
render_responses() {
local post_data="$1"
local responses_data="$2"
local level="$3"
if test -z "$level"; then level=0; fi
local id="$(echo "$post_data" | jq -r '.id')"
local responses="$(echo "$responses_data" | grep "in_reply_to_id.*$id")"
local IFS="
"
for response in $responses; do
render_post "$response" "$responses_data" "$(expr "$level" + 1)"
done
}
# Accepts a string over stdin; it will replace all emoji shortcodes along stdin
# input with appropriate <img> HTML, based on $EMOJI_TEMPLATE, and based on
# a posts JSON.
# echo ":blobcat:" | replace_emojis $post_data
replace_emojis() {
local post_data="$1"
local emojis="$(echo "$post_data" | jq -r '.emojis[]|(.url + "\t" + .shortcode)')"
local temp="$(mktemp)"
local IFS="
"
cat > "$temp"
for line in $emojis; do
local EMOJI_URL="$(echo "$line" | awk -F'\t' '{print $1}')"
local EMOJI_SHORTCODE="$(echo "$line" | awk -F'\t' '{print $2}')"
local value="$(env_subst "$EMOJI_TEMPLATE")"
sed -i "s%:${EMOJI_SHORTCODE}:%${value}%g" "$temp"
done
cat "$temp"
rm "$temp"
}
# Given a posts JSON data, return the appropriate HTML corresponding to its
# media attachments, if any. Will return an empty string if none.
# media_attachments $post_data
media_attachments() {
local post_data="$1"
local attachments="$(echo "$post_data" | jq -r '.media_attachments[]|(.type + "\t" + .url + "\t" + .description + "\t" + .preview_url)')"
local IFS="
"
for line in $attachments; do
local ATTACH_TYPE="$(echo "$line" | awk -F'\t' '{print $1}')"
local ATTACH_URL="$(echo "$line" | awk -F'\t' '{print $2}')"
local ATTACH_DESC="$(echo "$line" | awk -F'\t' '{print $3}')"
local ATTACH_PREVIEW="$(echo "$line" | awk -F'\t' '{print $4}')"
local ATTACH_NAME="$(basename "$ATTACH_URL")"
if test "$ATTACH_TYPE" = "image"; then
env_subst "$ATTACH_IMAGE_TEMPLATE"
else
env_subst "$ATTACH_TEMPLATE"
fi
done
}
# Pass a posts context JSON along stdin; out comes the response_data in JSON.
# fetch_post_context $url | context_to_responses
context_to_responses() {
jq -cr '.descendants[] |= sort_by(.created_at)'
}
# Make a request to the /api/v1/statuses/:id/$request API endpoint.
# statuses_api_request $post_url $request
statuses_api_request() {
local post_url="$1"
local api_request="$2"
if test -n "$api_request"; then
api_request="/$api_request"
fi
local id="$(url_id "$url")"
local server="$(url_server "$url")"
curl --location --header 'Accept: application/json,application/activity+json' \
"$server/api/v1/statuses/${id}${api_request}"
}
# Require the context-JSON of a post, by URL.
# fetch_post_context $url
fetch_post_context() {
local url="$1"
statuses_api_request "$url" "context"
}
# Given a post URL, request its JSON.
# fetch_post $url
fetch_post() {
local url="$1"
statuses_api_request "$url"
}
# Return the ID of a post, based on its URL.
# url_id $url
url_id() {
local url="$1"
# Pleroma-style URLs: https://jam.xwx.moe/notice/Ac6PIZAP0ZzkMTYBBg
# Mastodon-style URLs: https://esperanto.masto.host/@minjo/111461250815264185
if echo "$url" | grep "/notice/" > /dev/null; then
echo "$url" \
| sed 's%.*/notice/%%'
else
echo "$url" \
| sed 's%.*/@[[:alnum:]]*/%%'
fi
}
# Return the server (including protocol) of a post, based on its URL.
# url_server $url
url_server() {
local url="$1"
local protocol="$(echo "$url" | grep --only-matching '[[:alnum:]]*://')"
printf "$protocol"
echo "$url" \
| sed 's%^'"$protocol"'%%' \
| sed 's%/.*%%'
}
# 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")\""
}
usage() {
echo "usage: $(basename "$0") POST_URL"
echo
echo "$(basename "$0") does exactly what it says on the tin: It formats"
echo "a fediverse post (and its replies) into simple-and-embeddable HTML."
echo
echo "It works with posts from any server that supports Mastodons API,"
echo "including Pleroma, Akkoma, Glitch, etc."
echo
echo "Notably, it supports post-atachments and custom-emoji. Keep in mind"
echo "that images are all fetched from remote sources. It is recommended,"
echo "if privacy or total archival, is a concern, to use wget(1)s --mirror"
echo "(or something like it) to fetch even these foreign files."
}
URL="$1"
if test -z "$URL"; then
usage 1>&2
exit 2
elif test "$URL" = "-h" -o "$URL" = "--help"; then
usage
exit 0
fi
POST="$(fetch_post "$URL")"
RESPONSES="$(fetch_post_context "$URL" | context_to_responses)"
render_post "$POST" "$RESPONSES" 0