Merge branch 'master' of https://github.com/nojhan/forthlift
Conflicts: forthlift.py
This commit is contained in:
commit
c3ed359cf2
3 changed files with 236 additions and 101 deletions
59
README.md
59
README.md
|
|
@ -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/
|
||||
|
||||
|
|
|
|||
162
forthlift.py
162
forthlift.py
|
|
@ -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 )
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
[Auth]
|
||||
key=
|
||||
key_secret=
|
||||
token=
|
||||
token_secret=
|
||||
[App]
|
||||
app_key=
|
||||
app_key_secret=
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue