comparison sat/plugins/plugin_misc_email_invitation.py @ 4037:524856bd7b19

massive refactoring to switch from camelCase to snake_case: historically, Libervia (SàT before) was using camelCase as allowed by PEP8 when using a pre-PEP8 code, to use the same coding style as in Twisted. However, snake_case is more readable and it's better to follow PEP8 best practices, so it has been decided to move on full snake_case. Because Libervia has a huge codebase, this ended with a ugly mix of camelCase and snake_case. To fix that, this patch does a big refactoring by renaming every function and method (including bridge) that are not coming from Twisted or Wokkel, to use fully snake_case. This is a massive change, and may result in some bugs.
author Goffi <goffi@goffi.org>
date Sat, 08 Apr 2023 13:54:42 +0200
parents cfc06915de15
children
comparison
equal deleted inserted replaced
4036:c4464d7ae97b 4037:524856bd7b19
75 75
76 def __init__(self, host): 76 def __init__(self, host):
77 log.info(_("plugin Invitations initialization")) 77 log.info(_("plugin Invitations initialization"))
78 self.host = host 78 self.host = host
79 self.invitations = persistent.LazyPersistentBinaryDict('invitations') 79 self.invitations = persistent.LazyPersistentBinaryDict('invitations')
80 host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sasssssssssa{ss}s', 80 host.bridge.add_method("invitation_create", ".plugin", in_sign='sasssssssssa{ss}s',
81 out_sign='a{ss}', 81 out_sign='a{ss}',
82 method=self._create, 82 method=self._create,
83 async_=True) 83 async_=True)
84 host.bridge.addMethod("invitationGet", ".plugin", in_sign='s', out_sign='a{ss}', 84 host.bridge.add_method("invitation_get", ".plugin", in_sign='s', out_sign='a{ss}',
85 method=self.get, 85 method=self.get,
86 async_=True) 86 async_=True)
87 host.bridge.addMethod("invitationDelete", ".plugin", in_sign='s', out_sign='', 87 host.bridge.add_method("invitation_delete", ".plugin", in_sign='s', out_sign='',
88 method=self._delete, 88 method=self._delete,
89 async_=True) 89 async_=True)
90 host.bridge.addMethod("invitationModify", ".plugin", in_sign='sa{ss}b', 90 host.bridge.add_method("invitation_modify", ".plugin", in_sign='sa{ss}b',
91 out_sign='', 91 out_sign='',
92 method=self._modify, 92 method=self._modify,
93 async_=True) 93 async_=True)
94 host.bridge.addMethod("invitationList", ".plugin", in_sign='s', 94 host.bridge.add_method("invitation_list", ".plugin", in_sign='s',
95 out_sign='a{sa{ss}}', 95 out_sign='a{sa{ss}}',
96 method=self._list, 96 method=self._list,
97 async_=True) 97 async_=True)
98 host.bridge.addMethod("invitationSimpleCreate", ".plugin", in_sign='sssss', 98 host.bridge.add_method("invitation_simple_create", ".plugin", in_sign='sssss',
99 out_sign='a{ss}', 99 out_sign='a{ss}',
100 method=self._simpleCreate, 100 method=self._simple_create,
101 async_=True) 101 async_=True)
102 102
103 def checkExtra(self, extra): 103 def check_extra(self, extra):
104 if EXTRA_RESERVED.intersection(extra): 104 if EXTRA_RESERVED.intersection(extra):
105 raise ValueError( 105 raise ValueError(
106 _("You can't use following key(s) in extra, they are reserved: {}") 106 _("You can't use following key(s) in extra, they are reserved: {}")
107 .format(', '.join(EXTRA_RESERVED.intersection(extra)))) 107 .format(', '.join(EXTRA_RESERVED.intersection(extra))))
108 108
130 value = locals()[key] 130 value = locals()[key]
131 if value: 131 if value:
132 kwargs[key] = str(value) 132 kwargs[key] = str(value)
133 return defer.ensureDeferred(self.create(**kwargs)) 133 return defer.ensureDeferred(self.create(**kwargs))
134 134
135 async def getExistingInvitation(self, email: Optional[str]) -> Optional[dict]: 135 async def get_existing_invitation(self, email: Optional[str]) -> Optional[dict]:
136 """Retrieve existing invitation with given email 136 """Retrieve existing invitation with given email
137 137
138 @param email: check if any invitation exist with this email 138 @param email: check if any invitation exist with this email
139 @return: first found invitation, or None if nothing found 139 @return: first found invitation, or None if nothing found
140 """ 140 """
149 for id_, invitation in all_invitations.items(): 149 for id_, invitation in all_invitations.items():
150 if invitation.get("email") == email: 150 if invitation.get("email") == email:
151 invitation[KEY_ID] = id_ 151 invitation[KEY_ID] = id_
152 return invitation 152 return invitation
153 153
154 async def _createAccountAndProfile( 154 async def _create_account_and_profile(
155 self, 155 self,
156 id_: str, 156 id_: str,
157 kwargs: dict, 157 kwargs: dict,
158 extra: dict 158 extra: dict
159 ) -> None: 159 ) -> None:
160 """Create XMPP account and Libervia profile for guest""" 160 """Create XMPP account and Libervia profile for guest"""
161 ## XMPP account creation 161 ## XMPP account creation
162 password = kwargs.pop('password', None) 162 password = kwargs.pop('password', None)
163 if password is None: 163 if password is None:
164 password = utils.generatePassword() 164 password = utils.generate_password()
165 assert password 165 assert password
166 # XXX: password is here saved in clear in database 166 # XXX: password is here saved in clear in database
167 # it is needed for invitation as the same password is used for profile 167 # it is needed for invitation as the same password is used for profile
168 # and SàT need to be able to automatically open the profile with the uuid 168 # and SàT need to be able to automatically open the profile with the uuid
169 # FIXME: we could add an extra encryption key which would be used with the 169 # FIXME: we could add an extra encryption key which would be used with the
171 # not be saved and could be used to encrypt profile password. 171 # not be saved and could be used to encrypt profile password.
172 extra[KEY_PASSWORD] = password 172 extra[KEY_PASSWORD] = password
173 173
174 jid_ = kwargs.pop('jid_', None) 174 jid_ = kwargs.pop('jid_', None)
175 if not jid_: 175 if not jid_:
176 domain = self.host.memory.getConfig(None, 'xmpp_domain') 176 domain = self.host.memory.config_get(None, 'xmpp_domain')
177 if not domain: 177 if not domain:
178 # TODO: fallback to profile's domain 178 # TODO: fallback to profile's domain
179 raise ValueError(_("You need to specify xmpp_domain in sat.conf")) 179 raise ValueError(_("You need to specify xmpp_domain in sat.conf"))
180 jid_ = "invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(), 180 jid_ = "invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(),
181 domain=domain) 181 domain=domain)
184 184
185 if jid_.user: 185 if jid_.user:
186 # we don't register account if there is no user as anonymous login is then 186 # we don't register account if there is no user as anonymous login is then
187 # used 187 # used
188 try: 188 try:
189 await self.host.plugins['XEP-0077'].registerNewAccount(jid_, password) 189 await self.host.plugins['XEP-0077'].register_new_account(jid_, password)
190 except error.StanzaError as e: 190 except error.StanzaError as e:
191 prefix = jid_.user 191 prefix = jid_.user
192 idx = 0 192 idx = 0
193 while e.condition == 'conflict': 193 while e.condition == 'conflict':
194 if idx >= SUFFIX_MAX: 194 if idx >= SUFFIX_MAX:
195 raise exceptions.ConflictError(_("Can't create XMPP account")) 195 raise exceptions.ConflictError(_("Can't create XMPP account"))
196 jid_.user = prefix + '_' + str(idx) 196 jid_.user = prefix + '_' + str(idx)
197 log.info(_("requested jid already exists, trying with {}".format( 197 log.info(_("requested jid already exists, trying with {}".format(
198 jid_.full()))) 198 jid_.full())))
199 try: 199 try:
200 await self.host.plugins['XEP-0077'].registerNewAccount( 200 await self.host.plugins['XEP-0077'].register_new_account(
201 jid_, 201 jid_,
202 password 202 password
203 ) 203 )
204 except error.StanzaError: 204 except error.StanzaError:
205 idx += 1 205 idx += 1
214 214
215 extra[KEY_GUEST_PROFILE] = guest_profile = INVITEE_PROFILE_TPL.format( 215 extra[KEY_GUEST_PROFILE] = guest_profile = INVITEE_PROFILE_TPL.format(
216 uuid=id_ 216 uuid=id_
217 ) 217 )
218 # profile creation should not fail as we generate unique name ourselves 218 # profile creation should not fail as we generate unique name ourselves
219 await self.host.memory.createProfile(guest_profile, password) 219 await self.host.memory.create_profile(guest_profile, password)
220 await self.host.memory.startSession(password, guest_profile) 220 await self.host.memory.start_session(password, guest_profile)
221 await self.host.memory.setParam("JabberID", jid_.full(), "Connection", 221 await self.host.memory.param_set("JabberID", jid_.full(), "Connection",
222 profile_key=guest_profile) 222 profile_key=guest_profile)
223 await self.host.memory.setParam("Password", password, "Connection", 223 await self.host.memory.param_set("Password", password, "Connection",
224 profile_key=guest_profile) 224 profile_key=guest_profile)
225 225
226 async def create(self, **kwargs): 226 async def create(self, **kwargs):
227 r"""Create an invitation 227 r"""Create an invitation
228 228
288 if set(kwargs).intersection(extra): 288 if set(kwargs).intersection(extra):
289 raise ValueError( 289 raise ValueError(
290 _("You can't use following key(s) in both args and extra: {}").format( 290 _("You can't use following key(s) in both args and extra: {}").format(
291 ', '.join(set(kwargs).intersection(extra)))) 291 ', '.join(set(kwargs).intersection(extra))))
292 292
293 self.checkExtra(extra) 293 self.check_extra(extra)
294 294
295 email = kwargs.pop('email', None) 295 email = kwargs.pop('email', None)
296 296
297 existing = await self.getExistingInvitation(email) 297 existing = await self.get_existing_invitation(email)
298 if existing is not None: 298 if existing is not None:
299 log.info(f"There is already an invitation for {email!r}") 299 log.info(f"There is already an invitation for {email!r}")
300 extra.update(existing) 300 extra.update(existing)
301 del extra[KEY_ID] 301 del extra[KEY_ID]
302 302
314 ## uuid 314 ## uuid
315 log.info(_("creating an invitation")) 315 log.info(_("creating an invitation"))
316 id_ = existing[KEY_ID] if existing else str(shortuuid.uuid()) 316 id_ = existing[KEY_ID] if existing else str(shortuuid.uuid())
317 317
318 if existing is None: 318 if existing is None:
319 await self._createAccountAndProfile(id_, kwargs, extra) 319 await self._create_account_and_profile(id_, kwargs, extra)
320 320
321 profile = kwargs.pop('profile', None) 321 profile = kwargs.pop('profile', None)
322 guest_profile = extra[KEY_GUEST_PROFILE] 322 guest_profile = extra[KEY_GUEST_PROFILE]
323 jid_ = jid.JID(extra[KEY_JID]) 323 jid_ = jid.JID(extra[KEY_JID])
324 324
331 id_plugin = self.host.plugins['IDENTITY'] 331 id_plugin = self.host.plugins['IDENTITY']
332 except KeyError: 332 except KeyError:
333 pass 333 pass
334 else: 334 else:
335 await self.host.connect(guest_profile, password) 335 await self.host.connect(guest_profile, password)
336 guest_client = self.host.getClient(guest_profile) 336 guest_client = self.host.get_client(guest_profile)
337 await id_plugin.setIdentity(guest_client, {'nicknames': [name]}) 337 await id_plugin.set_identity(guest_client, {'nicknames': [name]})
338 await self.host.disconnect(guest_profile) 338 await self.host.disconnect(guest_profile)
339 339
340 ## email 340 ## email
341 language = kwargs.pop('language', None) 341 language = kwargs.pop('language', None)
342 if language is not None: 342 if language is not None:
368 format_args['host_name'] = extra['host_name'] = host_name 368 format_args['host_name'] = extra['host_name'] = host_name
369 369
370 invite_url = url_template.format(**format_args) 370 invite_url = url_template.format(**format_args)
371 format_args['url'] = invite_url 371 format_args['url'] = invite_url
372 372
373 await sat_email.sendEmail( 373 await sat_email.send_email(
374 self.host.memory.config, 374 self.host.memory.config,
375 [email] + emails_extra, 375 [email] + emails_extra,
376 (kwargs.pop('message_subject', None) or DEFAULT_SUBJECT).format( 376 (kwargs.pop('message_subject', None) or DEFAULT_SUBJECT).format(
377 **format_args), 377 **format_args),
378 (kwargs.pop('message_body', None) or DEFAULT_BODY).format(**format_args), 378 (kwargs.pop('message_body', None) or DEFAULT_BODY).format(**format_args),
382 382
383 # we automatically add guest to host roster (if host is specified) 383 # we automatically add guest to host roster (if host is specified)
384 # FIXME: a parameter to disable auto roster adding would be nice 384 # FIXME: a parameter to disable auto roster adding would be nice
385 if profile is not None: 385 if profile is not None:
386 try: 386 try:
387 client = self.host.getClient(profile) 387 client = self.host.get_client(profile)
388 except Exception as e: 388 except Exception as e:
389 log.error(f"Can't get host profile: {profile}: {e}") 389 log.error(f"Can't get host profile: {profile}: {e}")
390 else: 390 else:
391 await self.host.updateContact(client, jid_, name, ['guests']) 391 await self.host.contact_update(client, jid_, name, ['guests'])
392 392
393 if kwargs: 393 if kwargs:
394 log.warning(_("Not all arguments have been consumed: {}").format(kwargs)) 394 log.warning(_("Not all arguments have been consumed: {}").format(kwargs))
395 395
396 ## extra data saving 396 ## extra data saving
398 398
399 extra[KEY_ID] = id_ 399 extra[KEY_ID] = id_
400 400
401 return extra 401 return extra
402 402
403 def _simpleCreate(self, invitee_email, invitee_name, url_template, extra_s, profile): 403 def _simple_create(self, invitee_email, invitee_name, url_template, extra_s, profile):
404 client = self.host.getClient(profile) 404 client = self.host.get_client(profile)
405 # FIXME: needed because python-dbus use a specific string class 405 # FIXME: needed because python-dbus use a specific string class
406 invitee_email = str(invitee_email) 406 invitee_email = str(invitee_email)
407 invitee_name = str(invitee_name) 407 invitee_name = str(invitee_name)
408 url_template = str(url_template) 408 url_template = str(url_template)
409 extra = data_format.deserialise(extra_s) 409 extra = data_format.deserialise(extra_s)
410 d = defer.ensureDeferred( 410 d = defer.ensureDeferred(
411 self.simpleCreate(client, invitee_email, invitee_name, url_template, extra) 411 self.simple_create(client, invitee_email, invitee_name, url_template, extra)
412 ) 412 )
413 d.addCallback(lambda data: {k: str(v) for k,v in data.items()}) 413 d.addCallback(lambda data: {k: str(v) for k,v in data.items()})
414 return d 414 return d
415 415
416 async def simpleCreate( 416 async def simple_create(
417 self, client, invitee_email, invitee_name, url_template, extra): 417 self, client, invitee_email, invitee_name, url_template, extra):
418 """Simplified method to invite somebody by email""" 418 """Simplified method to invite somebody by email"""
419 return await self.create( 419 return await self.create(
420 name=invitee_name, 420 name=invitee_name,
421 email=invitee_email, 421 email=invitee_email,
441 data = await self.get(id_) 441 data = await self.get(id_)
442 guest_profile = data['guest_profile'] 442 guest_profile = data['guest_profile']
443 password = data['password'] 443 password = data['password']
444 try: 444 try:
445 await self.host.connect(guest_profile, password) 445 await self.host.connect(guest_profile, password)
446 guest_client = self.host.getClient(guest_profile) 446 guest_client = self.host.get_client(guest_profile)
447 # XXX: be extra careful to use guest_client and not client below, as this will 447 # XXX: be extra careful to use guest_client and not client below, as this will
448 # delete the associated XMPP account 448 # delete the associated XMPP account
449 log.debug("deleting XMPP account") 449 log.debug("deleting XMPP account")
450 await self.host.plugins['XEP-0077'].unregister(guest_client, None) 450 await self.host.plugins['XEP-0077'].unregister(guest_client, None)
451 except (error.StanzaError, sasl.SASLAuthError) as e: 451 except (error.StanzaError, sasl.SASLAuthError) as e:
452 log.warning( 452 log.warning(
453 f"Can't delete {guest_profile}'s XMPP account, maybe it as already been " 453 f"Can't delete {guest_profile}'s XMPP account, maybe it as already been "
454 f"deleted: {e}") 454 f"deleted: {e}")
455 try: 455 try:
456 await self.host.memory.asyncDeleteProfile(guest_profile, True) 456 await self.host.memory.profile_delete_async(guest_profile, True)
457 except Exception as e: 457 except Exception as e:
458 log.warning(f"Can't delete guest profile {guest_profile}: {e}") 458 log.warning(f"Can't delete guest profile {guest_profile}: {e}")
459 log.debug("removing guest data") 459 log.debug("removing guest data")
460 await self.invitations.adel(id_) 460 await self.invitations.adel(id_)
461 log.info(f"{id_} invitation has been deleted") 461 log.info(f"{id_} invitation has been deleted")
472 empty values will be deleted if replace is True 472 empty values will be deleted if replace is True
473 @param replace(bool): if True replace the data 473 @param replace(bool): if True replace the data
474 else update them 474 else update them
475 @raise KeyError: there is not invitation with this id_ 475 @raise KeyError: there is not invitation with this id_
476 """ 476 """
477 self.checkExtra(new_extra) 477 self.check_extra(new_extra)
478 def gotCurrentData(current_data): 478 def got_current_data(current_data):
479 if replace: 479 if replace:
480 new_data = new_extra 480 new_data = new_extra
481 for k in EXTRA_RESERVED: 481 for k in EXTRA_RESERVED:
482 try: 482 try:
483 new_data[k] = current_data[k] 483 new_data[k] = current_data[k]
498 pass 498 pass
499 499
500 self.invitations[id_] = new_data 500 self.invitations[id_] = new_data
501 501
502 d = self.invitations[id_] 502 d = self.invitations[id_]
503 d.addCallback(gotCurrentData) 503 d.addCallback(got_current_data)
504 return d 504 return d
505 505
506 def _list(self, profile=C.PROF_KEY_NONE): 506 def _list(self, profile=C.PROF_KEY_NONE):
507 return defer.ensureDeferred(self.list(profile)) 507 return defer.ensureDeferred(self.list(profile))
508 508