From 71800d21b6511a5baa4ac8addf2f4d54b24ac5e1 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Sun, 18 Aug 2024 01:52:37 +0200 Subject: [PATCH] Implement an app rejection list (#2515) Co-authored-by: Alexandre Aubin Co-authored-by: OniriCorpe --- rejectedlist.toml | 212 ++++++++++++++++++++++++++++++++++++++ store/app.py | 18 ++++ tools/webhooks/webhook.py | 145 ++++++++++++++++++-------- 3 files changed, 332 insertions(+), 43 deletions(-) create mode 100644 rejectedlist.toml diff --git a/rejectedlist.toml b/rejectedlist.toml new file mode 100644 index 00000000..31c87b4d --- /dev/null +++ b/rejectedlist.toml @@ -0,0 +1,212 @@ +[sillytavern] +name = "SillyTavern" +description = "LLM Frontend for power users" +upstream = "https://github.com/SillyTavern/SillyTavern" +website = "https://sillytavern.app/" +reason = "Based on a LLM" + +[e621-image-hosting] +name = "e621 image hosting" +description = "An Imageboard with a heavy focus on tagging and searchability" +upstream = "https://github.com/e621ng/e621ng" +website = "" +reason = "Docker-compose mess" + +[photo-sphere-viewer] +name = "Photo Sphere Viewer" +description = "A JavaScript library to display 360° sphere panoramas. Very customizable." +upstream = "https://github.com/mistic100/Photo-Sphere-Viewer" +website = "https://photo-sphere-viewer.js.org/" +reason = "Just a library, can be integrated in a My Webapp" + +[minecraft-bedrock-server] +name = "Minecraft Bedrock Server" +description = "This allows you to host your own world server on Minecraft Bedrock Edition." +upstream = "https://github.com/TheRemote/MinecraftBedrockServer" +website = "https://jamesachambers.com/minecraft-bedrock-edition-ubuntu-dedicated-server-guide/" +reason = "MineCraft is not free, whatever its flavour" + +[kirbycms] +name = "KirbyCMS" +description = "Kirby: the CMS that adapts to any project, loved by developers and editors alike." +upstream = "https://github.com/getkirby/kirby" +website = "https://getkirby.com/" +reason = "Not free" + +[coolify] +name = "Coolify" +description = "Alternative à Vercel" +upstream = "https://github.com/coollabsio/coolify" +website = "https://coolify.io/" +reason = "Does not make sense to integrate in YunoHost, and most likely incompatible anyways" + +[chatmail] +name = "chatmail" +description = "Deltachat server" +upstream = "https://github.com/deltachat/chatmail" +website = "https://delta.chat/en/2023-12-13-chatmail" +reason = "YunoHost already integrates a mail server" + +[stalwart-mail-server] +name = "Stalwart Mail Server" +description = "Modern All-in-One Mail Server (IMAP, JMAP, SMTP) written in Rust." +upstream = "https://github.com/stalwartlabs/mail-server" +website = "https://stalw.art" +reason = "YunoHost already integrates a mail server" + +[retronas] +name = "RetroNAS" +description = "It allows you to store ROMs and sync to different consoles with networking connecting." +upstream = "https://github.com/retronas/retronas" +website = "" +reason = "Big scary security warning in the app's README" + +[quakejs] +name = "QuakeJS" +description = "QuakeJS allows you to play Quake III Arena Multiplayer on Web Browser using JavaScript." +upstream = "https://github.com/inolen/quakejs" +website = "http://www.quakejs.com/" +reason = "Unmaintained project" + +[speckle-server] +name = "Speckle Server" +description = "Data infrastructure for the AEC industry." +upstream = "https://github.com/specklesystems/speckle-server" +website = "https://speckle.systems/" +reason = "Too niche of a use case, and no Docker-less install options" + +[graphite] +name = "Graphite" +description = "Création non destructive d'image" +upstream = "https://github.com/GraphiteEditor/Graphite" +website = "https://graphite.rs/" +reason = "No self-hosting documentation" + +[affine] +name = "Affine" +description = "Wiki, écriture de document, Mindmap, Moodboard, un melting polt d'outil pour créer divers document." +upstream = "https://github.com/toeverything/AFFiNE" +website = "https://affine.pro/" +reason = "Unclear license and too big of a framework" + + +[authentik] +name = "authentik" +description = "Replace Active Directory, Okta and Auth0. Supports OIDC, SAML, LDAP, SCIM, Radius, and Proxy." +upstream = "https://github.com/goauthentik/authentik" +website = "https://goauthentik.io/" +reason = "YunoHost already integrates a SSO, it will conflict." + +[mirlo] +name = "Mirlo" +description = "Mirlo provides a user-friendly space to help musicians sell music, manage subscriptions, and share w" +upstream = "https://github.com/funmusicplace/mirlo/" +website = "" +reason = "Too big of a Docker-compose mess" + +[h5p] +name = "H5P" +description = "This collection of tools aim to creates interactive exercices for learning purposes." +upstream = "https://github.com/h5p" +website = "https://h5p.org/" +reason = "The tools are spread across too many repos, making packaging difficult" + +[gophish] +name = "Gophish" +description = "Phishing toolkit for security awareness training and penetration testers" +upstream = "https://github.com/gophish/gophish" +website = "https://getgophish.com" +reason = "Too niche of a use case" + +[pdftochat] +name = "PDFTOCHAT" +description = "Chat with your PDFs in seconds. Powered by Together AI and Pinecone." +upstream = "https://github.com/Nutlope/pdftochat" +website = "https://www.pdftochat.com/" +reason = "whatthefuck.gif, also AI" + +[jam] +name = "Jam" +description = "With Jam you can create audio rooms that can be used for panel discussions, jam sessions, ..." +upstream = "https://github.com/jam-systems/jam" +website = "https://jam.systems/" +reason = "Unmaintained" + +[bluesky-pds] +name = "Bluesky PDS" +description = "Personal Data Server for Bluesky, federated social network based on the AT protocol." +upstream = "https://github.com/bluesky-social/pds" +website = "https://bsky.social/about" +reason = "One of the principles at the heart of the YunoHost project is to be a modest counterweight to GAFAM and surveillance capitalism, [...] [not to] provide a ready-to-use tool to our users to be part of it directly" + +[anysync] +name = "AnySync" +description = "Allows sync between devices for Aytype." +upstream = "https://github.com/anyproto/any-sync" +website = "https://anytype.io/" +reason = "Clients are not totally free" + +[dub] +name = "dub" +description = "Open-source link management infrastructure." +upstream = "https://github.com/dubinc/dub" +website = "https://dub.co" +reason = "Tracking system, against YunoHost core values" + +[maybe] +name = "Maybe" +description = "The OS for your personal finances" +upstream = "https://github.com/maybe-finance/maybe" +website = "https://hello.maybe.co" +reason = "Surely not ready for self-hosting" + +[osclass] +name = "Osclass" +description = "Classifieds website" +upstream = "https://github.com/osclass/Osclass" +website = "https://osclass-classifieds.com" +reason = "Unmaintained" + +[opentalk] +name = "OpenTalk" +description = "German developed, modern, conferencing tool running in browser alone." +upstream = "https://gitlab.opencode.de/opentalk" +website = "https://opentalk.eu/en" +reason = "Looks unpackageable" + +[verdaccio] +name = "Verdaccio" +description = "Locally managed backup & proxy for npm package registry." +upstream = "https://github.com/verdaccio/verdaccio" +website = "https://verdaccio.org/" +reason = "Too niche of a use case" + +[tabula] +name = "Tabula" +description = "Extract tables from PDF; single executable .jar on 127.0.0.1.\r\rComparable to Excalibur, I think." +upstream = "https://github.com/tabulapdf/tabula" +website = "https://tabula.technology/" +reason = "Unmaintained" + +[llamafile] +name = "Llamafile" +description = "Single file LLMs from Mozilla Innovation Projects, that will run locally on any available processors" +upstream = "https://github.com/Mozilla-Ocho/llamafile" +website = "https://hacks.mozilla.org/2023/11/introducing-llamafile/" +reason = "Not packageable as is, it is a binary tool for other apps" + + +[fastchat] +name = "FastChat" +description = "- open platform for LLM chatbots incl training, evaluating, & serving via openai-compatible APIs." +upstream = "https://github.com/lm-sys/FastChat" +website = "https://chat.lmsys.org/" +reason = "YunoHost packagers do not wish to promote AI, and training AIs is not lean" + +[slidge] +name = "Slidge" +description = "XMPP (puppeteer) gateway library to other instant messenger platforms/protocols." +upstream = "https://sr.ht/~nicoco/slidge/sources" +website = "https://sr.ht/~nicoco/slidge/" +reason = "This is only a library, so not packageable as is" + diff --git a/store/app.py b/store/app.py index caac4278..4d19d2dc 100644 --- a/store/app.py +++ b/store/app.py @@ -364,6 +364,24 @@ def add_to_wishlist(): ), ) + rejectedlist_rawtoml = repo.get_contents( + "rejectedlist.toml", ref=repo.default_branch + ) + rejectedlist_rawtoml = rejectedlist_rawtoml.decoded_content.decode() + rejectedlist = tomlkit.loads(rejectedlist_rawtoml) + + for rejectedslug, rejectedinfo in rejectedlist.items(): + if upstream.strip("/ ") in rejectedinfo["upstream"]: + return render_template( + "wishlist_add.html", + csrf_token=csrf_token, + successmsg=None, + errormsg=_( + "We're sorry, but this app is listed among the already declined apps. The specified reason is:
%(reason)s", + reason=rejectedinfo["reason"], + ), + ) + app_catalog = get_catalog()["apps"] if slug in app_catalog: diff --git a/tools/webhooks/webhook.py b/tools/webhooks/webhook.py index e310a98f..725420fa 100755 --- a/tools/webhooks/webhook.py +++ b/tools/webhooks/webhook.py @@ -10,6 +10,7 @@ import tempfile import aiohttp import logging from pathlib import Path +import re from typing import Optional from git import Actor, Repo, GitCommandError @@ -116,44 +117,58 @@ def on_push(request: Request) -> HTTPResponse: repository = data["repository"]["full_name"] branch = data["ref"].split("/", 2)[2] - logging.info(f"{repository} -> branch '{branch}'") + if repository.startswith("YunoHost-Apps/"): - need_push = False - with tempfile.TemporaryDirectory() as folder_str: - folder = Path(folder_str) - repo = Repo.clone_from( - f"https://{github_login()}:{github_token()}@github.com/{repository}", - to_path=folder, - ) + logging.info(f"{repository} -> branch '{branch}'") - # First rebase the testing branch if possible - if branch in ["master", "testing"]: - result = git_repo_rebase_testing_fast_forward(repo) + need_push = False + with tempfile.TemporaryDirectory() as folder_str: + folder = Path(folder_str) + repo = Repo.clone_from( + f"https://{github_login()}:{github_token()}@github.com/{repository}", + to_path=folder, + ) + + # First rebase the testing branch if possible + if branch in ["master", "testing"]: + result = git_repo_rebase_testing_fast_forward(repo) + need_push = need_push or result + + repo.git.checkout(branch) + result = generate_and_commit_readmes(repo) need_push = need_push or result - repo.git.checkout(branch) - result = generate_and_commit_readmes(repo) - need_push = need_push or result + if not need_push: + logging.debug("nothing to do") + return response.text("nothing to do") - if not need_push: - logging.debug("nothing to do") - return response.text("nothing to do") + logging.debug(f"Pushing {repository}") + repo.remote().push(quiet=False, all=True) - logging.debug(f"Pushing {repository}") - repo.remote().push(quiet=False, all=True) - - return response.text("ok") + return response.text("ok") def on_pr_comment(request: Request, pr_infos: dict) -> HTTPResponse: body = request.json["comment"]["body"].strip()[:100].lower() # Check the comment contains proper keyword trigger + BUMP_REV_COMMANDS = ["!bump", "!new_revision", "!newrevision"] if any(trigger.lower() in body for trigger in BUMP_REV_COMMANDS): bump_revision(request, pr_infos) return response.text("ok") + REJECT_WISHLIST_COMMANDS = ["!reject", "!nope"] + if any(trigger.lower() in body for trigger in REJECT_WISHLIST_COMMANDS): + reason = "" + for command in REJECT_WISHLIST_COMMANDS: + try: + reason = re.search(f"{command} (.*)", body).group(1).rstrip() + except: + pass + reject_wishlist(request, pr_infos, reason) + return response.text("ok") + return response.empty() @@ -162,32 +177,76 @@ def bump_revision(request: Request, pr_infos: dict) -> HTTPResponse: repository = data["repository"]["full_name"] branch = pr_infos["head"]["ref"] - logging.info(f"Will bump revision on {repository} branch {branch}...") - with tempfile.TemporaryDirectory() as folder_str: - folder = Path(folder_str) - repo = Repo.clone_from( - f"https://{github_login()}:{github_token()}@github.com/{repository}", - to_path=folder, - ) - repo.git.checkout(branch) + if repository.startswith("YunoHost-Apps/"): - manifest_file = folder / "manifest.toml" - manifest = tomlkit.load(manifest_file.open("r", encoding="utf-8")) - version, revision = manifest["version"].split("~ynh") - revision = str(int(revision) + 1) - manifest["version"] = "~ynh".join([version, revision]) - tomlkit.dump(manifest, manifest_file.open("w", encoding="utf-8")) + logging.info(f"Will bump revision on {repository} branch {branch}...") + with tempfile.TemporaryDirectory() as folder_str: + folder = Path(folder_str) + repo = Repo.clone_from( + f"https://{github_login()}:{github_token()}@github.com/{repository}", + to_path=folder, + ) + repo.git.checkout(branch) - repo.git.add("manifest.toml") - repo.index.commit( - "Bump package revision", - author=Actor("yunohost-bot", "yunohost@yunohost.org"), - ) + manifest_file = folder / "manifest.toml" + manifest = tomlkit.load(manifest_file.open("r", encoding="utf-8")) + version, revision = manifest["version"].split("~ynh") + revision = str(int(revision) + 1) + manifest["version"] = "~ynh".join([version, revision]) + tomlkit.dump(manifest, manifest_file.open("w", encoding="utf-8")) - logging.debug(f"Pushing {repository}") - repo.remote().push(quiet=False, all=True) - return response.text("ok") + repo.git.add("manifest.toml") + repo.index.commit( + "Bump package revision", + author=Actor("yunohost-bot", "yunohost@yunohost.org"), + ) + logging.debug(f"Pushing {repository}") + repo.remote().push(quiet=False, all=True) + return response.text("ok") + +def reject_wishlist(request: Request, pr_infos: dict, reason=None) -> HTTPResponse: + data = request.json + repository = data["repository"]["full_name"] + branch = pr_infos["head"]["ref"] + + if repository == "YunoHost/apps" and branch.startswith("add-to-wishlist"): + + logging.info(f"Will put the suggested app in the rejected list on {repository} branch {branch}...") + with tempfile.TemporaryDirectory() as folder_str: + folder = Path(folder_str) + repo = Repo.clone_from( + f"https://{github_login()}:{github_token()}@github.com/{repository}", + to_path=folder, + ) + repo.git.checkout(branch) + + rejectedlist_file = (folder / "rejectedlist.toml") + rejectedlist = tomlkit.load(rejectedlist_file.open("r", encoding="utf-8")) + + wishlist_file = (folder / "wishlist.toml") + wishlist = tomlkit.load(wishlist_file.open("r", encoding="utf-8")) + + suggestedapp_slug = branch.replace("add-to-wishlist-", "") + suggestedapp = {suggestedapp_slug:wishlist[suggestedapp_slug]} + suggestedapp[suggestedapp_slug]["rejection_pr"] = pr_infos["html_url"] + suggestedapp[suggestedapp_slug]["reason"] = reason + + wishlist.pop(suggestedapp_slug) + rejectedlist.update(suggestedapp) + + tomlkit.dump(rejectedlist, rejectedlist_file.open("w", encoding="utf-8")) + tomlkit.dump(wishlist, wishlist_file.open("w", encoding="utf-8")) + + repo.git.add("rejectedlist.toml") + repo.git.add("wishlist.toml") + + suggestedapp_name = suggestedapp[suggestedapp_slug]["name"] + repo.index.commit(f"Reject {suggestedapp_name} from catalog", author=Actor("yunohost-bot", "yunohost@yunohost.org")) + + logging.debug(f"Pushing {repository}") + repo.remote().push(quiet=False, all=True, force=True) + return response.text("ok") def generate_and_commit_readmes(repo: Repo) -> bool: assert repo.working_tree_dir is not None