xep_0045.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  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. got_offline = True
  134. else:
  135. if entry['nick'] not in self.rooms[entry['room']]:
  136. got_online = True
  137. self.rooms[entry['room']][entry['nick']] = entry
  138. log.debug("MUC presence from %s/%s : %s", entry['room'],entry['nick'], entry)
  139. self.xmpp.event("groupchat_presence", pr)
  140. self.xmpp.event("muc::%s::presence" % entry['room'], pr)
  141. if got_offline:
  142. self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
  143. if got_online:
  144. self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
  145. def handle_groupchat_message(self, msg):
  146. """ Handle a message event in a muc.
  147. """
  148. self.xmpp.event('groupchat_message', msg)
  149. self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
  150. def handle_groupchat_error_message(self, msg):
  151. """ Handle a message error event in a muc.
  152. """
  153. self.xmpp.event('groupchat_message_error', msg)
  154. self.xmpp.event("muc::%s::message_error" % msg['from'].bare, msg)
  155. def handle_groupchat_subject(self, msg):
  156. """ Handle a message coming from a muc indicating
  157. a change of subject (or announcing it when joining the room)
  158. """
  159. self.xmpp.event('groupchat_subject', msg)
  160. def jidInRoom(self, room, jid):
  161. for nick in self.rooms[room]:
  162. entry = self.rooms[room][nick]
  163. if entry is not None and entry['jid'].full == jid:
  164. return True
  165. return False
  166. def getNick(self, room, jid):
  167. for nick in self.rooms[room]:
  168. entry = self.rooms[room][nick]
  169. if entry is not None and entry['jid'].full == jid:
  170. return nick
  171. def configureRoom(self, room, form=None, ifrom=None):
  172. if form is None:
  173. form = self.getRoomConfig(room, ifrom=ifrom)
  174. iq = self.xmpp.makeIqSet()
  175. iq['to'] = room
  176. if ifrom is not None:
  177. iq['from'] = ifrom
  178. query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
  179. form = form.getXML('submit')
  180. query.append(form)
  181. iq.append(query)
  182. # For now, swallow errors to preserve existing API
  183. try:
  184. result = iq.send()
  185. except IqError:
  186. return False
  187. except IqTimeout:
  188. return False
  189. return True
  190. def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None, pfrom=None):
  191. """ Join the specified room, requesting 'maxhistory' lines of history.
  192. """
  193. stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow, pfrom=pfrom)
  194. x = ET.Element('{http://jabber.org/protocol/muc}x')
  195. if password:
  196. passelement = ET.Element('{http://jabber.org/protocol/muc}password')
  197. passelement.text = password
  198. x.append(passelement)
  199. if maxhistory:
  200. history = ET.Element('{http://jabber.org/protocol/muc}history')
  201. if maxhistory == "0":
  202. history.attrib['maxchars'] = maxhistory
  203. else:
  204. history.attrib['maxstanzas'] = maxhistory
  205. x.append(history)
  206. stanza.append(x)
  207. if not wait:
  208. self.xmpp.send(stanza)
  209. else:
  210. #wait for our own room presence back
  211. expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)})
  212. self.xmpp.send(stanza, expect)
  213. self.rooms[room] = {}
  214. self.ourNicks[room] = nick
  215. def destroy(self, room, reason='', altroom = '', ifrom=None):
  216. iq = self.xmpp.makeIqSet()
  217. if ifrom is not None:
  218. iq['from'] = ifrom
  219. iq['to'] = room
  220. query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
  221. destroy = ET.Element('{http://jabber.org/protocol/muc#owner}destroy')
  222. if altroom:
  223. destroy.attrib['jid'] = altroom
  224. xreason = ET.Element('{http://jabber.org/protocol/muc#owner}reason')
  225. xreason.text = reason
  226. destroy.append(xreason)
  227. query.append(destroy)
  228. iq.append(query)
  229. # For now, swallow errors to preserve existing API
  230. try:
  231. r = iq.send()
  232. except IqError:
  233. return False
  234. except IqTimeout:
  235. return False
  236. return True
  237. def setAffiliation(self, room, jid=None, nick=None, affiliation='member', ifrom=None):
  238. """ Change room affiliation."""
  239. if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
  240. raise TypeError
  241. query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
  242. if nick is not None:
  243. item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'nick':nick})
  244. else:
  245. item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'jid':jid})
  246. query.append(item)
  247. iq = self.xmpp.makeIqSet(query)
  248. iq['to'] = room
  249. iq['from'] = ifrom
  250. # For now, swallow errors to preserve existing API
  251. try:
  252. result = iq.send()
  253. except IqError:
  254. return False
  255. except IqTimeout:
  256. return False
  257. return True
  258. def setRole(self, room, nick, role):
  259. """ Change role property of a nick in a room.
  260. Typically, roles are temporary (they last only as long as you are in the
  261. room), whereas affiliations are permanent (they last across groupchat
  262. sessions).
  263. """
  264. if role not in ('moderator', 'participant', 'visitor', 'none'):
  265. raise TypeError
  266. query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
  267. item = ET.Element('item', {'role':role, 'nick':nick})
  268. query.append(item)
  269. iq = self.xmpp.makeIqSet(query)
  270. iq['to'] = room
  271. result = iq.send()
  272. if result is False or result['type'] != 'result':
  273. raise ValueError
  274. return True
  275. def invite(self, room, jid, reason='', mfrom=''):
  276. """ Invite a jid to a room."""
  277. msg = self.xmpp.makeMessage(room)
  278. msg['from'] = mfrom
  279. x = ET.Element('{http://jabber.org/protocol/muc#user}x')
  280. invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
  281. if reason:
  282. rxml = ET.Element('{http://jabber.org/protocol/muc#user}reason')
  283. rxml.text = reason
  284. invite.append(rxml)
  285. x.append(invite)
  286. msg.append(x)
  287. self.xmpp.send(msg)
  288. def leaveMUC(self, room, nick, msg='', pfrom=None):
  289. """ Leave the specified room.
  290. """
  291. if msg:
  292. self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg, pfrom=pfrom)
  293. else:
  294. self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom)
  295. del self.rooms[room]
  296. def getRoomConfig(self, room, ifrom=''):
  297. iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
  298. iq['to'] = room
  299. iq['from'] = ifrom
  300. # For now, swallow errors to preserve existing API
  301. try:
  302. result = iq.send()
  303. except IqError:
  304. raise ValueError
  305. except IqTimeout:
  306. raise ValueError
  307. form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
  308. if form is None:
  309. raise ValueError
  310. return self.xmpp.plugin['xep_0004'].buildForm(form)
  311. def cancelConfig(self, room, ifrom=None):
  312. query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
  313. x = ET.Element('{jabber:x:data}x', type='cancel')
  314. query.append(x)
  315. iq = self.xmpp.makeIqSet(query)
  316. iq['to'] = room
  317. iq['from'] = ifrom
  318. iq.send()
  319. def setRoomConfig(self, room, config, ifrom=''):
  320. query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
  321. x = config.getXML('submit')
  322. query.append(x)
  323. iq = self.xmpp.makeIqSet(query)
  324. iq['to'] = room
  325. iq['from'] = ifrom
  326. iq.send()
  327. def getJoinedRooms(self):
  328. return self.rooms.keys()
  329. def getOurJidInRoom(self, roomJid):
  330. """ Return the jid we're using in a room.
  331. """
  332. return "%s/%s" % (roomJid, self.ourNicks[roomJid])
  333. def getJidProperty(self, room, nick, jidProperty):
  334. """ Get the property of a nick in a room, such as its 'jid' or 'affiliation'
  335. If not found, return None.
  336. """
  337. if room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]:
  338. return self.rooms[room][nick][jidProperty]
  339. else:
  340. return None
  341. def getRoster(self, room):
  342. """ Get the list of nicks in a room.
  343. """
  344. if room not in self.rooms.keys():
  345. return None
  346. return self.rooms[room].keys()
  347. xep_0045 = XEP_0045
  348. register_plugin(XEP_0045)