From 030ec5d7798ec9ef8b2a0e9531777313f1029e80 Mon Sep 17 00:00:00 2001 From: Andrea Spacca Date: Wed, 21 Apr 2021 10:06:58 +0200 Subject: [PATCH] New bots (giphy, gfycat, tautulli), changes for upload and send image with url or blob content --- Pipfile | 4 + bot.py | 93 ++++++++++++++------ modules/apod.py | 12 +-- modules/gfycat.py | 100 +++++++++++++++++++++ modules/giphy.py | 45 ++++++++++ modules/tautulli.py | 208 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 427 insertions(+), 35 deletions(-) create mode 100644 modules/gfycat.py create mode 100644 modules/giphy.py create mode 100644 modules/tautulli.py diff --git a/Pipfile b/Pipfile index aed25da..b119757 100644 --- a/Pipfile +++ b/Pipfile @@ -20,6 +20,10 @@ wolframalpha = "*" Mastodon-py = "*" pycups = "*" pygithub = "*" +pillow = "*" +giphypop = "*" +tzlocal = "*" +nest_asyncio = "*" [dev-packages] pylint = "*" diff --git a/bot.py b/bot.py index bd0031f..e7900ca 100755 --- a/bot.py +++ b/bot.py @@ -17,6 +17,8 @@ import logging import logging.config import datetime from importlib import reload +from io import BytesIO +from PIL import Image import requests from nio import AsyncClient, InviteEvent, JoinError, RoomMessageText, MatrixRoom, LoginError, RoomMemberEvent, RoomVisibility, RoomPreset, RoomCreateError, RoomResolveAliasResponse, UploadError, UploadResponse @@ -71,6 +73,53 @@ class Bot: self.logger.debug("Logger initialized") + async def upload_and_send_image(self, room, url, text=None, blob=False, blob_content_type="image/png"): + matrix_uri, mimetype, w, h, size = await self.upload_image(url, blob, blob_content_type) + + if not text and not blob: + text = f"{url}" + + if matrix_uri is not None: + await self.send_image(room, matrix_uri, text, mimetype, w, h, size) + else: + await self.send_text(room, "sorry. something went wrong uploading the image to matrix server :(") + + # Helper function to upload a image from URL to homeserver. Use send_image() to actually send it to room. + async def upload_image(self, url, blob=False, blob_content_type="image/png"): + self.client: AsyncClient + response: UploadResponse + if blob: + (response, alist) = await self.client.upload(lambda a, b: url, blob_content_type) + i = Image.open(BytesIO(url)) + image_length = len(url) + content_type = blob_content_type + else: + self.logger.debug(f"start downloading image from url {url}") + headers = {'User-Agent': 'Mozilla/5.0'} + url_response = requests.get(url, headers=headers) + self.logger.debug(f"response [status_code={url_response.status_code}, headers={url_response.headers}") + + if url_response.status_code == 200: + content_type = url_response.headers.get("content-type") + self.logger.info(f"uploading content to matrix server [size={len(url_response.content)}, content-type: {content_type}]") + (response, alist) = await self.client.upload(lambda a, b: url_response.content, content_type) + self.logger.debug("response: %s", response) + i = Image.open(BytesIO(url_response.content)) + image_length = len(url_response.content) + else: + self.logger.error("unable to request url: %s", url_response) + + return None, None, None, None + + if isinstance(response, UploadResponse): + self.logger.info("uploaded file to %s", response.content_uri) + return response.content_uri, content_type, i.size[0], i.size[1], image_length + else: + response: UploadError + self.logger.error("unable to upload file. msg: %s", response.message) + + return None, None, None, None + async def send_text(self, room, body, msgtype="m.notice", bot_ignore=False): msg = { "body": body, @@ -92,7 +141,7 @@ class Bot: msg["org.vranki.hemppa.ignore"] = "true" await self.client.room_send(room.room_id, 'm.room.message', msg) - async def send_image(self, room, url, body): + async def send_image(self, room, url, body, mimetype=None, width=None, height=None, size=None): """ :param room: A MatrixRoom the image should be send to @@ -103,8 +152,21 @@ class Bot: msg = { "url": url, "body": body, - "msgtype": "m.image" + "msgtype": "m.image", + "info": { + "thumbnail_info": None, + "thumbnail_url": None, + }, } + if mimetype: + msg["info"]["mimetype"] = mimetype + if width: + msg["info"]["w"] = width + if height: + msg["info"]["h"] = height + if size: + msg["info"]["size"] = size + await self.client.room_send(room.room_id, 'm.room.message', msg) async def send_msg(self, mxid, roomname, message): @@ -181,33 +243,6 @@ class Bot: def should_ignore_event(self, event): return "org.vranki.hemppa.ignore" in event.source['content'] - # Helper function to upload a image from URL to homeserver. Use send_image() to actually send it to room. - async def upload_image(self, url): - self.client: AsyncClient - response: UploadResponse - - self.logger.debug(f"start downloading image from url {url}") - url_response = requests.get(url) - self.logger.debug(f"response [status_code={url_response.status_code}, headers={url_response.headers}") - - if url_response.status_code == 200: - content_type = url_response.headers.get("content-type") - self.logger.info(f"uploading content to matrix server [size={len(url_response.content)}, content-type: {content_type}]") - (response, alist) = await self.client.upload(lambda a, b: url_response.content, content_type) - self.logger.debug("response: %s", response) - - if isinstance(response, UploadResponse): - self.logger.info("uploaded file to %s", response.content_uri) - return response.content_uri - else: - response: UploadError - self.logger.error("unable to upload file. msg: %s", response.message) - else: - self.logger.error("unable to request url: %s", url_response) - - return None - - def save_settings(self): module_settings = dict() for modulename, moduleobject in self.modules.items(): diff --git a/modules/apod.py b/modules/apod.py index 4468cda..040b475 100644 --- a/modules/apod.py +++ b/modules/apod.py @@ -92,20 +92,20 @@ class MatrixModule(BotModule): await bot.send_text(room, f"{apod.explanation} || date: {apod.date} || original-url: {apod.url}") async def upload_and_send_image(self, room, bot, apod): + send_again = True + await bot.send_text(room, f"{apod.title} ({apod.date})") if apod.date in self.matrix_uri_cache: matrix_uri = self.matrix_uri_cache.get(apod.date) self.logger.debug(f"already uploaded picture {matrix_uri} for date {apod.date}") else: - matrix_uri = await bot.upload_image(apod.hdurl) - if matrix_uri is None: - self.logger.warning("unable to upload hdurl. try url next.") - matrix_uri = await bot.upload_image(apod.url) + matrix_uri = await bot.upload_and_send_image(room, apod.hdurl, f"{apod.title}") + send_again = False - await bot.send_text(room, f"{apod.title} ({apod.date})") if matrix_uri is not None: self.matrix_uri_cache[apod.date] = matrix_uri bot.save_settings() - await bot.send_image(room, matrix_uri, f"{apod.title}") + if send_again: + await bot.send_image(room, matrix_uri, f"{apod.title}") else: await bot.send_text(room, "Sorry. Something went wrong uploading the image to Matrix server :(") await bot.send_text(room, f"{apod.explanation}") diff --git a/modules/gfycat.py b/modules/gfycat.py new file mode 100644 index 0000000..52efdbc --- /dev/null +++ b/modules/gfycat.py @@ -0,0 +1,100 @@ +import urllib.request +import urllib.parse +import urllib.error + +import requests +from nio import AsyncClient, UploadError +from nio import UploadResponse + +from collections import namedtuple +from modules.common.module import BotModule + +class gfycat(object): + """ + A very simple module that allows you to + 1. search a gif on gfycat from a remote location + """ + + # Urls + url = "https://api.gfycat.com" + + def __init__(self): + super(gfycat, self).__init__() + + def __fetch(self, url, param): + import json + try: + # added simple User-Ajent string to avoid CloudFlare block this request + headers = {'User-Agent': 'Mozilla/5.0'} + req = urllib.request.Request(url+param, headers=headers) + connection = urllib.request.urlopen(req).read() + except urllib.error.HTTPError as err: + raise ValueError(err.read()) + result = namedtuple("result", "raw json") + return result(raw=connection, json=json.loads(connection)) + + def search(self, param): + result = self.__fetch(self.url, "/v1/gfycats/search?search_text=%s" % urllib.parse.quote_plus(param)) + if "errorMessage" in result.json: + raise ValueError("%s" % self.json["errorMessage"]) + return _gfycatSearch(result) + +class _gfycatUtils(object): + + """ + A utility class that provides the necessary common + for all the other classes + """ + + def __init__(self, param, json): + super(_gfycatUtils, self).__init__() + # This can be used for other functions related to this class + self.res = param + self.js = json + + def raw(self): + return self.res.raw + + def json(self): + return self.js + + def __len__(self): + return len(self.js) + + def get(self, what): + try: + return self.js[what] + except KeyError as error: + return ("Sorry, can't find %s" % error) + +class _gfycatSearch(_gfycatUtils): + + """ + This class will provide more information for an existing url + """ + + def __init__(self, param): + super(_gfycatSearch, self).__init__(param, param.json["gfycats"]) + +class MatrixModule(BotModule): + async def matrix_message(self, bot, room, event): + args = event.body.split() + if len(args) > 1: + gif_url = "No image found" + query = event.body[len(args[0])+1:] + try: + gifs = gfycat().search(query) + if len(gifs) < 1: + await bot.send_text(room, gif_url) + return + + gif_url = gifs.get(0)["content_urls"]["largeGif"]["url"] + await bot.upload_and_send_image(room, gif_url) + except Exception as exc: + gif_url = str(exc) + await bot.send_text(room, gif_url) + else: + await bot.send_text(room, 'Usage: !gfycat ') + + def help(self): + return ('Gfycat bot') diff --git a/modules/giphy.py b/modules/giphy.py new file mode 100644 index 0000000..511c7dd --- /dev/null +++ b/modules/giphy.py @@ -0,0 +1,45 @@ +import urllib.request +import urllib.parse +import urllib.error + +import os +import giphypop +import requests +from nio import AsyncClient, UploadError +from nio import UploadResponse + +from collections import namedtuple +from modules.common.module import BotModule + +class MatrixModule(BotModule): + async def matrix_message(self, bot, room, event): + args = event.body.split() + if len(args) > 1: + gif_url = "No image found" + query = event.body[len(args[0])+1:] + try: + g = giphypop.Giphy(api_key=os.getenv("GIPHY_API_KEY")) + gifs = [] + try: + for x in g.search(phrase=query, limit=1): + gifs.append(x) + except Exception: + pass + if len(gifs) < 1: + await bot.send_text(room, gif_url) + return + + gif_url = gifs[0].media_url + await bot.upload_and_send_image(room, gif_url) + return + except Exception as exc: + gif_url = str(exc) + await bot.send_text(room, gif_url) + else: + await bot.send_text(room, 'Usage: !giphy ') + + def set_settings(self, data): + super().set_settings(data) + + def help(self): + return ('Giphy bot') diff --git a/modules/tautulli.py b/modules/tautulli.py new file mode 100644 index 0000000..fb28a47 --- /dev/null +++ b/modules/tautulli.py @@ -0,0 +1,208 @@ +import urllib.request +import urllib.parse +import urllib.error +import tzlocal +import pytz +import os +import sys +import json + +import importlib +from importlib import reload + +from nio import MatrixRoom + +from aiohttp import web +import asyncio +import nest_asyncio +nest_asyncio.apply() + +from modules.common.module import BotModule + +tautulli_path = os.getenv("TAUTULLI_PATH") + +def load_tautulli(): + try: + global tautulli_path + + sys.path.append(tautulli_path) + sys.path.append("{}/lib".format(tautulli_path)) + module = importlib.import_module("plexpy") + module = reload(module) + return module + except ModuleNotFoundError: + return None + +plexpy = load_tautulli() + +async def send_entry(bot, room, entry): + if "art" in entry: + global plexpy + if plexpy: + pms = plexpy.pmsconnect.PmsConnect() + (image0, image1) = pms.get_image(entry["art"], 600, 300) + matrix_uri = await bot.upload_and_send_image(room, image0, "", True, image1) + + if matrix_uri is not None: + await bot.send_image(room, matrix_uri, "") + + fmt_params = { + "title": entry["title"], + "year": entry["year"], + "audience_rating": entry["audience_rating"], + "directors": ", ".join(entry["directors"]), + "actors": ", ".join(entry["actors"]), + "summary": entry["summary"], + "tagline": entry["tagline"], + "genres": ", ".join(entry["genres"]) + } + + await bot.send_html(room, + msg_template_html.format(**fmt_params), + msg_template_plain.format(**fmt_params)) + +msg_template_html = """ + {title} -({year})- Rating: {audience_rating}
+ Director(s): {directors}
+ Actors: {actors}
+ {summary}
+ {tagline}
+ Genre(s): {genres}

