Merge lp:~gingerchris/endroid/nonmemberkick into lp:endroid

Proposed by ChrisD
Status: Superseded
Proposed branch: lp:~gingerchris/endroid/nonmemberkick
Merge into: lp:endroid
Prerequisite: lp:~gingerchris/endroid/bugfix
Diff against target: 474 lines (+202/-95)
3 files modified
src/endroid/rosterhandler.py (+21/-10)
src/endroid/usermanagement.py (+173/-78)
src/endroid/wokkelhandler.py (+8/-7)
To merge this branch: bzr merge lp:~gingerchris/endroid/nonmemberkick
Reviewer Review Type Date Requested Status
Martin Morrison Approve
Review via email: mp+182957@code.launchpad.net

This proposal has been superseded by a proposal from 2013-09-06.

Commit message

Add support for users to be kicked from rooms if they are not members of the room.

Description of the change

Add support for users to be kicked from rooms if they are not members of the room.
This is needed because some webex servers don't support mamber-only rooms.

As part of this:

 * Add a new JID class in usermanagement with the aim of this become the common representation of JIDs (users and rooms)
 * Remove any use of parse that I came accross (it's not needed)
 * Make the the set_available methods take this new JID object as an argument.
 * Override set_avail and unavail in the Room roster class to support the kicking (also means more accurate debugging can be added)
 * Add a tiny kick method on the UM object and migrate the other use of kick to this.
 * Rename um.JOIN_ATTEMPTS_MAX so its less shouty
 * Acknowledge that it is not ideal that UM is reading roomowner config.
 * Allow a rooms member list to be specified in the roomowner section of config [Though more thought needed on this e.g. how EnDroid should support joining rooms it doesn't own]
 * Remove unnecessary set_registration_list method on UM (it was only called from UM methods which can call the roster method directly).

To post a comment you must log in.
Revision history for this message
Matthew Earl (matthew-earl) wrote :

line 21 (src/endroid/rosterhandler.py):
    logging.info("Available from {} '{}' priority: '{}'".format(str(jid), show, priority))

    Superfluous use of str?

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

The presence of an explicit fetch of the roomowner plugin's config in the core seems horrible. Can't we just move that config up to be core config? We can then move it back out to plugin config once we've cleaned up the interactions throughout.

Some of your docstrings don't have a newline after the opening triple quotes. Please change, as it is easier to read the other way.

Revision history for this message
ChrisD (gingerchris) wrote :

> line 21 (src/endroid/rosterhandler.py):
> logging.info("Available from {} '{}' priority: '{}'".format(str(jid),
> show, priority))
>
> Superfluous use of str?

Meh

Revision history for this message
ChrisD (gingerchris) wrote :

> The presence of an explicit fetch of the roomowner plugin's config in the core
> seems horrible. Can't we just move that config up to be core config? We can
> then move it back out to plugin config once we've cleaned up the interactions
> throughout.

There are two cases:
 * The password
 * The user list

The password was already there and will be a pain to move.
The user one was added by me - I can remove that but if I do it means you need two room sections:

[room : <email address hidden>] - with the user list
and
[room : <email address hidden> : plugin : endroid.plugins.roomowner] - with the name and description

when owning a room. This is a pain.

While the password config read hack is still there, having the user list hack seems sensible. Removing the password config hack opens the larger can of worms.

>
> Some of your docstrings don't have a newline after the opening triple quotes.
> Please change, as it is easier to read the other way.

Done.

Revision history for this message
ChrisD (gingerchris) wrote :

> > The presence of an explicit fetch of the roomowner plugin's config in the
> core
> > seems horrible. Can't we just move that config up to be core config? We can
> > then move it back out to plugin config once we've cleaned up the
> interactions
> > throughout.
>
> There are two cases:
> * The password
> * The user list
>
> The password was already there and will be a pain to move.
> The user one was added by me - I can remove that but if I do it means you need
> two room sections:
>
> [room : <email address hidden>] - with the user list
> and
> [room : <email address hidden> : plugin : endroid.plugins.roomowner] - with the name
> and description
>
> when owning a room. This is a pain.
>
> While the password config read hack is still there, having the user list hack
> seems sensible. Removing the password config hack opens the larger can of
> worms.

As per discussion; will leave this for now. See my advanced-roomowner blueprint for some thoughts on sorting this:

https://blueprints.launchpad.net/endroid/+spec/advanced-roomowner

>
>
> >
> > Some of your docstrings don't have a newline after the opening triple
> quotes.
> > Please change, as it is easier to read the other way.
>
> Done.

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

More than one proposal found for merge of lp:~gingerchris/endroid/bugfix into lp:endroid, which is not Superseded.

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

More than one proposal found for merge of lp:~gingerchris/endroid/bugfix into lp:endroid, which is not Superseded.

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

More than one proposal found for merge of lp:~gingerchris/endroid/bugfix into lp:endroid, which is not Superseded.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/endroid/rosterhandler.py'
2--- src/endroid/rosterhandler.py 2013-08-21 09:56:42 +0000
3+++ src/endroid/rosterhandler.py 2013-09-06 10:08:14 +0000
4@@ -6,7 +6,7 @@
5
6 import logging
7
8-from twisted.words.protocols.jabber.jid import JID
9+from endroid.usermanagement import JID
10 from wokkel.xmppim import RosterClientProtocol, PresenceClientProtocol, RosterItem
11
12
13@@ -81,23 +81,34 @@
14 self.available()
15
16 def availableReceived(self, entity, show=None, statuses=None, priority=0):
17- userhost, full_jid = entity.userhost(), entity.full()
18- logging.info("Available from {} '{}' priority: '{}'".format(full_jid, show, priority))
19+ jid = JID(entity.full())
20+ userhost = jid.userhost()
21+ logging.info("Available from {} '{}' priority: '{}'".format(jid, show, priority))
22 # entity has come online - update our online set
23 if userhost in self.um.users():
24- self.um.set_available(full_jid, show=show, priority=priority)
25+ self.um.set_available(jid, show=show, priority=priority)
26 # make them available in their groups
27 for group in self.um.get_groups(userhost):
28- self.um.set_available(full_jid, group, show=show, priority=priority)
29+ self.um.set_available(jid, group, show=show, priority=priority)
30+ else:
31+ # Probably a user entering a room EnDroid is in.
32+ # If this is the case EnDroid will find out via a userJoinedRoom call
33+ # on wokkelhandler
34+ pass
35
36 def unavailableReceived(self, entity, statuses=None):
37- userhost, full_jid = entity.userhost(), entity.full()
38- logging.info("Unavailable from {}".format(full_jid))
39+ jid = JID(entity.full())
40+ userhost = jid.userhost()
41+ logging.info("Unavailable from {}".format(str(jid)))
42 # entity has gone offline - update our online set
43 if userhost in self.um.available_users():
44- self.um.set_unavailable(full_jid)
45+ self.um.set_unavailable(jid)
46 # make them unavailable in their groups
47 for group in self.um.get_groups(userhost):
48- self.um.set_unavailable(full_jid, group)
49-
50+ self.um.set_unavailable(jid, group)
51+ else:
52+ # Probably a user leaving a room EnDroid is in.
53+ # If this is the case EnDroid will find out via a userLeftRoom call
54+ # on wokkelhandler
55+ pass
56
57
58=== modified file 'src/endroid/usermanagement.py'
59--- src/endroid/usermanagement.py 2013-08-21 17:00:41 +0000
60+++ src/endroid/usermanagement.py 2013-09-06 10:08:14 +0000
61@@ -5,7 +5,8 @@
62 # -----------------------------------------
63
64 import logging
65-from twisted.words.protocols.jabber.jid import JID, parse, InvalidFormat
66+import twisted.words.protocols.jabber.jid
67+from twisted.words.protocols.jabber.jid import InvalidFormat
68 from twisted.words.protocols.jabber.error import StanzaError
69 from endroid.pluginmanager import PluginManager
70 from random import choice
71@@ -24,6 +25,38 @@
72
73 Place = namedtuple("Place", ("type", "name"))
74
75+
76+class JID(twisted.words.protocols.jabber.jid.JID):
77+ """
78+ Wrapper around twisteds JID class to provide more functionality.
79+
80+
81+ Attributes:
82+ Inherited from twisted:
83+ user - the user part of the JID
84+ host - the host part of the JID
85+ resource - the resource part of the JID
86+ Added by this class:
87+ nick - nickname of this JID if in a room
88+
89+ Methods:
90+ Inherited from twisted:
91+ full - string representation of the JID - user str(JID) instead
92+ userhost - string representation of just the user and host
93+ parts of this JID
94+ userhostJID - a new JID object representing just the user and
95+ host parts of this JID
96+
97+ """
98+
99+ def __init__(self, str=None, tuple=None, nick=None, **kwargs):
100+ self.nick = nick
101+ super(JID, self).__init__(str=str, tuple=tuple, **kwargs)
102+
103+ def __str__(self):
104+ return self.full()
105+
106+
107 class Roster(object):
108 """
109 Provides functions for maintaining sets of users registered with and
110@@ -67,17 +100,29 @@
111 self._members.pop(name, None)
112 self.deregistration_cb(name, self.name)
113
114- def set_available(self, full_jid, show=None, priority=0):
115- userhost = UserManagement.get_userhost(full_jid)
116- logging.debug("{} came online as {}".format(userhost, full_jid))
117- if userhost in self._members:
118- self._members[userhost][full_jid] = Resource(show, priority)
119-
120- def set_unavailable(self, full_jid):
121- userhost = UserManagement.get_userhost(full_jid)
122- logging.debug("{} went offline as {}".format(userhost, full_jid))
123- if userhost in self._members:
124- self._members[userhost].pop(full_jid, None)
125+ def set_available(self, jid, show=None, priority=0):
126+ """
127+ Mark a JID as available.
128+
129+ jid should be a JID object
130+
131+ """
132+
133+ logging.debug("{} available at resource {}".format(jid.userhost(), jid.resource))
134+ if jid.userhost() in self._members:
135+ self._members[jid.userhost()][jid.full()] = Resource(show, priority)
136+
137+ def set_unavailable(self, jid):
138+ """
139+ Mark a JID as unavailable.
140+
141+ jid should be a JID object
142+
143+ """
144+
145+ logging.debug("{} unavailable at resource {}".format(jid.userhost(), jid.resource))
146+ if jid.userhost() in self._members:
147+ self._members[jid.userhost()].pop(jid.full(), None)
148
149 def __repr__(self):
150 name = self.name or "contacts"
151@@ -119,14 +164,48 @@
152 options[MUC + "passwordprotectedroom"] = True
153 return options
154
155- def __init__(self, name, nick, password, *args, **kwargs):
156+ def __init__(self, name, nick, password, um, *args, **kwargs):
157 self.nick = nick
158 self.password = password
159- # our add/remove user deferreds can only be run after we join the room,
160- # so keep this list to store those generated before we do so
161+
162+ # Keep a reference to the usermanagement object
163+ # @@@ Evil hack until the roomowner plugin can register for
164+ # presence notifications - the UM ref is needed to kick
165+ # unwanted members
166+ self.um = um
167+
168+ # Initialise a list to keep membership changes to be made
169+ # once EnDroid has joined the room
170 self.deferreds = []
171 super(Room, self).__init__(name, *args, **kwargs)
172
173+ def set_available(self, user):
174+ """
175+ Mark a user as being available in a room.
176+
177+ user is a JID object.
178+ If the user isn't registered with the room they are kicked.
179+
180+ """
181+
182+ logging.debug("{} ({}) joined room {}".format(
183+ user.nick, user, self.name))
184+ if user.userhost() in self._members:
185+ self._members[user.userhost()][user.full()] = Resource(False, 1)
186+ else:
187+ # This person shouldn't be here, kick
188+ self.um.kick(room=self.name, nick=user.nick, reason="You are "
189+ "not on the memberlist, sorry.")
190+
191+ def set_unavailable(self, user):
192+ """Process a user leaving a room."""
193+
194+ logging.debug("{} ({}) left room {}".format(
195+ user.nick, user, self.name))
196+ if user.userhost() in self._members:
197+ self._members[user.userhost()].pop(user.full(), None)
198+
199+
200 class Resource(object):
201 __slots__ = ("show", "priority")
202
203@@ -153,6 +232,7 @@
204 self._pms = {} # a dict of {room/group names : pluginmanager objects}
205
206 self.jid = None
207+ self.jid_obj = None
208 # our contact list and room list
209 self._users = Roster(None, self._cb_add_contact, self._cb_rem_contact)
210 self._rooms = Roster()
211@@ -163,38 +243,48 @@
212 self.conf = config
213 self._read_config(config)
214
215+ def _allowed_users(self, r_g, name):
216+ """Get the set of allowed users for this room or group."""
217+
218+ # The config get could return an empty list e.g. if 'users='
219+ users = set(self.conf.get(r_g, name, "users", default=self.users()))
220+ return users & self.users()
221+
222 def _read_config(self, config):
223- def allowed_users(r_g, name):
224- # get may return an empty string (if the line is 'users='), so use
225- # or to make sure we have a set
226- users = set(config.get(r_g, name, "users", default=self.users()))
227- return users & self.users()
228-
229- # set our contact list and room list
230+ # Set our contact list and room list
231 self.jid = config.get("setup", "jid")
232+ self.jid_obj = JID(self.jid)
233 self._users.set_registration_list(config.get("setup", "users", default=[]))
234 self._rooms.set_registration_list(config.get("setup", "rooms", default=[]))
235
236- self.JOIN_ATTEMPTS_MAX = config.get("setup", "join_attempts", default=5)
237+ self.join_attempts = config.get("setup", "join_attempts",
238+ default=self.JOIN_ATTEMPTS_MAX)
239
240- # set contact lists for our rooms
241+ nick = config.get("setup", "nick", default=self.jid_obj.user)
242+ # Set contact lists for our rooms
243 for room in config.get("setup", "rooms", default=[]):
244- # what we need to join the room
245- nick = config.get("setup", "nick", default=parse(self.jid)[0])
246+ # What we need to join the room
247+ # @@@ Evil hack until roomowner can be made a global plugin
248+ # that triggers room joins
249 password = config.get("room", room, "plugin",
250 "endroid.plugins.roomowner", "password",
251 default="")
252- # user data
253- self.room_rosters[room] = Room(room, nick, password,
254+ try:
255+ users = config.get("room", room, "plugin",
256+ "endroid.plugins.roomowner", "users")
257+ users = set(users) & self.users()
258+ except KeyError:
259+ # User list may have been specified old style:
260+ users = self._allowed_users('room', room)
261+ self.room_rosters[room] = Room(room, nick, password, self,
262 self._cb_add_room_contact,
263 self._cb_rem_room_contact)
264- users = allowed_users("room", room)
265- self.set_registration_list(users, room)
266+ self.room_rosters[room].set_registration_list(users)
267
268 for group in config.get("setup", "groups", default=['all']):
269 self.group_rosters[group] = Roster(group)
270- users = allowed_users("group", group)
271- self.set_registration_list(users, group)
272+ users = self._allowed_users("group", group)
273+ self.group_rosters[group].set_registration_list(users)
274
275 # rosterhandler receives presence notifications and gives them to us
276 self.rh.set_presence_handler(self)
277@@ -317,31 +407,18 @@
278 or if place is None (default), the user part of the jid.
279
280 """
281- user = self.get_userhost(user)
282+ user_jid = JID(user)
283 if place is None or place in self.group_rosters:
284- return parse(user)[0]
285+ return user_jid.user
286 elif place in self.room_rosters:
287 for (nick, rosteritem) in self.wh._rooms[JID(place)].roster.items():
288- if user == rosteritem.entity.userhost():
289+ if user_jid.userhost() == rosteritem.entity.userhost():
290 return nick
291 return "unknown"
292 get_nickname = nickname
293
294 ### Functions for managing contact lists ###
295
296- def set_registration_list(self, names, place=None):
297- """
298- Set the 'member' list for our contacts (place = None) or in a group or
299- room. This specifies who is allowed in said place.
300-
301- """
302- if place is None:
303- self._users.set_registration_list(names)
304- elif place in self.group_rosters:
305- self.group_rosters[place].set_registration_list(names)
306- elif place in self.room_rosters:
307- self.room_rosters[place].set_registration_list(names)
308-
309 def register_user(self, name, place=None):
310 """
311 Add a user to the 'member' list for our contacts (place=None) or in a
312@@ -368,31 +445,41 @@
313 elif place in self.room_rosters:
314 self.room_rosters[place].deregister_user(name)
315
316- def set_available(self, name, place=None, show=None, priority=0):
317- """
318- Add a user to the 'available' list for our contacts (place=None) or in a
319- group or room.
320-
321- """
322- if place is None:
323- self._users.set_available(name, show, priority)
324- elif place in self.group_rosters:
325- self.group_rosters[place].set_available(name, show, priority)
326- elif place in self.room_rosters:
327- self.room_rosters[place].set_available(name, show, priority)
328-
329- def set_unavailable(self, name, place=None):
330- """
331- Add a user to the 'available' list for our contacts (place=None) or in a
332- group or room.
333-
334- """
335- if place is None:
336- self._users.set_unavailable(name)
337- elif place in self.group_rosters:
338- self.group_rosters[place].set_unavailable(name)
339- elif place in self.room_rosters:
340- self.room_rosters[place].set_unavailable(name)
341+ def set_available(self, user, place=None, show=None, priority=0):
342+ """
343+ Add a user to the 'available' list for our contacts (place=None) or in a
344+ group or room.
345+
346+ user should be a JID object
347+
348+ """
349+ if place is None:
350+ self._users.set_available(user, show, priority)
351+ elif place in self.group_rosters:
352+ self.group_rosters[place].set_available(user, show, priority)
353+ elif place in self.room_rosters:
354+ self.room_rosters[place].set_available(user)
355+ else:
356+ logging.error("User {} available but {} not in any roster".format(
357+ user, place))
358+
359+ def set_unavailable(self, user, place=None):
360+ """
361+ Add a user to the 'available' list for our contacts (place=None) or in a
362+ group or room.
363+
364+ user should be a JID object
365+
366+ """
367+ if place is None:
368+ self._users.set_unavailable(user)
369+ elif place in self.group_rosters:
370+ self.group_rosters[place].set_unavailable(user)
371+ elif place in self.room_rosters:
372+ self.room_rosters[place].set_unavailable(user)
373+ else:
374+ logging.error("User {} unavailable but {} not in any roster".format(
375+ user, room))
376
377 # callbacks for adding/removing from our main contact list
378 def _cb_add_contact(self, name, _):
379@@ -443,6 +530,8 @@
380 members.append(user)
381 else:
382 nones.append(user)
383+ # Reset the list as we don't need it anymore
384+ self.room_rosters[name].deferreds = []
385
386 fmt_add = "Added {} to {}"
387 fmt_rem = "Removed {} from {}"
388@@ -465,20 +554,21 @@
389 def join_room(self, name, attempts=0):
390 """
391 Attempt to join the room 'name'. On failure will try again with
392- different nicknames until attempts == JOIN_ATTEMPTS_MAX succes.
393+ different nicknames until attempts == join_attempts.
394
395 """
396 if not name in self.room_rosters:
397 return "Failed to join room {}: not allowed".format(name)
398
399 def retry(failure, room, nick, attempts):
400- if attempts < self.JOIN_ATTEMPTS_MAX:
401+ if attempts < self.join_attempts:
402 return self.join_room(room, attempts+1)
403 else:
404 return "Failed to join room {} as {}".format(room, nick)
405 def joined(_, room, nick, attempts):
406 if attempts:
407- self.wh.kick(JID(room), nick, "Begone thou who taketh my name!")
408+ self.kick(room, nick, "You've taken EnDroid's nick, "
409+ "please reconnect with another nick")
410 self.wh.nick(JID(room), nick)
411 return "Joined room {} as {}".format(room, nick)
412
413@@ -493,11 +583,16 @@
414
415 return d
416
417+ def kick(self, room, nick, reason=None):
418+ """Kick the user with the specified nick from the room."""
419+ logging.debug("Kicking {} from {}".format(nick, room))
420+ self.wh.kick(JID(room), nick, reason)
421+
422 def joined_room(self, name):
423 logging.info("Joined room {}".format(name))
424 self._do_room_contacts(name)
425- self._rooms.set_available(name)
426- if not name in self._pms:
427+ self._rooms.set_available(JID(name))
428+ if name not in self._pms:
429 self.start_pm(None, "room", name)
430
431 def joined_group(self, name):
432
433=== modified file 'src/endroid/wokkelhandler.py'
434--- src/endroid/wokkelhandler.py 2013-08-28 17:22:30 +0000
435+++ src/endroid/wokkelhandler.py 2013-09-06 10:08:14 +0000
436@@ -5,12 +5,12 @@
437 # -----------------------------------------
438
439 from endroid.messagehandler import Message
440+from endroid.usermanagement import JID
441 from endroid.cron import Cron
442 import logging
443
444 from wokkel.muc import MUCClient
445 from wokkel.xmppim import MessageProtocol
446-from twisted.words.protocols.jabber.jid import JID
447 from twisted.internet.task import LoopingCall
448
449
450@@ -106,17 +106,18 @@
451 # MUCClient methods to be overridden
452
453 def userJoinedRoom(self, room, user):
454- room_name = room.roomJID.userhost()
455- user_name = user.entity.full()
456- if user_name == self.jid:
457+ user_obj = JID(user.entity.full(), nick=user.nick)
458+ if user_obj.full() == self.jid:
459 # This is EnDroid joining a room nothing to do
460 pass
461 else:
462- logging.info(user_name + " joined " + room_name)
463- self.usermanagement.set_available(user, room)
464+ self.usermanagement.set_available(user_obj,
465+ room.roomJID.userhost())
466
467 def userLeftRoom(self, room, user):
468- self.usermanagement.set_unavailable(user, room)
469+ user_obj = JID(user.entity.full(), nick=user.nick)
470+ self.usermanagement.set_unavailable(user_obj,
471+ room.roomJID.userhost())
472
473 def userUpdatedStatus(self, room, user, show, status):
474 pass

Subscribers

People subscribed via source and target branches