# HG changeset patch # User Goffi # Date 1555222911 -7200 # Node ID 90146552cde51ef91dda7dad6e49f601e96712dd # Parent 695fc440c3b8ddc17e1fe070fcde24fb5168f9c6 core (memory), plugin XEP-0329, plugin invitation: minor style improvments diff -r 695fc440c3b8 -r 90146552cde5 sat/memory/memory.py --- a/sat/memory/memory.py Sun Apr 14 08:21:51 2019 +0200 +++ b/sat/memory/memory.py Sun Apr 14 08:21:51 2019 +0200 @@ -1417,27 +1417,11 @@ @defer.inlineCallbacks def setFile( - self, - client, - name, - file_id=None, - version=u"", - parent=None, - path=None, - type_=C.FILE_TYPE_FILE, - file_hash=None, - hash_algo=None, - size=None, - namespace=None, - mime_type=None, - created=None, - modified=None, - owner=None, - access=None, - extra=None, - peer_jid=None, - perms_to_check=(C.ACCESS_PERM_WRITE,), - ): + self, client, name, file_id=None, version=u"", parent=None, path=None, + type_=C.FILE_TYPE_FILE, file_hash=None, hash_algo=None, size=None, + namespace=None, mime_type=None, created=None, modified=None, owner=None, + access=None, extra=None, peer_jid=None, perms_to_check=(C.ACCESS_PERM_WRITE,) + ): """set a file metadata @param name(unicode): basename of the file @@ -1446,32 +1430,38 @@ empty string for current version or when there is no versioning @param parent(unicode, None): id of the directory containing the files @param path(unicode, None): virtual path of the file in the namespace - if set, parent must be None. All intermediate directories will be created if needed, - using current access. + if set, parent must be None. All intermediate directories will be created + if needed, using current access. @param file_hash(unicode): unique hash of the payload @param hash_algo(unicode): algorithm used for hashing the file (usually sha-256) @param size(int): size in bytes - @param namespace(unicode, None): identifier (human readable is better) to group files - for instance, namespace could be used to group files in a specific photo album + @param namespace(unicode, None): identifier (human readable is better) to group + files + For instance, namespace could be used to group files in a specific photo album @param mime_type(unicode): MIME type of the file, or None if not known/guessed @param created(int): UNIX time of creation - @param modified(int,None): UNIX time of last modification, or None to use created date - @param owner(jid.JID, None): jid of the owner of the file (mainly useful for component) + @param modified(int,None): UNIX time of last modification, or None to use + created date + @param owner(jid.JID, None): jid of the owner of the file (mainly useful for + component) will be used to check permission (only bare jid is used, don't use with MUC). Use None to ignore permission (perms_to_check must be None too) @param access(dict, None): serialisable dictionary with access rules. - None (or empty dict) to use private access, i.e. allow only profile's jid to access the file + None (or empty dict) to use private access, i.e. allow only profile's jid to + access the file key can be on on C.ACCESS_PERM_*, then a sub dictionary with a type key is used (one of C.ACCESS_TYPE_*). According to type, extra keys can be used: - C.ACCESS_TYPE_PUBLIC: the permission is granted for everybody - - C.ACCESS_TYPE_WHITELIST: the permission is granted for jids (as unicode) in the 'jids' key + - C.ACCESS_TYPE_WHITELIST: the permission is granted for jids (as unicode) + in the 'jids' key will be encoded to json in database @param extra(dict, None): serialisable dictionary of any extra data will be encoded to json in database @param perms_to_check(tuple[unicode],None): permission to check must be a tuple of C.ACCESS_PERM_* or None - if None, permission will no be checked (peer_jid must be None too in this case) + if None, permission will no be checked (peer_jid must be None too in this + case) @param profile(unicode): profile owning the file """ if "/" in name: diff -r 695fc440c3b8 -r 90146552cde5 sat/plugins/plugin_misc_invitations.py --- a/sat/plugins/plugin_misc_invitations.py Sun Apr 14 08:21:51 2019 +0200 +++ b/sat/plugins/plugin_misc_invitations.py Sun Apr 14 08:21:51 2019 +0200 @@ -17,20 +17,21 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import shortuuid +from twisted.internet import defer +from twisted.words.protocols.jabber import jid +from twisted.words.protocols.jabber import error 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 sat.tools.common import data_format -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 +log = getLogger(__name__) + PLUGIN_INFO = { C.PI_NAME: "Invitations", @@ -53,7 +54,8 @@ KEY_GUEST_PROFILE = u'guest_profile' KEY_PASSWORD = u'password' KEY_EMAILS_EXTRA = u'emails_extra' -EXTRA_RESERVED = {KEY_ID, KEY_JID, KEY_CREATED, u'jid_', u'jid', KEY_LAST_CONNECTION, KEY_GUEST_PROFILE, KEY_PASSWORD, KEY_EMAILS_EXTRA} +EXTRA_RESERVED = {KEY_ID, KEY_JID, KEY_CREATED, u'jid_', u'jid', KEY_LAST_CONNECTION, + KEY_GUEST_PROFILE, KEY_PASSWORD, KEY_EMAILS_EXTRA} DEFAULT_SUBJECT = D_(u"You have been invited by {host_name} to {app_name}") DEFAULT_BODY = D_(u"""Hello {name}! @@ -74,26 +76,33 @@ log.info(_(u"plugin Invitations initialization")) self.host = host self.invitations = persistent.LazyPersistentBinaryDict(u'invitations') - host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sasssssssssa{ss}s', out_sign='a{ss}', + host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sasssssssssa{ss}s', + out_sign='a{ss}', method=self._create, async=True) host.bridge.addMethod("invitationGet", ".plugin", in_sign='s', out_sign='a{ss}', method=self.get, async=True) - host.bridge.addMethod("invitationModify", ".plugin", in_sign='sa{ss}b', out_sign='', + host.bridge.addMethod("invitationModify", ".plugin", in_sign='sa{ss}b', + out_sign='', method=self._modify, async=True) - host.bridge.addMethod("invitationList", ".plugin", in_sign='s', out_sign='a{sa{ss}}', + host.bridge.addMethod("invitationList", ".plugin", in_sign='s', + out_sign='a{sa{ss}}', method=self._list, async=True) def checkExtra(self, extra): if EXTRA_RESERVED.intersection(extra): - raise ValueError(_(u"You can't use following key(s) in extra, they are reserved: {}").format( - u', '.join(EXTRA_RESERVED.intersection(extra)))) + raise ValueError( + _(u"You can't use following key(s) in extra, they are reserved: {}") + .format(u', '.join(EXTRA_RESERVED.intersection(extra)))) - 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''): - # XXX: we don't use **kwargs here to keep arguments name for introspection with D-Bus bridge + 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''): + # XXX: we don't use **kwargs here to keep arguments name for introspection with + # D-Bus bridge if emails_extra is None: emails_extra = [] @@ -106,8 +115,10 @@ KEY_EMAILS_EXTRA: [unicode(e) for e in emails_extra] } - # we need to be sure that values are unicode, else they won't be pickled correctly with D-Bus - for key in ("jid_", "password", "name", "host_name", "email", "language", "url_template", "message_subject", "message_body", "profile"): + # we need to be sure that values are unicode, else they won't be pickled correctly + # with D-Bus + 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) @@ -120,32 +131,45 @@ @defer.inlineCallbacks def create(self, **kwargs): - ur"""create an invitation + 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-[random UUID]@domain.tld") (note that this UUID is not the - same as the invitation one, as jid can be used publicly (leaking the UUID), and invitation UUID give access to account. - 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) + 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-[random UUID]@domain.tld") (note that this UUID is not the + same as the invitation one, as jid can be used publicly (leaking the + UUID), and invitation UUID give access to account. + 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 will be set as profile identity if present 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 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) + 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) + 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 the invitation email + /!\ 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 the 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 @@ -169,7 +193,8 @@ ## initial checks extra = kwargs.pop('extra', {}) if set(kwargs).intersection(extra): - raise ValueError(_(u"You can't use following key(s) in both args and extra: {}").format( + raise ValueError( + _(u"You can't use following key(s) in both args and extra: {}").format( u', '.join(set(kwargs).intersection(extra)))) self.checkExtra(extra) @@ -177,10 +202,14 @@ email = kwargs.pop(u'email', None) emails_extra = kwargs.pop(u'emails_extra', []) if not email and emails_extra: - raise ValueError(_(u'You need to provide a main email address before using emails_extra')) + raise ValueError( + _(u'You need to provide a main email address before using emails_extra')) - if email is not None and not 'url_template' in kwargs and not 'message_body' in kwargs: - raise ValueError(_(u"You need to provide url_template if you use default message body")) + if (email is not None + and not 'url_template' in kwargs + and not 'message_body' in kwargs): + raise ValueError( + _(u"You need to provide url_template if you use default message body")) ## uuid log.info(_(u"creating an invitation")) @@ -195,8 +224,8 @@ # 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. + # when the invitee is connecting (e.g. with URL). This key would not be + # saved and could be used to encrypt profile password. extra[KEY_PASSWORD] = password jid_ = kwargs.pop(u'jid_', None) @@ -205,10 +234,12 @@ 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=shortuuid.uuid(), domain=domain) + jid_ = u"invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(), + 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 + # 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: @@ -218,9 +249,11 @@ 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()))) + log.info(_(u"requested jid already exists, trying with {}".format( + jid_.full()))) try: - yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password) + yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, + password) except error.StanzaError as e: idx += 1 else: @@ -236,8 +269,10 @@ # 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) + 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) name = kwargs.pop(u'name', None) if name is not None: extra[u'name'] = name @@ -288,7 +323,8 @@ yield sat_email.sendEmail( self.host, [email] + emails_extra, - (kwargs.pop(u'message_subject', None) or DEFAULT_SUBJECT).format(**format_args), + (kwargs.pop(u'message_subject', None) or DEFAULT_SUBJECT).format( + **format_args), (kwargs.pop(u'message_body', None) or DEFAULT_BODY).format(**format_args), ) @@ -312,7 +348,8 @@ return self.invitations[id_] def _modify(self, id_, new_extra, replace): - return self.modify(id_, {unicode(k): unicode(v) for k,v in new_extra.iteritems()}, replace) + return self.modify(id_, {unicode(k): unicode(v) for k,v in new_extra.iteritems()}, + replace) def modify(self, id_, new_extra, replace=False): """Modify invitation data @@ -366,6 +403,7 @@ """ invitations = yield self.invitations.items() if profile != C.PROF_KEY_NONE: - invitations = {id_:data for id_, data in invitations.iteritems() if data.get(u'profile') == profile} + invitations = {id_:data for id_, data in invitations.iteritems() + if data.get(u'profile') == profile} defer.returnValue(invitations) diff -r 695fc440c3b8 -r 90146552cde5 sat/plugins/plugin_xep_0329.py --- a/sat/plugins/plugin_xep_0329.py Sun Apr 14 08:21:51 2019 +0200 +++ b/sat/plugins/plugin_xep_0329.py Sun Apr 14 08:21:51 2019 +0200 @@ -59,7 +59,7 @@ class ShareNode(object): - """node containing directory or files to share, virtual or real""" + """Node containing directory or files to share, virtual or real""" host = None @@ -112,7 +112,7 @@ return self.children.itervalues() def getOrCreate(self, name, type_=TYPE_VIRTUAL, access=None): - """get a node or create a virtual node and return it""" + """Get a node or create a virtual node and return it""" if access is None: access = {C.ACCESS_PERM_READ: {KEY_TYPE: C.ACCESS_TYPE_PUBLIC}} try: @@ -157,11 +157,12 @@ def checkPermissions( self, client, peer_jid, perms=(C.ACCESS_PERM_READ,), check_parents=True ): - """check that peer_jid can access this node and all its parents + """Check that peer_jid can access this node and all its parents @param peer_jid(jid.JID): entrity trying to access the node @param perms(unicode): permissions to check, iterable of C.ACCESS_PERM_* - @param check_parents(bool): if True, access of all parents of this node will be checked too + @param check_parents(bool): if True, access of all parents of this node will be + checked too @return (bool): True if entity can access this node """ peer_jid = peer_jid.userhostJID() @@ -191,11 +192,9 @@ path_elts = filter(None, path.split(u"/")) if u".." in path_elts: - log.warning( - _( - u'parent dir ("..") found in path, hack attempt? path is {path} [{profile}]' - ).format(path=path, profile=client.profile) - ) + log.warning(_( + u'parent dir ("..") found in path, hack attempt? path is {path} ' + u'[{profile}]').format(path=path, profile=client.profile)) raise exceptions.PermissionError(u"illegal path elements") if not path_elts: @@ -312,9 +311,10 @@ def _fileSendingRequestTrigger( self, client, session, content_data, content_name, file_data, file_elt ): - """this trigger check that a requested file is available, and fill suitable data if so + """This trigger check that a requested file is available, and fill suitable data - path and name are used to retrieve the file. If path is missing, we try our luck with known names + Path and name are used to retrieve the file. If path is missing, we try our luck + with known names """ if client.is_component: return True, None @@ -423,7 +423,8 @@ ) query_elt.addChild(file_elt) - # we don't specify hash as it would be too resource intensive to calculate it for all files + # we don't specify hash as it would be too resource intensive to calculate + # it for all files. # we add file to name_data, so users can request it later name_data = client._XEP_0329_names_data.setdefault(name, {}) if path not in name_data: @@ -547,7 +548,9 @@ """retrieve files from local files repository according to permissions result stanza is then built and sent to requestor - @trigger XEP-0329_compGetFilesFromNode(client, iq_elt, owner, node_path, files_data): can be used to add data/elements + @trigger XEP-0329_compGetFilesFromNode(client, iq_elt, owner, node_path, + files_data): + can be used to add data/elements """ peer_jid = jid.JID(iq_elt["from"]) try: @@ -660,7 +663,8 @@ node = client._XEP_0329_root_node node_type = TYPE_PATH if os.path.isfile(path): - #  we have a single file, the workflow is diferrent as we store all single files in the same dir + # we have a single file, the workflow is diferrent as we store all single + # files in the same dir node = node.getOrCreate(SINGLE_FILES_DIR) if not name: @@ -675,13 +679,9 @@ idx += 1 new_name = name + "_" + unicode(idx) name = new_name - log.info( - _( - u"A directory with this name is already shared, renamed to {new_name} [{profile}]".format( - new_name=new_name, profile=client.profile - ) - ) - ) + log.info(_( + u"A directory with this name is already shared, renamed to {new_name} " + u"[{profile}]".format( new_name=new_name, profile=client.profile))) ShareNode(name=name, parent=node, type_=node_type, access=access, path=path) self.host.bridge.FISSharedPathNew(path, name, client.profile)