Mercurial > libervia-backend
comparison sat/plugins/plugin_misc_invitations.py @ 2909:90146552cde5
core (memory), plugin XEP-0329, plugin invitation: minor style improvments
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 14 Apr 2019 08:21:51 +0200 |
parents | 003b8b4b56a7 |
children |
comparison
equal
deleted
inserted
replaced
2908:695fc440c3b8 | 2909:90146552cde5 |
---|---|
15 # GNU Affero General Public License for more details. | 15 # GNU Affero General Public License for more details. |
16 | 16 |
17 # You should have received a copy of the GNU Affero General Public License | 17 # You should have received a copy of the GNU Affero General Public License |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 import shortuuid | |
21 from twisted.internet import defer | |
22 from twisted.words.protocols.jabber import jid | |
23 from twisted.words.protocols.jabber import error | |
20 from sat.core.i18n import _, D_ | 24 from sat.core.i18n import _, D_ |
21 from sat.core.constants import Const as C | 25 from sat.core.constants import Const as C |
22 from sat.core import exceptions | 26 from sat.core import exceptions |
23 from sat.core.log import getLogger | 27 from sat.core.log import getLogger |
24 log = getLogger(__name__) | |
25 import shortuuid | |
26 from sat.tools import utils | 28 from sat.tools import utils |
27 from sat.tools.common import data_format | 29 from sat.tools.common import data_format |
28 from twisted.internet import defer | |
29 from twisted.words.protocols.jabber import jid | |
30 from twisted.words.protocols.jabber import error | |
31 from sat.memory import persistent | 30 from sat.memory import persistent |
32 from sat.tools import email as sat_email | 31 from sat.tools import email as sat_email |
32 | |
33 log = getLogger(__name__) | |
33 | 34 |
34 | 35 |
35 PLUGIN_INFO = { | 36 PLUGIN_INFO = { |
36 C.PI_NAME: "Invitations", | 37 C.PI_NAME: "Invitations", |
37 C.PI_IMPORT_NAME: "INVITATIONS", | 38 C.PI_IMPORT_NAME: "INVITATIONS", |
51 KEY_CREATED = u'created' | 52 KEY_CREATED = u'created' |
52 KEY_LAST_CONNECTION = u'last_connection' | 53 KEY_LAST_CONNECTION = u'last_connection' |
53 KEY_GUEST_PROFILE = u'guest_profile' | 54 KEY_GUEST_PROFILE = u'guest_profile' |
54 KEY_PASSWORD = u'password' | 55 KEY_PASSWORD = u'password' |
55 KEY_EMAILS_EXTRA = u'emails_extra' | 56 KEY_EMAILS_EXTRA = u'emails_extra' |
56 EXTRA_RESERVED = {KEY_ID, KEY_JID, KEY_CREATED, u'jid_', u'jid', KEY_LAST_CONNECTION, KEY_GUEST_PROFILE, KEY_PASSWORD, KEY_EMAILS_EXTRA} | 57 EXTRA_RESERVED = {KEY_ID, KEY_JID, KEY_CREATED, u'jid_', u'jid', KEY_LAST_CONNECTION, |
58 KEY_GUEST_PROFILE, KEY_PASSWORD, KEY_EMAILS_EXTRA} | |
57 DEFAULT_SUBJECT = D_(u"You have been invited by {host_name} to {app_name}") | 59 DEFAULT_SUBJECT = D_(u"You have been invited by {host_name} to {app_name}") |
58 DEFAULT_BODY = D_(u"""Hello {name}! | 60 DEFAULT_BODY = D_(u"""Hello {name}! |
59 | 61 |
60 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}". |
61 To join, you just have to click on the following URL: | 63 To join, you just have to click on the following URL: |
72 | 74 |
73 def __init__(self, host): | 75 def __init__(self, host): |
74 log.info(_(u"plugin Invitations initialization")) | 76 log.info(_(u"plugin Invitations initialization")) |
75 self.host = host | 77 self.host = host |
76 self.invitations = persistent.LazyPersistentBinaryDict(u'invitations') | 78 self.invitations = persistent.LazyPersistentBinaryDict(u'invitations') |
77 host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sasssssssssa{ss}s', out_sign='a{ss}', | 79 host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sasssssssssa{ss}s', |
80 out_sign='a{ss}', | |
78 method=self._create, | 81 method=self._create, |
79 async=True) | 82 async=True) |
80 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}', |
81 method=self.get, | 84 method=self.get, |
82 async=True) | 85 async=True) |
83 host.bridge.addMethod("invitationModify", ".plugin", in_sign='sa{ss}b', out_sign='', | 86 host.bridge.addMethod("invitationModify", ".plugin", in_sign='sa{ss}b', |
87 out_sign='', | |
84 method=self._modify, | 88 method=self._modify, |
85 async=True) | 89 async=True) |
86 host.bridge.addMethod("invitationList", ".plugin", in_sign='s', out_sign='a{sa{ss}}', | 90 host.bridge.addMethod("invitationList", ".plugin", in_sign='s', |
91 out_sign='a{sa{ss}}', | |
87 method=self._list, | 92 method=self._list, |
88 async=True) | 93 async=True) |
89 | 94 |
90 def checkExtra(self, extra): | 95 def checkExtra(self, extra): |
91 if EXTRA_RESERVED.intersection(extra): | 96 if EXTRA_RESERVED.intersection(extra): |
92 raise ValueError(_(u"You can't use following key(s) in extra, they are reserved: {}").format( | 97 raise ValueError( |
93 u', '.join(EXTRA_RESERVED.intersection(extra)))) | 98 _(u"You can't use following key(s) in extra, they are reserved: {}") |
94 | 99 .format(u', '.join(EXTRA_RESERVED.intersection(extra)))) |
95 def _create(self, email=u'', emails_extra=None, jid_=u'', password=u'', name=u'', host_name=u'', language=u'', url_template=u'', message_subject=u'', message_body=u'', extra=None, profile=u''): | 100 |
96 # XXX: we don't use **kwargs here to keep arguments name for introspection with D-Bus bridge | 101 def _create(self, email=u'', emails_extra=None, jid_=u'', password=u'', name=u'', |
102 host_name=u'', language=u'', url_template=u'', message_subject=u'', | |
103 message_body=u'', extra=None, profile=u''): | |
104 # XXX: we don't use **kwargs here to keep arguments name for introspection with | |
105 # D-Bus bridge | |
97 if emails_extra is None: | 106 if emails_extra is None: |
98 emails_extra = [] | 107 emails_extra = [] |
99 | 108 |
100 if extra is None: | 109 if extra is None: |
101 extra = {} | 110 extra = {} |
104 | 113 |
105 kwargs = {"extra": extra, | 114 kwargs = {"extra": extra, |
106 KEY_EMAILS_EXTRA: [unicode(e) for e in emails_extra] | 115 KEY_EMAILS_EXTRA: [unicode(e) for e in emails_extra] |
107 } | 116 } |
108 | 117 |
109 # we need to be sure that values are unicode, else they won't be pickled correctly with D-Bus | 118 # we need to be sure that values are unicode, else they won't be pickled correctly |
110 for key in ("jid_", "password", "name", "host_name", "email", "language", "url_template", "message_subject", "message_body", "profile"): | 119 # with D-Bus |
120 for key in ("jid_", "password", "name", "host_name", "email", "language", | |
121 "url_template", "message_subject", "message_body", "profile"): | |
111 value = locals()[key] | 122 value = locals()[key] |
112 if value: | 123 if value: |
113 kwargs[key] = unicode(value) | 124 kwargs[key] = unicode(value) |
114 d = self.create(**kwargs) | 125 d = self.create(**kwargs) |
115 def serialize(data): | 126 def serialize(data): |
118 d.addCallback(serialize) | 129 d.addCallback(serialize) |
119 return d | 130 return d |
120 | 131 |
121 @defer.inlineCallbacks | 132 @defer.inlineCallbacks |
122 def create(self, **kwargs): | 133 def create(self, **kwargs): |
123 ur"""create an invitation | 134 ur"""Create an invitation |
124 | 135 |
125 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. |
126 the profile is automatically generated in the form guest@@[UUID], this way they can be retrieved easily | 137 The profile is automatically generated in the form guest@@[UUID], this way they |
127 **kwargs: keywords arguments which can have the following keys, unset values are equivalent to None: | 138 can be retrieved easily |
128 jid_(jid.JID, None): jid to use for invitation, the jid will be created using XEP-0077 | 139 **kwargs: keywords arguments which can have the following keys, unset values are |
129 if the jid has no user part, an anonymous account will be used (no XMPP account created in this case) | 140 equivalent to None: |
130 if None, automatically generate an account name (in the form "invitation-[random UUID]@domain.tld") (note that this UUID is not the | 141 jid_(jid.JID, None): jid to use for invitation, the jid will be created using |
131 same as the invitation one, as jid can be used publicly (leaking the UUID), and invitation UUID give access to account. | 142 XEP-0077 |
132 in case of conflict, a suffix number is added to the account until a free one if found (with a failure if SUFFIX_MAX is reached) | 143 if the jid has no user part, an anonymous account will be used (no XMPP |
133 password(unicode, None): password to use (will be used for XMPP account and profile) | 144 account created in this case) |
145 if None, automatically generate an account name (in the form | |
146 "invitation-[random UUID]@domain.tld") (note that this UUID is not the | |
147 same as the invitation one, as jid can be used publicly (leaking the | |
148 UUID), and invitation UUID give access to account. | |
149 in case of conflict, a suffix number is added to the account until a free | |
150 one if found (with a failure if SUFFIX_MAX is reached) | |
151 password(unicode, None): password to use (will be used for XMPP account and | |
152 profile) | |
134 None to automatically generate one | 153 None to automatically generate one |
135 name(unicode, None): name of the invitee | 154 name(unicode, None): name of the invitee |
136 will be set as profile identity if present | 155 will be set as profile identity if present |
137 host_name(unicode, None): name of the host | 156 host_name(unicode, None): name of the host |
138 email(unicode, None): email to send the invitation to | 157 email(unicode, None): email to send the invitation to |
139 if None, no invitation email is sent, you can still associate email using extra | 158 if None, no invitation email is sent, you can still associate email using |
159 extra | |
140 if email is used, extra can't have "email" key | 160 if email is used, extra can't have "email" key |
141 language(unicode): language of the invitee (used notabily to translate the invitation) | 161 language(unicode): language of the invitee (used notabily to translate the |
162 invitation) | |
142 TODO: not used yet | 163 TODO: not used yet |
143 url_template(unicode, None): template to use to construct the invitation URL | 164 url_template(unicode, None): template to use to construct the invitation URL |
144 use {uuid} as a placeholder for identifier | 165 use {uuid} as a placeholder for identifier |
145 use None if you don't want to include URL (or if it is already specified in custom message) | 166 use None if you don't want to include URL (or if it is already specified |
167 in custom message) | |
146 /!\ you must put full URL, don't forget https:// | 168 /!\ you must put full URL, don't forget https:// |
147 /!\ the URL will give access to the invitee account, you should warn in message to not publish it publicly | 169 /!\ the URL will give access to the invitee account, you should warn in |
148 message_subject(unicode, None): customised message body for the invitation email | 170 message to not publish it publicly |
171 message_subject(unicode, None): customised message body for the invitation | |
172 email | |
149 None to use default subject | 173 None to use default subject |
150 uses the same substitution as for message_body | 174 uses the same substitution as for message_body |
151 message_body(unicode, None): customised message body for the invitation email | 175 message_body(unicode, None): customised message body for the invitation email |
152 None to use default body | 176 None to use default body |
153 use {name} as a place holder for invitee name | 177 use {name} as a place holder for invitee name |
167 - filled extra dictionary, as saved in the databae | 191 - filled extra dictionary, as saved in the databae |
168 """ | 192 """ |
169 ## initial checks | 193 ## initial checks |
170 extra = kwargs.pop('extra', {}) | 194 extra = kwargs.pop('extra', {}) |
171 if set(kwargs).intersection(extra): | 195 if set(kwargs).intersection(extra): |
172 raise ValueError(_(u"You can't use following key(s) in both args and extra: {}").format( | 196 raise ValueError( |
197 _(u"You can't use following key(s) in both args and extra: {}").format( | |
173 u', '.join(set(kwargs).intersection(extra)))) | 198 u', '.join(set(kwargs).intersection(extra)))) |
174 | 199 |
175 self.checkExtra(extra) | 200 self.checkExtra(extra) |
176 | 201 |
177 email = kwargs.pop(u'email', None) | 202 email = kwargs.pop(u'email', None) |
178 emails_extra = kwargs.pop(u'emails_extra', []) | 203 emails_extra = kwargs.pop(u'emails_extra', []) |
179 if not email and emails_extra: | 204 if not email and emails_extra: |
180 raise ValueError(_(u'You need to provide a main email address before using emails_extra')) | 205 raise ValueError( |
181 | 206 _(u'You need to provide a main email address before using emails_extra')) |
182 if email is not None and not 'url_template' in kwargs and not 'message_body' in kwargs: | 207 |
183 raise ValueError(_(u"You need to provide url_template if you use default message body")) | 208 if (email is not None |
209 and not 'url_template' in kwargs | |
210 and not 'message_body' in kwargs): | |
211 raise ValueError( | |
212 _(u"You need to provide url_template if you use default message body")) | |
184 | 213 |
185 ## uuid | 214 ## uuid |
186 log.info(_(u"creating an invitation")) | 215 log.info(_(u"creating an invitation")) |
187 id_ = unicode(shortuuid.uuid()) | 216 id_ = unicode(shortuuid.uuid()) |
188 | 217 |
193 assert password | 222 assert password |
194 # XXX: password is here saved in clear in database | 223 # XXX: password is here saved in clear in database |
195 # 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 |
196 # and SàT need to be able to automatically open the profile with the uuid | 225 # and SàT need to be able to automatically open the profile with the uuid |
197 # 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 |
198 # when the invitee is connecting (e.g. with URL). This key would not be saved | 227 # when the invitee is connecting (e.g. with URL). This key would not be |
199 # and could be used to encrypt profile password. | 228 # saved and could be used to encrypt profile password. |
200 extra[KEY_PASSWORD] = password | 229 extra[KEY_PASSWORD] = password |
201 | 230 |
202 jid_ = kwargs.pop(u'jid_', None) | 231 jid_ = kwargs.pop(u'jid_', None) |
203 if not jid_: | 232 if not jid_: |
204 domain = self.host.memory.getConfig(None, 'xmpp_domain') | 233 domain = self.host.memory.getConfig(None, 'xmpp_domain') |
205 if not domain: | 234 if not domain: |
206 # TODO: fallback to profile's domain | 235 # TODO: fallback to profile's domain |
207 raise ValueError(_(u"You need to specify xmpp_domain in sat.conf")) | 236 raise ValueError(_(u"You need to specify xmpp_domain in sat.conf")) |
208 jid_ = u"invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(), domain=domain) | 237 jid_ = u"invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(), |
238 domain=domain) | |
209 jid_ = jid.JID(jid_) | 239 jid_ = jid.JID(jid_) |
210 if jid_.user: | 240 if jid_.user: |
211 # we don't register account if there is no user as anonymous login is then used | 241 # we don't register account if there is no user as anonymous login is then |
242 # used | |
212 try: | 243 try: |
213 yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password) | 244 yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password) |
214 except error.StanzaError as e: | 245 except error.StanzaError as e: |
215 prefix = jid_.user | 246 prefix = jid_.user |
216 idx = 0 | 247 idx = 0 |
217 while e.condition == u'conflict': | 248 while e.condition == u'conflict': |
218 if idx >= SUFFIX_MAX: | 249 if idx >= SUFFIX_MAX: |
219 raise exceptions.ConflictError(_(u"Can't create XMPP account")) | 250 raise exceptions.ConflictError(_(u"Can't create XMPP account")) |
220 jid_.user = prefix + '_' + unicode(idx) | 251 jid_.user = prefix + '_' + unicode(idx) |
221 log.info(_(u"requested jid already exists, trying with {}".format(jid_.full()))) | 252 log.info(_(u"requested jid already exists, trying with {}".format( |
253 jid_.full()))) | |
222 try: | 254 try: |
223 yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password) | 255 yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, |
256 password) | |
224 except error.StanzaError as e: | 257 except error.StanzaError as e: |
225 idx += 1 | 258 idx += 1 |
226 else: | 259 else: |
227 break | 260 break |
228 if e.condition != u'conflict': | 261 if e.condition != u'conflict': |
234 | 267 |
235 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_) |
236 # profile creation should not fail as we generate unique name ourselves | 269 # profile creation should not fail as we generate unique name ourselves |
237 yield self.host.memory.createProfile(guest_profile, password) | 270 yield self.host.memory.createProfile(guest_profile, password) |
238 yield self.host.memory.startSession(password, guest_profile) | 271 yield self.host.memory.startSession(password, guest_profile) |
239 yield self.host.memory.setParam("JabberID", jid_.full(), "Connection", profile_key=guest_profile) | 272 yield self.host.memory.setParam("JabberID", jid_.full(), "Connection", |
240 yield self.host.memory.setParam("Password", password, "Connection", profile_key=guest_profile) | 273 profile_key=guest_profile) |
274 yield self.host.memory.setParam("Password", password, "Connection", | |
275 profile_key=guest_profile) | |
241 name = kwargs.pop(u'name', None) | 276 name = kwargs.pop(u'name', None) |
242 if name is not None: | 277 if name is not None: |
243 extra[u'name'] = name | 278 extra[u'name'] = name |
244 try: | 279 try: |
245 id_plugin = self.host.plugins[u'IDENTITY'] | 280 id_plugin = self.host.plugins[u'IDENTITY'] |
286 format_args[u'url'] = invite_url | 321 format_args[u'url'] = invite_url |
287 | 322 |
288 yield sat_email.sendEmail( | 323 yield sat_email.sendEmail( |
289 self.host, | 324 self.host, |
290 [email] + emails_extra, | 325 [email] + emails_extra, |
291 (kwargs.pop(u'message_subject', None) or DEFAULT_SUBJECT).format(**format_args), | 326 (kwargs.pop(u'message_subject', None) or DEFAULT_SUBJECT).format( |
327 **format_args), | |
292 (kwargs.pop(u'message_body', None) or DEFAULT_BODY).format(**format_args), | 328 (kwargs.pop(u'message_body', None) or DEFAULT_BODY).format(**format_args), |
293 ) | 329 ) |
294 | 330 |
295 ## extra data saving | 331 ## extra data saving |
296 self.invitations[id_] = extra | 332 self.invitations[id_] = extra |
310 @raise KeyError: there is not invitation with this id_ | 346 @raise KeyError: there is not invitation with this id_ |
311 """ | 347 """ |
312 return self.invitations[id_] | 348 return self.invitations[id_] |
313 | 349 |
314 def _modify(self, id_, new_extra, replace): | 350 def _modify(self, id_, new_extra, replace): |
315 return self.modify(id_, {unicode(k): unicode(v) for k,v in new_extra.iteritems()}, replace) | 351 return self.modify(id_, {unicode(k): unicode(v) for k,v in new_extra.iteritems()}, |
352 replace) | |
316 | 353 |
317 def modify(self, id_, new_extra, replace=False): | 354 def modify(self, id_, new_extra, replace=False): |
318 """Modify invitation data | 355 """Modify invitation data |
319 | 356 |
320 @param id_(unicode): UUID linked to an invitation | 357 @param id_(unicode): UUID linked to an invitation |
364 C.PROF_KEY_NONE: don't filter invitations | 401 C.PROF_KEY_NONE: don't filter invitations |
365 @return list(unicode): invitations uids | 402 @return list(unicode): invitations uids |
366 """ | 403 """ |
367 invitations = yield self.invitations.items() | 404 invitations = yield self.invitations.items() |
368 if profile != C.PROF_KEY_NONE: | 405 if profile != C.PROF_KEY_NONE: |
369 invitations = {id_:data for id_, data in invitations.iteritems() if data.get(u'profile') == profile} | 406 invitations = {id_:data for id_, data in invitations.iteritems() |
407 if data.get(u'profile') == profile} | |
370 | 408 |
371 defer.returnValue(invitations) | 409 defer.returnValue(invitations) |