332 |
333 | This program is free software: you can redistribute it and/or modify it under
334 | the terms of the GNU General Public License as published by the Free Software
335 | Foundation, either version 3 of the License, or (at your option) any later
336 | version.
337 |
338 | This program is distributed in the hope that it will be useful, but WITHOUT ANY
339 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
340 | PARTICULAR PURPOSE. See the GNU General Public License for more details.
341 |
342 | You should have received a copy of the GNU General Public License along with
343 | this program. If not, see http://www.gnu.org/licenses/.
344 |
--------------------------------------------------------------------------------
/bin/txtnish:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | VERSION="0.2"
4 |
5 | TAB=$(printf "\t")
6 |
7 | ###################
8 | ## Default config #
9 | ###################
10 |
11 | limit=20
12 | formatter="fold -s"
13 | use_pager=1
14 | use_color=1
15 | always_update=1
16 | sort_order=descending
17 | twtfile=~/twtxt.txt
18 | disclose_identity=0
19 | max_procs=50
20 | xargs_parallel=1
21 | editor=${EDITOR:-vi}
22 | pager=${PAGER:-less -R}
23 | color_nick=yellow
24 | color_time=blue
25 | color_hashtag=cyan
26 | color_mention=yellow
27 | gpg_bin=${gpg_bin:-gpg}
28 | sign_user=""
29 | sign_twtfile=0
30 | check_signatures=0
31 | ipfs_publish=0
32 | ipfs_wrap_with_dir=0
33 | ipfs_recursive=0
34 | ipfs_gateway=http://localhost:8080
35 | nick="${USER}"
36 | sync_followings=""
37 | awk=awk
38 | sed=sed
39 | # sync_followings="https://raw.githubusercontent.com/mdom/we-are-twtxt/master/we-are-twtxt.txt"
40 | timeout=0
41 | verbose=0
42 | force=0
43 | add_metadata=0
44 |
45 | http_proxy=""
46 | https_proxy=""
47 |
48 | ftp_user=""
49 | ftp_host=""
50 |
51 | sftp_over_scp=0
52 | scp_user=""
53 | scp_host=""
54 |
55 | http_backend_args=""
56 |
57 | theme="default"
58 |
59 | # this is the password for mailpipe. Mailpipe expects the first line of the
60 | # mail to be in the form "password $mail_password"
61 | mail_password=""
62 |
63 | # timestamps defaults
64 | last_timeline=0
65 |
66 | if [ -n "${NO_COLOR+1}" ];then
67 | use_color=0
68 | fi
69 |
70 | ######################
71 | ## Runtime variables #
72 | ######################
73 |
74 | program_name=${0##*/}
75 | config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/$program_name"
76 | config_file="$config_dir/config"
77 | follow_file="$config_dir/following"
78 | draft_file="$config_dir/draft"
79 | cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/$program_name"
80 | log_dir="$cache_dir/logs"
81 |
82 | #####################
83 | ## Helper Functions #
84 | #####################
85 |
86 | # Description: Check if command is in path
87 | # Synopsis: have_cmd COMMAND
88 | # Returns: success if command is found; fails otherwise
89 |
90 | have_cmd () {
91 | command -v "$1" >/dev/null 2>&1
92 | }
93 |
94 | # Description: Print error message and exit
95 | # Synopsis: die MESSAGE RETURN_CODE
96 | # Returns: nothing
97 |
98 | die () {
99 | printf "%s: %s\n" "$program_name" "$1" >&2
100 | exit "${2:-1}"
101 | }
102 |
103 | warn () {
104 | printf "%s: %s\n" "$program_name" "$1" >&2
105 | }
106 |
107 | info () {
108 | [ "$verbose" -gt 0 ] && printf "%s: %s\n" "$program_name" "$1"
109 | }
110 |
111 | # Description: Source configuration file if it exists
112 | # Synopsis: read_config
113 | # Returns: nothing
114 |
115 | read_config () {
116 | if [ -e "$config_file" ];then
117 | # shellcheck source=/dev/null
118 | . "$config_file"
119 | fi
120 |
121 | if [ "$disclose_identity" -eq 1 ] && [ -n "$twturl" ];then
122 | user_agent="txtnish/$VERSION (+${twturl}; @$nick)"
123 | else
124 | user_agent="txtnish/$VERSION (+https://github.com/mdom/txtnish)"
125 | fi
126 |
127 | [ -n "$http_proxy" ] && export http_proxy
128 | [ -n "$https_proxy" ] && export https_proxy
129 | }
130 |
131 | # Description: Print arguments for curl on stdout for xargs
132 | # Synopsis: args_for_curl NICK URL COUNTER
133 | # Returns: Nothing
134 |
135 | args_for_curl () {
136 | if [ "$timeout" -ne 0 ];then
137 | printf "%s\n" "--max-time" "$timeout"
138 | fi
139 | printf "\"%s\"\n" \
140 | --user-agent "$user_agent" \
141 | --location \
142 | --stderr "$log_dir/http.log.$1" \
143 | --show-error \
144 | --silent \
145 | --compressed \
146 | --output "$cache_dir/twtfiles/$1.txt" \
147 | --time-cond "$cache_dir/twtfiles/$1.txt" \
148 | --write-out '%{filename_effective}\t%{http_code}\t%{num_redirects}\t%{url_effective}\n' \
149 | "$2"
150 | }
151 |
152 | rewrite_url () {
153 | if [ -z "$_ipfs_checked" ];then
154 | if ! curl -s "$ipfs_gateway" > /dev/null 2>&1 ;then
155 | ipfs_gateway=https://ipfs.io
156 | fi
157 | _ipfs_checked=1
158 | fi
159 |
160 | case $url in
161 | ipns://* )
162 | url=$ipfs_gateway/ipns/${url#ipns://}
163 | ;;
164 | esac
165 | }
166 |
167 | maybe_update () {
168 | if [ "$always_update" -eq 1 ];then
169 | update "$@"
170 | fi
171 | }
172 |
173 | format_msg_html () {
174 | $awk '
175 | function escape (str) {
176 | gsub(/&/,"\\&",str)
177 | gsub(/,"\\<",str)
178 | gsub(/>/,"\\>",str)
179 | return str
180 | }
181 |
182 | function linkify (str, new_str, n, url, nick) {
183 | while ( str ) {
184 | if ( match(str,/@<[^&]+>/) ) {
185 | new_str = new_str substr(str,1,RSTART-1)
186 | n = split(substr(str,RSTART+5,RLENGTH-9),fields," ")
187 | if ( n == 1 ) {
188 | nick = ""
189 | url = fields[1]
190 | } else {
191 | nick = fields[1]
192 | url = fields[2]
193 | }
194 |
195 | new_str = new_str "@" nick ""
196 | str = substr(str,RSTART+RLENGTH)
197 | } else if ( match(str,/https?:\/\/[^ ]+/) ) {
198 | new_str = new_str substr(str,1,RSTART-1)
199 | url = substr(str,RSTART,RLENGTH)
200 |
201 | # a trailing point or comma could be
202 | # part of the url, but it is probably a
203 | # punctuation mark.
204 | if ( substr(url,length(url),1) ~ /[,.]/ ) {
205 | url = substr(url,1,length(url)-1)
206 | RSTART -= 1
207 | }
208 |
209 | new_str = new_str "" url ""
210 | str = substr(str,RSTART+RLENGTH)
211 | } else {
212 | new_str = new_str str
213 | str = ""
214 | }
215 | }
216 | return new_str
217 | }
218 |
219 | BEGIN {
220 | FS = "\t"
221 | srand()
222 | now = srand()
223 | printf "\n" \
224 | "" \
225 | "" \
226 | "" \
227 | "" \
231 | ""
232 | }
233 | {
234 | nick=$1;url=$2;props=$3;ts=$4;msg=$5
235 | seconds=now-ts
236 | if ( seconds >= 172800 ) {
237 | ts = int(seconds / 86400) " days ago"
238 | } else if ( seconds >= 86400 ) {
239 | ts = "1 day ago"
240 | } else if ( seconds >= 7200 ) {
241 | ts = int(seconds / 3600) " hours ago"
242 | } else if ( seconds >= 3600 ) {
243 | ts = "1 hour ago"
244 | } else if ( seconds >= 120 ) {
245 | ts = int(seconds / 60) " minutes ago"
246 | } else if ( seconds >= 60 ) {
247 | ts = "1 minute ago"
248 | } else if ( seconds == 1 ) {
249 | ts = "1 second ago"
250 | } else {
251 | ts = seconds " seconds ago"
252 | }
253 |
254 | nick = escape(nick)
255 | msg = escape(msg)
256 | msg = linkify(msg)
257 |
258 | print "* " nick " (" ts ")
" msg "
"
259 | }
260 |
261 | END {
262 | print ""
263 | }
264 | '
265 | }
266 |
267 | format_msg () {
268 |
269 | export color_nick color_time color_hashtag color_mention use_color formatter
270 | $awk -v theme="$1" '
271 | function colors_to_escape ( color, fgc, bgc, n, c, attr ) {
272 | n = split(color,c,/ /)
273 | for (i=1; i<=n; i++ ) {
274 | if ( match(c[i], /^on_/ ) ) {
275 | bgc = bg colors[substr(c[i],4)]
276 | } else if ( colors[c[i]] != "" ) {
277 | fgc = fg colors[c[i]]
278 | } else if ( attribs[c[i]] != "" ) {
279 | if ( attr )
280 | attr = attr ";" attribs[c[i]]
281 | else
282 | attr = ";" attribs[c[i]]
283 | }
284 | }
285 | if ( bgc && fgc ) {
286 | return csi fgc ";" bgc attr "m"
287 | }
288 | if ( fgc ) {
289 | return csi fgc attr "m"
290 | }
291 | }
292 |
293 | function colorize ( layer, color, text ) {
294 | return csi layer colors[color] "m" text reset
295 | }
296 | BEGIN {
297 | FS="\t"
298 | ORS="\n\n"
299 | csi = "\033["
300 | reset = csi "0m"
301 | fg = 3
302 | bg = 4
303 | colors["black"] = 0
304 | colors["red"] = 1
305 | colors["green"] = 2
306 | colors["yellow"] = 3
307 | colors["blue"] = 4
308 | colors["magenta"] = 5
309 | colors["cyan"] = 6
310 | colors["white"] = 7
311 |
312 | attribs["bold"] = 1
313 | attribs["bright"] = 1
314 | attribs["faint"] = 2
315 | attribs["italic"] = 3
316 | attribs["underline"] = 4
317 | attribs["blink"] = 5
318 | attribs["fastblink"] = 6
319 | srand()
320 | now=srand()
321 |
322 | color_nick = colors_to_escape(ENVIRON["color_nick"])
323 | color_time = colors_to_escape(ENVIRON["color_time"])
324 | color_hashtag = colors_to_escape(ENVIRON["color_hashtag"])
325 | color_mention = colors_to_escape(ENVIRON["color_mention"])
326 | }
327 |
328 | {
329 | nick=$1;url=$2;props=$3;ts=$4;msg=$5
330 | seconds=now-ts
331 | if ( seconds >= 172800 ) {
332 | ts = int(seconds / 86400) " days ago"
333 | } else if ( seconds >= 86400 ) {
334 | ts = "1 day ago"
335 | } else if ( seconds >= 7200 ) {
336 | ts = int(seconds / 3600) " hours ago"
337 | } else if ( seconds >= 3600 ) {
338 | ts = "1 hour ago"
339 | } else if ( seconds >= 120 ) {
340 | ts = int(seconds / 60) " minutes ago"
341 | } else if ( seconds >= 60 ) {
342 | ts = "1 minute ago"
343 | } else if ( seconds == 1 ) {
344 | ts = "1 second ago"
345 | } else {
346 | ts = seconds " seconds ago"
347 | }
348 |
349 | if ( ENVIRON["use_color"] == 1 ) {
350 | n = split(props,prop_array,/,/)
351 | props=""
352 | for ( i in prop_array ) {
353 | if ( prop_array[i] == "tls" || prop_array[i] == "gpg_trusted" ) {
354 | prop_array[i] = colorize(fg,"green",prop_array[i])
355 | }
356 | if ( prop_array[i] == "notls" ) {
357 | prop_array[i] = colorize(fg,"red",prop_array[i])
358 | }
359 | }
360 |
361 | props = prop_array[1]
362 | for (i = 2; i <= n; i++)
363 | props = props "," prop_array[i]
364 |
365 | nick = color_nick nick reset
366 | ts = color_time ts reset
367 | gsub(/#[[:alnum:]_-]+/, color_hashtag "&" reset, msg)
368 | gsub(/@[[:alnum:]_-]+/, color_mention "&" reset, msg)
369 | }
370 | fmt = ENVIRON["formatter"]
371 | if ( theme == "oneline" ) {
372 | printf "* %s (%s %s)\n", msg, nick, ts
373 | } else {
374 | printf "* %s (%s)", nick, ts
375 | if ( props ) printf " [%s]", props
376 | printf "\n"
377 | print msg | fmt
378 | close(fmt)
379 | }
380 | }'
381 | }
382 |
383 | sort_tweets () {
384 | case $sort_order in
385 | ascending | descending ) : ;;
386 | * ) die "Sort order must be either ascending or descending." ;;
387 | esac
388 |
389 | $awk 'BEGIN { FS=OFS="\t" }{ split($4,a,/\./); print $0, a[0] ? a[0] :0, a[1] }' |\
390 | sort -rn -k6,6 -k7,7 -t "$TAB" | \
391 | limit_uniq_tweets
392 | }
393 |
394 | limit_uniq_tweets () {
395 | $awk '
396 | BEGIN {
397 | FS=OFS="\t"
398 | limit='"$limit"'
399 | sort_order="'"$sort_order"'"
400 | }
401 | {
402 | key = $5 $6 $7
403 | NF-=2
404 | if ( last != key ) {
405 | lines_seen++
406 | if ( sort_order == "ascending" ) {
407 | lines[lines_seen] = $0
408 | } else {
409 | print
410 | }
411 | if ( limit > 0 && lines_seen >= limit )
412 | exit
413 | }
414 | last = key
415 | }
416 | END {
417 | if ( sort_order == "ascending" ) {
418 | for ( i = lines_seen; i>0; i-- ) {
419 | print lines[i]
420 | }
421 | }
422 | }
423 | '
424 | }
425 |
426 | maybe_pager () {
427 | if [ "$use_pager" -eq 1 ];then
428 | $pager
429 | else
430 | cat
431 | fi
432 | }
433 |
434 | # Description: Prefixes every line in stdin with arguments
435 | # Synopsis: prefix column NICK URL PROPS
436 |
437 | prefix_columns () {
438 | $awk 'BEGIN{OFS="\t"}{print "'"$1"'", "'"$2"'", "'"$3"'",$0}'
439 | }
440 |
441 | filter_tweets () {
442 | $awk '
443 | BEGIN {
444 | FS=OFS="\t"
445 | last_timeline = "'"$last_timeline"'"
446 | srand()
447 | now=srand()
448 | }
449 | function new () {
450 | return $4 > last_timeline
451 |
452 | }
453 | {
454 | nick=$1;url=$2;props=$3;ts=$4;msg=$5;
455 | if ('"${filter_expr:-1}"') {
456 | print
457 | }
458 | }
459 | '
460 | }
461 |
462 | display_tweets () {
463 | case $theme in
464 | raw ) normalize_tweets | filter_tweets | sort_tweets ;;
465 | html ) normalize_tweets | filter_tweets | sort_tweets | format_msg_html ;;
466 | default ) normalize_tweets | filter_tweets | sort_tweets | collapse_mentions | format_msg| maybe_pager ;;
467 | oneline ) normalize_tweets | filter_tweets | sort_tweets | collapse_mentions | format_msg "oneline" | maybe_pager ;;
468 | * ) die "Unknown theme $theme." ;;
469 | esac
470 | }
471 |
472 | normalize_tweets () {
473 | $awk '
474 | BEGIN{
475 | FS=OFS="\t"
476 | rfc3339 = "^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\\.[0-9]+)?(([Zz])|([\\+|\\-]([01][0-9]|2[0-3]):[0-5][0-9]))$"
477 | srand()
478 | now = srand()
479 | }
480 | {
481 | ## handle timestamps without T
482 | if ( substr($4,11,1) == " " ) {
483 | $4 = substr($4,1,10) "T" substr($4,12)
484 | }
485 |
486 | ## handle any ws as seperator
487 | sub(/[[:space:]]+/, "\t", $4)
488 | $0=$0
489 |
490 | ## remove escape sequences
491 | ## the trailing hyphen is important:
492 | ## https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=214783
493 | gsub(/[^[:print:][:space:]-]/,"")
494 |
495 | #normlize timestamp
496 | sub(/Z$/,"+00:00",$4)
497 |
498 | # remove leading spaces
499 | sub(/^[[:space:]]*/,"",$4)
500 |
501 | # add : to offset if missing
502 | if ( match($4,/[+-][0-9]{4}$/) ) {
503 | l = length($4)
504 | $4 = substr($4,1,l-4) substr($4,l-3,2) ":" substr($4,l-1,2)
505 | }
506 |
507 | ## add seconds if missing
508 | if ( match( $4,/T[0-9]{2}:[0-9]{2}[+-]/ ) ) {
509 | $4 = substr($4,1,RSTART + RLENGTH -2) ":00" substr($4,RSTART + RLENGTH -1 )
510 | }
511 |
512 | # ignore lines not matching spec
513 | if ( NF != 5 || $4 !~ rfc3339 )
514 | next
515 |
516 | no_fields = split( $4, ta, /[Tt:+.-]/)
517 | year = ta[1]; month = ta[2]; day = ta[3]
518 | hour = ta[4]; minute = ta[5]; seconds = ta[6]
519 |
520 | if ( no_fields == 9 ) {
521 | offset = ta[8] * 3600 + ta[9] * 60
522 | fracsecs=ta[7]
523 | }
524 | else {
525 | offset = ta[7] * 3600 + ta[8] * 60
526 | fracsecs=0
527 | }
528 | epoch_days = 719591
529 |
530 | if ( month > 2 ) {
531 | month++
532 | } else {
533 | year--
534 | month+=13
535 | }
536 | tweet_days = (year*365)+int(year/4)-int(year/100)+int(year/400) + int(month*30.6)+ day
537 |
538 | days_since_epoch = tweet_days - epoch_days
539 |
540 | seconds_since_epoch = (days_since_epoch*86400)+(hour*3600)+(minute*60)+seconds
541 |
542 | mod = substr( $4, length($4)-5, 1)
543 | if ( mod == "+" ) {
544 | seconds_since_epoch -= offset
545 | } else {
546 | seconds_since_epoch += offset
547 | }
548 |
549 | $4 = seconds_since_epoch "." fracsecs
550 |
551 | if ( $4 > now )
552 | next
553 |
554 | print
555 | }
556 | '
557 | }
558 |
559 | gpg_verify () {
560 | "$gpg_bin" --status-fd=1 --no-verbose --quiet --batch --verify "$1" 2>/dev/null
561 | }
562 |
563 | collapse_mentions () {
564 | ## TODO this *has* to be easier
565 | export follow_file nick twturl
566 | $awk '
567 | function normalize_url (url, host) {
568 | sub(/^http:/,"https:",url)
569 |
570 | if ( url ~ /^https:/ ) {
571 | host = url
572 | sub(/^https:\/\//,"", host)
573 | sub(/\/.*/,"", host)
574 | host = tolower(host)
575 | url = "https://" host substr(url,9+length(host))
576 | }
577 | return url
578 | }
579 | BEGIN {
580 | FS=" "
581 | while ( (getline < ENVIRON["follow_file"] ) > 0 ) {
582 | urls[normalize_url($2)] = $1
583 | }
584 | if ( ENVIRON["twturl"] && ENVIRON["nick"] )
585 | urls[ENVIRON["twturl"]] = ENVIRON["nick"]
586 | FS="\t"
587 | OFS=FS
588 | }
589 | {
590 | new_msg=""
591 | while ( match($5,/@<[^>]+>/) ) {
592 | new_msg = new_msg substr($5,1,RSTART-1)
593 | n = split(substr($5,RSTART+2,RLENGTH-3),fields," ")
594 | if ( n == 1 ) {
595 | url = fields[1]
596 | } else {
597 | url = fields[2]
598 | }
599 |
600 | url = normalize_url(url)
601 |
602 | if ( url in urls ) {
603 | new_msg = new_msg "@" urls[url]
604 | }
605 | else {
606 | new_msg = new_msg substr($5,RSTART,RLENGTH)
607 | }
608 | $5 = substr($5,RSTART+RLENGTH)
609 | }
610 | new_msg = new_msg $5
611 | $5=new_msg
612 | print
613 | }
614 | '
615 | }
616 |
617 | expand_mentions () {
618 | export follow_file nick twturl
619 | $awk '
620 | BEGIN {
621 | FS=" "
622 | while ( (getline < ENVIRON["follow_file"] ) > 0 ) {
623 | nicks[$1]=$2
624 | }
625 | if ( ENVIRON["twturl"] )
626 | urls[ENVIRON["nick"]] = ENVIRON["twturl"]
627 | FS="\t"
628 | OFS=FS
629 | }
630 | {
631 | expanded_line = ""
632 | while ( match($2, /@[[:alnum:]_-]+/ )) {
633 | expanded_line = expanded_line substr($2,1,RSTART-1)
634 | nick = substr($2,RSTART+1,RLENGTH-1)
635 | rest = substr($2,RSTART+RLENGTH)
636 | prev = substr($2,RSTART-1,1);
637 | expand = nick in nicks && (RSTART == 1 || match(prev, /[[:space:]]/))
638 | if (expand) {
639 | expanded_line = expanded_line "@<" nick " " nicks[nick] ">"
640 | }
641 | else {
642 | expanded_line = expanded_line "@" nick
643 | }
644 | $2 = rest
645 | }
646 | expanded_line = expanded_line $2
647 | $2 = expanded_line
648 | print
649 | }
650 | '
651 | }
652 |
653 | draft_to_twtfile () {
654 | [ -s "$draft_file" ] || return
655 | pre_tweet_hook
656 | _fracsecs=0
657 | _timestamp_fmt=$(TZ=C date "+%Y-%m-%dT%H:%M:%S.%%06iZ")
658 | while read -r msg;do
659 | [ -n "$msg" ] || continue
660 | case $msg in
661 | /follow\ *|/unfollow\ * )
662 | # shellcheck disable=2086
663 | set -- ${msg#/}
664 | if [ $# -eq 3 ]; then
665 | "$@"
666 | continue
667 | fi
668 | ;;
669 | esac
670 | printf "$_timestamp_fmt\t%s\n" "$_fracsecs" "$msg"
671 | _fracsecs=$(( _fracsecs + 1 ))
672 | done < "$draft_file" | expand_mentions >> "$twtfile"
673 | publish
674 | }
675 |
676 | cleanup () {
677 | [ -e "$tempfile" ] && rm "$tempfile"
678 | [ -d "$tempdir" ] && rm -r "$tempdir"
679 | }
680 |
681 | sync_twtfile () {
682 | if [ -n "$twtfile" ] && [ -n "$twturl" ]; then
683 | if ! curl -sS -o "$twtfile" "$twturl"; then
684 | die "Can't sync twtfile. Aborting."
685 | fi
686 | else
687 | die "Set twtfile and twturl to sync twtfile. Aborting.\n"
688 | fi
689 | }
690 |
691 | pre_tweet_hook () {
692 | :
693 | }
694 |
695 | post_tweet_hook () {
696 | :
697 | }
698 |
699 | process_stat_log () {
700 | tempfile="$follow_file.$$"
701 |
702 | ## Remove dos line endings before reading twtfiles
703 | for file in "$cache_dir"/twtfiles/*.txt;do
704 | sed 's/
$//' "$file" > "$file~" && mv "$file~" "$file"
705 | done
706 |
707 | $awk '
708 |
709 | function get_location (file, line, meta) {
710 | while((getline line < file) > 0 ) {
711 | if ( line ~ /^#[ \t]*[^ \t=]+[ \t]*=[ \t]*[^ \t]+/ ) {
712 | sub(/^#[ \t]*/,"", line)
713 | split(line,meta,/[ \t]*=[ \t]*/)
714 | if ( meta[1] == "url" ) {
715 | return meta[2]
716 | }
717 | }
718 | }
719 | return ""
720 | }
721 |
722 | function redirect ( nick, location ) {
723 | following[nick] = location
724 | printf "Following %s now at %s.\n", nick, following[nick] | stderr
725 | changed = 1
726 | }
727 |
728 | BEGIN {
729 | tempfile="'"$tempfile"'"
730 | follow_file="'"$follow_file"'"
731 | file = 1
732 | code = 2
733 | redirects = 3
734 | url = 4
735 | while ( (getline < follow_file ) > 0 ) {
736 | following[$1] = $2
737 | }
738 | stderr = "cat >&2"
739 | }
740 | {
741 | match($file, /[^\/]+.txt$/)
742 | nick = substr($file,RSTART,RLENGTH - 4 )
743 | location = get_location($file)
744 | if ( location && following[nick] != location ) {
745 | redirect( nick, location )
746 | } else if ( $code == 200 || $code == 304 ) {
747 | if ( $redirects > 0 ) {
748 | redirect( nick, $url )
749 | }
750 | } else if ( $code == 410 ) {
751 | delete following[nick];
752 | changed=1
753 | } else if ( $code == 000 ) {
754 | # curl error
755 | } else {
756 | printf "Fetching %s at %s returned %s.\n", nick, $url, $code | stderr
757 | }
758 | }
759 | END {
760 | if ( changed ) {
761 | for ( nick in following ) {
762 | print nick, following[nick] > tempfile
763 | }
764 | system("mv " tempfile " " follow_file)
765 | }
766 | }'
767 | }
768 |
769 | read_key () {
770 | _key=
771 | if [ -t 0 ];then
772 | if [ -z "$_stty" ];then
773 | _stty=$(stty -g)
774 | fi
775 | stty -echo -icanon min 1
776 | _key=$(dd bs=1 count=1 2>/dev/null)
777 | stty "$_stty"
778 | fi
779 | }
780 |
781 | getline () {
782 | _var=${2:-_line}
783 | if [ -t 0 ];then
784 | if [ -n "$BASH_VERSION" ];then
785 | # shellcheck disable=SC2039
786 | read -erp "$1: " "$_var"
787 | else
788 | printf "%s: " "$1"
789 | IFS= read -r "$_var"
790 | fi
791 | fi
792 | }
793 |
794 | yesno () {
795 | printf "%s [yN] " "$1"
796 | read_key
797 | case $_key in
798 | y ) _rc=0 ;;
799 | * ) _rc=1 ;;
800 | esac
801 | printf "\n"
802 | return $_rc
803 | }
804 |
805 | parallel_curl () {
806 | _no_args=$( args_for_curl | wc -l )
807 |
808 | if [ "$xargs_parallel" -eq 0 ];then
809 | unset xargs_parallel
810 | fi
811 |
812 | ## POSIX xargs will even try to run an empty command
813 | ## therefor we wrap the call to curl in a shell that first checks
814 | ## if there any arguments
815 |
816 | # shellcheck disable=SC2016
817 | xargs -n "$_no_args" ${xargs_parallel+-P "$max_procs"} \
818 | sh -c '[ $# -gt 0 ] && exec "$0" "$@"' curl $http_backend_args
819 | }
820 |
821 | set_timestamp () {
822 | if [ -e "$cache_dir/timestamps/$1" ];then
823 | get_timestamp "$@"
824 | fi
825 | $awk 'BEGIN{ srand(); print srand() > "'"$cache_dir/timestamps/$1"'"}'
826 | }
827 |
828 | get_timestamp () {
829 | read -r "${2:-$1}" < "$cache_dir/timestamps/$1"
830 | }
831 |
832 | ################
833 | ## Subcommands #
834 | ################
835 |
836 | sync_followings () {
837 |
838 | if [ -z "$sync_followings" ];then
839 | die "You have to configure sync_followings in $config_file."
840 | fi
841 |
842 | curl --compressed -LSs "$sync_followings" > "$cache_dir/sync_followings.txt.new" || return
843 |
844 | touch "$cache_dir/sync_followings.txt"
845 | if cmp "$cache_dir/sync_followings.txt" "$cache_dir/sync_followings.txt.new" >/dev/null 2>&1; then
846 | rm "$cache_dir/sync_followings.txt.new"
847 | return
848 | fi
849 |
850 | tempdir="$cache_dir/tmp.$$"
851 | mkdir -p "$tempdir" || return
852 |
853 | sort "$follow_file" > "$tempdir/followings"
854 | sort "$cache_dir/sync_followings.txt" > "$tempdir/sync_followings.txt"
855 | sort "$cache_dir/sync_followings.txt.new" > "$tempdir/sync_followings.txt.new"
856 |
857 | comm -13 "$tempdir/sync_followings.txt" "$tempdir/sync_followings.txt.new" | \
858 | comm -13 "$tempdir/followings" - | \
859 | while read -r _nick _url; do
860 | follow "$_nick" "$_url"
861 | done
862 | mv "$cache_dir/sync_followings.txt.new" "$cache_dir/sync_followings.txt"
863 | rm -R "$tempdir"
864 | }
865 |
866 | update () {
867 | if ! [ -s "$follow_file" ] ;then
868 | die "You're not following anyone."
869 | fi
870 |
871 | if [ -e "$twtfile" ];then
872 | ln -sf "$twtfile" "$cache_dir/twtfiles/$nick.txt"
873 | fi
874 |
875 | {
876 | if [ $# -eq 0 ];then
877 | cat "$follow_file"
878 | else
879 | printf "%s\n" "$@" | $awk '
880 | BEGIN {
881 | follow_file = "'"$follow_file"'"
882 | while ((getline < follow_file) > 0 )
883 | followings[$1] = $2
884 | }
885 | followings[$1] {
886 | print $1 " " followings[$1]
887 | }
888 | '
889 | fi
890 | } | while read -r nick url;do
891 | rewrite_url
892 | args_for_curl "$nick" "$url"
893 | done | parallel_curl | process_stat_log
894 |
895 | for logfile in "$log_dir"/http.log.*;do
896 | [ -e "$logfile" ] || continue
897 | _nick="${logfile##*.}"
898 | $awk '{print "'"$_nick"'" ": " $0 }' "$logfile" >&2
899 | rm "$logfile"
900 | done
901 | }
902 |
903 | follow () {
904 |
905 | if [ "$1" = "$nick" ]; then
906 | die "You can't follow someone under your nick.";
907 | fi
908 |
909 | tempfile="$follow_file.$$"
910 | $awk '
911 | BEGIN {
912 | nick = "'"$1"'"
913 | url = "'"$2"'"
914 | force = '"$force"'
915 | stderr = "cat >&2"
916 | }
917 | $1 == nick || $2 == url {
918 | if ( force ) next
919 | printf "You are already following %s at %s.\n", $1, $2 | stderr
920 | exit 1
921 | }
922 | 1
923 | END {
924 | print nick, url
925 | }
926 | ' "$follow_file" > "$tempfile" && mv "$tempfile" "$follow_file" && info "You're now following $1."
927 | }
928 |
929 | unfollow () {
930 | tempfile="$follow_file.$$"
931 |
932 | UNFOLLOW="$*" $awk '
933 | BEGIN {
934 | split(ENVIRON["UNFOLLOW"], unfollow, " ")
935 | stderr = "cat >&2"
936 | }
937 | {
938 | remove=0
939 | for ( idx in unfollow ) {
940 | if ( unfollow[idx] == $1 || unfollow[idx] == $2 ) {
941 | remove=1
942 | delete unfollow[idx]
943 | break
944 | }
945 | }
946 | if ( remove == 0 ) {
947 | print
948 | }
949 | }
950 | END {
951 | for ( idx in unfollow ) {
952 | printf "You are not following %s.\n", unfollow[idx] | stderr
953 | }
954 | }
955 | ' < "$follow_file" > "$tempfile" && mv "$tempfile" "$follow_file" && info "You're not following $* anymore."
956 | }
957 |
958 | following () {
959 | if [ -n "$1" ]; then
960 | maybe_update "$@"
961 | export cache_dir follow_file
962 | printf "%s\n" "$@" | $awk '
963 | BEGIN {
964 | follow_file = ENVIRON["follow_file"]
965 | while ( ( getline < follow_file ) > 0 )
966 | followings[$1] = $2
967 | close(follow_file)
968 | }
969 | {
970 | if ( followings[$1] ) {
971 | twtfile = ENVIRON["cache_dir"] "/twtfiles/" $1 ".txt"
972 | while ( (getline < twtfile ) > 0 )
973 | if ( /^#/ && $2 == "following" )
974 | print $4 " " $5
975 | }
976 | }
977 | ' | sort | uniq
978 | elif [ -e "$follow_file" ];then
979 | cat "$follow_file"
980 | fi
981 | }
982 |
983 | url () {
984 | for url; do
985 | curl -s "$url" | $awk '
986 | BEGIN {
987 | url = "'"$url"'"
988 | }
989 | {
990 | c = c $0
991 | }
992 | END {
993 | if ( match(c,//) ) {
994 | start = RSTART+RLENGTH
995 | match(c,/<\/title>/)
996 | len = RSTART-start
997 | title = substr(c,start,len)
998 | gsub(/^[ \t]+|[ \t]+$/, "", title)
999 | sub(/\.$/,"", title)
1000 | print title " ⌘ " url
1001 | } else {
1002 | print url
1003 | }
1004 | }
1005 | '; done | tweet
1006 | }
1007 |
1008 | # shellcheck disable=SC2120
1009 | tweet () {
1010 | : >"$draft_file"
1011 |
1012 | if [ $# -eq 0 ];then
1013 | if [ -t 0 ];then
1014 | "$EDITOR" "$draft_file"
1015 | else
1016 | cat > "$draft_file"
1017 | fi
1018 | else
1019 | printf "%s\n" "$@" > "$draft_file"
1020 | fi
1021 | draft_to_twtfile
1022 | }
1023 |
1024 | ui () {
1025 | export FZF_DEFAULT_COMMAND="txtnish timeline -l 200 --theme=oneline 2>/dev/null"
1026 | fzf \
1027 | --exact \
1028 | --ansi \
1029 | --no-sort \
1030 | --bind="ctrl-t:execute(txtnish tweet)+reload($FZF_DEFAULT_COMMAND),ctrl-u:reload($FZF_DEFAULT_COMMAND),enter:ignore" \
1031 | --header 'Help: ctrl-u:Update ctrl-t:Tweet ctrl-q:Quit' \
1032 | --preview="echo {} | $formatter" --preview-window=down:10% \
1033 | --layout=reverse
1034 | }
1035 |
1036 | reply () {
1037 | use_pager=0
1038 | use_color=0
1039 | cat <<-EOF > "$draft_file"
1040 |
1041 | # Please enter your tweets. Lines starting with '# ' and empty
1042 | # lines will be ignored.
1043 | # You can follow or unfollow feeds by starting a line with a /,
1044 | # for example:
1045 | # /follow foo https://example.com/foo.txt
1046 | # Those lines will not be tweeted.
1047 |
1048 | EOF
1049 | timeline "$@" | $sed -e 's/^/# /' -e 's/^# $//' >> "$draft_file"
1050 | $editor "$draft_file"
1051 | tempfile="$draft_file.$$"
1052 | $sed -e '/^# /d' -e '/^[[:space:]]*$/d' "$draft_file" > "$tempfile"
1053 | mv "$tempfile" "$draft_file"
1054 | draft_to_twtfile
1055 | }
1056 |
1057 | mailpipe () {
1058 | if [ -z "$mail_password" ];then
1059 | die "You have to set mail_password!"
1060 | fi
1061 | # shellcheck disable=SC2119
1062 | $awk '
1063 | NR==1,/^$/ {next }
1064 | !pw_seen {
1065 | if ( "password '"$mail_password"'" != $0 ) {
1066 | exit
1067 | }
1068 | pw_seen=1;
1069 | next
1070 | }
1071 | /^[[:space:]]*$/ || /^>/ { next }
1072 | /-- / { exit }
1073 | 1
1074 | ' | tweet
1075 | }
1076 |
1077 | mail () {
1078 | have_cmd mail || die "mailx(1) not installed."
1079 |
1080 | use_pager=0
1081 | use_color=0
1082 | filter_expr="new()"
1083 | tempfile="$cache_dir/timeline.$$"
1084 | timeline > "$tempfile"
1085 | if [ -s "$tempfile" ]; then
1086 | command mail -s twtxt "$@" < "$tempfile"
1087 | fi
1088 | rm -f "$tempfile"
1089 | }
1090 |
1091 | timeline () {
1092 | set_timestamp last_timeline
1093 |
1094 | case $1 in
1095 | *://* )
1096 | curl --compressed -Ss -L --user-agent "$user_agent" "$1" \
1097 | | $awk 'BEGIN{OFS="\t"; url = "'"$1"'"}{ print url, url, "", $0 }' \
1098 | | display_tweets
1099 | exit 0
1100 | ;;
1101 | esac
1102 | maybe_update "$@";
1103 |
1104 | have_gpg=0
1105 | if have_cmd "$gpg_bin" ;then
1106 | have_gpg=1
1107 | fi
1108 |
1109 | {
1110 | if [ $# -eq 0 ];then
1111 | following | while read -r _nick _url; do
1112 | printf "%s %s\n" "$_nick" "$_url"
1113 | done
1114 | printf "%s %s\n" "$nick" "$twturl"
1115 | else
1116 | printf "%s\n" "$@" | $awk '
1117 | BEGIN {
1118 | follow_file = "'"$follow_file"'"
1119 | while ( (getline < follow_file) > 0 )
1120 | followings[$1] = $2
1121 | close(follow_file)
1122 | }
1123 | {
1124 | if ( followings[$1] )
1125 | print $1, followings[$1]
1126 | }
1127 | '
1128 | fi
1129 | } | while read -r nick url;do
1130 |
1131 | file="$cache_dir/twtfiles/$nick.txt"
1132 |
1133 | [ -e "$file" ] || continue
1134 |
1135 | prop=
1136 |
1137 | if [ "$have_gpg" -eq 1 ] && [ "$check_signatures" -eq 1 ];then
1138 | gpg_status="$(gpg_verify "$file")"
1139 | case $gpg_status in
1140 | *NODATA* ) prop=gpg_unsigned ;;
1141 | *NOPUBKEY* ) prop=gpg_signed ;;
1142 | *VALIDSIG* ) prop=gpg_trusted ;;
1143 | * ) prop=gpg_unknown ;;
1144 | esac
1145 | fi
1146 |
1147 | case $url in
1148 | https://* ) prop="${prop:+$prop,}tls" ;;
1149 | http://* ) prop="${prop:+$prop,}notls" ;;
1150 | ipfs://* | ipns://* ) prop=ipfs ;;
1151 | esac
1152 |
1153 | prefix_columns "$nick" "$url" "$prop" < "$file"
1154 | done | display_tweets
1155 | }
1156 |
1157 | update_metadata () {
1158 | [ "$add_metadata" -eq 0 ] && return
1159 | tempfile="$twtfile.$$"
1160 | {
1161 | printf "# %s = %s\n" \
1162 | client "txtnish/$VERSION" \
1163 | nick "$nick"
1164 | if [ -n "$twturl" ]; then
1165 | printf "# %s = %s\n" twturl "$twturl"
1166 | fi
1167 | if [ "$sign_twtfile" -eq 1 ];then
1168 | gpgconf --list-options gpg \
1169 | | awk -F: '$1 == "default-key" {print substr($10,2)}' \
1170 | | xargs "$gpg_bin" --fingerprint \
1171 | | awk '/Key fingerprint = / { print "# gpg_fingerprint = " substr($0,25) }'
1172 | fi
1173 | following | while read line; do printf "# following = %s\n" "$line" ;done
1174 | grep -v -e '^#' "$twtfile"
1175 | } > "$tempfile"
1176 | mv "$tempfile" "$twtfile"
1177 | }
1178 |
1179 | publish () {
1180 | update_metadata
1181 | if [ "$ipfs_publish" -eq 1 ] && have_cmd ipfs;then
1182 | _ipfs_path="$twtfile"
1183 | if [ "$ipfs_wrap_with_dir" -eq 1 ];then
1184 | _ipfs_args=-w
1185 | fi
1186 | if [ "$ipfs_recursive" -eq 1 ];then
1187 | _ipfs_args="$_ipfs_args -r"
1188 | _ipfs_path="${twtfile%/*}"
1189 | fi
1190 |
1191 | # shellcheck disable=SC2086
1192 | if ipfs add -q $_ipfs_args "$_ipfs_path" > "$cache_dir/ipfs";then
1193 | $awk 'END{ print $1 }' "$cache_dir/ipfs" | xargs ipfs name "publish"
1194 | fi
1195 | fi
1196 | if [ "$sign_twtfile" -eq 1 ];then
1197 | if [ -n "$sign_user" ]; then
1198 | signopt="-u $sign_user"
1199 | # for security, echo this
1200 | printf "Signing as %s.\n" "$sign_user"
1201 | else
1202 | unset signopt
1203 | fi
1204 | if mkdir -p "$cache_dir/tmp.$$/";then
1205 | tempdir="$cache_dir/tmp.$$/"
1206 | else
1207 | die "Can't create temporary dir $tempdir"
1208 | fi
1209 | if "$gpg_bin" --clearsign $signopt --output "$tempdir/${twtfile##*/}" "$twtfile";then
1210 | twtfile="$tempdir/${twtfile##*/}"
1211 | else
1212 | die "Can't sign twtfile. Exiting";
1213 | fi
1214 | fi
1215 | if [ -n "$scp_user" ] && [ -n "$scp_host" ];then
1216 | if [ "$sftp_over_scp" -eq 1 ]; then
1217 | sftp "$scp_user@$scp_host" <<-EOF
1218 | put "$twtfile" "${scp_remote_name:-${twtfile##*/}}"
1219 | EOF
1220 | else
1221 | scp "$twtfile" "$scp_user@$scp_host:${scp_remote_name:-${twtfile##*/}}"
1222 | fi
1223 | fi
1224 | if [ -n "$ftp_user" ] && [ -n "$ftp_host" ];then
1225 | if curl -Ss -nT "$twtfile" "ftp://$ftp_user@$ftp_host/${ftp_remote_name:-${twtfile##*/}}";then
1226 | info "Uploaded twtfile to ftp://$ftp_user@$ftp_host/${ftp_remote_name:-${twtfile##*/}}"
1227 | fi
1228 | fi
1229 | post_tweet_hook
1230 | }
1231 |
1232 | quickstart () {
1233 |
1234 | ## Import settings from twtxt
1235 |
1236 | if [ -e "${XDG_CONFIG_HOME:-$HOME/.config}/twtxt/config" ];then
1237 | if ! [ -e "$follow_file" ] && yesno "Import followings from twtxt?" ;then
1238 |
1239 | twtxt following | $awk '{ print $1, $3 }' > "$follow_file"
1240 | fi
1241 |
1242 | if ! [ -e "$config_file" ] && yesno "Import settings from twtxt?" ;then
1243 | nick=$(twtxt config twtxt.nick)
1244 | twturl=$(twtxt config twtxt.twturl)
1245 | limit=$(twtxt config twtxt.limit_timeline)
1246 | twtfile=$(twtxt config twtxt.twtfile)
1247 | sort_order=$(twtxt config twtxt.sorting)
1248 | disclose_identity=$(twtxt config twtxt.disclose_identity)
1249 |
1250 | {
1251 | [ -n "$nick" ] && printf "nick=%s\n" "$nick"
1252 | [ -n "$twturl" ] && printf "twturl=%s\n" "$twturl"
1253 | [ -n "$limit" ] && printf "limit=%s\n" "$limit"
1254 | [ -n "$twtfile" ] && printf "twtfile=%s\n" "$twtfile"
1255 | [ -n "$sort_order" ] && printf "sort_order=%s\n" "$sort_order"
1256 | [ -n "$disclose_identity" ] && printf "disclose_identity=%s\n" "$disclose_identity"
1257 |
1258 | } > "$config_file"
1259 | fi
1260 | fi
1261 |
1262 | ## Quickstart for new users
1263 |
1264 | getline "Please enter your desired nick" nick
1265 | getline "Please enter the desired location for your twtxt file" twtfile
1266 | getline "Please enter the URL your twtxt file will be accessible from" twturl
1267 | if yesno "Do you want to disclose your identity? Your nick and URL will be shared when making HTTP requests?";then
1268 | disclose_identity=1
1269 | fi
1270 |
1271 | if yesno "Import urls to follow we-are-twtx?" ;then
1272 | curl -Ss https://raw.githubusercontent.com/mdom/we-are-twtxt/master/we-are-twtxt.txt | \
1273 | xargs -n2 "$program_name" "follow"
1274 | fi
1275 |
1276 | if yesno "Do you want to upload your twtfile with scp?";then
1277 | getline "Please enter your scp username" scp_user
1278 | getline "Pleaser enter scp host" scp_host
1279 | fi
1280 |
1281 | if yesno "Do you want to upload your twtfile with ftp?";then
1282 | getline "Please enter your ftp username" ftp_user
1283 | getline "Pleaser enter ftp host" ftp_host
1284 | fi
1285 |
1286 | if yesno "Write configuration to $config_file?";then
1287 | if [ -e "$config_file" ];then
1288 | mv "$config_file" "$config_file.bak"
1289 | printf "Backup old config to %s.bak\n" "$config_file"
1290 | fi
1291 | cat <<-EOF > "$config_file"
1292 | nick="$nick"
1293 | twturl="$twturl"
1294 | twtfile="$twtfile"
1295 | disclose_identity="$disclose_identity"
1296 | EOF
1297 | if [ -n "$scp_user" ] && [ -n "$scp_host" ];then
1298 | cat <<-EOF >> "$config_file"
1299 | scp_user="$scp_user"
1300 | scp_host="$scp_host"
1301 | EOF
1302 | fi
1303 | if [ -n "$ftp_user" ] && [ -n "$ftp_host" ];then
1304 | cat <<-EOF >> "$config_file"
1305 | ftp_user="$ftp_user"
1306 | ftp_host="$ftp_host"
1307 | EOF
1308 | fi
1309 | printf "Write new configuration to %s\n" "$config_file"
1310 | fi
1311 |
1312 |
1313 | }
1314 |
1315 | ########################
1316 | # Command line parsing #
1317 | ########################
1318 |
1319 | check_if_valid_option () {
1320 | case ",$options," in
1321 | *,$1,* ) : ;;
1322 | * ) usage "Invalid option $OPTION" ;;
1323 | esac
1324 | }
1325 |
1326 |
1327 | set_optarg () {
1328 | case $1 in
1329 | -[!-]?* )
1330 | [ -n "${1#??}" ] || usage "Option ${1%%${1#??}} requires an argument."
1331 | OPTARG="${1#*??}"
1332 | ;;
1333 | --?*=?* )
1334 | [ -n "${1#*=}" ] || usage "Option ${1%%=*} requires an argument."
1335 | OPTARG="${1#*=}"
1336 | ;;
1337 | * )
1338 | [ -n "$2" ] || usage "Option $1 requires an argument."
1339 | OPTARG="$2"
1340 | SHIFT=2
1341 | ;;
1342 | esac
1343 | }
1344 |
1345 | set_optarg_bool () {
1346 | case $OPTION in
1347 | -[A-Z] ) OPTARG=0 ;;
1348 | -[a-z] ) OPTARG=1 ;;
1349 | --no-?* ) OPTARG=0 ;;
1350 | --?* ) OPTARG=1 ;;
1351 | esac
1352 | }
1353 |
1354 | check_arguments () {
1355 | $awk '
1356 | BEGIN {
1357 | args_given = '"$1"'
1358 | args_expected = split("'"$2"'",args)
1359 | stderr = "cat >&2"
1360 |
1361 | if ( args_given == args_expected ) exit 0
1362 |
1363 | if ( args_given > args_expected )
1364 | if ( args[ args_expected ] !~ /\.\.\.\]?$/ ) {
1365 | print "Too many arguments." | stderr
1366 | exit 1
1367 | }
1368 |
1369 | if ( args_given < args_expected )
1370 | for( i = 1; i <= args_expected; i++ )
1371 | if ( i > args_given && args[i] !~ /^\[.+\]$/ ) {
1372 | printf "Required argument %s missing.\n", args[i] | stderr
1373 | exit 1
1374 | }
1375 | }
1376 | '
1377 | }
1378 |
1379 | call_mode () {
1380 | while [ -n "$1" ]; do
1381 | OPTION="$1"
1382 | SHIFT=1
1383 | case $1 in
1384 | --theme | --theme=?* )
1385 | check_if_valid_option theme
1386 | set_optarg "$@"
1387 | theme="$OPTARG"
1388 | ;;
1389 | --timeout | --timeout=?* )
1390 | check_if_valid_option timeout
1391 | set_optarg "$@"
1392 | timeout="$OPTARG"
1393 | ;;
1394 | -l | --limit | --limit=?* | -l?* )
1395 | check_if_valid_option limit
1396 | set_optarg "$@"
1397 | limit="$OPTARG"
1398 | ;;
1399 | -N | --max-procs | --max-procs=?* | -N?* )
1400 | check_if_valid_option max-procs
1401 | set_optarg "$@"
1402 | max_procs="$OPTARG"
1403 | ;;
1404 | -S | --search | --search=?* | -S?* )
1405 | check_if_valid_option search
1406 | set_optarg "$2"
1407 | filter_expr="$OPTARG"
1408 | ;;
1409 | -h | --help )
1410 | check_if_valid_option help
1411 | usage
1412 | ;;
1413 | -p | -P | --pager | --no-pager )
1414 | check_if_valid_option pager
1415 | set_optarg_bool
1416 | use_pager="$OPTARG"
1417 | ;;
1418 | -u | -U | --update | --no-update )
1419 | check_if_valid_option update
1420 | set_optarg_bool
1421 | always_update="$OPTARG"
1422 | ;;
1423 | -a | --ascending )
1424 | check_if_valid_option ascending
1425 | sort_order=ascending
1426 | ;;
1427 | -d | --descending )
1428 | check_if_valid_option descending
1429 | sort_order=descending
1430 | ;;
1431 | -R | --raw )
1432 | check_if_valid_option raw
1433 | theme=raw
1434 | ;;
1435 | -v | --verbose )
1436 | check_if_valid_option verbose
1437 | verbose=1
1438 | ;;
1439 | -f | --force )
1440 | check_if_valid_option force
1441 | force=1
1442 | ;;
1443 | -- )
1444 | shift
1445 | break
1446 | ;;
1447 | -[!-]?* )
1448 | shift
1449 | ## dash 0.5.7 can't handle direct expansion
1450 | _tmp=${OPTION#??}
1451 | set -- "${OPTION%$_tmp}" "-$_tmp" "$@"
1452 | SHIFT=0
1453 | ;;
1454 | -* )
1455 | usage "Invalid option $1."
1456 | ;;
1457 | * )
1458 | break
1459 | ;;
1460 | esac
1461 | shift "$SHIFT"
1462 | done
1463 |
1464 | if [ -n "$arguments" ]; then
1465 | if ! check_arguments $# "$arguments"; then
1466 | usage
1467 | exit 1
1468 | fi
1469 | fi
1470 |
1471 | case $mode in
1472 | *-* ) mode="${mode%-*}_${mode#*-}"
1473 | esac
1474 |
1475 | $mode "$@"
1476 | }
1477 |
1478 | usage_main () {
1479 | if [ -n "$1" ];then
1480 | printf "%s\n" "$1" >&2;
1481 | exec >&2
1482 | fi
1483 | cat <<-EOF
1484 | usage: $program_name COMMAND [OPTIONS...]
1485 |
1486 | Command:
1487 | tweet Append a new tweet to your twtxt file.
1488 | timeline Retrieve your personal timeline.
1489 | follow Add a new source to your followings.
1490 | unfollow Remove an existing source from your followings.
1491 | following Return the list of sources you're following.
1492 | reply Reply to tweets.
1493 | publish Publish your twtfile.
1494 | sync-followings Sync followings from remote file.
1495 | mail Send new tweets per mail.
1496 | url Share urls
1497 | ui Start fzf based user-interface (experimental).
1498 |
1499 | Options:
1500 | -h, --help Print a help message and exit.
1501 | -V, --version Print version and exit.
1502 |
1503 | EOF
1504 | if [ -n "$1" ];then
1505 | exit 1
1506 | else
1507 | exit 0
1508 | fi
1509 | }
1510 |
1511 | usage () {
1512 | _err=$1
1513 | if [ -n "$_err" ];then
1514 | printf "%s\n" "$_err" >&2
1515 | exec >&2
1516 | fi
1517 |
1518 | cat <<-EOF
1519 | usage: $program_name $mode [OPTIONS...]${arguments:+ $arguments}
1520 |
1521 | Synopsis:
1522 | $synopsis
1523 |
1524 | Options:
1525 | EOF
1526 |
1527 | options="$options,"
1528 | while [ -n "$options" ] ;do
1529 | c=${options%%,*}
1530 | case $c in
1531 | help ) printf " -h, --help\n\tPrint a help message and exit.\n" ;;
1532 | limit ) printf " -l, --limit\n\t NUM Limit total numer of tweets shown.\n" ;;
1533 | ascending ) printf " -a, --ascending\n\tSort timeline in ascending order.\n" ;;
1534 | descending ) printf " -d, --descending\n\tSort timeline in descending order.\n" ;;
1535 | pager ) printf " -p, --pager\n\tUse pager to display content.\n" ;;
1536 | no-pager ) printf " -P, --no-pager\n\tDo not use pager to display content.\n" ;;
1537 | update ) printf " -u, --update\n\tUpdate sources.\n" ;;
1538 | no-update ) printf " -U, --no-update\n\tDo not update sources.\n" ;;
1539 | raw ) printf " -R, --raw\n\tPrint raw timeline.\n" ;;
1540 | max-procs ) printf " -N, --max-procs NUM\n\tUse NUM parallel download processes.\n" ;;
1541 | search ) printf " -S, --search EXP\n\tFilter tweets\n" ;;
1542 | config ) printf " -c, --config CFG\n\tSpecify a custom config file location.\n" ;;
1543 | force ) printf " -f, --force\n\tDisable safety checks and force action.\n" ;;
1544 | theme ) printf " --theme THEME\n\tUse theme to display timeline.\n" ;;
1545 | timeout ) printf " --timeout SECONDS\n\tMaximum time in seconds to fetch a feed.\n" ;;
1546 | esac
1547 | options=${options#$c,}
1548 | done
1549 |
1550 | ## Always end usage with a empty line
1551 | printf "\n"
1552 |
1553 | if [ -n "$_err" ];then
1554 | exit 1
1555 | else
1556 | exit 0
1557 | fi
1558 | }
1559 |
1560 | check_curl () {
1561 | if ! have_cmd curl;then
1562 | die "curl has to be installed."
1563 | fi
1564 |
1565 | oIFS="$IFS"
1566 | IFS=.
1567 | set -- $(curl -V | $awk '{print $2;exit}')
1568 | if [ "$1" -lt 7 ] || [ "$1" -eq 7 ] && [ "$2" -lt 26 ];then
1569 | die "Need at least curl 7.26.0."
1570 | fi
1571 |
1572 | IFS="$oIFS"
1573 | }
1574 |
1575 | #########
1576 | ## Main #
1577 | #########
1578 |
1579 | main() {
1580 |
1581 | trap cleanup EXIT
1582 |
1583 | mkdir -p "$config_dir"
1584 | mkdir -p "$cache_dir"
1585 | mkdir -p "$cache_dir/twtfiles"
1586 | mkdir -p "$cache_dir/timestamps"
1587 | mkdir -p "$log_dir"
1588 |
1589 | if ! [ -e "$follow_file" ];then
1590 | :> "$follow_file"
1591 | fi
1592 |
1593 | check_curl
1594 |
1595 | while [ -n "$1" ]; do
1596 | SHIFT=1
1597 | OPTION="$1"
1598 | case $1 in
1599 | -V | --version )
1600 | printf "%s\n" "$VERSION"
1601 | exit 0
1602 | ;;
1603 | -h | --help )
1604 | usage_main
1605 | ;;
1606 | -c | --config | --config=?* | -c?* )
1607 | set_optarg "$@"
1608 | config_file="$OPTARG"
1609 | [ -e "$config_file" ] || die "Missing configuration file '$config_file'";
1610 | ;;
1611 | -??* )
1612 | shift
1613 | ## dash 0.5.7 can't handle direct expansion
1614 | _tmp=${OPTION#??}
1615 | set -- "${OPTION%$_tmp}" "-$_tmp" "$@"
1616 | SHIFT=0
1617 | ;;
1618 | -* )
1619 | usage "Invalid option $1"
1620 | ;;
1621 | * )
1622 | break
1623 | ;;
1624 | esac
1625 | shift "$SHIFT"
1626 | done
1627 |
1628 | shift $(( OPTIND - 1))
1629 |
1630 | read_config
1631 |
1632 | mode=$1
1633 |
1634 | if [ -z "$mode" ];then
1635 | usage_main
1636 | exit 1
1637 | fi
1638 |
1639 | shift
1640 |
1641 | case $mode in
1642 | update )
1643 | synopsis="Fetching new twtfiles from all your sources."
1644 | options="help,verbose,max-procs,timeout"
1645 | ;;
1646 | follow )
1647 | synopsis="Add a new source to your followings."
1648 | arguments="NICK SOURCE"
1649 | options="help,verbose,force"
1650 | ;;
1651 | unfollow )
1652 | synopsis="Remove an existing source from your followings."
1653 | arguments="NICK..."
1654 | options="help,verbose"
1655 | ;;
1656 | following )
1657 | synopsis="Return the list of sources you're following."
1658 | options="help,verbose"
1659 | ;;
1660 | timeline | view )
1661 | synopsis="Display timeline."
1662 | arguments="[NICK...]"
1663 | options="help,limit,verbose,ascending,descending,update,no-update,max-procs,search,pager,no-pager,raw,theme,timeout"
1664 | ;;
1665 | ui )
1666 | synopsis="Start fzf based user-interface (experimental)."
1667 | options="help,verbose"
1668 | ;;
1669 | reply )
1670 | synopsis="Reply to tweets."
1671 | arguments="[NICK]"
1672 | options="help,limit,verbose,ascending,descending,update,no-update,max-procs,search,timeout"
1673 | ;;
1674 | publish )
1675 | synopsis="Publish your twtfile."
1676 | options="help,verbose"
1677 | ;;
1678 | tweet )
1679 | synopsis="Append a new tweet to your twtxt file."
1680 | arguments="[TWEET...]"
1681 | options="help,verbose"
1682 | ;;
1683 | url )
1684 | synopsis="Share urls with your followers."
1685 | arguments="URL..."
1686 | options="help,verbose"
1687 | ;;
1688 | sync-followings )
1689 | synopsis="Sync followings from a remote file."
1690 | options="help,verbose"
1691 | ;;
1692 | mail )
1693 | synopsis="Mail new tweets."
1694 | arguments="ADDRESS..."
1695 | options="help,verbose,ascending,descending,update,no-update"
1696 | ;;
1697 | mailpipe )
1698 | synopsis="Read mails from stdin and tweet them."
1699 | options="help,verbose"
1700 | ;;
1701 | quickstart )
1702 | synopsis="Import settings from twtxt."
1703 | options="help,verbose"
1704 | ;;
1705 | * ) printf "Unknown mode %s.\n" "$mode" >&2; usage_main; exit 1;;
1706 |
1707 | esac
1708 |
1709 | call_mode "$@"
1710 |
1711 | exit 0
1712 | }
1713 |
1714 | main "$@"
1715 |
--------------------------------------------------------------------------------
/completions/_txtnish:
--------------------------------------------------------------------------------
1 | #compdef txtnish
2 | # ---------------------------------------------------------------------
3 | # Copyright (c) 2017 Timur Demin
4 | # All rights reserved.
5 | #
6 | # This file is part of txtnish.
7 | #
8 | # txtnish is free software: you can redistribute it and/or modify
9 | # it under the terms of the GNU General Public License as published by
10 | # the Free Software Foundation, either version 3 of the License, or
11 | # (at your option) any later version.
12 | #
13 | # txtnish is distributed in the hope that it will be useful,
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | # GNU General Public License for more details.
17 | #
18 | # You should have received a copy of the GNU General Public License
19 | # along with txtnish. If not, see .
20 |
21 | _alternative 'args:custom args:((\
22 | follow\:"Add a new source to your followings" \
23 | following\:"Return the list of sources you are following" \
24 | mail\:"Send new tweets per mail" \
25 | publish\:"Publish your twtfile" \
26 | reply\:"Reply to tweets" \
27 | sync-followings\:"Sync followings from remote file" \
28 | timeline\:"Retrieve your personal timeline" \
29 | tweet\:"Append a new tweet to your twtxt file" \
30 | unfollow\:"Remove an existing source from your followings" \
31 | url\:"Share urls" \
32 | -h\:"Print a help message and exit" \
33 | -V\:"Print version and exit"))' \
34 | "files:filenames:($([ -d ${HOME}/.cache/txtnish/twtfiles ] && /bin/ls \
35 | ~/.cache/txtnish/twtfiles | sed 's/.txt//g'))"
36 |
37 |
--------------------------------------------------------------------------------
/example.config:
--------------------------------------------------------------------------------
1 | # Example config for txtnish
2 | # This is a normal shell file that gets sourced by txtnish
3 |
4 | # How many tweets should be shown in timeline
5 | limit=20
6 |
7 | # Command to wrap long tweets in timeline
8 | formatter="fmt"
9 |
10 | # Should the timeline be shown in a pager
11 | use_pager=1
12 |
13 | # Use ANSI colors
14 | use_color=1
15 |
16 | # Always update all feeds before showing tweets. If you set this variable to 0,
17 | # you need to update manually with the update command.
18 | always_update=1
19 |
20 | # How to sort tweets. This can be either ascending or descending. ascending
21 | # starts with the oldest tweet, descending with the newest.
22 | sort_order=descending
23 |
24 | # Location of your twtxt file. The tweet subcommand will append to this file.
25 | twtfile=~/twtxt.txt
26 |
27 | # Send your nick and twturl in the http user-agent field
28 | disclose_identity=0
29 |
30 | # How many parallel http processes to use
31 | max_procs=20
32 |
33 | # Editor to tweet
34 | editor=vi
35 |
36 | # Pager to use, use_pager has to be also set to one
37 | pager="less -R"
38 |
39 | # Maximum time in seconds for an http operation. Set to zero to not set
40 | # any timeout.
41 | timeout=5
42 |
43 | # Use xargs with -P. If you have a xargs without -P (it's not part of POSIX)
44 | # and disable xargs_parallel, you probably also want to set always_update to 0,
45 | # otherwise any command will take a long time.
46 | xargs_parallel=1
47 |
48 | # If set to 1, sign the twtfile with pgp. In case you are also overwriting
49 | # the `post_tweet_hook` note that this will create a signed file in a
50 | # temporary directory and change the value of `twtfile` accordingly.
51 | sign_twtfile=0
52 |
53 | # Verify pgp signatures and show the result in the timeline if set to 1. Defaults to 0.
54 | check_signature=1
55 |
56 | # If these paramters are set, txtnish will transfer the twtfile with ftp using
57 | # the given user and hostname. The paramter ftp_remote_name is optional, if not
58 | # given the basename of your twtfile is used.
59 | ftp_user=$user
60 | ftp_host=localhost
61 | ftp_remote_name=twtxt.txt
62 |
63 | # If these paramters are set, txtnish will transfer the twtfile with scp using
64 | # the given user and hostname. The paramter scp_remote_name is optional, if not
65 | # given the basename of your twtfile is used.
66 | scp_user=$user
67 | scp_host=localhost
68 | scp_remote_name=twtxt.txt
69 |
70 | # post_tweet_hook is called after txtnish has appended new tweets to your
71 | # twtfile. It's a good place to uploade your file somewhere.
72 | post_tweet_hook () {
73 | gist -u ID -f "$twtfile"
74 | }
75 |
--------------------------------------------------------------------------------
/t/tapsig:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | TAPSIG_TEST=1
4 |
5 | PATH="$(pwd)/bin:$PATH"
6 |
7 | plan=0
8 | rc_plan=0
9 | tapsig_state="running"
10 |
11 | ok () {
12 | plan=$(( plan + 1 ))
13 | _ok "$@"
14 | }
15 |
16 | not_ok () {
17 | plan=$(( plan + 1 ))
18 | _not_ok "$@"
19 | }
20 |
21 | _ok () {
22 | printf "ok %i%s%s\n" "$plan" "${name:+ $name}" "${todo:+ # TODO $todo}"
23 | }
24 |
25 | _not_ok () {
26 | printf "not ok %i%s%s\n" "$plan" "${name:+ $name}" "${todo:+ # TODO $todo}"
27 | }
28 |
29 | skip () {
30 | skip=$1
31 | }
32 |
33 | todo () {
34 | todo=$1
35 | }
36 |
37 | depends_on () {
38 | if ! command -v "$1" >/dev/null 2>&1;then
39 | skip_all "$1 not installed."
40 | fi
41 | }
42 |
43 | skip_all () {
44 | clean_up
45 | printf "1..0 # SKIP%s\n" "${1+ $1}"
46 | exit 0
47 | }
48 |
49 | # shellcheck disable=SC2015
50 | check_expections () {
51 | if [ "$plan" != 0 ];then
52 | test_files &&
53 | [ "${rc-0}" -eq "$rc_plan" ] &&
54 | _ok ||
55 | _not_ok
56 | fi
57 | }
58 |
59 | test_files () {
60 | for i in "$TAPSIG_WORK_DIR"/*_plan;do
61 | if ! test_output "$i" "${i%_plan}";then
62 | return 1
63 | fi
64 | done
65 | return 0
66 | }
67 |
68 | name () {
69 | name=$1
70 | }
71 |
72 | test_output () {
73 | if cmp -s "$1" "$2";then
74 | return 0
75 | else
76 | # shellcheck disable=SC2119
77 | diff "$1" "$2" | diag
78 | return 1
79 | fi
80 | }
81 |
82 | done_testing () {
83 | tapsig_state="done"
84 | }
85 |
86 | tap () {
87 | touch "$TAPSIG_WORK_DIR/stdout_plan" "$TAPSIG_WORK_DIR/stderr_plan"
88 | plan=$(( plan + 1 ))
89 | if [ -n "$skip" ];then
90 | printf "ok %i # SKIP $skip\n" "$plan"
91 | else
92 | "$@" > "$TAPSIG_WORK_DIR/stdout" 2> "$TAPSIG_WORK_DIR/stderr"
93 | rc=$?
94 | check_expections
95 | fi
96 | clean_up
97 | }
98 |
99 | # shellcheck disable=SC2120
100 | diag () {
101 | if [ -n "$1" ];then
102 | printf "# %s\n" "$1"
103 | else
104 | sed 's/^/# /'
105 | fi
106 | }
107 |
108 | bail_out () {
109 | printf "Bail out!%s\n" "${1+ $1}"
110 | exit 0
111 | }
112 |
113 | stdout_is () { cat > "$TAPSIG_WORK_DIR/stdout_plan"; }
114 | stderr_is () { cat > "$TAPSIG_WORK_DIR/stderr_plan"; }
115 |
116 | file_is () {
117 | cat > "$TAPSIG_WORK_DIR/$1_plan"
118 | }
119 |
120 | rc_is () { rc_plan=$1; }
121 |
122 | clean_up () {
123 | for i in "$TAPSIG_WORK_DIR"/*_plan;do
124 | rm -f "$i" "${i%_plan}"
125 | done
126 | rc_plan=0
127 | todo=""
128 | skip=""
129 | name=""
130 | }
131 |
132 | if [ -e "$1" ];then
133 | TAPSIG_WORK_DIR=".tapsig.$$"
134 | if ! mkdir "$TAPSIG_WORK_DIR";then
135 | printf "Can't create working directory %s.\n" "$TAPSIG_WORK_DIR" >&2
136 | exit 1
137 | fi
138 |
139 | . "$1"
140 |
141 | rm -R "$TAPSIG_WORK_DIR"
142 |
143 | if [ "$tapsig_state" = "done" ];then
144 | printf "1..%s\n" "$plan"
145 | exit 0
146 | elif [ $plan -gt 0 ];then
147 | printf "# Tests were run but done_testing() was not seen.\n" >&2
148 | exit 1
149 | else
150 | exit 1
151 | fi
152 | fi
153 |
154 | exit 0
155 |
156 |
--------------------------------------------------------------------------------
/xt/01shellcheck.t:
--------------------------------------------------------------------------------
1 | #!tapsig
2 |
3 | depends_on shellcheck
4 |
5 | tap shellcheck $TAPSIG_ORIG_DIR/bin/txtnish
6 |
7 | done_testing
8 |
--------------------------------------------------------------------------------