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}" |
|
47 KEY_CREATED = u'created' |
|
48 KEY_LAST_CONNECTION = u'last_connection' |
|
49 EXTRA_RESERVED = {KEY_CREATED, u'jid_', u'jid', KEY_LAST_CONNECTION} |
|
50 DEFAULT_SUBJECT = D_(u"You have been invited by {host_name} to {app_name}") |
|
51 DEFAULT_BODY = D_(u"""Hello {name}! |
|
52 |
|
53 You have received an invitation from {host_name} to participate to "{app_name}". |
|
54 To join, you just have to click on the following URL: |
|
55 {url} |
|
56 |
|
57 Please note that this URL should not be shared with anybody! |
|
58 If you want more details on {app_name}, you can check {app_url}. |
|
59 |
|
60 Welcome! |
|
61 """) |
|
62 |
|
63 |
|
64 class InvitationsPlugin(object): |
|
65 # TODO: plugin unload |
|
66 |
|
67 def __init__(self, host): |
|
68 log.info(_(u"plugin Invitations initialization")) |
|
69 self.host = host |
|
70 self.invitations = persistent.LazyPersistentBinaryDict(u'invitations') |
|
71 host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sssssssssa{ss}s', out_sign='(sa{ss})', |
|
72 method=self._createInvitation, |
|
73 async=True) |
|
74 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''): |
|
75 # XXX: we don't use **kwargs here to keep arguments name for introspection with D-Bus bridge |
|
76 |
|
77 if extra is None: |
|
78 extra = {} |
|
79 else: |
|
80 extra = {unicode(k): unicode(v) for k,v in extra.iteritems()} |
|
81 |
|
82 # we need to be sure that values are unicode, else they won't be pickled correctly with D-Bus |
|
83 kwargs = {"extra": extra} |
|
84 for key in ("jid_", "password", "name", "host_name", "email", "language", "url_template", "message_subject", "message_body", "profile"): |
|
85 value = locals()[key] |
|
86 if value: |
|
87 kwargs[key] = unicode(value) |
|
88 return self.createInvitation(**kwargs) |
|
89 |
|
90 @defer.inlineCallbacks |
|
91 def createInvitation(self, **kwargs): |
|
92 ur"""create an invitation |
|
93 |
|
94 this will create an XMPP account and a profile, and use a UUID to retrieve them. |
|
95 the profile is automatically generated in the form guest@@[UUID], this way they can be retrieved easily |
|
96 **kwargs: keywords arguments which can have the following keys, unset values are equivalent to None: |
|
97 jid_(jid.JID, None): jid to use for invitation, the jid will be created using XEP-0077 |
|
98 if the jid has no user part, an anonymous account will be used (no XMPP account created in this case) |
|
99 if None, automatically generate an account name (in the form "invitation-[UUID]@domain.tld") |
|
100 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) |
|
101 password(unicode, None): password to use (will be used for XMPP account and profile) |
|
102 None to automatically generate one |
|
103 name(unicode, None): name of the invitee |
|
104 host_name(unicode, None): name of the host |
|
105 email(unicode, None): email to send the invitation to |
|
106 if None, no invitation email is sent, you can still associate email using extra |
|
107 if email is used, extra can't have "email" key |
|
108 language(unicode): language of the invitee (used notabily to translate the invitation) |
|
109 TODO: not used yet |
|
110 url_template(unicode, None): template to use to construct the invitation URL |
|
111 use {uuid} as a placeholder for identifier |
|
112 use None if you don't want to include URL (or if it is already specified in custom message) |
|
113 /!\ you must put full URL, don't forget https:// |
|
114 /!\ the URL will give access to the invitee account, you should warn in message to not publish it publicly |
|
115 message_subject(unicode, None): customised message body for t:o he invitation email |
|
116 None to use default subject |
|
117 uses the same substitution as for message_body |
|
118 message_body(unicode, None): customised message body for the invitation email |
|
119 None to use default body |
|
120 use {name} as a place holder for invitee name |
|
121 use {url} as a placeholder for the invitation url |
|
122 use {uuid} as a placeholder for the identifier |
|
123 use {app_name} as a placeholder for this software name |
|
124 use {app_url} as a placeholder for this software official website |
|
125 use {profile} as a placeholder for host's profile |
|
126 use {host_name} as a placeholder for host's name |
|
127 extra(dict, None): extra data to associate with the invitee |
|
128 some keys are reserved: |
|
129 - created (creation date) |
|
130 if email argument is used, "email" key can't be used |
|
131 profile(unicode, None): profile of the host (person who is inviting) |
|
132 @return (unicode, dict[unicode, unicode]): tuple with: |
|
133 - UUID associated with the invitee |
|
134 - filled extra dictionary, as saved in the databae |
|
135 """ |
|
136 ## initial checks |
|
137 extra = kwargs.pop('extra', {}) |
|
138 if set(kwargs).intersection(extra): |
|
139 raise exceptions.ValueError(_(u"You can't use following key(s) in both args and extra: {}").format( |
|
140 u', '.join(set(kwargs).intersection(extra)))) |
|
141 |
|
142 if EXTRA_RESERVED.intersection(extra): |
|
143 raise exceptions.ValueError(_(u"You can't use following key(s) in extra, they are reserved: {}").format( |
|
144 u', '.join(EXTRA_RESERVED.intersection(extra)))) |
|
145 |
|
146 ## uuid |
|
147 log.info(_(u"creating an invitation")) |
|
148 id_ = unicode(shortuuid.uuid()) |
|
149 |
|
150 ## XMPP account creation |
|
151 password = kwargs.pop(u'password', None) |
|
152 if password is None: |
|
153 password = utils.generatePassword() |
|
154 assert password |
|
155 # XXX: password is here saved in clear in database |
|
156 # it is needed for invitation as the same password is used for profile |
|
157 # and SàT need to be able to automatically open the profile with the uuid |
|
158 # FIXME: we could add an extra encryption key which would be used with the uuid |
|
159 # when the invitee is connecting (e.g. with URL). This key would not be saved |
|
160 # and could be used to encrypt profile password. |
|
161 extra[u'password'] = password |
|
162 |
|
163 jid_ = kwargs.pop(u'jid_', None) |
|
164 if not jid_: |
|
165 domain = self.host.memory.getConfig(None, 'xmpp_domain') |
|
166 if not domain: |
|
167 # TODO: fallback to profile's domain |
|
168 raise ValueError(_(u"You need to specify xmpp_domain in sat.conf")) |
|
169 jid_ = u"invitation-{uuid}@{domain}".format(uuid=id_, domain=domain) |
|
170 jid_ = jid.JID(jid_) |
|
171 if jid_.user: |
|
172 # we don't register account if there is no user as anonymous login is then used |
|
173 try: |
|
174 yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password) |
|
175 except error.StanzaError as e: |
|
176 prefix = jid_.user |
|
177 idx = 0 |
|
178 while e.condition == u'conflict': |
|
179 if idx >= SUFFIX_MAX: |
|
180 raise exceptions.ConflictError(_(u"Can't create XMPP account")) |
|
181 jid_.user = prefix + '_' + unicode(idx) |
|
182 log.info(_(u"requested jid already exists, trying with {}".format(jid_.full()))) |
|
183 try: |
|
184 yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password) |
|
185 except error.StanzaError as e: |
|
186 idx += 1 |
|
187 else: |
|
188 break |
|
189 if e.condition != u'conflict': |
|
190 raise e |
|
191 |
|
192 log.info(_(u"account {jid_} created").format(jid_=jid_.full())) |
|
193 |
|
194 ## profile creation |
|
195 extra['guest_profile'] = guest_profile = INVITEE_PROFILE_TPL.format(uuid=id_) |
|
196 # profile creation should not fail as we generate unique name ourselves |
|
197 yield self.host.memory.createProfile(guest_profile, password) |
|
198 yield self.host.memory.startSession(password, guest_profile) |
|
199 yield self.host.memory.setParam("JabberID", jid_.full(), "Connection", profile_key=guest_profile) |
|
200 yield self.host.memory.setParam("Password", password, "Connection", profile_key=guest_profile) |
|
201 |
|
202 ## email |
|
203 language = kwargs.pop(u'language', None) |
|
204 if language is not None: |
|
205 extra[u'language'] = language |
|
206 email = kwargs.pop(u'email', None) |
|
207 |
|
208 if email is not None: |
|
209 url_template = kwargs.pop(u'url_template', '') |
|
210 format_args = { |
|
211 u'uuid': id_, |
|
212 u'app_name': C.APP_NAME, |
|
213 u'app_url': C.APP_URL} |
|
214 |
|
215 name = kwargs.pop(u'name', None) |
|
216 if name is None: |
|
217 format_args[u'name'] = email |
|
218 else: |
|
219 format_args[u'name'] = extra[u'name'] = name |
|
220 |
|
221 profile = kwargs.pop(u'profile', None) |
|
222 if profile is None: |
|
223 format_args[u'profile'] = u'' |
|
224 else: |
|
225 format_args[u'profile'] = extra[u'profile'] = profile |
|
226 |
|
227 host_name = kwargs.pop(u'host_name', None) |
|
228 if host_name is None: |
|
229 format_args[u'host_name'] = profile or _(u"somebody") |
|
230 else: |
|
231 format_args[u'host_name'] = extra[u'host_name'] = host_name |
|
232 |
|
233 invite_url = url_template.format(**format_args) |
|
234 format_args[u'url'] = invite_url |
|
235 |
|
236 yield sat_email.sendEmail( |
|
237 self.host, |
|
238 [email], |
|
239 (kwargs.pop(u'message_subject', None) or DEFAULT_SUBJECT).format(**format_args), |
|
240 (kwargs.pop(u'message_body', None) or DEFAULT_BODY).format(**format_args), |
|
241 ) |
|
242 |
|
243 ## extra data saving |
|
244 self.invitations[id_] = extra |
|
245 |
|
246 if kwargs: |
|
247 log.warning(_(u"Not all arguments have been consumed: {}").format(kwargs)) |
|
248 |
|
249 defer.returnValue((id_, extra)) |