""" web.py Contains functions for interacting with web services. Created by: - Bjorn Neergaard <https://github.com/neersighted> Maintainer: - Luke Rogers <https://github.com/lukeroge> License: GPL v3 """ import json import logging import time from operator import attrgetter from typing import Optional import requests from requests import RequestException, Response, PreparedRequest, HTTPError # Constants DEFAULT_SHORTENER = 'is.gd' DEFAULT_PASTEBIN = '' HASTEBIN_SERVER = 'https://hastebin.com' logger = logging.getLogger('cloudbot') # Shortening / pasting # Public API class Registry: class Item: def __init__(self, item): self.item = item self.working = True self.last_check = 0 self.uses = 0 def failed(self): self.working = False self.last_check = time.time() @property def should_use(self): if self.working: return True if (time.time() - self.last_check) > (5*60): # It's been 5 minutes, try again self.working = True return True return False def __init__(self): self._items = {} def register(self, name, item): if name in self._items: raise ValueError("Attempt to register duplicate item") self._items[name] = self.Item(item) def get(self, name): val = self._items.get(name) if val: return val.item return val def get_item(self, name): return self._items.get(name) def get_working(self) -> Optional['Item']: working = [ item for item in self._items.values() if item.should_use ] if not working: return None return min(working, key=attrgetter('uses')) def remove(self, name): del self._items[name] def items(self): return self._items.items() def __iter__(self): return iter(self._items) def __getitem__(self, item): return self._items[item].item def set_working(self): for item in self._items.values(): item.working = True def shorten(url, custom=None, key=None, service=DEFAULT_SHORTENER): impl = shorteners[service] return impl.shorten(url, custom, key) def try_shorten(url, custom=None, key=None, service=DEFAULT_SHORTENER): impl = shorteners[service] return impl.try_shorten(url, custom, key) def expand(url, service=None): if service: impl = shorteners[service] else: impl = None for name in shorteners: if name in url: impl = shorteners[name] break if impl is None: impl = Shortener() return impl.expand(url) class NoPasteException(Exception): """No pastebins succeeded""" def paste(data, ext='txt', service=DEFAULT_PASTEBIN, raise_on_no_paste=False): if service: impl = pastebins.get_item(service) else: impl = pastebins.get_working() if not impl: pastebins.set_working() impl = pastebins.get_working() while impl: try: out = impl.item.paste(data, ext) impl.uses += 1 return out except ServiceError: impl.failed() logger.exception("Paste failed") impl = pastebins.get_working() if raise_on_no_paste: raise NoPasteException("Unable to paste data") return "Unable to paste data" class ServiceError(Exception): def __init__(self, request: PreparedRequest, message: str): super().__init__(message) self.request = request class ServiceHTTPError(ServiceError): def __init__(self, message: str, response: Response): super().__init__( response.request, '[HTTP {}] {}'.format(response.status_code, message) ) self.message = message self.response = response class Shortener: def __init__(self): pass def shorten(self, url, custom=None, key=None): return url def try_shorten(self, url, custom=None, key=None): try: return self.shorten(url, custom, key) except ServiceError: return url def expand(self, url): try: r = requests.get(url, allow_redirects=False) r.raise_for_status() except HTTPError as e: r = e.response raise ServiceHTTPError(r.reason, r) from e except RequestException as e: raise ServiceError(e.request, "Connection error occurred") from e if 'location' in r.headers: return r.headers['location'] raise ServiceHTTPError('That URL does not exist', r) class Pastebin: def __init__(self): pass def paste(self, data, ext): raise NotImplementedError shorteners = Registry() pastebins = Registry() # Internal Implementations class Isgd(Shortener): def shorten(self, url, custom=None, key=None): p = {'url': url, 'shorturl': custom, 'format': 'json'} try: r = requests.get('http://is.gd/create.php', params=p) r.raise_for_status() except HTTPError as e: r = e.response raise ServiceHTTPError(r.reason, r) from e except RequestException as e: raise ServiceError(e.request, "Connection error occurred") from e j = r.json() if 'shorturl' in j: return j['shorturl'] raise ServiceHTTPError(j['errormessage'], r) def expand(self, url): p = {'shorturl': url, 'format': 'json'} try: r = requests.get('http://is.gd/forward.php', params=p) r.raise_for_status() except HTTPError as e: r = e.response raise ServiceHTTPError(r.reason, r) from e except RequestException as e: raise ServiceError(e.request, "Connection error occurred") from e j = r.json() if 'url' in j: return j['url'] raise ServiceHTTPError(j['errormessage'], r) class Googl(Shortener): def shorten(self, url, custom=None, key=None): h = {'content-type': 'application/json'} k = {'key': key} p = {'longUrl': url} try: r = requests.post('https://www.googleapis.com/urlshortener/v1/url', params=k, data=json.dumps(p), headers=h) r.raise_for_status() except HTTPError as e: r = e.response raise ServiceHTTPError(r.reason, r) from e except RequestException as e: raise ServiceError(e.request, "Connection error occurred") from e j = r.json() if 'error' not in j: return j['id'] raise ServiceHTTPError(j['error']['message'], r) def expand(self, url): p = {'shortUrl': url} try: r = requests.get('https://www.googleapis.com/urlshortener/v1/url', params=p) r.raise_for_status() except HTTPError as e: r = e.response raise ServiceHTTPError(r.reason, r) from e except RequestException as e: raise ServiceError(e.request, "Connection error occurred") from e j = r.json() if 'error' not in j: return j['longUrl'] raise ServiceHTTPError(j['error']['message'], r) class Gitio(Shortener): def shorten(self, url, custom=None, key=None): p = {'url': url, 'code': custom} try: r = requests.post('http://git.io', data=p) r.raise_for_status() except HTTPError as e: r = e.response raise ServiceHTTPError(r.reason, r) from e except RequestException as e: raise ServiceError(e.request, "Connection error occurred") from e if r.status_code == requests.codes.created: s = r.headers['location'] if custom and custom not in s: raise ServiceHTTPError('That URL is already in use', r) return s raise ServiceHTTPError(r.text, r) class Hastebin(Pastebin): def __init__(self, base_url): super().__init__() self.url = base_url def paste(self, data, ext): if isinstance(data, str): encoded = data.encode() else: encoded = data try: r = requests.post(self.url + '/documents', data=encoded) r.raise_for_status() except HTTPError as e: r = e.response raise ServiceHTTPError(r.reason, r) from e except RequestException as e: raise ServiceError(e.request, "Connection error occurred") from e else: j = r.json() if r.status_code is requests.codes.ok: return '{}/{}.{}'.format(self.url, j['key'], ext) raise ServiceHTTPError(j['message'], r) pastebins.register('hastebin', Hastebin(HASTEBIN_SERVER)) shorteners.register('git.io', Gitio()) shorteners.register('goo.gl', Googl()) shorteners.register('is.gd', Isgd())