comparison sat/plugins/plugin_misc_email_invitation.py @ 3028:ab2696e34d29

Python 3 port: /!\ this is a huge commit /!\ starting from this commit, SàT is needs Python 3.6+ /!\ SàT maybe be instable or some feature may not work anymore, this will improve with time This patch port backend, bridge and frontends to Python 3. Roughly this has been done this way: - 2to3 tools has been applied (with python 3.7) - all references to python2 have been replaced with python3 (notably shebangs) - fixed files not handled by 2to3 (notably the shell script) - several manual fixes - fixed issues reported by Python 3 that where not handled in Python 2 - replaced "async" with "async_" when needed (it's a reserved word from Python 3.7) - replaced zope's "implements" with @implementer decorator - temporary hack to handle data pickled in database, as str or bytes may be returned, to be checked later - fixed hash comparison for password - removed some code which is not needed anymore with Python 3 - deactivated some code which needs to be checked (notably certificate validation) - tested with jp, fixed reported issues until some basic commands worked - ported Primitivus (after porting dependencies like urwid satext) - more manual fixes
author Goffi <goffi@goffi.org>
date Tue, 13 Aug 2019 19:08:41 +0200
parents 420897488080
children fee60f17ebac
comparison
equal deleted inserted replaced
3027:ff5bcb12ae60 3028:ab2696e34d29
1 #!/usr/bin/env python2 1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*- 2 # -*- coding: utf-8 -*-
3 3
4 # SAT plugin for file tansfer 4 # SAT plugin for file tansfer
5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org) 5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org)
6 6
39 C.PI_TYPE: C.PLUG_TYPE_MISC, 39 C.PI_TYPE: C.PLUG_TYPE_MISC,
40 C.PI_DEPENDENCIES: ['XEP-0077'], 40 C.PI_DEPENDENCIES: ['XEP-0077'],
41 C.PI_RECOMMENDATIONS: ["IDENTITY"], 41 C.PI_RECOMMENDATIONS: ["IDENTITY"],
42 C.PI_MAIN: "InvitationsPlugin", 42 C.PI_MAIN: "InvitationsPlugin",
43 C.PI_HANDLER: "no", 43 C.PI_HANDLER: "no",
44 C.PI_DESCRIPTION: _(u"""invitation of people without XMPP account""") 44 C.PI_DESCRIPTION: _("""invitation of people without XMPP account""")
45 } 45 }
46 46
47 47
48 SUFFIX_MAX = 5 48 SUFFIX_MAX = 5
49 INVITEE_PROFILE_TPL = u"guest@@{uuid}" 49 INVITEE_PROFILE_TPL = "guest@@{uuid}"
50 KEY_ID = u'id' 50 KEY_ID = 'id'
51 KEY_JID = u'jid' 51 KEY_JID = 'jid'
52 KEY_CREATED = u'created' 52 KEY_CREATED = 'created'
53 KEY_LAST_CONNECTION = u'last_connection' 53 KEY_LAST_CONNECTION = 'last_connection'
54 KEY_GUEST_PROFILE = u'guest_profile' 54 KEY_GUEST_PROFILE = 'guest_profile'
55 KEY_PASSWORD = u'password' 55 KEY_PASSWORD = 'password'
56 KEY_EMAILS_EXTRA = u'emails_extra' 56 KEY_EMAILS_EXTRA = 'emails_extra'
57 EXTRA_RESERVED = {KEY_ID, KEY_JID, KEY_CREATED, u'jid_', u'jid', KEY_LAST_CONNECTION, 57 EXTRA_RESERVED = {KEY_ID, KEY_JID, KEY_CREATED, 'jid_', 'jid', KEY_LAST_CONNECTION,
58 KEY_GUEST_PROFILE, KEY_PASSWORD, KEY_EMAILS_EXTRA} 58 KEY_GUEST_PROFILE, KEY_PASSWORD, KEY_EMAILS_EXTRA}
59 DEFAULT_SUBJECT = D_(u"You have been invited by {host_name} to {app_name}") 59 DEFAULT_SUBJECT = D_("You have been invited by {host_name} to {app_name}")
60 DEFAULT_BODY = D_(u"""Hello {name}! 60 DEFAULT_BODY = D_("""Hello {name}!
61 61
62 You have received an invitation from {host_name} to participate to "{app_name}". 62 You have received an invitation from {host_name} to participate to "{app_name}".
63 To join, you just have to click on the following URL: 63 To join, you just have to click on the following URL:
64 {url} 64 {url}
65 65
71 71
72 72
73 class InvitationsPlugin(object): 73 class InvitationsPlugin(object):
74 74
75 def __init__(self, host): 75 def __init__(self, host):
76 log.info(_(u"plugin Invitations initialization")) 76 log.info(_("plugin Invitations initialization"))
77 self.host = host 77 self.host = host
78 self.invitations = persistent.LazyPersistentBinaryDict(u'invitations') 78 self.invitations = persistent.LazyPersistentBinaryDict('invitations')
79 host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sasssssssssa{ss}s', 79 host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sasssssssssa{ss}s',
80 out_sign='a{ss}', 80 out_sign='a{ss}',
81 method=self._create, 81 method=self._create,
82 async=True) 82 async_=True)
83 host.bridge.addMethod("invitationGet", ".plugin", in_sign='s', out_sign='a{ss}', 83 host.bridge.addMethod("invitationGet", ".plugin", in_sign='s', out_sign='a{ss}',
84 method=self.get, 84 method=self.get,
85 async=True) 85 async_=True)
86 host.bridge.addMethod("invitationModify", ".plugin", in_sign='sa{ss}b', 86 host.bridge.addMethod("invitationModify", ".plugin", in_sign='sa{ss}b',
87 out_sign='', 87 out_sign='',
88 method=self._modify, 88 method=self._modify,
89 async=True) 89 async_=True)
90 host.bridge.addMethod("invitationList", ".plugin", in_sign='s', 90 host.bridge.addMethod("invitationList", ".plugin", in_sign='s',
91 out_sign='a{sa{ss}}', 91 out_sign='a{sa{ss}}',
92 method=self._list, 92 method=self._list,
93 async=True) 93 async_=True)
94 94
95 def checkExtra(self, extra): 95 def checkExtra(self, extra):
96 if EXTRA_RESERVED.intersection(extra): 96 if EXTRA_RESERVED.intersection(extra):
97 raise ValueError( 97 raise ValueError(
98 _(u"You can't use following key(s) in extra, they are reserved: {}") 98 _("You can't use following key(s) in extra, they are reserved: {}")
99 .format(u', '.join(EXTRA_RESERVED.intersection(extra)))) 99 .format(', '.join(EXTRA_RESERVED.intersection(extra))))
100 100
101 def _create(self, email=u'', emails_extra=None, jid_=u'', password=u'', name=u'', 101 def _create(self, email='', emails_extra=None, jid_='', password='', name='',
102 host_name=u'', language=u'', url_template=u'', message_subject=u'', 102 host_name='', language='', url_template='', message_subject='',
103 message_body=u'', extra=None, profile=u''): 103 message_body='', extra=None, profile=''):
104 # XXX: we don't use **kwargs here to keep arguments name for introspection with 104 # XXX: we don't use **kwargs here to keep arguments name for introspection with
105 # D-Bus bridge 105 # D-Bus bridge
106 if emails_extra is None: 106 if emails_extra is None:
107 emails_extra = [] 107 emails_extra = []
108 108
109 if extra is None: 109 if extra is None:
110 extra = {} 110 extra = {}
111 else: 111 else:
112 extra = {unicode(k): unicode(v) for k,v in extra.iteritems()} 112 extra = {str(k): str(v) for k,v in extra.items()}
113 113
114 kwargs = {"extra": extra, 114 kwargs = {"extra": extra,
115 KEY_EMAILS_EXTRA: [unicode(e) for e in emails_extra] 115 KEY_EMAILS_EXTRA: [str(e) for e in emails_extra]
116 } 116 }
117 117
118 # we need to be sure that values are unicode, else they won't be pickled correctly 118 # we need to be sure that values are unicode, else they won't be pickled correctly
119 # with D-Bus 119 # with D-Bus
120 for key in ("jid_", "password", "name", "host_name", "email", "language", 120 for key in ("jid_", "password", "name", "host_name", "email", "language",
121 "url_template", "message_subject", "message_body", "profile"): 121 "url_template", "message_subject", "message_body", "profile"):
122 value = locals()[key] 122 value = locals()[key]
123 if value: 123 if value:
124 kwargs[key] = unicode(value) 124 kwargs[key] = str(value)
125 d = self.create(**kwargs) 125 d = self.create(**kwargs)
126 def serialize(data): 126 def serialize(data):
127 data[KEY_JID] = data[KEY_JID].full() 127 data[KEY_JID] = data[KEY_JID].full()
128 return data 128 return data
129 d.addCallback(serialize) 129 d.addCallback(serialize)
130 return d 130 return d
131 131
132 @defer.inlineCallbacks 132 @defer.inlineCallbacks
133 def create(self, **kwargs): 133 def create(self, **kwargs):
134 ur"""Create an invitation 134 r"""Create an invitation
135 135
136 This will create an XMPP account and a profile, and use a UUID to retrieve them. 136 This will create an XMPP account and a profile, and use a UUID to retrieve them.
137 The profile is automatically generated in the form guest@@[UUID], this way they 137 The profile is automatically generated in the form guest@@[UUID], this way they
138 can be retrieved easily 138 can be retrieved easily
139 **kwargs: keywords arguments which can have the following keys, unset values are 139 **kwargs: keywords arguments which can have the following keys, unset values are
192 """ 192 """
193 ## initial checks 193 ## initial checks
194 extra = kwargs.pop('extra', {}) 194 extra = kwargs.pop('extra', {})
195 if set(kwargs).intersection(extra): 195 if set(kwargs).intersection(extra):
196 raise ValueError( 196 raise ValueError(
197 _(u"You can't use following key(s) in both args and extra: {}").format( 197 _("You can't use following key(s) in both args and extra: {}").format(
198 u', '.join(set(kwargs).intersection(extra)))) 198 ', '.join(set(kwargs).intersection(extra))))
199 199
200 self.checkExtra(extra) 200 self.checkExtra(extra)
201 201
202 email = kwargs.pop(u'email', None) 202 email = kwargs.pop('email', None)
203 emails_extra = kwargs.pop(u'emails_extra', []) 203 emails_extra = kwargs.pop('emails_extra', [])
204 if not email and emails_extra: 204 if not email and emails_extra:
205 raise ValueError( 205 raise ValueError(
206 _(u'You need to provide a main email address before using emails_extra')) 206 _('You need to provide a main email address before using emails_extra'))
207 207
208 if (email is not None 208 if (email is not None
209 and not 'url_template' in kwargs 209 and not 'url_template' in kwargs
210 and not 'message_body' in kwargs): 210 and not 'message_body' in kwargs):
211 raise ValueError( 211 raise ValueError(
212 _(u"You need to provide url_template if you use default message body")) 212 _("You need to provide url_template if you use default message body"))
213 213
214 ## uuid 214 ## uuid
215 log.info(_(u"creating an invitation")) 215 log.info(_("creating an invitation"))
216 id_ = unicode(shortuuid.uuid()) 216 id_ = str(shortuuid.uuid())
217 217
218 ## XMPP account creation 218 ## XMPP account creation
219 password = kwargs.pop(u'password', None) 219 password = kwargs.pop('password', None)
220 if password is None: 220 if password is None:
221 password = utils.generatePassword() 221 password = utils.generatePassword()
222 assert password 222 assert password
223 # XXX: password is here saved in clear in database 223 # XXX: password is here saved in clear in database
224 # it is needed for invitation as the same password is used for profile 224 # it is needed for invitation as the same password is used for profile
226 # FIXME: we could add an extra encryption key which would be used with the uuid 226 # FIXME: we could add an extra encryption key which would be used with the uuid
227 # when the invitee is connecting (e.g. with URL). This key would not be 227 # when the invitee is connecting (e.g. with URL). This key would not be
228 # saved and could be used to encrypt profile password. 228 # saved and could be used to encrypt profile password.
229 extra[KEY_PASSWORD] = password 229 extra[KEY_PASSWORD] = password
230 230
231 jid_ = kwargs.pop(u'jid_', None) 231 jid_ = kwargs.pop('jid_', None)
232 if not jid_: 232 if not jid_:
233 domain = self.host.memory.getConfig(None, 'xmpp_domain') 233 domain = self.host.memory.getConfig(None, 'xmpp_domain')
234 if not domain: 234 if not domain:
235 # TODO: fallback to profile's domain 235 # TODO: fallback to profile's domain
236 raise ValueError(_(u"You need to specify xmpp_domain in sat.conf")) 236 raise ValueError(_("You need to specify xmpp_domain in sat.conf"))
237 jid_ = u"invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(), 237 jid_ = "invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(),
238 domain=domain) 238 domain=domain)
239 jid_ = jid.JID(jid_) 239 jid_ = jid.JID(jid_)
240 if jid_.user: 240 if jid_.user:
241 # we don't register account if there is no user as anonymous login is then 241 # we don't register account if there is no user as anonymous login is then
242 # used 242 # used
243 try: 243 try:
244 yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password) 244 yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
245 except error.StanzaError as e: 245 except error.StanzaError as e:
246 prefix = jid_.user 246 prefix = jid_.user
247 idx = 0 247 idx = 0
248 while e.condition == u'conflict': 248 while e.condition == 'conflict':
249 if idx >= SUFFIX_MAX: 249 if idx >= SUFFIX_MAX:
250 raise exceptions.ConflictError(_(u"Can't create XMPP account")) 250 raise exceptions.ConflictError(_("Can't create XMPP account"))
251 jid_.user = prefix + '_' + unicode(idx) 251 jid_.user = prefix + '_' + str(idx)
252 log.info(_(u"requested jid already exists, trying with {}".format( 252 log.info(_("requested jid already exists, trying with {}".format(
253 jid_.full()))) 253 jid_.full())))
254 try: 254 try:
255 yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, 255 yield self.host.plugins['XEP-0077'].registerNewAccount(jid_,
256 password) 256 password)
257 except error.StanzaError as e: 257 except error.StanzaError as e:
258 idx += 1 258 idx += 1
259 else: 259 else:
260 break 260 break
261 if e.condition != u'conflict': 261 if e.condition != 'conflict':
262 raise e 262 raise e
263 263
264 log.info(_(u"account {jid_} created").format(jid_=jid_.full())) 264 log.info(_("account {jid_} created").format(jid_=jid_.full()))
265 265
266 ## profile creation 266 ## profile creation
267 267
268 extra[KEY_GUEST_PROFILE] = guest_profile = INVITEE_PROFILE_TPL.format(uuid=id_) 268 extra[KEY_GUEST_PROFILE] = guest_profile = INVITEE_PROFILE_TPL.format(uuid=id_)
269 # profile creation should not fail as we generate unique name ourselves 269 # profile creation should not fail as we generate unique name ourselves
271 yield self.host.memory.startSession(password, guest_profile) 271 yield self.host.memory.startSession(password, guest_profile)
272 yield self.host.memory.setParam("JabberID", jid_.full(), "Connection", 272 yield self.host.memory.setParam("JabberID", jid_.full(), "Connection",
273 profile_key=guest_profile) 273 profile_key=guest_profile)
274 yield self.host.memory.setParam("Password", password, "Connection", 274 yield self.host.memory.setParam("Password", password, "Connection",
275 profile_key=guest_profile) 275 profile_key=guest_profile)
276 name = kwargs.pop(u'name', None) 276 name = kwargs.pop('name', None)
277 if name is not None: 277 if name is not None:
278 extra[u'name'] = name 278 extra['name'] = name
279 try: 279 try:
280 id_plugin = self.host.plugins[u'IDENTITY'] 280 id_plugin = self.host.plugins['IDENTITY']
281 except KeyError: 281 except KeyError:
282 pass 282 pass
283 else: 283 else:
284 yield self.host.connect(guest_profile, password) 284 yield self.host.connect(guest_profile, password)
285 guest_client = self.host.getClient(guest_profile) 285 guest_client = self.host.getClient(guest_profile)
286 yield id_plugin.setIdentity(guest_client, {u'nick': name}) 286 yield id_plugin.setIdentity(guest_client, {'nick': name})
287 yield self.host.disconnect(guest_profile) 287 yield self.host.disconnect(guest_profile)
288 288
289 ## email 289 ## email
290 language = kwargs.pop(u'language', None) 290 language = kwargs.pop('language', None)
291 if language is not None: 291 if language is not None:
292 extra[u'language'] = language.strip() 292 extra['language'] = language.strip()
293 293
294 if email is not None: 294 if email is not None:
295 extra[u'email'] = email 295 extra['email'] = email
296 data_format.iter2dict(KEY_EMAILS_EXTRA, extra) 296 data_format.iter2dict(KEY_EMAILS_EXTRA, extra)
297 url_template = kwargs.pop(u'url_template', '') 297 url_template = kwargs.pop('url_template', '')
298 format_args = { 298 format_args = {
299 u'uuid': id_, 299 'uuid': id_,
300 u'app_name': C.APP_NAME, 300 'app_name': C.APP_NAME,
301 u'app_url': C.APP_URL} 301 'app_url': C.APP_URL}
302 302
303 if name is None: 303 if name is None:
304 format_args[u'name'] = email 304 format_args['name'] = email
305 else: 305 else:
306 format_args[u'name'] = name 306 format_args['name'] = name
307 307
308 profile = kwargs.pop(u'profile', None) 308 profile = kwargs.pop('profile', None)
309 if profile is None: 309 if profile is None:
310 format_args[u'profile'] = u'' 310 format_args['profile'] = ''
311 else: 311 else:
312 format_args[u'profile'] = extra[u'profile'] = profile 312 format_args['profile'] = extra['profile'] = profile
313 313
314 host_name = kwargs.pop(u'host_name', None) 314 host_name = kwargs.pop('host_name', None)
315 if host_name is None: 315 if host_name is None:
316 format_args[u'host_name'] = profile or _(u"somebody") 316 format_args['host_name'] = profile or _("somebody")
317 else: 317 else:
318 format_args[u'host_name'] = extra[u'host_name'] = host_name 318 format_args['host_name'] = extra['host_name'] = host_name
319 319
320 invite_url = url_template.format(**format_args) 320 invite_url = url_template.format(**format_args)
321 format_args[u'url'] = invite_url 321 format_args['url'] = invite_url
322 322
323 yield sat_email.sendEmail( 323 yield sat_email.sendEmail(
324 self.host, 324 self.host,
325 [email] + emails_extra, 325 [email] + emails_extra,
326 (kwargs.pop(u'message_subject', None) or DEFAULT_SUBJECT).format( 326 (kwargs.pop('message_subject', None) or DEFAULT_SUBJECT).format(
327 **format_args), 327 **format_args),
328 (kwargs.pop(u'message_body', None) or DEFAULT_BODY).format(**format_args), 328 (kwargs.pop('message_body', None) or DEFAULT_BODY).format(**format_args),
329 ) 329 )
330 330
331 ## extra data saving 331 ## extra data saving
332 self.invitations[id_] = extra 332 self.invitations[id_] = extra
333 333
334 if kwargs: 334 if kwargs:
335 log.warning(_(u"Not all arguments have been consumed: {}").format(kwargs)) 335 log.warning(_("Not all arguments have been consumed: {}").format(kwargs))
336 336
337 extra[KEY_ID] = id_ 337 extra[KEY_ID] = id_
338 extra[KEY_JID] = jid_ 338 extra[KEY_JID] = jid_
339 defer.returnValue(extra) 339 defer.returnValue(extra)
340 340
346 @raise KeyError: there is not invitation with this id_ 346 @raise KeyError: there is not invitation with this id_
347 """ 347 """
348 return self.invitations[id_] 348 return self.invitations[id_]
349 349
350 def _modify(self, id_, new_extra, replace): 350 def _modify(self, id_, new_extra, replace):
351 return self.modify(id_, {unicode(k): unicode(v) for k,v in new_extra.iteritems()}, 351 return self.modify(id_, {str(k): str(v) for k,v in new_extra.items()},
352 replace) 352 replace)
353 353
354 def modify(self, id_, new_extra, replace=False): 354 def modify(self, id_, new_extra, replace=False):
355 """Modify invitation data 355 """Modify invitation data
356 356
370 new_data[k] = current_data[k] 370 new_data[k] = current_data[k]
371 except KeyError: 371 except KeyError:
372 continue 372 continue
373 else: 373 else:
374 new_data = current_data 374 new_data = current_data
375 for k,v in new_extra.iteritems(): 375 for k,v in new_extra.items():
376 if k in EXTRA_RESERVED: 376 if k in EXTRA_RESERVED:
377 log.warning(_(u"Skipping reserved key {key}".format(k))) 377 log.warning(_("Skipping reserved key {key}".format(k)))
378 continue 378 continue
379 if v: 379 if v:
380 new_data[k] = v 380 new_data[k] = v
381 else: 381 else:
382 try: 382 try:
399 399
400 @param profile(unicode): return invitation linked to this profile only 400 @param profile(unicode): return invitation linked to this profile only
401 C.PROF_KEY_NONE: don't filter invitations 401 C.PROF_KEY_NONE: don't filter invitations
402 @return list(unicode): invitations uids 402 @return list(unicode): invitations uids
403 """ 403 """
404 invitations = yield self.invitations.items() 404 invitations = yield list(self.invitations.items())
405 if profile != C.PROF_KEY_NONE: 405 if profile != C.PROF_KEY_NONE:
406 invitations = {id_:data for id_, data in invitations.iteritems() 406 invitations = {id_:data for id_, data in invitations.items()
407 if data.get(u'profile') == profile} 407 if data.get('profile') == profile}
408 408
409 defer.returnValue(invitations) 409 defer.returnValue(invitations)