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