commit 94ce88aa65d09f9fd559d1ed4ee50f98b965601c Author: pancakes Date: Mon Feb 19 21:41:28 2024 +1000 meow diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34d56ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +**/**.gif +**/**.jpeg +**/**.jpg +**/**.mp4 +**/**.png +.venv/ +auth/*.secret +config/*.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..17ae5ef --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Fedi Board Bot + +A bot that posts random images from image boards to Mastodon, Iceshrimp, Sharkey or Akkoma. + +## Config + +Copy `config/example.json.example` and remove the `.example` from the end. All fields are explained in the file. Config names should not have any spaces. + +When config-name is mentioned later do not include the `.json` extension. + +## Usage + +Install the requirements. You should ideally do this in a venv. + +```sh +pip install -r requirements.txt +``` + +For every new config generate a login token by running + +```sh +python3 login.py +``` + +Run the bot by running + +```sh +python3 bot.py +``` diff --git a/auth/.gitkeep b/auth/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..6abd9ea --- /dev/null +++ b/bot.py @@ -0,0 +1,173 @@ +from dataclasses import dataclass +from mastodon import Mastodon +from os import environ +from sys import argv + +import json +import pathlib +import random +import requests + +class Config: + block_tags: list[str] + nsfw_tags: list[str] + search_tags: list[str] + text_cw: str + text_filler: list[str] + type: str + + def __init__(self, config_name: str): + with open(f"config/{config_name}.json") as file: + config = json.load(file) + + self.block_tags = config["blockTags"] + self.nsfw_tags = config["nsfwTags"] + self.search_tags = config["searchTags"] + self.text_cw = config["textCW"] + self.text_filler = config["textFiller"] + self.type = config["type"] + +def random_image_danbooru(block_tags: list[str], search_tags: list[str]) -> dict | None: + tags = [] + for tag in block_tags: + tags.append("-" + tag.strip()) + for tag in search_tags: + tags.append(tag.strip()) + tags = " ".join(tags) + + params = { + "random": "1", + "tags": tags + } + if environ.get("BOARD_API_KEY") and environ.get("BOARD_USERNAME"): + params["api_key"] = environ.get("BOARD_API_KEY") + params["login"] = environ.get("BOARD_USERNAME") + resp = requests.get("https://danbooru.donmai.us/posts.json", params=params) + + if resp.ok == False: + print(resp.json()) + return None + elif resp.json() == []: + return None + + return random.choice(resp.json()) + +def random_image_gelbooru(block_tags: list[str], search_tags: list[str]) -> dict | None: + tags = ["sort:random"] + for tag in block_tags: + tags.append("-" + tag.strip()) + for tag in search_tags: + tags.append(tag.strip()) + tags = " ".join(tags) + + params = { + "page": "dapi", + "s": "post", + "q": "index", + "pid": 0, + "json": "1", + "limit": "1", + "tags": tags + } + resp = requests.get("https://gelbooru.com/index.php", params=params) + + if resp.ok == False: + print(resp.json()) + return None + elif resp.json()["post"] == []: + return None + + return random.choice(resp.json()["post"]) + +# Exit if config isn't specified +if len(argv) < 2: + print(f"Usage: python3 {argv[0]} ") + exit() + +config = Config(argv[1]) + +match config.type: + case "danbooru": + post = random_image_danbooru(config.block_tags, config.search_tags) + case "gelbooru": + post = random_image_gelbooru(config.block_tags, config.search_tags) + case _: + print("'type' in config must be 'danbooru' or 'gelbooru'") + exit() + +if post == None: + print("Couldn't get an image") + exit() + +match config.type: + case "danbooru": + filename = f'{post["md5"]}.{post["file_ext"]}' + case "gelbooru": + filename = post["image"] + case _: + filename = "img.png" + +print(post) + +image = requests.get(post["file_url"]) + +if image.ok == False: + print("Couldn't download image") + exit() + +with open(filename, "wb") as f: + f.write(image.content) + +# Image is NSFW if rated Explicit, Sensitive or Unknown +is_nsfw = post["rating"].startswith("e") or post["rating"].startswith("s") or post["rating"] == None + +if is_nsfw == False: + for tag in config.nsfw_tags: + # Check if any config defined NSFW tags are present + match config.type: + case "danbooru": + if tag in post["tag_string"] or tag in post["tag_string_general"] or tag in post["tag_string_artist"] or tag in post["tag_string_copyright"] or tag in post["tag_string_character"] or tag in post["tag_string_meta"]: + is_nsfw = True + break + case "gelbooru": + if tag in post["tags"]: + is_nsfw = True + break + case _: + is_nsfw = True + break + +# This sucks but it works :3 +post_content = [ + random.choice(config.text_filler), + "\n\n", + "Artist: " + post["tag_string_artist"] + "\n" if config.type == "danbooru" and len(post["tag_string_artist"]) > 0 else "", + "Score: " + str(post["score"]) + "\n", + "Source: " + post["source"] + "\n\n" if len(post["source"]) > 0 else "\n", +] + +mastodon = Mastodon(access_token=f"auth/{argv[1]}.auth.secret") + +match config.type: + case "danbooru": + tags = post["tag_string"] + case "gelbooru": + tags = post["tags"] + case _: + tags = "unknown" + +media = mastodon.media_post( + description="Automatically posted image. Tagged: " + tags, + media_file=filename +) + +status = mastodon.status_post( + "".join(post_content), + media_ids=[media["id"]], + sensitive=is_nsfw, + spoiler_text="(NSFW) " + config.text_cw if is_nsfw else config.text_cw, + visibility="unlisted" +) +print(status["url"]) + +pathlib.Path(filename).unlink() diff --git a/config/example.json.example b/config/example.json.example new file mode 100644 index 0000000..72f7c69 --- /dev/null +++ b/config/example.json.example @@ -0,0 +1,16 @@ +{ + "blockTags": [ + "List of tags that should be ignored by the bot." + ], + "nsfwTags": [ + "If these tags are present the post will be marked as sensitive" + ], + "searchTags": [ + "Tags that will be searched for" + ], + "textCW": "All posts will have this content warning", + "textFiller": [ + "Flavor text for posts" + ], + "type": "danbooru or gelbooru" +} \ No newline at end of file diff --git a/login.py b/login.py new file mode 100644 index 0000000..924daaf --- /dev/null +++ b/login.py @@ -0,0 +1,11 @@ +from mastodon import Mastodon +from sys import argv + +if len(argv) < 4: + print(f"Usage: python3 {argv[0]} ") + exit() + +Mastodon.create_app(argv[2], scopes=["write:media", "write:statuses"], to_file=f"auth/{argv[1]}.client.secret", api_base_url=argv[3]) +mastodon = Mastodon(client_id=f"auth/{argv[1]}.client.secret", api_base_url=argv[3]) +print(mastodon.auth_request_url(scopes=["write:media", "write:statuses"])) +mastodon.log_in(code=input("Code="), scopes=["write:media", "write:statuses"], to_file=f"auth/{argv[1]}.auth.secret") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b033626 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Mastodon.py \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..38ae469 --- /dev/null +++ b/shell.nix @@ -0,0 +1,15 @@ +{ pkgs ? import { } }: +pkgs.mkShell { + packages = [ + (pkgs.python3.withPackages (python-packages: [ + python-packages.black + python-packages.pip + python-packages.virtualenv + ])) + ]; + + shellHook = '' + python3 -m venv .venv + source .venv/bin/activate + ''; +}