From 030ec5d7798ec9ef8b2a0e9531777313f1029e80 Mon Sep 17 00:00:00 2001 From: Andrea Spacca Date: Wed, 21 Apr 2021 10:06:58 +0200 Subject: [PATCH 1/5] 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') From 4af5825bff7dd195ada327caed5077ba6746c0c5 Mon Sep 17 00:00:00 2001 From: Andrea Spacca Date: Thu, 22 Apr 2021 15:39:44 +0200 Subject: [PATCH 2/5] fix bug --- modules/tautulli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/tautulli.py b/modules/tautulli.py index fb28a47..a570b50 100644 --- a/modules/tautulli.py +++ b/modules/tautulli.py @@ -111,7 +111,7 @@ class WebServer: 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]) + room = MatrixRoom(room_id=room_id, own_user_id=os.getenv("BOT_OWNERS"), encrypted=self.rooms[room_id]) await send_entry(self.bot, room, data) except Exception as exc: From 63035eea8d184b926e9a335b07bb373308d0c34d Mon Sep 17 00:00:00 2001 From: Andrea Spacca Date: Thu, 22 Apr 2021 17:57:55 +0200 Subject: [PATCH 3/5] documentation --- README.md | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ bot.py | 47 ++++++++++++++++++ 2 files changed, 187 insertions(+) diff --git a/README.md b/README.md index c78056f..7278b76 100644 --- a/README.md +++ b/README.md @@ -464,6 +464,77 @@ SVG files are printed as text currently, avoid printing them. This module is disabled by default. +### Giphy + +Can be used to post a picture from giphy given a query string. + +API Key: + +The module has no API Key set up by defualt. You have to provide an api key by setting the environment variable `GIPHY_API_KEY`. + +Read the documentation to create one at https://developers.giphy.com/docs/api + +Environ variables seen by commands: + +* GIPHY_API_KEY: API Key for Giphy access + +Commands: + +* !giphy "query" - Post the first image result from giphy for the given "query" + +Example: + +* !giphy test + +### Gfycat + +Can be used to post a picture from Gfycat given a query string. + +Commands: + +* !gfycat "query" - Post the first image result from gfycat for the given "query" + +Example: + +* !gfycat test + +### Tautulli + +Can be used to fetch recently added information from Tautulli or receive Tautulli recently added notification webhook + +Commands: + +* !tautulli movie - Show the last 10 recently added movies to Plex library monitered by Tautulli +* !tautulli show - Show the last 10 recently added tv series epsiodes to Plex library monitered by Tautulli +* !tautulli artist - Show the last 10 recently added music artists to Plex library monitered by Tautulli +* !tautulli add "room_id" encrypted - Add the provided encrypted "room_id" to the list of rooms to post the recently added notifications received by the webhook +* !tautulli add "room_id" plain - Add the provided unencrypted "room_id" to the list of rooms to post the recently added notifications received by the webhook +* !tautulli remove "room_id" encrypted - Remove the provided encrypted "room_id" to the list of rooms to post the recently added notifications received by the webhook +* !tautulli remove "room_id" plain - Remove the provided unencrypted "room_id" to the list of rooms to post the recently added notifications received by the webhook + +Tautulli instance and API Key: + +The module work with an existing installed instance of Tautulli accessible on the machine at the path defined by env variable `TAUTULLI_PATH` +You have to provide an api key by setting the environment variable `TAUTULLI_API_KEY`. + +Read the documentation to create one at https://developers.giphy.com/docs/api + +Environ variables seen by commands: + +* TAUTULLI_PATH: Path accessible from the machine to the installed instance of Tautulli +* TAUTULLI_URL: Url accessible from the machine to the installed instance of Tautulli +* TAUTULLI_API_KEY: API Key for Tautulli access +* TAUTULLI_NOTIFIER_ADDR: Listening address for the Tautulli webhook handler target +* TAUTULLI_NOTIFIER_PORT: Listening port for the Tautulli webhook handler target +* BOT_OWNERS: Owner of the rooms in the list for the notification webhook + + +Example: + +* !tautulli movie +* !tautulli add !OGEhHVWSdvArJzumhm:matrix.org plain +* !tautulli remove !OGEhHVWSdvArJzumhm:matrix.org plain + ### Github based asset management This module was written for asset (machines, tasks and todo) management by @@ -632,6 +703,75 @@ class MatrixModule(BotModule): You only need to implement the ones you need. See existing bots for examples. +## Bot API +```python +class Bot: + async def send_msg(self, mxid, roomname, message): + """ + + :param mxid: A Matrix user id to send the message to + :param roomname: A Matrix room id to send the message to + :param message: Text to be sent as message + :return bool: Success upon sending the message + """ + + async def send_text(self, room, body, msgtype="m.notice", bot_ignore=False): + """ + + :param room: A MatrixRoom the text should be send to + :param body: Textual content of the message + :param msgtype: The message type for the room https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes + :param bot_ignore: Flag to mark the message to be ignored by the bot + :return: + """ + + async def send_html(self, room, html, plaintext, msgtype="m.notice", bot_ignore=False): + """ + + :param room: A MatrixRoom the html should be send to + :param html: Html content of the message + :param plaintext: Plaintext content of the message + :param msgtype: The message type for the room https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes + :param bot_ignore: Flag to mark the message to be ignored by the bot + :return: + """ + + 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 + :param url: A MXC-Uri https://matrix.org/docs/spec/client_server/r0.6.0#mxc-uri + :param body: A textual representation of the image + :param mimetype: The mimetype of the image + :param width: Width in pixel of the image + :param height: Height in pixel of the image + :param size: Size in bytes of the image + :return: + """ + + async def upload_image(self, url, blob=False, blob_content_type="image/png"): + """ + + :param url: Url of binary content of the image to upload + :param blob: Flag to indicate if the first param is an url or a binary content + :param blob_content_type: Content type of the image in case of binary content + :return: A MXC-Uri https://matrix.org/docs/spec/client_server/r0.6.0#mxc-uri, Content type, Width, Height, Image size in bytes + """ + + + async def upload_and_send_image(self, room, url, text=None, blob=False, blob_content_type="image/png"): + """ + + :param room: A MatrixRoom the image should be send to after uploading + :param url: Url of binary content of the image to upload + :param text: A textual representation of the image + :param blob: Flag to indicate if the second param is an url or a binary content + :param blob_content_type: Content type of the image in case of binary content + :return: + """ + +``` + ### Logging Uses [python logging facility](https://docs.python.org/3/library/logging.html) to print information to the console. Customize it to your needs editing `config/logging.yml`. diff --git a/bot.py b/bot.py index e7900ca..d4bfe48 100755 --- a/bot.py +++ b/bot.py @@ -74,6 +74,15 @@ 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"): + """ + + :param room: A MatrixRoom the image should be send to after uploading + :param url: Url of binary content of the image to upload + :param text: A textual representation of the image + :param blob: Flag to indicate if the second param is an url or a binary content + :param blob_content_type: Content type of the image in case of binary content + :return: + """ matrix_uri, mimetype, w, h, size = await self.upload_image(url, blob, blob_content_type) if not text and not blob: @@ -86,6 +95,14 @@ class Bot: # 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"): + """ + + :param url: Url of binary content of the image to upload + :param blob: Flag to indicate if the first param is an url or a binary content + :param blob_content_type: Content type of the image in case of binary content + :return: A MXC-Uri https://matrix.org/docs/spec/client_server/r0.6.0#mxc-uri, Content type, Width, Height, Image size in bytes + """ + self.client: AsyncClient response: UploadResponse if blob: @@ -121,6 +138,15 @@ class Bot: return None, None, None, None async def send_text(self, room, body, msgtype="m.notice", bot_ignore=False): + """ + + :param room: A MatrixRoom the text should be send to + :param body: Textual content of the message + :param msgtype: The message type for the room https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes + :param bot_ignore: Flag to mark the message to be ignored by the bot + :return: + """ + msg = { "body": body, "msgtype": msgtype, @@ -131,6 +157,16 @@ class Bot: await self.client.room_send(room.room_id, 'm.room.message', msg) async def send_html(self, room, html, plaintext, msgtype="m.notice", bot_ignore=False): + """ + + :param room: A MatrixRoom the html should be send to + :param html: Html content of the message + :param plaintext: Plaintext content of the message + :param msgtype: The message type for the room https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes + :param bot_ignore: Flag to mark the message to be ignored by the bot + :return: + """ + msg = { "msgtype": msgtype, "format": "org.matrix.custom.html", @@ -147,6 +183,10 @@ class Bot: :param room: A MatrixRoom the image should be send to :param url: A MXC-Uri https://matrix.org/docs/spec/client_server/r0.6.0#mxc-uri :param body: A textual representation of the image + :param mimetype: The mimetype of the image + :param width: Width in pixel of the image + :param height: Height in pixel of the image + :param size: Size in bytes of the image :return: """ msg = { @@ -170,6 +210,13 @@ class Bot: await self.client.room_send(room.room_id, 'm.room.message', msg) async def send_msg(self, mxid, roomname, message): + """ + + :param mxid: A Matrix user id to send the message to + :param roomname: A Matrix room id to send the message to + :param message: Text to be sent as message + :return bool: Success upon sending the message + """ # Sends private message to user. Returns true on success. # Find if we already have a common room with user: From e528b9600b679c564f22e88fd7f343bcda2b63f3 Mon Sep 17 00:00:00 2001 From: Andrea Spacca Date: Fri, 23 Apr 2021 11:01:50 +0200 Subject: [PATCH 4/5] cr fix --- README.md | 32 ++++++++++++++------------------ modules/giphy.py | 19 +++++++++++++++++-- modules/tautulli.py | 14 ++++++++++++-- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 7278b76..cdd79eb 100644 --- a/README.md +++ b/README.md @@ -470,17 +470,15 @@ Can be used to post a picture from giphy given a query string. API Key: -The module has no API Key set up by defualt. You have to provide an api key by setting the environment variable `GIPHY_API_KEY`. +The module has no API Key set up by default. You have to provide an api key by using the relevant command. Read the documentation to create one at https://developers.giphy.com/docs/api -Environ variables seen by commands: - -* GIPHY_API_KEY: API Key for Giphy access - Commands: -* !giphy "query" - Post the first image result from giphy for the given "query" +* !giphy apikey [apikey] - Set api key (Must be done as bot owner) +* !giphy [query] - Post the first image result from giphy for the given [query] + Example: @@ -492,7 +490,7 @@ Can be used to post a picture from Gfycat given a query string. Commands: -* !gfycat "query" - Post the first image result from gfycat for the given "query" +* !gfycat [query] - Post the first image result from gfycat for the given [query] Example: @@ -504,26 +502,24 @@ Can be used to fetch recently added information from Tautulli or receive Tautull Commands: -* !tautulli movie - Show the last 10 recently added movies to Plex library monitered by Tautulli -* !tautulli show - Show the last 10 recently added tv series epsiodes to Plex library monitered by Tautulli -* !tautulli artist - Show the last 10 recently added music artists to Plex library monitered by Tautulli -* !tautulli add "room_id" encrypted - Add the provided encrypted "room_id" to the list of rooms to post the recently added notifications received by the webhook -* !tautulli add "room_id" plain - Add the provided unencrypted "room_id" to the list of rooms to post the recently added notifications received by the webhook -* !tautulli remove "room_id" encrypted - Remove the provided encrypted "room_id" to the list of rooms to post the recently added notifications received by the webhook -* !tautulli remove "room_id" plain - Remove the provided unencrypted "room_id" to the list of rooms to post the recently added notifications received by the webhook +* !tautulli apikey [apikey] - Set api key (Must be done as bot owner) +* !tautulli movie - Show the last 10 recently added movies to Plex library monitered by Tautulli +* !tautulli show - Show the last 10 recently added tv series epsiodes to Plex library monitered by Tautulli +* !tautulli artist - Show the last 10 recently added music artists to Plex library monitered by Tautulli +* !tautulli add [room_id] encrypted - Add the provided encrypted [room_id] to the list of rooms to post the recently added notifications received by the webhook +* !tautulli add [room_id] plain - Add the provided unencrypted [room_id] to the list of rooms to post the recently added notifications received by the webhook +* !tautulli remove [room_id] encrypted - Remove the provided encrypted [room_id] to the list of rooms to post the recently added notifications received by the webhook +* !tautulli remove [room_id] plain - Remove the provided unencrypted [room_id] to the list of rooms to post the recently added notifications received by the webhook Tautulli instance and API Key: The module work with an existing installed instance of Tautulli accessible on the machine at the path defined by env variable `TAUTULLI_PATH` -You have to provide an api key by setting the environment variable `TAUTULLI_API_KEY`. - -Read the documentation to create one at https://developers.giphy.com/docs/api +You have to provide an api key by using the relevant command. Environ variables seen by commands: * TAUTULLI_PATH: Path accessible from the machine to the installed instance of Tautulli * TAUTULLI_URL: Url accessible from the machine to the installed instance of Tautulli -* TAUTULLI_API_KEY: API Key for Tautulli access * TAUTULLI_NOTIFIER_ADDR: Listening address for the Tautulli webhook handler target * TAUTULLI_NOTIFIER_PORT: Listening port for the Tautulli webhook handler target * BOT_OWNERS: Owner of the rooms in the list for the notification webhook diff --git a/modules/giphy.py b/modules/giphy.py index 511c7dd..a4f7224 100644 --- a/modules/giphy.py +++ b/modules/giphy.py @@ -12,13 +12,21 @@ from collections import namedtuple from modules.common.module import BotModule class MatrixModule(BotModule): + api_key = None + async def matrix_message(self, bot, room, event): args = event.body.split() - if len(args) > 1: + if len(args) == 3 and args[1] == 'apikey': + bot.must_be_owner(event) + + self.api_key = args[2] + bot.save_settings() + await bot.send_text(room, 'Api key set') + elif 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")) + g = giphypop.Giphy(api_key=self.api_key) gifs = [] try: for x in g.search(phrase=query, limit=1): @@ -38,8 +46,15 @@ class MatrixModule(BotModule): else: await bot.send_text(room, 'Usage: !giphy ') + def get_settings(self): + data = super().get_settings() + data["api_key"] = self.api_key + return data + def set_settings(self, data): super().set_settings(data) + if data.get("api_key"): + self.api_key = data["api_key"] def help(self): return ('Giphy bot') diff --git a/modules/tautulli.py b/modules/tautulli.py index a570b50..a9f3c49 100644 --- a/modules/tautulli.py +++ b/modules/tautulli.py @@ -123,6 +123,7 @@ class WebServer: class MatrixModule(BotModule): httpd = None rooms = dict() + api_key = None def __init__(self, name): super().__init__(name) @@ -151,14 +152,20 @@ class MatrixModule(BotModule): async def matrix_message(self, bot, room, event): args = event.body.split() - if len(args) == 2: + if len(args) == 3 and args[1] == 'apikey': + bot.must_be_owner(event) + + self.api_key = args[2] + bot.save_settings() + await bot.send_text(room, 'Api key set') + elif 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")) + url = "{}/api/v2?apikey={}&cmd=get_recently_added&count=10".format(os.getenv("TAUTULLI_URL"), self.api_key) req = urllib.request.Request(url+"&media_type="+media_type) connection = urllib.request.urlopen(req).read() entries = json.loads(connection) @@ -194,6 +201,7 @@ class MatrixModule(BotModule): def get_settings(self): data = super().get_settings() + data["api_key"] = self.api_key data["rooms"] = self.rooms self.httpd.rooms = self.rooms return data @@ -203,6 +211,8 @@ class MatrixModule(BotModule): if data.get("rooms"): self.rooms = data["rooms"] self.httpd.rooms = self.rooms + if data.get("api_key"): + self.api_key = data["api_key"] def help(self): return ('Tautulli recently added bot') From 8da787b586c57e9ca2f09c282ade11f8a4030be3 Mon Sep 17 00:00:00 2001 From: Andrea Spacca Date: Fri, 23 Apr 2021 11:24:12 +0200 Subject: [PATCH 5/5] cr fix --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cdd79eb..31eea64 100644 --- a/README.md +++ b/README.md @@ -516,7 +516,7 @@ Tautulli instance and API Key: The module work with an existing installed instance of Tautulli accessible on the machine at the path defined by env variable `TAUTULLI_PATH` You have to provide an api key by using the relevant command. -Environ variables seen by commands: +Environ variables seen by command: * TAUTULLI_PATH: Path accessible from the machine to the installed instance of Tautulli * TAUTULLI_URL: Url accessible from the machine to the installed instance of Tautulli @@ -524,6 +524,10 @@ Environ variables seen by commands: * TAUTULLI_NOTIFIER_PORT: Listening port for the Tautulli webhook handler target * BOT_OWNERS: Owner of the rooms in the list for the notification webhook +Docker environment: + +Since the module needs access to the source of the running Tautulli instance volumes on both Docker (hemppa and Tautulli) should be defined and being visible each other. +When running on Docker the env variables seen by command should be defined for the bot instance. Example: