Merge lp:~ben-hutchings/endroid/presence into lp:endroid

Proposed by Martin Morrison
Status: Merged
Approved by: Martin Morrison
Approved revision: 77
Merged at revision: 43
Proposed branch: lp:~ben-hutchings/endroid/presence
Merge into: lp:endroid
Diff against target: 294 lines (+193/-15)
3 files modified
src/endroid/plugins/broadcast.py (+171/-0)
src/endroid/rosterhandler.py (+5/-8)
src/endroid/usermanagement.py (+17/-7)
To merge this branch: bzr merge lp:~ben-hutchings/endroid/presence
Reviewer Review Type Date Requested Status
Martin Morrison Approve
Review via email: mp+180390@code.launchpad.net

This proposal supersedes a proposal from 2013-08-09.

Commit message

Rosterhandler pushes up more information about available presences - now storing the priority and status of resources.

The broadcast plugin can be configured to the following levels:
 - all: broadcast to all a user's available resources
 - positive: broadcast to all a user's available resources with positive (or zero) priorities
 - max: broadcast to the user's highest priority resource
 - none: do not broadcast (leave it up to the server)

This should allow EnDroid to emulate the most common server behaviours when it comes to sending messages.

Description of the change

Rosterhandler pushes up more information about available presences - now storing the priority and status of resources.

The broadcast plugin can be configured to the following levels:
 - all: broadcast to all a user's available resources
 - positive: broadcast to all a user's available resources with positive (or zero) priorities
 - max: broadcast to the user's highest priority resource
 - none: do not broadcast (leave it up to the server)

This should allow EnDroid to emulate the most common server behaviours when it comes to sending messages.

To post a comment you must log in.
Revision history for this message
Martin Morrison (isoschiz) wrote : Posted in a previous version of this proposal

usermanagement and rosterhandler changes are logically nice. However, the implementation is a little too voodoo for my liking. :-)

I think it's as simple as turning _members[userhost] from a set of magic (and error-prone) Resource object into a dictionary of full_jid -> "properties" (which can be as simple as a tuple of (show, priority)). This would make the "updating" look less magic, without any loss of functionality.

In the broadcast plugin, the StringKeys stuff is a little nasty too. You can get the same behaviour with the simpler:

class Level(object):
    NONE = "none"
    ALL = "all"
    POSITIVE = "positive"
    MAX = "max"

Which incidentally is also closer to the true enum support that now exists in Python 3.4.

Revision history for this message
Ben Hutchings (ben-hutchings) wrote : Posted in a previous version of this proposal

Done all this and added a bit more nice functionality to the plugin.
Ben

On 10/08/2013 21:22, Martin Morrison wrote:
> usermanagement and rosterhandler changes are logically nice. However, the implementation is a little too voodoo for my liking. :-)
>
> I think it's as simple as turning _members[userhost] from a set of magic (and error-prone) Resource object into a dictionary of full_jid -> "properties" (which can be as simple as a tuple of (show, priority)). This would make the "updating" look less magic, without any loss of functionality.
>
>
> In the broadcast plugin, the StringKeys stuff is a little nasty too. You can get the same behaviour with the simpler:
>
> class Level(object):
> NONE = "none"
> ALL = "all"
> POSITIVE = "positive"
> MAX = "max"
>
> Which incidentally is also closer to the true enum support that now exists in Python 3.4.

Revision history for this message
Martin Morrison (isoschiz) wrote : Posted in a previous version of this proposal

Ideally the new plugin would be migrated to some of the new APIs that are being collapsed as well, but accepting this as is for now.

One change that could be made is that Resource is effectively just a namedtuple - and using namedtuple would make the code slightly shorter/simpler.

review: Approve
Revision history for this message
Martin Morrison (isoschiz) wrote : Posted in a previous version of this proposal

No proposals found for merge of lp:~ben-hutchings/endroid/roomowner into lp:endroid.

