twtxt-v2.sh
Raw
#!/bin/bash
# twtxt.sh - A simple script to manage a Twtxt v2 feed with following capabilities
FEED_FILE="twtxt.txt"
CONFIG_DIR="$HOME/.config/twtxt"
FOLLOWING_FILE="$CONFIG_DIR/following.txt"
FEEDS_DIR="$CONFIG_DIR/feeds"
TIMELINE_FILE="$CONFIG_DIR/timeline.txt"
FEED_URLS_FILE="$CONFIG_DIR/feed_urls.txt"
# Create configuration directories if they don't exist
mkdir -p "$CONFIG_DIR"
mkdir -p "$FEEDS_DIR"
# Initialize feed file if it doesn't exist
if [ ! -f "$FEED_FILE" ]; then
echo "# nick: ${USER}" >"$FEED_FILE"
echo "# url: https://yourdomain.com/$FEED_FILE" >>"$FEED_FILE"
echo "" >>"$FEED_FILE"
fi
# Get the URL of a feed from its metadata file
# $1 - path to the metadata file
get_feed_url() {
grep -m1 -E "^#\s*url\s*[:=]\s*" "$1" | tail -1 | sed 's/^#\s*url\s*[:=]\s*//'
}
# Get the nick of a feed from its metadata file
# $1 - path to the metadata file
get_feed_nick() {
grep -m1 -E "^#\s*nick\s*[:=]\s*" "$1" | tail -1 | sed 's/^#\s*nick\s*[:=]\s*//'
}
# Check if GNU date is available
#
# GNU date is needed to support the -d option for parsing dates in any format.
# If GNU date is not available, the script will not be able to parse dates
# in other formats than the default ISO 8601 format.
check_gnu_date() {
if date --version >/dev/null 2>&1; then
DATE_IS_GNU=1
else
DATE_IS_GNU=0
fi
}
# Convert a timestamp in ISO 8601 format to a UNIX timestamp
#
# $1 - timestamp in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)
#
# Returns the timestamp as a UNIX timestamp (seconds since Jan 1, 1970, 00:00:00 UTC)
timestamp_to_unix() {
local timestamp="$1"
# Remove fractional seconds if present
timestamp=$(echo "$timestamp" | sed -E 's/\.[0-9]+//')
if [ "$DATE_IS_GNU" -eq 1 ]; then
# GNU date
date -u -d "$timestamp" +"%s" 2>/dev/null
else
# BSD date (macOS)
date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$timestamp" +"%s" 2>/dev/null
fi
}
# Calculate the Twt Hash for a given feed URL, timestamp, and content
#
# $1 - feed URL
# $2 - timestamp
# $3 - content
#
# Returns the first 11 characters of the SHA-256 hash of the concatenated
# components, separated by newline characters.
calculate_hash() {
local feed_url timestamp content data hash
feed_url="$1"
timestamp="$2"
content="$3"
# Concatenate components with newline separators
data="${feed_url}\n${timestamp}\n${content}"
# Calculate SHA-256 hash and get the first 11 characters
hash=$(echo -e "$data" | sha256sum | awk '{print $1}' | cut -c1-11)
echo "$hash"
}
# Post a new twt to the feed file
# $1 - the content of the twt
#
# Appends the twt to the feed file and calculates the Twt Hash
post_twt() {
local content timestamp feed_url hash
content="$1"
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
feed_url=$(get_feed_url "$FEED_FILE")
# Append the twt to the feed file
echo -e "${timestamp}\t${content}" >>"$FEED_FILE"
# Calculate and display the Twt Hash
hash=$(calculate_hash "$feed_url" "$timestamp" "$content")
echo "Posted twt with hash: $hash"
}
# Reply to a twt
#
# $1 - the hash of the twt to reply to
# $2 - the content of the reply
#
# Appends the reply to the feed file and calculates the Twt Hash
reply_twt() {
local reply_to_hash="$1"
shift
local content="$* (reply-to:${reply_to_hash})"
post_twt "$content"
}
# Edit a twt
#
# $1 - the hash of the twt to edit
# $2 - the new content of the twt
#
# Appends the edited twt to the feed file and calculates the Twt Hash
edit_twt() {
local edit_hash="$1"
shift
local content="$* (edit:${edit_hash})"
post_twt "$content"
}
# Delete a twt
#
# $1 - the hash of the twt to delete
#
# Posts a special twt with the content "(delete:<hash>)" to the feed file,
# which marks the twt with the given hash as deleted when processing the feed.
delete_twt() {
local delete_hash="$1"
local content="(delete:${delete_hash})"
post_twt "$content"
}
# Calculate the hash of a twt in a feed file by line number
#
# $1 - line number of the twt
# $2 - path to the feed file
#
# Extracts the timestamp and content of the twt from the feed file,
# calculates the hash using the feed URL and the extracted timestamp
# and content, and returns the hash.
calculate_twt_hash_by_line() {
local line_number feed_file feed_url line_content timestamp content
line_number="$1"
feed_file="$2"
feed_url=$(get_feed_url "$feed_file")
line_content=$(sed -n "${line_number}p" "$feed_file")
timestamp=$(echo "$line_content" | cut -f1)
content=$(echo "$line_content" | cut -f2-)
calculate_hash "$feed_url" "$timestamp" "$content"
}
# Follow a new feed
#
# $1 - URL of the feed to follow
#
# If the feed is already being followed, a message is printed indicating
# that. Otherwise, the URL is appended to the following file and a message
# indicating that the feed was added is printed.
follow_feed() {
local feed_url="$1"
# Check if the feed is already being followed
if grep -Fxq "$feed_url" "$FOLLOWING_FILE" 2>/dev/null; then
echo "You are already following $feed_url"
else
echo "$feed_url" >>"$FOLLOWING_FILE"
echo "Started following $feed_url"
fi
}
# Unfollow a feed
#
# $1 - URL of the feed to unfollow
#
# If the feed is being followed, the URL is removed from the following file
# and a message indicating that the feed was removed is printed. Otherwise,
# a message indicating that you are not following the feed is printed.
unfollow_feed() {
local feed_url="$1"
if grep -Fxq "$feed_url" "$FOLLOWING_FILE" 2>/dev/null; then
grep -Fxv "$feed_url" "$FOLLOWING_FILE" >"${FOLLOWING_FILE}.tmp"
mv "${FOLLOWING_FILE}.tmp" "$FOLLOWING_FILE"
echo "Stopped following $feed_url"
else
echo "You are not following $feed_url"
fi
}
# Fetch all feeds listed in $FOLLOWING_FILE and store them in $FEEDS_DIR.
#
# For each feed, fetch the content using curl and store it in a file
# with the same name as the feed URL, but with all non-alphanumeric
# characters replaced with underscores. The mapping between the feed
# file, feed URL, and nick is recorded in $FEED_URLS_FILE. If the feed
# does not contain a # url: line, one is prepended to the feed file.
fetch_feeds() {
if [ ! -f "$FOLLOWING_FILE" ]; then
echo "You are not following any feeds."
exit 1
fi
# Create or clear the mapping file
echo -n >"$FEED_URLS_FILE"
while read -r feed_url; do
feed_filename=${feed_url//[^a-zA-Z0-9]/_}
feed_path="$FEEDS_DIR/${feed_filename}.txt"
echo "Fetching $feed_url"
if curl -s -f -A "twtxt.sh/1.0 (+$feed_url; @${USER})" "$feed_url" -o "$feed_path"; then
# Extract nick from the feed
feed_nick="$(get_feed_nick "$feed_path")"
if [ -z "$feed_nick" ]; then
feed_nick="unknown"
fi
# Record the mapping between feed file, feed URL, and nick
echo "${feed_filename}.txt|${feed_url}|${feed_nick}" >>"$FEED_URLS_FILE"
# Check if the feed contains a # url: line
if ! grep -q "^# url:" "$feed_path"; then
# Prepend the # url: line to the feed file
sed -i "1i# url: $feed_url" "$feed_path"
fi
else
echo "Failed to fetch $feed_url"
fi
done <"$FOLLOWING_FILE"
}
# Display your timeline, which is a combined feed of your own feed and
# the feeds you are following. The timeline is sorted in reverse
# chronological order and the specified number of most recent twts are
# displayed. If no number is specified, 20 twts are displayed.
#
# $1 - number of twts to display (default: 20)
display_timeline() {
local num_twts="$1"
num_twts="${num_twts:-20}" # Default to 20 if not specified
# Remove any existing combined feed
combined_feed="$CONFIG_DIR/combined_feed.txt"
rm -f "$combined_feed"
# Check if GNU date is available
check_gnu_date
# Process your own feed
process_feed "$FEED_FILE" "$combined_feed" "local"
# Process followed feeds
if [ -d "$FEEDS_DIR" ]; then
for feed_file in "$FEEDS_DIR"/*.txt; do
[ -e "$feed_file" ] || continue
process_feed "$feed_file" "$combined_feed"
done
fi
# Sort and display the combined feed
sort -n -k 1 "$combined_feed" | tail -n "$num_twts" >"$TIMELINE_FILE"
# Display the timeline with ANSI colors
while IFS=$'\t' read -r unix_timestamp display_line; do
echo -e "$display_line"
done <"$TIMELINE_FILE"
}
# Process a single feed and append formatted twts to combined feed
#
# Reads a single feed, extracts the nick and URL from the metadata or
# mapping file, formats each twt, and appends it to the combined feed.
#
# $1 - path to the feed file
# $2 - path to the output file
# $3 - if set to "local", this is your own feed
process_feed() {
local feed_file output_file feed_filename feed_url feed_nick mapping_line feed_domain unix_timestamp hash display_line
feed_file="$1"
output_file="$2"
local_feed="$3" # If set to "local", this is your own feed
feed_filename=$(basename "$feed_file")
if [ "$local_feed" == "local" ]; then
# For your own feed, get the URL and nick from the metadata
feed_url=$(get_feed_url "$feed_file")
feed_nick=$(get_feed_nick "$feed_file")
else
# Get feed_url and feed_nick from mapping file
mapping_line=$(grep "^${feed_filename}|" "$FEED_URLS_FILE")
if [ -n "$mapping_line" ]; then
feed_url=$(echo "$mapping_line" | cut -d'|' -f2)
feed_nick=$(echo "$mapping_line" | cut -d'|' -f3)
else
# Fallback to metadata if mapping not found
feed_url=$(get_feed_url "$feed_file")
feed_nick=$(get_feed_nick "$feed_file")
fi
fi
# Ensure feed_url and feed_nick are set
[ -z "$feed_url" ] && feed_url="unknown"
[ -z "$feed_nick" ] && feed_nick="unknown"
feed_domain=$(echo "$feed_url" | awk -F[/:] '{print $4}')
[ -z "$feed_domain" ] && feed_domain="unknown"
# Read the feed and format each twt
while IFS=$'\t' read -r timestamp content; do
# Skip empty lines or lines without a timestamp
[[ "$timestamp" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T ]] || continue
# Calculate the UNIX timestamp
unix_timestamp=$(timestamp_to_unix "$timestamp")
if [ -z "$unix_timestamp" ]; then
# Skip if unable to parse timestamp
continue
fi
# Calculate the hash of the twt
hash=$(calculate_hash "$feed_url" "$timestamp" "$content")
# Format the display line
display_line="$(format_twt "$feed_nick" "$feed_domain" "$timestamp" "$hash" "$content")"
# Prepend the UNIX timestamp for sorting
echo -e "${unix_timestamp}\t${display_line}" >>"$output_file"
done <"$feed_file"
}
# Format a twt for display with ANSI colors
#
# $1 - nick
# $2 - domain
# $3 - timestamp
# $4 - hash
# $5 - content
#
# Returns a formatted line with ANSI colors.
format_twt() {
local nick="$1"
local domain="$2"
local timestamp="$3"
local hash="$4"
local content="$5"
# ANSI color codes
local color_nick="\033[1;34m" # Bold Blue
local color_domain="\033[0;34m" # Blue
local color_timestamp="\033[0;32m" # Green
local color_hash="\033[0;33m" # Yellow
local color_reset="\033[0m" # Reset
# Build the formatted line
local formatted_line="${color_nick}${nick}${color_reset}@${color_domain}${domain}${color_reset} "
formatted_line+="[${color_timestamp}${timestamp}${color_reset}] "
formatted_line+="<${color_hash}${hash}${color_reset}> "
formatted_line+="${content}"
echo -e "$formatted_line"
}
# _main() - Main entry point for twtxt CLI
#
# Checks the first argument to determine the command to execute and
# calls the appropriate function with the remaining arguments.
#
# Available commands:
# post <message> - Post a new twt
# reply <hash> <message> - Reply to a twt
# edit <hash> <new message> - Edit a twt
# delete <hash> - Delete a twt
# calc-hash <line-number> - Calculate hash of twt at line number
# follow <feed-url> - Follow a new feed
# unfollow <feed-url> - Unfollow a feed
# fetch - Fetch latest twts from followed feeds
# timeline [N] - Display the latest N twts (default 20)
_main() {
case "$1" in
post)
shift
if [ -z "$1" ]; then
echo "Usage: $0 post <message>"
exit 1
fi
post_twt "$*"
;;
reply)
shift
if [ -z "$1" ] || [ -z "$2" ]; then
echo "Usage: $0 reply <reply-to-hash> <message>"
exit 1
fi
reply_to_hash="$1"
shift
reply_twt "$reply_to_hash" "$*"
;;
edit)
shift
if [ -z "$1" ] || [ -z "$2" ]; then
echo "Usage: $0 edit <edit-hash> <new message>"
exit 1
fi
edit_hash="$1"
shift
edit_twt "$edit_hash" "$*"
;;
delete)
shift
if [ -z "$1" ]; then
echo "Usage: $0 delete <delete-hash>"
exit 1
fi
delete_hash="$1"
delete_twt "$delete_hash"
;;
calc-hash)
shift
if [ -z "$1" ]; then
echo "Usage: $0 calc-hash <line-number>"
exit 1
fi
line_number="$1"
hash=$(calculate_twt_hash_by_line "$line_number" "$FEED_FILE")
echo "Hash of twt at line $line_number: $hash"
;;
follow)
shift
if [ -z "$1" ]; then
echo "Usage: $0 follow <feed-url>"
exit 1
fi
follow_feed "$1"
;;
unfollow)
shift
if [ -z "$1" ]; then
echo "Usage: $0 unfollow <feed-url>"
exit 1
fi
unfollow_feed "$1"
;;
fetch)
fetch_feeds
;;
timeline)
shift
display_timeline "$1"
;;
*)
echo "Usage: $0 <command> [options]"
echo "Commands:"
echo " post <message> - Post a new twt"
echo " reply <hash> <message> - Reply to a twt"
echo " edit <hash> <new message> - Edit a twt"
echo " delete <hash> - Delete a twt"
echo " calc-hash <line-number> - Calculate hash of twt at line number"
echo " follow <feed-url> - Follow a new feed"
echo " unfollow <feed-url> - Unfollow a feed"
echo " fetch - Fetch latest twts from followed feeds"
echo " timeline [N] - Display the latest N twts (default 20)"
;;
esac
}
if [ -n "$0" ] && [ x"$0" != x"-bash" ]; then
_main "$@"
fi
1 | #!/bin/bash |
2 | |
3 | # twtxt.sh - A simple script to manage a Twtxt v2 feed with following capabilities |
4 | |
5 | FEED_FILE="twtxt.txt" |
6 | CONFIG_DIR="$HOME/.config/twtxt" |
7 | FOLLOWING_FILE="$CONFIG_DIR/following.txt" |
8 | FEEDS_DIR="$CONFIG_DIR/feeds" |
9 | TIMELINE_FILE="$CONFIG_DIR/timeline.txt" |
10 | FEED_URLS_FILE="$CONFIG_DIR/feed_urls.txt" |
11 | |
12 | # Create configuration directories if they don't exist |
13 | mkdir -p "$CONFIG_DIR" |
14 | mkdir -p "$FEEDS_DIR" |
15 | |
16 | # Initialize feed file if it doesn't exist |
17 | if [ ! -f "$FEED_FILE" ]; then |
18 | echo "# nick: ${USER}" >"$FEED_FILE" |
19 | echo "# url: https://yourdomain.com/$FEED_FILE" >>"$FEED_FILE" |
20 | echo "" >>"$FEED_FILE" |
21 | fi |
22 | |
23 | # Get the URL of a feed from its metadata file |
24 | # $1 - path to the metadata file |
25 | get_feed_url() { |
26 | grep -m1 -E "^#\s*url\s*[:=]\s*" "$1" | tail -1 | sed 's/^#\s*url\s*[:=]\s*//' |
27 | } |
28 | |
29 | # Get the nick of a feed from its metadata file |
30 | # $1 - path to the metadata file |
31 | get_feed_nick() { |
32 | grep -m1 -E "^#\s*nick\s*[:=]\s*" "$1" | tail -1 | sed 's/^#\s*nick\s*[:=]\s*//' |
33 | } |
34 | |
35 | # Check if GNU date is available |
36 | # |
37 | # GNU date is needed to support the -d option for parsing dates in any format. |
38 | # If GNU date is not available, the script will not be able to parse dates |
39 | # in other formats than the default ISO 8601 format. |
40 | check_gnu_date() { |
41 | if date --version >/dev/null 2>&1; then |
42 | DATE_IS_GNU=1 |
43 | else |
44 | DATE_IS_GNU=0 |
45 | fi |
46 | } |
47 | |
48 | # Convert a timestamp in ISO 8601 format to a UNIX timestamp |
49 | # |
50 | # $1 - timestamp in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ) |
51 | # |
52 | # Returns the timestamp as a UNIX timestamp (seconds since Jan 1, 1970, 00:00:00 UTC) |
53 | timestamp_to_unix() { |
54 | local timestamp="$1" |
55 | # Remove fractional seconds if present |
56 | timestamp=$(echo "$timestamp" | sed -E 's/\.[0-9]+//') |
57 | if [ "$DATE_IS_GNU" -eq 1 ]; then |
58 | # GNU date |
59 | date -u -d "$timestamp" +"%s" 2>/dev/null |
60 | else |
61 | # BSD date (macOS) |
62 | date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$timestamp" +"%s" 2>/dev/null |
63 | fi |
64 | } |
65 | |
66 | # Calculate the Twt Hash for a given feed URL, timestamp, and content |
67 | # |
68 | # $1 - feed URL |
69 | # $2 - timestamp |
70 | # $3 - content |
71 | # |
72 | # Returns the first 11 characters of the SHA-256 hash of the concatenated |
73 | # components, separated by newline characters. |
74 | calculate_hash() { |
75 | local feed_url timestamp content data hash |
76 | |
77 | feed_url="$1" |
78 | timestamp="$2" |
79 | content="$3" |
80 | |
81 | # Concatenate components with newline separators |
82 | data="${feed_url}\n${timestamp}\n${content}" |
83 | |
84 | # Calculate SHA-256 hash and get the first 11 characters |
85 | hash=$(echo -e "$data" | sha256sum | awk '{print $1}' | cut -c1-11) |
86 | echo "$hash" |
87 | } |
88 | |
89 | # Post a new twt to the feed file |
90 | # $1 - the content of the twt |
91 | # |
92 | # Appends the twt to the feed file and calculates the Twt Hash |
93 | post_twt() { |
94 | local content timestamp feed_url hash |
95 | |
96 | content="$1" |
97 | timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") |
98 | feed_url=$(get_feed_url "$FEED_FILE") |
99 | |
100 | # Append the twt to the feed file |
101 | echo -e "${timestamp}\t${content}" >>"$FEED_FILE" |
102 | |
103 | # Calculate and display the Twt Hash |
104 | hash=$(calculate_hash "$feed_url" "$timestamp" "$content") |
105 | echo "Posted twt with hash: $hash" |
106 | } |
107 | |
108 | # Reply to a twt |
109 | # |
110 | # $1 - the hash of the twt to reply to |
111 | # $2 - the content of the reply |
112 | # |
113 | # Appends the reply to the feed file and calculates the Twt Hash |
114 | reply_twt() { |
115 | local reply_to_hash="$1" |
116 | shift |
117 | local content="$* (reply-to:${reply_to_hash})" |
118 | post_twt "$content" |
119 | } |
120 | |
121 | # Edit a twt |
122 | # |
123 | # $1 - the hash of the twt to edit |
124 | # $2 - the new content of the twt |
125 | # |
126 | # Appends the edited twt to the feed file and calculates the Twt Hash |
127 | edit_twt() { |
128 | local edit_hash="$1" |
129 | shift |
130 | local content="$* (edit:${edit_hash})" |
131 | post_twt "$content" |
132 | } |
133 | |
134 | # Delete a twt |
135 | # |
136 | # $1 - the hash of the twt to delete |
137 | # |
138 | # Posts a special twt with the content "(delete:<hash>)" to the feed file, |
139 | # which marks the twt with the given hash as deleted when processing the feed. |
140 | delete_twt() { |
141 | local delete_hash="$1" |
142 | local content="(delete:${delete_hash})" |
143 | post_twt "$content" |
144 | } |
145 | |
146 | # Calculate the hash of a twt in a feed file by line number |
147 | # |
148 | # $1 - line number of the twt |
149 | # $2 - path to the feed file |
150 | # |
151 | # Extracts the timestamp and content of the twt from the feed file, |
152 | # calculates the hash using the feed URL and the extracted timestamp |
153 | # and content, and returns the hash. |
154 | calculate_twt_hash_by_line() { |
155 | local line_number feed_file feed_url line_content timestamp content |
156 | |
157 | line_number="$1" |
158 | feed_file="$2" |
159 | feed_url=$(get_feed_url "$feed_file") |
160 | line_content=$(sed -n "${line_number}p" "$feed_file") |
161 | timestamp=$(echo "$line_content" | cut -f1) |
162 | content=$(echo "$line_content" | cut -f2-) |
163 | |
164 | calculate_hash "$feed_url" "$timestamp" "$content" |
165 | } |
166 | |
167 | # Follow a new feed |
168 | # |
169 | # $1 - URL of the feed to follow |
170 | # |
171 | # If the feed is already being followed, a message is printed indicating |
172 | # that. Otherwise, the URL is appended to the following file and a message |
173 | # indicating that the feed was added is printed. |
174 | follow_feed() { |
175 | local feed_url="$1" |
176 | |
177 | # Check if the feed is already being followed |
178 | if grep -Fxq "$feed_url" "$FOLLOWING_FILE" 2>/dev/null; then |
179 | echo "You are already following $feed_url" |
180 | else |
181 | echo "$feed_url" >>"$FOLLOWING_FILE" |
182 | echo "Started following $feed_url" |
183 | fi |
184 | } |
185 | |
186 | # Unfollow a feed |
187 | # |
188 | # $1 - URL of the feed to unfollow |
189 | # |
190 | # If the feed is being followed, the URL is removed from the following file |
191 | # and a message indicating that the feed was removed is printed. Otherwise, |
192 | # a message indicating that you are not following the feed is printed. |
193 | unfollow_feed() { |
194 | local feed_url="$1" |
195 | |
196 | if grep -Fxq "$feed_url" "$FOLLOWING_FILE" 2>/dev/null; then |
197 | grep -Fxv "$feed_url" "$FOLLOWING_FILE" >"${FOLLOWING_FILE}.tmp" |
198 | mv "${FOLLOWING_FILE}.tmp" "$FOLLOWING_FILE" |
199 | echo "Stopped following $feed_url" |
200 | else |
201 | echo "You are not following $feed_url" |
202 | fi |
203 | } |
204 | |
205 | # Fetch all feeds listed in $FOLLOWING_FILE and store them in $FEEDS_DIR. |
206 | # |
207 | # For each feed, fetch the content using curl and store it in a file |
208 | # with the same name as the feed URL, but with all non-alphanumeric |
209 | # characters replaced with underscores. The mapping between the feed |
210 | # file, feed URL, and nick is recorded in $FEED_URLS_FILE. If the feed |
211 | # does not contain a # url: line, one is prepended to the feed file. |
212 | fetch_feeds() { |
213 | if [ ! -f "$FOLLOWING_FILE" ]; then |
214 | echo "You are not following any feeds." |
215 | exit 1 |
216 | fi |
217 | |
218 | # Create or clear the mapping file |
219 | echo -n >"$FEED_URLS_FILE" |
220 | |
221 | while read -r feed_url; do |
222 | feed_filename=${feed_url//[^a-zA-Z0-9]/_} |
223 | feed_path="$FEEDS_DIR/${feed_filename}.txt" |
224 | |
225 | echo "Fetching $feed_url" |
226 | if curl -s -f -A "twtxt.sh/1.0 (+$feed_url; @${USER})" "$feed_url" -o "$feed_path"; then |
227 | # Extract nick from the feed |
228 | feed_nick="$(get_feed_nick "$feed_path")" |
229 | if [ -z "$feed_nick" ]; then |
230 | feed_nick="unknown" |
231 | fi |
232 | # Record the mapping between feed file, feed URL, and nick |
233 | echo "${feed_filename}.txt|${feed_url}|${feed_nick}" >>"$FEED_URLS_FILE" |
234 | # Check if the feed contains a # url: line |
235 | if ! grep -q "^# url:" "$feed_path"; then |
236 | # Prepend the # url: line to the feed file |
237 | sed -i "1i# url: $feed_url" "$feed_path" |
238 | fi |
239 | else |
240 | echo "Failed to fetch $feed_url" |
241 | fi |
242 | done <"$FOLLOWING_FILE" |
243 | } |
244 | |
245 | # Display your timeline, which is a combined feed of your own feed and |
246 | # the feeds you are following. The timeline is sorted in reverse |
247 | # chronological order and the specified number of most recent twts are |
248 | # displayed. If no number is specified, 20 twts are displayed. |
249 | # |
250 | # $1 - number of twts to display (default: 20) |
251 | display_timeline() { |
252 | local num_twts="$1" |
253 | num_twts="${num_twts:-20}" # Default to 20 if not specified |
254 | |
255 | # Remove any existing combined feed |
256 | combined_feed="$CONFIG_DIR/combined_feed.txt" |
257 | rm -f "$combined_feed" |
258 | |
259 | # Check if GNU date is available |
260 | check_gnu_date |
261 | |
262 | # Process your own feed |
263 | process_feed "$FEED_FILE" "$combined_feed" "local" |
264 | |
265 | # Process followed feeds |
266 | if [ -d "$FEEDS_DIR" ]; then |
267 | for feed_file in "$FEEDS_DIR"/*.txt; do |
268 | [ -e "$feed_file" ] || continue |
269 | process_feed "$feed_file" "$combined_feed" |
270 | done |
271 | fi |
272 | |
273 | # Sort and display the combined feed |
274 | sort -n -k 1 "$combined_feed" | tail -n "$num_twts" >"$TIMELINE_FILE" |
275 | |
276 | # Display the timeline with ANSI colors |
277 | while IFS=$'\t' read -r unix_timestamp display_line; do |
278 | echo -e "$display_line" |
279 | done <"$TIMELINE_FILE" |
280 | } |
281 | |
282 | # Process a single feed and append formatted twts to combined feed |
283 | # |
284 | # Reads a single feed, extracts the nick and URL from the metadata or |
285 | # mapping file, formats each twt, and appends it to the combined feed. |
286 | # |
287 | # $1 - path to the feed file |
288 | # $2 - path to the output file |
289 | # $3 - if set to "local", this is your own feed |
290 | process_feed() { |
291 | local feed_file output_file feed_filename feed_url feed_nick mapping_line feed_domain unix_timestamp hash display_line |
292 | |
293 | feed_file="$1" |
294 | output_file="$2" |
295 | local_feed="$3" # If set to "local", this is your own feed |
296 | |
297 | feed_filename=$(basename "$feed_file") |
298 | |
299 | if [ "$local_feed" == "local" ]; then |
300 | # For your own feed, get the URL and nick from the metadata |
301 | feed_url=$(get_feed_url "$feed_file") |
302 | feed_nick=$(get_feed_nick "$feed_file") |
303 | else |
304 | # Get feed_url and feed_nick from mapping file |
305 | mapping_line=$(grep "^${feed_filename}|" "$FEED_URLS_FILE") |
306 | if [ -n "$mapping_line" ]; then |
307 | feed_url=$(echo "$mapping_line" | cut -d'|' -f2) |
308 | feed_nick=$(echo "$mapping_line" | cut -d'|' -f3) |
309 | else |
310 | # Fallback to metadata if mapping not found |
311 | feed_url=$(get_feed_url "$feed_file") |
312 | feed_nick=$(get_feed_nick "$feed_file") |
313 | fi |
314 | fi |
315 | |
316 | # Ensure feed_url and feed_nick are set |
317 | [ -z "$feed_url" ] && feed_url="unknown" |
318 | [ -z "$feed_nick" ] && feed_nick="unknown" |
319 | |
320 | feed_domain=$(echo "$feed_url" | awk -F[/:] '{print $4}') |
321 | [ -z "$feed_domain" ] && feed_domain="unknown" |
322 | |
323 | # Read the feed and format each twt |
324 | while IFS=$'\t' read -r timestamp content; do |
325 | # Skip empty lines or lines without a timestamp |
326 | [[ "$timestamp" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T ]] || continue |
327 | |
328 | # Calculate the UNIX timestamp |
329 | unix_timestamp=$(timestamp_to_unix "$timestamp") |
330 | if [ -z "$unix_timestamp" ]; then |
331 | # Skip if unable to parse timestamp |
332 | continue |
333 | fi |
334 | |
335 | # Calculate the hash of the twt |
336 | hash=$(calculate_hash "$feed_url" "$timestamp" "$content") |
337 | |
338 | # Format the display line |
339 | display_line="$(format_twt "$feed_nick" "$feed_domain" "$timestamp" "$hash" "$content")" |
340 | |
341 | # Prepend the UNIX timestamp for sorting |
342 | echo -e "${unix_timestamp}\t${display_line}" >>"$output_file" |
343 | done <"$feed_file" |
344 | } |
345 | |
346 | # Format a twt for display with ANSI colors |
347 | # |
348 | # $1 - nick |
349 | # $2 - domain |
350 | # $3 - timestamp |
351 | # $4 - hash |
352 | # $5 - content |
353 | # |
354 | # Returns a formatted line with ANSI colors. |
355 | format_twt() { |
356 | local nick="$1" |
357 | local domain="$2" |
358 | local timestamp="$3" |
359 | local hash="$4" |
360 | local content="$5" |
361 | |
362 | # ANSI color codes |
363 | local color_nick="\033[1;34m" # Bold Blue |
364 | local color_domain="\033[0;34m" # Blue |
365 | local color_timestamp="\033[0;32m" # Green |
366 | local color_hash="\033[0;33m" # Yellow |
367 | local color_reset="\033[0m" # Reset |
368 | |
369 | # Build the formatted line |
370 | local formatted_line="${color_nick}${nick}${color_reset}@${color_domain}${domain}${color_reset} " |
371 | formatted_line+="[${color_timestamp}${timestamp}${color_reset}] " |
372 | formatted_line+="<${color_hash}${hash}${color_reset}> " |
373 | formatted_line+="${content}" |
374 | |
375 | echo -e "$formatted_line" |
376 | } |
377 | |
378 | # _main() - Main entry point for twtxt CLI |
379 | # |
380 | # Checks the first argument to determine the command to execute and |
381 | # calls the appropriate function with the remaining arguments. |
382 | # |
383 | # Available commands: |
384 | # post <message> - Post a new twt |
385 | # reply <hash> <message> - Reply to a twt |
386 | # edit <hash> <new message> - Edit a twt |
387 | # delete <hash> - Delete a twt |
388 | # calc-hash <line-number> - Calculate hash of twt at line number |
389 | # follow <feed-url> - Follow a new feed |
390 | # unfollow <feed-url> - Unfollow a feed |
391 | # fetch - Fetch latest twts from followed feeds |
392 | # timeline [N] - Display the latest N twts (default 20) |
393 | _main() { |
394 | case "$1" in |
395 | post) |
396 | shift |
397 | if [ -z "$1" ]; then |
398 | echo "Usage: $0 post <message>" |
399 | exit 1 |
400 | fi |
401 | post_twt "$*" |
402 | ;; |
403 | reply) |
404 | shift |
405 | if [ -z "$1" ] || [ -z "$2" ]; then |
406 | echo "Usage: $0 reply <reply-to-hash> <message>" |
407 | exit 1 |
408 | fi |
409 | reply_to_hash="$1" |
410 | shift |
411 | reply_twt "$reply_to_hash" "$*" |
412 | ;; |
413 | edit) |
414 | shift |
415 | if [ -z "$1" ] || [ -z "$2" ]; then |
416 | echo "Usage: $0 edit <edit-hash> <new message>" |
417 | exit 1 |
418 | fi |
419 | edit_hash="$1" |
420 | shift |
421 | edit_twt "$edit_hash" "$*" |
422 | ;; |
423 | delete) |
424 | shift |
425 | if [ -z "$1" ]; then |
426 | echo "Usage: $0 delete <delete-hash>" |
427 | exit 1 |
428 | fi |
429 | delete_hash="$1" |
430 | delete_twt "$delete_hash" |
431 | ;; |
432 | calc-hash) |
433 | shift |
434 | if [ -z "$1" ]; then |
435 | echo "Usage: $0 calc-hash <line-number>" |
436 | exit 1 |
437 | fi |
438 | line_number="$1" |
439 | hash=$(calculate_twt_hash_by_line "$line_number" "$FEED_FILE") |
440 | echo "Hash of twt at line $line_number: $hash" |
441 | ;; |
442 | follow) |
443 | shift |
444 | if [ -z "$1" ]; then |
445 | echo "Usage: $0 follow <feed-url>" |
446 | exit 1 |
447 | fi |
448 | follow_feed "$1" |
449 | ;; |
450 | unfollow) |
451 | shift |
452 | if [ -z "$1" ]; then |
453 | echo "Usage: $0 unfollow <feed-url>" |
454 | exit 1 |
455 | fi |
456 | unfollow_feed "$1" |
457 | ;; |
458 | fetch) |
459 | fetch_feeds |
460 | ;; |
461 | timeline) |
462 | shift |
463 | display_timeline "$1" |
464 | ;; |
465 | *) |
466 | echo "Usage: $0 <command> [options]" |
467 | echo "Commands:" |
468 | echo " post <message> - Post a new twt" |
469 | echo " reply <hash> <message> - Reply to a twt" |
470 | echo " edit <hash> <new message> - Edit a twt" |
471 | echo " delete <hash> - Delete a twt" |
472 | echo " calc-hash <line-number> - Calculate hash of twt at line number" |
473 | echo " follow <feed-url> - Follow a new feed" |
474 | echo " unfollow <feed-url> - Unfollow a feed" |
475 | echo " fetch - Fetch latest twts from followed feeds" |
476 | echo " timeline [N] - Display the latest N twts (default 20)" |
477 | ;; |
478 | esac |
479 | } |
480 | |
481 | if [ -n "$0" ] && [ x"$0" != x"-bash" ]; then |
482 | _main "$@" |
483 | fi |