Merge lp:~gingerchris/endroid/nonmemberkick into lp:endroid
- nonmemberkick
- Merge into trunk
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 |
Related bugs: |
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_
* 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_registratio
Matthew Earl (matthew-earl) wrote : | # |
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.
ChrisD (gingerchris) wrote : | # |
> line 21 (src/endroid/
> logging.
> show, priority))
>
> Superfluous use of str?
Meh
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.
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.
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.
> 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:/
>
>
> >
> > 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.
Martin Morrison (isoschiz) : | # |
Martin Morrison (isoschiz) wrote : | # |
More than one proposal found for merge of lp:~gingerchris/endroid/bugfix into lp:endroid, which is not Superseded.
Martin Morrison (isoschiz) wrote : | # |
More than one proposal found for merge of lp:~gingerchris/endroid/bugfix into lp:endroid, which is not Superseded.
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
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 |
line 21 (src/endroid/ rosterhandler. py): info("Available from {} '{}' priority: '{}'".format( str(jid) , show, priority))
logging.
Superfluous use of str?