plugin invitations: first draft:
/!\ new dependency: shortuuid Plugin invitations allows to invite somebody without XMPP account, using an unique identifier. The various arguments are explained in the docstring.
This commit is contained in:
parent
94370a8da3
commit
d7d5ca1d18
2 changed files with 250 additions and 1 deletions
249
src/plugins/plugin_misc_invitations.py
Normal file
249
src/plugins/plugin_misc_invitations.py
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
#!/usr/bin/env python2
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# SAT plugin for file tansfer
|
||||
# Copyright (C) 2009-2016 Jérôme Poisson (goffi@goffi.org)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from sat.core.i18n import _, D_
|
||||
from sat.core.constants import Const as C
|
||||
from sat.core import exceptions
|
||||
from sat.core.log import getLogger
|
||||
log = getLogger(__name__)
|
||||
import shortuuid
|
||||
from sat.tools import utils
|
||||
from twisted.internet import defer
|
||||
from twisted.words.protocols.jabber import jid
|
||||
from twisted.words.protocols.jabber import error
|
||||
from sat.memory import persistent
|
||||
from sat.tools import email as sat_email
|
||||
|
||||
|
||||
PLUGIN_INFO = {
|
||||
C.PI_NAME: "Invitations",
|
||||
C.PI_IMPORT_NAME: "INVITATIONS",
|
||||
C.PI_TYPE: C.PLUG_TYPE_MISC,
|
||||
C.PI_DEPENDENCIES: ['XEP-0077'],
|
||||
C.PI_MAIN: "InvitationsPlugin",
|
||||
C.PI_HANDLER: "no",
|
||||
C.PI_DESCRIPTION: _(u"""invitation of people without XMPP account""")
|
||||
}
|
||||
|
||||
|
||||
SUFFIX_MAX = 5
|
||||
INVITEE_PROFILE_TPL = u"guest@@{uuid}"
|
||||
KEY_CREATED = u'created'
|
||||
KEY_LAST_CONNECTION = u'last_connection'
|
||||
EXTRA_RESERVED = {KEY_CREATED, u'jid_', u'jid', KEY_LAST_CONNECTION}
|
||||
DEFAULT_SUBJECT = D_(u"You have been invited by {host_name} to {app_name}")
|
||||
DEFAULT_BODY = D_(u"""Hello {name}!
|
||||
|
||||
You have received an invitation from {host_name} to participate to "{app_name}".
|
||||
To join, you just have to click on the following URL:
|
||||
{url}
|
||||
|
||||
Please note that this URL should not be shared with anybody!
|
||||
If you want more details on {app_name}, you can check {app_url}.
|
||||
|
||||
Welcome!
|
||||
""")
|
||||
|
||||
|
||||
class InvitationsPlugin(object):
|
||||
# TODO: plugin unload
|
||||
|
||||
def __init__(self, host):
|
||||
log.info(_(u"plugin Invitations initialization"))
|
||||
self.host = host
|
||||
self.invitations = persistent.LazyPersistentBinaryDict(u'invitations')
|
||||
host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sssssssssa{ss}s', out_sign='(sa{ss})',
|
||||
method=self._createInvitation,
|
||||
async=True)
|
||||
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''):
|
||||
# XXX: we don't use **kwargs here to keep arguments name for introspection with D-Bus bridge
|
||||
|
||||
if extra is None:
|
||||
extra = {}
|
||||
else:
|
||||
extra = {unicode(k): unicode(v) for k,v in extra.iteritems()}
|
||||
|
||||
# we need to be sure that values are unicode, else they won't be pickled correctly with D-Bus
|
||||
kwargs = {"extra": extra}
|
||||
for key in ("jid_", "password", "name", "host_name", "email", "language", "url_template", "message_subject", "message_body", "profile"):
|
||||
value = locals()[key]
|
||||
if value:
|
||||
kwargs[key] = unicode(value)
|
||||
return self.createInvitation(**kwargs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def createInvitation(self, **kwargs):
|
||||
ur"""create an invitation
|
||||
|
||||
this will create an XMPP account and a profile, and use a UUID to retrieve them.
|
||||
the profile is automatically generated in the form guest@@[UUID], this way they can be retrieved easily
|
||||
**kwargs: keywords arguments which can have the following keys, unset values are equivalent to None:
|
||||
jid_(jid.JID, None): jid to use for invitation, the jid will be created using XEP-0077
|
||||
if the jid has no user part, an anonymous account will be used (no XMPP account created in this case)
|
||||
if None, automatically generate an account name (in the form "invitation-[UUID]@domain.tld")
|
||||
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)
|
||||
password(unicode, None): password to use (will be used for XMPP account and profile)
|
||||
None to automatically generate one
|
||||
name(unicode, None): name of the invitee
|
||||
host_name(unicode, None): name of the host
|
||||
email(unicode, None): email to send the invitation to
|
||||
if None, no invitation email is sent, you can still associate email using extra
|
||||
if email is used, extra can't have "email" key
|
||||
language(unicode): language of the invitee (used notabily to translate the invitation)
|
||||
TODO: not used yet
|
||||
url_template(unicode, None): template to use to construct the invitation URL
|
||||
use {uuid} as a placeholder for identifier
|
||||
use None if you don't want to include URL (or if it is already specified in custom message)
|
||||
/!\ you must put full URL, don't forget https://
|
||||
/!\ the URL will give access to the invitee account, you should warn in message to not publish it publicly
|
||||
message_subject(unicode, None): customised message body for t:o he invitation email
|
||||
None to use default subject
|
||||
uses the same substitution as for message_body
|
||||
message_body(unicode, None): customised message body for the invitation email
|
||||
None to use default body
|
||||
use {name} as a place holder for invitee name
|
||||
use {url} as a placeholder for the invitation url
|
||||
use {uuid} as a placeholder for the identifier
|
||||
use {app_name} as a placeholder for this software name
|
||||
use {app_url} as a placeholder for this software official website
|
||||
use {profile} as a placeholder for host's profile
|
||||
use {host_name} as a placeholder for host's name
|
||||
extra(dict, None): extra data to associate with the invitee
|
||||
some keys are reserved:
|
||||
- created (creation date)
|
||||
if email argument is used, "email" key can't be used
|
||||
profile(unicode, None): profile of the host (person who is inviting)
|
||||
@return (unicode, dict[unicode, unicode]): tuple with:
|
||||
- UUID associated with the invitee
|
||||
- filled extra dictionary, as saved in the databae
|
||||
"""
|
||||
## initial checks
|
||||
extra = kwargs.pop('extra', {})
|
||||
if set(kwargs).intersection(extra):
|
||||
raise exceptions.ValueError(_(u"You can't use following key(s) in both args and extra: {}").format(
|
||||
u', '.join(set(kwargs).intersection(extra))))
|
||||
|
||||
if EXTRA_RESERVED.intersection(extra):
|
||||
raise exceptions.ValueError(_(u"You can't use following key(s) in extra, they are reserved: {}").format(
|
||||
u', '.join(EXTRA_RESERVED.intersection(extra))))
|
||||
|
||||
## uuid
|
||||
log.info(_(u"creating an invitation"))
|
||||
id_ = unicode(shortuuid.uuid())
|
||||
|
||||
## XMPP account creation
|
||||
password = kwargs.pop(u'password', None)
|
||||
if password is None:
|
||||
password = utils.generatePassword()
|
||||
assert password
|
||||
# XXX: password is here saved in clear in database
|
||||
# it is needed for invitation as the same password is used for profile
|
||||
# and SàT need to be able to automatically open the profile with the uuid
|
||||
# FIXME: we could add an extra encryption key which would be used with the uuid
|
||||
# when the invitee is connecting (e.g. with URL). This key would not be saved
|
||||
# and could be used to encrypt profile password.
|
||||
extra[u'password'] = password
|
||||
|
||||
jid_ = kwargs.pop(u'jid_', None)
|
||||
if not jid_:
|
||||
domain = self.host.memory.getConfig(None, 'xmpp_domain')
|
||||
if not domain:
|
||||
# TODO: fallback to profile's domain
|
||||
raise ValueError(_(u"You need to specify xmpp_domain in sat.conf"))
|
||||
jid_ = u"invitation-{uuid}@{domain}".format(uuid=id_, domain=domain)
|
||||
jid_ = jid.JID(jid_)
|
||||
if jid_.user:
|
||||
# we don't register account if there is no user as anonymous login is then used
|
||||
try:
|
||||
yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
|
||||
except error.StanzaError as e:
|
||||
prefix = jid_.user
|
||||
idx = 0
|
||||
while e.condition == u'conflict':
|
||||
if idx >= SUFFIX_MAX:
|
||||
raise exceptions.ConflictError(_(u"Can't create XMPP account"))
|
||||
jid_.user = prefix + '_' + unicode(idx)
|
||||
log.info(_(u"requested jid already exists, trying with {}".format(jid_.full())))
|
||||
try:
|
||||
yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
|
||||
except error.StanzaError as e:
|
||||
idx += 1
|
||||
else:
|
||||
break
|
||||
if e.condition != u'conflict':
|
||||
raise e
|
||||
|
||||
log.info(_(u"account {jid_} created").format(jid_=jid_.full()))
|
||||
|
||||
## profile creation
|
||||
extra['guest_profile'] = guest_profile = INVITEE_PROFILE_TPL.format(uuid=id_)
|
||||
# profile creation should not fail as we generate unique name ourselves
|
||||
yield self.host.memory.createProfile(guest_profile, password)
|
||||
yield self.host.memory.startSession(password, guest_profile)
|
||||
yield self.host.memory.setParam("JabberID", jid_.full(), "Connection", profile_key=guest_profile)
|
||||
yield self.host.memory.setParam("Password", password, "Connection", profile_key=guest_profile)
|
||||
|
||||
## email
|
||||
language = kwargs.pop(u'language', None)
|
||||
if language is not None:
|
||||
extra[u'language'] = language
|
||||
email = kwargs.pop(u'email', None)
|
||||
|
||||
if email is not None:
|
||||
url_template = kwargs.pop(u'url_template', '')
|
||||
format_args = {
|
||||
u'uuid': id_,
|
||||
u'app_name': C.APP_NAME,
|
||||
u'app_url': C.APP_URL}
|
||||
|
||||
name = kwargs.pop(u'name', None)
|
||||
if name is None:
|
||||
format_args[u'name'] = email
|
||||
else:
|
||||
format_args[u'name'] = extra[u'name'] = name
|
||||
|
||||
profile = kwargs.pop(u'profile', None)
|
||||
if profile is None:
|
||||
format_args[u'profile'] = u''
|
||||
else:
|
||||
format_args[u'profile'] = extra[u'profile'] = profile
|
||||
|
||||
host_name = kwargs.pop(u'host_name', None)
|
||||
if host_name is None:
|
||||
format_args[u'host_name'] = profile or _(u"somebody")
|
||||
else:
|
||||
format_args[u'host_name'] = extra[u'host_name'] = host_name
|
||||
|
||||
invite_url = url_template.format(**format_args)
|
||||
format_args[u'url'] = invite_url
|
||||
|
||||
yield sat_email.sendEmail(
|
||||
self.host,
|
||||
[email],
|
||||
(kwargs.pop(u'message_subject', None) or DEFAULT_SUBJECT).format(**format_args),
|
||||
(kwargs.pop(u'message_body', None) or DEFAULT_BODY).format(**format_args),
|
||||
)
|
||||
|
||||
## extra data saving
|
||||
self.invitations[id_] = extra
|
||||
|
||||
if kwargs:
|
||||
log.warning(_(u"Not all arguments have been consumed: {}").format(kwargs))
|
||||
|
||||
defer.returnValue((id_, extra))
|
||||
Loading…
Reference in a new issue