minor: docs
[pdt.git] / pdt.sh
CommitLineData
c0e2704f 1#!/bin/bash
f22362c9
IK
2# pdt: post to FSF social media via command line
3# Copyright (C) 2021 Ian Kelling
c0e2704f
IK
4# SPDX-License-Identifier: AGPL-3.0-or-later
5
f22362c9
IK
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU Affero General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU Affero General Public License for more details.
15
16# You should have received a copy of the GNU Affero General Public License
17# along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19# This is meant to be sourced.
c0e2704f
IK
20
21PATH="$HOME/.local/bin:$PATH"
22
f9ace606
IK
23_pdtsh_file="$(readlink -f -- "${BASH_SOURCE[0]}")"
24_pdtsh_dir="${_pdtsh_file%/*}"
25_pdtsh_tweet="$_pdtsh_dir/t.py"
03c21027 26_pdtsh_blocks=██████████████████████████████████████████████████████████████
f9ace606 27
bcf71df6 28# usage: tweet [--PROFILE_NAME] [POST...]
f9ace606
IK
29# Uses variables as input:
30# $media for image file path.
31# $alt_text for image alt text.
026344f1 32#
bcf71df6
IK
33# --PROFILE_NAME Either dbd or fsf. Defaults to whatever profile was used
34# last in any pdt command.
026344f1
IK
35#
36# Note retweeting cannot be done with the gratis api level. I suggest
37# posting a message like: 'Retweet:
38# https://nitter.net/fsf/status/1726382360826958325'
f9ace606 39tweet() {
c8c18954
IK
40 local keys_file profile verbose_arg ret out line num_regex account_real_name
41 local md5_text
42 local -a out_lines
43 ret=0
44 if $verbose; then
45 verbose_arg=-v
46 fi
03c21027 47 keys_file=$_pdtsh_dir/twitter_keys.py
bcf71df6
IK
48 if [[ $1 == --* ]]; then
49 profile=${1#--}
03c21027 50 rm -f $keys_file
bcf71df6 51 ln -s $keys_file-$profile $keys_file
f9ace606
IK
52 shift
53 fi
c8c18954
IK
54 if [[ $profile == dbd ]]; then
55 account_real_name=endDRM
56 elif [[ $profile ]]; then
57 account_real_name=$profile
58 else
59 account_real_name=USERNAME_HERE
60 fi
03c21027
IK
61
62 # shellcheck disable=SC1090 # not relevant to this script
63 source ~/src/tweepy/venv/bin/activate
c8c18954
IK
64 out=$(
65 {
66 printf "%s\n" "$*"
67 if [[ $media ]]; then
68 printf "%s\n" "$media"
69 if [[ $alt_text ]]; then
70 printf "%s\n" "$alt_text"
71 fi
72 fi
73 } | $_pdtsh_tweet $verbose_arg 2>&1 ) || ret=1
74 if [[ $media ]]; then
75 md5_text="$(md5sum $media | awk '{print $1}') "
76 fi
77 post_info="$md5_text $*"
78 if (( ret == 1 )); then
79 if printf "%s\n" "$out" \
80 | grep -Fx 'You are not allowed to create a Tweet with duplicate content.' &>/dev/null; then
81 # We dont output duplicate error when not in verbose mode, because it is
82 # is pretty easy to trigger.
1bed5f3e 83 if [[ -s ~/pdt/tweets.log ]] && grep -qFx -- "$post_info" ~/pdt/tweets.log; then
c8c18954
IK
84 if $verbose; then
85 echo "pdt: error: post failed. twitter and our log says this tweet is duplicate." >&2
86 fi
87 else
88 printf "%s\n" "$post_info" >> ~/pdt/tweets.log
89 if $verbose; then
90 printf "%s\n" "$out" >&2
91 fi
f9ace606 92 fi
c8c18954
IK
93 else
94 printf "%s\n" "$out" >&2
f9ace606 95 fi
c8c18954
IK
96 deactivate
97 return 1
98 fi
99 ## handle case of ret == 0
100 mapfile -t out_lines <<<"$out"
101 num_regex='$[0-9][0-9]*$'
102 for line in "${out_lines[@]}"; do
103 if [[ $line =~ $num_regex ]]; then
104 echo "https://nitter.net/$account_real_name/status/$line"
105 fi
106 done
107 if [[ $line ]]; then
1bed5f3e
IK
108 if [[ -s ~/pdt/tweets.log ]] && grep -qFx -- "$post_info" ~/pdt/tweets.log; then
109 echo "pdt: this tweet was a duplicate according to our log but unexpectedly was allowed."
c8c18954
IK
110 else
111 printf "%s\n" "$post_info" >> ~/pdt/tweets.log
112 fi
113 fi
f9ace606
IK
114 deactivate
115}
116
026344f1
IK
117_pdtsh-tweet-option() {
118 local keys_file option
119 option=$1
120 shift
03c21027 121 keys_file=$_pdtsh_dir/twitter_keys.py
bcf71df6
IK
122 if [[ $1 == --* ]]; then
123 profile=${1#--}
03c21027 124 rm -f $keys_file
bcf71df6 125 ln -s $keys_file-$profile $keys_file
c0e2704f
IK
126 shift
127 fi
03c21027
IK
128 # shellcheck disable=SC1090 # not relevant to this script
129 source ~/src/tweepy/venv/bin/activate
026344f1 130 $_pdtsh_tweet $option $1 || { deactivate; return 1; }
c0e2704f
IK
131 deactivate
132}
133
bcf71df6 134# usage: tweetrm [--PROFILE_NAME] POST_ID
026344f1
IK
135#
136# Delete twitter post. Post id is the number at end of a url like:
137# https://nitter.net/user/status/1725640623149969527
138#
bcf71df6 139# --PROFILE_NAME see see documentation for tweet()
026344f1
IK
140tweetrm() {
141 _pdtsh-tweet-option --delete "$@"
142}
143
144
145
bcf71df6 146# usage: pin-tweet [--PROFILE_NAME] POST_ID
026344f1
IK
147#
148# Pin twitter post. Post id is the number at end of a url like:
149# https://nitter.net/user/status/1725640623149969527
150#
bcf71df6 151# --PROFILE_NAME see see documentation for tweet()
026344f1
IK
152#
153pin-tweet() {
154 _pdtsh-tweet-option --pin "$@"
155}
156
bcf71df6 157# usage: unpin-tweet [--PROFILE_NAME] POST_ID
026344f1
IK
158#
159# Unpin twitter post. Post id is the number at end of a url like:
160# https://nitter.net/user/status/1725640623149969527
161#
bcf71df6 162# --PROFILE_NAME see see documentation for tweet()
026344f1
IK
163#
164unpin-tweet() {
165 _pdtsh-tweet-option --unpin "$@"
166}
167
168
bcf71df6 169# usage: twitter-banner [--PROFILE_NAME] IMAGE_PATH
026344f1
IK
170#
171# IMAGE_PATH Path to jpg or png, 1500 px by 500px.
172#
bcf71df6 173# --PROFILE_NAME see see documentation for tweet()
026344f1
IK
174#
175twitter-banner() {
176 _pdtsh-tweet-option --banner "$@"
177}
178
f9ace606 179
bcf71df6
IK
180# usage: toot [--PROFILE_NAME] [TOOT_ARGS]
181#
182# --PROFILE_NAME Either dbd or fsf. Defaults to whatever profile was used
183# last in any pdt command.
c0e2704f 184toot() {
9667049b 185 local mast_profile
03c21027 186 # shellcheck disable=SC1090 # not relevant to this script
c0e2704f 187 source ~/src/toot/venv/bin/activate
bcf71df6
IK
188 if [[ $1 == --* ]]; then
189 mast_profile=${1#--}
c0e2704f 190 shift
c8c18954 191 command toot activate $toot_quiet_arg $mast_profile || { deactivate; return 1; }
c0e2704f
IK
192 fi
193 command toot "$@" || { deactivate; return 1; }
194 deactivate
195}
196
197
026344f1 198# post to mastodon and/org twitter.
03c21027
IK
199#
200# usage:
201# pdt [-s mastodon|twitter|gnusocial] [--dbd] [-m IMAGE_FILE] [-a ALT_TEXT] [POST]
9667049b 202#
026344f1
IK
203# -s mastodon|twitter|gnusocial Post to a single social network.
204#
bcf71df6
IK
205# --dbd Use dbd account instead of fsf.
206#
026344f1
IK
207# -m IMAGE_FILE Uploads IMAGE_FILE. if MEDIA_FILE.txt exists, the last
208# line of that file will be used as ALT_TEXT unless -a
209# has been used.
03c21027 210#
bcf71df6
IK
211# -a ALT_TEXT Alt-text of IMAGE_FILE.
212#
7759d3bf
IK
213# Without POST, you will be prompted for it. POST can have have some
214# special markup for twitter so you can have longer than 280 char posts
215# to mastodon, then split them or truncate them for twitter.
03c21027 216#
026344f1
IK
217# * /tnt/ short for "twitter next tweet". /tnt/ will be removed, and the
218# tweet will be split into 2 at that point.
219#
220# * /teof/ short for "twitter end of file." It will be removed and text
221# after it won't be posted to twitter. An example of how this is
222# useful: You write a post which is 284 characters. Instead of
223# splitting it in 2 for twitter, you could put the least important
224# hash tag and the end and omit it on twitter.
225#
1bed5f3e
IK
226# Twitter considers all urls to be 23 characters.
227#
228# To search a file of posts for any posts with longer than 280
229# characters which haven't been split with /tnt/, run the following
230# (replacing FILE with a path like /home/common/campaigns/pdt/pdt.txt).
03c21027 231#
026344f1 232# sed -r 's,/teof/.*,,;s/ *-(m|a|-dbd) [^ ]*//;s,https?://[^ ]*,https://xxxxxxxxxxxxxxx,g' FILE | awk '$0 !~ /\/tnt\// && length > 280 {print length, $0}'
03c21027 233#
08604409
IK
234# That will print the length of the post as twitter sees it (with 23
235# characters per url).
236#
21bcee5c
IK
237# broken usage:
238# pdt [-v VIDEO_PATH] [POST]
239#
026344f1
IK
240# Outdated usage:
241#
242# Gnu Social posting code exists, but we aren't using it. You would need
243# to run pdt-gnusocial-setup beforehand. Alt text does not work on
244# gnusocial.
245#
c0e2704f 246pdt() {
af6ab451 247 local video media twitter_account gs_account mastodon_account video gs_arg network
c8c18954 248 local do_mastodon do_twitter do_gnusocial verbose
9667049b 249 local -a toot_args
c0e2704f 250 if [[ $pdttest ]]; then
f9ace606 251 twitter_account=iank
c0e2704f 252 gs_account=fsfes
9667049b 253 mastodon_account=fsftest@hostux.social
c0e2704f 254 else
f9ace606 255 twitter_account=fsf
c0e2704f
IK
256 gs_account=fsf
257 mastodon_account=fsf@hostux.social
258 fi
9c8eb6e7 259 video=false
9667049b 260 do_mastodon=true
03c21027
IK
261 do_twitter=true
262 do_gnusocial=false
cdecedd1 263 alt_text=
c0e2704f
IK
264 while [[ $1 == -* ]]; do
265 case $1 in
9667049b
IK
266 -s)
267 network="$2"
268 do_mastodon=false
269 do_twitter=false
270 do_gnusocial=false
271 case $network in
272 mastodon)
273 do_mastodon=true
274 ;;
275 twitter)
276 do_twitter=true
277 ;;
278 gnusocial)
279 do_gnusocial=true
280 ;;
281 *)
282 echo "pdt: error: expected -s mastodon|twitter|gnusocial"
283 return 1
284 ;;
285 esac
286 shift 2
287 ;;
288 -a)
289 alt_text="$2"
9667049b
IK
290 shift 2
291 ;;
c0e2704f
IK
292 -m)
293 media="$2"
294 shift 2
9667049b 295 toot_args+=(--media "$media" )
c0e2704f
IK
296 gs_arg="-F media=@$media"
297 ;;
298 --dbd)
f9ace606 299 twitter_account=dbd
c0e2704f
IK
300 gs_account=dbd
301 mastodon_account=endDRM@hostux.social
302 shift
303 ;;
9c8eb6e7
IK
304 -v)
305 video=true
306 media="$2"
307 shift 2
308 ;;
c0e2704f
IK
309 esac
310 done
9c8eb6e7
IK
311 if [[ $media ]]; then
312 if [[ ! -e $media ]]; then
313 echo "error: file not found $media"
314 return 1
315 fi
316 if [[ $media == *\ * ]]; then
317 echo "error: file path contains a space. move it to non-space path"
318 return 1
319 fi
c95a2b7e
IK
320 if [[ ! $alt_text && -r $media.txt && -s $media.txt ]]; then
321 alt_text=$(tail -n1 $media.txt)
322 fi
af6ab451
IK
323 if [[ $alt_text ]]; then
324 toot_args+=(--description "$alt_text" )
325 fi
9c8eb6e7 326 fi
c0e2704f
IK
327 # if we have no argument
328 if (( ! $# )); then
329 read -r -p "input PDT text: " input
330 set -- "$input"
331 fi
c8c18954
IK
332 verbose=false
333 toot_quiet_arg=--quiet
c0e2704f 334 if [[ $- == *i* ]]; then
c8c18954
IK
335 verbose=true
336 toot_quiet_arg=
c0e2704f 337 echo "About to PDT the following line. Press enter to confirm or ctrl-c to quit:"
9667049b
IK
338 echo "$*"
339 read -r
c0e2704f 340 fi
9c8eb6e7
IK
341 if $video; then
342 local oath
343 oath=$HOME/.rainbow_oauth
9c8eb6e7 344 rm -f $oath
f9ace606 345 ln -s ${oath}-$twitter_account $oath
4b26c43d 346 python3 ~/src/video-tweet/async-upload.py "$media" "$*"
9c8eb6e7
IK
347 return
348 fi
c0e2704f 349 fails=()
9667049b 350 if $do_twitter; then
7759d3bf 351 if ! tweet --$twitter_account "${*%/teof/*}"; then
9667049b
IK
352 fails+=(tweet)
353 fi
c0e2704f 354 fi
03c21027
IK
355
356 # remove /tnt/ for non-twitter social media.
357 text="$*"
358 text=${text// \/tnt\/ / }
359 text="${text//\/tnt\/}"
360 text="${text//\/teof\/}"
361
9667049b 362 if $do_mastodon; then
c8c18954 363 if ! toot --$mastodon_account post $toot_quiet_arg "$text" "${toot_args[@]}"; then
9667049b
IK
364 fails+=(toot)
365 fi
c0e2704f 366 fi
9667049b
IK
367 if $do_gnusocial; then
368 # https://gnusocial.net/doc/twitterapi
369 if ! curl -o /dev/null -sS -u "$gs_account:$(cat ~/.gnusocial_login-$gs_account)" \
c8c18954 370 $gs_arg -F "status=$text" https://status.fsf.org/api/statuses/update.xml; then
9667049b
IK
371 fails+=(gnu-social)
372 fi
c0e2704f
IK
373 fi
374 if (( ${#fails[@]} )); then
03c21027 375 printf "%s\n" "$(tput setaf 5 2>/dev/null ||:)${_pdtsh_blocks:0:${COLUMNS:-60}}$(tput sgr0 2>/dev/null||:)"
1bed5f3e 376 echo "FSF ERROR: ${fails[*]} might not have posted. post text: $text" >&2
c0e2704f
IK
377 fi
378}
379
9667049b
IK
380# Usage: toot-setup
381#
382# Only run manually for testing.
383#
384# this expects pdt-pip-setup has been run already, and it doesn't setup
385# errhandle.
386#
387# note: auth info is stored at ~/.config/toot/config.json
f9ace606 388pdt-toot-setup() {
03c21027 389 local -a mastodon_accounts
c0e2704f 390 if [[ $pdttest ]]; then
9667049b 391 mastodon_accounts=(fsftest)
c0e2704f
IK
392 else
393 mastodon_accounts=(fsf endDRM)
394 fi
395
9667049b
IK
396 rm -rf ~/src/toot
397 mkdir -p ~/src/toot
03c21027
IK
398 # subshell for cd
399 (
400 cd ~/src/toot
401 # on t11, got myself into a situation where when doing pip install virtualenv,
402 # it gave an error that /usr/bin/pip didn't exist, so i did
403 # sudo ln -s /home/iank/.local/bin/pip /usr/bin
404 #
405 python3 -m virtualenv -p python3 venv
026344f1 406 # shellcheck disable=SC1091 # not relevant to this script
03c21027
IK
407 source venv/bin/activate
408 # pip freeze after a pip install, as of 2022-11-28
409 cat >requirements.txt <<'EOF'
9667049b
IK
410beautifulsoup4==4.11.1
411certifi==2022.9.24
412charset-normalizer==2.1.1
413idna==2.8
414requests==2.22.0
415soupsieve==2.3.2.post1
416toot==0.29.0
417urllib3==1.25.8
418urwid==2.1.2
419wcwidth==0.2.5
420EOF
421
03c21027
IK
422 # new 2022-11 packages
423 # beautifulsoup4==4.11.1
424 # certifi==2022.9.24
425 # charset-normalizer==2.1.1
426 # idna==3.4
427 # requests==2.28.1
428 # soupsieve==2.3.2.post1
429 # toot==0.29.0
430 # urllib3==1.26.13
431 # urwid==2.1.2
432 # wcwidth==0.2.5
433
434 # old 2019 packages
435 # beautifulsoup4==4.8.2
436 # certifi==2019.11.28
437 # chardet==3.0.4
438 # idna==2.8
439 # requests==2.22.0
440 # soupsieve==1.9.5
441 # toot==0.25.2
442 # urllib3==1.25.8
443 # urwid==2.1.0
444 # wcwidth==0.1.8
445
446 # i mixed in some old packages to get newer toot working on t9.
447
448 python3 -m pip install -r requirements.txt
449 # not needed due to subshell
450 # deactivate
451 )
9667049b
IK
452 for account in ${mastodon_accounts[@]}; do
453 if ! toot activate $account@hostux.social &>/dev/null; then
03c21027 454 printf "%s\n" "$(tput setaf 5 2>/dev/null ||:)${_pdtsh_blocks:0:${COLUMNS:-60}}$(tput sgr0 2>/dev/null||:)"
9667049b
IK
455 echo "Please login to the account named \"$account\" on https://hostux.social in your main browser then press enter."
456 echo "WARNING: if you log into an account other than \"$account\", this won't work"
457 read -r
458 toot login -i hostux.social
459 fi
460 done
461}
462
463# Usage: pdt-pip-setup
464pdt-pip-setup() {
465 if [[ ! -e ~/.local/bin/pip ]]; then
466 tmp=$(mktemp)
467 pyver=$(python3 --version | sed -r 's/.*(3\.[0-9]+).*/\1/')
468 echo fyi: detected pyver: $pyver. this should look something like 3.6
469
470 if dpkg --compare-versions 3.6 ge $pyver; then
471 # The bootstrap script at https://bootstrap.pypa.io/get-pip.py required 3.7+
472 # when i wrote this.
473 wget -O$tmp https://bootstrap.pypa.io/pip/$pyver/get-pip.py
474 else
475 wget -O$tmp https://bootstrap.pypa.io/get-pip.py
476 fi
477 python3 $tmp --user
478 hash -r
479 fi
480 python3 -m pip install --user -U virtualenv
481}
482
f9ace606
IK
483# generally only meant to be called internally from pdt-setup
484pdt-twitter-setup() {
03c21027
IK
485 (
486 cd ~/src/tweepy
487 python3 -m virtualenv -p python3 venv
026344f1 488 # shellcheck disable=SC1091 # not relevant to this script
03c21027
IK
489 source venv/bin/activate
490 python3 -m pip install .
491 )
492}
f9ace606 493
03c21027
IK
494pdt-gnusocial-setup() {
495 for account in dbd fsf; do
496 if [[ ! -s ~/.gnusocial_login-$account ]]; then
497 printf "%s\n" "$(tput setaf 5 2>/dev/null ||:)${_pdtsh_blocks:0:${COLUMNS:-60}}$(tput sgr0 2>/dev/null||:)"
498 read -r -p "please enter the password for $account@status.fsf.org > " pass
499 touch ~/.gnusocial_login-$account
500 chmod 600 ~/.gnusocial_login-$account
501 printf "%s\n" "$pass" > ~/.gnusocial_login-$account
f9ace606
IK
502 fi
503 done
f9ace606
IK
504}
505
9667049b 506# Usage: pdt-setup
03c21027
IK
507#
508# # Twitter requires separate installation of 2 files alongside this file,
509# twitter_keys.py-fsf, # twitter_keys.py-dbd. They look like this:
510#
511# access_token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
512# access_token_secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
513# consumer_key = 'xxxxxxxxxxxxxxxxxxxxxxxxx'
514# consumer_secret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
515#
516# To get them, as of 2023-11-17:
517# * Go to https://developer.twitter.com,
518# * You will need to allow some nonfree javascript scripts to
519# run. Generally, just the ones that come from twitter.come, not
520# things like google analytics. This is a one time sacrifice of software
521# freedom which will enable posting to twitter in freedom indefinitely
522# until some major change at twitter happens, for example, they shut
523# down the api. That could be many years from now.
524# * Click developer portal
525# * A login page appears, enter your twitter credentials.
526# * If you've never gotten an api key before, click sign up for a free account
527# * If you had a developer account from before 2023, you will need to
528# create a project and an app or add an old app to the project.
529#
530# * If this is a new developer account, you have to click setup user
531# authentication settings. Select read/write permissions, automated
532# app or bot, and give it any random url in app info. Ignore the
533# OAuth 2.0 keys.
534#
535# * Click on the app, click keys and tokens,
536#
537# * Under consumer keys, sub-item "api key and secret", click
538# regenerate. copy to vars consumer_key and consumer_secret
539#
540# * Under authentication tokens, sub-item Access Token and Secret,
541# click generate. copy to vars access_token and access_token_secret.
542#
9667049b
IK
543pdt-setup() {
544
c0e2704f 545 mkdir -p ~/src
03c21027 546 for repo in errhandle video-tweet; do
026344f1 547 if [[ -e ~/src/$repo/.git ]]; then
7759d3bf 548 if git -C ~/src/$repo remote -v | grep -E "^origin[[:space:]]+[^[:space:]]*vcs.fsf.org[^[:space:]]+$repo.git" &>/dev/null; then
026344f1
IK
549 git -C ~/src/$repo fetch
550 git -C ~/src/$repo reset --hard origin/master
551 git -C ~/src/$repo clean -xfffd
552 else
553 rm -rf ~/src/$repo
554 git clone https://vcs.fsf.org/git/$repo.git ~/src/$repo
555 fi
c0e2704f 556 else
03c21027 557 git clone https://vcs.fsf.org/git/$repo.git ~/src/$repo
c0e2704f
IK
558 fi
559 done
03c21027 560
03c21027 561 # shellcheck disable=SC1090 # tested separately
f46eda55 562 source ~/src/errhandle/bash-bear
c0e2704f 563
9667049b 564 pdt-pip-setup
c0e2704f 565
f9ace606 566 pdt-toot-setup
c0e2704f 567
03c21027 568 pdt-twitter-setup
c0e2704f 569
03c21027
IK
570 # not using gnusocial by default.
571 # pdt-gnusocial-setup
c0e2704f 572
c0e2704f 573 err-allow
c70ad097 574 echo "pdt-setup complete"
c0e2704f 575}