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

Proposed by ChrisD
Status: Merged
Approved by: ChrisD
Approved revision: 63
Merged at revision: 61
Proposed branch: lp:~gingerchris/endroid/roomownerfixes
Merge into: lp:endroid
Diff against target: 308 lines (+164/-72)
2 files modified
src/endroid/plugins/roomowner.py (+60/-4)
src/endroid/usermanagement.py (+104/-68)
To merge this branch: bzr merge lp:~gingerchris/endroid/roomownerfixes
Reviewer Review Type Date Requested Status
Martin Morrison Approve
Review via email: mp+184596@code.launchpad.net

Commit message

RoomOwner fixes

Description of the change

Following roomowner changes:

 * Mark and sweep MUC member list before changing any member affiliations (#1222857)
 * Change room owner to only configure MUCs that EnDroid owns (#1222858)

In addition:

 * As a result of moving MUC affiliation changes to the roomowner plugin, the functions are removed from usermanagement that performed these actions.
 * Now that affiliation is only changed once EnDroid has joined the room the old 'deferreds' list is no longer required so that is removed
 * Correct the error handling in configure_room to use trap correctly.

The following APIs are added to UM:

 * room_modify_affiliation
 * room_memberlist_change
 * is_room_owner
 * get_configured_room_memberlist
 * get_room_memberlist

It is noted that these are room-centric API's on an object/class called 'usermanagement'. Though there is precedence here (e.g. configure_room) this isn't ideal. Open to suggestions (blueprints) on how to re-org.

Note: this goes some way to implementing BP advanced-roomowner however it falls short in a few key areas:

 * MUC memberlist config is being read by both core EnDroid and roomowner (and indeed can be specified in both config sections).
 * EnDroid core is still joing all rooms and roomowner assumes it owns all rooms specified in config.
 * No additional support for non-standard MUC implementations (and supported config).

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

get_room_memberlist and is_room_owner should guarantee to return a Deferred, rather than returning None if bad args are given. Using defer.fail would make sense here.

I think owner_result and parse_memberlist in the RoomOwner plugin should be inner funcs as they are just locally used callbacks (this would match the pattn used elsewhere in Enadroid, including further down in this diff).

review: Needs Fixing
62. By Chris Davidson <email address hidden>

Markups

63. By Chris Davidson <email address hidden>

Windows mount commit

Revision history for this message
ChrisD (gingerchris) wrote :

> get_room_memberlist and is_room_owner should guarantee to return a Deferred,
> rather than returning None if bad args are given. Using defer.fail would make
> sense here.

Done.

>
> I think owner_result and parse_memberlist in the RoomOwner plugin should be
> inner funcs as they are just locally used callbacks (this would match the
> pattn used elsewhere in Enadroid, including further down in this diff).

k. Have basically reworked the methods here in the process:
 * Made private
 * Daisy changed better
 * Split out to make a bit easier to read

Let me know if that works.

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/endroid/plugins/roomowner.py'
2--- src/endroid/plugins/roomowner.py 2013-07-31 15:39:20 +0000
3+++ src/endroid/plugins/roomowner.py 2013-09-10 15:35:09 +0000
4@@ -1,9 +1,65 @@
5+import logging
6 from endroid.pluginmanager import Plugin
7
8+
9 class RoomOwner(Plugin):
10+
11 def endroid_init(self):
12- if self.place == "room":
13- # note that configure_room internally sanitises anything it gets
14- # - allowing only predefined keys and ignoring the rest (so giving it
15+ if self.place != "room":
16+ return
17+
18+ def owner_result(is_owner):
19+ if not is_owner:
20+ logging.error("EnDroid doesn't appear to be the owner for "
21+ "room {}".format(self.place_name))
22+ return
23+
24+ logging.debug("EnDroid owns room {}".format(self.place_name))
25+
26+ self._update_memberlist()
27+
28+ # Apply the config
29+ # @@@Config mark and sweep should be added here
30+ # configure_room internally sanitises anything it gets
31+ # allowing only predefined keys and ignoring the rest (so giving it
32 # our whole .vars dictionary is safe (and easy))
33- self.usermanagement.configure_room(self.place_name, self.vars)
34+ self.usermanagement.configure_room(self.place_name, self.vars)
35+
36+ # First check if EnDroid owns this room
37+ d = self.usermanagement.is_room_owner(self.place_name)
38+ d.addCallback(owner_result)
39+
40+ def _get_configured_memberlist(self):
41+ if 'users' in self.vars:
42+ memberlist = set(self.vars['users'])
43+ else:
44+ # Check for old-style config
45+ memberlist = self.usermanagement.get_configured_room_memberlist(
46+ self.place_name)
47+
48+ return memberlist
49+
50+ def _update_memberlist(self):
51+ """Update the member list of this room based on config."""
52+
53+ def parse_memberlist(memberlist):
54+ current_memberlist = set(memberlist)
55+ conf_memberlist = self._get_configured_memberlist()
56+
57+ logging.debug('Configured memberlist for room {}: {}'.format(
58+ self.place_name, ", ".join(conf_memberlist)))
59+ logging.debug('Current memberlist for room {}: {}'.format(
60+ self.place_name, ", ".join(current_memberlist)))
61+
62+ new_members = conf_memberlist - current_memberlist
63+ dead_members = current_memberlist - conf_memberlist
64+ if new_members:
65+ self.usermanagement.room_memberlist_change(
66+ self.place_name, new_members)
67+ if dead_members:
68+ self.usermanagement.room_memberlist_change(
69+ self.place_name, dead_members, remove=True)
70+
71+ # Get the current memberlist to determine what changes need to be made
72+ d = self.usermanagement.get_room_memberlist(self.place_name)
73+ d.addCallback(parse_memberlist)
74
75=== modified file 'src/endroid/usermanagement.py'
76--- src/endroid/usermanagement.py 2013-09-06 10:06:52 +0000
77+++ src/endroid/usermanagement.py 2013-09-10 15:35:09 +0000
78@@ -174,9 +174,6 @@
79 # unwanted members
80 self.um = um
81
82- # Initialise a list to keep membership changes to be made
83- # once EnDroid has joined the room
84- self.deferreds = []
85 super(Room, self).__init__(name, *args, **kwargs)
86
87 def set_available(self, user):
88@@ -276,9 +273,7 @@
89 except KeyError:
90 # User list may have been specified old style:
91 users = self._allowed_users('room', room)
92- self.room_rosters[room] = Room(room, nick, password, self,
93- self._cb_add_room_contact,
94- self._cb_rem_room_contact)
95+ self.room_rosters[room] = Room(room, nick, password, self)
96 self.room_rosters[room].set_registration_list(users)
97
98 for group in config.get("setup", "groups", default=['all']):
99@@ -494,58 +489,6 @@
100 self.rh.removeItem(name) # sets the subscription to None both ways
101 logging.info("Removed {} from {}".format(name, "contacts"))
102
103- # callbacks for adding/removing from rooms - can only be called after we are
104- # in the room ie if room is in self._wh.rooms. otherwise append the callback
105- # to a list to be called when we join (from userJoinedRoom in wokkelhandler)
106- def _cb_add_room_contact(self, name, room):
107- fmt_add = "Added {} to {}"
108- fmt_fail = "Failed to add {} to {}"
109-
110- if room in self.wh._rooms:
111- d = self.wh.modifyAffiliationList(JID(room), [JID(name)], "member")
112- d.addCallback(lambda _: logging.info(fmt_add.format(name, room)))
113- d.addErrback(lambda _: logging.info(fmt_fail.format(name, room)))
114- logging.debug("Adding {} to {}".format(name, room))
115- return d
116- else:
117- self.room_rosters[room].deferreds.append((name, 1))
118-
119- def _cb_rem_room_contact(self, name, room):
120- fmt_rem = "Removed {} from {}"
121- fmt_fail = "Failed to remove {} from {}"
122-
123- if room in self.wh._rooms:
124- d = self.wh.modifyAffiliationList(JID(room), [JID(name)], "none")
125- d.addCallback(lambda _: logging.info(fmt_rem.format(name, room)))
126- d.addErrback(lambda _: logging.info(fmt_fail.format(name, room)))
127- logging.debug("Removing {} from {}".format(name, room))
128- return d
129- else:
130- self.room_rosters[room].deferreds.append((name, 0))
131-
132- def _do_room_contacts(self, name):
133- members, nones = [], []
134- for (user, add) in self.room_rosters[name].deferreds:
135- if add:
136- members.append(user)
137- else:
138- nones.append(user)
139- # Reset the list as we don't need it anymore
140- self.room_rosters[name].deferreds = []
141-
142- fmt_add = "Added {} to {}"
143- fmt_rem = "Removed {} from {}"
144-
145- if members:
146- string = fmt_add.format(', '.join(members), name)
147- d1 = self.wh.modifyAffiliationList(JID(name), map(JID, members), "member")
148- d1.addCallback(lambda _: logging.info(string))
149- if nones:
150- string = fmt_rem.format(', '.join(nones), name)
151- d2 = self.wh.modifyAffiliationList(JID(name), map(JID, nones), "none")
152- d2.addCallback(lambda _: logging.info(string))
153-
154-
155 ### Room functions
156
157 # @@@ at the moment this assumes that if we failed to join a room it was because
158@@ -590,7 +533,6 @@
159
160 def joined_room(self, name):
161 logging.info("Joined room {}".format(name))
162- self._do_room_contacts(name)
163 self._rooms.set_available(JID(name))
164 if name not in self._pms:
165 self.start_pm(None, "room", name)
166@@ -617,10 +559,38 @@
167 d = self.join_room(room)
168 d.addCallback(lambda _, r: self.joined_room(r), room)
169
170+ # @@@ This function is in the wrong place. This is the room section
171 def join_all_groups(self):
172 for group in self.get_groups():
173 self.joined_group(group)
174
175+ def room_modify_affiliation(self, room, users, affiliation):
176+ """Update the affiliation list for this room."""
177+
178+ # Check that only EnDroid's contacts can be added to rooms
179+ user_set = set(users)
180+ un_reg_users = user_set - self.users()
181+ if len(un_reg_users) > 0 and affiliation != 'none':
182+ logging.warning('Attempt to allow {} unregistered user(s) '
183+ 'access to {}'.format(len(un_reg_users), room))
184+
185+ allowed_users = user_set & self.users()
186+
187+ jids = [JID(user) for user in allowed_users]
188+ d = self.wh.modifyAffiliationList(JID(room), jids, affiliation)
189+ d.addCallback(lambda _: logging.info("Modified affiliation for "
190+ "room {}.".format(room)))
191+ d.addErrback(lambda _: logging.error("Failed to modify room {}".format(
192+ room)))
193+ logging.debug("Changing affiliation for {} users in room {} to "
194+ "{}".format(len(users), room, affiliation))
195+ return d
196+
197+ def room_memberlist_change(self, room, users, remove=False):
198+ if remove:
199+ return self.room_modify_affiliation(room, users, 'none')
200+ else:
201+ return self.room_modify_affiliation(room, users, 'member')
202
203 def configure_room(self, name, options):
204 """
205@@ -630,21 +600,22 @@
206 description.
207
208 """
209- def cb(value, name):
210- try:
211- failure = value.trap(StanzaError)
212- if failure == StanzaError:
213- return "Configured {}: not all options set".format(name)
214- except:
215- return "Configured {}".format(name)
216+ def conf_failed(failure):
217+ failure.trap(StanzaError)
218+ logging.error("Failure configuring {}: code {}".format(name,
219+ failure.value.code))
220+
221+ def cb(value):
222+ logging.info("Configured {}".format(name))
223
224 mucoptions = Room.config_default.copy()
225 # options is a human-readable dict, we intersect with Room.config_options
226 # and translate into muc#roomconfig_xxx terms in translate_options
227 mucoptions.update(Room.translate_options(options))
228 d = self.wh.configure(JID(name), mucoptions)
229- d.addBoth(cb, name)
230- return d.addCallback(logging.info)
231+ d.addCallback(cb)
232+ d.addErrback(conf_failed)
233+ return d
234
235 def get_configuration(self, name):
236 """
237@@ -660,6 +631,71 @@
238 d = self.wh.getConfiguration(JID(name))
239 return d.addCallback(form_to_string)
240
241+ def is_room_owner(self, room):
242+ """Find out if EnDroid is an owner for the specified room. """
243+
244+ def determine_if_owner(owner_list):
245+ """
246+ Search the list of AdminItems to see if EnDroid's JID is there.
247+
248+ Should resource be listened to here?
249+
250+ """
251+
252+ return self.jid_obj.userhost() in (owner.entity.userhost()
253+ for owner in owner_list)
254+
255+ def error_determining_ownership(failure):
256+ # Re-raise the failure if its not a StanzaError
257+ failure.trap(StanzaError)
258+
259+ if failure.value.code == '403':
260+ # This is the forbidden code - so no EnDroid doesn't own the
261+ # room
262+ return False
263+ else:
264+ # If it's any other error let the caller deal with it
265+ logging.error("Error determining ownership of {}. Code "
266+ "{}".format(room, failure.value.code))
267+ return failure
268+
269+ if room in self.get_rooms():
270+ return self.wh.getOwnerList(JID(room)).addCallbacks(
271+ determine_if_owner, error_determining_ownership)
272+ else:
273+ # Always return a deferred
274+ return twisted.internet.defer.fail()
275+
276+ def get_configured_room_memberlist(self, room):
277+ """
278+ Return the list (if any) of configured members for this room.
279+
280+ This is to support old style config where the user list was owned by
281+ EnDroid core.
282+
283+ """
284+ return self._allowed_users('room', room)
285+
286+ def get_room_memberlist(self, room):
287+ """
288+ Get the member list for a room.
289+
290+ Note: EnDroid must be room owner for this to work.
291+
292+ """
293+
294+ def convert_admin_list(member_list):
295+ """Convert the list of AdminItems into userhosts. """
296+
297+ return [member.entity.userhost() for member in member_list]
298+
299+ if room in self.get_rooms():
300+ return self.wh.getMemberList(JID(room)).addCallback(
301+ convert_admin_list)
302+ else:
303+ # Always return a deferred
304+ return twisted.internet.defer.fail()
305+
306 def invite(self, user, room, reason=None):
307 """
308 Invite a user to a room.

Subscribers

People subscribed via source and target branches