prologic revised this gist . 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