Index: trunk/extensions/XMLRC/client/rcclient.ini.sample |
— | — | @@ -8,6 +8,6 @@ |
9 | 9 | password: snoopy64 |
10 | 10 | |
11 | 11 | # If you want to join a group chat, use the lines below |
12 | | -# or use --group and optionally --nick on the command line |
| 12 | +# or specify the group's JID on the command line |
13 | 13 | #group: recentchanges@conference.jabber.yourhost.com |
14 | 14 | #nick: john |
\ No newline at end of file |
Index: trunk/extensions/XMLRC/client/rcclient.py |
— | — | @@ -21,7 +21,7 @@ |
22 | 22 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
23 | 23 | ############################################################################## |
24 | 24 | |
25 | | -import sys, os, os.path, traceback, datetime |
| 25 | +import sys, os, os.path, traceback, datetime, re |
26 | 26 | import ConfigParser, optparse |
27 | 27 | import select, xmpp # using the xmpppy library <http://xmpppy.sourceforge.net/>, GPL |
28 | 28 | |
— | — | @@ -34,11 +34,12 @@ |
35 | 35 | ################################################################################## |
36 | 36 | class RecentChange(object): |
37 | 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). """ |
| 38 | + using item syntax (e.g. rc['revid']) or attribute syntax (e.g. rc.revid). |
| 39 | + Well known attributes are converted to the appropriate type automatically. """ |
39 | 40 | |
40 | | - flags = set( ( 'anon', 'bot', 'minor' ) ) |
41 | | - numerics = set( ( 'rcid', 'pageid', 'revid', 'old_revid', 'newlen', 'oldlen', 'ns' ) ) |
42 | | - times = set( ( 'timestamp' ) ) |
| 41 | + flags = set( ( 'anon', 'bot', 'minor', ) ) |
| 42 | + numerics = set( ( 'rcid', 'pageid', 'revid', 'old_revid', 'newlen', 'oldlen', 'ns', ) ) |
| 43 | + times = set( ( 'timestamp', ) ) |
43 | 44 | |
44 | 45 | def __init__(self, dom): |
45 | 46 | self.dom = dom |
— | — | @@ -46,6 +47,9 @@ |
47 | 48 | def get_property(self, prop): |
48 | 49 | a = self.dom.getAttr(prop) |
49 | 50 | |
| 51 | + if type(prop) == unicode: |
| 52 | + prop = prop.encode("ascii") |
| 53 | + |
50 | 54 | if prop in RecentChange.flags: |
51 | 55 | if a is None or a is False: |
52 | 56 | return False |
— | — | @@ -54,11 +58,24 @@ |
55 | 59 | elif a is None: |
56 | 60 | a = self.dom.getTag(prop) |
57 | 61 | # 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 | + elif prop in RecentChange.numerics: |
| 63 | + if a == "": |
| 64 | + a = None |
| 65 | + else: |
| 66 | + a = int( a ) |
| 67 | + elif prop in RecentChange.times: |
| 68 | + if a == "": |
| 69 | + a = None |
| 70 | + else: |
| 71 | + # Using ISO 8601: 2010-10-12T08:57:03Z |
| 72 | + # XXX: python apparently does not support time zone offsets (%z) when parsing the date time?? |
| 73 | + # a = datetime.datetime.strptime( a, '%Y-%m-%dT%H:%M:%S%z' ) |
62 | 74 | |
| 75 | + a = re.sub( r'[A-Z]+$|[-+][0-9]+$', r'', a ) # strip time zone |
| 76 | + a = datetime.datetime.strptime( a, '%Y-%m-%dT%H:%M:%S' ) # naive time. This sucks, but MW should use UTC anyway |
| 77 | + else: |
| 78 | + pass |
| 79 | + |
63 | 80 | return a |
64 | 81 | |
65 | 82 | def __getitem__(self, prop): |
— | — | @@ -76,22 +93,32 @@ |
77 | 94 | pass |
78 | 95 | |
79 | 96 | class RCEcho(RCHandler): |
| 97 | + """ Implementation of RCHandler that will print the RecentChanges- |
| 98 | + record to the shell. """ |
| 99 | + |
80 | 100 | props = ( 'rcid', 'timestamp', 'type', 'ns', 'title', 'pageid', 'revid', 'old_revid', |
81 | | - 'user', 'oldlen', 'newlen', 'comment', 'logid', 'logtype', 'logaction' ) |
| 101 | + 'user', 'oldlen', 'newlen', 'comment', 'logid', 'logtype', 'logaction', |
| 102 | + 'anon', 'bot', 'minor' ) |
82 | 103 | |
83 | 104 | def process(self, rc): |
| 105 | + print "-----------------------------------------------" |
84 | 106 | for p in RCEcho.props: |
85 | 107 | self.print_prop(rc, p) |
| 108 | + print "-----------------------------------------------" |
86 | 109 | |
87 | 110 | def print_prop(self, rc, prop): |
88 | 111 | v = rc[prop] |
89 | 112 | if v is None: v = '' |
90 | 113 | |
91 | | - print "%s: %s" % (prop, v) # XXX: check encoding crap |
| 114 | + print "%s: %s" % (prop, v) |
92 | 115 | |
93 | 116 | ################################################################################## |
94 | 117 | |
95 | 118 | class RCClient(object): |
| 119 | + """ XMPP client listeneing for RecentChanges-messages, and passing them |
| 120 | + to any instances of RCHandler that have been registered using |
| 121 | + RCClient.add_handler(). """ |
| 122 | + |
96 | 123 | def __init__( self, console_encoding = 'utf-8' ): |
97 | 124 | self.console_encoding = console_encoding |
98 | 125 | self.handlers = [] |
— | — | @@ -100,7 +127,7 @@ |
101 | 128 | self.xmpp = None |
102 | 129 | self.jid = None |
103 | 130 | |
104 | | - self.group = None |
| 131 | + self.room = None |
105 | 132 | self.nick = None |
106 | 133 | |
107 | 134 | def warn(self, message): |
— | — | @@ -120,33 +147,38 @@ |
121 | 148 | |
122 | 149 | self.online = 1 |
123 | 150 | |
124 | | - while self.online: |
125 | | - (in_socks , out_socks, err_socks) = select.select(sockets, [], sockets, 1) |
| 151 | + try: |
| 152 | + while self.online: |
| 153 | + (in_socks , out_socks, err_socks) = select.select(sockets, [], sockets, 1) |
126 | 154 | |
127 | | - for sock in in_socks: |
128 | | - try: |
129 | | - self.xmpp.Process(1) |
| 155 | + for sock in in_socks: |
| 156 | + try: |
| 157 | + self.xmpp.Process(1) |
130 | 158 | |
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() |
| 159 | + if not self.xmpp.isConnected(): |
| 160 | + self.warn("connection lost, reconnecting...") |
| 161 | + |
| 162 | + if self.xmpp.reconnectAndReauth(): |
| 163 | + self.warn("re-connect successful.") |
| 164 | + self.on_connect() |
137 | 165 | |
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 |
| 166 | + except Exception, e: |
| 167 | + error_type, error_value, trbk = sys.exc_info() |
| 168 | + self.warn( "Error while processing! %s" % " ".join( traceback.format_exception( error_type, error_value, trbk ) ) ) |
| 169 | + # TODO: detect when we should kill the loop because a connection failed |
142 | 170 | |
143 | | - for sock in err_socks: |
144 | | - raise Exception( "Error in socket: %s" % repr(sock) ) |
| 171 | + for sock in err_socks: |
| 172 | + raise Exception( "Error in socket: %s" % repr(sock) ) |
| 173 | + except KeyboardInterrupt: |
| 174 | + pass |
145 | 175 | |
146 | 176 | self.info("service loop terminated, disconnecting") |
147 | 177 | |
148 | 178 | for sock in sockets: |
149 | | - con.close() |
| 179 | + sock.close() |
150 | 180 | |
| 181 | + # TODO: how to leave chat room cleanly ? |
| 182 | + |
151 | 183 | self.info("done.") |
152 | 184 | |
153 | 185 | def process_message(self, con, message): |
— | — | @@ -214,18 +246,18 @@ |
215 | 247 | self.xmpp.sendInitPresence(self) |
216 | 248 | self.roster = self.xmpp.getRoster() |
217 | 249 | |
218 | | - if self.group: |
219 | | - self.join( self.group ) |
| 250 | + if self.room: |
| 251 | + self.join( self.room ) |
220 | 252 | |
221 | | - def join(self, group, nick = None): |
| 253 | + def join(self, room, nick = None): |
222 | 254 | if not nick: |
223 | 255 | nick = self.jid.getNode() |
224 | 256 | |
225 | | - if type( group ) != object: |
226 | | - group = xmpp.protocol.JID( group ) |
| 257 | + if type( room ) != object: |
| 258 | + room = xmpp.protocol.JID( room ) |
227 | 259 | |
228 | | - # use our own desired nickname as resource part of the group's JID |
229 | | - gjid = group.getStripped() + "/" + nick; |
| 260 | + # use our own desired nickname as resource part of the room's JID |
| 261 | + gjid = room.getStripped() + "/" + nick; |
230 | 262 | |
231 | 263 | #create presence stanza |
232 | 264 | join = xmpp.Presence( to= gjid ) |
— | — | @@ -237,7 +269,7 @@ |
238 | 270 | |
239 | 271 | self.info( 'joined room %s' % self.jid.getStripped() ) |
240 | 272 | |
241 | | - self.group = group |
| 273 | + self.room = room |
242 | 274 | self.nick = nick |
243 | 275 | |
244 | 276 | return True |
— | — | @@ -246,12 +278,15 @@ |
247 | 279 | |
248 | 280 | if __name__ == '__main__': |
249 | 281 | |
| 282 | + # -- CONFIG & COMMAND LINE ---------------------------------------------------------------------- |
| 283 | + |
250 | 284 | # find the location of this script |
251 | 285 | bindir= os.path.dirname( os.path.realpath( sys.argv[0] ) ) |
252 | 286 | extdir= os.path.dirname( bindir ) |
253 | 287 | |
254 | 288 | # set up command line options........ |
255 | 289 | option_parser = optparse.OptionParser() |
| 290 | + option_parser.set_usage( "usage: %prog [options] [room]" ) |
256 | 291 | option_parser.add_option("--config", dest="config_file", |
257 | 292 | help="read config from FILE", metavar="FILE") |
258 | 293 | |
— | — | @@ -261,11 +296,8 @@ |
262 | 297 | option_parser.add_option("--debug", action="store_const", dest="loglevel", const=LOG_DEBUG, |
263 | 298 | help="print debug messages") |
264 | 299 | |
265 | | - option_parser.add_option("--group", "--muc", dest="group", default=None, |
266 | | - help="join MUC chat group") |
267 | | - |
268 | 300 | option_parser.add_option("--nick", dest="nick", metavar="NICKNAME", default=None, |
269 | | - help="use NICKNAME in the MUC group") |
| 301 | + help="use NICKNAME in the MUC room") |
270 | 302 | |
271 | 303 | (options, args) = option_parser.parse_args() |
272 | 304 | |
— | — | @@ -285,7 +317,7 @@ |
286 | 318 | config = ConfigParser.SafeConfigParser() |
287 | 319 | |
288 | 320 | config.add_section( 'XMPP' ) |
289 | | - config.set( 'XMPP', 'group', '' ) |
| 321 | + config.set( 'XMPP', 'room', '' ) |
290 | 322 | config.set( 'XMPP', 'nick', '' ) |
291 | 323 | |
292 | 324 | # read config file........ |
— | — | @@ -296,33 +328,36 @@ |
297 | 329 | jid = config.get( 'XMPP', 'jid' ) |
298 | 330 | password = config.get( 'XMPP', 'password' ) |
299 | 331 | |
300 | | - if options.group is None: |
301 | | - group = config.get( 'XMPP', 'group' ) |
| 332 | + if len(args) >= 1: |
| 333 | + room = args[0] |
302 | 334 | else: |
303 | | - group = options.group |
| 335 | + room = config.get( 'XMPP', 'room' ) |
304 | 336 | |
305 | 337 | if options.nick is None: |
306 | 338 | nick = config.get( 'XMPP', 'nick' ) |
307 | 339 | else: |
308 | 340 | nick = options.nick |
309 | 341 | |
310 | | - if group == '': group = None |
| 342 | + if room == '': room = None |
311 | 343 | if nick == '': nick = None |
312 | 344 | |
| 345 | + # -- DO STUFF ----------------------------------------------------------------------------------- |
| 346 | + |
313 | 347 | # create rc client instance |
314 | 348 | client = RCClient( ) |
315 | 349 | client.loglevel = options.loglevel |
316 | 350 | |
317 | | - # -- DO STUFF ----------------------------------------------------------------------------------- |
| 351 | + # add an echo handler that prints the RC info to the shell |
| 352 | + client.add_handler( RCEcho() ) |
318 | 353 | |
319 | 354 | # connect................ |
320 | | - if not client.connect( jid = jid, password = password ): |
| 355 | + if not client.connect( jid, password ): |
321 | 356 | sys.exit(1) |
322 | 357 | |
323 | | - if group: |
324 | | - client.join( group, nick ) |
| 358 | + if room: |
| 359 | + client.join( room, nick ) |
325 | 360 | |
326 | | - # run relay loop................ |
| 361 | + # run listener loop................ |
327 | 362 | client.service_loop( ) |
328 | 363 | |
329 | 364 | print "done." |