r70937 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r70936‎ | r70937 | r70938 >
Date:10:06, 12 August 2010
Author:daniel
Status:deferred
Tags:
Comment:
sample client for receiving RC records via XMPP (incomplete)
Modified paths:
  • /trunk/extensions/XMLRC/client (added) (history)
  • /trunk/extensions/XMLRC/client/.htaccess (added) (history)
  • /trunk/extensions/XMLRC/client/rcclient.ini.sample (added) (history)
  • /trunk/extensions/XMLRC/client/rcclient.py (added) (history)

Diff [purge]

Index: trunk/extensions/XMLRC/client/rcclient.ini.sample
@@ -0,0 +1,13 @@
 2+# Configuration file for rcclient
 3+# WARNING: make sure this file is not readable from the web!
 4+
 5+[XMPP]
 6+# NOTE: please put your XMPP login info
 7+# WARNING: make sure this file is not readable from the web!
 8+jid: example@jabber.org
 9+password: snoopy64
 10+
 11+# If you want to join a group chat, use the lines below
 12+# or use --group and optionally --nick on the command line
 13+#group: recentchanges@conference.jabber.yourhost.com
 14+#nick: john
\ No newline at end of file
Index: trunk/extensions/XMLRC/client/rcclient.py
@@ -0,0 +1,329 @@
 2+#!/usr/bin/python
 3+
 4+##############################################################################
 5+# XMPP client for XMLRC
 6+#
 7+#
 8+# Copyright (c) 2010, Wikimedia Deutschland; Author: Daniel Kinzler
 9+# All rights reserved.
 10+#
 11+# This program is free software: you can redistribute it and/or modify
 12+# it under the terms of the GNU General Public License as published by
 13+# the Free Software Foundation, either version 3 of the License, or
 14+# (at your option) any later version.
 15+#
 16+# This program is distributed in the hope that it will be useful,
 17+# but WITHOUT ANY WARRANTY; without even the implied warranty of
 18+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 19+# GNU General Public License for more details.
 20+#
 21+# You should have received a copy of the GNU General Public License
 22+# along with this program. If not, see <http://www.gnu.org/licenses/>.
 23+##############################################################################
 24+
 25+import sys, os, os.path, traceback, datetime
 26+import ConfigParser, optparse
 27+import select, xmpp # using the xmpppy library <http://xmpppy.sourceforge.net/>, GPL
 28+
 29+
 30+LOG_MUTE = 0
 31+LOG_QUIET = 10
 32+LOG_VERBOSE = 20
 33+LOG_DEBUG = 30
 34+
 35+##################################################################################
 36+class RecentChange(object):
 37+ """ Represence a RecentChanges-Record. Properties of a change can be accessed
 38+ using item syntax (e.g. rc['revid']) or attribute syntax (e.g. rc.revid). """
 39+
 40+ flags = set( ( 'anon', 'bot', 'minor' ) )
 41+ numerics = set( ( 'rcid', 'pageid', 'revid', 'old_revid', 'newlen', 'oldlen', 'ns' ) )
 42+ times = set( ( 'timestamp' ) )
 43+
 44+ def __init__(self, dom):
 45+ self.dom = dom
 46+
 47+ def get_property(self, prop):
 48+ a = self.dom.getAttr(prop)
 49+
 50+ if prop in RecentChange.flags:
 51+ if a is None or a is False:
 52+ return False
 53+ else:
 54+ return True
 55+ elif a is None:
 56+ a = self.dom.getTag(prop)
 57+ # TODO: wrap for conversion. known tags: <tags>, <param>, <block>
 58+ elif a in RecentChange.numerics:
 59+ a = int( a )
 60+ elif a in RecentChange.times:
 61+ a = datetime.strptime( a, '%Y-%m-%dT%H:%M:%S%Z' ) # 2010-10-12T08:57:03Z
 62+
 63+ return a
 64+
 65+ def __getitem__(self, prop):
 66+ return self.get_property(prop)
 67+
 68+ def __getattr__(self, prop):
 69+ return self.get_property(prop)
 70+
 71+class RCHandler(object):
 72+ """ Base class for RC hanlders. Instances compatible with this class
 73+ can be registered with RCClient.add_handler(). Their process(rc)
 74+ method will then be called for every change record received. """
 75+
 76+ def process(self, rc):
 77+ pass
 78+
 79+class RCEcho(RCHandler):
 80+ props = ( 'rcid', 'timestamp', 'type', 'ns', 'title', 'pageid', 'revid', 'old_revid',
 81+ 'user', 'oldlen', 'newlen', 'comment', 'logid', 'logtype', 'logaction' )
 82+
 83+ def process(self, rc):
 84+ for p in RCEcho.props:
 85+ self.print_prop(rc, p)
 86+
 87+ def print_prop(self, rc, prop):
 88+ v = rc[prop]
 89+ if v is None: v = ''
 90+
 91+ print "%s: %s" % (prop, v) # XXX: check encoding crap
 92+
 93+##################################################################################
 94+
 95+class RCClient(object):
 96+ def __init__( self, console_encoding = 'utf-8' ):
 97+ self.console_encoding = console_encoding
 98+ self.handlers = []
 99+ self.loglevel = LOG_VERBOSE
 100+
 101+ self.xmpp = None
 102+ self.jid = None
 103+
 104+ self.group = None
 105+ self.nick = None
 106+
 107+ def warn(self, message):
 108+ if self.loglevel >= LOG_QUIET:
 109+ sys.stderr.write( "WARNING: %s\n" % ( message.encode( self.console_encoding ) ) )
 110+
 111+ def info(self, message):
 112+ if self.loglevel >= LOG_VERBOSE:
 113+ sys.stderr.write( "INFO: %s\n" % ( message.encode( self.console_encoding ) ) )
 114+
 115+ def debug(self, message):
 116+ if self.loglevel >= LOG_DEBUG:
 117+ sys.stderr.write( "DEBUG: %s\n" % ( message.encode( self.console_encoding ) ) )
 118+
 119+ def service_loop( self ):
 120+ sockets = ( self.xmpp.Connection._sock, )
 121+
 122+ self.online = 1
 123+
 124+ while self.online:
 125+ (in_socks , out_socks, err_socks) = select.select(sockets, [], sockets, 1)
 126+
 127+ for sock in in_socks:
 128+ try:
 129+ self.xmpp.Process(1)
 130+
 131+ if not self.xmpp.isConnected():
 132+ self.warn("connection lost, reconnecting...")
 133+
 134+ if self.xmpp.reconnectAndReauth():
 135+ self.warn("re-connect successful.")
 136+ self.on_connect()
 137+
 138+ except Exception, e:
 139+ error_type, error_value, trbk = sys.exc_info()
 140+ self.warn( "Error while processing! %s" % " ".join( traceback.format_exception( error_type, error_value, trbk ) ) )
 141+ # TODO: detect when we should kill the loop because a connection failed
 142+
 143+ for sock in err_socks:
 144+ raise Exception( "Error in socket: %s" % repr(sock) )
 145+
 146+ self.info("service loop terminated, disconnecting")
 147+
 148+ for sock in sockets:
 149+ con.close()
 150+
 151+ self.info("done.")
 152+
 153+ def process_message(self, con, message):
 154+ if (message.getError()):
 155+ self.warn("received %s error from <%s>: %s" % (message.getType(), message.getError(), message.getFrom() ))
 156+ elif message.getBody():
 157+ rc_dom = message.T.rc
 158+ if rc_dom:
 159+ self.debug("RC %s message from <%s>: %s" % (message.getType(), message.getFrom(), message.getBody().strip() ))
 160+ rc = RecentChange( rc_dom )
 161+ self.dispatch_rc( rc )
 162+ else:
 163+ self.info("plain %s message from <%s>: %s" % (message.getType(), message.getFrom(), message.getBody().strip() ))
 164+
 165+ def dispatch_rc(self, rc):
 166+ for h in self.handlers:
 167+ h.process( rc )
 168+
 169+ def add_handler(self, handler):
 170+ self.handlers.append( handler )
 171+
 172+ def remove_handler(self, handler):
 173+ self.handlers.remove( handler )
 174+
 175+ def guess_local_resource(self):
 176+ resource = "%s-%d" % ( socket.gethostname(), os.getpid() )
 177+
 178+ return resource;
 179+
 180+ def connect( self, jid, password ):
 181+
 182+ if type( jid ) != object:
 183+ jid = xmpp.protocol.JID( jid )
 184+
 185+ if jid.getResource() is None:
 186+ jid = xmpp.protocol.JID( host= jid.getHost(), node= jid.getNode(), resource = self.guess_local_resource() )
 187+
 188+ self.xmpp = xmpp.Client(jid.getDomain(),debug=[])
 189+ con= self.xmpp.connect()
 190+
 191+ if not con:
 192+ self.warn( 'could not connect to %s!' % jid.getDomain() )
 193+ return False
 194+
 195+ self.debug( 'connected with %s' % con )
 196+
 197+ auth= self.xmpp.auth( jid.getNode(), password, resource= jid.getResource() )
 198+
 199+ if not auth:
 200+ self.warn( 'could not authenticate as %s!' % jid )
 201+ return False
 202+
 203+ self.debug('authenticated using %s as %s' % ( auth, jid ) )
 204+
 205+ self.xmpp.RegisterHandler( 'message', self.process_message )
 206+
 207+ self.jid = jid;
 208+ self.info( 'connected as %s' % ( jid ) )
 209+
 210+ self.on_connect()
 211+
 212+ return con
 213+
 214+ def on_connect( self ):
 215+ self.xmpp.sendInitPresence(self)
 216+ self.roster = self.xmpp.getRoster()
 217+
 218+ if self.group:
 219+ self.join( self.group )
 220+
 221+ def join(self, group, nick = None):
 222+ if not nick:
 223+ nick = self.jid.getNode()
 224+
 225+ if type( group ) != object:
 226+ group = xmpp.protocol.JID( group )
 227+
 228+ # use our own desired nickname as resource part of the group's JID
 229+ gjid = group.getStripped() + "/" + nick;
 230+
 231+ #create presence stanza
 232+ join = xmpp.Presence( to= gjid )
 233+
 234+ #announce full MUC support
 235+ join.addChild( name = 'x', namespace = 'http://jabber.org/protocol/muc' )
 236+
 237+ self.xmpp.send( join )
 238+
 239+ self.info( 'joined room %s' % self.jid.getStripped() )
 240+
 241+ self.group = group
 242+ self.nick = nick
 243+
 244+ return True
 245+
 246+##################################################################################
 247+
 248+if __name__ == '__main__':
 249+
 250+ # find the location of this script
 251+ bindir= os.path.dirname( os.path.realpath( sys.argv[0] ) )
 252+ extdir= os.path.dirname( bindir )
 253+
 254+ # set up command line options........
 255+ option_parser = optparse.OptionParser()
 256+ option_parser.add_option("--config", dest="config_file",
 257+ help="read config from FILE", metavar="FILE")
 258+
 259+ option_parser.add_option("--quiet", action="store_const", dest="loglevel", const=LOG_QUIET, default=LOG_VERBOSE,
 260+ help="suppress informational messages, only print warnings and errors")
 261+
 262+ option_parser.add_option("--debug", action="store_const", dest="loglevel", const=LOG_DEBUG,
 263+ help="print debug messages")
 264+
 265+ option_parser.add_option("--group", "--muc", dest="group", default=None,
 266+ help="join MUC chat group")
 267+
 268+ option_parser.add_option("--nick", dest="nick", metavar="NICKNAME", default=None,
 269+ help="use NICKNAME in the MUC group")
 270+
 271+ (options, args) = option_parser.parse_args()
 272+
 273+ # find config file........
 274+ if options.config_file:
 275+ cfg = options.config_file #take it from --config
 276+ else:
 277+ cfg = extdir + "/../../rcclient.ini" #installation root
 278+
 279+ if not os.path.exists( cfg ):
 280+ cfg = extdir + "/../../phase3/rcclient.ini" #installation root in dev environment
 281+
 282+ if not os.path.exists( cfg ):
 283+ cfg = bindir + "/rcclient.ini" #extension dir
 284+
 285+ # define config defaults........
 286+ config = ConfigParser.SafeConfigParser()
 287+
 288+ config.add_section( 'XMPP' )
 289+ config.set( 'XMPP', 'group', '' )
 290+ config.set( 'XMPP', 'nick', '' )
 291+
 292+ # read config file........
 293+ if not config.read( cfg ):
 294+ sys.stderr.write( "failed to read config from %s\n" % cfg )
 295+ sys.exit(2)
 296+
 297+ jid = config.get( 'XMPP', 'jid' )
 298+ password = config.get( 'XMPP', 'password' )
 299+
 300+ if options.group is None:
 301+ group = config.get( 'XMPP', 'group' )
 302+ else:
 303+ group = options.group
 304+
 305+ if options.nick is None:
 306+ nick = config.get( 'XMPP', 'nick' )
 307+ else:
 308+ nick = options.nick
 309+
 310+ if group == '': group = None
 311+ if nick == '': nick = None
 312+
 313+ # create rc client instance
 314+ client = RCClient( )
 315+ client.loglevel = options.loglevel
 316+
 317+ # -- DO STUFF -----------------------------------------------------------------------------------
 318+
 319+ # connect................
 320+ if not client.connect( jid = jid, password = password ):
 321+ sys.exit(1)
 322+
 323+ if group:
 324+ client.join( group, nick )
 325+
 326+ # run relay loop................
 327+ client.service_loop( )
 328+
 329+ print "done."
 330+
\ No newline at end of file
Index: trunk/extensions/XMLRC/client/.htaccess
@@ -0,0 +1 @@
 2+Deny From All
\ No newline at end of file

Status & tagging log