xep_0045.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. """
  2. SleekXMPP: The Sleek XMPP Library
  3. Copyright (C) 2010 Nathanael C. Fritz
  4. This file is part of SleekXMPP.
  5. See the file LICENSE for copying permission.
  6. """
  7. from __future__ import with_statement
  8. import logging
  9. from sleekxmpp import Presence
  10. from sleekxmpp.plugins import BasePlugin, register_plugin
  11. from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, JID, ET
  12. from sleekxmpp.xmlstream.handler.callback import Callback
  13. from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
  14. from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask
  15. from sleekxmpp.exceptions import IqError, IqTimeout
  16. log = logging.getLogger(__name__)
  17. class MUCPresence(ElementBase):
  18. name = 'x'
  19. namespace = 'http://jabber.org/protocol/muc#user'
  20. plugin_attrib = 'muc'
  21. interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room'))
  22. affiliations = set(('', ))
  23. roles = set(('', ))
  24. def getXMLItem(self):
  25. item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
  26. if item is None:
  27. item = ET.Element('{http://jabber.org/protocol/muc#user}item')
  28. self.xml.append(item)
  29. return item
  30. def getAffiliation(self):
  31. #TODO if no affilation, set it to the default and return default
  32. item = self.getXMLItem()
  33. return item.get('affiliation', '')
  34. def setAffiliation(self, value):
  35. item = self.getXMLItem()
  36. #TODO check for valid affiliation
  37. item.attrib['affiliation'] = value
  38. return self
  39. def delAffiliation(self):
  40. item = self.getXMLItem()
  41. #TODO set default affiliation
  42. if 'affiliation' in item.attrib: del item.attrib['affiliation']
  43. return self
  44. def getJid(self):
  45. item = self.getXMLItem()
  46. return JID(item.get('jid', ''))
  47. def setJid(self, value):
  48. item = self.getXMLItem()
  49. if not isinstance(value, str):
  50. value = str(value)
  51. item.attrib['jid'] = value
  52. return self
  53. def delJid(self):
  54. item = self.getXMLItem()
  55. if 'jid' in item.attrib: del item.attrib['jid']
  56. return self
  57. def getRole(self):
  58. item = self.getXMLItem()
  59. #TODO get default role, set default role if none
  60. return item.get('role', '')
  61. def setRole(self, value):
  62. item = self.getXMLItem()
  63. #TODO check for valid role
  64. item.attrib['role'] = value
  65. return self
  66. def delRole(self):
  67. item = self.getXMLItem()
  68. #TODO set default role
  69. if 'role' in item.attrib: del item.attrib['role']
  70. return self
  71. def getNick(self):
  72. return self.parent()['from'].resource
  73. def getRoom(self):
  74. return self.parent()['from'].bare
  75. def setNick(self, value):
  76. log.warning("Cannot set nick through mucpresence plugin.")
  77. return self
  78. def setRoom(self, value):
  79. log.warning("Cannot set room through mucpresence plugin.")
  80. return self
  81. def delNick(self):
  82. log.warning("Cannot delete nick through mucpresence plugin.")
  83. return self
  84. def delRoom(self):
  85. log.warning("Cannot delete room through mucpresence plugin.")
  86. return self
  87. class XEP_0045(BasePlugin):
  88. """
  89. Implements XEP-0045 Multi-User Chat
  90. """
  91. name = 'xep_0045'
  92. description = 'XEP-0045: Multi-User Chat'
  93. dependencies = set(['xep_0030', 'xep_0004'])
  94. def plugin_init(self):
  95. self.rooms = {}
  96. self.ourNicks = {}
  97. self.xep = '0045'
  98. # load MUC support in presence stanzas
  99. register_stanza_plugin(Presence, MUCPresence)
  100. self.xmpp.register_handler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
  101. self.xmpp.register_handler(Callback('MUCError', MatchXMLMask("<message xmlns='%s' type='error'><error/></message>" % self.xmpp.default_ns), self.handle_groupchat_error_message))
  102. self.xmpp.register_handler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
  103. self.xmpp.register_handler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
  104. self.xmpp.register_handler(Callback('MUCConfig', MatchXMLMask("<message xmlns='%s' type='groupchat'><x xmlns='http://jabber.org/protocol/muc#user'><status/></x></message>" % self.xmpp.default_ns), self.handle_config_change))
  105. self.xmpp.register_handler(Callback('MUCInvite', MatchXPath("{%s}message/{%s}x/{%s}invite" % (
  106. self.xmpp.default_ns,
  107. 'http://jabber.org/protocol/muc#user',
  108. 'http://jabber.org/protocol/muc#user')), self.handle_groupchat_invite))
  109. def handle_groupchat_invite(self, inv):
  110. """ Handle an invite into a muc.
  111. """
  112. logging.debug("MUC invite to %s from %s: %s", inv['to'], inv["from"], inv)
  113. if inv['from'] not in self.rooms.keys():
  114. self.xmpp.event("groupchat_invite", inv)
  115. def handle_config_change(self, msg):
  116. """Handle a MUC configuration change (with status code)."""
  117. self.xmpp.event('groupchat_config_status', msg)
  118. self.xmpp.event('muc::%s::config_status' % msg['from'].bare , msg)
  119. def handle_groupchat_presence(self, pr):
  120. """ Handle a presence in a muc.
  121. """
  122. got_offline = False
  123. got_online = False
  124. if pr['muc']['room'] not in self.rooms.keys():
  125. return
  126. entry = pr['muc'].getStanzaValues()
  127. entry['show'] = pr['show']
  128. entry['status'] = pr['status']
  129. entry['alt_nick'] = pr['nick']
  130. if pr['type'] == 'unavailable':
  131. if entry['nick'] in self.rooms[entry['room']]:
  132. del self.rooms[entry['room']][entry['nick']]
  133. if '{}/{}'.format(entry['room'], entry['nick']) == self.getOurJidInRoom(entry['room']):
  134. log.debug("I got kicked :( from %s" % entry['room'])
  135. del self.rooms[entry['room']]
  136. got_offline = True
  137. else:
  138. if entry['nick'] not in self.rooms[entry['room']]:
  139. got_online = True
  140. self.rooms[entry['room']][entry['nick']] = entry
  141. log.debug("MUC presence from %s/%s : %s", entry['room'],entry['nick'], entry)
  142. self.xmpp.event("groupchat_presence", pr)
  143. self.xmpp.event("muc::%s::presence" % entry['room'], pr)
  144. if got_offline:
  145. self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
  146. if got_online:
  147. self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
  148. def handle_groupchat_message(self, msg):
  149. """ Handle a message event in a muc.
  150. """
  151. self.xmpp.event('groupchat_message', msg)
  152. self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
  153. def handle_groupchat_error_message(self, msg):
  154. """ Handle a message error event in a muc.
  155. """
  156. self.xmpp.event('groupchat_message_error', msg)
  157. self.xmpp.event("muc::%s::message_error" % msg['from'].bare, msg)
  158. def handle_groupchat_subject(self, msg):
  159. """ Handle a message coming from a muc indicating
  160. a change of subject (or announcing it when joining the room)
  161. """
  162. self.xmpp.event('groupchat_subject', msg)
  163. def jidInRoom(self, room, jid):
  164. for nick in self.rooms[room]:
  165. entry = self.rooms[room][nick]
  166. if entry is not None and entry['jid'].full == jid:
  167. return True
  168. return False
  169. def getNick(self, room, jid):
  170. for nick in self.rooms[room]:
  171. entry = self.rooms[room][nick]
  172. if entry is not None and entry['jid'].full == jid:
  173. return nick
  174. def configureRoom(self, room, form=None, ifrom=None):
  175. if form is None:
  176. form = self.getRoomConfig(room, ifrom=ifrom)
  177. iq = self.xmpp.makeIqSet()
  178. iq['to'] = room
  179. if ifrom is not None:
  180. iq['from'] = ifrom
  181. query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
  182. form = form.getXML('submit')
  183. query.append(form)
  184. iq.append(query)
  185. # For now, swallow errors to preserve existing API
  186. try:
  187. result = iq.send()
  188. except IqError:
  189. return False
  190. except IqTimeout:
  191. return False
  192. return True
  193. def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None, pfrom=None):
  194. """ Join the specified room, requesting 'maxhistory' lines of history.
  195. """
  196. stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow, pfrom=pfrom)
  197. x = ET.Element('{http://jabber.org/protocol/muc}x')
  198. if password:
  199. passelement = ET.Element('{http://jabber.org/protocol/muc}password')
  200. passelement.text = password
  201. x.append(passelement)
  202. if maxhistory:
  203. history = ET.Element('{http://jabber.org/protocol/muc}history')
  204. if maxhistory == "0":
  205. history.attrib['maxchars'] = maxhistory
  206. else:
  207. history.attrib['maxstanzas'] = maxhistory
  208. x.append(history)
  209. stanza.append(x)
  210. if not wait:
  211. self.xmpp.send(stanza)
  212. else:
  213. #wait for our own room presence back
  214. expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)})
  215. self.xmpp.send(stanza, expect)
  216. self.rooms[room] = {}
  217. self.ourNicks[room] = nick
  218. def destroy(self, room, reason='', altroom = '', ifrom=None):
  219. iq = self.xmpp.makeIqSet()
  220. if ifrom is not None:
  221. iq['from'] = ifrom
  222. iq['to'] = room
  223. query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
  224. destroy = ET.Element('{http://jabber.org/protocol/muc#owner}destroy')
  225. if altroom:
  226. destroy.attrib['jid'] = altroom
  227. xreason = ET.Element('{http://jabber.org/protocol/muc#owner}reason')
  228. xreason.text = reason
  229. destroy.append(xreason)
  230. query.append(destroy)
  231. iq.append(query)
  232. # For now, swallow errors to preserve existing API
  233. try:
  234. r = iq.send()
  235. except IqError:
  236. return False
  237. except IqTimeout:
  238. return False
  239. return True
  240. def setAffiliation(self, room, jid=None, nick=None, affiliation='member', ifrom=None):
  241. """ Change room affiliation."""
  242. if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
  243. raise TypeError
  244. query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
  245. if nick is not None:
  246. item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'nick':nick})
  247. else:
  248. item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'jid':jid})
  249. query.append(item)
  250. iq = self.xmpp.makeIqSet(query)
  251. iq['to'] = room
  252. iq['from'] = ifrom
  253. # For now, swallow errors to preserve existing API
  254. try:
  255. result = iq.send()
  256. except IqError:
  257. return False
  258. except IqTimeout:
  259. return False
  260. return True
  261. def setRole(self, room, nick, role):
  262. """ Change role property of a nick in a room.
  263. Typically, roles are temporary (they last only as long as you are in the
  264. room), whereas affiliations are permanent (they last across groupchat
  265. sessions).
  266. """
  267. if role not in ('moderator', 'participant', 'visitor', 'none'):
  268. raise TypeError
  269. query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
  270. item = ET.Element('item', {'role':role, 'nick':nick})
  271. query.append(item)
  272. iq = self.xmpp.makeIqSet(query)
  273. iq['to'] = room
  274. result = iq.send()
  275. if result is False or result['type'] != 'result':
  276. raise ValueError
  277. return True
  278. def invite(self, room, jid, reason='', mfrom=''):
  279. """ Invite a jid to a room."""
  280. msg = self.xmpp.makeMessage(room)
  281. msg['from'] = mfrom
  282. x = ET.Element('{http://jabber.org/protocol/muc#user}x')
  283. invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
  284. if reason:
  285. rxml = ET.Element('{http://jabber.org/protocol/muc#user}reason')
  286. rxml.text = reason
  287. invite.append(rxml)
  288. x.append(invite)
  289. msg.append(x)
  290. self.xmpp.send(msg)
  291. def leaveMUC(self, room, nick, msg='', pfrom=None):
  292. """ Leave the specified room.
  293. """
  294. if msg:
  295. self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg, pfrom=pfrom)
  296. else:
  297. self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom)
  298. del self.rooms[room]
  299. def getRoomConfig(self, room, ifrom=''):
  300. iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
  301. iq['to'] = room
  302. iq['from'] = ifrom
  303. # For now, swallow errors to preserve existing API
  304. try:
  305. result = iq.send()
  306. except IqError:
  307. raise ValueError
  308. except IqTimeout:
  309. raise ValueError
  310. form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
  311. if form is None:
  312. raise ValueError
  313. return self.xmpp.plugin['xep_0004'].buildForm(form)
  314. def cancelConfig(self, room, ifrom=None):
  315. query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
  316. x = ET.Element('{jabber:x:data}x', type='cancel')
  317. query.append(x)
  318. iq = self.xmpp.makeIqSet(query)
  319. iq['to'] = room
  320. iq['from'] = ifrom
  321. iq.send()
  322. def setRoomConfig(self, room, config, ifrom=''):
  323. query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
  324. x = config.getXML('submit')
  325. query.append(x)
  326. iq = self.xmpp.makeIqSet(query)
  327. iq['to'] = room
  328. iq['from'] = ifrom
  329. iq.send()
  330. def getJoinedRooms(self):
  331. return self.rooms.keys()
  332. def getOurJidInRoom(self, roomJid):
  333. """ Return the jid we're using in a room.
  334. """
  335. return "%s/%s" % (roomJid, self.ourNicks[roomJid])
  336. def getJidProperty(self, room, nick, jidProperty):
  337. """ Get the property of a nick in a room, such as its 'jid' or 'affiliation'
  338. If not found, return None.
  339. """
  340. if room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]:
  341. return self.rooms[room][nick][jidProperty]
  342. else:
  343. return None
  344. def getRoster(self, room):
  345. """ Get the list of nicks in a room.
  346. """
  347. if room not in self.rooms.keys():
  348. return None
  349. return self.rooms[room].keys()
  350. def getUsersByAffiliation(cls, room, affiliation='member', ifrom=None):
  351. if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
  352. raise TypeError
  353. query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
  354. item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation': affiliation})
  355. query.append(item)
  356. iq = cls.xmpp.Iq(sto=room, sfrom=ifrom, stype='get')
  357. iq.append(query)
  358. return iq.send()
  359. xep_0045 = XEP_0045
  360. register_plugin(XEP_0045)