Mercurial > libervia-backend
annotate src/plugins/plugin_misc_invitations.py @ 2211:df115e4a36c7
plugin invitations: invitation id and invitee jid are now added to return dict in invitationCreate, bridge signature has changed too
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 29 Mar 2017 19:35:55 +0200 |
parents | f8d61592f1fc |
children | eaf2467d19ce |
rev | line source |
---|---|
2184 | 1 #!/usr/bin/env python2 |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SAT plugin for file tansfer | |
5 # Copyright (C) 2009-2016 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
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/>. | |
19 | |
20 from sat.core.i18n import _, D_ | |
21 from sat.core.constants import Const as C | |
22 from sat.core import exceptions | |
23 from sat.core.log import getLogger | |
24 log = getLogger(__name__) | |
25 import shortuuid | |
26 from sat.tools import utils | |
27 from twisted.internet import defer | |
28 from twisted.words.protocols.jabber import jid | |
29 from twisted.words.protocols.jabber import error | |
30 from sat.memory import persistent | |
31 from sat.tools import email as sat_email | |
32 | |
33 | |
34 PLUGIN_INFO = { | |
35 C.PI_NAME: "Invitations", | |
36 C.PI_IMPORT_NAME: "INVITATIONS", | |
37 C.PI_TYPE: C.PLUG_TYPE_MISC, | |
38 C.PI_DEPENDENCIES: ['XEP-0077'], | |
39 C.PI_MAIN: "InvitationsPlugin", | |
40 C.PI_HANDLER: "no", | |
41 C.PI_DESCRIPTION: _(u"""invitation of people without XMPP account""") | |
42 } | |
43 | |
44 | |
45 SUFFIX_MAX = 5 | |
46 INVITEE_PROFILE_TPL = u"guest@@{uuid}" | |
2211
df115e4a36c7
plugin invitations: invitation id and invitee jid are now added to return dict in invitationCreate, bridge signature has changed too
Goffi <goffi@goffi.org>
parents:
2210
diff
changeset
|
47 KEY_ID = u'id' |
df115e4a36c7
plugin invitations: invitation id and invitee jid are now added to return dict in invitationCreate, bridge signature has changed too
Goffi <goffi@goffi.org>
parents:
2210
diff
changeset
|
48 KEY_JID = u'jid' |
2184 | 49 KEY_CREATED = u'created' |
50 KEY_LAST_CONNECTION = u'last_connection' | |
2211
df115e4a36c7
plugin invitations: invitation id and invitee jid are now added to return dict in invitationCreate, bridge signature has changed too
Goffi <goffi@goffi.org>
parents:
2210
diff
changeset
|
51 EXTRA_RESERVED = {KEY_ID, KEY_JID, KEY_CREATED, u'jid_', u'jid', KEY_LAST_CONNECTION} |
2184 | 52 DEFAULT_SUBJECT = D_(u"You have been invited by {host_name} to {app_name}") |
53 DEFAULT_BODY = D_(u"""Hello {name}! | |
54 | |
55 You have received an invitation from {host_name} to participate to "{app_name}". | |
56 To join, you just have to click on the following URL: | |
57 {url} | |
58 | |
59 Please note that this URL should not be shared with anybody! | |
60 If you want more details on {app_name}, you can check {app_url}. | |
61 | |
62 Welcome! | |
63 """) | |
64 | |
65 | |
66 class InvitationsPlugin(object): | |
67 | |
68 def __init__(self, host): | |
69 log.info(_(u"plugin Invitations initialization")) | |
70 self.host = host | |
71 self.invitations = persistent.LazyPersistentBinaryDict(u'invitations') | |
2211
df115e4a36c7
plugin invitations: invitation id and invitee jid are now added to return dict in invitationCreate, bridge signature has changed too
Goffi <goffi@goffi.org>
parents:
2210
diff
changeset
|
72 host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sssssssssa{ss}s', out_sign='a{ss}', |
2184 | 73 method=self._createInvitation, |
74 async=True) | |
75 def _createInvitation(self, jid_=u'', password=u'', name=u'', host_name=u'', email=u'', language=u'', url_template=u'', message_subject=u'', message_body=u'', extra=None, profile=u''): | |
76 # XXX: we don't use **kwargs here to keep arguments name for introspection with D-Bus bridge | |
77 | |
78 if extra is None: | |
79 extra = {} | |
80 else: | |
81 extra = {unicode(k): unicode(v) for k,v in extra.iteritems()} | |
82 | |
83 # we need to be sure that values are unicode, else they won't be pickled correctly with D-Bus | |
84 kwargs = {"extra": extra} | |
85 for key in ("jid_", "password", "name", "host_name", "email", "language", "url_template", "message_subject", "message_body", "profile"): | |
86 value = locals()[key] | |
87 if value: | |
88 kwargs[key] = unicode(value) | |
2211
df115e4a36c7
plugin invitations: invitation id and invitee jid are now added to return dict in invitationCreate, bridge signature has changed too
Goffi <goffi@goffi.org>
parents:
2210
diff
changeset
|
89 d = self.createInvitation(**kwargs) |
df115e4a36c7
plugin invitations: invitation id and invitee jid are now added to return dict in invitationCreate, bridge signature has changed too
Goffi <goffi@goffi.org>
parents:
2210
diff
changeset
|
90 def serialize(data): |
df115e4a36c7
plugin invitations: invitation id and invitee jid are now added to return dict in invitationCreate, bridge signature has changed too
Goffi <goffi@goffi.org>
parents:
2210
diff
changeset
|
91 data[KEY_JID] = data[KEY_JID].full() |
df115e4a36c7
plugin invitations: invitation id and invitee jid are now added to return dict in invitationCreate, bridge signature has changed too
Goffi <goffi@goffi.org>
parents:
2210
diff
changeset
|
92 d.addCallback(serialize) |
df115e4a36c7
plugin invitations: invitation id and invitee jid are now added to return dict in invitationCreate, bridge signature has changed too
Goffi <goffi@goffi.org>
parents:
2210
diff
changeset
|
93 return d |
2184 | 94 |
95 @defer.inlineCallbacks | |
96 def createInvitation(self, **kwargs): | |
97 ur"""create an invitation | |
98 | |
99 this will create an XMPP account and a profile, and use a UUID to retrieve them. | |
100 the profile is automatically generated in the form guest@@[UUID], this way they can be retrieved easily | |
101 **kwargs: keywords arguments which can have the following keys, unset values are equivalent to None: | |
102 jid_(jid.JID, None): jid to use for invitation, the jid will be created using XEP-0077 | |
103 if the jid has no user part, an anonymous account will be used (no XMPP account created in this case) | |
2208
c316c6f6a737
plugin invitations: fixed leak of uuid in jid
Goffi <goffi@goffi.org>
parents:
2185
diff
changeset
|
104 if None, automatically generate an account name (in the form "invitation-[random UUID]@domain.tld") (note that this UUID is not the |
c316c6f6a737
plugin invitations: fixed leak of uuid in jid
Goffi <goffi@goffi.org>
parents:
2185
diff
changeset
|
105 same as the invitation one, as jid can be used publicly (leaking the UUID), and invitation UUID give access to account. |
2184 | 106 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) |
107 password(unicode, None): password to use (will be used for XMPP account and profile) | |
108 None to automatically generate one | |
109 name(unicode, None): name of the invitee | |
110 host_name(unicode, None): name of the host | |
111 email(unicode, None): email to send the invitation to | |
112 if None, no invitation email is sent, you can still associate email using extra | |
113 if email is used, extra can't have "email" key | |
114 language(unicode): language of the invitee (used notabily to translate the invitation) | |
115 TODO: not used yet | |
116 url_template(unicode, None): template to use to construct the invitation URL | |
117 use {uuid} as a placeholder for identifier | |
118 use None if you don't want to include URL (or if it is already specified in custom message) | |
119 /!\ you must put full URL, don't forget https:// | |
120 /!\ the URL will give access to the invitee account, you should warn in message to not publish it publicly | |
2210
f8d61592f1fc
plugin invitations: raise ValueError if url_template and message_body are both not specified
Goffi <goffi@goffi.org>
parents:
2208
diff
changeset
|
121 message_subject(unicode, None): customised message body for the invitation email |
2184 | 122 None to use default subject |
123 uses the same substitution as for message_body | |
124 message_body(unicode, None): customised message body for the invitation email | |
125 None to use default body | |
126 use {name} as a place holder for invitee name | |
127 use {url} as a placeholder for the invitation url | |
128 use {uuid} as a placeholder for the identifier | |
129 use {app_name} as a placeholder for this software name | |
130 use {app_url} as a placeholder for this software official website | |
131 use {profile} as a placeholder for host's profile | |
132 use {host_name} as a placeholder for host's name | |
133 extra(dict, None): extra data to associate with the invitee | |
134 some keys are reserved: | |
135 - created (creation date) | |
136 if email argument is used, "email" key can't be used | |
137 profile(unicode, None): profile of the host (person who is inviting) | |
2211
df115e4a36c7
plugin invitations: invitation id and invitee jid are now added to return dict in invitationCreate, bridge signature has changed too
Goffi <goffi@goffi.org>
parents:
2210
diff
changeset
|
138 @return (dict[unicode, unicode]): dictionary with: |
df115e4a36c7
plugin invitations: invitation id and invitee jid are now added to return dict in invitationCreate, bridge signature has changed too
Goffi <goffi@goffi.org>
parents:
2210
diff
changeset
|
139 - UUID associated with the invitee (key: id) |
2184 | 140 - filled extra dictionary, as saved in the databae |
141 """ | |
142 ## initial checks | |
143 extra = kwargs.pop('extra', {}) | |
144 if set(kwargs).intersection(extra): | |
145 raise exceptions.ValueError(_(u"You can't use following key(s) in both args and extra: {}").format( | |
146 u', '.join(set(kwargs).intersection(extra)))) | |
147 | |
148 if EXTRA_RESERVED.intersection(extra): | |
149 raise exceptions.ValueError(_(u"You can't use following key(s) in extra, they are reserved: {}").format( | |
150 u', '.join(EXTRA_RESERVED.intersection(extra)))) | |
151 | |
2210
f8d61592f1fc
plugin invitations: raise ValueError if url_template and message_body are both not specified
Goffi <goffi@goffi.org>
parents:
2208
diff
changeset
|
152 if not 'url_template' in extra and not 'message_body' in extra: |
f8d61592f1fc
plugin invitations: raise ValueError if url_template and message_body are both not specified
Goffi <goffi@goffi.org>
parents:
2208
diff
changeset
|
153 raise ValueError(_(u"You need to provide url_template if you use default message body")) |
f8d61592f1fc
plugin invitations: raise ValueError if url_template and message_body are both not specified
Goffi <goffi@goffi.org>
parents:
2208
diff
changeset
|
154 |
f8d61592f1fc
plugin invitations: raise ValueError if url_template and message_body are both not specified
Goffi <goffi@goffi.org>
parents:
2208
diff
changeset
|
155 |
2184 | 156 ## uuid |
157 log.info(_(u"creating an invitation")) | |
158 id_ = unicode(shortuuid.uuid()) | |
159 | |
160 ## XMPP account creation | |
161 password = kwargs.pop(u'password', None) | |
162 if password is None: | |
163 password = utils.generatePassword() | |
164 assert password | |
165 # XXX: password is here saved in clear in database | |
166 # it is needed for invitation as the same password is used for profile | |
167 # and SàT need to be able to automatically open the profile with the uuid | |
168 # FIXME: we could add an extra encryption key which would be used with the uuid | |
169 # when the invitee is connecting (e.g. with URL). This key would not be saved | |
170 # and could be used to encrypt profile password. | |
171 extra[u'password'] = password | |
172 | |
173 jid_ = kwargs.pop(u'jid_', None) | |
174 if not jid_: | |
175 domain = self.host.memory.getConfig(None, 'xmpp_domain') | |
176 if not domain: | |
177 # TODO: fallback to profile's domain | |
178 raise ValueError(_(u"You need to specify xmpp_domain in sat.conf")) | |
2208
c316c6f6a737
plugin invitations: fixed leak of uuid in jid
Goffi <goffi@goffi.org>
parents:
2185
diff
changeset
|
179 jid_ = u"invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(), domain=domain) |
2184 | 180 jid_ = jid.JID(jid_) |
181 if jid_.user: | |
182 # we don't register account if there is no user as anonymous login is then used | |
183 try: | |
184 yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password) | |
185 except error.StanzaError as e: | |
186 prefix = jid_.user | |
187 idx = 0 | |
188 while e.condition == u'conflict': | |
189 if idx >= SUFFIX_MAX: | |
190 raise exceptions.ConflictError(_(u"Can't create XMPP account")) | |
191 jid_.user = prefix + '_' + unicode(idx) | |
192 log.info(_(u"requested jid already exists, trying with {}".format(jid_.full()))) | |
193 try: | |
194 yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password) | |
195 except error.StanzaError as e: | |
196 idx += 1 | |
197 else: | |
198 break | |
199 if e.condition != u'conflict': | |
200 raise e | |
201 | |
202 log.info(_(u"account {jid_} created").format(jid_=jid_.full())) | |
203 | |
204 ## profile creation | |
205 extra['guest_profile'] = guest_profile = INVITEE_PROFILE_TPL.format(uuid=id_) | |
206 # profile creation should not fail as we generate unique name ourselves | |
207 yield self.host.memory.createProfile(guest_profile, password) | |
208 yield self.host.memory.startSession(password, guest_profile) | |
209 yield self.host.memory.setParam("JabberID", jid_.full(), "Connection", profile_key=guest_profile) | |
210 yield self.host.memory.setParam("Password", password, "Connection", profile_key=guest_profile) | |
211 | |
212 ## email | |
213 language = kwargs.pop(u'language', None) | |
214 if language is not None: | |
215 extra[u'language'] = language | |
216 email = kwargs.pop(u'email', None) | |
217 | |
218 if email is not None: | |
219 url_template = kwargs.pop(u'url_template', '') | |
220 format_args = { | |
221 u'uuid': id_, | |
222 u'app_name': C.APP_NAME, | |
223 u'app_url': C.APP_URL} | |
224 | |
225 name = kwargs.pop(u'name', None) | |
226 if name is None: | |
227 format_args[u'name'] = email | |
228 else: | |
229 format_args[u'name'] = extra[u'name'] = name | |
230 | |
231 profile = kwargs.pop(u'profile', None) | |
232 if profile is None: | |
233 format_args[u'profile'] = u'' | |
234 else: | |
235 format_args[u'profile'] = extra[u'profile'] = profile | |
236 | |
237 host_name = kwargs.pop(u'host_name', None) | |
238 if host_name is None: | |
239 format_args[u'host_name'] = profile or _(u"somebody") | |
240 else: | |
241 format_args[u'host_name'] = extra[u'host_name'] = host_name | |
242 | |
243 invite_url = url_template.format(**format_args) | |
244 format_args[u'url'] = invite_url | |
245 | |
246 yield sat_email.sendEmail( | |
247 self.host, | |
248 [email], | |
249 (kwargs.pop(u'message_subject', None) or DEFAULT_SUBJECT).format(**format_args), | |
250 (kwargs.pop(u'message_body', None) or DEFAULT_BODY).format(**format_args), | |
251 ) | |
252 | |
253 ## extra data saving | |
254 self.invitations[id_] = extra | |
255 | |
256 if kwargs: | |
257 log.warning(_(u"Not all arguments have been consumed: {}").format(kwargs)) | |
258 | |
2211
df115e4a36c7
plugin invitations: invitation id and invitee jid are now added to return dict in invitationCreate, bridge signature has changed too
Goffi <goffi@goffi.org>
parents:
2210
diff
changeset
|
259 extra[KEY_ID] = id_ |
df115e4a36c7
plugin invitations: invitation id and invitee jid are now added to return dict in invitationCreate, bridge signature has changed too
Goffi <goffi@goffi.org>
parents:
2210
diff
changeset
|
260 extra[KEY_JID] = jid |
df115e4a36c7
plugin invitations: invitation id and invitee jid are now added to return dict in invitationCreate, bridge signature has changed too
Goffi <goffi@goffi.org>
parents:
2210
diff
changeset
|
261 defer.returnValue(extra) |