Conflicts:
	forthlift.py
This commit is contained in:
Johann Dreo 2016-05-17 18:43:15 +02:00
commit c3ed359cf2
3 changed files with 236 additions and 101 deletions

View file

@ -1,22 +1,24 @@
forthlift — post sequences of texts on social media forthlift — post sequences of texts on social media
=================================================== ===================================================
NOTE: FORTHLIFT IS IN AN EARLY PRE-ALPHA STAGE.
Forthlift is a command line application to post sequences of text lines on social media. Forthlift is a command line application to post sequences of text lines on social media.
It is designed to ease automation and integration in existing tools.
Its main use case is to post on Twitter several status that you have prepared first in your text editor.
It is thus designed to ease automation and integration in existing tools
and had an Unix-like text-and-pipes command line interface.
For example, it makes it easy to post "chained" twitter status (that answer to each others) For example, it makes it easy to post "chained" twitter status (that answer to each others)
from your text editor. Just send the selected text to its standard input, selecting the from your text editor. Just send the selected text to its standard input, selecting the
twitter API, and voilà. Using Vim, you can tweet the selected lines with: twitter API, and voilà. Using Vim, you can tweet the visually-selected lines with:
`:'<,'>w !forthlift -a twitter --chain` `:'<,'>w !forthlift -a twitter`
## SYNOPSIS ## SYNOPSIS
`forthlift` [-h] `forthlift` [-h]
`forthlift` [-a {stdout,twitter}] [-m MAXLEN] [-i] [-t] [-d] [-q] [-c] `forthlift` [-a {stdout,twitter}] [-m MAXLEN] [-i|-t] [-c] [-q] [-d] [-s]
## DESCRIPTION ## DESCRIPTION
@ -40,26 +42,61 @@ Depending on the chosen API, this could means different things:
as a sequence of *answers* and not as a list of independent tweets. as a sequence of *answers* and not as a list of independent tweets.
* for the "stdout" API, this means that each printed line will start * for the "stdout" API, this means that each printed line will start
with as many spaces as its index in the input list. with its index in the input list.
While it is recommended to prepare the input text with other text-processing tools While it is recommended to prepare the input text with other text-processing tools
(fold, fmt, tr, sed, grep, your text editor, etc.), (fold, fmt, tr, sed, grep, your text editor, etc.),
forthlift comes with some rough text-processing capabilities, among which: forthlift comes with some rough text-processing capabilities, among which:
* ignore or trim lines that are longer than a given size, * ignore or trim lines that are longer than a given size (see `--trim` and `--ignore`),
* add a counter of the form `<current index>/<total lines>` at the end of the lines. * add a counter of the form `<current index>/<total lines>` at the end of the lines (see `--counter`).
## OPTIONS ## OPTIONS
TODO * -h, --help: show this help message and exit
* -a {stdout,twitter}, --api {stdout,twitter}: Name of the API to use.
(default: stdout)
* -m MAXLEN, --max-len MAXLEN: Maximum number of characters in the lines.
(default: 140)
* -i, --ignore: Ignore lines that are longer than MAXLEN (default: False)
* -t, --trim: Trim down lines that are longer than MAXLEN. (default: False)
* -c, --counter: Add a counter of the form " x/N" at the end of the lines,
with N being the number of lines read and x the current index of the line.
NOTE: this necessitate to read all the input before processing it.
(default: False)
* -q, --quiet: Do not print errors and warnings on the standard error output.
(default: False)
* -s, --setup: Setup the selected API (e.g. authenticate and get authorization to post).
(default: False)
* -d, --independent: Post each item independently from the previous one
The behaviour of this option depends on the selected API.
For example on Twitter: do not post a line as an answer to the previous one but as a new tweet.
(default: False)
* --twitter-images FILENAME(S) [FILENAME(S) ...]:
Upload each given image files along with the corresponding tweets in the sequence.
If there are more images than tweets, they are silently ignored.
(default: None)
## INSTALLATION ## INSTALLATION
### Twitter ### Twitter
Copy `twitter.conf-dist` as `twitter.conf` and indicate your developer's API keys and tokens. 1) Copy `twitter.conf-dist` as `twitter.conf` and indicate your developer's API keys in the corresponding fields.
2) Run `forthlift --api twitter --setup` and follow the instructions (go to the given URL, then paste the given PIN).
Why should you get developer's API keys? Because Twitter does not like open-source desktop applications, see:
http://arstechnica.com/security/2010/09/twitter-a-case-study-on-how-to-do-oauth-wrong/

