#!/usr/bin/python
# The MIT License
#
# Copyright (c) 2007
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# Version: 0.7
#
# README
# This is a simple python command-line application intended to allow the
# user to create and edit posts on a Livejournal, MT or Wordpress based
# Weblog
#
# Usage
# $ python weblog.py
#
# CONFIG
# Users may override any config option in the
# WeblogConsoleApplicationConfig class below. (around line 130) Examples
# for configuring default XMl-RPC server values are included
#
# serverUrl:
# The XML-RPC Url for your weblog. See your weblog provider and/or
# manual for this information
#
# serverApi:
# The API used to talk to your weblog. Valid values are 'blogger' or
# 'metaweblog'. Livejournal weblogs use blogger, MT and Wordpress
# weblogs use metaweblog. The statment is 90% true.
#
# username:
# Your weblog username
#
# password:
# Your weblog password
#
# editorCommand:
# The command to invoke your text editor. Include any flags you would
# for editing a stand alone file. (AKA, python calls this command by
# appending the path to a temp file)
#
# spellcheckCommand:
# The command to invoke your spellchecker. Include any flags you would
# for editing a stand alone file. (AKA, python calls this command by
# appending the path to a temp file) If left out (commented)
# spellchecking will be skipped
#
# metaweblog_post_options:
# A dictionary to provide additional paramaters to the your post
# creation/edit requests. There is currently no way to do this on a per
# post basis.
#
# BUGS
# Bugs may be reported to via the contact form at
# http://alanstorm.com/site/contact. This was a small side project to
# get myself used to python's syntax, so I make no promises about
# support :)
#
# FEEDBACK
# Any other feedback can be sent via the contact form at
# http://alanstorm.com/site/contact
#
#----------- For Next Version, if ever -----------#
#TO DO: Remove getters/setters from wrapper class. I didn't
# know about property()
#
#TO DO: New Config method. Python's config parser maybe?
#
#TO DO: allow user to conifgure all metaweblog/mt custom paramas per
# post
#
#TO DO: Config Sets: Handle Multiple Installs with a single script?
#
#TO DO: Right now if metaweblog is set as the API, the application will
# fall back on the blogger API to get a list of configured
# weblogs. Need/Want something more elegant and "right" here.
# Probably need to move to classifying each config set by system,
# and then using any/all API methos provided (naively assumed each
# API supported the "core" action of listing a weblog)
#
#TO DO: Order posts by date instead of ID
#
#TO DO: "Functionalize" the Wrapper Class, that pattern is way to
# obvious NOT to
#
#TO DO: Remove wonk surrounding handling of titles (re: blogger
# problem). Still keep
in text for all editing
# though, easy way to edit the title
#
#TO DO: ".." navigation to move up a step
#
#TO DO: Other APIs: "new" blogger, for example. ATOM?
#-------------------------------------------------#
import getpass
import xmlrpclib
import re
import tempfile
import os
import sys
class WeblogConsoleApplicationConfig:
""" Static Conifg Object
A static config object. Left alone, the script will prompt you for
each config option. You can override these prompts by setting a
value directly (see examples below). You WILL need to comment out
or remove the existing line
ex.
#serverUrl = raw_input("Enter XML-RPC URL: ")
serverUrl = https://www.example.com/xmlrpc
"""
serverUrl = raw_input("These variables can be configured by editing the weblog.py file\nEnter XML-RPC URL: ")
serverApi = raw_input("Enter Platform ('blogger' or 'metaweblog' supported): ")
username = raw_input("Enter Username: ")
password = getpass.getpass("Enter Password: ")
#example config for a LiveJournal weblog
# serverUrl = 'https://www.livejournal.com/interface/blogger'
# serverApi = 'blogger'
# username = 'username'
# password = 'password'
#example config for a MT or Wordpress weblog
# serverUrl = 'https://example.com/path/to/xmlrpc.php'
# serverApi = 'metaweblog'
# username = 'username'
# password = 'password'
#other config options
editorCommand = '/usr/bin/pico'
#spellcheckCommand = '/usr/local/bin/aspell -c'
metaweblog_post_options = {
#'mt_convert_breaks':1
}
#----------- Change Nothing Below This Line (unless you want to) ----------#
class WeblogXmlRpcWrapper:
""" XML-RPC Weblog wrapper
wrapper class for the various weblog apis
initially, just blooger and metaweblog apis
...
After doing all this, I can see why ATOM was invented
"""
def __init__(self):
"""Standard "Not a Constructor init"""
self.setUsername('')
self.setPassword('')
self.setServerUrl('')
self.setApi('')
#END def
#property to hold username
def setUsername(self,value):
"""Accessor: Setter"""
self.username = value
pass
def getUsername(self):
"""Accessor: Getter"""
return self.username
pass
#property to hold password
def setPassword(self,value):
"""Accessor: Setter"""
self.password = value
pass
def getPassword(self):
"""Accessor: Getter"""
return self.password
pass
#property to hold server url
def setServerUrl(self,value):
"""Accessor: Setter"""
self.serverUrl = value
pass
def getServerUrl(self):
"""Accessor: Getter"""
return self.serverUrl
pass
#property to hold which api
def setApi(self,value):
"""Accessor: Setter"""
self.api = value
pass
def getApi(self):
"""Accessor: Getter"""
return self.api
pass
def getXmlRpcServer(self):
return xmlrpclib.Server(self.getServerUrl());
def fetchAvailableWeblogs(self):
""" Fetches a list of weblogs from the server and returns in a normalized fashion
Returns a list of dictionaries
item['url']
item['blogName']
item['blogid']
"""
server = self.getXmlRpcServer()
if ('blogger' == self.api):
the_result = server.blogger.getUsersBlogs('',self.getUsername(),self.getPassword())
blogs = []
for item in the_result:
tmp = {}
tmp['url'] = item['url']
tmp['blog-name'] = item['blogName']
tmp['blog-id'] = item['blogid']
blogs.append(tmp)
elif ('metaweblog' == self.api):
raise "The Metaweblog API doesn't support fetching a list of weblogs"
else:
raise "Unknown API, don't know what to do"
return blogs
#property yo hold xmlrpc server instance
#method to get a "list" of posts
def fetchPostList(self, blog_id, how_many=10):
""" fetches a list of posts from the server, and normalizes
Returns a list of dictionaries
item['postId']
item['content']
item['dateCreated']
item['title']
"""
server = self.getXmlRpcServer()
normalized_server_posts = []
#based on API, fetch posts then iterate through to normalize
if ('blogger' == self.api):
server_posts = server.blogger.getRecentPosts('',blog_id,self.getUsername(),self.getPassword(),how_many)
for item in server_posts:
tmp = {}
tmp['post-id'] = item['postId']
tmp['content'] = item['content']
tmp['date-created'] = item['dateCreated']
#blogger api has no concept of title. livejournal and others work
#around that by inserting a into the content
try:
tmp['title'] = re.search('(.+?)', item['content']).groups()[0]
except:
tmp['title'] = "No Title"
normalized_server_posts.append(tmp)
pass
elif ('metaweblog' == self.api):
server_posts = server.metaWeblog.getRecentPosts(blog_id,self.getUsername(),self.getPassword(),how_many)
for item in server_posts:
tmp = {}
tmp['post-id'] = item['postid']
tmp['content'] = item['description']
tmp['date-created'] = item['dateCreated']
tmp['title'] = item['title']
if '' == tmp['title'].strip():
tmp['title'] = "No Title"
normalized_server_posts.append(tmp)
else:
raise "Unknown API, don't know what to do"
return normalized_server_posts
#END def
#method to create a new post
def newPost(self,blog_id,content,options={}):
""" Creates a new post
Creates a new post. Content is a dict with
content and title keys. Options is an dict that
contains metaweblog's additional options
"""
server = self.getXmlRpcServer()
#don't forget to update editPost with any changes to the content parsing.
#TODO: Centralize content parsing code so the above isn't relevent
if ('blogger' == self.api):
if (not ('' == str(content['title'])) ):
the_content = ''+str(content['title'])+"" + "\n" + str(content['content']).strip()
else:
the_content = content['content']
the_id = server.blogger.newPost('',blog_id,self.getUsername(),self.getPassword(),the_content, True)
elif ('metaweblog' == self.api):
the_content = {}
the_content['title'] = content['title']
the_content['description'] = re.sub('(.+?)', '', content['content'])
the_content['description'] = the_content['description'].strip()
for item in options:
the_content[item] = options[item]
the_id = server.metaWeblog.newPost(blog_id,self.getUsername(),self.getPassword(),the_content, True)
else:
raise "Unknown API, don't know what to do"
return the_id
#end def
#method to edit a post
def editPost(self,post_id,content,options={}):
""" Replaces an existed post
Replaces an existing post. Content is a dict with
content and title keys. Options is an dict that
contains metaweblog's additional options
"""
server = self.getXmlRpcServer()
#don't forget to update newPost with any changes to the content parsing.
#TODO: Centralize content parsing code so the above isn't relevent
if ('blogger' == self.api):
if (not ('' == str(content['title'])) ):
the_content = ''+str(content['title'])+"" + "\n" + str(content['content']).strip()
else:
the_content = content['content']
the_result = server.blogger.editPost('',post_id,self.getUsername(),self.getPassword(),the_content,True)
elif ('metaweblog' == self.api):
the_content = {}
the_content['title'] = content['title']
the_content['description'] = re.sub('(.+?)', '', content['content'])
the_content['description'] = the_content['description'].strip()
for item in options:
the_content[item] = options[item]
the_result = server.metaWeblog.editPost(post_id,self.getUsername(),self.getPassword(),the_content, True)
else:
raise "Unknown API, don't know what to do"
return the_result
#END def
#method to delete a post
def deletePost(self,post_id):
""" Deletes a post
"""
server = self.getXmlRpcServer()
if ('blogger' == self.api):
the_result = server.blogger.deletePost('',post_id,self.getUsername(),self.getPassword())
elif ('metaweblog' == self.api):
raise "The metaWeblog api does not support deletePost"
else:
raise "Unknown API, don't know what to do"
return the_result
#END def
#END class
class ConsoleApplication:
""" Methods for building a simple input based app
Class that holds some generic methods for building a simple input
based console appplication
"""
def __init__(self):
"""Standard "Not a constructor" init"""
self.steps = []
self.currentStep = -1
def run(self):
""" Moves us on to the next step """
self.currentStep += 1
#don't le the step count get too big or too small
if self.currentStep >= len(self.steps) or self.currentStep < 0:
self.currentStep = 0
#run our current step
self.steps[self.currentStep]()
#run the next step
self.run()
def addStep(self,f):
""" Adds a step to the application """
self.steps.append(f)
def yesOrNo(self,the_question,exit_command=None):
""" Do you wish to continue (y|n)
prompts user for a Y/N answer. Only listens to first characters,
so yes, no, No, YeS, noooooooo, etc. will all work
"""
answer = raw_input(the_question + ' (y|n): ')
answer = str(answer)
if answer == exit_command:
sys.exit()
try:
answer = answer[0]
answer = answer.lower()
if 'y' == answer:
return True
elif 'n' == answer:
return False
else:
return self.yesOrNo(the_question,exit_command)
except:
return self.yesOrNo(the_question,exit_command)
#end DEF
#prompts user to select from a list of items
#returns index of selected value
def inputFromDatastructure(self,prompt,data_structure,sort_list=None,exit_command=None):
""" Create a list of selectable options from a data_structure
Pass in a tupple, dict or list and this method will print out
a #) Value list for each one, and then ask the user to input
a number to select an option. Method will then return the KEY
value that the user selected. For lists, this is an numerical
index. For tuples and dicts, this is the actual key value
sort_list is an optional list of keys you can pass in to sort
the printed list by. You could also use the sort_list to
explude certain items from the list.
the_keys = someDict.keys()
the_keys.sort()
result = foo.inputFromDatastructure("Make a choice: ",someDict, the_keys)
The only restrition on keys in the word "new" If there's a key named
new in the data_structure, then this item will always be printed first
"""
if type(data_structure) in (list,dict,tuple):
#keeps a list of selected value:key mappings
the_keys = {}
#a key named 'new' should always be the first item
try:
print '1) ' + str(data_structure['new'])
the_keys[1] = 'new'
c = 2
except:
c = 1
#if a sort list has been passed in,
#itterate using it to get the desired order
if(sort_list):
iterator = sort_list
else:
iterator = data_structure
for item in iterator:
item_description = ''
if type(data_structure) == dict and (not 'new' == str(item)) :
the_keys[c] = item
item_description = data_structure[item] + " ("+item+") "
print str(c) + ') ' + item_description
c+=1
elif (not 'new' == str(item)):
the_keys[c] = (c-1)
item_description = item
print str(c) + ') ' + item_description
c+=1
choice = raw_input(prompt)
try:
if exit_command == choice:
print "here"
sys.exit()
choice = int(choice)
except SystemExit:
pass
except:
print str(choice) + " is an Invalid Response"
#recurse!
return self.inputFromDatastructure(prompt,data_structure,sort_list,exit_command)
else:
#make sure we're within range
if(choice > len(data_structure) ):
print str(choice) + " is an Invalid Response"
#recurse!
return self.inputFromDatastructure(prompt,data_structure,sort_list,exit_command)
else:
#valid response
pass
#print "Valid Resonse: " + str(choice)
#decrement for index
#choice -= 1
else:
raise "Invalid Type passed to inputFromDatastructure"
return the_keys[choice]
#END def
def getInputFromShellCommandWithTempFile(self,cmd, default_info=False):
""" Runs a file based shell command and returns the results
cmd is the full path to some command that operates on a file. For example
cmd = "/usr/bin/vi"
cmd = "/usr/local/bin/aspell -c"
This method
1. creates a temp file,
2. uses the above command line string to edit the temp file
3. when the program exits, reads in whatever has been saved to the temp file
A default value for the temp file is set with the default_info parameter
"""
#create tempfile
(file_descriptor,file_path) = tempfile.mkstemp()
#write default_info out to tempfile
#If default info is False, write out a default template
if(not default_info):
default_info = ''
file_handler = open(file_path,'w')
file_handler.write(default_info)
file_handler.close()
#open file with editor in a way that waits
os.system(cmd+' '+file_path)
#read tempfile contents into memory
file_handler = open(file_path,'r')
input_text =file_handler.read()
file_handler.close()
#remove tempfile
os.unlink(file_path)
#return contents
return input_text
#END def
#END class
class WeblogConsoleApplication(ConsoleApplication):
""" The main class file for out application
Inherits from ConsoleApplication, which provides input functions, as well
as a framework for running each "step"/method that makes up the app
"""
def __init__(self):
""" More than the usual init
In addition to setting up our properties with empty values, this
init initializes out xmlrpc server, and adds methods as steps to
the application.
"""
ConsoleApplication.__init__(self)
#intiialize default values
self.indexWeblog = ''
self.posts_raw = ''
self.indexPosts = ''
self.new_post = ''
#conifgure our XML-RPC server
self.weblog_server = WeblogXmlRpcWrapper()
self.weblog_server.setUsername(WeblogConsoleApplicationConfig.username)
self.weblog_server.setPassword(WeblogConsoleApplicationConfig.password)
self.weblog_server.setApi(WeblogConsoleApplicationConfig.serverApi)
self.weblog_server.setServerUrl(WeblogConsoleApplicationConfig.serverUrl)
#setpup application steps
self.addStep(self.chooseWeblog)
self.addStep(self.choosePost)
self.addStep(self.createOrEditPost)
self.addStep(self.sendPost)
def getWeblogs(self,weblog_server):
""" Grabs a list of weblogs conifgured in a system
Even if metaweblog is the api, we fall back to the blogger api,
since metaweblog doesn't have the needed functionality.
"""
if 'metaweblog' == WeblogConsoleApplicationConfig.serverApi:
weblog_server.setApi('blogger')
raw_blogs = weblog_server.fetchAvailableWeblogs()
weblog_server.setApi('metaweblog')
else:
raw_blogs = weblog_server.fetchAvailableWeblogs()
weblogs = {}
for item in raw_blogs:
weblogs[str(item['blog-id'])] = str(item['blog-name']) + ' (' + str(item['url']) + ')'
#weblogs.append(str(item['blog-name']))
return weblogs
#END def
def getRawWeblogPosts(self,weblog_server, blog_id):
"""grabs a list of posts form the API"""
return weblog_server.fetchPostList(blog_id)
#END def
def getWeblogPosts(self,posts_raw):
"""grabs a list of posts from the API and returns in a normalized fashion"""
posts = {'new':"Create New"}
for item in posts_raw:
id = item['post-id']
content = item['content']
title = item['title']
posts[id] = title
return posts
#END def
def getPostContent(self,key,posts):
"""grab our post content, return a default string if the key isn't found"""
post_content = "This is some default post content"
for item in posts:
if item['post-id'] == key:
#this is so screwed up. Livejournal's use of the blogger API and it's
#wonkiness surrounding titles are screwy. Livejournal will return BOTH
#the tag as part of the content AND a title key in the dictionary.
#So, we'll strip out the title tag form teh content before re-adding it.
#
#UPDATE: Wow, I R Idiot. It's my wrapper class that adds the 'title'
# ... I think I see why ATOM was invented
post_content = re.sub('(.+?)', '', item['content'])
post_content.strip()
try:
post_content = "" + item['title'] + "\n" + post_content.strip()
except:
pass
#normalize line endings to \n, maybe look into doing the "right"
#thing based on the OS
post_content = re.sub(r'\r\n','\n',post_content)
post_content = re.sub(r'\r','\n',post_content)
return post_content
#END def
def mainConfirmPost(self,post_content,is_new=False):
""" Edit, spellcheck, and otherwise get out finalized post content
This is recursivly called until a user either bails out or
finished editing
"""
application = WeblogConsoleApplication()
if (is_new):
new_title = raw_input("Post Title: ")
post_content = "" + new_title + "" + "\n" + post_content.strip()
new_post = application.getInputFromShellCommandWithTempFile(WeblogConsoleApplicationConfig.editorCommand,post_content)
new_post = new_post.strip()
if new_post == post_content:
answer = application.yesOrNo("Hey, it doesn't look like you made any changes. Go ahead anyway?",'exit')
if(not answer):
answer = application.yesOrNo("Do you want to edit the post again?",'exit')
if(answer):
return self.mainConfirmPost(post_content)
else:
self.currentStep -= 2
return
print "Your new post is below"
print "+--------------------------------+"
print new_post
print "+--------------------------------+"
try:
WeblogConsoleApplicationConfig.spellcheckCommand
answer = application.yesOrNo("Do you wish to spellcheck? ",'exit')
if (answer):
new_post = application.getInputFromShellCommandWithTempFile(WeblogConsoleApplicationConfig.spellcheckCommand,new_post)
print "Your new post is below"
print "+--------------------------------+"
print new_post
print "+--------------------------------+\n"
except:
pass
answer = application.yesOrNo("Do you wish to post the above? ",'exit')
if answer:
return new_post
else:
answer = application.yesOrNo("Do you wish to edit your post? ",'exit')
if answer:
return self.mainConfirmPost(new_post)
else:
self.currentStep -= 2
return
#end DEF
def start(self):
""" Start out Application, main entry point"""
#do any init stuff here
self.run()
#END def
def chooseWeblog(self):
""" #step 1: Choose weblog """
#get list of weblogs and choose a weblog
weblogs = self.getWeblogs(self.weblog_server)
self.indexWeblog = self.inputFromDatastructure("Please Select a Weblog: ",weblogs,exit_command='exit')
print "You selected "+str(weblogs[self.indexWeblog])
#END def
def choosePost(self):
""" #step 2: Choose post """
#get list of posts and choose a post
self.posts_raw = self.getRawWeblogPosts(self.weblog_server,self.indexWeblog)
posts = self.getWeblogPosts(self.posts_raw)
#TODO: build sortlist based on post date
sort_list = posts.keys()
sort_list.sort()
self.indexPosts = self.inputFromDatastructure("Please Select a Post: ",posts, sort_list,'exit')
print "You selected "+str(posts[self.indexPosts])
#END def
def createOrEditPost(self):
""" #step 3: Create/Edit post """
if ('new' == self.indexPosts):
is_new = True
else:
is_new = False
#get post content and edit
post_content = self.getPostContent(self.indexPosts, self.posts_raw)
post_content = post_content.strip()
self.new_post = self.mainConfirmPost(post_content, is_new)
#END def
def sendPost(self):
""" #step 4: Post Away """
content = {}
content['content'] = self.new_post
try:
#first populate the title
content['title'] = re.search('(.+?)', self.new_post).groups()[0]
#then remove the title from the content
content['content'] = re.sub('(.+?)\s*', '', content['content'])
except:
content['title'] = ''
if 'new' == str(self.indexPosts):
self.weblog_server.newPost(self.indexWeblog, content, WeblogConsoleApplicationConfig.metaweblog_post_options)
else:
self.weblog_server.editPost(self.indexPosts, content, WeblogConsoleApplicationConfig.metaweblog_post_options)
#END def
#END class
def main():
"""Main Flow Control Function. No Global Namespace for us. Does this even matter in python?"""
application = WeblogConsoleApplication()
application.start()
#end def
print "\nTo quit, type 'exit', Ctrl-D, or Ctrl-C\n"
main()