commit 83d30ceab6826c2e2514c2f633fd3c6378430e9b Author: Dave Pedu Date: Sat Dec 28 12:58:20 2013 -0500 Initial commit diff --git a/config.main.yml b/config.main.yml new file mode 100644 index 0000000..19d4007 --- /dev/null +++ b/config.main.yml @@ -0,0 +1,2 @@ +botdir: /home/example/bot/pyircbot/ +moduledir: /home/example/bot/pyircbot/modules/ diff --git a/data/config/DogeDice.yml b/data/config/DogeDice.yml new file mode 100644 index 0000000..28b01d0 --- /dev/null +++ b/data/config/DogeDice.yml @@ -0,0 +1,5 @@ +minBet: .01 +lobbyIdleSeconds: 15 +channelWhitelistOn: True +channelWhitelist: + - test \ No newline at end of file diff --git a/data/config/DogeRPC.yml b/data/config/DogeRPC.yml new file mode 100644 index 0000000..8939bf3 --- /dev/null +++ b/data/config/DogeRPC.yml @@ -0,0 +1,4 @@ +host: 127.0.0.1 +username: root +password: root +port: 22555 \ No newline at end of file diff --git a/data/config/DogeScramble.yml b/data/config/DogeScramble.yml new file mode 100644 index 0000000..9754af9 --- /dev/null +++ b/data/config/DogeScramble.yml @@ -0,0 +1,7 @@ +hintDelay: 15 +delayNext: 5 +maxHints: 5 +abortAfterNoGuesses: 2 +winAmount: 0.1 +categoryduration: 2 +decreaseFactor: 0.83 \ No newline at end of file diff --git a/data/config/GameBase.yml b/data/config/GameBase.yml new file mode 100644 index 0000000..9f93ded --- /dev/null +++ b/data/config/GameBase.yml @@ -0,0 +1,3 @@ +channelWhitelistOn: True +channelWhitelist: + - test \ No newline at end of file diff --git a/data/config/MySQL.yml b/data/config/MySQL.yml new file mode 100644 index 0000000..92ce094 --- /dev/null +++ b/data/config/MySQL.yml @@ -0,0 +1,4 @@ +host: 127.0.0.1 +username: root +password: root +database: pyircbot diff --git a/data/config/Scramble.yml b/data/config/Scramble.yml new file mode 100644 index 0000000..cbd5bc5 --- /dev/null +++ b/data/config/Scramble.yml @@ -0,0 +1,5 @@ +hintDelay: 15 +delayNext: 5 +maxHints: 5 +abortAfterNoGuesses: 5 +decreaseFactor: 0.83 \ No newline at end of file diff --git a/data/config/Services.yml b/data/config/Services.yml new file mode 100644 index 0000000..6cdb994 --- /dev/null +++ b/data/config/Services.yml @@ -0,0 +1,23 @@ +user: + nick: + - pyircbot3 + - pyircbot3_ + - pyircbot3__ + password: nickservpassword + username: pyircbot3 + hostname: pyircbot3.domain.com + realname: pyircbot3 +ident: + enable: yes + to: nickserv + command: identify %(password)s + ghost: yes + ghost_to: nickserv + ghost_cmd: ghost %(nick)s %(password)s +channels: + - "#pyircbot3" +privatechannels: + to: chanserv + command: invite %(channel)s + list: + - "#aprivatechannel" diff --git a/data/data/DogeScramble/us_states.txt b/data/data/DogeScramble/us_states.txt new file mode 100644 index 0000000..ad6632e --- /dev/null +++ b/data/data/DogeScramble/us_states.txt @@ -0,0 +1,50 @@ +Alabama +Alaska +Arizona +Arkansas +California +Colorado +Connecticut +Delaware +Florida +Georgia +Hawaii +Idaho +Illinois +Indiana +Iowa +Kansas +Kentucky +Louisiana +Maine +Maryland +Massachusetts +Michigan +Minnesota +Mississippi +Missouri +Montana +Nebraska +Nevada +New Hampshire +New Jersey +New Mexico +New York +North Carolina +North Dakota +Ohio +Oklahoma +Oregon +Pennsylvania +Rhode Island +South Carolina +South Dakota +Tennessee +Texas +Utah +Vermont +Virginia +Washington +West Virginia +Wisconsin +Wyoming \ No newline at end of file diff --git a/data/data/Scramble/words.txt b/data/data/Scramble/words.txt new file mode 100644 index 0000000..3241431 --- /dev/null +++ b/data/data/Scramble/words.txt @@ -0,0 +1,313 @@ +Air +Stone +Grass +Dirt +Cobblestone +Wooden Plank +Sapling +Redwood Sapling +Birch Sapling +Bedrock +Water +Lava +Sand +Gravel +Gold Ore +Iron Ore +Coal Ore +Wood +Redwood +Birchwood +Leaves +Redwood Leaves +Birchwood Leaves +Sponge +Glass +Lapis Lazuli Ore +Lapis Lazuli Block +Dispenser +Sandstone +Note Block +Bed Block +Powered Rail +Detector Rail +Sticky Piston +Web +Dead Shrub +Tall Grass +Live Shrub +Dead Shrub +Piston +Piston Head +White Wool +Orange Wool +Magenta Wool +Light Blue Wool +Yellow Wool +Light Green Wool +Pink Wool +Gray Wool +Light Gray Wool +Cyan Wool +Purple Wool +Blue Wool +Brown Wool +Dark Green Wool +Red Wool +Black Wool +Dandelion +Rose +Brown Mushroom +Red Mushroom +Gold Block +Iron Block +Double Stone Slab +Double Sandstone Slab +Double Wooden Slab +Double Cobblestone Slab +Double Brick Slab +Double Stone Brick Slab +Stone Slab +Sandstone Slab +Wooden Slab +Cobblestone Slab +Brick Slab +Stone Brick Slab +Brick +TNT +Bookshelf +Mossy Cobblestone +Obsidian +Torch +Fire +Monster Spawner +Wooden Stairs +Chest +Redstone Wire +Diamond Ore +Diamond Block +Workbench +Wheat Crops +Soil +Furnace +Sign Post +Wooden Door +Ladder +Rails +Cobblestone Stairs +Wall Sign +Lever +Stone Pressure Plate +Iron Door Block +Wooden Pressure Plate +Redstone Ore +Redstone Torch +Stone Button +Snow +Ice +Snow Block +Cactus +Clay +Sugar Cane +Jukebox +Fence +Pumpkin +Netherrack +Soul Sand +Glowstone +Portal +Jack-O-Lantern +Cake Block +Redstone Repeater Block +Locked Chest +Trapdoor +Stone (Silverfish) +Stone Brick +Mossy Stone Brick +Cracked Stone Brick +Red Mushroom Cap +Brown Mushroom Cap +Iron Bars +Glass Pane +Melon Block +Pumpkin Stem +Melon Stem +Vines +Fence Gate +Brick Stairs +Stone Brick Stairs +Mycelium +Lily Pad +Nether Brick +Nether Brick Fence +Nether Brick Stairs +Nether Wart +Iron Shovel +Iron Pickaxe +Iron Axe +Flint and Steel +Apple +Bow +Arrow +Coal +Charcoal +Diamond +Iron Ingot +Gold Ingot +Iron Sword +Wooden Sword +Wooden Shovel +Wooden Pickaxe +Wooden Axe +Stone Sword +Stone Shovel +Stone Pickaxe +Stone Axe +Diamond Sword +Diamond Shovel +Diamond Pickaxe +Diamond Axe +Stick +Bowl +Mushroom Soup +Gold Sword +Gold Shovel +Gold Pickaxe +Gold Axe +String +Feather +Sulphur +Wooden Hoe +Stone Hoe +Iron Hoe +Diamond Hoe +Gold Hoe +Wheat Seeds +Wheat +Bread +Leather Helmet +Leather Chestplate +Leather Leggings +Leather Boots +Chainmail Helmet +Chainmail Chestplate +Chainmail Leggings +Chainmail Boots +Iron Helmet +Iron Chestplate +Iron Leggings +Iron Boots +Diamond Helmet +Diamond Chestplate +Diamond Leggings +Diamond Boots +Gold Helmet +Gold Chestplate +Gold Leggings +Gold Boots +Flint +Raw Porkchop +Cooked Porkchop +Painting +Golden Apple +Sign +Wooden Door +Bucket +Water Bucket +Lava Bucket +Minecart +Saddle +Iron Door +Redstone +Snowball +Boat +Leather +Milk Bucket +Clay Brick +Clay Balls +Sugarcane +Paper +Book +Slimeball +Storage Minecart +Powered Minecart +Egg +Compass +Fishing Rod +Clock +Glowstone Dust +Raw Fish +Cooked Fish +Ink Sack +Rose Red +Cactus Green +Coco Beans +Lapis Lazuli +Purple Dye +Cyan Dye +Light Gray Dye +Gray Dye +Pink Dye +Lime Dye +Dandelion Yellow +Light Blue Dye +Magenta Dye +Orange Dye +Bone Meal +Bone +Sugar +Cake +Bed +Redstone Repeater +Cookie +Map +Shears +Melon +Pumpkin Seeds +Melon Seeds +Raw Beef +Steak +Raw Chicken +Cooked Chicken +Rotten Flesh +Ender Pearl +Blaze Rod +Ghast Tear +Gold Nugget +Nether Wart Seeds +Potion +Glass Bottle +Spider Eye +Fermented Spider Eye +Blaze Powder +Magma Cream +Gold Music Disc +Green Music Disc +Chicken +Cow +Mooshroom +Ocelot +Pig +Sheep +Squid +Villager +Enderman +Wolf +Zombie Pigman +Wolf +Ocelot +Blaze +Cave Spider +Creeper +Ghast +Magma Cube +Silverfish +Skeleton +Slime +Spider +Spider Jockey +Zombie +Snow Golem +Iron Golem +Ender Dragon +Rana \ No newline at end of file diff --git a/pyircbot.yml b/pyircbot.yml new file mode 100644 index 0000000..9559ede --- /dev/null +++ b/pyircbot.yml @@ -0,0 +1,13 @@ +bot: + datadir: /home/example/bot/data/ + rpcbind: 0.0.0.0 + rpcport: 1876 +connection: + server: irc.freenode.net + ipv6: off + port: 6667 +modules: + - PingResponder + - Services + - MySQL + - AttributeStorage \ No newline at end of file diff --git a/pyircbot/core/__init__.py b/pyircbot/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyircbot/core/jsonrpc.py b/pyircbot/core/jsonrpc.py new file mode 100644 index 0000000..95661fe --- /dev/null +++ b/pyircbot/core/jsonrpc.py @@ -0,0 +1,1113 @@ +#!/usr/bin/env python +# -*- coding: ascii -*- +""" +JSON-RPC (remote procedure call). + +It consists of 3 (independent) parts: + - proxy/dispatcher + - data structure / serializer + - transport + +It's intended for JSON-RPC, but since the above 3 parts are independent, +it could be used for other RPCs as well. + +Currently, JSON-RPC 2.0(pre) and JSON-RPC 1.0 are implemented + +:Version: 2013-07-17-beta +:Status: experimental + +:Example: + simple Client with JsonRPC2.0 and TCP/IP:: + + >>> proxy = ServerProxy( JsonRpc20(), TransportTcpIp(addr=("127.0.0.1",31415)) ) + >>> proxy.echo( "hello world" ) + u'hello world' + >>> proxy.echo( "bye." ) + u'bye.' + + simple Server with JsonRPC2.0 and TCP/IP with logging to STDOUT:: + + >>> server = Server( JsonRpc20(), TransportTcpIp(addr=("127.0.0.1",31415), logfunc=log_stdout) ) + >>> def echo( s ): + ... return s + >>> server.register_function( echo ) + >>> server.serve( 2 ) # serve 2 requests # doctest: +ELLIPSIS + listen ('127.0.0.1', 31415) + ('127.0.0.1', ...) connected + ('127.0.0.1', ...) <-- {"jsonrpc": "2.0", "method": "echo", "params": ["hello world"], "id": 0} + ('127.0.0.1', ...) --> {"jsonrpc": "2.0", "result": "hello world", "id": 0} + ('127.0.0.1', ...) close + ('127.0.0.1', ...) connected + ('127.0.0.1', ...) <-- {"jsonrpc": "2.0", "method": "echo", "params": ["bye."], "id": 0} + ('127.0.0.1', ...) --> {"jsonrpc": "2.0", "result": "bye.", "id": 0} + ('127.0.0.1', ...) close + close ('127.0.0.1', 31415) + + Client with JsonRPC2.0 and an abstract Unix Domain Socket:: + + >>> proxy = ServerProxy( JsonRpc20(), TransportUnixSocket(addr="\\x00.rpcsocket") ) + >>> proxy.hi( message="hello" ) #named parameters + u'hi there' + >>> proxy.test() #fault + Traceback (most recent call last): + ... + jsonrpc.RPCMethodNotFound: + >>> proxy.debug.echo( "hello world" ) #hierarchical procedures + u'hello world' + + Server with JsonRPC2.0 and abstract Unix Domain Socket with a logfile:: + + >>> server = Server( JsonRpc20(), TransportUnixSocket(addr="\\x00.rpcsocket", logfunc=log_file("mylog.txt")) ) + >>> def echo( s ): + ... return s + >>> def hi( message ): + ... return "hi there" + >>> server.register_function( hi ) + >>> server.register_function( echo, name="debug.echo" ) + >>> server.serve( 3 ) # serve 3 requests + + "mylog.txt" then contains: + listen '\\x00.rpcsocket' + '' connected + '' --> '{"jsonrpc": "2.0", "method": "hi", "params": {"message": "hello"}, "id": 0}' + '' <-- '{"jsonrpc": "2.0", "result": "hi there", "id": 0}' + '' close + '' connected + '' --> '{"jsonrpc": "2.0", "method": "test", "id": 0}' + '' <-- '{"jsonrpc": "2.0", "error": {"code":-32601, "message": "Method not found."}, "id": 0}' + '' close + '' connected + '' --> '{"jsonrpc": "2.0", "method": "debug.echo", "params": ["hello world"], "id": 0}' + '' <-- '{"jsonrpc": "2.0", "result": "hello world", "id": 0}' + '' close + close '\\x00.rpcsocket' + +:Note: all exceptions derived from RPCFault are propagated to the client. + other exceptions are logged and result in a sent-back "empty" INTERNAL_ERROR. +:Uses: simplejson, socket, sys,time,codecs +:SeeAlso: JSON-RPC 2.0 proposal, 1.0 specification +:Warning: + .. Warning:: + This is **experimental** code! +:Bug: + +:Author: Roland Koebler (rk(at)simple-is-better.org) +:Copyright: 2007-2008 by Roland Koebler (rk(at)simple-is-better.org) +:License: see __license__ +:Changelog: + - 2008-08-31: 1st release + +TODO: + - server: multithreading rpc-server + - client: multicall (send several requests) + - transport: SSL sockets, maybe HTTP, HTTPS + - types: support for date/time (ISO 8601) + - errors: maybe customizable error-codes/exceptions + - mixed 1.0/2.0 server ? + - system description etc. ? + - maybe test other json-serializers, like cjson? +""" + +__version__ = "2008-08-31-beta" +__author__ = "Roland Koebler " +__license__ = """Copyright (c) 2007-2008 by Roland Koebler (rk(at)simple-is-better.org) + +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.""" + +#========================================= +#import + +import logging +import sys + +#========================================= +# errors + +#---------------------- +# error-codes + exceptions + +#JSON-RPC 2.0 error-codes +PARSE_ERROR = -32700 +INVALID_REQUEST = -32600 +METHOD_NOT_FOUND = -32601 +INVALID_METHOD_PARAMS = -32602 #invalid number/type of parameters +INTERNAL_ERROR = -32603 #"all other errors" + +#additional error-codes +PROCEDURE_EXCEPTION = -32000 +AUTHENTIFICATION_ERROR = -32001 +PERMISSION_DENIED = -32002 +INVALID_PARAM_VALUES = -32003 + +#human-readable messages +ERROR_MESSAGE = { + PARSE_ERROR : "Parse error.", + INVALID_REQUEST : "Invalid Request.", + METHOD_NOT_FOUND : "Method not found.", + INVALID_METHOD_PARAMS : "Invalid parameters.", + INTERNAL_ERROR : "Internal error.", + + PROCEDURE_EXCEPTION : "Procedure exception.", + AUTHENTIFICATION_ERROR : "Authentification error.", + PERMISSION_DENIED : "Permission denied.", + INVALID_PARAM_VALUES: "Invalid parameter values." + } + +#---------------------- +# exceptions + +class RPCError(Exception): + """Base class for rpc-errors.""" + + +class RPCTransportError(RPCError): + """Transport error.""" +class RPCTimeoutError(RPCTransportError): + """Transport/reply timeout.""" + +class RPCFault(RPCError): + """RPC error/fault package received. + + This exception can also be used as a class, to generate a + RPC-error/fault message. + + :Variables: + - error_code: the RPC error-code + - error_string: description of the error + - error_data: optional additional information + (must be json-serializable) + :TODO: improve __str__ + """ + def __init__(self, error_code, error_message, error_data=None): + RPCError.__init__(self) + self.error_code = error_code + self.error_message = error_message + self.error_data = error_data + def __str__(self): + return repr(self) + def __repr__(self): + return( "" % (self.error_code, repr(self.error_message), repr(self.error_data)) ) + +class RPCParseError(RPCFault): + """Broken rpc-package. (PARSE_ERROR)""" + def __init__(self, error_data=None): + RPCFault.__init__(self, PARSE_ERROR, ERROR_MESSAGE[PARSE_ERROR], error_data) + +class RPCInvalidRPC(RPCFault): + """Invalid rpc-package. (INVALID_REQUEST)""" + def __init__(self, error_data=None): + RPCFault.__init__(self, INVALID_REQUEST, ERROR_MESSAGE[INVALID_REQUEST], error_data) + +class RPCMethodNotFound(RPCFault): + """Method not found. (METHOD_NOT_FOUND)""" + def __init__(self, error_data=None): + RPCFault.__init__(self, METHOD_NOT_FOUND, ERROR_MESSAGE[METHOD_NOT_FOUND], error_data) + +class RPCInvalidMethodParams(RPCFault): + """Invalid method-parameters. (INVALID_METHOD_PARAMS)""" + def __init__(self, error_data=None): + RPCFault.__init__(self, INVALID_METHOD_PARAMS, ERROR_MESSAGE[INVALID_METHOD_PARAMS], error_data) + +class RPCInternalError(RPCFault): + """Internal error. (INTERNAL_ERROR)""" + def __init__(self, error_data=None): + RPCFault.__init__(self, INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], error_data) + + +class RPCProcedureException(RPCFault): + """Procedure exception. (PROCEDURE_EXCEPTION)""" + def __init__(self, error_data=None): + RPCFault.__init__(self, PROCEDURE_EXCEPTION, ERROR_MESSAGE[PROCEDURE_EXCEPTION], error_data) +class RPCAuthentificationError(RPCFault): + """AUTHENTIFICATION_ERROR""" + def __init__(self, error_data=None): + RPCFault.__init__(self, AUTHENTIFICATION_ERROR, ERROR_MESSAGE[AUTHENTIFICATION_ERROR], error_data) +class RPCPermissionDenied(RPCFault): + """PERMISSION_DENIED""" + def __init__(self, error_data=None): + RPCFault.__init__(self, PERMISSION_DENIED, ERROR_MESSAGE[PERMISSION_DENIED], error_data) +class RPCInvalidParamValues(RPCFault): + """INVALID_PARAM_VALUES""" + def __init__(self, error_data=None): + RPCFault.__init__(self, INVALID_PARAM_VALUES, ERROR_MESSAGE[INVALID_PARAM_VALUES], error_data) + + +#========================================= +# data structure / serializer + +try: + import json as simplejson +except ImportError as err: + print("FATAL: json-module 'simplejson' and 'json' is missing (%s)" % (err)) + sys.exit(1) + +#---------------------- +# +def dictkeyclean(d): + """Convert all keys of the dict 'd' to (ascii-)strings. + + :Raises: UnicodeEncodeError + """ + new_d = {} + for (k, v) in d.iteritems(): + new_d[str(k)] = v + return new_d + +#---------------------- +# JSON-RPC 1.0 + +class JsonRpc10: + """JSON-RPC V1.0 data-structure / serializer + + This implementation is quite liberal in what it accepts: It treats + missing "params" and "id" in Requests and missing "result"/"error" in + Responses as empty/null. + + :SeeAlso: JSON-RPC 1.0 specification + :TODO: catch simplejson.dumps not-serializable-exceptions + """ + def __init__(self, dumps=simplejson.dumps, loads=simplejson.loads): + """init: set serializer to use + + :Parameters: + - dumps: json-encoder-function + - loads: json-decoder-function + :Note: The dumps_* functions of this class already directly create + the invariant parts of the resulting json-object themselves, + without using the given json-encoder-function. + """ + self.dumps = dumps + self.loads = loads + + def dumps_request( self, method, params=(), id=0 ): + """serialize JSON-RPC-Request + + :Parameters: + - method: the method-name (str/unicode) + - params: the parameters (list/tuple) + - id: if id=None, this results in a Notification + :Returns: | {"method": "...", "params": ..., "id": ...} + | "method", "params" and "id" are always in this order. + :Raises: TypeError if method/params is of wrong type or + not JSON-serializable + """ + if not isinstance(method, (str, unicode)): + raise TypeError('"method" must be a string (or unicode string).') + if not isinstance(params, (tuple, list)): + raise TypeError("params must be a tuple/list.") + + return '{"method": %s, "params": %s, "id": %s}' % \ + (self.dumps(method), self.dumps(params), self.dumps(id)) + + def dumps_notification( self, method, params=() ): + """serialize a JSON-RPC-Notification + + :Parameters: see dumps_request + :Returns: | {"method": "...", "params": ..., "id": null} + | "method", "params" and "id" are always in this order. + :Raises: see dumps_request + """ + if not isinstance(method, (str, unicode)): + raise TypeError('"method" must be a string (or unicode string).') + if not isinstance(params, (tuple, list)): + raise TypeError("params must be a tuple/list.") + + return '{"method": %s, "params": %s, "id": null}' % \ + (self.dumps(method), self.dumps(params)) + + def dumps_response( self, result, id=None ): + """serialize a JSON-RPC-Response (without error) + + :Returns: | {"result": ..., "error": null, "id": ...} + | "result", "error" and "id" are always in this order. + :Raises: TypeError if not JSON-serializable + """ + return '{"result": %s, "error": null, "id": %s}' % \ + (self.dumps(result), self.dumps(id)) + + def dumps_error( self, error, id=None ): + """serialize a JSON-RPC-Response-error + + Since JSON-RPC 1.0 does not define an error-object, this uses the + JSON-RPC 2.0 error-object. + + :Parameters: + - error: a RPCFault instance + :Returns: | {"result": null, "error": {"code": error_code, "message": error_message, "data": error_data}, "id": ...} + | "result", "error" and "id" are always in this order, data is omitted if None. + :Raises: ValueError if error is not a RPCFault instance, + TypeError if not JSON-serializable + """ + if not isinstance(error, RPCFault): + raise ValueError("""error must be a RPCFault-instance.""") + if error.error_data is None: + return '{"result": null, "error": {"code":%s, "message": %s}, "id": %s}' % \ + (self.dumps(error.error_code), self.dumps(error.error_message), self.dumps(id)) + else: + return '{"result": null, "error": {"code":%s, "message": %s, "data": %s}, "id": %s}' % \ + (self.dumps(error.error_code), self.dumps(error.error_message), self.dumps(error.error_data), self.dumps(id)) + + def loads_request( self, string ): + """de-serialize a JSON-RPC Request/Notification + + :Returns: | [method_name, params, id] or [method_name, params] + | params is a tuple/list + | if id is missing, this is a Notification + :Raises: RPCParseError, RPCInvalidRPC, RPCInvalidMethodParams + """ + try: + data = self.loads(string) + except ValueError as err: + raise RPCParseError("No valid JSON. (%s)" % str(err)) + if not isinstance(data, dict): raise RPCInvalidRPC("No valid RPC-package.") + if "method" not in data: raise RPCInvalidRPC("""Invalid Request, "method" is missing.""") + if not isinstance(data["method"], (str, unicode)): + raise RPCInvalidRPC("""Invalid Request, "method" must be a string.""") + if "id" not in data: data["id"] = None #be liberal + if "params" not in data: data["params"] = () #be liberal + if not isinstance(data["params"], (list, tuple)): + raise RPCInvalidRPC("""Invalid Request, "params" must be an array.""") + if len(data) != 3: raise RPCInvalidRPC("""Invalid Request, additional fields found.""") + + # notification / request + if data["id"] is None: + return data["method"], data["params"] #notification + else: + return data["method"], data["params"], data["id"] #request + + def loads_response( self, string ): + """de-serialize a JSON-RPC Response/error + + :Returns: | [result, id] for Responses + :Raises: | RPCFault+derivates for error-packages/faults, RPCParseError, RPCInvalidRPC + | Note that for error-packages which do not match the + V2.0-definition, RPCFault(-1, "Error", RECEIVED_ERROR_OBJ) + is raised. + """ + try: + data = self.loads(string) + except ValueError as err: + raise RPCParseError("No valid JSON. (%s)" % str(err)) + if not isinstance(data, dict): raise RPCInvalidRPC("No valid RPC-package.") + if "id" not in data: raise RPCInvalidRPC("""Invalid Response, "id" missing.""") + if "result" not in data: data["result"] = None #be liberal + if "error" not in data: data["error"] = None #be liberal + if len(data) != 3: raise RPCInvalidRPC("""Invalid Response, additional or missing fields.""") + + #error + if data["error"] is not None: + if data["result"] is not None: + raise RPCInvalidRPC("""Invalid Response, one of "result" or "error" must be null.""") + #v2.0 error-format + if( isinstance(data["error"], dict) and "code" in data["error"] and "message" in data["error"] and + (len(data["error"])==2 or ("data" in data["error"] and len(data["error"])==3)) ): + if "data" not in data["error"]: + error_data = None + else: + error_data = data["error"]["data"] + + if data["error"]["code"] == PARSE_ERROR: + raise RPCParseError(error_data) + elif data["error"]["code"] == INVALID_REQUEST: + raise RPCInvalidRPC(error_data) + elif data["error"]["code"] == METHOD_NOT_FOUND: + raise RPCMethodNotFound(error_data) + elif data["error"]["code"] == INVALID_METHOD_PARAMS: + raise RPCInvalidMethodParams(error_data) + elif data["error"]["code"] == INTERNAL_ERROR: + raise RPCInternalError(error_data) + elif data["error"]["code"] == PROCEDURE_EXCEPTION: + raise RPCProcedureException(error_data) + elif data["error"]["code"] == AUTHENTIFICATION_ERROR: + raise RPCAuthentificationError(error_data) + elif data["error"]["code"] == PERMISSION_DENIED: + raise RPCPermissionDenied(error_data) + elif data["error"]["code"] == INVALID_PARAM_VALUES: + raise RPCInvalidParamValues(error_data) + else: + raise RPCFault(data["error"]["code"], data["error"]["message"], error_data) + #other error-format + else: + raise RPCFault(-1, "Error", data["error"]) + #result + else: + return data["result"], data["id"] + +#---------------------- +# JSON-RPC 2.0 + +class JsonRpc20: + """JSON-RPC V2.0 data-structure / serializer + + :SeeAlso: JSON-RPC 2.0 specification + :TODO: catch simplejson.dumps not-serializable-exceptions + """ + def __init__(self, dumps=simplejson.dumps, loads=simplejson.loads): + """init: set serializer to use + + :Parameters: + - dumps: json-encoder-function + - loads: json-decoder-function + :Note: The dumps_* functions of this class already directly create + the invariant parts of the resulting json-object themselves, + without using the given json-encoder-function. + """ + self.dumps = dumps + self.loads = loads + + def dumps_request( self, method, params=(), id=0 ): + """serialize JSON-RPC-Request + + :Parameters: + - method: the method-name (str/unicode) + - params: the parameters (list/tuple/dict) + - id: the id (should not be None) + :Returns: | {"jsonrpc": "2.0", "method": "...", "params": ..., "id": ...} + | "jsonrpc", "method", "params" and "id" are always in this order. + | "params" is omitted if empty + :Raises: TypeError if method/params is of wrong type or + not JSON-serializable + """ + if sys.version_info > (3,0): + if not isinstance(method, (str)): + raise TypeError('"method" must be a string (or unicode string).') + else: + if not isinstance(method, (str, unicode)): + raise TypeError('"method" must be a string (or unicode string).') + if not isinstance(params, (tuple, list, dict)): + raise TypeError("params must be a tuple/list/dict or None.") + + if params: + return '{"jsonrpc": "2.0", "method": %s, "params": %s, "id": %s}' % \ + (self.dumps(method), self.dumps(params), self.dumps(id)) + else: + return '{"jsonrpc": "2.0", "method": %s, "id": %s}' % \ + (self.dumps(method), self.dumps(id)) + + def dumps_notification( self, method, params=() ): + """serialize a JSON-RPC-Notification + + :Parameters: see dumps_request + :Returns: | {"jsonrpc": "2.0", "method": "...", "params": ...} + | "jsonrpc", "method" and "params" are always in this order. + :Raises: see dumps_request + """ + if not isinstance(method, (str, unicode)): + raise TypeError('"method" must be a string (or unicode string).') + if not isinstance(params, (tuple, list, dict)): + raise TypeError("params must be a tuple/list/dict or None.") + + if params: + return '{"jsonrpc": "2.0", "method": %s, "params": %s}' % \ + (self.dumps(method), self.dumps(params)) + else: + return '{"jsonrpc": "2.0", "method": %s}' % \ + (self.dumps(method)) + + def dumps_response( self, result, id=None ): + """serialize a JSON-RPC-Response (without error) + + :Returns: | {"jsonrpc": "2.0", "result": ..., "id": ...} + | "jsonrpc", "result", and "id" are always in this order. + :Raises: TypeError if not JSON-serializable + """ + return '{"jsonrpc": "2.0", "result": %s, "id": %s}' % \ + (self.dumps(result), self.dumps(id)) + + def dumps_error( self, error, id=None ): + """serialize a JSON-RPC-Response-error + + :Parameters: + - error: a RPCFault instance + :Returns: | {"jsonrpc": "2.0", "error": {"code": error_code, "message": error_message, "data": error_data}, "id": ...} + | "jsonrpc", "result", "error" and "id" are always in this order, data is omitted if None. + :Raises: ValueError if error is not a RPCFault instance, + TypeError if not JSON-serializable + """ + if not isinstance(error, RPCFault): + raise ValueError("""error must be a RPCFault-instance.""") + if error.error_data is None: + return '{"jsonrpc": "2.0", "error": {"code":%s, "message": %s}, "id": %s}' % \ + (self.dumps(error.error_code), self.dumps(error.error_message), self.dumps(id)) + else: + return '{"jsonrpc": "2.0", "error": {"code":%s, "message": %s, "data": %s}, "id": %s}' % \ + (self.dumps(error.error_code), self.dumps(error.error_message), self.dumps(error.error_data), self.dumps(id)) + + def loads_request( self, string ): + """de-serialize a JSON-RPC Request/Notification + + :Returns: | [method_name, params, id] or [method_name, params] + | params is a tuple/list or dict (with only str-keys) + | if id is missing, this is a Notification + :Raises: RPCParseError, RPCInvalidRPC, RPCInvalidMethodParams + """ + try: + data = self.loads(string) + except ValueError as err: + raise RPCParseError("No valid JSON. (%s)" % str(err)) + if not isinstance(data, dict): raise RPCInvalidRPC("No valid RPC-package.") + if "jsonrpc" not in data: raise RPCInvalidRPC("""Invalid Response, "jsonrpc" missing.""") + if sys.version_info > (3,0): + if not isinstance(data["jsonrpc"], (str)): + raise RPCInvalidRPC("""Invalid Response, "jsonrpc" must be a string.""") + else: + if not isinstance(data["jsonrpc"], (str, unicode)): + raise RPCInvalidRPC("""Invalid Response, "jsonrpc" must be a string.""") + if data["jsonrpc"] != "2.0": raise RPCInvalidRPC("""Invalid jsonrpc version.""") + if "method" not in data: raise RPCInvalidRPC("""Invalid Request, "method" is missing.""") + if sys.version_info > (3,0): + if not isinstance(data["method"], (str)): + raise RPCInvalidRPC("""Invalid Request, "method" must be a string.""") + else: + if not isinstance(data["method"], (str, unicode)): + raise RPCInvalidRPC("""Invalid Request, "method" must be a string.""") + if "params" not in data: data["params"] = () + #convert params-keys from unicode to str + elif isinstance(data["params"], dict): + try: + data["params"] = dictkeyclean(data["params"]) + except UnicodeEncodeError: + raise RPCInvalidMethodParams("Parameter-names must be in ascii.") + elif not isinstance(data["params"], (list, tuple)): + raise RPCInvalidRPC("""Invalid Request, "params" must be an array or object.""") + if not( len(data)==3 or ("id" in data and len(data)==4) ): + raise RPCInvalidRPC("""Invalid Request, additional fields found.""") + + # notification / request + if "id" not in data: + return data["method"], data["params"] #notification + else: + return data["method"], data["params"], data["id"] #request + + def loads_response( self, string ): + """de-serialize a JSON-RPC Response/error + + :Returns: | [result, id] for Responses + :Raises: | RPCFault+derivates for error-packages/faults, RPCParseError, RPCInvalidRPC + """ + try: + if sys.version_info >= (3,0): + data = self.loads(string.decode("UTF-8")) + else: + data = self.loads(string) + except ValueError as err: + raise RPCParseError("No valid JSON. (%s)" % str(err)) + if not isinstance(data, dict): raise RPCInvalidRPC("No valid RPC-package.") + if "jsonrpc" not in data: raise RPCInvalidRPC("""Invalid Response, "jsonrpc" missing.""") + if sys.version_info >= (3,0): + if not isinstance(data["jsonrpc"], (str)): + raise RPCInvalidRPC("""Invalid Response, "jsonrpc" must be a string.""") + else: + if not isinstance(data["jsonrpc"], (str, unicode)): + raise RPCInvalidRPC("""Invalid Response, "jsonrpc" must be a string.""") + if data["jsonrpc"] != "2.0": raise RPCInvalidRPC("""Invalid jsonrpc version.""") + if "id" not in data: raise RPCInvalidRPC("""Invalid Response, "id" missing.""") + if "result" not in data: data["result"] = None + if "error" not in data: data["error"] = None + if len(data) != 4: raise RPCInvalidRPC("""Invalid Response, additional or missing fields.""") + + #error + if data["error"] is not None: + if data["result"] is not None: + raise RPCInvalidRPC("""Invalid Response, only "result" OR "error" allowed.""") + if not isinstance(data["error"], dict): raise RPCInvalidRPC("Invalid Response, invalid error-object.") + if "code" not in data["error"] or "message" not in data["error"]: + raise RPCInvalidRPC("Invalid Response, invalid error-object.") + if "data" not in data["error"]: data["error"]["data"] = None + if len(data["error"]) != 3: + raise RPCInvalidRPC("Invalid Response, invalid error-object.") + + error_data = data["error"]["data"] + if data["error"]["code"] == PARSE_ERROR: + raise RPCParseError(error_data) + elif data["error"]["code"] == INVALID_REQUEST: + raise RPCInvalidRPC(error_data) + elif data["error"]["code"] == METHOD_NOT_FOUND: + raise RPCMethodNotFound(error_data) + elif data["error"]["code"] == INVALID_METHOD_PARAMS: + raise RPCInvalidMethodParams(error_data) + elif data["error"]["code"] == INTERNAL_ERROR: + raise RPCInternalError(error_data) + elif data["error"]["code"] == PROCEDURE_EXCEPTION: + raise RPCProcedureException(error_data) + elif data["error"]["code"] == AUTHENTIFICATION_ERROR: + raise RPCAuthentificationError(error_data) + elif data["error"]["code"] == PERMISSION_DENIED: + raise RPCPermissionDenied(error_data) + elif data["error"]["code"] == INVALID_PARAM_VALUES: + raise RPCInvalidParamValues(error_data) + else: + raise RPCFault(data["error"]["code"], data["error"]["message"], error_data) + #result + else: + return data["result"], data["id"] + + +#========================================= +# transports + +#---------------------- +# transport-logging + +import codecs +import time + +def log_dummy( message ): + """dummy-logger: do nothing""" + pass +def log_stdout( message ): + """print message to STDOUT""" + print(message) + +def log_file( filename ): + """return a logfunc which logs to a file (in utf-8)""" + def logfile( message ): + f = codecs.open( filename, 'a', encoding='utf-8' ) + f.write( message+"\n" ) + f.close() + return logfile + +def log_filedate( filename ): + """return a logfunc which logs date+message to a file (in utf-8)""" + def logfile( message ): + f = codecs.open( filename, 'a', encoding='utf-8' ) + f.write( time.strftime("%Y-%m-%d %H:%M:%S ")+message+"\n" ) + f.close() + return logfile + +#---------------------- + +class Transport: + """generic Transport-interface. + + This class, and especially its methods and docstrings, + define the Transport-Interface. + """ + def __init__(self): + pass + + def send( self, data ): + """send all data. must be implemented by derived classes.""" + raise NotImplementedError + def recv( self ): + """receive data. must be implemented by derived classes.""" + raise NotImplementedError + + def sendrecv( self, string ): + """send + receive data""" + self.send( string ) + return self.recv() + def serve( self, handler, n=None ): + """serve (forever or for n communicaions). + + - receive data + - call result = handler(data) + - send back result if not None + + The serving can be stopped by SIGINT. + + :TODO: + - how to stop? + maybe use a .run-file, and stop server if file removed? + - maybe make n_current accessible? (e.g. for logging) + """ + n_current = 0 + while 1: + if n is not None and n_current >= n: + break + data = self.recv() + result = handler(data) + if result is not None: + self.send( result ) + n_current += 1 + + +class TransportSTDINOUT(Transport): + """receive from STDIN, send to STDOUT. + + Useful e.g. for debugging. + """ + def send(self, string): + """write data to STDOUT with '***SEND:' prefix """ + print("***SEND:") + print(string) + def recv(self): + """read data from STDIN""" + print("***RECV (please enter, ^D ends.):") + return sys.stdin.read() + + +import socket, select +class TransportSocket(Transport): + """Transport via socket. + + :SeeAlso: python-module socket + :TODO: + - documentation + - improve this (e.g. make sure that connections are closed, socket-files are deleted etc.) + - exception-handling? (socket.error) + """ + def __init__( self, addr, limit=4096, sock_type=socket.AF_INET, sock_prot=socket.SOCK_STREAM, timeout=1.0, logfunc=log_dummy ): + """ + :Parameters: + - addr: socket-address + - timeout: timeout in seconds + - logfunc: function for logging, logfunc(message) + :Raises: socket.timeout after timeout + """ + self.limit = limit + self.addr = addr + self.s_type = sock_type + self.s_prot = sock_prot + self.s = None + self.timeout = timeout + self.log = logfunc + self._send = self._send_2x; + if sys.version_info > (3,0): + self._send = self._send_3x; + + def _send_3x(self, conn, result): + conn.send( bytes(result, 'utf-8') ) + def _send_2x(self, conn, result): + conn.send( result ) + + def connect( self ): + self.close() + self.log( "connect to %s" % repr(self.addr) ) + self.s = socket.socket( self.s_type, self.s_prot ) + self.s.settimeout( self.timeout ) + self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.s.connect( self.addr ) + def close( self ): + if self.s is not None: + self.log( "close %s" % repr(self.addr) ) + self.s.close() + self.s = None + def __repr__(self): + return "" % repr(self.addr) + + def send( self, string ): + if self.s is None: + self.connect() + self.log( "--> "+repr(string) ) + self.s.sendall( string ) + def recv( self ): + if self.s is None: + self.connect() + data = self.s.recv( self.limit ) + while( select.select((self.s,), (), (), 0.1)[0] ): #TODO: this select is probably not necessary, because server closes this socket + d = self.s.recv( self.limit ) + if len(d) == 0: + break + data += d + self.log( "<-- "+repr(data) ) + return data + + def sendrecv( self, string ): + """send data + receive data + close""" + try: + self.send( string ) + return self.recv() + finally: + self.close() + def serve(self, handler, n=None): + """open socket, wait for incoming connections and handle them. + + :Parameters: + - n: serve n requests, None=forever + """ + self.close() + self.s = socket.socket( self.s_type, self.s_prot ) + self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + self.log( "listen %s" % repr(self.addr) ) + self.s.bind( self.addr ) + self.s.listen(1) + n_current = 0 + while 1: + if n is not None and n_current >= n: + break + conn, addr = self.s.accept() + self.log( "%s connected" % repr(addr) ) + data = conn.recv(self.limit) + self.log( "%s --> %s" % (repr(addr), repr(data)) ) + result = handler(data) + if data is not None: + self.log( "%s <-- %s" % (repr(addr), repr(result)) ) + self._send(conn, result) + self.log( "%s close" % repr(addr) ) + conn.close() + n_current += 1 + finally: + self.close() + + +if hasattr(socket, 'AF_UNIX'): + + class TransportUnixSocket(TransportSocket): + """Transport via Unix Domain Socket. + """ + def __init__(self, addr=None, limit=4096, timeout=1.0, logfunc=log_dummy): + """ + :Parameters: + - addr: "socket_file" + :Note: | The socket-file is not deleted. + | If the socket-file begins with \x00, abstract sockets are used, + and no socket-file is created. + :SeeAlso: TransportSocket + """ + TransportSocket.__init__( self, addr, limit, socket.AF_UNIX, socket.SOCK_STREAM, timeout, logfunc ) + +class TransportTcpIp(TransportSocket): + """Transport via TCP/IP. + """ + def __init__(self, addr=None, limit=4096, timeout=1.0, logfunc=log_dummy): + """ + :Parameters: + - addr: ("host",port) + :SeeAlso: TransportSocket + """ + TransportSocket.__init__( self, addr, limit, socket.AF_INET, socket.SOCK_STREAM, timeout, logfunc ) + + +#========================================= +# client side: server proxy + +class ServerProxy: + """RPC-client: server proxy + + A logical connection to a RPC server. + + It works with different data/serializers and different transports. + + Notifications and id-handling/multicall are not yet implemented. + + :Example: + see module-docstring + + :TODO: verbose/logging? + """ + def __init__( self, data_serializer, transport ): + """ + :Parameters: + - data_serializer: a data_structure+serializer-instance + - transport: a Transport instance + """ + #TODO: check parameters + self.__data_serializer = data_serializer + if not isinstance(transport, Transport): + raise ValueError('invalid "transport" (must be a Transport-instance)"') + self.__transport = transport + + def __str__(self): + return repr(self) + def __repr__(self): + return "" % (self.__transport, self.__data_serializer) + + def __req( self, methodname, args=None, kwargs=None, id=0 ): + # JSON-RPC 1.0: only positional parameters + if len(kwargs) > 0 and isinstance(self.data_serializer, JsonRpc10): + raise ValueError("Only positional parameters allowed in JSON-RPC 1.0") + # JSON-RPC 2.0: only args OR kwargs allowed! + if len(args) > 0 and len(kwargs) > 0: + raise ValueError("Only positional or named parameters are allowed!") + if len(kwargs) == 0: + req_str = self.__data_serializer.dumps_request( methodname, args, id ) + else: + req_str = self.__data_serializer.dumps_request( methodname, kwargs, id ) + + try: + if sys.version_info > (3,0): + resp_str = self.__transport.sendrecv( bytes(req_str, 'utf-8') ) + else: + resp_str = self.__transport.sendrecv( req_str ) + except Exception as err: + raise RPCTransportError(err) + resp = self.__data_serializer.loads_response( resp_str ) + return resp[0] + + def __getattr__(self, name): + # magic method dispatcher + # note: to call a remote object with an non-standard name, use + # result getattr(my_server_proxy, "strange-python-name")(args) + return _method(self.__req, name) + +# request dispatcher +class _method: + """some "magic" to bind an RPC method to an RPC server. + + Supports "nested" methods (e.g. examples.getStateName). + + :Raises: AttributeError for method-names/attributes beginning with '_'. + """ + def __init__(self, req, name): + if name[0] == "_": #prevent rpc-calls for proxy._*-functions + raise AttributeError("invalid attribute '%s'" % name) + self.__req = req + self.__name = name + def __getattr__(self, name): + if name[0] == "_": #prevent rpc-calls for proxy._*-functions + raise AttributeError("invalid attribute '%s'" % name) + return _method(self.__req, "%s.%s" % (self.__name, name)) + def __call__(self, *args, **kwargs): + return self.__req(self.__name, args, kwargs) + +#========================================= +# server side: Server + +class Server: + """RPC-server. + + It works with different data/serializers and + with different transports. + + :Example: + see module-docstring + + :TODO: + - mixed JSON-RPC 1.0/2.0 server? + - logging/loglevels? + """ + def __init__( self, data_serializer, transport, logfile=None ): + """ + :Parameters: + - data_serializer: a data_structure+serializer-instance + - transport: a Transport instance + - logfile: file to log ("unexpected") errors to + """ + #TODO: check parameters + self.__data_serializer = data_serializer + if not isinstance(transport, Transport): + raise ValueError('invalid "transport" (must be a Transport-instance)"') + self.__transport = transport + self.logfile = logfile + if self.logfile is not None: #create logfile (or raise exception) + f = codecs.open( self.logfile, 'a', encoding='utf-8' ) + f.close() + + self.funcs = {} + + def __repr__(self): + return "" % (self.__transport, self.__data_serializer) + + def log(self, message): + """write a message to the logfile (in utf-8)""" + if self.logfile is not None: + f = codecs.open( self.logfile, 'a', encoding='utf-8' ) + f.write( time.strftime("%Y-%m-%d %H:%M:%S ")+message+"\n" ) + f.close() + + def register_instance(self, myinst, name=None): + """Add all functions of a class-instance to the RPC-services. + + All entries of the instance which do not begin with '_' are added. + + :Parameters: + - myinst: class-instance containing the functions + - name: | hierarchical prefix. + | If omitted, the functions are added directly. + | If given, the functions are added as "name.function". + :TODO: + - only add functions and omit attributes? + - improve hierarchy? + """ + for e in dir(myinst): + if e[0][0] != "_": + if name is None: + self.register_function( getattr(myinst, e) ) + else: + self.register_function( getattr(myinst, e), name="%s.%s" % (name, e) ) + def register_function(self, function, name=None): + """Add a function to the RPC-services. + + :Parameters: + - function: function to add + - name: RPC-name for the function. If omitted/None, the original + name of the function is used. + """ + if name is None: + self.funcs[function.__name__] = function + else: + self.funcs[name] = function + + def handle(self, rpcstr): + """Handle a RPC-Request. + + :Parameters: + - rpcstr: the received rpc-string + :Returns: the data to send back or None if nothing should be sent back + :Raises: RPCFault (and maybe others) + """ + #TODO: id + notification = False + try: + req = None + if sys.version_info > (3,0): + req = self.__data_serializer.loads_request( rpcstr.decode("UTF-8") ) + else: + req = self.__data_serializer.loads_request( rpcstr ) + if len(req) == 2: #notification + method, params = req + notification = True + else: #request + method, params, id = req + except RPCFault as err: + return self.__data_serializer.dumps_error( err, id=None ) + except Exception as err: + self.log( "%d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err)) ) + return self.__data_serializer.dumps_error( RPCFault(INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR]), id=None ) + + if method not in self.funcs: + if notification: + return None + return self.__data_serializer.dumps_error( RPCFault(METHOD_NOT_FOUND, ERROR_MESSAGE[METHOD_NOT_FOUND]), id ) + + try: + if isinstance(params, dict): + result = self.funcs[method]( **params ) + else: + result = self.funcs[method]( *params ) + except RPCFault as err: + if notification: + return None + return self.__data_serializer.dumps_error( err, id=None ) + except Exception as err: + logging.getLogger('RPCLib').error("Error executing RPC: %s" % str(err)) + if notification: + return None + self.log( "%d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err)) ) + return self.__data_serializer.dumps_error( RPCFault(INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err)), id ) + if notification: + return None + try: + return self.__data_serializer.dumps_response( result, id ) + except Exception as err: + self.log( "%d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err)) ) + return self.__data_serializer.dumps_error( RPCFault(INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR]), id ) + + def serve(self, n=None): + """serve (forever or for n communicaions). + + :See: Transport + """ + self.__transport.serve( self.handle, n ) + +#========================================= + diff --git a/pyircbot/core/modulebase.py b/pyircbot/core/modulebase.py new file mode 100644 index 0000000..d8f309a --- /dev/null +++ b/pyircbot/core/modulebase.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +import logging +import os +import yaml + +class ModuleBase: + " All modules must extend this class. " + def __init__(self, bot, moduleName): + " Module name is passed from the actual module " + self.moduleName=moduleName + " Reference to the bot is saved" + self.bot = bot + " Hooks are provided requested by the actual module " + self.hooks=[] + " Services provided by the actual module " + self.services=[] + " Config is blank until the Module calls loadConfig " + self.config={} + " Set up logging for this module " + self.log = logging.getLogger("Module.%s" % self.moduleName) + self.log.info("Loaded module %s" % self.moduleName) + + + def loadConfig(self): + configPath = self.bot.getConfigPath(self.moduleName) + + if os.path.exists( configPath ): + self.config = yaml.load(open(configPath, 'r')) + + def ondisable(self): + pass + + def getConfigPath(self): + return self.bot.getConfigPath(self.moduleName) + + def getFilePath(self, f=None): + return self.bot.getDataPath(self.moduleName) + (f if f else '') + +class ModuleHook: + def __init__(self, hook, method): + self.hook=hook + self.method=method \ No newline at end of file diff --git a/pyircbot/core/pyircbot.py b/pyircbot/core/pyircbot.py new file mode 100644 index 0000000..eb23b79 --- /dev/null +++ b/pyircbot/core/pyircbot.py @@ -0,0 +1,454 @@ +#!/usr/bin/env python +import socket +import asynchat +import logging +import traceback +import time +import sys +from socket import SHUT_RDWR +from core.rpc import BotRPC + +try: + from cStringIO import StringIO +except: + from io import BytesIO as StringIO + +class PyIRCBot(asynchat.async_chat): + def __init__(self, coreconfig, botconfig): + asynchat.async_chat.__init__(self) + " logging " + self.log = logging.getLogger('PyIRCBot') + " config " + self.coreconfig = coreconfig + self.botconfig = botconfig + " rpc " + self.rpc = BotRPC(self) + + " stringio object as buffer " + self.buffer = StringIO() + " line terminator " + self.set_terminator(b"\r\n") + + " Setup hooks for modules " + self.initHooks() + " Load modules " + self.initModules() + + self._connect() + self.connected=False + + def kill(self): + " Close RPC Socket " + #try: + # self.rpc.server._Server__transport.shutdown(SHUT_RDWR) + #except Exception as e: + # self.log.error(str(e)) + try: + self.rpc.server._Server__transport.close() + except Exception as e: + self.log.error(str(e)) + + " Kill RPC thread " + self.rpc._stop() + + " Close all modules " + self.closeAllModules() + + " Net related code " + + def getBuf(self): + " return buffer and clear " + self.buffer.seek(0) + data = self.buffer.read() + self.buffer = StringIO() + return data + + def collect_incoming_data(self, data): + " Recieve data from stream, add to buffer " + self.log.debug("<< %(message)s", {"message":repr(data)}) + self.buffer.write(data) + + def found_terminator(self): + " A complete command was pushed through, so clear the buffer and process it." + self.process_data(self.getBuf().decode("UTF-8")) + + def handle_close(self): + " called on socket shutdown " + self.log.debug("handle_close") + self.connected=False + self.close() + + self.log.warning("Connection was lost.") + + #self.log.warning("Connection was lost. Reconnecting in 5 seconds.") + #time.sleep(5) + #self._connect() + + def handle_error(self, *args, **kwargs): + raise + + def _connect(self): + self.log.debug("Connecting to %(server)s:%(port)i", {"server":self.botconfig["connection"]["server"], "port":self.botconfig["connection"]["port"]}) + socket_type = socket.AF_INET + if self.botconfig["connection"]["ipv6"]: + self.log.info("IPv6 is enabled.") + socket_type = socket.AF_INET6 + socketInfo = socket.getaddrinfo(self.botconfig["connection"]["server"], self.botconfig["connection"]["port"], socket_type) + self.create_socket(socket_type, socket.SOCK_STREAM) + if "bindaddr" in self.botconfig["connection"]: + self.bind((self.botconfig["connection"]["bindaddr"], 0)) + self.connect(socketInfo[0][4]) + + def handle_connect(self): + " Called when the first packets come through, so we ident here " + self.connected=True + self.log.debug("handle_connect: setting USER and NICK") + self.fire_hook("_CONNECT") + self.log.debug("handle_connect: complete") + + def sendRaw(self, text): + if self.connected: + self.log.debug(">> "+text) + self.send( (text+"\r\n").encode("ascii")) + else: + self.log.warning("Send attempted while disconnected. >> "+text) + + def process_data(self, data): + " called per line of irc sent through " + if data.strip() == "": + return + + prefix = None + command = None + args=[] + trailing=None + + if data[0]==":": + prefix=data.split(" ")[0][1:] + data=data[data.find(" ")+1:] + command = data.split(" ")[0] + data=data[data.find(" ")+1:] + if(data[0]==":"): + # no args + trailing = data[1:].strip() + else: + trailing = data[data.find(" :")+2:].strip() + data = data[:data.find(" :")] + args = data.split(" ") + for index,arg in enumerate(args): + args[index]=arg.strip() + if not command in self.hookcalls: + self.log.warning("Unknown command: cmd='%s' prefix='%s' args='%s' trailing='%s'" % (command, prefix, args, trailing)) + else: + self.fire_hook(command, args=args, prefix=prefix, trailing=trailing) + + + + + + + " Module related code " + def initHooks(self): + self.hooks = [ + '_CONNECT', # Called when the bot connects to IRC on the socket level + 'NOTICE', # :irc.129irc.com NOTICE AUTH :*** Looking up your hostname... + 'MODE', # :CloneABCD MODE CloneABCD :+iwx + 'PING', # PING :irc.129irc.com + 'JOIN', # :CloneA!dave@hidden-B4F6B1AA.rit.edu JOIN :#clonea + 'QUIT', # :HCSMPBot!~HCSMPBot@108.170.48.18 QUIT :Quit: Disconnecting! + 'NICK', # :foxiAway!foxi@irc.hcsmp.com NICK :foxi + 'PART', # :CloneA!dave@hidden-B4F6B1AA.rit.edu PART #clonea + 'PRIVMSG', # :CloneA!dave@hidden-B4F6B1AA.rit.edu PRIVMSG #clonea :aaa + 'KICK', # :xMopxShell!~rduser@host KICK #xMopx2 xBotxShellTest :xBotxShellTest + 'INVITE', # :gmx!~gmxgeek@irc.hcsmp.com INVITE Tyrone :#hcsmp' + '001', # :irc.129irc.com 001 CloneABCD :Welcome to the 129irc IRC Network CloneABCD!CloneABCD@djptwc-laptop1.rit.edu + '002', # :irc.129irc.com 002 CloneABCD :Your host is irc.129irc.com, running version Unreal3.2.8.1 + '003', # :irc.129irc.com 003 CloneABCD :This server was created Mon Jul 19 2010 at 03:12:01 EDT + '004', # :irc.129irc.com 004 CloneABCD irc.129irc.com Unreal3.2.8.1 iowghraAsORTVSxNCWqBzvdHtGp lvhopsmntikrRcaqOALQbSeIKVfMCuzNTGj + '005', # :irc.129irc.com 005 CloneABCD CMDS=KNOCK,MAP,DCCALLOW,USERIP UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=10 CHANLIMIT=#:10 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 :are supported by this server + '250', # :chaos.esper.net 250 xBotxShellTest :Highest connection count: 1633 (1632 clients) (186588 connections received) + '251', # :irc.129irc.com 251 CloneABCD :There are 1 users and 48 invisible on 2 servers + '252', # :irc.129irc.com 252 CloneABCD 9 :operator(s) online + '254', # :irc.129irc.com 254 CloneABCD 6 :channels formed + '255', # :irc.129irc.com 255 CloneABCD :I have 42 clients and 1 servers + '265', # :irc.129irc.com 265 CloneABCD :Current Local Users: 42 Max: 47 + '266', # :irc.129irc.com 266 CloneABCD :Current Global Users: 49 Max: 53 + '332', # :chaos.esper.net 332 xBotxShellTest #xMopx2 :/ #XMOPX2 / https://code.google.com/p/pyircbot/ (Channel Topic) + '333', # :chaos.esper.net 333 xBotxShellTest #xMopx2 xMopxShell!~rduser@108.170.60.242 1344370109 + '353', # :irc.129irc.com 353 CloneABCD = #clonea :CloneABCD CloneABC + '366', # :irc.129irc.com 366 CloneABCD #clonea :End of /NAMES list. + '372', # :chaos.esper.net 372 xBotxShell :motd text here + '375', # :chaos.esper.net 375 xBotxShellTest :- chaos.esper.net Message of the Day - + '376', # :chaos.esper.net 376 xBotxShell :End of /MOTD command. + '422', # :irc.129irc.com 422 CloneABCD :MOTD File is missing + '433', # :nova.esper.net 433 * pyircbot3 :Nickname is already in use. + ] + " mapping of hooks to methods " + self.hookcalls = {} + for command in self.hooks: + self.hookcalls[command]=[] + + def fire_hook(self, command, args=None, prefix=None, trailing=None): + for hook in self.hookcalls[command]: + try: + hook(args, prefix, trailing) + except: + self.log.warning("Error processing hook: \n%s"% self.trace()) + + def initModules(self): + " load modules specified in config " + " storage of imported modules " + self.modules = {} + " instances of modules " + self.moduleInstances = {} + " append module location to path " + sys.path.append(self.coreconfig["moduledir"]) + " append bot directory to path " + sys.path.append(self.coreconfig["botdir"]+"core/") + + for modulename in self.botconfig["modules"]: + self.loadmodule(modulename) + + def importmodule(self, name): + " import a module by name " + " check if already exists " + if not name in self.modules: + " attempt to load " + try: + moduleref = __import__(name) + self.modules[name]=moduleref + return (True, None) + except Exception as e: + " on failure (usually syntax error in Module code) print an error " + self.log.error("Module %s failed to load: " % name) + self.log.error("Module load failure reason: " + str(e)) + return (False, str(e)) + else: + self.log.warning("Module %s already imported" % name) + return (False, "Module already imported") + + def deportmodule(self, name): + " remove a module from memory by name " + " unload if necessary " + if name in self.moduleInstances: + self.unloadmodule(name) + " delete all references to the module" + if name in self.modules: + item = self.modules[name] + del self.modules[name] + del item + " delete copy that python stores in sys.modules " + if name in sys.modules: + del sys.modules[name] + + def loadmodule(self, name): + " load a module and activate it " + " check if already loaded " + if name in self.moduleInstances: + self.log.warning( "Module %s already loaded" % name ) + return False + " check if needs to be imported, and verify it was " + if not name in self.modules: + importResult = self.importmodule(name) + if not importResult[0]: + return importResult + " init the module " + self.moduleInstances[name] = getattr(self.modules[name], name)(self, name) + " load hooks " + self.loadModuleHooks(self.moduleInstances[name]) + + def unloadmodule(self, name): + " unload a module by name " + if name in self.moduleInstances: + " notify the module of disabling " + self.moduleInstances[name].ondisable() + " unload all hooks " + self.unloadModuleHooks(self.moduleInstances[name]) + " remove the instance " + item = self.moduleInstances.pop(name) + " delete the instance" + del item + self.log.info( "Module %s unloaded" % name ) + return (True, None) + else: + self.log.info("Module %s not loaded" % name) + return (False, "Module not loaded") + + def reloadmodule(self, name): + " unload then load a module by name " + " make sure it's imporeted" + if name in self.modules: + " remember if it was loaded before" + loadedbefore = name in self.moduleInstances + self.log.info("Reloading %s" % self.modules[name]) + " unload " + self.unloadmodule(name) + " load " + if loadedbefore: + self.loadmodule(name) + return (True, None) + return (False, "Module is not loaded") + + def redomodule(self, name): + " reload a modules code from disk " + " remember if it was loaded before " + loadedbefore = name in self.moduleInstances + " unload/deport " + self.deportmodule(name) + " import " + importResult = self.importmodule(name) + if not importResult[0]: + return importResult + " load " + if loadedbefore: + self.loadmodule(name) + return (True, None) + + def loadModuleHooks(self, module): + " activate a module's hooks " + for hook in module.hooks: + self.addHook(hook.hook, hook.method) + + def unloadModuleHooks(self, module): + " remove a modules hooks " + for hook in module.hooks: + self.removeHook(hook.hook, hook.method) + + def addHook(self, command, method): + " add a single hook " + if command in self.hooks: + self.hookcalls[command].append(method) + else: + self.log.warning("Invalid hook - %s" % command) + return False + + def removeHook(self, command, method): + " remove a single hook " + if command in self.hooks: + for hookedMethod in self.hookcalls[command]: + if hookedMethod == method: + self.hookcalls[command].remove(hookedMethod) + else: + self.log.warning("Invalid hook - %s" % command) + return False + + def getmodulebyname(self, name): + " return a module specified by the name " + if not name in self.moduleInstances: + return None + return self.moduleInstances[name] + + def getmodulesbyservice(self, service): + " get a list of modules that provide the specified service " + validModules = [] + for module in self.moduleInstances: + if service in self.moduleInstances[module].services: + validModules.append(self.moduleInstances[module]) + return validModules + + def getBestModuleForService(self, service): + m = self.getmodulesbyservice(service) + if len(m)>0: + return m[0] + return None + + def closeAllModules(self): + " Deport all modules (for shutdown). Modules are unloaded in the opposite order listed in the config. " + loaded = list(self.moduleInstances.keys()) + loadOrder = self.botconfig["modules"] + loadOrder.reverse() + for key in loadOrder: + if key in loaded: + loaded.remove(key) + self.deportmodule(key) + for key in loaded: + self.deportmodule(key) + + " Filesystem Methods " + def getDataPath(self, moduleName): + return "%s/data/%s/" % (self.botconfig["bot"]["datadir"], moduleName) + + def getConfigPath(self, moduleName): + return "%s/config/%s.yml" % (self.botconfig["bot"]["datadir"], moduleName) + + " Utility methods " + @staticmethod + def decodePrefix(prefix): + " Returns an object with nick, username, hostname attributes" + if "!" in prefix: + ob = type('UserPrefix', (object,), {}) + ob.nick, prefix = prefix.split("!") + ob.username, ob.hostname = prefix.split("@") + return ob + else: + ob = type('ServerPrefix', (object,), {}) + ob.hostname = prefix + return ob + + @staticmethod + def trace(): + return traceback.format_exc() + + @staticmethod + def messageHasCommand(command, message, requireArgs=False): + # Check if the message at least starts with the command + messageBeginning = message[0:len(command)] + if messageBeginning!=command: + return False + # Make sure it's not a subset of a longer command (ie .meme being set off by .memes) + subsetCheck = message[len(command):len(command)+1] + if subsetCheck!=" " and subsetCheck!="": + return False + + # We've got the command! Do we need args? + argsStart = len(command) + args = "" + if argsStart > 0: + args = message[argsStart+1:] + + if requireArgs and args.strip() == '': + return False + + # Verified! Return the set. + ob = type('ParsedCommand', (object,), {}) + ob.command = command + ob.args = [] if args=="" else args.split(" ") + ob.args_str = args + ob.message = message + return ob + # return (True, command, args, message) + + + " Data Methods " + def get_nick(self): + return self.config["nick"] + + + " Action Methods " + def act_PONG(self, data): + self.sendRaw("PONG :%s" % data) + + def act_USER(self, username, hostname, realname): + self.sendRaw("USER %s %s %s :%s" % (username, hostname, self.botconfig["connection"]["server"], realname)) + + def act_NICK(self, newNick): + self.sendRaw("NICK %s" % newNick) + + def act_JOIN(self, channel): + self.sendRaw("JOIN %s"%channel) + + def act_PRIVMSG(self, towho, message): + self.sendRaw("PRIVMSG %s :%s"%(towho,message)) + + def act_MODE(self, channel, mode, extra=None): + if extra != None: + self.sendRaw("MODE %s %s %s" % (channel,mode,extra)) + else: + self.sendRaw("MODE %s %s" % (channel,mode)) + + def act_ACTION(self, channel, action): + self.sendRaw("PRIVMSG %s :\x01ACTION %s"%(channel,action)) + + def act_KICK(self, channel, who, comment): + self.sendRaw("KICK %s %s :%s" % (channel, who, comment)) + diff --git a/pyircbot/core/rpc.py b/pyircbot/core/rpc.py new file mode 100644 index 0000000..258250f --- /dev/null +++ b/pyircbot/core/rpc.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +import traceback +import logging +from core import jsonrpc +from threading import Thread + +class BotRPC(Thread): + def __init__(self, main): + Thread.__init__(self) + self.bot = main + self.log = logging.getLogger('RPC') + self.server = jsonrpc.Server(jsonrpc.JsonRpc20(), jsonrpc.TransportTcpIp(addr=(self.bot.botconfig["bot"]["rpcbind"], self.bot.botconfig["bot"]["rpcport"]))) + + self.server.register_function( self.importModule ) + self.server.register_function( self.deportModule ) + self.server.register_function( self.loadModule ) + self.server.register_function( self.unloadModule ) + self.server.register_function( self.reloadModule ) + self.server.register_function( self.redoModule ) + self.server.register_function( self.getTraceback ) + self.start() + + def run(self): + self.server.serve() + + + def importModule(self, moduleName): + return self.bot.importmodule(moduleName) + + def deportModule(self, moduleName): + self.bot.deportmodule(moduleName) + + def loadModule(self, moduleName): + return self.bot.loadmodule(moduleName) + + def unloadModule(self, moduleName): + self.bot.unloadmodule(moduleName) + + def reloadModule(self, moduleName): + self.bot.unloadmodule(moduleName) + return self.bot.loadmodule(moduleName) + + def redoModule(self, moduleName): + return self.bot.redomodule(moduleName) + + def getTraceback(self): + tb = str(traceback.format_exc()) + print(tb) + return tb + + + diff --git a/pyircbot/main.py b/pyircbot/main.py new file mode 100644 index 0000000..13b4c21 --- /dev/null +++ b/pyircbot/main.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +import os +import sys +import logging +import yaml +import asyncore +from optparse import OptionParser +from core.pyircbot import PyIRCBot + +if __name__ == "__main__": + " logging level and facility " + logging.basicConfig(level=logging.DEBUG, format="%(asctime)-15s %(levelname)-8s %(message)s") + log = logging.getLogger('main') + + " parse command line args " + parser = OptionParser() + parser.add_option("-c", "--config", action="store", type="string", dest="config", help="Path to core config file") + parser.add_option("-b", "--bot", action="store", type="string", dest="bot", help="Path to bot config file") + + (options, args) = parser.parse_args() + + log.debug(options) + + if not options.config: + log.critical("No core config file specified (-c). Exiting.") + sys.exit(0) + if not options.bot: + log.critical("No bot config file specified (-b). Exiting.") + sys.exit(0) + + coreconfig = yaml.load(open(options.config, 'r')) + botconfig = yaml.load(open(options.bot, 'r')) + + log.debug(coreconfig) + log.debug(botconfig) + + bot = PyIRCBot(coreconfig, botconfig) + try: + asyncore.loop() + except KeyboardInterrupt: + bot.kill() + diff --git a/pyircbot/modules/AttributeStorage.py b/pyircbot/modules/AttributeStorage.py new file mode 100644 index 0000000..cd4b671 --- /dev/null +++ b/pyircbot/modules/AttributeStorage.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +from modulebase import ModuleBase,ModuleHook + +class AttributeStorage(ModuleBase): + def __init__(self, bot, moduleName): + ModuleBase.__init__(self, bot, moduleName); + self.hooks=[] + self.services=["attributes"] + self.db = None + serviceProviders = self.bot.getmodulesbyservice("mysql") + if len(serviceProviders)==0: + self.log.error("AttributeStorage: Could not find a valid mysql service provider") + else: + self.log.info("AttributeStorage: Selecting mysql service provider: %s" % serviceProviders[0]) + self.db = serviceProviders[0] + + if not self.db.connection.tableExists("attribute"): + self.log.info("AttributeStorage: Creating table: attribute") + c = self.db.connection.query("""CREATE TABLE IF NOT EXISTS `attribute` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `attribute` varchar(128) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `attribute` (`attribute`) + ) ENGINE=InnoDB DEFAULT CHARSET=latin1 ;""") + c.close() + + if not self.db.connection.tableExists("items"): + self.log.info("AttributeStorage: Creating table: items") + c = self.db.connection.query("""CREATE TABLE IF NOT EXISTS `items` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `item` varchar(512) CHARACTER SET utf8 NOT NULL, + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=latin1 ;""") + c.close() + + if not self.db.connection.tableExists("values"): + self.log.info("AttributeStorage: Creating table: values") + c = self.db.connection.query("""CREATE TABLE IF NOT EXISTS `values` ( + `itemid` int(11) NOT NULL, + `attributeid` int(11) NOT NULL, + `value` varchar(512) CHARACTER SET utf8 NOT NULL, + PRIMARY KEY (`itemid`,`attributeid`) + ) ENGINE=InnoDB DEFAULT CHARSET=latin1 ;""") + c.close() + + # self.getItem('xMopxShell', 'name') + # self.getAttribute('xMopxShell', 'name') + # self.setAttribute('xMopxShell', 'name', 'dave') + + + def getItem(self, name): + c = self.db.connection.query("""SELECT + `i`.`id`, + `i`.`item`, + `a`.`attribute`, + `v`.`value` + FROM + `items` `i` + INNER JOIN `values` `v` + on `v`.`itemid`=`i`.`id` + INNER JOIN `attribute` `a` + on `a`.`id`=`v`.`attributeid` + + WHERE + `i`.`item`=%s;""", + (name,) + ) + item = {} + while True: + row = c.fetchone() + if row == None: + break + item[row["attribute"]]=row["value"] + c.close() + + if len(item)==0: + return {} + return item + + def getAttribute(self, item, attribute): + c = self.db.connection.query("""SELECT + `i`.`id`, + `i`.`item`, + `a`.`attribute`, + `v`.`value` + FROM + `items` `i` + INNER JOIN `values` `v` + on `v`.`itemid`=`i`.`id` + INNER JOIN `attribute` `a` + on `a`.`id`=`v`.`attributeid` + + WHERE + `i`.`item`=%s + AND + `a`.`attribute`=%s;""", + (item,attribute) + ) + row = c.fetchone() + c.close() + if row == None: + return None + return row["value"] + + def setAttribute(self, item, attribute, value): + item = item.lower() + attribute = attribute.lower() + + # Check attribute exists + c = self.db.connection.query("SELECT `id` FROM `attribute` WHERE `attribute`=%s;", (attribute)) + row = c.fetchone() + attributeId = -1 + if row == None: + c = self.db.connection.query("INSERT INTO `attribute` (`attribute`) VALUES (%s);", (attribute)) + attributeId = c.lastrowid + else: + attributeId = row["id"] + c.close() + + # check item exists + c = self.db.connection.query("SELECT `id` FROM `items` WHERE `item`=%s;", (item)) + row = c.fetchone() + itemId = -1 + if row == None: + c = self.db.connection.query("INSERT INTO `items` (`item`) VALUES (%s);", (item)) + itemId = c.lastrowid + else: + itemId = row["id"] + c.close() + + if value == None: + # delete it + c = self.db.connection.query("DELETE FROM `values` WHERE `itemid`=%s AND `attributeid`=%s ;", (itemId, attributeId)) + self.log.debug("AttributeStorage: Stored item %s attribute %s value: %s (Deleted)" % (itemId, attributeId, value)) + else: + # add attribute + c = self.db.connection.query("REPLACE INTO `values` (`itemid`, `attributeid`, `value`) VALUES (%s, %s, %s);", (itemId, attributeId, value)) + self.log.debug("AttributeStorage: Stored item %s attribute %s value: %s" % (itemId, attributeId, value)) + c.close() diff --git a/pyircbot/modules/DogeDice.py b/pyircbot/modules/DogeDice.py new file mode 100644 index 0000000..80cc7a5 --- /dev/null +++ b/pyircbot/modules/DogeDice.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python +from modulebase import ModuleBase,ModuleHook +import random +import yaml +import os +import time +import math +import hashlib +from threading import Timer + +class DogeDice(ModuleBase): + def __init__(self, bot, moduleName): + ModuleBase.__init__(self, bot, moduleName); + self.hooks=[ModuleHook("PRIVMSG", self.gotMsg)] + self.loadConfig() + # Load attribute storage + self.attr = self.bot.getBestModuleForService("attributes") + # Load doge RPC + self.doge = self.bot.getBestModuleForService("dogerpc") + # Dict of #channel -> game object + self.games = {} + + def gotMsg(self, args, prefix, trailing): + prefixObj = self.bot.decodePrefix(prefix) + # Ignore messages from users not logged in + loggedinfrom = self.attr.getAttribute(prefixObj.nick, "loggedinfrom") + if loggedinfrom==None: + # Send them a hint? + return + elif prefixObj.hostname == loggedinfrom: + if args[0][0] == "#": + # create a blank game obj if there isn't one (and whitelisted ? ) + if not args[0] in self.games and (not self.config["channelWhitelistOn"] or (self.config["channelWhitelistOn"] and args[0][1:] in self.config["channelWhitelist"]) ): + self.games[args[0]]=gameObj(self, args[0]) + # Channel message + self.games[args[0]].gotMsg(args, prefix, trailing) + else: + # Private message + #self.games[args[0].gotPrivMsg(args, prefix, trailing) + pass + else: + # Ignore potential spoofing + pass + + def removeGame(self, channel): + del self.games[channel] + + def ondisable(self): + self.log.info("DogeDice: Unload requested, ending games...") + while len(self.games)>0: + first = list(self.games.keys())[0] + self.games[first].gameover() + +class gameObj: + def __init__(self, master, channel): + self.master = master + self.channel = channel + # Game state + # 0 = waiting for players + # - advertise self? + # - players must be registered and have enough doge for current bet + # 1 = enough players, countdown + # - Last warning to pull out + # 2 = locked in / game setup + # - Move doge from player's wallets to table wallet. kick players if they can't afford + # 3 = start of a round + # - Each player's turn to roll + # 4 = determine winner, move doge + # - if > 10 doge, house fee? + self.step = 0 + + # Bet amount + self.bet = 0.0 + # players list + self.players = [] + # min players + self.minPlayers = 2 + # max players + self.maxPlayers = 4 + # Lobby countdown timer + self.startCountdownTimer = None + # pre-result timer + self.endgameResultTimer = None + # in-game timeout + self.playTimeout = None + # Wallet for this game + self.walletName = None + + def getPlayer(self, nick): + for player in self.players: + if player.nick == nick: + return player + return None + + def gotPrivMsg(self, args, prefix, trailing): + prefix = self.master.bot.decodePrefix(prefix) + pass + + def gotMsg(self, args, prefix, trailing): + prefix = self.master.bot.decodePrefix(prefix) + if self.step == 0 or self.step == 1: + # Join game + cmd = self.master.bot.messageHasCommand(".join", trailing) + if cmd: + if len(self.players)-1 < self.maxPlayers: + if self.getPlayer(prefix.nick)==None: + userWallet = self.master.attr.getAttribute(prefix.nick, "dogeaccountname") + if userWallet == None: + self.master.bot.act_PRIVMSG(self.channel, "%s: You don't have enough DOGE!" % (prefix.nick)) + return + balance = self.master.doge.getBal(userWallet) + + # check if the room is 'opened' already: + if len(self.players)==0: + # require an amount + if len(cmd.args)==1: + # Check if they have enough coins + try: + bet = float(cmd.args[0]) + except: + return + + if bet < self.master.config["minBet"]: + self.master.bot.act_PRIVMSG(self.channel, "%s: Minimum bet is %s DOGE!" % (prefix.nick, self.master.config["minBet"])) + return + + if balance>=bet: + newPlayer = playerObj(self, prefix.nick) + newPlayer.dogeWalletName = userWallet + self.players.append(newPlayer) + self.bet = bet + self.master.bot.act_PRIVMSG(self.channel, "%s: You have joined!" % (prefix.nick)) + else: + self.master.bot.act_PRIVMSG(self.channel, "%s: You don't have enough DOGE!" % (prefix.nick)) + else: + self.master.bot.act_PRIVMSG(self.channel, "%s: You need to specify a bet amount: .join 10" % (prefix.nick)) + else: + # no amount required + if balance>=self.bet: + newPlayer = playerObj(self, prefix.nick) + newPlayer.dogeWalletName = userWallet + self.players.append(newPlayer) + self.master.bot.act_PRIVMSG(self.channel, "%s: You have joined!" % (prefix.nick)) + if self.canStart() and self.startCountdownTimer == None: + self.initStartCountdown() + self.master.bot.act_PRIVMSG(self.channel, "The game will start in %s seconds! Bet is %s DOGE each!" % (self.master.config["lobbyIdleSeconds"], self.bet)) + else: + self.master.bot.act_PRIVMSG(self.channel, "%s: You don't have enough DOGE!" % (prefix.nick)) + + else: + self.master.bot.act_PRIVMSG(self.channel, "%s: you're already in the game. Quit with .leave" % (prefix.nick)) + else: + self.master.bot.act_PRIVMSG(self.channel, "%s: the game is full (%s/%)! Cannot join." % (prefix.nick, len(self.players), self.maxPlayers)) + # Leave game + cmd = self.master.bot.messageHasCommand(".leave", trailing) + if cmd: + if self.getPlayer(prefix.nick)==None: + self.master.bot.act_PRIVMSG(self.channel, "%s: You're not in the game." % (prefix.nick)) + else: + self.removePlayer(prefix.nick) + self.master.bot.act_PRIVMSG(self.channel, "%s: You have left the game!" % (prefix.nick)) + if not self.canStart() and self.startCountdownTimer: + self.clearTimer(self.startCountdownTimer) + self.startCountdownTimer = None + self.master.bot.act_PRIVMSG(self.channel, "Game start aborted." ) + self.step = 0 + elif self.step == 2: + pass + elif self.step == 3: + # Ignore cmds from people outside the game + player = self.getPlayer(prefix.nick) + if not player: + return + + # handle a .roll + cmd = self.master.bot.messageHasCommand(".roll", trailing) + if cmd and not player.hasRolled: + roll1 = random.randint(1,6) + roll2 = random.randint(1,6) + self.master.bot.act_PRIVMSG(self.channel, "%s rolls %s and %s!" % (prefix.nick, roll1, roll2)) + player.hasRolled = True + player.rollValue = roll1+roll2 + + # Check if all players have rolled + for player in self.players: + if not player.hasRolled: + return + + # start endgame timer + self.step = 4 + self.endgameResultTimer = Timer(2, self.endgameResults) + self.endgameResultTimer.start() + + elif self.step == 4: + pass + + #senderIsOp = self.master.attr.getAttribute(prefix.nick, "op")=="yes" + def clearTimer(self, timer): + if timer: + timer.cancel() + timer = None + + def removePlayer(self, playerNick): + pos = -1 + for i in range(0, len(self.players)): + if self.players[i].nick == playerNick: + pos = i + break + if pos >= 0: + self.players.pop(pos) + + def canStart(self): + # Return true if the step is 'lobby' mode and player count is OK + return self.step == 0 and len(self.players)>=self.minPlayers + def initStartCountdown(self): + # Start the game-start countdown + self.startCountdownTimer = Timer(self.master.config["lobbyIdleSeconds"], self.lobbyCountdownDone) + self.startCountdownTimer.start() + self.step = 1 + + def lobbyCountdownDone(self): + self.step = 2 + self.master.bot.act_PRIVMSG(self.channel, "Collecting DOGE and starting game.. Type .roll !") + # Make a wallet for this game + self.walletName = "DogeDice-"+self.channel + # Generate an address to 'create' a wallet + self.master.doge.getAcctAddr(self.walletName) + + # Verify and move funds from each player + for player in self.players: + playerBalance = self.master.doge.getAcctBal(player.dogeWalletName) + if playerBalance < self.bet: + self.master.bot.act_PRIVMSG(self.channel, "%s was dropped from the game!") + self.removePlayer(player.nick) + + if len(self.players) <= 1: + self.master.bot.act_PRIVMSG(self.channel, "1 or players left - game over!") + self.resetGame() + return + + # Take doges + for player in self.players: + self.master.doge.move(player.dogeWalletName, self.walletName, self.bet) + + # Pre-game setup (nothing !) + + # Accept game commands + self.step = 3 + + # Start play timeout + self.playTimeout = Timer(30, self.gamePlayTimeoutExpired) + self.playTimeout.start() + + def gamePlayTimeoutExpired(self): + # Time out - return doges + self.master.bot.act_PRIVMSG(self.channel, "Time expired! Returning all doges.") + if self.step == 3: + # In game step. Refund doges + for player in self.players: + self.master.doge.move(self.walletName, player.dogeWalletName, self.bet) + self.resetGame() + + def endgameResults(self): + maxRollNames = [] + maxRollValue = 0 + for player in self.players: + if player.rollValue > maxRollValue: + maxRollNames = [] + maxRollNames.append(player.nick) + maxRollValue = player.rollValue + if player.rollValue == maxRollValue: + if not player.nick in maxRollNames: + maxRollNames.append(player.nick) + + pot = self.master.doge.getAcctBal(self.walletName) + DOGEeachDec = pot/len(maxRollNames) + DOGEeach = math.floor(DOGEeachDec*100000000) / 100000000 + + if len(maxRollNames)==1: + self.master.bot.act_PRIVMSG(self.channel, "We have a winner - %s! Winnings are: %s DOGE" % (maxRollNames[0], DOGEeach)) + else: + self.master.bot.act_PRIVMSG(self.channel, "We have a tie between %s - The take is %s DOGE each" % (' and '.join(maxRollNames), DOGEeach)) + + # Pay out + for nick in maxRollNames: + player = self.getPlayer(nick) + self.master.doge.move(self.walletName, player.dogeWalletName, DOGEeach) + + # the end! + self.resetGame() + + def resetGame(self): + self.clearTimer(self.startCountdownTimer) + self.startCountdownTimer = None + self.clearTimer(self.endgameResultTimer) + self.endgameResultTimer = None + self.clearTimer(self.playTimeout) + self.playTimeout = None + self.master.removeGame(self.channel) + + def gameover(self): + self.gamePlayTimeoutExpired() + + +class playerObj: + def __init__(self, game, nick): + self.game = game + self.nick = nick + # Save the player's wallet name + self.dogeWalletName = None + # Set to true after they roll + self.hasRolled = False + # Sum of their dice + self.rollValue = None + diff --git a/pyircbot/modules/DogeRPC.py b/pyircbot/modules/DogeRPC.py new file mode 100644 index 0000000..16e4512 --- /dev/null +++ b/pyircbot/modules/DogeRPC.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +from modulebase import ModuleBase,ModuleHook +from bitcoinrpc.authproxy import AuthServiceProxy + +class DogeRPC(ModuleBase): + def __init__(self, bot, moduleName): + ModuleBase.__init__(self, bot, moduleName); + self.hooks=[] + self.services=["dogerpc"] + self.loadConfig() + self.rpc = DogeController(self) + + def getBal(self, acct): + " get a balance of an address or an account " + return self.getAcctBal(acct) + + def getAcctAddr(self, acct): + " returns the address for an account. creates if necessary " + self.rpc.ping() + addrs = self.rpc.con.getaddressesbyaccount(acct) + if len(addrs)==0: + return self.rpc.con.getnewaddress(acct) + return addrs[0] + + def getAcctBal(self, acct): + " returns an account's balance" + self.rpc.ping() + return float(self.rpc.con.getbalance(acct)) + + def canMove(self, fromAcct, toAcct, amount): + " true or false if fromAcct can afford to give toAcct an amount of coins " + balfrom = self.getAcctBal(fromAcct) + return balfrom >= amount + + def move(self, fromAcct, toAcct, amount): + " move coins from one account to another " + self.rpc.ping() + if self.canMove(fromAcct, toAcct, amount): + return self.rpc.con.move(fromAcct, toAcct, amount) + return False + + def send(self, fromAcct, toAddr, amount): + " send coins to an external addr " + self.rpc.ping() + if self.canMove(fromAcct, toAddr, amount): + return self.rpc.con.sendfrom(fromAcct, toAddr, amount) + return False + +class DogeController: + def __init__(self, master): + self.config = master.config + self.log = master.log + self.con = None + self.ping() + + def ping(self): + try: + self.con.getinfo() + except: + self.connect() + + def connect(self): + self.log.debug("DogeRPC: Connecting to dogecoind") + self.con = AuthServiceProxy("http://%s:%s@%s:%s" % (self.config["username"], self.config["password"], self.config["host"], self.config["port"])) + self.con.getinfo() + self.log.debug("DogeRPC: Connected to %s:%s" % (self.config["host"], self.config["port"])) diff --git a/pyircbot/modules/DogeScramble.py b/pyircbot/modules/DogeScramble.py new file mode 100644 index 0000000..44c923f --- /dev/null +++ b/pyircbot/modules/DogeScramble.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python +from modulebase import ModuleBase,ModuleHook +import random +import yaml +import os +import time +from threading import Timer + +class DogeScramble(ModuleBase): + def __init__(self, bot, moduleName): + ModuleBase.__init__(self, bot, moduleName); + self.hooks=[ModuleHook("PRIVMSG", self.scramble)] + self.loadConfig() + + # Load attribute storage + self.attr = None + serviceProviders = self.bot.getmodulesbyservice("attributes") + if len(serviceProviders)==0: + self.log.error("DogeScramble: Could not find a valid attributes service provider") + else: + self.log.info("DogeScramble: Selecting attributes service provider: %s" % serviceProviders[0]) + self.attr = serviceProviders[0] + + # Load doge RPC + self.doge = self.bot.getBestModuleForService("dogerpc") + + # Per channel games + self.games = {} + + def scramble(self, args, prefix, trailing): + channel = args[0] + if channel[0] == "#": + # Ignore messages from users without a dogewallet password + prefixObj = self.bot.decodePrefix(prefix) + if self.attr.getAttribute(prefixObj.nick, "password")==None: + return + if not channel in self.games: + self.games[channel]=scrambleGame(self, channel) + self.games[channel].scramble(args, prefix, trailing) + def ondisable(self): + self.log.info("DogeScramble: Unload requested, ending games...") + for game in self.games: + self.games[game].gameover() + +class scrambleGame: + def __init__(self, master, channel): + self.master = master + self.channel = channel + # Running? + self.running = False + # Current word + self.currentWord = None + # Current word, scrambled + self.scrambled = None + # Online? + self.scrambleOn = False + # Count down to hints + self.hintTimer = None + # of hints given + self.hintsGiven = 0 + # Cooldown between words + self.nextTimer = None + # How many guesses submitted this round + self.guesses = 0; + # How many games in a row where nobody guessed + self.gamesWithoutGuesses = 0; + # What file are we using + self.category_file = None; + # How many words in this category have been used? + self.category_count = 0 + # How long between categories + self.change_category_after_words = self.master.config["categoryduration"] + # Should we change categories at the next pick? + self.should_change_category = True + # Holds the processed category name + self.category_name = None + # list of last picked words + self.lastwords = [] + # name of last winner for decreasing return + self.lastwinner = None + self.lastwinvalue = 0 + + self.delayHint = self.master.config["hintDelay"]; + self.delayNext = self.master.config["delayNext"]; + self.maxHints = self.master.config["maxHints"]; + self.abortAfterNoGuesses = self.master.config["abortAfterNoGuesses"]; + + def gameover(self): + self.clearTimers(); + self.running = False + def clearTimers(self): + self.clearTimer(self.nextTimer) + self.clearTimer(self.hintTimer) + def clearTimer(self, timer): + if timer: + timer.cancel() + def scramble(self, args, prefix, trailing): + prefix = self.master.bot.decodePrefix(prefix) + sender = prefix.nick + + senderIsOp = self.master.attr.getAttribute(prefix.nick, "op")=="yes" + + cmd = self.master.bot.messageHasCommand(".scramble", trailing) + if cmd and not self.running: + #and senderIsOp + self.running = True + self.startScramble() + return + cmd = self.master.bot.messageHasCommand(".scrambleoff", trailing) + if cmd and senderIsOp and self.running: + self.gameover() + self.running = False + return + + if self.currentWord and trailing.strip().lower() == self.currentWord: + # Get winner withdraw address + useraddr = self.master.attr.getAttribute(prefix.nick, "dogeaddr") + userwallet = self.master.attr.getAttribute(prefix.nick, "dogeaccountname") + + self.master.bot.act_PRIVMSG(self.channel, "%s got the word - %s!" % (sender, self.currentWord)) + + if not useraddr: + self.master.bot.act_PRIVMSG(self.channel, "%s: to win DOGE, you must set an wallet address by PMing me \".setdogeaddr\". Next word in %s seconds." % (prefix.nick, self.delayNext)) + else: + winamount = float(self.master.config["winAmount"]) + if self.lastwinner == prefix.nick: + winamount = self.lastwinvalue * self.master.config["decreaseFactor"] + self.lastwinvalue = winamount + self.lastwinner = prefix.nick + + self.master.bot.act_PRIVMSG(self.channel, "%s won %s DOGE! Next word in %s seconds." % (prefix.nick, round(winamount, 8), self.delayNext)) + self.master.doge.move('', userwallet, winamount) + + self.currentWord = None + self.clearTimers() + self.hintsGiven = 0 + self.nextTimer = Timer(self.delayNext, self.startNewWord) + self.nextTimer.start() + self.guesses=0 + self.category_count+=1 + self.master.log.debug("DogeScramble: category_count is: %s" % (self.category_count)) + if self.category_count >= self.change_category_after_words: + self.should_change_category = True + else: + self.guesses+=1 + + def startScramble(self): + self.clearTimer(self.nextTimer) + self.nextTimer = Timer(0, self.startNewWord) + self.nextTimer.start() + + def startNewWord(self): + self.currentWord = self.pickWord() + self.scrambled = self.scrambleWord(self.currentWord) + self.master.bot.act_PRIVMSG(self.channel, "[Category: %s] Unscramble this: %s " % (self.category_name, self.scrambled)) + + self.clearTimer(self.hintTimer) + self.hintTimer = Timer(self.delayHint, self.giveHint) + self.hintTimer.start() + + def giveHint(self): + self.hintsGiven+=1 + + if self.hintsGiven>=len(self.currentWord) or self.hintsGiven > self.maxHints: + self.abortWord() + return + + blanks = "" + for letter in list(self.currentWord): + if letter == " ": + blanks+=" " + else: + blanks+="_" + partFromWord = self.currentWord[0:self.hintsGiven] + partFromBlanks = blanks[self.hintsGiven:] + hintstr = partFromWord+partFromBlanks + + self.master.bot.act_PRIVMSG(self.channel, "Hint: - %s" % (hintstr)) + + self.clearTimer(self.hintTimer) + self.hintTimer = Timer(self.delayHint, self.giveHint) + self.hintTimer.start() + + def abortWord(self): + cur = self.currentWord + self.currentWord = None + self.master.bot.act_PRIVMSG(self.channel, "Word expired - the answer was '%s'. Next word in %s seconds." % (cur, self.delayNext)) + self.hintsGiven = 0 + self.clearTimer(self.nextTimer) + + if self.guesses==0: + self.gamesWithoutGuesses+=1 + if self.gamesWithoutGuesses >= self.abortAfterNoGuesses: + self.master.bot.act_PRIVMSG(self.channel, "No one seems to be playing - type .scramble to start again.") + self.gameover() + return + else: + self.gamesWithoutGuesses=0 + + self.nextTimer = Timer(self.delayNext, self.startNewWord) + self.nextTimer.start() + + def catFileNameToStr(self, s): + s=s.split(".")[0] + s=s.replace("_", " ") + return s.title() + + def pickWord(self): + if self.should_change_category: + # clear flags + self.should_change_category = False + self.category_count = 0 + # Get the path to word files dir + dirpath = self.master.getFilePath("") + # List dir + files = os.listdir(dirpath) + # choose a random file + random.shuffle(files) + self.category_file = files[0] + self.category_name = self.catFileNameToStr(self.category_file) + # Process the name & announce + self.master.bot.act_PRIVMSG(self.channel, "The category is now: %s " % self.category_name) + # count lines + f = open(self.master.getFilePath(self.category_file), "r") + lines = 0 + while True: + lines+=1 + if f.readline() == "": + break + f.close() + # change category + picked = "" + while picked == "" or picked in self.lastwords: + + skip = random.randint(0, lines) + f = open(self.master.getFilePath(self.category_file), "r") + while skip>=0: + f.readline() + skip-=1 + picked = f.readline().strip().lower() + f.close() + + self.master.log.debug("DogeScramble: picked %s for %s" % (picked, self.channel)) + self.lastwords.append(picked) + if len(self.lastwords) > 5: + self.lastwords.pop(0) + return picked + + def scrambleWord(self, word): + scrambled = "" + for subword in word.split(" "): + scrambled+=self.scrambleIndividualWord(subword)+ " " + return scrambled.strip() + + def scrambleIndividualWord(self, word): + scrambled = list(word) + random.shuffle(scrambled) + return ''.join(scrambled).lower() diff --git a/pyircbot/modules/DogeWallet.py b/pyircbot/modules/DogeWallet.py new file mode 100644 index 0000000..a70daf3 --- /dev/null +++ b/pyircbot/modules/DogeWallet.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python +from modulebase import ModuleBase,ModuleHook +import time +import hashlib + +class DogeWallet(ModuleBase): + def __init__(self, bot, moduleName): + ModuleBase.__init__(self, bot, moduleName); + self.hooks=[ModuleHook("PRIVMSG", self.gotmsg)] + # Load attribute storage + self.attr = self.bot.getBestModuleForService("attributes") + # Load doge RPC + self.doge = self.bot.getBestModuleForService("dogerpc") + + def gotmsg(self, args, prefix, trailing): + channel = args[0] + if channel[0] == "#": + # Ignore channel messages + pass + else: + self.handlePm(args, prefix, trailing) + + def handlePm(self, args, prefix, trailing): + prefix = self.bot.decodePrefix(prefix) + cmd = self.bot.messageHasCommand(".setpass", trailing) + if cmd: + if len(cmd.args)==0: + self.bot.act_PRIVMSG(prefix.nick, ".setpass: usage: \".setpass newpass\" or \".setpass oldpass newpass\"") + else: + oldpass = self.attr.getAttribute(prefix.nick, "password") + if oldpass == None: + self.attr.setAttribute(prefix.nick, "password", cmd.args[0]) + self.bot.act_PRIVMSG(prefix.nick, ".setpass: Your password has been set to \"%s\"." % cmd.args[0]) + else: + if len(cmd.args)==2: + if cmd.args[0] == oldpass: + self.attr.setAttribute(prefix.nick, "password", cmd.args[1]) + self.bot.act_PRIVMSG(prefix.nick, ".setpass: Your password has been set to \"%s\"." % cmd.args[1]) + else: + self.bot.act_PRIVMSG(prefix.nick, ".setpass: Old password incorrect.") + else: + self.bot.act_PRIVMSG(prefix.nick, ".setpass: You must provide the old password when setting a new one.") + cmd = self.bot.messageHasCommand(".setdogeaddr", trailing) + if cmd: + userpw = self.attr.getAttribute(prefix.nick, "password") + if userpw==None: + self.bot.act_PRIVMSG(prefix.nick, ".setdogeaddr: You must first set a password with .setpass") + else: + if len(cmd.args)==2: + if userpw == cmd.args[0]: + self.attr.setAttribute(prefix.nick, "dogeaddr", cmd.args[1]) + self.bot.act_PRIVMSG(prefix.nick, ".setdogeaddr: Your doge address has been set to \"%s\"." % cmd.args[1]) + # if they don't have a wallet name, we'll make one now + if self.attr.getAttribute(prefix.nick, "dogeaccountname")==None: + randName = self.md5(str(time.time()))[0:10] + self.attr.setAttribute(prefix.nick, "dogeaccountname", randName) + + else: + self.bot.act_PRIVMSG(prefix.nick, ".setdogeaddr: incorrect password.") + else: + self.bot.act_PRIVMSG(prefix.nick, ".setdogeaddr: usage: \".setdogeaddr password address\" or \".setdogeaddr mypassword D8VNy3zkMGspffcFSWWqsxx7GrtVsmF2up\"") + + cmd = self.bot.messageHasCommand(".getdogebal", trailing) + if cmd: + userpw = self.attr.getAttribute(prefix.nick, "password") + if userpw==None: + self.bot.act_PRIVMSG(prefix.nick, ".getdogebal: You must first set a password with .setpass") + else: + if len(cmd.args)==1: + if userpw == cmd.args[0]: + ################# + walletname = self.attr.getAttribute(prefix.nick, "dogeaccountname") + amount = 0.0 + if walletname: + amount = self.doge.getBal(walletname) + + self.bot.act_PRIVMSG(prefix.nick, ".getdogebal: Your balance is: %s DOGE" % amount) + + ################# + else: + self.bot.act_PRIVMSG(prefix.nick, ".getdogebal: incorrect password.") + else: + self.bot.act_PRIVMSG(prefix.nick, ".getdogebal: usage: \".getdogebal password\"") + + cmd = self.bot.messageHasCommand(".withdrawdoge", trailing) + if cmd: + userpw = self.attr.getAttribute(prefix.nick, "password") + useraddr = self.attr.getAttribute(prefix.nick, "dogeaddr") + if userpw==None: + self.bot.act_PRIVMSG(prefix.nick, ".withdrawdoge: You must first set a password with .setpass") + elif useraddr==None: + self.bot.act_PRIVMSG(prefix.nick, ".withdrawdoge: You must first set a withdraw address .setdogeaddr") + else: + if len(cmd.args)==2: + if userpw == cmd.args[0]: + ################# + walletname = self.attr.getAttribute(prefix.nick, "dogeaccountname") + walletbal = self.doge.getBal(walletname) + + desiredAmount = float(cmd.args[1]) + + if walletbal >= desiredAmount: + txn = self.doge.send(walletname, useraddr, desiredAmount) + if txn: + self.bot.act_PRIVMSG(prefix.nick, ".withdrawdoge: %s DOGE sent to %s. Transaction ID: %s"% (desiredAmount, useraddr, txn)) + else: + self.bot.act_PRIVMSG(prefix.nick, ".withdrawdoge: Unable to create transaction. Please contact an Operator.") + else: + self.bot.act_PRIVMSG(prefix.nick, ".withdrawdoge: You only have %s DOGE. You cannot withdraw %s DOGE." % (walletbal, desiredAmount)) + ################# + else: + self.bot.act_PRIVMSG(prefix.nick, ".withdrawdoge: incorrect password.") + else: + self.bot.act_PRIVMSG(prefix.nick, ".withdrawdoge: usage: \".withdrawdoge password amount\" - \".withdrawdoge mypassword 5.0\" - ") + + cmd = self.bot.messageHasCommand(".getdepositaddr", trailing) + if cmd: + userpw = self.attr.getAttribute(prefix.nick, "password") + if userpw==None: + self.bot.act_PRIVMSG(prefix.nick, ".getdepositaddr: You must first set a password with .setpass") + else: + if len(cmd.args)==1: + if userpw == cmd.args[0]: + ################# + walletname = self.attr.getAttribute(prefix.nick, "dogeaccountname") + addr = self.doge.getAcctAddr(walletname) + self.bot.act_PRIVMSG(prefix.nick, ".getdepositaddr: Your deposit address is: %s" % addr) + ################# + else: + self.bot.act_PRIVMSG(prefix.nick, ".getdepositaddr: incorrect password.") + else: + self.bot.act_PRIVMSG(prefix.nick, ".getdepositaddr: usage: \".getdepositaddr password\"") + + + + cmd = self.bot.messageHasCommand(".login", trailing) + if cmd: + userpw = self.attr.getAttribute(prefix.nick, "password") + if userpw==None: + self.bot.act_PRIVMSG(prefix.nick, ".login: You must first set a password with .setpass") + else: + if len(cmd.args)==1: + if userpw == cmd.args[0]: + ################# + self.attr.setAttribute(prefix.nick, "loggedinfrom", prefix.hostname) + self.bot.act_PRIVMSG(prefix.nick, ".login: You have been logged in from: %s" % prefix.hostname) + ################# + else: + self.bot.act_PRIVMSG(prefix.nick, ".login: incorrect password.") + else: + self.bot.act_PRIVMSG(prefix.nick, ".login: usage: \".login password\"") + cmd = self.bot.messageHasCommand(".logout", trailing) + if cmd: + loggedin = self.attr.getAttribute(prefix.nick, "loggedinfrom") + if loggedin == None: + self.bot.act_PRIVMSG(prefix.nick, ".logout: You must first be logged in") + else: + self.attr.setAttribute(prefix.nick, "loggedinfrom", None) + self.bot.act_PRIVMSG(prefix.nick, ".logout: You have been logged out.") + + def md5(self, data): + m = hashlib.md5() + m.update(data.encode("ascii")) + return m.hexdigest() diff --git a/pyircbot/modules/GameBase.py b/pyircbot/modules/GameBase.py new file mode 100644 index 0000000..ff3c662 --- /dev/null +++ b/pyircbot/modules/GameBase.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +from modulebase import ModuleBase,ModuleHook +import random +import yaml +import os +import time +from threading import Timer + +class GameBase(ModuleBase): + def __init__(self, bot, moduleName): + ModuleBase.__init__(self, bot, moduleName); + self.hooks=[ModuleHook("PRIVMSG", self.gotMsg)] + self.loadConfig() + # Load attribute storage + self.attr = self.bot.getBestModuleForService("attributes") + # Load doge RPC + self.doge = self.bot.getBestModuleForService("dogerpc") + # Dict of #channel -> game object + self.games = {} + + def gotMsg(self, args, prefix, trailing): + prefixObj = self.bot.decodePrefix(prefix) + # Ignore messages from users not logged in + if self.attr.getAttribute(prefixObj.nick, "loggedinfrom")==None: + # Send them a hint? + return + else: + if args[0][0] == "#": + # create a blank game obj if there isn't one (and whitelisted ? ) + if not args[0] in self.games and (not self.config["channelWhitelistOn"] or (self.config["channelWhitelistOn"] and args[0][1:] in self.config["channelWhitelist"]) ): + self.games[args[0]]=gameObj(self, args[0]) + # Channel message + self.games[args[0]].gotMsg(args, prefix, trailing) + else: + # Private message + #self.games[args[0]].gotPrivMsg(args, prefix, trailing) + pass + + def ondisable(self): + self.log.info("GameBase: Unload requested, ending games...") + for game in self.games: + self.games[game].gameover() + +class gameObj: + def __init__(self, master, channel): + self.master = master + self.channel = channel + + def gotPrivMsg(self, args, prefix, trailing): + prefix = self.master.bot.decodePrefix(prefix) + pass + + def gotMsg(self, args, prefix, trailing): + prefix = self.master.bot.decodePrefix(prefix) + pass + + #senderIsOp = self.master.attr.getAttribute(prefix.nick, "op")=="yes" + def gameover(self): + pass + +class playerObj: + def __init__(self, game, nick): + self.game = game + self.nick = nick + diff --git a/pyircbot/modules/MySQL.py b/pyircbot/modules/MySQL.py new file mode 100644 index 0000000..ca20023 --- /dev/null +++ b/pyircbot/modules/MySQL.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +from modulebase import ModuleBase,ModuleHook +import sys + +try: + import MySQLdb #python 2.x +except: + import pymysql as MySQLdb #python 3.x + +class MySQL(ModuleBase): + def __init__(self, bot, moduleName): + ModuleBase.__init__(self, bot, moduleName); + self.hooks=[] + self.services=["mysql"] + self.loadConfig() + self.connection = self.getConnection() + + def getConnection(self): + return Connection(self) + + +class Connection: + def __init__(self, master): + self.config = master.config + self.log = master.log + self._connect() + + # Check if the table requested exists + def tableExists(self, tablename): + c = self.getCursor() + c.execute("SHOW TABLES;") + tables = c.fetchall() + if len(tables)==0: + return False; + key = list(tables[0].keys())[0] + for table in tables: + if table[key]==tablename: + return True; + return False + + def query(self, queryText, args=()): + c = self.getCursor() + if len(args)==0: + c.execute(queryText) + else: + c.execute(queryText, args) + return c + + # Returns a cusor object, after checking for connectivity + def getCursor(self): + self.ensureConnected() + if sys.version_info > (3,0): + c = self.connection.cursor(MySQLdb.cursors.DictCursor) + else: + c = self.connection.cursor(cursorclass=MySQLdb.cursors.DictCursor) + c.execute("USE `%s`;" % self.config["database"]) + return c + + def escape(self, s): + self.ensureConnected() + return self.connection.escape_string(s) + + def ensureConnected(self): + try: + self.connection.ping() + except: + try: + self.connection.close() + except: + pass + del self.connection + self._connect() + + def ondisable(self): + self.connection.close() + + # Connects to the database server, and selects a database (Or attempts to create it if it doesn't exist yet) + def _connect(self): + self.log.info("MySQL: Connecting to db host at %s" % self.config["host"]) + self.connection = MySQLdb.connect(host=self.config["host"],user=self.config["username"] ,passwd=self.config["password"]) + self.log.info("MySQL: Connected.") + self.connection.autocommit(True) + c = None + if sys.version_info > (3,0): + c = self.connection.cursor(MySQLdb.cursors.DictCursor) + else: + c = self.connection.cursor(cursorclass=MySQLdb.cursors.DictCursor) + + c.execute("SHOW DATABASES") + dblist = c.fetchall() + found = False + for row in dblist: + if row["Database"]==self.config["database"]: + found = True + if not found: + c.execute("CREATE DATABASE `%s`;" % self.config["database"]) + c.execute("USE `%s`;" % self.config["database"]) + c.close() diff --git a/pyircbot/modules/PingResponder.py b/pyircbot/modules/PingResponder.py new file mode 100644 index 0000000..2f1b207 --- /dev/null +++ b/pyircbot/modules/PingResponder.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +from modulebase import ModuleBase,ModuleHook + +class PingResponder(ModuleBase): + def __init__(self, bot, moduleName): + ModuleBase.__init__(self, bot, moduleName); + self.hooks=[ModuleHook("PING", self.pingrespond)] + def pingrespond(self, args, prefix, trailing): + # got a ping? send it right back + self.bot.act_PONG(trailing) + self.log.info("Responded to a ping: %s" % trailing) diff --git a/pyircbot/modules/Scramble.py b/pyircbot/modules/Scramble.py new file mode 100644 index 0000000..d1cfc84 --- /dev/null +++ b/pyircbot/modules/Scramble.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python +from modulebase import ModuleBase,ModuleHook +import random +import yaml +import os +import time +from threading import Timer +from operator import itemgetter + +class Scramble(ModuleBase): + def __init__(self, bot, moduleName): + # init the base module + ModuleBase.__init__(self, bot, moduleName); + self.hooks=[ModuleHook("PRIVMSG", self.scramble)] + self.loadConfig() + + # Dictionary + self.wordsCount=0; + self.wordsFile = self.getFilePath("words.txt") + print(self.wordsFile) + wordsF = open(self.wordsFile, "r") + while True: + word = wordsF.readline() + if word=="": + break + self.wordsCount+=1 + wordsF.close + self.log.info("Scramble: Loaded %s words" % str(self.wordsCount)) + # Load scores + self.scoresFile = self.getFilePath("scores.yml") + if not os.path.exists(self.scoresFile): + yaml.dump({}, file(self.scoresFile, 'w')) + self.scores = yaml.load(file(self.scoresFile, 'r')) + # Per channel games + self.games = {} + # Hook in + self.hooks=[ModuleBase.ModuleHook("PRIVMSG", self.scramble)] + + def scramble(self, args, prefix, trailing): + channel = args[0] + if channel[0] == "#": + if not channel in self.games: + self.games[channel]=scrambleGame(self, channel) + self.games[channel].scramble(args, prefix, trailing) + + def saveScores(self): + yaml.dump(self.scores, file(self.scoresFile, 'w')) + + def getScore(self, player, add=0): + player = player.lower() + if not player in self.scores: + self.scores[player] = 0 + if not add == 0: + self.scores[player]+=add + self.saveScores() + + return self.scores[player] + + def getScoreNoWrite(self, player): + if not player.lower() in self.scores: + return 0 + else: + return self.getScore(player) + + def ondisable(self): + self.log.info("Scramble: Unload requested, ending games...") + for game in self.games: + self.games[game].gameover() + self.saveScores() + +class scrambleGame: + def __init__(self, master, channel): + self.master = master + self.channel = channel + # Running? + self.running = False + # Current word + self.currentWord = None + # Current word, scrambled + self.scrambled = None + # Online? + self.scrambleOn = False + # Count down to hints + self.hintTimer = None + # of hints given + self.hintsGiven = 0 + # Cooldown between words + self.nextTimer = None + # How many guesses submitted this round + self.guesses = 0; + # How many games in a row where nobody guessed + self.gamesWithoutGuesses = 0; + + self.delayHint = self.master.config["hintDelay"]; + self.delayNext = self.master.config["delayNext"]; + self.maxHints = self.master.config["maxHints"]; + self.abortAfterNoGuesses = self.master.config["abortAfterNoGuesses"]; + + def gameover(self): + self.clearTimers(); + self.running = False + def clearTimers(self): + self.clearTimer(self.nextTimer) + self.clearTimer(self.hintTimer) + def clearTimer(self, timer): + if timer: + timer.cancel() + def scramble(self, args, prefix, trailing): + prefix = self.master.bot.decodePrefix(prefix) + sender = prefix.nick + cmd = self.master.bot.messageHasCommand(".scrambleon", trailing) + if cmd and not self.running: + self.running = True + self.startScramble() + return + cmd = self.master.bot.messageHasCommand(".scrambleoff", trailing) + if cmd and self.running: + self.gameover() + self.running = False + return + cmd = self.master.bot.messageHasCommand(".scramble top", trailing) + if cmd: + sortedscores = [] + for player in self.master.scores: + sortedscores.append({'name':player, 'score':self.master.scores[player]}) + sortedscores = sorted(sortedscores, key=itemgetter('score')) + sortedscores.reverse() + numScores = len(sortedscores) + if numScores>3: + numScores=3 + resp = "Top %s: " % str(numScores) + which = 1 + while which<=numScores: + resp+="%s: %s, " % (sortedscores[which-1]["name"], sortedscores[which-1]["score"]) + which+=1 + self.master.bot.act_PRIVMSG(self.channel, resp[:-2]) + cmd = self.master.bot.messageHasCommand(".scramble score", trailing) + if cmd: + someone = cmd.args.strip() + if len(someone) > 0: + self.master.bot.act_PRIVMSG(self.channel, "%s: %s has a score of %s" % (sender, someone, self.master.getScoreNoWrite(someone))) + else: + self.master.bot.act_PRIVMSG(self.channel, "%s: %s" % (sender, self.master.getScore(sender))) + if self.currentWord and trailing.strip().lower() == self.currentWord: + playerScore = self.master.getScore(sender, 1) + self.master.bot.act_PRIVMSG(self.channel, "%s guessed the word - %s! %s now has %s points. Next word in %s seconds." % (sender, self.currentWord, sender, playerScore, self.delayNext)) + self.currentWord = None + self.clearTimers() + self.hintsGiven = 0 + self.nextTimer = Timer(self.delayNext, self.startNewWord) + self.nextTimer.start() + self.guesses=0 + else: + self.guesses+=1 + + def startScramble(self): + self.clearTimer(self.nextTimer) + self.nextTimer = Timer(0, self.startNewWord) + self.nextTimer.start() + + def startNewWord(self): + self.currentWord = self.pickWord() + self.master.log.info("Scramble: New word for %s: %s" % (self.channel, self.currentWord)) + self.scrambled = self.scrambleWord(self.currentWord) + self.master.bot.act_PRIVMSG(self.channel, "New word - %s " % (self.scrambled)) + + self.clearTimer(self.hintTimer) + self.hintTimer = Timer(self.delayHint, self.giveHint) + self.hintTimer.start() + + def giveHint(self): + self.hintsGiven+=1 + + if self.hintsGiven>=len(self.currentWord) or self.hintsGiven > self.maxHints: + self.abortWord() + return + + blanks = "" + for letter in list(self.currentWord): + if letter == " ": + blanks+=" " + else: + blanks+="_" + partFromWord = self.currentWord[0:self.hintsGiven] + partFromBlanks = blanks[self.hintsGiven:] + hintstr = partFromWord+partFromBlanks + + self.master.bot.act_PRIVMSG(self.channel, "Hint: - %s" % (hintstr)) + + self.clearTimer(self.hintTimer) + self.hintTimer = Timer(self.delayHint, self.giveHint) + self.hintTimer.start() + + def abortWord(self): + cur = self.currentWord + self.currentWord = None + self.master.bot.act_PRIVMSG(self.channel, "Word expired - the answer was %s. Next word in %s seconds." % (cur, self.delayNext)) + self.hintsGiven = 0 + self.clearTimer(self.nextTimer) + + if self.guesses==0: + self.gamesWithoutGuesses+=1 + if self.gamesWithoutGuesses >= self.abortAfterNoGuesses: + self.master.bot.act_PRIVMSG(self.channel, "No one seems to be playing - type .scrambleon to start again.") + self.gameover() + return + else: + self.gamesWithoutGuesses=0 + + self.nextTimer = Timer(self.delayNext, self.startNewWord) + self.nextTimer.start() + + def pickWord(self): + f = open(self.master.wordsFile, "r") + skip = random.randint(0, self.master.wordsCount) + while skip>=0: + f.readline() + skip-=1 + picked = f.readline().strip().lower() + f.close() + return picked + + def scrambleWord(self, word): + scrambled = "" + for subword in word.split(" "): + scrambled+=self.scrambleIndividualWord(subword)+ " " + return scrambled.strip() + + def scrambleIndividualWord(self, word): + scrambled = list(word) + random.shuffle(scrambled) + return ''.join(scrambled).lower() \ No newline at end of file diff --git a/pyircbot/modules/Services.py b/pyircbot/modules/Services.py new file mode 100644 index 0000000..040b7ee --- /dev/null +++ b/pyircbot/modules/Services.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +from modulebase import ModuleBase,ModuleHook +from time import sleep + +class Services(ModuleBase): + def __init__(self, bot, moduleName): + ModuleBase.__init__(self, bot, moduleName) + self.hooks=[ModuleHook("_CONNECT", self.doConnect), ModuleHook("433", self.nickTaken), ModuleHook("001", self.initservices), ModuleHook("INVITE", self.invited), ] + self.loadConfig() + self.current_nick = 0 + self.do_ghost = False + def doConnect(self, args, prefix, trailing): + self.bot.act_NICK(self.config["user"]["nick"][0]) + self.bot.act_USER(self.config["user"]["username"], self.config["user"]["hostname"], self.config["user"]["realname"]) + + def nickTaken(self, args, prefix, trailing): + if self.config["ident"]["ghost"]: + self.do_ghost = True + self.current_nick+=1 + if self.current_nick >= len(self.config["user"]["nick"]): + self.log.critical("Ran out of usernames while selecting backup username!") + return + self.bot.act_NICK(self.config["user"]["nick"][self.current_nick]) + + def initservices(self, args, prefix, trailing): + if self.do_ghost: + self.bot.act_PRIVMSG(self.config["ident"]["ghost_to"], self.config["ident"]["ghost_cmd"] % {"nick":self.config["user"]["nick"][0], "password":self.config["user"]["password"]}) + sleep(2) + self.bot.act_NICK(self.config["user"]["nick"][0]) + self.do_initservices() + + def invited(self, args, prefix, trailing): + if trailing.lower() in self.config["privatechannels"]["list"]: + self.log.info("Invited to %s, joining" % trailing) + self.bot.act_JOIN(trailing) + + def do_initservices(self): + " id to nickserv " + if self.config["ident"]["enable"]: + self.bot.act_PRIVMSG(self.config["ident"]["to"], self.config["ident"]["command"] % {"password":self.config["user"]["password"]}) + + " join plain channels " + for channel in self.config["channels"]: + self.log.info("Joining %s" % channel) + self.bot.act_JOIN(channel) + + " request invite for private message channels " + for channel in self.config["privatechannels"]["list"]: + self.log.info("Requesting invite to %s" % channel) + self.bot.act_PRIVMSG(self.config["privatechannels"]["to"], self.config["privatechannels"]["command"] % {"channel":channel}) + diff --git a/run-example.sh b/run-example.sh new file mode 100644 index 0000000..68727cf --- /dev/null +++ b/run-example.sh @@ -0,0 +1,2 @@ +#!/bin/sh +./pyircbot/main.py -c config.main.yml -b pyircbot.yml