Revision history for this message
Martin Morrison (isoschiz) wrote :

Approving resubmit (which was to remove dependent branch that was confusing tarmac)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'src/endroid/plugins/broadcast.py'
2--- src/endroid/plugins/broadcast.py 1970-01-01 00:00:00 +0000
3+++ src/endroid/plugins/broadcast.py 2013-08-15 18:05:18 +0000
4@@ -0,0 +1,171 @@
5+from endroid.plugins.command import CommandPlugin
6+from endroid.database import Database
7+import logging
8+
9+
10+DB_NAME = "BROADCAST"
11+DB_TABLE = "BROADCAST"
12+
13+
14+class Levels(object):
15+ NONE = "none"
16+ ALL = "all"
17+ POSITIVE = "positive"
18+ MAX = "max"
19+
20+ @classmethod
21+ def __contains__(cls, key):
22+ return key in (cls.NONE, cls.ALL, cls.POSITIVE, cls.MAX)
23+
24+
25+class Broadcast(CommandPlugin):
26+ """
27+ This plugin partially solves the problem with XMPP that if a user is logged
28+ in on multiple devices (resources) then messages sent by EnDroid may not
29+ arrive at all of them.
30+
31+ When broadcasting is enabled by a user, any messages sent by EnDroid to them
32+ will be intercepted and replaced by several identical messages sent
33+ individually to a selection of their resources.
34+
35+ Note:
36+ - this plugin only applies to chat messages (room messages do not suffer
37+ the same problem)
38+ - the plugin may be configured to broadcast at levels:
39+ - all: send to all the recipient's available resources
40+ - positive: send to the recipient's available resources with priority >=0
41+ - max: send to the recipient's maximum priority resource
42+ - none (default): disable broadcasting
43+
44+ """
45+ help = "Plugin to enable the broadcasting of messages to all resources."
46+ users = {}
47+
48+ levels = Levels()
49+
50+ ID = "broadcast_plugin" # this will be set as the source of sent messages
51+
52+ def endroid_init(self):
53+ self.register_chat_send_filter(self.do_broadcast)
54+ self.db = Database(DB_NAME)
55+ if not self.db.table_exists(DB_TABLE):
56+ self.db.create_table(DB_TABLE, ('user', 'do_broadcast'))
57+
58+ # make a local copy of the registration database
59+ data = self.db.fetch(DB_TABLE, ['user', 'do_broadcast'])
60+ for row in data:
61+ self.users[row['user']] = row['do_broadcast']
62+
63+ def do_broadcast(self, msg):
64+ sender = msg.sender
65+ if sender == self.ID: # we sent this message
66+ return True
67+
68+ recipient = msg.recipient
69+ recip_host = self.usermanagement.get_userhost(recipient)
70+
71+ logging.debug("Broadcast got message {} -> {}".format(sender, recipient))
72+ # Check the broadcast level for recip_host (if they are not
73+ # registered return levels.NONE).
74+ level = self.users.get(recip_host, self.levels.NONE)
75+
76+ if level == self.levels.NONE:
77+ # we are not broadcasting to this user, let the original message
78+ # through and do nothing
79+ return True
80+ else:
81+ # we have some broadcasting to do
82+ rs = self._get_resources(recip_host, level)
83+
84+ sent_num = 0
85+ for resource in rs:
86+ self.messagehandler.send_chat(resource, msg.body, self.ID)
87+ sent_num += 1
88+
89+ fmt = "Broadcast '{}' sent {} messages to {}"
90+ logging.debug(fmt.format(level, sent_num, recip_host))
91+ # drop the original message
92+ return False
93+
94+
95+ def cmd_set_broadcast(self, msg, arg):
96+ """
97+ When this is called, messages EnDroid sends will be sent to _all_
98+ the user's available resources.
99+
100+ """
101+ level = arg.split()[0]
102+ level = level if level in self.levels else self.levels.ALL
103+ # this will be broadcasted
104+ msg.reply("Setting broadcast level '{}'.".format(level))
105+ if not msg.sender in self.users:
106+ self._register_user(msg.sender, level=level)
107+ else:
108+ self._update_user(msg.sender, level=level)
109+ # this may not be broadcasted, depending on what level has been set to
110+ msg.reply("Set broadcast level '{}'.".format(level))
111+
112+ cmd_set_broadcast.helphint = ("{all|positive|max|none} (all, positive, max "
113+ "process resource priorities.")
114+
115+
116+ def cmd_disable_broadcast(self, msg, arg):
117+ """Disable broadcasting."""
118+ self.cmd_set_broadcast(msg, self.levels.NONE)
119+ # msg.reply("Disabling broadcast.")
120+ # if self.users[msg.sender]:
121+ # self._update_user(msg.sender, level=self.levels.NONE)
122+ # msg.reply("Disabled broadcast.")
123+
124+ cmd_disable_broadcast.helphint = ("Equivalent to 'set broadcast none'.")
125+
126+ def cmd_get_broadcast(self, msg, arg):
127+ """Get broadcast level."""
128+ level = self.users.get(msg.sender, self.levels.NONE)
129+ message = "Broadcast level '{}'".format(level)
130+ if arg and arg.split()[0] in ("ls", "list"):
131+ rs = self._get_resources(msg.sender, level)
132+ message += ':'
133+ msg.reply('\n'.join([message] + (rs or ["none"])))
134+ else:
135+ message += '.'
136+ msg.reply(message)
137+
138+ cmd_get_broadcast.helphint = ("{ls|list}?")
139+
140+ def cmd_get_resources(self, msg, arg):
141+ """Return msg.sender's available resources.
142+
143+ If arg is "broadcast", return those we are broadcasting to.
144+
145+ """
146+ message = ["Available resources:"]
147+ rs = self._get_resources(msg.sender)
148+ msg.reply('\n'.join(message + (rs or ["none"])))
149+
150+ cmd_get_resources.helphint = ("Report all available resources.")
151+
152+ def _register_user(self, user, level=levels.NONE):
153+ self.db.insert(DB_TABLE, {'user' : user, 'do_broadcast' : level})
154+ self.users[user] = level
155+
156+ def _update_user(self, user, level=levels.NONE):
157+ self.db.update(DB_TABLE, {'do_broadcast' : level}, {'user' : user})
158+ self.users[user] = level
159+
160+ def _get_resources(self, user, level=levels.ALL):
161+ resources = self.usermanagement.users.get_resources(user)
162+ addresses = []
163+
164+ if level == self.levels.ALL:
165+ addresses = resources.keys()
166+ elif level == self.levels.POSITIVE:
167+ addresses = [j for j, r in resources.items() if r.priority >= 0]
168+ elif level == self.levels.MAX:
169+ # resources.items is a list of tuples of form: (jid : Resource)
170+ # sort on Resource.priority
171+ # this returns the tuple (jid : max_priority_resource)
172+ # get just the jid by indexing with [0]
173+ addresses = [max(resources.items(), key=lambda (j,r): r.priority)[0]]
174+
175+ return addresses
176
177=== modified file 'src/endroid/rosterhandler.py'
178--- src/endroid/rosterhandler.py 2013-08-06 13:52:29 +0000
179+++ src/endroid/rosterhandler.py 2013-08-15 18:05:18 +0000
180@@ -47,11 +47,8 @@
181 rosterd.addCallback(d.callback)
182 # Advertises as available - otherwise MUC will probably work
183 # but SUC clients won't send messages on to EnDroid
184- self.set_available()
185+ self.available()
186
187- def set_available(self, entity=None):
188- super(RosterHandler, self).available(entity)
189-
190 def reRoster(self):
191 return self.getRoster()
192
193@@ -77,21 +74,21 @@
194 self.subscribed(presence)
195 # let the presence know that we are available (this does not affect
196 # subscription status)
197- self.set_available(presence)
198+ self.available(presence)
199
200 def probeReceived(self, presence):
201 PresenceClientProtocol.probeReceived(self, presence)
202- self.set_available()
203+ self.available()
204
205 def availableReceived(self, entity, show=None, statuses=None, priority=0):
206 userhost, full_jid = entity.userhost(), entity.full()
207 logging.info("Available from {} '{}' priority: '{}'".format(full_jid, show, priority))
208 # entity has come online - update our online set
209 if userhost in self.um.users.registered:
210- self.um.users.set_available(full_jid)
211+ self.um.users.set_available(full_jid, show, priority)
212 # make them available in their groups
213 for group in self.um.get_groups(userhost):
214- self.um.group_rosters[group].set_available(full_jid)
215+ self.um.group_rosters[group].set_available(full_jid, show, priority)
216
217 def unavailableReceived(self, entity, statuses=None):
218 userhost, full_jid = entity.userhost(), entity.full()
219
220=== modified file 'src/endroid/usermanagement.py'
221--- src/endroid/usermanagement.py 2013-08-14 18:45:45 +0000
222+++ src/endroid/usermanagement.py 2013-08-15 18:05:18 +0000
223@@ -41,14 +41,14 @@
224 def available(self):
225 # jids is a set of the resources on which a user is avaialbe
226 # if jids is empty then the user is unavailable
227- return set(u for (u, jids) in self._members.items() if jids)
228+ return set(u for (u, avs) in self._members.items() if avs)
229
230 @property
231 def registered(self):
232 return set(self._members.keys())
233
234 def get_resources(self, name):
235- return self._members.get(name, set())
236+ return self._members.get(name, {})
237
238 def set_registration_list(self, names):
239 # if we have a list callback then don't do sub-callbacks
240@@ -60,24 +60,24 @@
241
242 def register_user(self, name):
243 if not name in self._members:
244- self._members[name] = set()
245+ self._members[name] = {}
246 self.registration_cb(name, self.name)
247
248 def deregister_user(self, name):
249 self._members.pop(name, None)
250 self.deregistration_cb(name, self.name)
251
252- def set_available(self, full_jid):
253+ def set_available(self, full_jid, show=None, priority=0):
254 userhost = UserManagement.get_userhost(full_jid)
255 logging.debug("{} came online as {}".format(userhost, full_jid))
256 if userhost in self._members:
257- self._members[userhost].add(full_jid)
258+ self._members[userhost][full_jid] = Resource(show, priority)
259
260 def set_unavailable(self, full_jid):
261 userhost = UserManagement.get_userhost(full_jid)
262 logging.debug("{} went offline as {}".format(userhost, full_jid))
263 if userhost in self._members:
264- self._members[userhost].discard(full_jid)
265+ self._members[userhost].pop(full_jid, None)
266
267 def __repr__(self):
268 name = self.name or "contacts"
269@@ -127,6 +127,16 @@
270 self.deferreds = []
271 super(Room, self).__init__(name, *args, **kwargs)
272
273+class Resource(object):
274+ __slots__ = ("show", "priority")
275+
276+ def __init__(self, show=None, priority=0):
277+ self.show = show
278+ self.priority = priority
279+
280+ def __repr__(self):
281+ name = type(self).__name__
282+ return "{0}(show='{1.show}', priority='{1.priority}'".format(name, self)
283
284 class UserManagement(object):
285 """An abstraction of XMPP's presence protocols."""
286@@ -352,7 +362,7 @@
287 self.rh.setItem(name)
288 # send a subscribe request - confirmation in rosterhandler.subscribeReceived
289 self.rh.subscribe(JID(name))
290- self.rh.set_available(JID(name))
291+ self.rh.available(JID(name))
292
293 def _cb_rem_contact(self, name, _):
294 logging.debug("Removing contact: {}".format(name))

Subscribers

People subscribed via source and target branches