├── .gitignore ├── LICENSE ├── README.md ├── base-config.yaml ├── giphy.py └── maubot.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | *.mbp 2 | *.tar 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tom Casavant 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Giphy Maubot 2 | A simple [maubot](https://github.com/maubot/maubot) that generates a random gif given a search term. 3 | 4 | ## Setup 5 | 1. Get API key from [giphy](https://developers.giphy.com/docs/) and [tenor](https://tenor.com/gifapi) 6 | 2. Fill in `giphy_api_key` and `tenor_api_key` field in base-config.yaml config file or in online maubot config editor 7 | 3. It is no longer possible to register new keys for Tenor API v1 (https://tenor.com/developer/keyregistration). Select your tenor API version (v1 or v2) 8 | 4. Decide what (giphy) endpoint to get random gifs from (e.g. trending, random) in config file 9 | 5. Choose a response type: 10 | - `message` will send a regular message to the room 11 | - `reply` will send a quoted reply message to the room 12 | - `upload` will actually upload the GIF as an image to the room 13 | 14 | 15 | ## Usage 16 | `!gif word` - Bot replies with a link to a gif given the search term 17 | `!gif` - Bot replies with a link to a random gif 18 | 19 | Also, `!giphy`, `!g` and `!tenor` may be used. 20 | -------------------------------------------------------------------------------- /base-config.yaml: -------------------------------------------------------------------------------- 1 | giphy_api_key: API_KEY_HERE 2 | tenor_api_key: API_KEY_HERE 3 | # Which Tenor API version to use: v1 or v2? 4 | tenor_api_version: v2 5 | # Source of the gifs, can be either giphy or tenor 6 | provider: tenor 7 | # Number of results of the query in which the gif 8 | # is randomly chosen. Set 1 for the top result. 9 | # (tenor-only option) 10 | num_results: 1 11 | # Uncomment desired source for random gifs on giphy 12 | # can be either trending or random 13 | # note: this is only used when no query is passed! 14 | source: trending 15 | # desired response type: 16 | # message: regular message containing url to gif 17 | # reply: reply-message containing url to gif 18 | # upload: image upload to the room 19 | response_type: message 20 | # Choose an MPAA-like content "rating" 21 | # Giphy values: [g | pg | pg-13 | r] 22 | # Tenor values: [high | medium | low | off] 23 | # default values: 'g' for Giphy, 'off' for Tenor 24 | rating: off 25 | -------------------------------------------------------------------------------- /giphy.py: -------------------------------------------------------------------------------- 1 | from typing import Type 2 | import urllib.parse 3 | import random 4 | from mautrix.types import RoomID, ImageInfo 5 | from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper 6 | from maubot import Plugin, MessageEvent 7 | from maubot.handlers import command 8 | 9 | 10 | # Setup config file 11 | class Config(BaseProxyConfig): 12 | def do_update(self, helper: ConfigUpdateHelper) -> None: 13 | helper.copy("giphy_api_key") 14 | helper.copy("tenor_api_key") 15 | helper.copy("tenor_api_version") 16 | helper.copy("provider") 17 | helper.copy("source") 18 | helper.copy("response_type") 19 | helper.copy("num_results") 20 | helper.copy("rating") 21 | 22 | 23 | class GiphyPlugin(Plugin): 24 | async def start(self) -> None: 25 | await super().start() 26 | self.config.load_and_update() 27 | 28 | async def send_gif( 29 | self, room_id: RoomID, gif_link: str, query: str, info: dict 30 | ) -> None: 31 | resp = await self.http.get(gif_link) 32 | if resp.status != 200: 33 | self.log.warning(f"Unexpected status fetching image {url}: {resp.status}") 34 | return None 35 | 36 | data = await resp.read() 37 | mime = info["mime"] 38 | filename = f"{query}.gif" if len(query) > 0 else "giphy.gif" 39 | uri = await self.client.upload_media(data, mime_type=mime, filename=filename) 40 | 41 | await self.client.send_image( 42 | room_id, 43 | url=uri, 44 | file_name=filename, 45 | info=ImageInfo( 46 | mimetype=info["mime"], width=info["width"], height=info["height"] 47 | ), 48 | ) 49 | 50 | @classmethod 51 | def get_config_class(cls) -> Type[BaseProxyConfig]: 52 | return Config 53 | 54 | @command.new( 55 | "giphy", 56 | aliases=("gif", "g", "tenor"), 57 | ) 58 | @command.argument("search_term", pass_raw=True, required=False) 59 | async def handler(self, evt: MessageEvent, search_term: str) -> None: 60 | await evt.mark_read() 61 | if not search_term: 62 | # If user doesn't supply a search term, set to empty string 63 | search_term = "" 64 | source = self.config["source"] 65 | else: 66 | source = "translate" 67 | 68 | if self.config["provider"] == "giphy": 69 | api_key = self.config["giphy_api_key"] 70 | rating = self.config["rating"] 71 | url_params = urllib.parse.urlencode({"s": search_term, "api_key": api_key, "rating": rating}) 72 | response_type = self.config["response_type"] 73 | # Get random gif url using search term 74 | async with self.http.get( 75 | "http://api.giphy.com/v1/gifs/{}?{}".format(source, url_params) 76 | ) as response: 77 | data = await response.json() 78 | 79 | # Retrieve gif link from JSON response 80 | try: 81 | gif_link = data["data"]["images"]["original"]["url"] 82 | info = {} 83 | info["width"] = int(data["data"]["images"]["original"]["width"]) 84 | info["height"] = int(data["data"]["images"]["original"]["height"]) 85 | info["mime"] = "image/gif" # this shouldn't really change 86 | except Exception as e: 87 | await evt.respond("Blip bloop... Something is wrecked up here!") 88 | return None 89 | 90 | elif self.config["provider"] == "tenor": 91 | api_key = self.config["tenor_api_key"] 92 | api_version = self.config["tenor_api_version"] 93 | rating = self.config["rating"] 94 | url_params = urllib.parse.urlencode({"q": search_term, "key": api_key, "contentfilter": rating}) 95 | response_type = self.config["response_type"] 96 | # Get random gif url using search term 97 | async with self.http.get( 98 | f"https://g.tenor.com/{api_version}/search?{url_params}" 99 | ) as response: 100 | data = await response.json() 101 | 102 | # Retrieve gif link from JSON response 103 | try: 104 | image_num = random.randint(0, self.config["num_results"] - 1) 105 | result = data["results"][image_num] 106 | gif = ( 107 | result["media_formats"] if api_version == "v2" 108 | else result["media"][0] 109 | )["gif"] 110 | gif_link = gif["url"] 111 | info = {} 112 | info["width"] = int(gif["dims"][0]) 113 | info["height"] = int(gif["dims"][1]) 114 | info["mime"] = "image/gif" # this shouldn't really change 115 | except Exception as e: 116 | await evt.respond("Blip bloop... Something is wrecked up here!") 117 | return None 118 | else: 119 | raise (Exception("Wrong provider:", self.config["provider"])) 120 | 121 | if response_type == "message": 122 | await evt.respond(gif_link, allow_html=True) # Respond to user 123 | elif response_type == "reply": 124 | await evt.reply(gif_link, allow_html=True) # Reply to user 125 | elif response_type == "upload": 126 | await self.send_gif( 127 | evt.room_id, gif_link, search_term, info 128 | ) # Upload the GIF to the room 129 | else: 130 | await evt.respond( 131 | "something is wrong with my config, be sure to set a response_type" 132 | ) 133 | -------------------------------------------------------------------------------- /maubot.yaml: -------------------------------------------------------------------------------- 1 | 2 | # This is an example maubot plugin definition file. 3 | # All plugins must include a file like this named "maubot.yaml" in their root directory. 4 | 5 | # Target maubot version 6 | maubot: 0.1.0 7 | 8 | # The unique ID for the plugin. Java package naming style. (i.e. use your own domain, not xyz.maubot) 9 | id: casavant.tom.giphy 10 | 11 | # A PEP 440 compliant version string. 12 | version: 3.3.0 13 | 14 | # The SPDX license identifier for the plugin. https://spdx.org/licenses/ 15 | # Optional, assumes all rights reserved if omitted. 16 | license: MIT 17 | 18 | # The list of modules to load from the plugin archive. 19 | # Modules can be directories with an __init__.py file or simply python files. 20 | # Submodules that are imported by modules listed here don't need to be listed separately. 21 | # However, top-level modules must always be listed even if they're imported by other modules. 22 | modules: 23 | - giphy 24 | #- config 25 | # The main class of the plugin. Format: module/Class 26 | # If `module` is omitted, will default to last module specified in the module list. 27 | # Even if `module` is not omitted here, it must be included in the modules list. 28 | # The main class must extend maubot.Plugin 29 | main_class: GiphyPlugin 30 | 31 | # Whether or not instances need a database 32 | database: false 33 | 34 | # Extra files that the upcoming build tool should include in the mbp file. 35 | extra_files: 36 | - base-config.yaml 37 | #- LICENSE 38 | 39 | # List of dependencies 40 | #dependencies: 41 | #- config 42 | 43 | #soft_dependencies: 44 | #- bar>=0.1 45 | --------------------------------------------------------------------------------