""" + +msg_template_plain = """*{title} -({year})- Rating: {audience_rating}* + Director(s): {directors} + Actors: {actors} + {summary} + {tagline} + Genre(s): {genres} + +""" + +class WebServer: + bot = None + rooms = dict() + + def __init__(self, host, port): + self.app = web.Application() + self.host = host + self.port = port + self.app.router.add_post('/notify', self.notify) + + async def run(self): + if not self.host or not self.port: + return + + loop = asyncio.get_event_loop() + runner = web.AppRunner(self.app) + loop.run_until_complete(runner.setup()) + site = web.TCPSite(runner, host=self.host, port=self.port) + loop.run_until_complete(site.start()) + + async def notify(self, request: web.Request) -> web.Response: + try: + data = await request.json() + if "genres" in data: + data["genres"] = data["genres"].split(",") + + if "actors" in data: + data["actors"] = data["actors"].split(",") + + if "directors" in data: + data["directors"] = data["directors"].split(",") + + for room_id in self.rooms: + room = MatrixRoom(room_id=room_id, own_user_id=os.getenv("BOT_OWNERS"), encrypted=rooms[room_id]) + await send_entry(self.bot, room, data) + + except Exception as exc: + message = str(exc) + return web.HTTPBadRequest(body=message) + + return web.Response() + +class MatrixModule(BotModule): + httpd = None + rooms = dict() + + def __init__(self, name): + super().__init__(name) + global plexpy + if plexpy: + global tautulli_path + plexpy.FULL_PATH = "{}/Tautulli.py".format(tautulli_path) + plexpy.PROG_DIR = os.path.dirname(plexpy.FULL_PATH) + plexpy.DATA_DIR = tautulli_path + plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, plexpy.database.FILENAME) + + try: + plexpy.SYS_TIMEZONE = tzlocal.get_localzone() + except (pytz.UnknownTimeZoneError, LookupError, ValueError) as e: + plexpy.SYS_TIMEZONE = pytz.UTC + + plexpy.initialize("{}/config.ini".format(tautulli_path)) + + self.httpd = WebServer(os.getenv("TAUTULLI_NOTIFIER_ADDR"), os.getenv("TAUTULLI_NOTIFIER_PORT")) + loop = asyncio.get_event_loop() + loop.run_until_complete(self.httpd.run()) + + def matrix_start(self, bot): + super().matrix_start(bot) + self.httpd.bot = bot + + async def matrix_message(self, bot, room, event): + args = event.body.split() + if len(args) == 2: + media_type = args[1] + if media_type != "movie" and media_type != "show" and media_type != "artist": + await bot.send_text(room, "media type '%s' provided not valid" % media_type) + return + + try: + url = "{}/api/v2?apikey={}&cmd=get_recently_added&count=10".format(os.getenv("TAUTULLI_URL"), os.getenv("TAUTULLI_API_KEY")) + req = urllib.request.Request(url+"&media_type="+media_type) + connection = urllib.request.urlopen(req).read() + entries = json.loads(connection) + if "response" not in entries and "data" not in entries["response"] and "recently_added" not in entries["response"]["data"]: + await bot.send_text(room, "no recently added for %s" % media_type) + return + + for entry in entries["response"]["data"]["recently_added"]: + await send_entry(bot, room, entry) + + except urllib.error.HTTPError as err: + raise ValueError(err.read()) + except Exception as exc: + message = str(exc) + await bot.send_text(room, message) + elif len(args) == 4: + if args[1] == "add" or args[1] == "remove": + room_id = args[2] + encrypted = args[3] + if args[1] == "add": + self.rooms[room_id] = encrypted == "encrypted" + await bot.send_text(room, f"Added {room_id} to rooms notification list") + else: + del self.rooms[room_id] + await bot.send_text(room, f"Removed {room_id} to rooms notification list") + + bot.save_settings() + self.httpd.rooms = self.rooms + else: + await bot.send_text(room, 'Usage: !tautulli | %room_id% %encrypted%') + else: + await bot.send_text(room, 'Usage: !tautulli | %room_id% %encrypted%') + + def get_settings(self): + data = super().get_settings() + data["rooms"] = self.rooms + self.httpd.rooms = self.rooms + return data + + def set_settings(self, data): + super().set_settings(data) + if data.get("rooms"): + self.rooms = data["rooms"] + self.httpd.rooms = self.rooms + + def help(self): + return ('Tautulli recently added bot')