From 03c21027d1a53bbf7d0a5efffda929e30e31ed11 Mon Sep 17 00:00:00 2001 From: Ian Kelling Date: Sun, 19 Nov 2023 08:25:57 -0500 Subject: [PATCH] update mainly to fix twitter --- .gitignore | 2 + pdt.sh | 330 ++++++++++++++++++++++++++++------------------------- t.py | 133 ++++++++++++--------- 3 files changed, 257 insertions(+), 208 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a54e4eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/twitter_keys* +/__pycache__ diff --git a/pdt.sh b/pdt.sh index d24feea..7e21692 100644 --- a/pdt.sh +++ b/pdt.sh @@ -24,20 +24,23 @@ PATH="$HOME/.local/bin:$PATH" _pdtsh_file="$(readlink -f -- "${BASH_SOURCE[0]}")" _pdtsh_dir="${_pdtsh_file%/*}" _pdtsh_tweet="$_pdtsh_dir/t.py" +_pdtsh_blocks=██████████████████████████████████████████████████████████████ # usage: tweet [-PROFILE_NAME] [POST...] # Uses variables as input: # $media for image file path. # $alt_text for image alt text. tweet() { - local oath - oath=$HOME/.rainbow_oauth + local keys_file + keys_file=$_pdtsh_dir/twitter_keys.py if [[ $1 == -* ]]; then - rm -f $oath - ln -s $oath$1 $oath + rm -f $keys_file + ln -s $keys_file$1 $keys_file shift fi - source ~/src/twitter/venv/bin/activate + + # shellcheck disable=SC1090 # not relevant to this script + source ~/src/tweepy/venv/bin/activate { printf "%s\n" "$*" if [[ $media ]]; then @@ -46,44 +49,27 @@ tweet() { printf "%s\n" "$alt_text" fi fi - } | $_pdtsh_tweet + } | $_pdtsh_tweet || { deactivate; return 1; } deactivate } -# usage: rbow [-PROFILE_NAME] [COMMAND] +# usage: tweetrm [-PROFILE_NAME] POST_ID # -# Wrapper for rainbowstream to use multiple logins and tweet directly -# from the command line. PROFILE_NAME can be anything you want. -rbow() { - local oath f - oath=$HOME/.rainbow_oauth +# Delete twitter post. Post id is the number at end of a url like: +# https://nitter.net/user/status/1725640623149969527 +# +tweetrm() { + local keys_file + keys_file=$_pdtsh_dir/twitter_keys.py if [[ $1 == -* ]]; then - rm -f $oath - ln -s $oath$1 $oath + rm -f $keys_file + ln -s $keys_file$1 $keys_file shift fi - source ~/src/rainbowstream/venv/bin/activate - if (($#)); then - f=$(mktemp) - cat >$f <<'EOF' -# adds a short delay after each send for more reliable operation -set force_conservative 1 -spawn "rainbowstream" -# wait for prompt -expect -nocase timeout {exit 1} "@*]: " -set cmd [lindex $argv 0]; -send "$cmd\r\r" -expect -nocase timeout {exit 1} "@*]: " -# this may not be needed. didnt test -sleep 2 -send "q\r" -interact -EOF - expect $f "$*" || { echo error: expect failed; deactivate; rm -f $f; return 1; } - rm -f $f - else - rainbowstream - fi + + # shellcheck disable=SC1090 # not relevant to this script + source ~/src/tweepy/venv/bin/activate + $_pdtsh_tweet $1 || { deactivate; return 1; } deactivate } @@ -91,6 +77,7 @@ EOF # usage: toot [-PROFILE_NAME] [TOOT_ARGS] toot() { local mast_profile + # shellcheck disable=SC1090 # not relevant to this script source ~/src/toot/venv/bin/activate if [[ $1 == -* ]]; then mast_profile=${1#-} @@ -102,17 +89,32 @@ toot() { } -# post to mastodon + twitter + gnu social post +# post to mastodon + twitter. # -# If posting video, it only goes to twitter. Other accounts can be -# posted to manually with free software. # -# Alt text only goes to mastodon. +# Video posting to twitter is almost certainly broken. +# +# Gnu Social posting code exists, but we aren't using it. You would need +# to run pdt-gnusocial-setup beforehand. Alt text does not work on +# gnusocial. +# +# usage: +# pdt [-s mastodon|twitter|gnusocial] [--dbd] [-m IMAGE_FILE] [-a ALT_TEXT] [POST] +# pdt [-v VIDEO_PATH] [POST] +# pdt [-d POST_ID] # -# usage: pdt [-s mastodon|twitter|gnusocial] [--dbd] [-m IMAGE_FILE] [-a ALT_TEXT] [-v VIDEO_PATH] [POST] # -s mastodon|twitter|gnusocial = post to a single social network. # -m IMAGE_FILE = Uploads IMAGE_FILE. if MEDIA_FILE.txt exists, the last line of that file will be used as ALT_TEXT # unless -a has been used. +# +# POST can have have some special markup for twitter to deal with the 280 character limit: +# /tnt/ short for "twitter next tweet", The /tnt/ will be removed, and the tweet split into 2 at that point. +# /teof/ means twitter end of file. It will be removed, and text after it won't be posted to twitter. +# +# If you have some pdt arguments in a file, FILE, and you want to test if they are under 280 chars, you can run: +# +# sed -r 's,/teof/.*,,;s/-(m|a|-dbd) [^ ]*//;s,https?://[^ ]*,https://xxxxxxxxxxxxxxx,g' FILE | awk '$0 !~ /\/tnt\// && length > 280 {print length, $0}' +# pdt() { local video media twitter_account gs_account mastodon_account video gs_arg network local do_mastodon do_twitter do_gnusocial @@ -128,9 +130,8 @@ pdt() { fi video=false do_mastodon=true - # no twitter by default. - do_twitter=false - do_gnusocial=true + do_twitter=true + do_gnusocial=false alt_text= while [[ $1 == -* ]]; do case $1 in @@ -215,10 +216,18 @@ pdt() { fi fails=() if $do_twitter; then - if ! tweet -$twitter_account "$*"; then + if ! tweet -$twitter_account "${*%/teof/*}"; then fails+=(tweet) fi fi + + # remove /tnt/ for non-twitter social media. + text="$*" + text=${text// \/tnt\/ / } + text="${text//\/tnt\/}" + text="${text//\/teof\/}" + + if $do_mastodon; then if ! toot -$mastodon_account post "$*" "${toot_args[@]}"; then fails+=(toot) @@ -232,7 +241,7 @@ pdt() { fi fi if (( ${#fails[@]} )); then - printf "$(tput setaf 5)█$(tput sgr0)%.0s" $(eval echo "{1..${COLUMNS:-60}}"); echo + printf "%s\n" "$(tput setaf 5 2>/dev/null ||:)${_pdtsh_blocks:0:${COLUMNS:-60}}$(tput sgr0 2>/dev/null||:)" echo "FSF ERROR: ${fails[*]} might not have posted" >&2 fi } @@ -246,26 +255,27 @@ pdt() { # # note: auth info is stored at ~/.config/toot/config.json pdt-toot-setup() { - local -a twitter_accounts mastodon_accounts + local -a mastodon_accounts if [[ $pdttest ]]; then - twitter_accounts=(iank) mastodon_accounts=(fsftest) else - twitter_accounts=(fsf dbd) mastodon_accounts=(fsf endDRM) fi rm -rf ~/src/toot mkdir -p ~/src/toot - cd ~/src/toot - # on t11, got myself into a situation where when doing pip install virtualenv, - # it gave an error that /usr/bin/pip didn't exist, so i did - # sudo ln -s /home/iank/.local/bin/pip /usr/bin - # - python3 -m virtualenv -p python3 venv - source venv/bin/activate - # pip freeze after a pip install, as of 2022-11-28 - cat >requirements.txt <<'EOF' + # subshell for cd + ( + cd ~/src/toot + # on t11, got myself into a situation where when doing pip install virtualenv, + # it gave an error that /usr/bin/pip didn't exist, so i did + # sudo ln -s /home/iank/.local/bin/pip /usr/bin + # + python3 -m virtualenv -p python3 venv + # shellcheck disable=SC1090 # not relevant to this script + source venv/bin/activate + # pip freeze after a pip install, as of 2022-11-28 + cat >requirements.txt <<'EOF' beautifulsoup4==4.11.1 certifi==2022.9.24 charset-normalizer==2.1.1 @@ -278,38 +288,39 @@ urwid==2.1.2 wcwidth==0.2.5 EOF - # new 2022-11 packages - # beautifulsoup4==4.11.1 - # certifi==2022.9.24 - # charset-normalizer==2.1.1 - # idna==3.4 - # requests==2.28.1 - # soupsieve==2.3.2.post1 - # toot==0.29.0 - # urllib3==1.26.13 - # urwid==2.1.2 - # wcwidth==0.2.5 - - # old 2019 packages - # beautifulsoup4==4.8.2 - # certifi==2019.11.28 - # chardet==3.0.4 - # idna==2.8 - # requests==2.22.0 - # soupsieve==1.9.5 - # toot==0.25.2 - # urllib3==1.25.8 - # urwid==2.1.0 - # wcwidth==0.1.8 - - # i mixed in some old packages to get newer toot working on t9. - - python3 -m pip install -r requirements.txt - cd - - deactivate + # new 2022-11 packages + # beautifulsoup4==4.11.1 + # certifi==2022.9.24 + # charset-normalizer==2.1.1 + # idna==3.4 + # requests==2.28.1 + # soupsieve==2.3.2.post1 + # toot==0.29.0 + # urllib3==1.26.13 + # urwid==2.1.2 + # wcwidth==0.2.5 + + # old 2019 packages + # beautifulsoup4==4.8.2 + # certifi==2019.11.28 + # chardet==3.0.4 + # idna==2.8 + # requests==2.22.0 + # soupsieve==1.9.5 + # toot==0.25.2 + # urllib3==1.25.8 + # urwid==2.1.0 + # wcwidth==0.1.8 + + # i mixed in some old packages to get newer toot working on t9. + + python3 -m pip install -r requirements.txt + # not needed due to subshell + # deactivate + ) for account in ${mastodon_accounts[@]}; do if ! toot activate $account@hostux.social &>/dev/null; then - printf "$(tput setaf 5)█$(tput sgr0)%.0s" $(eval echo "{1..${COLUMNS:-60}}"); + printf "%s\n" "$(tput setaf 5 2>/dev/null ||:)${_pdtsh_blocks:0:${COLUMNS:-60}}$(tput sgr0 2>/dev/null||:)" echo "Please login to the account named \"$account\" on https://hostux.social in your main browser then press enter." echo "WARNING: if you log into an account other than \"$account\", this won't work" read -r @@ -340,91 +351,102 @@ pdt-pip-setup() { # generally only meant to be called internally from pdt-setup pdt-twitter-setup() { - # twitter setup - mkdir -p ~/src/twitter - cd ~/src/twitter - python3 -m virtualenv -p python3 venv - source venv/bin/activate - # as of 2022-11-30 - cat >requirements.txt <<'EOF' -certifi==2022.9.24 -twitter==1.19.6 -EOF - python3 -m pip install -r requirements.txt - deactivate - cd - - - # note: we could ditch rainbowstream altogether and just pass around - # oath api key string in a password file or something if we learned - # how to get it from the new twitter client im using. I'm just keeping - # this because it works and I like the format of how it stores its - # auth secrets. - cd ~/src/rainbowstream - python3 -m virtualenv -p python3 venv - source venv/bin/activate - python3 -m pip install -r requirements.txt - python3 -m pip install -e . - deactivate + ( + cd ~/src/tweepy + python3 -m virtualenv -p python3 venv + # shellcheck disable=SC1090 # not relevant to this script + source venv/bin/activate + python3 -m pip install . + ) +} - for account in ${twitter_accounts[@]}; do - if [[ ! -s ~/.rainbow_oauth-$account ]]; then - printf "$(tput setaf 5)█$(tput sgr0)%.0s" $(eval echo "{1..${COLUMNS:-60}}"); - echo "Please login to the account named \"$account\" on twitter in your main browser then press enter. After rainbowstream prompt loads, quit by typing q then enter" - read -r - rbow -$account - fi - done - for account in ${twitter_accounts[@]}; do - if [[ ! -s $HOME/.rainbow_oauth-$account ]]; then - echo "pdt-setup error: expected non-empty file at $HOME/.rainbow_oauth-$account by this point. try reruning pdt-setup and logging in to the correct twitter account in browser or contact ian" - return 1 +pdt-gnusocial-setup() { + for account in dbd fsf; do + if [[ ! -s ~/.gnusocial_login-$account ]]; then + printf "%s\n" "$(tput setaf 5 2>/dev/null ||:)${_pdtsh_blocks:0:${COLUMNS:-60}}$(tput sgr0 2>/dev/null||:)" + read -r -p "please enter the password for $account@status.fsf.org > " pass + touch ~/.gnusocial_login-$account + chmod 600 ~/.gnusocial_login-$account + printf "%s\n" "$pass" > ~/.gnusocial_login-$account fi done - if [[ ! $pdttest ]] && diff -q $HOME/.rainbow_oauth-fsf $HOME/.rainbow_oauth-dbd &>/dev/null; then - echo "pdt-setup error: error, $HOME/.rainbow_oauth-fsf $HOME/.rainbow_oauth-dbd are the same. Did you follow the instructions closely and log into fsf and then dbd when prompted? try reruning pdt-setup and doing that" - return 1 - fi - cd - } - # Usage: pdt-setup +# +# # Twitter requires separate installation of 2 files alongside this file, +# twitter_keys.py-fsf, # twitter_keys.py-dbd. They look like this: +# +# access_token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +# access_token_secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +# consumer_key = 'xxxxxxxxxxxxxxxxxxxxxxxxx' +# consumer_secret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' +# +# To get them, as of 2023-11-17: +# * Go to https://developer.twitter.com, +# * You will need to allow some nonfree javascript scripts to +# run. Generally, just the ones that come from twitter.come, not +# things like google analytics. This is a one time sacrifice of software +# freedom which will enable posting to twitter in freedom indefinitely +# until some major change at twitter happens, for example, they shut +# down the api. That could be many years from now. +# * Click developer portal +# * A login page appears, enter your twitter credentials. +# * If you've never gotten an api key before, click sign up for a free account +# * If you had a developer account from before 2023, you will need to +# create a project and an app or add an old app to the project. +# +# * If this is a new developer account, you have to click setup user +# authentication settings. Select read/write permissions, automated +# app or bot, and give it any random url in app info. Ignore the +# OAuth 2.0 keys. +# +# * Click on the app, click keys and tokens, +# +# * Under consumer keys, sub-item "api key and secret", click +# regenerate. copy to vars consumer_key and consumer_secret +# +# * Under authentication tokens, sub-item Access Token and Secret, +# click generate. copy to vars access_token and access_token_secret. +# pdt-setup() { - start_dir="$PWD" mkdir -p ~/src - cd ~/src - for repo in errhandle rainbowstream video-tweet; do - if [[ -e $repo ]]; then - cd $repo - git fetch - git reset --hard origin/master - git clean -xfffd - cd .. + for repo in errhandle video-tweet; do + if [[ -e ~/src/$repo ]]; then + ( + cd ~/src/$repo + git fetch + git reset --hard origin/master + git clean -xfffd + ) else - git clone https://vcs.fsf.org/git/$repo.git + git clone https://vcs.fsf.org/git/$repo.git ~/src/$repo fi done - cd "$start_dir" + + if [[ -e ~/src/tweepy ]]; then + ( + cd ~/src/tweepy + git fetch + git reset --hard f32d12dbddbd877470446657812a10a04292d0c9 + git clean -xfffd + ) + else + git clone https://github.com/tweepy/tweepy.git ~/src/tweepy + fi + + # shellcheck disable=SC1090 # tested separately source ~/src/errhandle/err - cp ~/src/rainbowstream/rainbowstream/colorset/config ~/.rainbow_config.json pdt-pip-setup pdt-toot-setup - # pdt-twitter-setup + pdt-twitter-setup - - for account in dbd fsf; do - if [[ ! -s ~/.gnusocial_login-$account ]]; then - printf "$(tput setaf 5)█$(tput sgr0)%.0s" $(eval echo "{1..${COLUMNS:-60}}"); - read -r -p "please enter the password for $account@status.fsf.org > " pass - touch ~/.gnusocial_login-$account - chmod 600 ~/.gnusocial_login-$account - printf "%s\n" "$pass" > ~/.gnusocial_login-$account - fi - done + # not using gnusocial by default. + # pdt-gnusocial-setup err-allow echo "pdt-setup complete" diff --git a/t.py b/t.py index de8b9e8..9148987 100755 --- a/t.py +++ b/t.py @@ -1,59 +1,84 @@ #!/usr/bin/env python3 +# pdt: post to FSF social media via command line +# Copyright (C) 2021 Ian Kelling +# SPDX-License-Identifier: AGPL-3.0-or-later -# based on -# https://github.com/python-twitter-tools/twitter +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# Usage: see pdt.sh example. import sys -import os -from twitter import * -import fileinput - -# if needed for development, just hardcode -#oauth_token = "" -#oauth_secret = "" - - -first = True -with fileinput.input(files=(os.environ.get('HOME') + os.sep + '.rainbow_oauth')) as f: - # ok, this is dumb, but it works - for line in f: - if first: - oauth_token = line.rstrip() - first = False - else: - oauth_secret = line.rstrip() - -CONSUMER_KEY = 'hJHyPhuU7nSVHrKTVsGVDM4Lw' -CONSUMER_SECRET = '8UCYePqc1y9DY6mg0yQzLoTuq57AIysype2Si63714uACGMCbO' - -t = Twitter(auth=OAuth(oauth_token, oauth_secret, CONSUMER_KEY, CONSUMER_SECRET)) - -tweet_text = input() -have_image = False -try: - image_path = input() - have_image = True -except: - pass - -have_alt = False -try: - alt = input() - have_alt = True -except: - pass - -if have_image: - with open(image_path, "rb") as imagefile: - imagedata = imagefile.read() - t_upload = Twitter(domain='upload.twitter.com', - auth=OAuth(oauth_token, oauth_secret, CONSUMER_KEY, CONSUMER_SECRET)) - id_img = t_upload.media.upload(media=imagedata)["media_id_string"] - -if have_alt: - t_upload.media.metadata.create(media_id=id_img, text=alt) - -if have_image: - t.statuses.update(status=tweet_text, media_ids=id_img) +import tweepy +import time +from twitter_keys import * + + +auth = tweepy.OAuth1UserHandler( + consumer_key, consumer_secret, access_token, access_token_secret +) + +api = tweepy.API(auth) + +client = tweepy.Client( + consumer_key=consumer_key, consumer_secret=consumer_secret, + access_token=access_token, access_token_secret=access_token_secret +) + +if len(sys.argv) == 2: + response = client.delete_tweet(sys.argv[1]) else: - t.statuses.update(status=tweet_text) + input_text = input() + + posts=input_text.split("/tnt/") + post_count=len(posts) + + tweet_text=posts[0].strip() + + if post_count > 1: + tweet_text=tweet_text + f" 1/{post_count}" + + have_image = False + try: + image_path = input() + have_image = True + except: + pass + + have_alt = False + try: + alt = input() + have_alt = True + except: + pass + if have_image: + media = api.media_upload(image_path) + media_id = media.media_id_string + + if have_alt: + api.create_media_metadata(media_id, alt) + + + if have_image: + response = client.create_tweet(text=tweet_text, media_ids=[media_id]) + else: + #print(tweet_text) + response = client.create_tweet(text=tweet_text) + print(f"https://nitter.net/user/status/{response.data['id']}") + if post_count > 1: + for x in range(1, post_count): + time.sleep(1) + tweet_text=posts[x].strip() + f" {x+1}/{post_count}" + response = client.create_tweet(text=tweet_text, in_reply_to_tweet_id=response.data['id']) + print(f"{x+1}/{post_count} https://nitter.net/user/status/{response.data['id']}") + #print(tweet_text) -- 2.25.1