Skip to content

Instantly share code, notes, and snippets.

@Xon
Created July 8, 2011 15:15
Show Gist options
  • Save Xon/1072059 to your computer and use it in GitHub Desktop.
Save Xon/1072059 to your computer and use it in GitHub Desktop.
Updated py interface for jsonapi bukkit w/ multi-subscription streaming
#!/usr/bin/python
import json
import socket
from hashlib import sha256
from urllib import urlencode
from urllib2 import urlopen
class MinecraftStream(object):
# Extends the basic stream object and adds a readjson method to the object
def __getattribute__(self, name):
if name not in ['readjson', '_original_stream']:
return getattr(
object.__getattribute__(self, '_original_stream'),
name
)
else:
return object.__getattribute__(self, name)
def __init__(self, stream):
self._original_stream = stream
def readjson(self, *args, **kwargs):
ret = self._original_stream.readline(*args, **kwargs)
return json.loads(ret[2:])
class MinecraftJsonApi (object):
'''
Python Interface to JSONAPI for Bukkit (Minecraft)
Based off of the PHP interface by Alec Gorge <alecgorge@gmail.com>
https://github.com/alecgorge/jsonapi/raw/master/sdk/php/JSONAPI.php
(c) 2011 Accalia.de.Elementia <Accalia.de.Elementia@gmail.com>
This work is licensed under a Creative Commons Attribution
3.0 Unported License <http://creativecommons.org/licenses/by/3.0/>
JSONAPI homepage:
http://ramblingwood.com/minecraft/jsonapi/
'''
__basic_url = 'http://{host}:{port}/api/call?{query}'
__subscribe_url = '/api/subscribe?{query}'
__letters = list('abcdefghijklmnopqrstuvwxyz')
def __createkey(self, method):
'''
Create an authentication hash for the given method.
'''
return sha256('{username}{method}{password}{salt}'.format(
username = self.username,
method = method,
password = self.password,
salt = self.salt
)
).hexdigest()
def __createURL(self, method, args):
'''
Create the full URL for calling a method.
'''
key = self.__createkey(method)
return self.__basic_url.format(
host = self.host,
port = self.port,
query = urlencode([
('method', method),
('args', json.dumps(args)),
('key', key),
])
)
def __createStreamURL(self, sources,backlog):
'''
Create the full URL for subscribing to a stream.
'''
if backlog:
backlog = 'true'
else:
backlog = 'false'
if len(sources) == 1:
key = self.__createkey(sources[0])
query = urlencode([('source', sources[0]),('key', key),('show_previous',backlog)])
else:
key = self.__createkey(json.dumps(sources))
print json.dumps(sources)
query = urlencode([('sources', json.dumps(sources)),('key', key),('show_previous',backlog)])
print query
return self.__subscribe_url.format(query = query)
def __createsocket(self):
'''
Setup a socket connection to the server and return a file like
object for reading and writing.
Copied with minor edits from examples on:
http://docs.python.org/library/socket.html
'''
try:
flags = socket.AI_ADDRCONFIG
except AttributeError:
flags = 0
for res in socket.getaddrinfo(self.host, (self.port+1),
socket.AF_UNSPEC, socket.SOCK_STREAM,
socket.IPPROTO_TCP, flags):
af, socktype, proto, canonname, sa = res
try:
sock = socket.socket(af, socktype, proto)
sock.connect(sa)
except socket.error:
if sock:
sock.close()
sock = None
continue
break
if not sock:
raise Exception('Connect failed')
return MinecraftStream(sock.makefile('rwb'))
def __createMethodAttributes(self, method):
'''
Yet another translation method.
Transform the method definition JSON into a dictionary
containing only the attributes needed for the wrapper.
'''
attrs = {}
attrs['name'] = method.get('name', '')
if attrs['name'] < 0:
raise Exception('Malformed method definition in JSON')
attrs['description'] = method.get('desc','')
attrs['namespace'] = method.get('namespace','')
attrs['enabled'] = method.get('enabled',False)
if attrs['namespace']:
attrs['method_name'] = attrs['namespace']+ '_'+attrs['name']
attrs['call_name'] = attrs['namespace'] + '.'+attrs['name']
else:
attrs['method_name'] = attrs['name']
attrs['call_name'] = attrs['name']
attrs['returns'] = method.get('returns',
[None,'Unspecified return type.'])[1]
args = method.get('args',[])
num_args = len(args)
alpha = self.__letters
attrs['args'] = str(alpha[:num_args]).replace('\'','')[1:-1]
attrs['params'] = '\n'.join([
'{1} ({0})'.format(a[0], a[1]) for a in args
])
return attrs
def __createMethod (self, method):
'''
Create a dynamic method based on provided definition.
TODO: Is there a better way to do this? Possibly via closure to
avoid exec
'''
def makeMethod (method):
call_name = method['call_name']
def _method (self, *args):
return self.call(call_name,*args)
_method.__name__ = str(method['method_name'])
_method.__doc__ = """{description}
{returns}
Parameters:
{params}
""".format(**method)
return _method
attributes = self.__createMethodAttributes(method)
if method['enabled']:
rv_method = makeMethod(attributes)
else:
rv_method = None
attributes['method'] = rv_method
del attributes['call_name']
del attributes['args']
return attributes
def __loadMethods(self):
'''Load methods defined by the remote server into the API.
This will generate conveniance methods based on the available
server methods, the methods generated by this method are not
guaranteed to exist for all servers, when in doubt
MinecraftJsonApi.call should be used instead.
'''
# Retrieve the JSON config files used by JSONAPI and
# server plugin list
files = [i for i in self.call('getPluginFiles','JSONAPI') if i.endswith('json')]
plugins = self.call('getPlugins',[])
for target in files:
contents = self.call('getFileContents', target)
config = json.loads(contents)
enabled = True
for dep in config['depends']:
match = [m for m in plugins if m['name'] == dep and m['enabled']]
if len(match) < 1:
enabled = False
break
namespace = config['namespace']
for method in config['methods']:
method['namespace'] = namespace
method['enabled'] = enabled
cfg = self.__createMethod(method)
if cfg['enabled']:
setattr(self.__class__, cfg['method_name'], cfg['method'])
cfg['params'] = [s for s in cfg['params'].split('\n') if len(s)]
self.__methods.append(cfg)
def __init__(self, host='localhost', port=20059, username='admin',
password='demo', salt='', autoload_methods=True):
self.host = host
self.username = username
self.password = password
self.port = port
self.salt = salt
self.__methods = []
if autoload_methods:
self.__loadMethods()
def rawCall (self, method, *args):
'''
Make a remote call and return the raw response.
'''
url = self.__createURL(method, args)
result = urlopen(url).read()
return result
def call (self, method, *args):
'''
Make a remote call and return the JSON response.
'''
data = self.rawCall(method, *args)
result = json.loads(data)
if result['result'] =='success':
return result['success']
else:
raise Exception('(%s) %s' %(result['result'], result[result['result']]))
def subscribe (self, feed,backlog):
'''
Subscribe to the remote stream.
Return a file like object for reading responses from. Use
read/readline for raw values, use readjson for parsed values.
'''
# This doesn't work right, I don't know why.... yet.
# raise NotImplementedError()
#if feed not in ['console', 'chat', 'connections','all']:
# raise NotImplementedError(
# 'Subscribing to feed \'%s\' is not supported.' % feed)
url = self.__createStreamURL(feed,backlog)
stream = self.__createsocket()
stream.write(url)
stream.write('\n')
stream.flush()
return stream
def getLoadedMethods(self, active_only=True):
'''
Get all methods recognized by the remote server.
'''
if active_only:
test = lambda x: x.get('enabled', False)
else:
test = lambda x: True
return [a for a in self.__methods if test(a)]
def getMethod(self, name):
'''
get method definition for the provided method name.
If the method is in a name space the namespace must be provided
too, the name having the form "{namespace}_{name}"
'''
method = [m for m in self.__methods if m['method_name'] == name]
if len(method):
return method[0]
else:
return None
if __name__ == '__main__':
# Some basic test code
# Read params
paramDefaults = {'host': 'localhost', 'port':20059, 'username':'admin', 'password':'demo', 'salt':''}
filterFuncs = {'host': str, 'port': int, 'username': str, 'password': str, 'salt': str}
params = {}
# for k in paramDefaults.keys():
# value = raw_input("%s (%s): " % (k.capitalize(), str(paramDefaults[k])))
# if len(value):
# params[k] = filterFuncs[k](value)
# else:
# params[k] = paramDefaults[k]
params = paramDefaults
api = MinecraftJsonApi(
host = params['host'],
port = params['port'],
username = params['username'],
password = params['password'],
salt = params['salt']
)
# print api.reloadServer()
# print api.broadcastWithName("test2","xon")
# print([m['method_name']+"\n" for m in api.getLoadedMethods()])
# print (api.getMethod('kickPlayer'))
# import time
feed = api.subscribe(['chat','connections'],None)
while 1:
print feed.readline()
# time.sleep(0.05)
["chat", "connections"]
sources=%5B%22chat%22%2C+%22connections%22%5D&key=4fb942b7cc73789919dc92f1ebbab214c1571b4d49e979a7070990dbc6606393&show_previous=false
{"result":"success","source":"chat","success":{"message":"test","time":1310108661,"player":"Xon"}}
{"result":"success","source":"connections","success":{"time":1310108656,"player":"Xon","action":"connected"}}
@Xon
Copy link
Author

Xon commented Jul 8, 2011

I originally thought the backlog was in reverse order, but it was an artefact of the fact that the it processes a stream at a time rather than multiplexing it based on each message's timestamp.

Here is the reconnect log after a few more bits;
{"result":"success","source":"chat","success":{"message":"test","time":1310108661,"player":"Xon"}}
{"result":"success","source":"chat","success":{"message":"test2","time":1310109542,"player":"Xon"}}
{"result":"success","source":"connections","success":{"time":1310108656,"player":"Xon","action":"connected"}}
{"result":"success","source":"connections","success":{"time":1310109544,"player":"Xon","action":"disconnected"}}

Having the individual messages of the stream being out of order with respect to the entire stream is jarring and non-intuitive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment