Last active 1726989895

A Twtxt v2 Bash script reference implementation

prologic revised this gist 1726989895. Go to revision

1 file changed, 483 insertions

twtxt-v2.sh(file created)

@@ -0,0 +1,483 @@
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
Newer Older

Powered by Opengist Load: 25ms