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
===================================================
NOTE: FORTHLIFT IS IN AN EARLY PRE-ALPHA STAGE.
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)
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:
`:'<,'>w !forthlift -a twitter --chain`
twitter API, and voilà. Using Vim, you can tweet the visually-selected lines with:
`:'<,'>w !forthlift -a twitter`
## SYNOPSIS
`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
@ -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.
* 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
(fold, fmt, tr, sed, grep, your text editor, etc.),
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
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
### 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 locale
import argparse
import datetime
import ConfigParser
import tweepy
class AppKeyError(Exception):
def __init__(self, msg):
self.msg = msg
def write(data, stream = sys.stdout):
"""
Write "data" on the given stream, then flush, silently handling broken pipes.
@ -38,24 +45,24 @@ def readline( stream_in ):
raise StopIteration
def setup_adorn( data, asked ):
assert(asked.adorn)
def setup_counter( data, asked ):
assert(asked.counter)
# unroll everything
data = list(data)
nb = len(data)
# " int/int"
adorn_size = len(str(nb))*2 + 1 + 1
counter_size = len(str(nb))*2 + 1 + 1
for i,line in enumerate(data):
adorn = " %i/%i" % (i+1,nb)
curmax = asked.max_len - len(adorn)
counter = " %i/%i" % (i+1,nb)
curmax = asked.max_len - len(counter)
if len(line) > curmax:
if asked.ignore:
data[i] = line
elif asked.trim:
data[i] = line[:curmax]
data[i] = data[i] + adorn
data[i] = data[i] + counter
return data
@ -72,8 +79,8 @@ def setup_hem(data, asked):
def setup( data, asked ):
if asked.adorn:
f = setup_adorn
if asked.counter:
f = setup_counter
else:
f = setup_hem
@ -81,21 +88,33 @@ def setup( data, asked ):
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" ):
lines = setup(data, asked)
# 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):
if endline:
if line[-1] != 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" ):
lines = setup(data, asked)
@ -118,17 +137,71 @@ def on_twitter( data, api, asked, endline="\n" ):
# API.update_status(status[, in_reply_to_status_id][, lat][, long][, source][, place_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
yield status.text + endline
def operate( func, *args ):
for line in func( readline(sys.stdin), *args ):
write(line)
def setup_twitter(configfile="twitter.conf"):
config = ConfigParser.RawConfigParser()
# Authenticate the application.
config.read(configfile)
if not config.has_section("App"):
raise AppKeyError("ERROR: did not found application keys, ask your distribution maintainer or get keys from Twitter first.")
app_key = config.get("App","app_key")
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)
username = api.me().name
print "Authentication successful, ready to post to account: " + username
# 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",
@ -150,7 +223,7 @@ parser.add_argument("-i", "--ignore", action="store_true",
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",
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.")
@ -160,17 +233,45 @@ parser.add_argument("-q", "--quiet", action="store_true",
# TODO option: rate at which to post lines
# API-dependant options
# API-dependent options
parser.add_argument("-c", "--chain", action="store_true",
help="Chained actions. Whatever that means depends on the chosen API.")
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 sequence. If there are more images than tweets, they are silently ignored.")
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:
@ -198,7 +299,6 @@ if asked.twitter_images:
print(img)
sys.exit(5) # I/O Error
# APIs
if asked.api == "stdout":
@ -208,24 +308,24 @@ if asked.api == "stdout":
elif asked.api == "twitter":
import ConfigParser
# Authenticate
config = ConfigParser.RawConfigParser()
config.read('twitter.conf')
consumer_key = config.get("Auth","key")
consumer_secret = config.get("Auth","key_secret")
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","token")
access_token_secret = config.get("Auth","token_secret")
access_token = config.get("Auth","local_token")
access_token_secret = config.get("Auth","local_token_secret")
auth = tweepy.OAuthHandler(consumer_key, consumer_secret, "https://api.twitter.com/1.1/")
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]
key=
key_secret=
token=
token_secret=
[App]
app_key=
app_key_secret=