| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # vi: sts=4 et sw=4 |
|---|
| 3 | |
|---|
| 4 | from common import debug |
|---|
| 5 | from xmpp.client import Client |
|---|
| 6 | from xmpp.protocol import JID, Presence, NS_PRESENCE, NS_IQ, NodeProcessed |
|---|
| 7 | import threading |
|---|
| 8 | try: |
|---|
| 9 | from xmpp.jep0106 import JIDEncode |
|---|
| 10 | except ImportError: |
|---|
| 11 | debug("Using embedded jep0106 module") # Old versions of xmpppy |
|---|
| 12 | from jep0106 import JIDEncode # don't provide this module |
|---|
| 13 | |
|---|
| 14 | class ConnectionError(Exception): pass |
|---|
| 15 | |
|---|
| 16 | class ForeignClient(threading.Thread): |
|---|
| 17 | '''A connection to a Jabber account, as a client.''' |
|---|
| 18 | |
|---|
| 19 | def __init__(self, jid, password, resource, registerer, pizzjaInstance): |
|---|
| 20 | '''Constructor. Initiates a new account from the given information. |
|---|
| 21 | ''' |
|---|
| 22 | threading.Thread.__init__(self, name=registerer) |
|---|
| 23 | debug('Initializing client for %s/%s, registered by %s' \ |
|---|
| 24 | % (jid, resource, registerer)) |
|---|
| 25 | self.jid = jid |
|---|
| 26 | self.password = password |
|---|
| 27 | self.resource = resource |
|---|
| 28 | self.registerer = registerer |
|---|
| 29 | self.component = pizzjaInstance |
|---|
| 30 | self.cnx = Client(JID(self.jid).getDomain(), debug=['socket']) |
|---|
| 31 | self.isConnected = self.cnx.isConnected |
|---|
| 32 | self.presence = Presence() |
|---|
| 33 | self.bye = False |
|---|
| 34 | self.controllers = {} # {'Home/Amarok': -42, 'Home/sendxmpp': 1} |
|---|
| 35 | |
|---|
| 36 | def run(self): |
|---|
| 37 | '''This method is run when self.start() is called. It overrides the |
|---|
| 38 | default Thread.run() method. It processes incoming stanzas in a loop |
|---|
| 39 | until disconnection. |
|---|
| 40 | ''' |
|---|
| 41 | debug('Client starts looping.') |
|---|
| 42 | while not self.bye: |
|---|
| 43 | self.cnx.Process(10) |
|---|
| 44 | self.onDisconnect() |
|---|
| 45 | debug('Disconnected. Client stopped looping.') |
|---|
| 46 | |
|---|
| 47 | def disconnect(self): |
|---|
| 48 | self.bye = True |
|---|
| 49 | |
|---|
| 50 | def connect(self): |
|---|
| 51 | '''Try to connect and auth, then pass every incoming stanza to the |
|---|
| 52 | main component. If the connection fails, simulate onDisconnect() |
|---|
| 53 | event, which will have the component clean us up. |
|---|
| 54 | ''' |
|---|
| 55 | #TODO: Don't crash because of invalid accounts, cf. ticket #5. |
|---|
| 56 | #TODO: In the future, we might want to call start() earlier for |
|---|
| 57 | # performance reasons: until we call start(), the process is |
|---|
| 58 | # blocking. |
|---|
| 59 | debug('Registering disconnect handler') |
|---|
| 60 | self.cnx.RegisterDisconnectHandler(self.onDisconnect) |
|---|
| 61 | if not self.cnx.connect(): |
|---|
| 62 | debug('Could not connect.') |
|---|
| 63 | self.onDisconnect() |
|---|
| 64 | raise ConnectionError('Unable to connect') |
|---|
| 65 | debug('Connection OK.') |
|---|
| 66 | if not self.cnx.auth(self.jid, self.password, self.resource): |
|---|
| 67 | debug('Could not auth. Disconnecting.') |
|---|
| 68 | self.cnx.disconnect() |
|---|
| 69 | raise ConnectionError('Unable to authenticate as %s' % (self.jid)) |
|---|
| 70 | debug('Authentified OK. Registering handlers.') |
|---|
| 71 | self.cnx.RegisterHandler(NS_IQ, self.iqReceived) |
|---|
| 72 | self.cnx.RegisterDefaultHandler(self.stanzaReceived) |
|---|
| 73 | debug('Start listening (in a separate thread)') |
|---|
| 74 | self.start() |
|---|
| 75 | debug('Thread started. Life is going on.') |
|---|
| 76 | |
|---|
| 77 | def iqReceived(self, cnx, iq): |
|---|
| 78 | '''When an IQ is received, decide what resource it should be |
|---|
| 79 | forwarded to, then pass it to the component. |
|---|
| 80 | - get/set: pick the most appropriate recipient. Reply with error |
|---|
| 81 | service-unavailable if no appropriate recipient is found. |
|---|
| 82 | - result/error: retrieve who sent the initial IQ get/set (ignore |
|---|
| 83 | if not found). |
|---|
| 84 | ''' |
|---|
| 85 | debug('An IQ was received.') |
|---|
| 86 | resource = None |
|---|
| 87 | if iq.getType() in ['get', 'set']: |
|---|
| 88 | debug('IQ get or set, picking a recipient...') |
|---|
| 89 | resource = self.pickIqRecipient(iq) |
|---|
| 90 | debug('... %s will do the job.' % resource) |
|---|
| 91 | if resource is None: |
|---|
| 92 | debug('Apologizing, nobody can handle the IQ.') |
|---|
| 93 | cnx.send(Error(iq, 'service-unavailable')) |
|---|
| 94 | elif iq.getType() in ['result', 'error']: |
|---|
| 95 | debug('IQ result or error, we should retrieve the initial sender.') |
|---|
| 96 | try: |
|---|
| 97 | resource = self.sentIQs.pop(iq.getId()) |
|---|
| 98 | debug('Found, the sender was %s.') |
|---|
| 99 | except KeyError: |
|---|
| 100 | debug('Unknown IQ response with id %s. Ignoring.' % iq.getId()) |
|---|
| 101 | if resource is not None: |
|---|
| 102 | jid = '%s/%s' % (self.registerer, resource) |
|---|
| 103 | debug('Now we have a recipient (%s), forwarding IQ.' % (jid)) |
|---|
| 104 | self.component.forwardStanzaIn(iq, [jid]) |
|---|
| 105 | debug('All right, IQ forwarded.') |
|---|
| 106 | raise NodeProcessed |
|---|
| 107 | |
|---|
| 108 | def stanzaReceived(self, cnx, stz): |
|---|
| 109 | '''When a stanza (other than IQ) is received, forward it to all the |
|---|
| 110 | controllers.''' |
|---|
| 111 | debug('A presence or message was received, forwarding in to everyone.') |
|---|
| 112 | self.component.forwardStanzaIn(stz, self.getControllers()) |
|---|
| 113 | |
|---|
| 114 | def send(self, stz): |
|---|
| 115 | '''Send the specified stanza on the client's connection. |
|---|
| 116 | For IQs get/set, remember which resource sent it. |
|---|
| 117 | When sending presence, remember it, and have the component reflect |
|---|
| 118 | it to the controlling resources. |
|---|
| 119 | ''' |
|---|
| 120 | debug('About to send a stanza as a client.') |
|---|
| 121 | id = self.cnx.send(stz) |
|---|
| 122 | debug('Stanza sent, id was %s' % id) |
|---|
| 123 | if stz.getName() == NS_IQ and stz.getType() in ['get', 'set']: |
|---|
| 124 | debug('It was an IQ get or set, recording id for the answer.') |
|---|
| 125 | self.sentIQs[id] = stz.getFrom().getResource() |
|---|
| 126 | elif stz.getName() == NS_PRESENCE and \ |
|---|
| 127 | stz.getType() not in ['subscribe', 'subscribed', 'unsubscribe', |
|---|
| 128 | 'unsubscribed', 'probe', 'error']: |
|---|
| 129 | debug('It was an presence change, reflecting it locally.') |
|---|
| 130 | self.presence = stz |
|---|
| 131 | self.component.reflectClientPresence(self) |
|---|
| 132 | return id |
|---|
| 133 | |
|---|
| 134 | def onDisconnect(self): |
|---|
| 135 | '''When disconnected, inform the component. Also Update self.presence |
|---|
| 136 | so that the component can reflect it to all controllers. |
|---|
| 137 | ''' |
|---|
| 138 | debug('Disconnect handler called, setting client unavailable presence', |
|---|
| 139 | 'and informing the component.') |
|---|
| 140 | self.presence = Presence(typ='unavailable') |
|---|
| 141 | self.component.clientDisconnected(self.registerer, self.resource) |
|---|
| 142 | |
|---|
| 143 | def hasController(self, resource): |
|---|
| 144 | '''Tell whether the given resource already showed up as a controller.''' |
|---|
| 145 | return (resource in self.controllers) |
|---|
| 146 | |
|---|
| 147 | def getControllers(self): |
|---|
| 148 | '''Return a list with the full JID of current controllers.''' |
|---|
| 149 | return ['%s/%s' % (self.registerer, resource) \ |
|---|
| 150 | for resource in self.controllers] |
|---|
| 151 | |
|---|
| 152 | def getMostAvailableController(self): |
|---|
| 153 | '''Return the 'most available' controller. Currently, this means the |
|---|
| 154 | resource with highest priority (the last one found if several of |
|---|
| 155 | them have the same priority). |
|---|
| 156 | ''' |
|---|
| 157 | resources = self.controllers.keys() |
|---|
| 158 | debug('We are wondering who is the most available controller among %s' \ |
|---|
| 159 | % resources) |
|---|
| 160 | current_best = resources[0] |
|---|
| 161 | for resource in resources: |
|---|
| 162 | debug('Considering %s (%s)' % (resource, self.controllers[resource])) |
|---|
| 163 | if self.controllers[resource] >= self.controllers[current_best]: |
|---|
| 164 | current_best = resource |
|---|
| 165 | debug('And the winner is... %s.' % current_best) |
|---|
| 166 | return current_best |
|---|
| 167 | |
|---|
| 168 | def resourceAvailable(self, resource, priority): |
|---|
| 169 | '''A controller sent an availability information. Create or update the |
|---|
| 170 | association between the resource and the priority. |
|---|
| 171 | If it is the first controller, connect. |
|---|
| 172 | ''' |
|---|
| 173 | debug('Resource %s sent availability, priority=%s' % (resource, priority)) |
|---|
| 174 | if self.controllers == {}: |
|---|
| 175 | debug('First available controller, congratulations. Connecting.') |
|---|
| 176 | self.connect() |
|---|
| 177 | if priority is None: |
|---|
| 178 | debug('No priority, assuming 0.') |
|---|
| 179 | priority = 0 |
|---|
| 180 | debug('Recording controller %s with priority %s' % (resource, priority)) |
|---|
| 181 | self.controllers[resource] = priority |
|---|
| 182 | |
|---|
| 183 | def resourceUnavailable(self, resource): |
|---|
| 184 | '''A controller sent unavailability information. Delete it from the |
|---|
| 185 | list of controllers. |
|---|
| 186 | If is was the last controller, disconnect. |
|---|
| 187 | ''' |
|---|
| 188 | debug('Resource %s sent unavailable presence.' % (resource)) |
|---|
| 189 | if resource in self.controllers: |
|---|
| 190 | debug('Removing from controllers.') |
|---|
| 191 | del self.controllers[resource] |
|---|
| 192 | if self.controllers == {}: |
|---|
| 193 | debug('It was the last controller, disconnecting.') |
|---|
| 194 | self.bye = True |
|---|
| 195 | debug('Disconnected.') |
|---|
| 196 | else: |
|---|
| 197 | debug("We didn't know %s/%s was online anyway." \ |
|---|
| 198 | % (self.registerer, resource)) |
|---|
| 199 | |
|---|
| 200 | def updatePriority(self): |
|---|
| 201 | '''Change priority according to controllers' ability to receive |
|---|
| 202 | messages. |
|---|
| 203 | THINKME: What criteria should be used? Currently, the highest |
|---|
| 204 | priority is reflected.''' |
|---|
| 205 | debug('We should for some reason re-evaluate our priorities ;)') |
|---|
| 206 | new_prio = max(self.controllers.values()) |
|---|
| 207 | debug('New priority is %s' % (new_prio)) |
|---|
| 208 | if new_prio != self.presence.getPriority(): |
|---|
| 209 | debug('Change needed, sending new presence.') |
|---|
| 210 | self.presence.setPriority(new_prio) |
|---|
| 211 | self.send(self.presence) |
|---|
| 212 | |
|---|
| 213 | def pickIqRecipient(self, iq): |
|---|
| 214 | '''Return the name of the resource the incoming IQ should be |
|---|
| 215 | forwarded to, or None is no acceptable candidate could be |
|---|
| 216 | determined. |
|---|
| 217 | Currently, the most available resource is chosen.''' |
|---|
| 218 | return self.getMostAvailableController() |
|---|
| 219 | |
|---|
| 220 | def getPresence(self): |
|---|
| 221 | return self.presence |
|---|