[107] | 1 | # -*- test-case-name: wokkel.test.test_muc -*- |
---|
| 2 | # |
---|
[143] | 3 | # Copyright (c) Ralph Meijer. |
---|
[107] | 4 | # See LICENSE for details. |
---|
| 5 | |
---|
| 6 | """ |
---|
| 7 | XMPP Multi-User Chat protocol. |
---|
| 8 | |
---|
| 9 | This protocol is specified in |
---|
| 10 | U{XEP-0045<http://www.xmpp.org/extensions/xep-0045.html>}. |
---|
| 11 | """ |
---|
[141] | 12 | from dateutil.tz import tzutc |
---|
[107] | 13 | |
---|
| 14 | from zope.interface import implements |
---|
| 15 | |
---|
[145] | 16 | from twisted.internet import defer |
---|
[107] | 17 | from twisted.words.protocols.jabber import jid, error, xmlstream |
---|
| 18 | from twisted.words.xish import domish |
---|
| 19 | |
---|
[141] | 20 | from wokkel import data_form, generic, xmppim |
---|
| 21 | from wokkel.delay import Delay, DelayMixin |
---|
[129] | 22 | from wokkel.subprotocols import XMPPHandler |
---|
[108] | 23 | from wokkel.iwokkel import IMUCClient |
---|
[107] | 24 | |
---|
| 25 | # Multi User Chat namespaces |
---|
[130] | 26 | NS_MUC = 'http://jabber.org/protocol/muc' |
---|
| 27 | NS_MUC_USER = NS_MUC + '#user' |
---|
| 28 | NS_MUC_ADMIN = NS_MUC + '#admin' |
---|
| 29 | NS_MUC_OWNER = NS_MUC + '#owner' |
---|
[116] | 30 | NS_MUC_ROOMINFO = NS_MUC + '#roominfo' |
---|
[130] | 31 | NS_MUC_CONFIG = NS_MUC + '#roomconfig' |
---|
| 32 | NS_MUC_REQUEST = NS_MUC + '#request' |
---|
[116] | 33 | NS_MUC_REGISTER = NS_MUC + '#register' |
---|
[110] | 34 | |
---|
[130] | 35 | NS_REQUEST = 'jabber:iq:register' |
---|
[107] | 36 | |
---|
[130] | 37 | MESSAGE = '/message' |
---|
[107] | 38 | PRESENCE = '/presence' |
---|
| 39 | |
---|
[145] | 40 | GROUPCHAT = MESSAGE +'[@type="groupchat"]' |
---|
[107] | 41 | |
---|
[118] | 42 | DEFER_TIMEOUT = 30 # basic timeout is 30 seconds |
---|
[107] | 43 | |
---|
[129] | 44 | |
---|
| 45 | |
---|
[148] | 46 | class ConfigureRequest(generic.Request): |
---|
[110] | 47 | """ |
---|
| 48 | Configure MUC room request. |
---|
| 49 | |
---|
[113] | 50 | http://xmpp.org/extensions/xep-0045.html#roomconfig |
---|
| 51 | |
---|
[110] | 52 | @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'} |
---|
| 53 | @type method: C{str} |
---|
| 54 | """ |
---|
| 55 | |
---|
[148] | 56 | def __init__(self, recipient, sender=None, options=None): |
---|
| 57 | if options is None: |
---|
| 58 | stanzaType = 'get' |
---|
| 59 | else: |
---|
| 60 | stanzaType = 'set' |
---|
[129] | 61 | |
---|
[148] | 62 | generic.Request.__init__(self, recipient, sender, stanzaType) |
---|
| 63 | self.options = options |
---|
| 64 | |
---|
| 65 | |
---|
| 66 | def toElement(self): |
---|
| 67 | element = generic.Request.toElement(self) |
---|
| 68 | |
---|
| 69 | query = element.addElement((NS_MUC_OWNER, 'query')) |
---|
| 70 | if self.options is None: |
---|
| 71 | # This is a request for the configuration form. |
---|
| 72 | form = None |
---|
| 73 | elif not self.options: |
---|
| 74 | form = data_form.Form(formType='cancel') |
---|
| 75 | else: |
---|
| 76 | form = data_form.Form(formType='submit', formNamespace=NS_MUC_CONFIG) |
---|
| 77 | form.makeFields(self.options) |
---|
| 78 | |
---|
| 79 | if form: |
---|
| 80 | query.addChild(form.toElement()) |
---|
| 81 | |
---|
| 82 | return element |
---|
[110] | 83 | |
---|
| 84 | |
---|
[129] | 85 | |
---|
[111] | 86 | class RegisterRequest(xmlstream.IQ): |
---|
| 87 | """ |
---|
| 88 | Register room request. |
---|
| 89 | |
---|
| 90 | @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'} |
---|
| 91 | @type method: C{str} |
---|
| 92 | """ |
---|
| 93 | |
---|
| 94 | def __init__(self, xs, method='get', fields=[]): |
---|
| 95 | xmlstream.IQ.__init__(self, xs, method) |
---|
| 96 | q = self.addElement((NS_REQUEST, 'query')) |
---|
| 97 | if method == 'set': |
---|
| 98 | # build data form |
---|
[129] | 99 | form_type = 'submit' |
---|
[116] | 100 | form = data_form.Form(form_type, formNamespace=NS_MUC_REGISTER) |
---|
[129] | 101 | q.addChild(form.toElement()) |
---|
| 102 | |
---|
[111] | 103 | for f in fields: |
---|
| 104 | # create a field |
---|
| 105 | form.addField(f) |
---|
| 106 | |
---|
[112] | 107 | |
---|
[129] | 108 | |
---|
[119] | 109 | class AdminRequest(xmlstream.IQ): |
---|
| 110 | """ |
---|
[131] | 111 | A basic admin iq request. |
---|
[119] | 112 | |
---|
| 113 | @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'} |
---|
| 114 | @type method: C{str} |
---|
| 115 | """ |
---|
| 116 | |
---|
| 117 | def __init__(self, xs, method='get'): |
---|
| 118 | xmlstream.IQ.__init__(self, xs, method) |
---|
| 119 | q = self.addElement((NS_MUC_ADMIN, 'query')) |
---|
| 120 | |
---|
| 121 | |
---|
[129] | 122 | |
---|
[147] | 123 | class DestructionRequest(generic.Request): |
---|
[119] | 124 | """ |
---|
[147] | 125 | Room destruction request. |
---|
[119] | 126 | |
---|
[147] | 127 | @param reason: Optional reason for the destruction of this room. |
---|
| 128 | @type reason: C{unicode}. |
---|
| 129 | |
---|
| 130 | @param alternate: Optional room JID of an alternate venue. |
---|
| 131 | @type alternate: L{jid.JID} |
---|
| 132 | |
---|
| 133 | @param password: Optional password for entering the alternate venue. |
---|
| 134 | @type password: C{unicode} |
---|
[119] | 135 | """ |
---|
| 136 | |
---|
[147] | 137 | stanzaType = 'set' |
---|
| 138 | |
---|
| 139 | def __init__(self, recipient, sender=None, reason=None, alternate=None, |
---|
| 140 | password=None): |
---|
| 141 | generic.Request.__init__(self, recipient, sender) |
---|
| 142 | self.reason = reason |
---|
| 143 | self.alternate = alternate |
---|
| 144 | self.password = password |
---|
| 145 | |
---|
| 146 | |
---|
| 147 | def toElement(self): |
---|
| 148 | element = generic.Request.toElement(self) |
---|
| 149 | element.addElement((NS_MUC_OWNER, 'query')) |
---|
| 150 | element.query.addElement('destroy') |
---|
| 151 | |
---|
| 152 | if self.alternate: |
---|
| 153 | element.query.destroy['jid'] = self.alternate.full() |
---|
| 154 | |
---|
| 155 | if self.password: |
---|
| 156 | element.query.destroy.addElement('password', |
---|
| 157 | content=self.password) |
---|
| 158 | |
---|
| 159 | if self.reason: |
---|
| 160 | element.query.destroy.addElement('reason', content=self.reason) |
---|
| 161 | |
---|
| 162 | return element |
---|
[119] | 163 | |
---|
[129] | 164 | |
---|
[119] | 165 | |
---|
| 166 | class AffiliationRequest(AdminRequest): |
---|
[112] | 167 | """ |
---|
| 168 | Register room request. |
---|
| 169 | |
---|
| 170 | @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'} |
---|
| 171 | @type method: C{str} |
---|
| 172 | |
---|
| 173 | @ivar affiliation: The affiliation type to send to room. |
---|
| 174 | @type affiliation: C{str} |
---|
| 175 | """ |
---|
| 176 | |
---|
[131] | 177 | def __init__(self, xs, method='get', affiliation='none', |
---|
| 178 | entityOrNick=None, reason=None): |
---|
[119] | 179 | AdminRequest.__init__(self, xs, method) |
---|
| 180 | |
---|
[127] | 181 | self.affiliation = affiliation |
---|
[131] | 182 | self.reason = reason |
---|
| 183 | if entityOrNick: |
---|
| 184 | self.items([entityOrNick]) |
---|
[112] | 185 | |
---|
[131] | 186 | def items(self, entities=None): |
---|
[129] | 187 | """ |
---|
| 188 | Set or Get the items list for this affiliation request. |
---|
[127] | 189 | """ |
---|
[131] | 190 | if entities: |
---|
| 191 | for entityOrNick in entities: |
---|
| 192 | item = self.query.addElement('item') |
---|
| 193 | item['affiliation'] = self.affiliation |
---|
| 194 | try: |
---|
| 195 | item['jid'] = entityOrNick.full() |
---|
| 196 | except AttributeError: |
---|
| 197 | item['nick'] = entityOrNick |
---|
| 198 | |
---|
| 199 | if self.reason: |
---|
| 200 | item.addElement('reason', content=self.reason) |
---|
[127] | 201 | |
---|
| 202 | return self.query.elements() |
---|
[129] | 203 | |
---|
| 204 | |
---|
[127] | 205 | |
---|
[122] | 206 | class RoleRequest(AdminRequest): |
---|
[131] | 207 | def __init__(self, xs, method='get', role='none', |
---|
| 208 | entityOrNick=None, reason=None): |
---|
[122] | 209 | AdminRequest.__init__(self, xs, method) |
---|
| 210 | |
---|
[131] | 211 | item = self.query.addElement('item') |
---|
| 212 | item['role'] = role |
---|
| 213 | try: |
---|
| 214 | item['jid'] = entityOrNick.full() |
---|
| 215 | except AttributeError: |
---|
| 216 | item['nick'] = entityOrNick |
---|
[127] | 217 | |
---|
[122] | 218 | if reason: |
---|
[131] | 219 | item.addElement('reason', content=self.reason) |
---|
[129] | 220 | |
---|
| 221 | |
---|
| 222 | |
---|
[141] | 223 | class GroupChat(xmppim.Message, DelayMixin): |
---|
[129] | 224 | """ |
---|
[141] | 225 | A groupchat message. |
---|
[110] | 226 | """ |
---|
[141] | 227 | |
---|
| 228 | stanzaType = 'groupchat' |
---|
| 229 | |
---|
| 230 | def toElement(self, legacyDelay=False): |
---|
[127] | 231 | """ |
---|
[141] | 232 | Render into a domish Element. |
---|
| 233 | |
---|
| 234 | @param legacyDelay: If C{True} send the delayed delivery information |
---|
| 235 | in legacy format. |
---|
[110] | 236 | """ |
---|
[141] | 237 | element = xmppim.Message.toElement(self) |
---|
[112] | 238 | |
---|
[141] | 239 | if self.delay: |
---|
| 240 | element.addChild(self.delay.toElement()) |
---|
[112] | 241 | |
---|
[141] | 242 | return element |
---|
[129] | 243 | |
---|
[141] | 244 | |
---|
| 245 | |
---|
| 246 | class PrivateChat(xmppim.Message): |
---|
[129] | 247 | """ |
---|
[141] | 248 | A chat message. |
---|
[112] | 249 | """ |
---|
[127] | 250 | |
---|
[141] | 251 | stanzaType = 'chat' |
---|
[127] | 252 | |
---|
| 253 | |
---|
[129] | 254 | |
---|
[141] | 255 | class InviteMessage(xmppim.Message): |
---|
[129] | 256 | |
---|
[141] | 257 | def __init__(self, recipient=None, sender=None, invitee=None, reason=None): |
---|
| 258 | xmppim.Message.__init__(self, recipient, sender) |
---|
| 259 | self.invitee = invitee |
---|
| 260 | self.reason = reason |
---|
[129] | 261 | |
---|
[110] | 262 | |
---|
[141] | 263 | def toElement(self): |
---|
| 264 | element = xmppim.Message.toElement(self) |
---|
[129] | 265 | |
---|
[141] | 266 | child = element.addElement((NS_MUC_USER, 'x')) |
---|
| 267 | child.addElement('invite') |
---|
| 268 | child.invite['to'] = self.invitee.full() |
---|
[129] | 269 | |
---|
[141] | 270 | if self.reason: |
---|
| 271 | child.invite.addElement('reason', content=self.reason) |
---|
| 272 | |
---|
| 273 | return element |
---|
[110] | 274 | |
---|
[129] | 275 | |
---|
| 276 | |
---|
[121] | 277 | class HistoryOptions(object): |
---|
[129] | 278 | """ |
---|
[141] | 279 | A history configuration object. |
---|
[121] | 280 | |
---|
[129] | 281 | @ivar maxchars: Limit the total number of characters in the history to "X" |
---|
[141] | 282 | (where the character count is the characters of the complete XML |
---|
| 283 | stanzas, not only their XML character data). |
---|
| 284 | @type maxchars: C{int} |
---|
[129] | 285 | |
---|
[141] | 286 | @ivar maxstanzas: Limit the total number of messages in the history to "X". |
---|
| 287 | @type mazstanzas: C{int} |
---|
[121] | 288 | |
---|
[141] | 289 | @ivar seconds: Send only the messages received in the last "X" seconds. |
---|
| 290 | @type seconds: C{int} |
---|
[121] | 291 | |
---|
[141] | 292 | @ivar since: Send only the messages received since the datetime specified. |
---|
| 293 | Note that this must be an offset-aware instance. |
---|
[123] | 294 | @type since: L{datetime.datetime} |
---|
[121] | 295 | """ |
---|
| 296 | attributes = ['maxchars', 'maxstanzas', 'seconds', 'since'] |
---|
| 297 | |
---|
[141] | 298 | def __init__(self, maxchars=None, maxstanzas=None, seconds=None, |
---|
| 299 | since=None): |
---|
[130] | 300 | self.maxchars = maxchars |
---|
[121] | 301 | self.maxstanzas = maxstanzas |
---|
[130] | 302 | self.seconds = seconds |
---|
| 303 | self.since = since |
---|
[121] | 304 | |
---|
[129] | 305 | |
---|
[121] | 306 | def toElement(self): |
---|
[129] | 307 | """ |
---|
[145] | 308 | Returns a L{domish.Element} representing the history options. |
---|
[127] | 309 | """ |
---|
[141] | 310 | element = domish.Element((NS_MUC, 'history')) |
---|
| 311 | |
---|
[121] | 312 | for key in self.attributes: |
---|
[141] | 313 | value = getattr(self, key, None) |
---|
| 314 | if value is not None: |
---|
[122] | 315 | if key == 'since': |
---|
[141] | 316 | stamp = value.astimezone(tzutc()) |
---|
| 317 | element[key] = stamp.strftime('%Y%m%dT%H:%M:%SZ') |
---|
[122] | 318 | else: |
---|
[141] | 319 | element[key] = str(value) |
---|
[129] | 320 | |
---|
[141] | 321 | return element |
---|
[121] | 322 | |
---|
[129] | 323 | |
---|
| 324 | |
---|
[113] | 325 | class User(object): |
---|
| 326 | """ |
---|
| 327 | A user/entity in a multi-user chat room. |
---|
| 328 | """ |
---|
[129] | 329 | |
---|
[130] | 330 | def __init__(self, nick, entity=None): |
---|
[113] | 331 | self.nick = nick |
---|
[130] | 332 | self.entity = entity |
---|
[113] | 333 | self.affiliation = 'none' |
---|
| 334 | self.role = 'none' |
---|
[129] | 335 | |
---|
[113] | 336 | self.status = None |
---|
[130] | 337 | self.show = None |
---|
[113] | 338 | |
---|
| 339 | |
---|
[129] | 340 | |
---|
[107] | 341 | class Room(object): |
---|
| 342 | """ |
---|
[141] | 343 | A Multi User Chat Room. |
---|
| 344 | |
---|
| 345 | An in memory object representing a MUC room from the perspective of |
---|
| 346 | a client. |
---|
| 347 | |
---|
| 348 | @ivar roomIdentifier: The Room ID of the MUC room. |
---|
| 349 | @type roomIdentifier: C{unicode} |
---|
| 350 | |
---|
| 351 | @ivar service: The server where the MUC room is located. |
---|
| 352 | @type service: C{unicode} |
---|
| 353 | |
---|
| 354 | @ivar nick: The nick name for the client in this room. |
---|
| 355 | @type nick: C{unicode} |
---|
| 356 | |
---|
| 357 | @ivar state: The status code of the room. |
---|
| 358 | @type state: L{int} |
---|
| 359 | |
---|
[145] | 360 | @ivar occupantJID: The JID of the occupant in the room. Generated from |
---|
| 361 | roomIdentifier, service, and nick. |
---|
[141] | 362 | @type occupantJID: L{jid.JID} |
---|
[107] | 363 | """ |
---|
| 364 | |
---|
[129] | 365 | |
---|
[130] | 366 | def __init__(self, roomIdentifier, service, nick, state=None): |
---|
[129] | 367 | """ |
---|
| 368 | Initialize the room. |
---|
[107] | 369 | """ |
---|
[130] | 370 | self.roomIdentifier = roomIdentifier |
---|
| 371 | self.service = service |
---|
[141] | 372 | self.setNick(nick) |
---|
| 373 | self.state = state |
---|
| 374 | |
---|
[114] | 375 | self.status = 0 |
---|
[110] | 376 | |
---|
[107] | 377 | self.roster = {} |
---|
| 378 | |
---|
[129] | 379 | |
---|
[141] | 380 | def setNick(self, nick): |
---|
[132] | 381 | self.occupantJID = jid.internJID(u"%s@%s/%s" % (self.roomIdentifier, |
---|
[141] | 382 | self.service, |
---|
| 383 | nick)) |
---|
| 384 | self.nick = nick |
---|
[129] | 385 | |
---|
[107] | 386 | |
---|
[113] | 387 | def addUser(self, user): |
---|
[129] | 388 | """ |
---|
| 389 | Add a user to the room roster. |
---|
[127] | 390 | |
---|
| 391 | @param user: The user object that is being added to the room. |
---|
[131] | 392 | @type user: L{User} |
---|
[113] | 393 | """ |
---|
[141] | 394 | self.roster[user.nick] = user |
---|
[113] | 395 | |
---|
[129] | 396 | |
---|
[113] | 397 | def inRoster(self, user): |
---|
[129] | 398 | """ |
---|
| 399 | Check if a user is in the MUC room. |
---|
[127] | 400 | |
---|
| 401 | @param user: The user object to check. |
---|
[131] | 402 | @type user: L{User} |
---|
[113] | 403 | """ |
---|
| 404 | |
---|
[141] | 405 | return user.nick in self.roster |
---|
[113] | 406 | |
---|
[129] | 407 | |
---|
[113] | 408 | def getUser(self, nick): |
---|
[129] | 409 | """ |
---|
| 410 | Get a user from the room's roster. |
---|
[127] | 411 | |
---|
| 412 | @param nick: The nick for the user in the MUC room. |
---|
[132] | 413 | @type nick: C{unicode} |
---|
[113] | 414 | """ |
---|
[141] | 415 | return self.roster.get(nick) |
---|
[113] | 416 | |
---|
[129] | 417 | |
---|
[114] | 418 | def removeUser(self, user): |
---|
[129] | 419 | """ |
---|
| 420 | Remove a user from the MUC room's roster. |
---|
| 421 | |
---|
[127] | 422 | @param user: The user object to check. |
---|
[131] | 423 | @type user: L{User} |
---|
[127] | 424 | """ |
---|
[114] | 425 | if self.inRoster(user): |
---|
[141] | 426 | del self.roster[user.nick] |
---|
[129] | 427 | |
---|
| 428 | |
---|
[113] | 429 | |
---|
[141] | 430 | class BasicPresence(xmppim.AvailabilityPresence): |
---|
[107] | 431 | """ |
---|
[141] | 432 | Availability presence sent from MUC client to service. |
---|
| 433 | |
---|
| 434 | @type history: L{HistoryOptions} |
---|
| 435 | """ |
---|
| 436 | history = None |
---|
| 437 | password = None |
---|
| 438 | |
---|
| 439 | def toElement(self): |
---|
| 440 | element = xmppim.AvailabilityPresence.toElement(self) |
---|
| 441 | |
---|
| 442 | muc = element.addElement((NS_MUC, 'x')) |
---|
| 443 | if self.password: |
---|
| 444 | muc.addElement('password', content=self.password) |
---|
| 445 | if self.history: |
---|
| 446 | muc.addChild(self.history.toElement()) |
---|
| 447 | |
---|
| 448 | return element |
---|
| 449 | |
---|
| 450 | |
---|
| 451 | |
---|
| 452 | class UserPresence(xmppim.AvailabilityPresence): |
---|
| 453 | """ |
---|
| 454 | Availability presence sent from MUC service to client. |
---|
[107] | 455 | """ |
---|
| 456 | |
---|
[141] | 457 | statusCode = None |
---|
[107] | 458 | |
---|
[141] | 459 | childParsers = {(NS_MUC_USER, 'x'): '_childParser_mucUser'} |
---|
[107] | 460 | |
---|
[141] | 461 | def _childParser_mucUser(self, element): |
---|
| 462 | for child in element.elements(): |
---|
| 463 | if child.uri != NS_MUC_USER: |
---|
| 464 | continue |
---|
| 465 | elif child.name == 'status': |
---|
| 466 | self.statusCode = child.getAttribute('code') |
---|
| 467 | # TODO: item, destroy |
---|
[129] | 468 | |
---|
[141] | 469 | |
---|
| 470 | |
---|
| 471 | class VoiceRequest(xmppim.Message): |
---|
[107] | 472 | """ |
---|
[141] | 473 | Voice request message. |
---|
[107] | 474 | """ |
---|
| 475 | |
---|
[141] | 476 | def toElement(self): |
---|
| 477 | element = xmppim.Message.toElement(self) |
---|
[107] | 478 | |
---|
[110] | 479 | # build data form |
---|
[116] | 480 | form = data_form.Form('submit', formNamespace=NS_MUC_REQUEST) |
---|
[110] | 481 | form.addField(data_form.Field(var='muc#role', |
---|
[129] | 482 | value='participant', |
---|
[110] | 483 | label='Requested role')) |
---|
[141] | 484 | element.addChild(form.toElement()) |
---|
[129] | 485 | |
---|
[141] | 486 | return element |
---|
[129] | 487 | |
---|
[110] | 488 | |
---|
[141] | 489 | |
---|
| 490 | class MUCClient(xmppim.BasePresenceProtocol): |
---|
[107] | 491 | """ |
---|
[130] | 492 | Multi-User Chat client protocol. |
---|
[123] | 493 | |
---|
[130] | 494 | This is a subclass of L{XMPPHandler} and implements L{IMUCCLient}. |
---|
| 495 | |
---|
| 496 | @ivar _rooms: Collection of occupied rooms, keyed by the bare JID of the |
---|
| 497 | room. Note that a particular entity can only join a room once |
---|
| 498 | at a time. |
---|
| 499 | @type _rooms: C{dict} |
---|
[107] | 500 | """ |
---|
| 501 | |
---|
| 502 | implements(IMUCClient) |
---|
| 503 | |
---|
[118] | 504 | timeout = None |
---|
| 505 | |
---|
[145] | 506 | presenceTypeParserMap = { |
---|
| 507 | 'error': generic.ErrorStanza, |
---|
| 508 | 'available': UserPresence, |
---|
| 509 | 'unavailable': UserPresence, |
---|
| 510 | } |
---|
| 511 | |
---|
| 512 | def __init__(self, reactor=None): |
---|
[130] | 513 | XMPPHandler.__init__(self) |
---|
| 514 | |
---|
| 515 | self._rooms = {} |
---|
| 516 | self._deferreds = [] |
---|
| 517 | |
---|
[145] | 518 | if reactor: |
---|
| 519 | self._reactor = reactor |
---|
| 520 | else: |
---|
| 521 | from twisted.internet import reactor |
---|
| 522 | self._reactor = reactor |
---|
| 523 | |
---|
[118] | 524 | |
---|
[107] | 525 | def connectionInitialized(self): |
---|
[129] | 526 | """ |
---|
[141] | 527 | Called when the XML stream has been initialized. |
---|
| 528 | |
---|
| 529 | It initializes several XPath events to handle MUC stanzas that come in. |
---|
| 530 | After those are initialized the method L{initialized} is called to |
---|
| 531 | signal that we have finished. |
---|
[123] | 532 | """ |
---|
[141] | 533 | xmppim.BasePresenceProtocol.connectionInitialized(self) |
---|
[110] | 534 | self.xmlstream.addObserver(GROUPCHAT, self._onGroupChat) |
---|
[115] | 535 | self.initialized() |
---|
[114] | 536 | |
---|
[129] | 537 | |
---|
[130] | 538 | def _addRoom(self, room): |
---|
[129] | 539 | """ |
---|
| 540 | Add a room to the room collection. |
---|
[130] | 541 | |
---|
| 542 | Rooms are stored by the JID of the room itself. I.e. it uses the Room |
---|
| 543 | ID and service parts of the Room JID. |
---|
| 544 | |
---|
| 545 | @note: An entity can only join a particular room once. |
---|
[123] | 546 | """ |
---|
[132] | 547 | roomJID = room.occupantJID.userhostJID() |
---|
| 548 | self._rooms[roomJID] = room |
---|
[107] | 549 | |
---|
[129] | 550 | |
---|
[145] | 551 | def _getRoom(self, roomJID): |
---|
[129] | 552 | """ |
---|
| 553 | Grab a room from the room collection. |
---|
[130] | 554 | |
---|
| 555 | This uses the Room ID and service parts of the given JID to look up |
---|
| 556 | the L{Room} instance associated with it. |
---|
[141] | 557 | |
---|
| 558 | @type occupantJID: L{jid.JID} |
---|
[123] | 559 | """ |
---|
[132] | 560 | return self._rooms.get(roomJID) |
---|
[107] | 561 | |
---|
[129] | 562 | |
---|
[132] | 563 | def _removeRoom(self, occupantJID): |
---|
[129] | 564 | """ |
---|
| 565 | Delete a room from the room collection. |
---|
[123] | 566 | """ |
---|
[132] | 567 | roomJID = occupantJID.userhostJID() |
---|
| 568 | if roomJID in self._rooms: |
---|
| 569 | del self._rooms[roomJID] |
---|
[107] | 570 | |
---|
[113] | 571 | |
---|
[141] | 572 | def unavailableReceived(self, presence): |
---|
[129] | 573 | """ |
---|
[141] | 574 | Unavailable presence was received. |
---|
| 575 | |
---|
| 576 | If this was received from a MUC room occupant JID, that occupant has |
---|
| 577 | left the room. |
---|
[113] | 578 | """ |
---|
[118] | 579 | |
---|
[141] | 580 | occupantJID = presence.sender |
---|
[113] | 581 | |
---|
[141] | 582 | if occupantJID: |
---|
| 583 | self._userLeavesRoom(occupantJID) |
---|
[129] | 584 | |
---|
[141] | 585 | |
---|
| 586 | def errorReceived(self, presence): |
---|
[129] | 587 | """ |
---|
[141] | 588 | Error presence was received. |
---|
| 589 | |
---|
| 590 | If this was received from a MUC room occupant JID, we conclude the |
---|
| 591 | occupant has left the room. |
---|
[113] | 592 | """ |
---|
[141] | 593 | occupantJID = presence.sender |
---|
| 594 | |
---|
| 595 | if occupantJID: |
---|
| 596 | self._userLeavesRoom(occupantJID) |
---|
[129] | 597 | |
---|
[145] | 598 | |
---|
[132] | 599 | def _userLeavesRoom(self, occupantJID): |
---|
[123] | 600 | # when a user leaves a room we need to update it |
---|
[145] | 601 | room = self._getRoom(occupantJID.userhostJID()) |
---|
[114] | 602 | if room is None: |
---|
| 603 | # not in the room yet |
---|
| 604 | return |
---|
| 605 | # check if user is in roster |
---|
[132] | 606 | user = room.getUser(occupantJID.resource) |
---|
[114] | 607 | if user is None: |
---|
| 608 | return |
---|
| 609 | if room.inRoster(user): |
---|
| 610 | room.removeUser(user) |
---|
| 611 | self.userLeftRoom(room, user) |
---|
[129] | 612 | |
---|
| 613 | |
---|
[141] | 614 | def availableReceived(self, presence): |
---|
[129] | 615 | """ |
---|
[141] | 616 | Available presence was received. |
---|
[110] | 617 | """ |
---|
[141] | 618 | |
---|
| 619 | occupantJID = presence.sender |
---|
| 620 | |
---|
| 621 | if not occupantJID: |
---|
[113] | 622 | return |
---|
[129] | 623 | |
---|
[113] | 624 | # grab room |
---|
[145] | 625 | room = self._getRoom(occupantJID.userhostJID()) |
---|
[113] | 626 | if room is None: |
---|
| 627 | # not in the room yet |
---|
| 628 | return |
---|
[141] | 629 | |
---|
| 630 | user = self._changeUserStatus(room, occupantJID, presence.status, |
---|
| 631 | presence.show) |
---|
[113] | 632 | |
---|
| 633 | if room.inRoster(user): |
---|
[129] | 634 | # we changed status or nick |
---|
[141] | 635 | if presence.statusCode: |
---|
| 636 | room.status = presence.statusCode # XXX |
---|
[113] | 637 | else: |
---|
[141] | 638 | self.userUpdatedStatus(room, user, presence.show, |
---|
| 639 | presence.status) |
---|
[129] | 640 | else: |
---|
[113] | 641 | room.addUser(user) |
---|
| 642 | self.userJoinedRoom(room, user) |
---|
[129] | 643 | |
---|
[110] | 644 | |
---|
[141] | 645 | def _onGroupChat(self, element): |
---|
[129] | 646 | """ |
---|
| 647 | A group chat message has been received from a MUC room. |
---|
| 648 | |
---|
[145] | 649 | There are a few event methods that may get called here. |
---|
| 650 | L{receivedGroupChat}, L{receivedHistory} or L{receivedHistory}. |
---|
[110] | 651 | """ |
---|
[141] | 652 | message = GroupChat.fromElement(element) |
---|
| 653 | |
---|
| 654 | occupantJID = message.sender |
---|
| 655 | if not occupantJID: |
---|
[113] | 656 | return |
---|
| 657 | |
---|
[145] | 658 | roomJID = occupantJID.userhostJID() |
---|
| 659 | |
---|
| 660 | room = self._getRoom(roomJID) |
---|
[113] | 661 | if room is None: |
---|
| 662 | # not in the room yet |
---|
| 663 | return |
---|
[141] | 664 | |
---|
| 665 | if occupantJID.resource: |
---|
| 666 | user = room.getUser(occupantJID.resource) |
---|
[112] | 667 | else: |
---|
[141] | 668 | # This message is from the room itself. |
---|
| 669 | user = None |
---|
| 670 | |
---|
[145] | 671 | if message.subject: |
---|
| 672 | self.receivedSubject(room, user, message.subject) |
---|
| 673 | elif message.delay is None: |
---|
[141] | 674 | self.receivedGroupChat(room, user, message) |
---|
| 675 | else: |
---|
| 676 | self.receivedHistory(room, user, message) |
---|
[110] | 677 | |
---|
| 678 | |
---|
[145] | 679 | def _joinedRoom(self, presence): |
---|
[129] | 680 | """ |
---|
| 681 | We have presence that says we joined a room. |
---|
[107] | 682 | """ |
---|
[145] | 683 | roomJID = presence.sender.userhostJID() |
---|
[129] | 684 | |
---|
[145] | 685 | # change the state of the room |
---|
| 686 | room = self._getRoom(roomJID) |
---|
| 687 | room.state = 'joined' |
---|
[129] | 688 | |
---|
[145] | 689 | # grab status |
---|
| 690 | if presence.statusCode: |
---|
| 691 | room.status = presence.statusCode |
---|
[110] | 692 | |
---|
[145] | 693 | return room |
---|
[107] | 694 | |
---|
[111] | 695 | |
---|
[145] | 696 | def _leftRoom(self, presence): |
---|
[129] | 697 | """ |
---|
[130] | 698 | We have presence that says we left a room. |
---|
[111] | 699 | """ |
---|
[145] | 700 | occupantJID = presence.sender |
---|
[129] | 701 | |
---|
[145] | 702 | # change the state of the room |
---|
| 703 | self._removeRoom(occupantJID) |
---|
[129] | 704 | |
---|
[145] | 705 | return True |
---|
[111] | 706 | |
---|
[129] | 707 | |
---|
[115] | 708 | def initialized(self): |
---|
[129] | 709 | """ |
---|
| 710 | Client is initialized and ready! |
---|
[115] | 711 | """ |
---|
| 712 | pass |
---|
| 713 | |
---|
[129] | 714 | |
---|
[113] | 715 | def userJoinedRoom(self, room, user): |
---|
[129] | 716 | """ |
---|
| 717 | User has joined a MUC room. |
---|
[125] | 718 | |
---|
| 719 | This method will need to be modified inorder for clients to |
---|
| 720 | do something when this event occurs. |
---|
| 721 | |
---|
| 722 | @param room: The room the user has joined. |
---|
[131] | 723 | @type room: L{Room} |
---|
[125] | 724 | |
---|
| 725 | @param user: The user that joined the MUC room. |
---|
[131] | 726 | @type user: L{User} |
---|
[113] | 727 | """ |
---|
| 728 | pass |
---|
| 729 | |
---|
[129] | 730 | |
---|
[114] | 731 | def userLeftRoom(self, room, user): |
---|
[129] | 732 | """ |
---|
| 733 | User has left a room. |
---|
| 734 | |
---|
[125] | 735 | This method will need to be modified inorder for clients to |
---|
| 736 | do something when this event occurs. |
---|
| 737 | |
---|
| 738 | @param room: The room the user has joined. |
---|
[131] | 739 | @type room: L{Room} |
---|
[125] | 740 | |
---|
[141] | 741 | @param user: The user that left the MUC room. |
---|
[131] | 742 | @type user: L{User} |
---|
[114] | 743 | """ |
---|
| 744 | pass |
---|
| 745 | |
---|
[113] | 746 | |
---|
[115] | 747 | def userUpdatedStatus(self, room, user, show, status): |
---|
[129] | 748 | """ |
---|
[131] | 749 | User Presence has been received. |
---|
[129] | 750 | |
---|
[125] | 751 | This method will need to be modified inorder for clients to |
---|
| 752 | do something when this event occurs. |
---|
[107] | 753 | """ |
---|
| 754 | pass |
---|
[129] | 755 | |
---|
[108] | 756 | |
---|
[113] | 757 | def receivedSubject(self, room, subject): |
---|
[110] | 758 | """ |
---|
[125] | 759 | This method will need to be modified inorder for clients to |
---|
| 760 | do something when this event occurs. |
---|
[110] | 761 | """ |
---|
| 762 | pass |
---|
| 763 | |
---|
[112] | 764 | |
---|
[141] | 765 | def receivedGroupChat(self, room, user, message): |
---|
[112] | 766 | """ |
---|
[141] | 767 | A groupchat message was received. |
---|
| 768 | |
---|
| 769 | @param room: The room the message was received from. |
---|
| 770 | @type room: L{Room} |
---|
| 771 | |
---|
| 772 | @param user: The user that sent the message, or C{None} if it was a |
---|
| 773 | message from the room itself. |
---|
| 774 | @type user: L{User} |
---|
| 775 | |
---|
| 776 | @param message: The message. |
---|
| 777 | @type message: L{GroupChat} |
---|
[112] | 778 | """ |
---|
| 779 | pass |
---|
| 780 | |
---|
| 781 | |
---|
[141] | 782 | def receivedHistory(self, room, user, message): |
---|
| 783 | """ |
---|
| 784 | A groupchat message from the room's discussion history was received. |
---|
[129] | 785 | |
---|
[141] | 786 | This is identical to L{receivedGroupChat}, with the delayed delivery |
---|
| 787 | information (timestamp and original sender) in C{message.delay}. For |
---|
| 788 | anonymous rooms, C{message.delay.sender} is the room's address. |
---|
| 789 | |
---|
| 790 | @param room: The room the message was received from. |
---|
| 791 | @type room: L{Room} |
---|
| 792 | |
---|
| 793 | @param user: The user that sent the message, or C{None} if it was a |
---|
| 794 | message from the room itself. |
---|
| 795 | @type user: L{User} |
---|
| 796 | |
---|
| 797 | @param message: The message. |
---|
| 798 | @type message: L{GroupChat} |
---|
| 799 | """ |
---|
| 800 | pass |
---|
[129] | 801 | |
---|
[118] | 802 | |
---|
[145] | 803 | def sendDeferred(self, stanza): |
---|
[129] | 804 | """ |
---|
[145] | 805 | Send presence stanza, adding a deferred with a timeout. |
---|
[129] | 806 | |
---|
[145] | 807 | @param stanza: The presence stanza to send over the wire. |
---|
| 808 | @type stanza: L{generic.Stanza} |
---|
[125] | 809 | |
---|
[145] | 810 | @param timeout: The number of seconds to wait before the deferred is |
---|
| 811 | timed out. |
---|
[131] | 812 | @type timeout: L{int} |
---|
[125] | 813 | |
---|
| 814 | The deferred object L{defer.Deferred} is returned. |
---|
[118] | 815 | """ |
---|
| 816 | d = defer.Deferred() |
---|
| 817 | |
---|
[145] | 818 | def onResponse(element): |
---|
| 819 | if element.getAttribute('type') == 'error': |
---|
| 820 | d.errback(error.exceptionFromStanza(element)) |
---|
| 821 | else: |
---|
| 822 | d.callback(UserPresence.fromElement(element)) |
---|
[118] | 823 | |
---|
| 824 | def onTimeout(): |
---|
[145] | 825 | d.errback(xmlstream.TimeoutError("Timeout waiting for response.")) |
---|
[118] | 826 | |
---|
[145] | 827 | call = self._reactor.callLater(DEFER_TIMEOUT, onTimeout) |
---|
[129] | 828 | |
---|
[118] | 829 | def cancelTimeout(result): |
---|
| 830 | if call.active(): |
---|
| 831 | call.cancel() |
---|
| 832 | |
---|
| 833 | return result |
---|
| 834 | |
---|
| 835 | d.addBoth(cancelTimeout) |
---|
| 836 | |
---|
[145] | 837 | query = "/presence[@from='%s' or (@from='%s' and @type='error')]" % ( |
---|
| 838 | stanza.recipient.full(), stanza.recipient.userhost()) |
---|
| 839 | self.xmlstream.addOnetimeObserver(query, onResponse, priority=1) |
---|
| 840 | self.xmlstream.send(stanza.toElement()) |
---|
[118] | 841 | return d |
---|
| 842 | |
---|
[129] | 843 | |
---|
[148] | 844 | def configure(self, roomJID, options): |
---|
[129] | 845 | """ |
---|
[131] | 846 | Configure a room. |
---|
[117] | 847 | |
---|
[148] | 848 | @param roomJID: The room to configure. |
---|
[132] | 849 | @type roomJID: L{jid.JID} |
---|
[117] | 850 | |
---|
[148] | 851 | @param options: A mapping of field names to values, or C{None} to cancel. |
---|
| 852 | @type options: C{dict} |
---|
[110] | 853 | """ |
---|
[148] | 854 | if not options: |
---|
| 855 | options = False |
---|
| 856 | request = ConfigureRequest(recipient=roomJID, options=options) |
---|
| 857 | return self.request(request) |
---|
[129] | 858 | |
---|
[110] | 859 | |
---|
[148] | 860 | def getConfiguration(self, roomJID): |
---|
[129] | 861 | """ |
---|
[148] | 862 | Grab the configuration from the room. |
---|
[145] | 863 | |
---|
| 864 | This sends an iq request to the room. |
---|
[117] | 865 | |
---|
[132] | 866 | @param roomJID: The bare JID of the room. |
---|
| 867 | @type roomJID: L{jid.JID} |
---|
[148] | 868 | |
---|
| 869 | @return: A deferred that fires with the room's configuration form as |
---|
| 870 | a L{data_form.Form} or C{None} if there are no configuration |
---|
| 871 | options available. |
---|
[117] | 872 | """ |
---|
[148] | 873 | def cb(response): |
---|
| 874 | form = data_form.findForm(response.query, NS_MUC_CONFIG) |
---|
| 875 | return form |
---|
| 876 | |
---|
| 877 | request = ConfigureRequest(recipient=roomJID, options=None) |
---|
| 878 | d = self.request(request) |
---|
| 879 | d.addCallback(cb) |
---|
| 880 | return d |
---|
[110] | 881 | |
---|
| 882 | |
---|
[130] | 883 | def join(self, service, roomIdentifier, nick, history=None): |
---|
[129] | 884 | """ |
---|
[130] | 885 | Join a MUC room by sending presence to it. |
---|
[129] | 886 | |
---|
[117] | 887 | @param server: The server where the room is located. |
---|
[132] | 888 | @type server: C{unicode} |
---|
[131] | 889 | |
---|
[117] | 890 | @param room: The room name the entity is joining. |
---|
[132] | 891 | @type room: C{unicode} |
---|
[131] | 892 | |
---|
[117] | 893 | @param nick: The nick name for the entitity joining the room. |
---|
[132] | 894 | @type nick: C{unicode} |
---|
[131] | 895 | |
---|
[121] | 896 | @param history: The maximum number of history stanzas you would like. |
---|
| 897 | |
---|
[130] | 898 | @return: A deferred that fires when the entity is in the room or an |
---|
| 899 | error has occurred. |
---|
[107] | 900 | """ |
---|
[130] | 901 | room = Room(roomIdentifier, service, nick, state='joining') |
---|
| 902 | self._addRoom(room) |
---|
[129] | 903 | |
---|
[141] | 904 | presence = BasicPresence(recipient=room.occupantJID) |
---|
| 905 | if history: |
---|
| 906 | presence.history = HistoryOptions(maxstanzas=history) |
---|
[121] | 907 | |
---|
[145] | 908 | d = self.sendDeferred(presence) |
---|
| 909 | d.addCallback(self._joinedRoom) |
---|
[107] | 910 | return d |
---|
[129] | 911 | |
---|
| 912 | |
---|
[132] | 913 | def _changeUserStatus(self, room, occupantJID, status, show): |
---|
[130] | 914 | """ |
---|
| 915 | Change the user status in a room. |
---|
| 916 | """ |
---|
[127] | 917 | |
---|
[126] | 918 | # check if user is in roster |
---|
[132] | 919 | user = room.getUser(occupantJID.resource) |
---|
[126] | 920 | if user is None: # create a user that does not exist |
---|
[132] | 921 | user = User(occupantJID.resource) |
---|
[126] | 922 | |
---|
| 923 | if status is not None: |
---|
[130] | 924 | user.status = unicode(status) |
---|
[126] | 925 | if show is not None: |
---|
[130] | 926 | user.show = unicode(show) |
---|
[126] | 927 | |
---|
| 928 | return user |
---|
| 929 | |
---|
[129] | 930 | |
---|
[145] | 931 | def _changed(self, presence, occupantJID): |
---|
[129] | 932 | """ |
---|
| 933 | Callback for changing the nick and status. |
---|
[117] | 934 | """ |
---|
[145] | 935 | room = self._getRoom(occupantJID.userhostJID()) |
---|
| 936 | self._changeUserStatus(room, occupantJID, presence.status, presence.show) |
---|
[117] | 937 | |
---|
[145] | 938 | return room |
---|
[117] | 939 | |
---|
| 940 | |
---|
[141] | 941 | def nick(self, roomJID, nick): |
---|
[129] | 942 | """ |
---|
[141] | 943 | Change an entity's nick name in a MUC room. |
---|
[129] | 944 | |
---|
[117] | 945 | See: http://xmpp.org/extensions/xep-0045.html#changenick |
---|
| 946 | |
---|
[132] | 947 | @param roomJID: The JID of the room, i.e. without a resource. |
---|
| 948 | @type roomJID: L{jid.JID} |
---|
[117] | 949 | |
---|
[141] | 950 | @param nick: The new nick name within the room. |
---|
| 951 | @type nick: C{unicode} |
---|
[117] | 952 | """ |
---|
[132] | 953 | room = self._getRoom(roomJID) |
---|
[130] | 954 | |
---|
| 955 | # Change the nickname |
---|
[141] | 956 | room.setNick(nick) |
---|
[130] | 957 | |
---|
| 958 | # Create presence |
---|
[141] | 959 | presence = BasicPresence(recipient=room.occupantJID) |
---|
[117] | 960 | |
---|
[145] | 961 | d = self.sendDeferred(presence) |
---|
| 962 | d.addCallback(self._changed, room.occupantJID) |
---|
[117] | 963 | return d |
---|
[107] | 964 | |
---|
[129] | 965 | |
---|
[145] | 966 | def leave(self, roomJID): |
---|
[129] | 967 | """ |
---|
| 968 | Leave a MUC room. |
---|
[123] | 969 | |
---|
| 970 | See: http://xmpp.org/extensions/xep-0045.html#exit |
---|
| 971 | |
---|
[145] | 972 | @param roomJID: The Room JID of the room to leave. |
---|
| 973 | @type roomJID: L{jid.JID} |
---|
[111] | 974 | """ |
---|
[145] | 975 | room = self._getRoom(roomJID) |
---|
[129] | 976 | |
---|
[141] | 977 | presence = xmppim.AvailabilityPresence(recipient=room.occupantJID, |
---|
| 978 | available=False) |
---|
[111] | 979 | |
---|
[145] | 980 | d = self.sendDeferred(presence) |
---|
| 981 | d.addCallback(self._leftRoom) |
---|
[111] | 982 | return d |
---|
[129] | 983 | |
---|
[111] | 984 | |
---|
[145] | 985 | def status(self, roomJID, show=None, status=None): |
---|
[129] | 986 | """ |
---|
| 987 | Change user status. |
---|
[119] | 988 | |
---|
| 989 | See: http://xmpp.org/extensions/xep-0045.html#changepres |
---|
| 990 | |
---|
[145] | 991 | @param roomJID: The Room JID of the room. |
---|
| 992 | @type roomJID: L{jid.JID} |
---|
[119] | 993 | |
---|
[145] | 994 | @param show: The availability of the entity. Common values are xa, |
---|
| 995 | available, etc |
---|
[132] | 996 | @type show: C{unicode} |
---|
[119] | 997 | |
---|
[129] | 998 | @param show: The current status of the entity. |
---|
[132] | 999 | @type show: C{unicode} |
---|
[119] | 1000 | """ |
---|
[145] | 1001 | room = self._getRoom(roomJID) |
---|
[119] | 1002 | |
---|
[141] | 1003 | presence = BasicPresence(recipient=room.occupantJID, |
---|
| 1004 | show=show, status=status) |
---|
[129] | 1005 | |
---|
[145] | 1006 | d = self.sendDeferred(presence) |
---|
| 1007 | d.addCallback(self._changed, room.occupantJID) |
---|
[119] | 1008 | return d |
---|
[111] | 1009 | |
---|
[129] | 1010 | |
---|
[141] | 1011 | def _sendMessage(self, message, children=None): |
---|
[130] | 1012 | """ |
---|
| 1013 | Send a message. |
---|
| 1014 | """ |
---|
[141] | 1015 | element = message.toElement() |
---|
[110] | 1016 | if children: |
---|
[130] | 1017 | for child in children: |
---|
[141] | 1018 | element.addChild(child) |
---|
[129] | 1019 | |
---|
[141] | 1020 | self.xmlstream.send(element) |
---|
[110] | 1021 | |
---|
[129] | 1022 | |
---|
[141] | 1023 | def groupChat(self, roomJID, body, children=None): |
---|
[129] | 1024 | """ |
---|
[131] | 1025 | Send a groupchat message. |
---|
[112] | 1026 | """ |
---|
[141] | 1027 | message = GroupChat(recipient=roomJID, body=body) |
---|
| 1028 | self._sendMessage(message, children=children) |
---|
[129] | 1029 | |
---|
[112] | 1030 | |
---|
[141] | 1031 | def chat(self, occupantJID, body, children=None): |
---|
[129] | 1032 | """ |
---|
| 1033 | Send a private chat message to a user in a MUC room. |
---|
| 1034 | |
---|
[125] | 1035 | See: http://xmpp.org/extensions/xep-0045.html#privatemessage |
---|
| 1036 | |
---|
[132] | 1037 | @param occupantJID: The Room JID of the other user. |
---|
| 1038 | @type occupantJID: L{jid.JID} |
---|
[125] | 1039 | """ |
---|
[141] | 1040 | message = PrivateChat(recipient=occupantJID, body=body) |
---|
| 1041 | self._sendMessage(message, children=children) |
---|
[125] | 1042 | |
---|
[112] | 1043 | |
---|
[141] | 1044 | def invite(self, roomJID, invitee, reason=None): |
---|
[129] | 1045 | """ |
---|
| 1046 | Invite a xmpp entity to a MUC room. |
---|
[125] | 1047 | |
---|
| 1048 | See: http://xmpp.org/extensions/xep-0045.html#invite |
---|
| 1049 | |
---|
[132] | 1050 | @param roomJID: The bare JID of the room. |
---|
| 1051 | @type roomJID: L{jid.JID} |
---|
[131] | 1052 | |
---|
[141] | 1053 | @param invitee: The entity that is being invited. |
---|
| 1054 | @type invitee: L{jid.JID} |
---|
| 1055 | |
---|
[125] | 1056 | @param reason: The reason for the invite. |
---|
[132] | 1057 | @type reason: C{unicode} |
---|
[113] | 1058 | """ |
---|
[141] | 1059 | message = InviteMessage(recipient=roomJID, invitee=invitee, |
---|
| 1060 | reason=reason) |
---|
| 1061 | self._sendMessage(message) |
---|
[112] | 1062 | |
---|
| 1063 | |
---|
[132] | 1064 | def password(self, roomJID, password): |
---|
[129] | 1065 | """ |
---|
| 1066 | Send a password to a room so the entity can join. |
---|
| 1067 | |
---|
[125] | 1068 | See: http://xmpp.org/extensions/xep-0045.html#enter-pw |
---|
| 1069 | |
---|
[132] | 1070 | @param roomJID: The bare JID of the room. |
---|
| 1071 | @type roomJID: L{jid.JID} |
---|
[129] | 1072 | |
---|
[125] | 1073 | @param password: The MUC room password. |
---|
[132] | 1074 | @type password: C{unicode} |
---|
[125] | 1075 | """ |
---|
[141] | 1076 | presence = BasicPresence(roomJID) |
---|
| 1077 | presence.password = password |
---|
[112] | 1078 | |
---|
[141] | 1079 | self.xmlstream.send(presence.toElement()) |
---|
[129] | 1080 | |
---|
| 1081 | |
---|
[132] | 1082 | def register(self, roomJID, fields=[]): |
---|
[129] | 1083 | """ |
---|
| 1084 | Send a request to register for a room. |
---|
[127] | 1085 | |
---|
[132] | 1086 | @param roomJID: The bare JID of the room. |
---|
| 1087 | @type roomJID: L{jid.JID} |
---|
[127] | 1088 | |
---|
| 1089 | @param fields: The fields you need to register. |
---|
[131] | 1090 | @type fields: L{list} of L{dataform.Field} |
---|
[125] | 1091 | """ |
---|
[111] | 1092 | iq = RegisterRequest(self.xmlstream, method='set', fields=fields) |
---|
[132] | 1093 | iq['to'] = roomJID.userhost() |
---|
[111] | 1094 | return iq.send() |
---|
| 1095 | |
---|
[120] | 1096 | |
---|
[132] | 1097 | def _getAffiliationList(self, roomJID, affiliation): |
---|
[131] | 1098 | """ |
---|
| 1099 | Send a request for an affiliation list in a room. |
---|
| 1100 | """ |
---|
[120] | 1101 | iq = AffiliationRequest(self.xmlstream, |
---|
| 1102 | method='get', |
---|
[129] | 1103 | affiliation=affiliation, |
---|
[120] | 1104 | ) |
---|
[132] | 1105 | iq['to'] = roomJID.userhost() |
---|
[129] | 1106 | return iq.send() |
---|
[120] | 1107 | |
---|
| 1108 | |
---|
[132] | 1109 | def _getRoleList(self, roomJID, role): |
---|
[131] | 1110 | """ |
---|
| 1111 | Send a role request. |
---|
| 1112 | """ |
---|
[123] | 1113 | iq = RoleRequest(self.xmlstream, |
---|
[131] | 1114 | method='get', |
---|
| 1115 | role=role, |
---|
| 1116 | ) |
---|
[132] | 1117 | iq['to'] = roomJID.full() |
---|
[129] | 1118 | return iq.send() |
---|
[123] | 1119 | |
---|
| 1120 | |
---|
[132] | 1121 | def _setAffiliationList(self, iq, affiliation, occupantJID): |
---|
[125] | 1122 | # set a rooms affiliation list |
---|
[132] | 1123 | room = self._getRoom(occupantJID) |
---|
[130] | 1124 | if room is not None: |
---|
[123] | 1125 | affiliation_list = [] |
---|
[130] | 1126 | setattr(room, affiliation, []) |
---|
[129] | 1127 | |
---|
[123] | 1128 | for item in iq.query.elements(): |
---|
[130] | 1129 | nick = item.getAttribute('nick', None) |
---|
[123] | 1130 | entity = item.getAttribute('jid', None) |
---|
[130] | 1131 | role = item.getAttribute('role', None) |
---|
| 1132 | user = None |
---|
[123] | 1133 | if nick is None and entity is None: |
---|
| 1134 | raise Exception, 'bad attributes in item list' |
---|
| 1135 | if nick is not None: |
---|
[130] | 1136 | user = room.getUser(nick) |
---|
| 1137 | if user is None: |
---|
| 1138 | user = User(nick, entity=jid.internJID(entity)) |
---|
| 1139 | user.affiliation = 'member' |
---|
| 1140 | if role is not None: |
---|
| 1141 | user.role = role |
---|
[129] | 1142 | |
---|
[130] | 1143 | affiliation_list.append(user) |
---|
[123] | 1144 | |
---|
[130] | 1145 | setattr(room, affiliation, affiliation_list) |
---|
| 1146 | return room |
---|
[120] | 1147 | |
---|
[129] | 1148 | |
---|
[132] | 1149 | def getMemberList(self, roomJID): |
---|
[129] | 1150 | """ |
---|
[130] | 1151 | Get the member list of a room. |
---|
[119] | 1152 | |
---|
[132] | 1153 | @param roomJID: The bare JID of the room. |
---|
| 1154 | @type roomJID: L{jid.JID} |
---|
[119] | 1155 | """ |
---|
[132] | 1156 | d = self._getAffiliationList(roomJID, 'member') |
---|
| 1157 | d.addCallback(self._setAffiliationList, 'members', roomJID) |
---|
[123] | 1158 | return d |
---|
[120] | 1159 | |
---|
[129] | 1160 | |
---|
[132] | 1161 | def getAdminList(self, roomJID): |
---|
[129] | 1162 | """ |
---|
[130] | 1163 | Get the admin list of a room. |
---|
[120] | 1164 | |
---|
[132] | 1165 | @param roomJID: The bare JID of the room. |
---|
| 1166 | @type roomJID: L{jid.JID} |
---|
[120] | 1167 | """ |
---|
[132] | 1168 | d = self._getAffiliationList(roomJID, 'admin') |
---|
| 1169 | d.addCallback(self._setAffiliationList, 'admin', roomJID) |
---|
[123] | 1170 | return d |
---|
[120] | 1171 | |
---|
[129] | 1172 | |
---|
[132] | 1173 | def getBanList(self, roomJID): |
---|
[129] | 1174 | """ |
---|
| 1175 | Get an outcast list from a room. |
---|
[120] | 1176 | |
---|
[132] | 1177 | @param roomJID: The bare JID of the room. |
---|
| 1178 | @type roomJID: L{jid.JID} |
---|
[120] | 1179 | """ |
---|
[132] | 1180 | d = self._getAffiliationList(roomJID, 'outcast') |
---|
| 1181 | d.addCallback(self._setAffiliationList, 'outcast', roomJID) |
---|
[123] | 1182 | return d |
---|
[120] | 1183 | |
---|
[129] | 1184 | |
---|
[132] | 1185 | def getOwnerList(self, roomJID): |
---|
[129] | 1186 | """ |
---|
| 1187 | Get an owner list from a room. |
---|
[120] | 1188 | |
---|
[145] | 1189 | @param roomJID: The room jabber/xmpp entity id for the requested member |
---|
| 1190 | list. |
---|
[132] | 1191 | @type roomJID: L{jid.JID} |
---|
[120] | 1192 | """ |
---|
[132] | 1193 | d = self._getAffiliationList(roomJID, 'owner') |
---|
| 1194 | d.addCallback(self._setAffiliationList, 'owner', roomJID) |
---|
[123] | 1195 | return d |
---|
[119] | 1196 | |
---|
[129] | 1197 | |
---|
[141] | 1198 | def getRegisterForm(self, roomJID): |
---|
[129] | 1199 | """ |
---|
| 1200 | Grab the registration form for a MUC room. |
---|
[119] | 1201 | |
---|
[145] | 1202 | @param room: The room jabber/xmpp entity id for the requested |
---|
| 1203 | registration form. |
---|
[131] | 1204 | @type room: L{jid.JID} |
---|
[113] | 1205 | """ |
---|
[111] | 1206 | iq = RegisterRequest(self.xmlstream) |
---|
[141] | 1207 | iq['to'] = roomJID.userhost() |
---|
[111] | 1208 | return iq.send() |
---|
| 1209 | |
---|
[129] | 1210 | |
---|
[147] | 1211 | def destroy(self, roomJID, reason=None, alternate=None, password=None): |
---|
[129] | 1212 | """ |
---|
| 1213 | Destroy a room. |
---|
| 1214 | |
---|
[147] | 1215 | @param roomJID: The JID of the room. |
---|
[132] | 1216 | @type roomJID: L{jid.JID} |
---|
[129] | 1217 | |
---|
[147] | 1218 | @param reason: The reason for the destruction of the room. |
---|
[132] | 1219 | @type reason: C{unicode} |
---|
[127] | 1220 | |
---|
[147] | 1221 | @param alternate: The JID of the room suggested as an alternate venue. |
---|
[131] | 1222 | @type alternate: L{jid.JID} |
---|
| 1223 | |
---|
[120] | 1224 | """ |
---|
| 1225 | def destroyed(iq): |
---|
[132] | 1226 | self._removeRoom(roomJID) |
---|
[120] | 1227 | |
---|
[147] | 1228 | request = DestructionRequest(recipient=roomJID, reason=reason, |
---|
| 1229 | alternate=alternate, password=password) |
---|
[131] | 1230 | |
---|
[147] | 1231 | d = self.request(request) |
---|
| 1232 | d.addCallback(destroyed) |
---|
| 1233 | return d |
---|
[120] | 1234 | |
---|
[129] | 1235 | |
---|
[132] | 1236 | def subject(self, roomJID, subject): |
---|
[129] | 1237 | """ |
---|
| 1238 | Change the subject of a MUC room. |
---|
[127] | 1239 | |
---|
| 1240 | See: http://xmpp.org/extensions/xep-0045.html#subject-mod |
---|
| 1241 | |
---|
[132] | 1242 | @param roomJID: The bare JID of the room. |
---|
| 1243 | @type roomJID: L{jid.JID} |
---|
[127] | 1244 | |
---|
| 1245 | @param subject: The subject you want to set. |
---|
[132] | 1246 | @type subject: C{unicode} |
---|
[110] | 1247 | """ |
---|
[132] | 1248 | msg = GroupChat(roomJID.userhostJID(), subject=subject) |
---|
[144] | 1249 | self.xmlstream.send(msg.toElement()) |
---|
[110] | 1250 | |
---|
[129] | 1251 | |
---|
[132] | 1252 | def voice(self, roomJID): |
---|
[129] | 1253 | """ |
---|
| 1254 | Request voice for a moderated room. |
---|
[127] | 1255 | |
---|
[132] | 1256 | @param roomJID: The room jabber/xmpp entity id. |
---|
| 1257 | @type roomJID: L{jid.JID} |
---|
[110] | 1258 | """ |
---|
[141] | 1259 | message = VoiceRequest(recipient=roomJID) |
---|
| 1260 | self.xmlstream.send(message.toElement()) |
---|
[110] | 1261 | |
---|
| 1262 | |
---|
[132] | 1263 | def history(self, roomJID, messages): |
---|
[129] | 1264 | """ |
---|
| 1265 | Send history to create a MUC based on a one on one chat. |
---|
[122] | 1266 | |
---|
[127] | 1267 | See: http://xmpp.org/extensions/xep-0045.html#continue |
---|
| 1268 | |
---|
[132] | 1269 | @param roomJID: The room jabber/xmpp entity id. |
---|
| 1270 | @type roomJID: L{jid.JID} |
---|
[127] | 1271 | |
---|
[131] | 1272 | @param messages: The history to send to the room as an ordered list of |
---|
| 1273 | message, represented by a dictionary with the keys |
---|
| 1274 | C{'stanza'}, holding the original stanza a |
---|
| 1275 | L{domish.Element}, and C{'timestamp'} with the |
---|
| 1276 | timestamp. |
---|
| 1277 | @type messages: L{list} of L{domish.Element} |
---|
[110] | 1278 | """ |
---|
[129] | 1279 | |
---|
[131] | 1280 | for message in messages: |
---|
| 1281 | stanza = message['stanza'] |
---|
| 1282 | stanza['type'] = 'groupchat' |
---|
[110] | 1283 | |
---|
[141] | 1284 | delay = Delay(stamp=message['timestamp']) |
---|
[110] | 1285 | |
---|
[141] | 1286 | sender = stanza.getAttribute('from') |
---|
[131] | 1287 | if sender is not None: |
---|
[141] | 1288 | delay.sender = jid.JID(sender) |
---|
| 1289 | |
---|
| 1290 | stanza.addChild(delay.toElement()) |
---|
[110] | 1291 | |
---|
[132] | 1292 | stanza['to'] = roomJID.userhost() |
---|
[131] | 1293 | if stanza.hasAttribute('from'): |
---|
| 1294 | del stanza['from'] |
---|
[129] | 1295 | |
---|
[131] | 1296 | self.xmlstream.send(stanza) |
---|
| 1297 | |
---|
| 1298 | |
---|
[132] | 1299 | def _setAffiliation(self, roomJID, entityOrNick, affiliation, |
---|
[131] | 1300 | reason=None, sender=None): |
---|
| 1301 | """ |
---|
| 1302 | Send a request to change an entity's affiliation to a MUC room. |
---|
| 1303 | """ |
---|
[112] | 1304 | iq = AffiliationRequest(self.xmlstream, |
---|
| 1305 | method='set', |
---|
[131] | 1306 | entityOrNick=entityOrNick, |
---|
[122] | 1307 | affiliation=affiliation, |
---|
[112] | 1308 | reason=reason) |
---|
[132] | 1309 | iq['to'] = roomJID.userhost() |
---|
[131] | 1310 | if sender is not None: |
---|
| 1311 | iq['from'] = unicode(sender) |
---|
| 1312 | |
---|
[112] | 1313 | return iq.send() |
---|
| 1314 | |
---|
[129] | 1315 | |
---|
[132] | 1316 | def _setRole(self, roomJID, entityOrNick, role, |
---|
[131] | 1317 | reason=None, sender=None): |
---|
[127] | 1318 | # send a role request |
---|
[122] | 1319 | iq = RoleRequest(self.xmlstream, |
---|
| 1320 | method='set', |
---|
| 1321 | role=role, |
---|
[131] | 1322 | entityOrNick=entityOrNick, |
---|
[122] | 1323 | reason=reason) |
---|
[129] | 1324 | |
---|
[132] | 1325 | iq['to'] = roomJID.userhost() |
---|
[131] | 1326 | if sender is not None: |
---|
| 1327 | iq['from'] = unicode(sender) |
---|
[112] | 1328 | return iq.send() |
---|
[122] | 1329 | |
---|
[129] | 1330 | |
---|
[132] | 1331 | def modifyAffiliationList(self, frm, roomJID, jid_list, affiliation): |
---|
[129] | 1332 | """ |
---|
| 1333 | Modify an affiliation list. |
---|
[122] | 1334 | |
---|
[127] | 1335 | @param frm: The entity sending the request. |
---|
[131] | 1336 | @type frm: L{jid.JID} |
---|
[123] | 1337 | |
---|
[132] | 1338 | @param roomJID: The bare JID of the room. |
---|
| 1339 | @type roomJID: L{jid.JID} |
---|
[122] | 1340 | |
---|
[132] | 1341 | @param entities: The list of entities to change in a room. This can be |
---|
| 1342 | a nick or a full jid. |
---|
| 1343 | @type jid_list: L{list} of C{unicode} for nicks. L{list} of L{jid.JID} |
---|
| 1344 | for jids. |
---|
[122] | 1345 | |
---|
[127] | 1346 | @param affiliation: The affilation to change. |
---|
[132] | 1347 | @type affiliation: C{unicode} |
---|
[127] | 1348 | |
---|
| 1349 | """ |
---|
| 1350 | iq = AffiliationRequest(self.xmlstream, |
---|
| 1351 | method='set', |
---|
| 1352 | affiliation=affiliation, |
---|
| 1353 | ) |
---|
| 1354 | iq.items(jid_list) |
---|
[132] | 1355 | iq['to'] = roomJID.userhost() |
---|
[127] | 1356 | iq['from'] = frm.full() |
---|
| 1357 | return iq.send() |
---|
| 1358 | |
---|
[129] | 1359 | |
---|
[132] | 1360 | def grantVoice(self, roomJID, nick, reason=None, sender=None): |
---|
[129] | 1361 | """ |
---|
| 1362 | Grant voice to an entity. |
---|
| 1363 | |
---|
[132] | 1364 | @param roomJID: The bare JID of the room. |
---|
| 1365 | @type roomJID: L{jid.JID} |
---|
[127] | 1366 | |
---|
[132] | 1367 | @param nick: The nick name for the user in this room. |
---|
| 1368 | @type nick: C{unicode} |
---|
[129] | 1369 | |
---|
[127] | 1370 | @param reason: The reason for granting voice to the entity. |
---|
[132] | 1371 | @type reason: C{unicode} |
---|
[127] | 1372 | |
---|
[132] | 1373 | @param sender: The entity sending the request. |
---|
| 1374 | @type sender: L{jid.JID} |
---|
[131] | 1375 | """ |
---|
[132] | 1376 | return self._setRole(roomJID, entityOrNick=nick, |
---|
[131] | 1377 | role='participant', |
---|
| 1378 | reason=reason, sender=sender) |
---|
[127] | 1379 | |
---|
[131] | 1380 | |
---|
[132] | 1381 | def revokeVoice(self, roomJID, nick, reason=None, sender=None): |
---|
[127] | 1382 | """ |
---|
[131] | 1383 | Revoke voice from a participant. |
---|
[127] | 1384 | |
---|
[131] | 1385 | This will disallow the entity to send messages to a moderated room. |
---|
[129] | 1386 | |
---|
[132] | 1387 | @param roomJID: The bare JID of the room. |
---|
| 1388 | @type roomJID: L{jid.JID} |
---|
[127] | 1389 | |
---|
[132] | 1390 | @param nick: The nick name for the user in this room. |
---|
| 1391 | @type nick: C{unicode} |
---|
[129] | 1392 | |
---|
[132] | 1393 | @param reason: The reason for revoking voice from the entity. |
---|
| 1394 | @type reason: C{unicode} |
---|
[127] | 1395 | |
---|
[132] | 1396 | @param sender: The entity sending the request. |
---|
| 1397 | @type sender: L{jid.JID} |
---|
[127] | 1398 | """ |
---|
[132] | 1399 | return self._setRole(roomJID, entityOrNick=nick, role='visitor', |
---|
[131] | 1400 | reason=reason, sender=sender) |
---|
[127] | 1401 | |
---|
[129] | 1402 | |
---|
[132] | 1403 | def grantModerator(self, roomJID, nick, reason=None, sender=None): |
---|
[129] | 1404 | """ |
---|
| 1405 | Grant moderator priviledges to a MUC room. |
---|
[127] | 1406 | |
---|
[132] | 1407 | @param roomJID: The bare JID of the room. |
---|
| 1408 | @type roomJID: L{jid.JID} |
---|
[127] | 1409 | |
---|
[132] | 1410 | @param nick: The nick name for the user in this room. |
---|
| 1411 | @type nick: C{unicode} |
---|
| 1412 | |
---|
| 1413 | @param reason: The reason for granting moderation to the entity. |
---|
| 1414 | @type reason: C{unicode} |
---|
| 1415 | |
---|
| 1416 | @param sender: The entity sending the request. |
---|
| 1417 | @type sender: L{jid.JID} |
---|
[127] | 1418 | """ |
---|
[132] | 1419 | return self._setRole(roomJID, entityOrNick=nick, role='moderator', |
---|
| 1420 | reason=reason, sender=sender) |
---|
[127] | 1421 | |
---|
[129] | 1422 | |
---|
[132] | 1423 | def ban(self, roomJID, entity, reason=None, sender=None): |
---|
[129] | 1424 | """ |
---|
| 1425 | Ban a user from a MUC room. |
---|
[127] | 1426 | |
---|
[132] | 1427 | @param roomJID: The bare JID of the room. |
---|
| 1428 | @type roomJID: L{jid.JID} |
---|
[127] | 1429 | |
---|
[132] | 1430 | @param entity: The room jabber/xmpp entity id. |
---|
| 1431 | @type entity: L{jid.JID} |
---|
[127] | 1432 | |
---|
| 1433 | @param reason: The reason for banning the entity. |
---|
[132] | 1434 | @type reason: C{unicode} |
---|
[127] | 1435 | |
---|
[132] | 1436 | @param sender: The entity sending the request. |
---|
| 1437 | @type sender: L{jid.JID} |
---|
[131] | 1438 | """ |
---|
[132] | 1439 | return self._setAffiliation(roomJID, entity, 'outcast', |
---|
[131] | 1440 | reason=reason, sender=sender) |
---|
[127] | 1441 | |
---|
| 1442 | |
---|
[132] | 1443 | def kick(self, roomJID, entityOrNick, reason=None, sender=None): |
---|
[129] | 1444 | """ |
---|
| 1445 | Kick a user from a MUC room. |
---|
[127] | 1446 | |
---|
[132] | 1447 | @param roomJID: The bare JID of the room. |
---|
| 1448 | @type roomJID: L{jid.JID} |
---|
[127] | 1449 | |
---|
[132] | 1450 | @param entityOrNick: The occupant to be banned. |
---|
| 1451 | @type entityOrNick: L{jid.JID} or C{unicode} |
---|
[127] | 1452 | |
---|
| 1453 | @param reason: The reason given for the kick. |
---|
[132] | 1454 | @type reason: C{unicode} |
---|
[127] | 1455 | |
---|
[132] | 1456 | @param sender: The entity sending the request. |
---|
| 1457 | @type sender: L{jid.JID} |
---|
[127] | 1458 | """ |
---|
[132] | 1459 | return self._setAffiliation(roomJID, entityOrNick, 'none', |
---|
| 1460 | reason=reason, sender=sender) |
---|