diff --git a/docs/api/plugins/xep_0363.rst b/docs/api/plugins/xep_0363.rst index ebbfdba1..4bbf95fa 100644 --- a/docs/api/plugins/xep_0363.rst +++ b/docs/api/plugins/xep_0363.rst @@ -8,6 +8,12 @@ XEP-0363: HTTP File Upload :members: :exclude-members: session_bind, plugin_init, plugin_end +.. autoclass:: UploadServiceNotFound + +.. autoclass:: FileTooBig + +.. autoclass:: HTTPError + Stanza elements --------------- diff --git a/slixmpp/plugins/xep_0363/__init__.py b/slixmpp/plugins/xep_0363/__init__.py index 0ed1d7c8..f693eb92 100644 --- a/slixmpp/plugins/xep_0363/__init__.py +++ b/slixmpp/plugins/xep_0363/__init__.py @@ -1,4 +1,3 @@ - # slixmpp: The Slick XMPP Library # Copyright (C) 2018 Emmanuel Gil Peyrot # This file is part of slixmpp. @@ -6,6 +5,12 @@ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0363.stanza import Request, Slot, Put, Get, Header -from slixmpp.plugins.xep_0363.http_upload import XEP_0363 +from slixmpp.plugins.xep_0363.http_upload import ( + XEP_0363, + UploadServiceNotFound, + FileTooBig, + HTTPError, + FileUploadError, +) register_plugin(XEP_0363) diff --git a/slixmpp/plugins/xep_0363/http_upload.py b/slixmpp/plugins/xep_0363/http_upload.py index 04b066cd..bae3ee7d 100644 --- a/slixmpp/plugins/xep_0363/http_upload.py +++ b/slixmpp/plugins/xep_0363/http_upload.py @@ -1,18 +1,21 @@ -""" - slixmpp: The Slick XMPP Library - Copyright (C) 2018 Emmanuel Gil Peyrot - This file is part of slixmpp. - - See the file LICENSE for copying permission. -""" +# slixmpp: The Slick XMPP Library +# Copyright (C) 2018 Emmanuel Gil Peyrot +# This file is part of slixmpp. +# See the file LICENSE for copying permission. import logging import os.path from aiohttp import ClientSession +from asyncio import Future from mimetypes import guess_type +from typing import ( + Optional, + IO, +) -from slixmpp import Iq, __version__ +from slixmpp import JID, __version__ +from slixmpp.stanza import Iq from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback @@ -25,19 +28,39 @@ class FileUploadError(Exception): pass class UploadServiceNotFound(FileUploadError): - pass + """ + Raised if no upload service can be found. + """ class FileTooBig(FileUploadError): + """ + Raised if the file size is above advertised server limits. + + args: + + - size of the file + - max file size allowed + """ def __str__(self): return 'File size too large: {} (max: {} bytes)' \ .format(self.args[0], self.args[1]) class HTTPError(FileUploadError): + """ + Raised when we receive an HTTP error response during upload. + + args: + + - HTTP Error code + - Content of the HTTP response + """ def __str__(self): return 'Could not upload file: %d (%s)' % (self.args[0], self.args[1]) class XEP_0363(BasePlugin): - ''' This plugin only supports Python 3.5+ ''' + """ + XEP-0363: HTTP File Upload + """ name = 'xep_0363' description = 'XEP-0363: HTTP File Upload' @@ -62,9 +85,7 @@ class XEP_0363(BasePlugin): self._handle_request)) def plugin_end(self): - self._http_session.close() self.xmpp.remove_handler('HTTP Upload Request') - self.xmpp.remove_handler('HTTP Upload Slot') self.xmpp['xep_0030'].del_feature(feature=Request.namespace) def session_bind(self, jid): @@ -73,9 +94,14 @@ class XEP_0363(BasePlugin): def _handle_request(self, iq): self.xmpp.event('http_upload_request', iq) - async def find_upload_service(self, domain=None, timeout=None): + async def find_upload_service(self, domain: Optional[JID] = None, **iqkwargs) -> Optional[Iq]: + """Find an upload service on a domain (our own by default). + + :param domain: Domain to disco to find a service. + """ results = await self.xmpp['xep_0030'].get_info_from_domain( - domain=domain, timeout=timeout) + domain=domain, **iqkwargs + ) candidates = [] for info in results: @@ -87,26 +113,49 @@ class XEP_0363(BasePlugin): if feature == Request.namespace: return info - def request_slot(self, jid, filename, size, content_type=None, ifrom=None, - timeout=None, callback=None, timeout_callback=None): - iq = self.xmpp.Iq() - iq['to'] = jid - iq['from'] = ifrom - iq['type'] = 'get' + def request_slot(self, jid: JID, filename: str, size: int, + content_type: Optional[str] = None, *, + ifrom: Optional[JID] = None, **iqkwargs) -> Future: + """Request an HTTP upload slot from a service. + + :param jid: Service to request the slot from. + :param filename: Name of the file that will be uploaded. + :param size: size of the file in bytes. + :param content_type: Type of the file that will be uploaded. + """ + iq = self.xmpp.make_iq_get(ito=jid, ifrom=ifrom) request = iq['http_upload_request'] request['filename'] = filename request['size'] = str(size) request['content-type'] = content_type or self.default_content_type - return iq.send(timeout=timeout, callback=callback, - timeout_callback=timeout_callback) + return iq.send(**iqkwargs) - async def upload_file(self, filename, size=None, content_type=None, *, - input_file=None, ifrom=None, domain=None, timeout=None, - callback=None, timeout_callback=None): - ''' Helper function which does all of the uploading process. ''' + async def upload_file(self, filename: str, size: Optional[int] = None, + content_type: Optional[str] = None, *, + input_file: Optional[IO[bytes]]=None, + domain: Optional[JID] = None, + **iqkwargs) -> str: + '''Helper function which does all of the uploading discovery and + process. + + :param filename: Path to the file to upload (or only the name if + ``input_file`` is provided. + :param size: size of the file in bytes. + :param content_type: Type of the file that will be uploaded. + :param input_file: Binary file stream on the file. + :param domain: Domain to query to find an HTTP upload service. + :raises .UploadServiceNotFound: If slixmpp is unable to find an + an available upload service. + :raises .FileTooBig: If the filesize is above what is accepted by + the service. + :raises .HTTPError: If there is an error in the HTTP operation. + :returns: The URL of the uploaded file. + ''' + timeout = iqkwargs.get('timeout', None) if self.upload_service is None: info_iq = await self.find_upload_service( - domain=domain, timeout=timeout) + domain=domain, **iqkwargs + ) if info_iq is None: raise UploadServiceNotFound() self.upload_service = info_iq['from'] @@ -137,9 +186,7 @@ class XEP_0363(BasePlugin): basename = os.path.basename(filename) slot_iq = await self.request_slot(self.upload_service, basename, size, - content_type, ifrom, timeout, - callback=callback, - timeout_callback=timeout_callback) + content_type, **iqkwargs) slot = slot_iq['http_upload_slot'] headers = {