View file

@ -3,10 +3,17 @@
import sys import sys
import locale import locale
import argparse import argparse
import datetime
import ConfigParser
import tweepy import tweepy
class AppKeyError(Exception):
def __init__(self, msg):
self.msg = msg
def write(data, stream = sys.stdout): def write(data, stream = sys.stdout):
""" """
Write "data" on the given stream, then flush, silently handling broken pipes. Write "data" on the given stream, then flush, silently handling broken pipes.
@ -38,24 +45,24 @@ def readline( stream_in ):
raise StopIteration raise StopIteration
def setup_adorn( data, asked ): def setup_counter( data, asked ):
assert(asked.adorn) assert(asked.counter)
# unroll everything # unroll everything
data = list(data) data = list(data)
nb = len(data) nb = len(data)
# " int/int" # " int/int"
adorn_size = len(str(nb))*2 + 1 + 1 counter_size = len(str(nb))*2 + 1 + 1
for i,line in enumerate(data): for i,line in enumerate(data):
adorn = " %i/%i" % (i+1,nb) counter = " %i/%i" % (i+1,nb)
curmax = asked.max_len - len(adorn) curmax = asked.max_len - len(counter)
if len(line) > curmax: if len(line) > curmax:
if asked.ignore: if asked.ignore:
data[i] = line data[i] = line
elif asked.trim: elif asked.trim:
data[i] = line[:curmax] data[i] = line[:curmax]
data[i] = data[i] + adorn data[i] = data[i] + counter
return data return data
@ -72,8 +79,8 @@ def setup_hem(data, asked):
def setup( data, asked ): def setup( data, asked ):
if asked.adorn: if asked.counter:
f = setup_adorn f = setup_counter
else: else:
f = setup_hem f = setup_hem
@ -81,21 +88,33 @@ def setup( data, asked ):
yield line yield line
def operate( func, *args ):
for line in func( readline(sys.stdin), *args ):
write(line)
#
# STDOUT API
#
def on_stdout( data, asked, endline="\n" ): def on_stdout( data, asked, endline="\n" ):
lines = setup(data, asked) lines = setup(data, asked)
# You can do something on the whole set of lines if you want. # You can do something on the whole set of lines if you want.
if asked.chain:
prefix = " "
else:
prefix = ""
for i,line in enumerate(lines): for i,line in enumerate(lines):
if endline: if endline:
if line[-1] != endline: if line[-1] != endline:
line += endline line += endline
yield prefix*i + line
if asked.independent:
yield line
else:
yield i + " " + line
#
# TWITTER API
#
def on_twitter( data, api, asked, endline="\n" ): def on_twitter( data, api, asked, endline="\n" ):
lines = setup(data, asked) lines = setup(data, asked)
@ -118,114 +137,195 @@ def on_twitter( data, api, asked, endline="\n" ):
# API.update_status(status[, in_reply_to_status_id][, lat][, long][, source][, place_id]) # API.update_status(status[, in_reply_to_status_id][, lat][, long][, source][, place_id])
status = api.update_status(line, prev_status_id) status = api.update_status(line, prev_status_id)
if asked.chain: if asked.independent:
prev_status_id = None
else:
prev_status_id = status.id prev_status_id = status.id
yield status.text + endline yield status.text + endline
def operate( func, *args ): def setup_twitter(configfile="twitter.conf"):
for line in func( readline(sys.stdin), *args ):
write(line)
usage = "Post text read on the standard input to a website or an API."
apis =["stdout", "twitter"] # TODO "http_post", "http_get",
parser = argparse.ArgumentParser( description=usage,
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-a", "--api", choices=apis, default="stdout",
help="Name of the API to use.")
# Generic options
parser.add_argument("-m", "--max-len", metavar="MAXLEN", type=int, default=140,
help="Maximum number of characters in the lines.")
parser.add_argument("-i", "--ignore", action="store_true",
help="Ignore lines that are longer than MAXLEN")
parser.add_argument("-t", "--trim", action="store_true",
help="Trim down lines that are longer than MAXLEN.")
parser.add_argument("-d", "--adorn", action="store_true",
help="Add a counter of the form \" x/N\" at the end of the lines, \
with N being the number of lines read and x the current index of the line. \
NOTE: this necessitate to read all the input before processing it.")
parser.add_argument("-q", "--quiet", action="store_true",
help="Do not print errors and warnings on the standard error output.")
# TODO option: rate at which to post lines
# API-dependant options
parser.add_argument("-c", "--chain", action="store_true",
help="Chained actions. Whatever that means depends on the chosen API.")
# Twitter
parser.add_argument("--twitter-images", metavar="FILENAME(S)", nargs="+", type=str,
help="Upload each given image files along with the corresponding tweets in sequence. If there are more images than tweets, they are silently ignored.")
asked = parser.parse_args()
# Consistency checks
if asked.ignore and asked.trim:
if not asked.quiet:
sys.stderr.write("WARNING: asking to trim AND to ignore is not logical, I will ignore.")
assert( not (asked.ignore and asked.trim) )
if asked.twitter_images:
if not asked.api == "twitter":
if not asked.quiet:
sys.stderr.write("WARNING: asking to upload images on twitter while not using the twitter API is not logical, I will ignore.")
assert( not (asked.twitter_images and not asked.api=="twitter") )
else: # Test readable images
cannot=[]
for img in asked.twitter_images:
try:
with open(img) as f:
f.read()
except OSError:
cannot.append(img)
if cannot:
print("Cannot open the following image files, I will not continue: ")
for img in cannot:
print(img)
sys.exit(5) # I/O Error
# APIs
if asked.api == "stdout":
operate( on_stdout, asked )
elif asked.api == "twitter":
import ConfigParser
config = ConfigParser.RawConfigParser() config = ConfigParser.RawConfigParser()
config.read('twitter.conf')
consumer_key = config.get("Auth","key") # Authenticate the application.
consumer_secret = config.get("Auth","key_secret") config.read(configfile)
try: if not config.has_section("App"):
verifier_code = config.get("Auth","code") raise AppKeyError("ERROR: did not found application keys, ask your distribution maintainer or get keys from Twitter first.")
except:
access_token = config.get("Auth","token")
access_token_secret = config.get("Auth","token_secret")
auth = tweepy.OAuthHandler(consumer_key, consumer_secret, "https://api.twitter.com/1.1/") app_key = config.get("App","app_key")
auth.set_access_token(access_token, access_token_secret) app_secret = config.get("App","app_key_secret")
auth = tweepy.OAuthHandler(app_key, app_secret)
# Authenticate the user.
auth_url = auth.get_authorization_url()
print "Copy and paste this URL in your browser while your are logged in the twitter account where you want to post."
print "Authorization URL: " + auth_url
print "Then paste the Personal Identification Number given by Twitter:"
verifier = raw_input('PIN: ').strip()
auth.get_access_token(verifier)
# print 'ACCESS_KEY = "%s"' % auth.access_token.key
# print 'ACCESS_SECRET = "%s"' % auth.access_token.secret
# Authenticate and get the user name.
auth.set_access_token(auth.access_token.key, auth.access_token.secret)
api = tweepy.API(auth) api = tweepy.API(auth)
username = api.me().name
print "Authentication successful, ready to post to account: " + username
operate( on_twitter, api, asked )
# Save authentication tokens.
if not config.has_section("Info"):
config.add_section('Info')
config.set('Info','account', username)
config.set('Info','auth_date', datetime.datetime.utcnow().isoformat())
if not config.has_section("Auth"):
config.add_section('Auth')
config.set('Auth', 'local_token', auth.access_token.key)
config.set('Auth', 'local_token_secret', auth.access_token.secret)
# Writing our configuration file to 'example.cfg'
with open(configfile, 'wb') as fd:
config.write(fd)
#
# CLI
#
if __name__=="__main__":
errors = {"NO_ERROR":0, "UNKNOWN_ERROR":1, "NO_SETUP_NEEDED":2, "NO_APP_KEY":10}
usage = "Post text read on the standard input to a website or an API."
apis =["stdout", "twitter"] # TODO "http_post", "http_get",
parser = argparse.ArgumentParser( description=usage,
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-a", "--api", choices=apis, default="stdout",
help="Name of the API to use.")
# Generic options
parser.add_argument("-m", "--max-len", metavar="MAXLEN", type=int, default=140,
help="Maximum number of characters in the lines.")
parser.add_argument("-i", "--ignore", action="store_true",
help="Ignore lines that are longer than MAXLEN")
parser.add_argument("-t", "--trim", action="store_true",
help="Trim down lines that are longer than MAXLEN.")
parser.add_argument("-c", "--counter", action="store_true",
help="Add a counter of the form \" x/N\" at the end of the lines, \
with N being the number of lines read and x the current index of the line. \
NOTE: this necessitate to read all the input before processing it.")
parser.add_argument("-q", "--quiet", action="store_true",
help="Do not print errors and warnings on the standard error output.")
# TODO option: rate at which to post lines
# API-dependent options
parser.add_argument("-s", "--setup", action="store_true",
help="Setup the selected API (e.g. authenticate and get authorization to post).")
parser.add_argument("-d", "--independent", action="store_true",
help="Post each item independently from the previous one. \
The behaviour of this option depends on the selected API. \
For example on Twitter: do not post a line as an answer to the previous one but as a new tweet.")
# Twitter
parser.add_argument("--twitter-images", metavar="FILENAME(S)", nargs="+", type=str,
help="Upload each given image files along with the corresponding tweets in the sequence. If there are more images than tweets, they are silently ignored.")
asked = parser.parse_args()
# Setup
if asked.setup:
configfile = asked.api+".conf"
if asked.api == "twitter":
try:
setup_twitter(configfile)
except AppKeyError as e:
if not asked.quiet:
sys.stderr.write(e.msg)
sys.exit(errors["NO_APP_KEY"])
except:
print "Unexpected error:", sys.exc_info()[0]
raise
else:
sys.exit(errors["NO_ERROR"])
else: # other API
if not asked.quiet:
sys.stderr.write("This API does not need setup.")
sys.exit(errors["NO_SETUP_NEEDED"])
# Consistency checks
if asked.ignore and asked.trim:
if not asked.quiet:
sys.stderr.write("WARNING: asking to trim AND to ignore is not logical, I will ignore.")
assert( not (asked.ignore and asked.trim) )
if asked.twitter_images:
if not asked.api == "twitter":
if not asked.quiet:
sys.stderr.write("WARNING: asking to upload images on twitter while not using the twitter API is not logical, I will ignore.")
assert( not (asked.twitter_images and not asked.api=="twitter") )
else: # Test readable images
cannot=[]
for img in asked.twitter_images:
try:
with open(img) as f:
f.read()
except OSError:
cannot.append(img)
if cannot:
print("Cannot open the following image files, I will not continue: ")
for img in cannot:
print(img)
sys.exit(5) # I/O Error
# APIs
if asked.api == "stdout":
operate( on_stdout, asked )
elif asked.api == "twitter":
# Authenticate
config = ConfigParser.RawConfigParser()
config.read('twitter.conf')
app_key = config.get("App","app_key")
app_secret = config.get("App","app_key_secret")
try:
verifier_code = config.get("Auth","code")
except:
access_token = config.get("Auth","local_token")
access_token_secret = config.get("Auth","local_token_secret")
auth = tweepy.OAuthHandler(app_key, app_secret, "https://api.twitter.com/1.1/")
auth.set_access_token(access_token, access_token_secret)
api = tweepy.API(auth)
# Post
operate( on_twitter, api, asked )

View file

@ -1,5 +1,3 @@
[Auth] [App]
key= app_key=
key_secret= app_key_secret=
token=
token_secret=