From a057aab19803f33fadf0a09c4a5e0144507b0f70 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 15 Aug 2023 18:49:45 +0200 Subject: [PATCH 01/51] Initial commit for new app store --- .gitignore | 1 + store/app.py | 121 +++++++++++++++++++++++++++++++++ store/requirements.txt | 1 + store/templates/app.html | 9 +++ store/templates/base.html | 123 ++++++++++++++++++++++++++++++++++ store/templates/catalog.html | 113 +++++++++++++++++++++++++++++++ store/templates/index.html | 40 +++++++++++ store/templates/wishlist.html | 103 ++++++++++++++++++++++++++++ 8 files changed, 511 insertions(+) create mode 100644 store/app.py create mode 100644 store/requirements.txt create mode 100644 store/templates/app.html create mode 100644 store/templates/base.html create mode 100644 store/templates/catalog.html create mode 100644 store/templates/index.html create mode 100644 store/templates/wishlist.html diff --git a/.gitignore b/.gitignore index 484a0956..1dc38528 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ tools/autopatches/token __pycache__ app_list_auto_update.log +venv diff --git a/store/app.py b/store/app.py new file mode 100644 index 00000000..d9cf32d9 --- /dev/null +++ b/store/app.py @@ -0,0 +1,121 @@ +from flask import Flask, send_from_directory, render_template, session, redirect, request +import base64 +import hashlib +import hmac +import os +import random +import urllib +import json +from settings import DISCOURSE_SSO_SECRET, DISCOURSE_SSO_ENDPOINT, CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE +app = Flask(__name__) + +app.debug = True +app.config["DEBUG"] = True +app.config['TEMPLATES_AUTO_RELOAD'] = True + +catalog = json.load(open("apps.json")) +catalog['categories'] = {c['id']:c for c in catalog['categories']} + +category_color = { + "synchronization": "sky", + "publishing": "yellow", + "communication": "amber", + "office": "lime", + "productivity_and_management": "purple", + "small_utilities": "", + "reading": "emerald", + "multimedia": "fuchsia", + "social_media": "rose", + "games": "violet", + "dev": "stone", + "system_tools": "white", + "iot": "orange", + "wat": "teal", +} + +for id_, category in catalog['categories'].items(): + category["color"] = category_color[id_] + +wishlist = json.load(open("wishlist.json")) + +# This is the secret key used for session signing +app.secret_key = ''.join([str(random.randint(0, 9)) for i in range(99)]) + + +@app.route('/login_using_discourse') +def login_using_discourse(): + """ + Send auth request to Discourse: + """ + + nonce, url = create_nonce_and_build_url_to_login_on_discourse_sso() + + session.clear() + session["nonce"] = nonce + + return redirect(url) + + +@app.route('/sso_login_callback') +def sso_login_callback(): + response = base64.b64decode(request.args['sso'].encode()).decode() + user_data = urllib.parse.parse_qs(response) + if user_data['nonce'][0] != session.get("nonce"): + return "Invalid nonce", 401 + else: + session.clear() + session['user'] = { + "id": user_data["external_id"][0], + "username": user_data["username"][0], + "avatar_url": user_data["avatar_url"][0], + } + return redirect("/") + + +@app.route('/logout') +def logout(): + session.clear() + return redirect("/") + + +@app.route('/') +def index(): + return render_template("index.html", user=session.get('user', {}), catalog=catalog) + + +@app.route('/catalog') +def browse_catalog(category_filter=None): + return render_template("catalog.html", user=session.get('user', {}), catalog=catalog) + + +@app.route('/app/') +def app_info(app_id): + infos = catalog["apps"].get(app_id) + if not infos: + return f"App {app_id} not found", 404 + return render_template("app.html", user=session.get('user', {}), app_id=app_id, infos=infos) + + +@app.route('/wishlist') +def browse_wishlist(): + return render_template("wishlist.html", user=session.get('user', {}), wishlist=wishlist) + + + +################################################ + +def create_nonce_and_build_url_to_login_on_discourse_sso(): + """ + Redirect the user to DISCOURSE_ROOT_URL/session/sso_provider?sso=URL_ENCODED_PAYLOAD&sig=HEX_SIGNATURE + """ + + nonce = ''.join([str(random.randint(0, 9)) for i in range(99)]) + + url_data = {"nonce": nonce, "return_sso_url": CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE} + url_encoded = urllib.parse.urlencode(url_data) + payload = base64.b64encode(url_encoded.encode()).decode() + sig = hmac.new(DISCOURSE_SSO_SECRET.encode(), msg=payload.encode(), digestmod=hashlib.sha256).hexdigest() + data = {"sig": sig, "sso": payload} + url = f"{DISCOURSE_SSO_ENDPOINT}?{urllib.parse.urlencode(data)}" + + return nonce, url diff --git a/store/requirements.txt b/store/requirements.txt new file mode 100644 index 00000000..5aad892a --- /dev/null +++ b/store/requirements.txt @@ -0,0 +1 @@ +Flask==2.3.2 diff --git a/store/templates/app.html b/store/templates/app.html new file mode 100644 index 00000000..ece72937 --- /dev/null +++ b/store/templates/app.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block main %} +
+ +

{{ app_id }}

+ +

{{ infos }}

+
+{% endblock %} diff --git a/store/templates/base.html b/store/templates/base.html new file mode 100644 index 00000000..5b2b69b3 --- /dev/null +++ b/store/templates/base.html @@ -0,0 +1,123 @@ + + + + + YunoHost app store + + + + + + + +
+
+ + Home + + + +
+ + +
+
+ + {% if not user %} + + Login using YunoHost's forum + + {% else %} + + + {% endif %} +
+ + +
+
+
+
+ + {% block main %} + {% endblock %} + +

TODO : add a proper footer
+ + + diff --git a/store/templates/catalog.html b/store/templates/catalog.html new file mode 100644 index 00000000..6f1782a9 --- /dev/null +++ b/store/templates/catalog.html @@ -0,0 +1,113 @@ +{% extends "base.html" %} +{% block main %} +{% set locale = 'en' %} +
+

+ Application Catalog +

+
+
+
+
+ + + + + + + +
+ +
+ +
+
+ + Sort by newest +
+
+ + Show only your bookmarks +
+
+
+ + + +{% endblock %} diff --git a/store/templates/index.html b/store/templates/index.html new file mode 100644 index 00000000..2691d726 --- /dev/null +++ b/store/templates/index.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% block main %} +{% set locale = 'en' %} + +
+ +

+ Application Store +

+
+ +
+ + {% for id, category in catalog['categories'].items() %} + + {% endfor %} +
+{% endblock %} diff --git a/store/templates/wishlist.html b/store/templates/wishlist.html new file mode 100644 index 00000000..a4651183 --- /dev/null +++ b/store/templates/wishlist.html @@ -0,0 +1,103 @@ +{% extends "base.html" %} +{% block main %} +
+

+ Application Wishlist +

+
+ +
+
+
+ + + + + + + +
+ + + + Add an app to the wishlist + +
+
+ + + + +
+ + + + + + + + + + + + + + {% for infos in wishlist %} + + + + + + + + + {% endfor %} + +
+ Name + + Description +
+ {{ infos['name'] }} + {{ infos['description'] }} + {% if infos['website'] %} + + + + {% endif %} + + {% if infos['upstream'] %} + + + + {% endif %} + + + + Vote + + + + 123 + +
+
+{% endblock %} From 96ce63d392033025e5bca38013656ec504451168 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 17 Aug 2023 13:56:09 +0200 Subject: [PATCH 02/51] appstore: use colored border instead of colored backgrounds for category badges --- store/templates/catalog.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/templates/catalog.html b/store/templates/catalog.html index 6f1782a9..08438f44 100644 --- a/store/templates/catalog.html +++ b/store/templates/catalog.html @@ -100,7 +100,7 @@ {{ infos['manifest']['description']['en'] }}

{% if infos['category'] %} - + {{ catalog['categories'][infos['category']]['title'][locale].lower() }} {% endif %} From 83075de5dd7620a93d8d99380745719aed4e0c51 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 17 Aug 2023 13:57:32 +0200 Subject: [PATCH 03/51] appstore: implement a proper config mechanism --- store/.gitignore | 1 + store/app.py | 26 +++++++++++++++++++------- store/config.toml.example | 5 +++++ 3 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 store/.gitignore create mode 100644 store/config.toml.example diff --git a/store/.gitignore b/store/.gitignore new file mode 100644 index 00000000..5b6c0960 --- /dev/null +++ b/store/.gitignore @@ -0,0 +1 @@ +config.toml diff --git a/store/app.py b/store/app.py index d9cf32d9..6837588f 100644 --- a/store/app.py +++ b/store/app.py @@ -1,4 +1,4 @@ -from flask import Flask, send_from_directory, render_template, session, redirect, request +import toml import base64 import hashlib import hmac @@ -6,16 +6,28 @@ import os import random import urllib import json -from settings import DISCOURSE_SSO_SECRET, DISCOURSE_SSO_ENDPOINT, CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE +import sys +from flask import Flask, send_from_directory, render_template, session, redirect, request + app = Flask(__name__) - -app.debug = True -app.config["DEBUG"] = True -app.config['TEMPLATES_AUTO_RELOAD'] = True - catalog = json.load(open("apps.json")) catalog['categories'] = {c['id']:c for c in catalog['categories']} +try: + config = toml.loads(open("config.toml").read()) + DISCOURSE_SSO_SECRET = config["DISCOURSE_SSO_SECRET"] + DISCOURSE_SSO_ENDPOINT = config["DISCOURSE_SSO_ENDPOINT"] + CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE = config["CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE"] +except Exception as e: + print("You should create a config.toml with the appropriate key/values, cf config.toml.example") + print(e) + sys.exit(1) + +if config.get("DEBUG"): + app.debug = True + app.config["DEBUG"] = True + app.config['TEMPLATES_AUTO_RELOAD'] = True + category_color = { "synchronization": "sky", "publishing": "yellow", diff --git a/store/config.toml.example b/store/config.toml.example new file mode 100644 index 00000000..f6e56171 --- /dev/null +++ b/store/config.toml.example @@ -0,0 +1,5 @@ +# This secret is configured in Discourse +DISCOURSE_SSO_SECRET = "abcdefghijklmnopqrstuvwxyz1234567890" +DISCOURSE_SSO_ENDPOINT = "https://forum.yunohost.org/session/sso_provider" +CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE = "http://localhost:5000/sso_login_callback" +DEBUG = false From 1d7b1fe32a81ffaff83aec735a3662597adbe8aa Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 17 Aug 2023 19:16:30 +0200 Subject: [PATCH 04/51] appstore: add wishlist.toml --- wishlist.toml | 1349 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1349 insertions(+) create mode 100644 wishlist.toml diff --git a/wishlist.toml b/wishlist.toml new file mode 100644 index 00000000..dad6a175 --- /dev/null +++ b/wishlist.toml @@ -0,0 +1,1349 @@ +[access-to-memory-atom] +name = "Access to Memory (AtoM)" +description = "Standards-based archival description and access in a multilingual, multi-repository environment." +upstream = "https://github.com/artefactual/atom" +website = "https://www.accesstomemory.org/" + +[ajenti] +name = "Ajenti" +description = "A modular server admin panel" +upstream = "https://github.com/ajenti/ajenti/" +website = "https://ajenti.org" + +[akaunting] +name = "Akaunting" +description = "Manage payments/invoices/expenses" +upstream = "https://github.com/akaunting/akaunting" +website = "" + +[amara] +name = "Amara" +description = "Collaborative translation of subtitles for videosCollaborative translation of subtitles for videos" +upstream = "https://gitlab.com/hanklank/amara-archive" +website = "https://amara.org" + +[anki-sync-server] +name = "Anki Sync Server" +description = "a personal Anki server" +upstream = "https://github.com/ankicommunity/anki-sync-server" +website = "" + +[anonaddy] +name = "AnonAddy" +description = "Anonymous email forwarding - Create Unlimited Email Aliases" +upstream = "https://github.com/anonaddy/anonaddy" +website = "https://anonaddy.com/" + +[ansible-matrix-docker-deploy] +name = "Ansible Matrix Docker Deploy" +description = "Full Featured Matrix Server Setup with All Bridges and Integrations" +upstream = "https://github.com/spantaleev/matrix-docker-ansible-deploy" +website = "" + +[apache-superset] +name = "Apache Superset" +description = "A Data Visualization and Data Exploration Platform" +upstream = "https://github.com/apache/superset" +website = "https://superset.apache.org/" + +[appflowy] +name = "Appflowy" +description = "Alternative to Notion" +upstream = "https://github.com/AppFlowy-IO/appflowy" +website = "https://appflowy.io/" + +[archivematica] +name = "Archivematica" +description = "Mature digital preservation system designed to maintain standards-based, long-term access to collections of digital objects." +upstream = "https://github.com/artefactual/archivematica" +website = "https://www.archivematica.org/" + +[archivesspace] +name = "ArchivesSpace" +description = "Archives information management application for managing and providing Web access to archives, manuscripts and digital objects." +upstream = "https://github.com/archivesspace/archivesspace" +website = "https://archivesspace.org/" + +[ass] +name = "ass" +description = "ShareX upload server written in Node.js." +upstream = "https://github.com/tycrek/ass" +website = "" + +[astral] +name = "Astral" +description = "Organize Your GitHub Stars With Ease" +upstream = "https://github.com/astralapp/astral" +website = "https://astralapp.com/" + +[asqatasun] +name = "Asqatasun" +description = "Website analyser for web accessibility and SEO" +upstream = "https://gitlab.com/asqatasun/Asqatasun" +website = "https://asqatasun.org/" + +[azuracast] +name = "Azuracast" +description = "A Web Radio Management Suite" +upstream = "https://github.com/AzuraCast/AzuraCast" +website = "https://azuracast.com/" + +[backstage-io] +name = "Backstage.io" +description = "Enterprise Developer Portal with Eco-System" +upstream = "https://github.com/backstage/backstage" +website = "https://backstage.io/" + +[baserow] +name = "Baserow" +description = "No-code database tool, alternative to Airtable" +upstream = "https://gitlab.com/bramw/baserow" +website = "https://baserow.io/" + +[beatbump] +name = "Beatbump" +description = "An alternative frontend for YouTube Music" +upstream = "https://github.com/snuffyDev/Beatbump" +website = "https://beatbump.ml/home" + +[beeper] +name = "Beeper" +description = "A unified inbox for 15 chat networks." +upstream = "https://gitlab.com/beeper" +website = "https://www.beeper.com/" + +[bigbluebutton] +name = "BigBlueButton" +description = "Web conferencing system" +upstream = "https://github.com/bigbluebutton/bigbluebutton" +website = "https://bigbluebutton.org" + +[bitcartcc] +name = "BitcartCC" +description = "All-in-one cryptocurrency solution" +upstream = "https://github.com/bitcartcc/bitcart" +website = "https://bitcartcc.com" + +[bitmessage] +name = "Bitmessage" +description = "P2P communication protocol used to send encrypted messages" +upstream = "https://github.com/Bitmessage/PyBitmessage" +website = "https://bitmessage.org/" + +[blynk] +name = "Blynk" +description = "Blynk library for embedded hardware. Works with Arduino, ESP8266, Raspberry Pi, Intel Edison/Galileo, LinkIt ONE, Particle Core/Photon, Energia, ARM mbed, etc." +upstream = "https://github.com/blynkkk/blynk-library" +website = "" + +[borgwarehouse] +name = "BorgWarehouse" +description = "A fast and modern WebUI for a BorgBackup's central repository server" +upstream = "https://github.com/ravinou/borgwarehouse" +website = "https://borgwarehouse.com" + +[btcpay-server] +name = "BTCPay Server" +description = "Bitcoin payment processor" +upstream = "https://github.com/btcpayserver/btcpayserver" +website = "https://btcpayserver.org" + +[budibase] +name = "Budibase" +description = "Low code platform for creating internal apps, workflows, and admin panels in minutes." +upstream = "https://github.com/Budibase/budibase" +website = "https://budibase.com/" + +[cactus-comments] +name = "Cactus Comments" +description = "Federated comment system, to embed into your webpages, based on the Matrix protocol." +upstream = "https://gitlab.com/cactus-comments" +website = "https://cactus.chat/" + +[cagette] +name = "Cagette" +description = "A marketplace for local farmers and producers" +upstream = "https://github.com/CagetteNet/cagette" +website = "https://www.cagette.net/" + +[cal-com] +name = "Cal.com" +description = "Formerly Calendso. Volunteer shift management and meeting scheduling. Alternative to Calendly." +upstream = "https://github.com/calcom/cal.com" +website = "https://cal.com/" + +[changedetection-io] +name = "changedetection.io" +description = "Monitor changes in web pages" +upstream = "https://github.com/dgtlmoon/changedetection.io" +website = "" + +[chaskiq] +name = "Chaskiq" +description = "A full featured Live Chat, Support & Marketing platform, alternative to Intercom, Drift, Crisp" +upstream = "https://github.com/chaskiq/chaskiq" +website = "" + +[chatterbox] +name = "Chatterbox" +description = "Embedded live chat for customer service" +upstream = "https://github.com/vector-im/chatterbox" +website = "https://element.io/solutions/chatterbox-embedded-live-chat-for-customer-service" + +[chatwoot] +name = "Chatwoot" +description = "Customer engagement suite, an alternative to Intercom, Zendesk, Salesforce Service Cloud" +upstream = "https://github.com/chatwoot/chatwoot" +website = "https://www.chatwoot.com/docs/self-hosted/" + +[checkmk] +name = "Checkmk" +description = "Monitoring for networks, servers, clouds, containers and applications" +upstream = "https://github.com/tribe29/checkmk" +website = "https://checkmk.com/" + +[checkup] +name = "CheckUp" +description = "Distributed, lock-free, self-hosted health checks and status pages" +upstream = "https://github.com/sourcegraph/checkup" +website = "https://sourcegraph.github.io/checkup" + +[ckan] +name = "CKAN" +description = "A tool for making open data websites" +upstream = "https://github.com/ckan/ckan" +website = "https://ckan.org/" + +[cloudtube] +name = "CloudTube" +description = "CloudTube front-end for YouTube" +upstream = "https://git.sr.ht/~cadence/cloudtube" +website = "https://tube.cadence.moe/" + +[commafeed] +name = "Commafeed" +description = "RSS reader" +upstream = "https://github.com/Athou/commafeed" +website = "https://www.commafeed.com/" + +[coquelicot] +name = "Coquelicot" +description = "A “one-click” file sharing web application" +upstream = "" +website = "https://coquelicot.potager.org/" + +[cusdis] +name = "Cusdis" +description = "A lightweight, privacy-friendly comment system alternative to Disqus." +upstream = "https://github.com/djyde/cusdis" +website = "https://cusdis.com/" + +[dataverse] +name = "Dataverse" +description = "Find, share, cite, and preserve research data " +upstream = "https://github.com/IQSS/dataverse" +website = "https://dataverse.org" + +[davmail] +name = "DavMail" +description = "Gateway from OWA and O365 to IMAP, POP, and CalDav for email and calendars" +upstream = "https://github.com/mguessan/davmail" +website = "http://davmail.sourceforge.net/" + +[docker-registry] +name = "Docker-registry" +description = "The toolkit to pack, ship, store, and deliver container content" +upstream = "https://github.com/docker/distribution/" +website = "" + +[docspell] +name = "Docspell" +description = "Simple document organizer" +upstream = "https://github.com/eikek/docspell" +website = "" + +[docusaurus] +name = "Docusaurus" +description = "Static site generator/SPA to build documentations" +upstream = "https://github.com/facebook/docusaurus" +website = "" + +[drawpile] +name = "Drawpile" +description = "Collaborative drawing program that allows multiple users to sketch on the same canvas simultaneously" +upstream = "https://github.com/drawpile/Drawpile" +website = "https://drawpile.net" + +[earthstar-project] +name = "Earthstar-Project" +description = "Storage for private, distributed, offline-first applications. " +upstream = "https://github.com/earthstar-project/earthstar" +website = "https://earthstar-project.org/" + +[element-call] +name = "Element Call" +description = "Showcase for full mesh video chat powered by Matrix" +upstream = "https://github.com/vector-im/element-call" +website = "https://element.io/blog/element-call-beta-2-encryption-spatial-audio-walkie-talkie-mode-and-more/" + +[elk] +name = "Elk" +description = "A nimble Mastodon web client, also works with other Fediverse servers" +upstream = "https://github.com/elk-zone/elk" +website = "https://elk.zone" + +[endlessh] +name = "Endlessh" +description = "SSH Tarpit" +upstream = "https://github.com/skeeto/endlessh" +website = "" + +[erine-email] +name = "erine.email" +description = "" +upstream = "https://gitlab.com/mdavranche/erine.email" +website = "https://erine.email/" + +[erpnext] +name = "ERPnext" +description = "Enterprise Resource Planning (ERP)" +upstream = "https://github.com/frappe/erpnext" +website = "https://erpnext.com/" + +[etesync] +name = "EteSync" +description = "The Etebase server (so you can run your own)" +upstream = "https://github.com/etesync/server" +website = "https://www.etesync.com/" + +[excalibur] +name = "Excalibur" +description = "A web interface to extract tabular data from PDFs (based on Camelot)" +upstream = "https://github.com/camelot-dev/excalibur" +website = "https://excalibur-py.readthedocs.io/en/master/" + +[farside] +name = "Farside" +description = "A redirecting service for FOSS alternative frontends" +upstream = "https://github.com/benbusby/farside" +website = "https://farside.link/" + +[federated-wiki] +name = "Federated wiki" +description = "Farm for fedwiki sites" +upstream = "https://github.com/fedwiki/wiki-server" +website = "http://fed.wiki.org/view/welcome-visitors/view/federated-wiki" + +[filestash] +name = "Filestash" +description = "A modern web client for SFTP, S3, FTP, WebDAV, Git, Minio, LDAP, CalDAV, CardDAV, Mysql, Backblaze, ..." +upstream = "https://github.com/mickael-kerjean/filestash" +website = "https://www.filestash.app/" + +[fishnet] +name = "fishnet" +description = "Distributed Stockfish analysis for lichess.org" +upstream = "https://github.com/niklasf/fishnet" +website = "https://lichess.org/get-fishnet" + +[flaresolverr] +name = "FlareSolverr" +description = "Proxy server to bypass Cloudflare protection" +upstream = "https://github.com/FlareSolverr/FlareSolverr" +website = "" + +[forem] +name = "Forem" +description = "Software for building communities." +upstream = "https://github.com/forem/selfhost" +website = "https://www.forem.com/" + +[fractale] +name = "Fractale" +description = "Platform for self-organization." +upstream = "https://github.com/fractal6/fractal6.go" +website = "https://fractale.co/" + +[framaestro-hub] +name = "Framaestro_hub" +description = "Online service aggregator hub" +upstream = "https://github.com/mozilla/togetherjs" +website = "" + +[freescout] +name = "Freescout" +description = "Helpdesk & Shared Mailbox" +upstream = "https://github.com/freescout-helpdesk/freescout" +website = "https://freescout.net/" + +[gancio] +name = "Gancio" +description = "" +upstream = "https://framagit.org/les/gancio" +website = "https://gancio.org/" + +[gatsby] +name = "Gatsby" +description = "Build blazing fast, modern apps and websites with React" +upstream = "https://github.com/gatsbyjs/gatsby" +website = "https://www.gatsbyjs.com/" + +[geneweb] +name = "Geneweb" +description = "Genealogy in a web interface" +upstream = "https://github.com/geneweb/geneweb" +website = "https://geneweb.tuxfamily.org" + +[goaccess] +name = "Goaccess" +description = "Web log analyzer" +upstream = "https://github.com/allinurl/goaccess" +website = "https://goaccess.io" + +[goatcounter] +name = "GoatCounter" +description = "privacy-friendly web analytics" +upstream = "https://github.com/arp242/goatcounter" +website = "https://www.goatcounter.com/" + +[gocd] +name = "gocd" +description = "CI/CD server" +upstream = "https://github.com/gocd/gocd" +website = "https://go.cd" + +[gollum] +name = "Gollum" +description = "A simple Git-powered wiki" +upstream = "https://github.com/gollum/gollum" +website = "" + +[granary] +name = "Granary" +description = "💬 The social web translator" +upstream = "https://github.com/snarfed/granary" +website = "" + +[graphhopper] +name = "Graphhopper" +description = "Routing engine for OpenStreetMap. Use it as Java library or standalone web server." +upstream = "https://github.com/graphhopper/graphhopper" +website = "https://www.graphhopper.com/" + +[greenlight] +name = "Greenlight" +description = "A really simple end-user interface for your BigBlueButton server" +upstream = "https://github.com/bigbluebutton/greenlight" +website = "https://blabla.aquilenet.fr/b" + +[grist] +name = "Grist" +description = "The evolution of spreadsheets" +upstream = "https://github.com/gristlabs/grist-core/" +website = "https://www.getgrist.com/" + +[habitica] +name = "Habitica" +description = "A habit tracker app which treats your goals like a Role Playing Game." +upstream = "https://github.com/HabitRPG/habitica" +website = "https://habitica.com/" + +[helpy] +name = "Helpy" +description = "A modern helpdesk customer support app, including knowledgebase, discussions and tickets" +upstream = "https://github.com/helpyio/helpy" +website = "" + +[hexo] +name = "Hexo" +description = "A fast, simple & powerful blog framework, powered by Node.js." +upstream = "https://github.com/hexojs/hexo" +website = "https://hexo.io/" + +[histopad] +name = "HistoPad" +description = "Log pads (etherpad) and archiving them in a git repository" +upstream = "https://github.com/24eme/histopad" +website = "" + +[hometown] +name = "Hometown" +description = "A Mastodon fork with local-only posting, support for more content types, and other features and tweaks." +upstream = "https://github.com/hometown-fork/hometown" +website = "" + +[hyperion] +name = "Hyperion" +description = "Ambient lightning software" +upstream = "https://github.com/hyperion-project/hyperion.ng" +website = "https://docs.hyperion-project.org/" + +[hypothes-is] +name = "Hypothes.is" +description = "Annotation server (and client) to create and share highlights and notes" +upstream = "https://github.com/hypothesis/h" +website = "https://hypothes.is" + +[icecast-2] +name = "Icecast 2" +description = "" +upstream = "https://gitlab.xiph.org/xiph/icecast-server/" +website = "https://www.icecast.org" + +[infcloud] +name = "InfCloud" +description = "A contacts, calendar and tasks web client for CalDAV and CardDAV" +upstream = "https://inf-it.com/open-source/download/InfCloud_0.13.1.zip" +website = "https://inf-it.com/open-source/clients/infcloud/" + +[inventaire] +name = "Inventaire" +description = "A collaborative resource mapper powered by open-knowledge, starting with books!" +upstream = "https://github.com/inventaire/inventaire" +website = "https://inventaire.io" + +[invoiceplane] +name = "InvoicePlane" +description = "Manage invoices, clients and payments." +upstream = "https://github.com/InvoicePlane/InvoicePlane" +website = "https://invoiceplane.com" + +[ipfs] +name = "IPFS" +description = "Peer-to-peer hypermedia protocol" +upstream = "https://github.com/ipfs/ipfs" +website = "https://ipfs.io" + +[joplin] +name = "Joplin" +description = "Note taking and to-do application with synchronisation capabilities for Windows, macOS, Linux, Android and iOS." +upstream = "https://github.com/laurent22/joplin" +website = "https://joplin.cozic.net/" + +[js-bin] +name = "JS Bin" +description = "Collaborative JavaScript Debugging App" +upstream = "https://github.com/jsbin/jsbin" +website = "https://jsbin.com/" + +[karaoke-forever] +name = "Karaoke-forever" +description = "Organize karaoke parties" +upstream = "https://github.com/bhj/karaoke-forever" +website = "https://www.karaoke-forever.com/" + +[kill-the-newsletter] +name = "Kill the newsletter" +description = "Convert email newsletters to RSS feeds" +upstream = "https://github.com/leafac/kill-the-newsletter.com" +website = "https://kill-the-newsletter.com/" + +[kitchenowl] +name = "Kitchenowl" +description = "Grocery list and recipe manager" +upstream ="https://github.com/TomBursch/kitchenowl" +website = "https://kitchenowl.org/" + +[klaxon] +name = "Klaxon" +description = "Easily create alerts for changes on the web" +upstream = "https://github.com/themarshallproject/klaxon" +website = "https://newsklaxon.org" + +[known] +name = "Known" +description = "A social publishing platform." +upstream = "https://github.com/idno/known" +website = "https://withknown.com" + +[koel] +name = "Koel" +description = "🐦 A personal music streaming server that works." +upstream = "https://github.com/phanan/koel" +website = "https://koel.phanan.net" + +[koha] +name = "Koha" +description = "Library system" +upstream = "https://git.koha-community.org/Koha-community/Koha" +website = "https://koha-community.org/" + +[l-atelier] +name = "L'atelier" +description = "A project management tool" +upstream = "https://github.com/jbl2024/latelier" +website = "" + +[lesspass] +name = "LessPass" +description = "Stateless password manager" +upstream = "https://github.com/lesspass/lesspass" +website = "https://www.lesspass.com/" + +[lichen] +name = "Lichen" +description = "Gemtext to HTML translator" +upstream = "https://git.sensorstation.co/lichen.git" +website = "" + +[lila] +name = "Lila" +description = "Online chess game server" +upstream = "https://github.com/ornicar/lila" +website = "https://lichess.org/" + +[lingva-translate] +name = "Lingva Translate" +description = "Alternative front-end for Google Translate" +upstream = "https://github.com/TheDavidDelta/lingva-translate" +website = "https://lingva.ml/" + +[liquidsoap] +name = "LiquidSoap" +description = "Audio and video streaming language" +upstream = "https://github.com/savonet/liquidsoap" +website = "https://www.liquidsoap.info/" + +[locomotivecms] +name = "LocomotiveCMS" +description = "A platform to create, publish and edit sites" +upstream = "https://github.com/locomotivecms/engine" +website = "" + +[logitech-media-server] +name = "Logitech Media Server" +description = "A streaming audio server (formerly SlimServer, SqueezeCenter and Squeezebox Server)" +upstream = "http://mysqueezebox.com/download" +website = "https://en.wikipedia.org/wiki/Logitech_Media_Server" + +[loomio] +name = "Loomio" +description = "A collaborative decision making tool" +upstream = "https://github.com/loomio/loomio/" +website = "https://www.loomio.org" + +[maidsafe] +name = "MaidSafe" +description = "The Safe Network Core. API message definitions, routing and nodes, client core api." +upstream = "https://github.com/maidsafe/safe_network" +website = "https://maidsafe.net" + +[mailpile] +name = "Mailpile" +description = "A modern, fast email client with user-friendly encryption and privacy features" +upstream = "https://github.com/mailpile/Mailpile" +website = "https://www.mailpile.is" + +[mailtrain] +name = "Mailtrain" +description = "Newsletter app" +upstream = "https://github.com/Mailtrain-org/mailtrain" +website = "https://mailtrain.org/" + +[majola] +name = "Majola" +description = "Music scrobble database, alternative to Last.fm" +upstream = "https://github.com/krateng/maloja" +website = "https://maloja.krateng.ch" + +[mautrix-discord] +name = "Mautrix-Discord" +description = "Matrix bridge for Discord" +upstream = "https://github.com/mautrix/discord" +website = "" + +[mealie] +name = "Mealie" +description = "Recipe manager and meal planner" +upstream = "https://github.com/hay-kot/mealie/" +website = "https://hay-kot.github.io/mealie/" + +[mediagoblin] +name = "Mediagoblin" +description = "Video streaming platform" +upstream = "https://savannah.gnu.org/projects/mediagoblin" +website = "https://mediagoblin.org/" + +[medusa] +name = "Medusa" +description = "Automatic TV shows downloader" +upstream = "" +website = "https://pymedusa.com/" + +[megaglest] +name = "Megaglest" +description = "realtime stategy game" +upstream = "https://megaglest.org/linux-packages.html" +website = "https://megaglest.org/" + +[meshery] +name = "Meshery" +description = "Cloudnative solution to bind multiple Service-Meshes together, not only K8s" +upstream = "https://github.com/meshery/meshery" +website = "https://meshery.io/" + +[microblog-pub] +name = "microblog.pub" +description = "A single-user ActivityPub-powered microblog." +upstream = "https://github.com/tsileo/microblog.pub" +website = "" + +[mindustry] +name = "Mindustry" +description = "A sandbox tower-defense game" +upstream = "https://github.com/Anuken/Mindustry" +website = "https://mindustrygame.github.io/" + +[modoboa] +name = "Modoboa" +description = "Mail hosting made simple" +upstream = "https://github.com/modoboa/modoboa" +website = "https://modoboa.org" + +[motioneye] +name = "MotionEye" +description = "A web frontend for the motion daemon" +upstream = "https://github.com/ccrisan/motioneye" +website = "" + +[nebula] +name = "Nebula" +description = "Scalable overlay networking tool with a focus on performance, simplicity and security." +upstream = "https://github.com/slackhq/nebula" +website = "https://nebula.defined.net/docs/" + +[netbird] +name = "Netbird" +description = "Create an overlay peer-to-peer network connecting machines regardless of their location" +upstream = "https://github.com/netbirdio/netbird" +website = "https://netbird.io/" + +[netlify-cms] +name = "Netlify CMS" +description = "A CMS for any static site generator that connects to a Gitlab/Github repo (requires netlify/gotrue)" +upstream = "https://github.com/netlify/netlify-cms" +website = "https://netlifycms.org/" + +[netrunner] +name = "Netrunner" +description = "A card game in a cyberpunk universe" +upstream = "https://github.com/mtgred/netrunner" +website = "" + +[newsblur] +name = "NewsBlur" +description = "RSS reader" +upstream = "https://github.com/samuelclay/NewsBlur" +website = "https://www.newsblur.com" + +[nostr] +name = "Nostr" +description = "Censorship-resistant alternative to Twitter" +upstream = "https://github.com/nostr-protocol/nostr" +website = "" + +[ohmyform] +name = "OhMyForm" +description = "Alternative to TypeForm, TellForm, or Google Forms" +upstream = "https://github.com/ohmyform/ohmyform" +website = "" + +[ombi] +name = "Ombi" +description = "Want a Movie or TV Show on Plex/Emby/Jellyfin? Use Ombi!" +upstream = "https://github.com/tidusjar/Ombi" +website = "" + +[omnivore] +name = "Omnivore" +description = "A read-it-later solution for people who like reading." +upstream = "https://github.com/omnivore-app/omnivore" +website = "https://omnivore.app" + +[opencart] +name = "OpenCart" +description = "Shopping cart system. An online e-commerce solution." +upstream = "https://github.com/opencart/opencart" +website = "https://www.opencart.com" + +[openhab] +name = "openHAB" +description = "Smart home platform" +upstream = "https://github.com/openhab/openhab-webui" +website = "https://www.openhab.org/" + +[osrm] +name = "OSRM" +description = "Routing Machine - C++ backend" +upstream = "https://github.com/Project-OSRM/osrm-backend" +website = "" + +[ox-open-xchange] +name = "OX Open-Xchange" +description = "Linux groupware solution" +upstream = "https://github.com/open-xchange/appsuite-frontend" +website = "https://www.open-xchange.com" + +[padloc] +name = "Padloc" +description = "Simple, secure password and data management for individuals and teams" +upstream = "https://github.com/padloc/padloc" +website = "https://padloc.app/" + +[paperless-ng] +name = "Paperless-ng" +description = "A supercharged version of paperless: scan, index and archive all your physical documents" +upstream = "https://github.com/jonaswinkler/paperless-ng" +website = "" + +[paperwork] +name = "Paperwork" +description = "Note-taking and archiving, alternative to Evernote, Microsoft OneNote & Google Keep" +upstream = "https://github.com/paperwork/paperwork" +website = "https://paperwork.cloud" + +[passbolt] +name = "Passbolt" +description = "Password manager" +upstream = "https://github.com/passbolt/passbolt_docker" +website = "https://www.passbolt.com" + +[penpot] +name = "Penpot" +description = "Design Freedom for Teams" +upstream = "https://github.com/penpot/penpot" +website = "https://penpot.app/" + +[personal-management-system] +name = "personal-management-system" +description = "Your web application for managing personal data." +upstream = "https://github.com/Volmarg/personal-management-system" +website = "" + +[phplist] +name = "PHPList" +description = "Email marketing manager: create, send, integrate, and analyze email campaigns and newsletters." +upstream = "https://github.com/phpList/phplist3" +website = "https://www.phplist.com" + +[pia] +name = "PIA" +description = "A tool to help carrying out Privacy Impact Assessments" +upstream = "https://github.com/LINCnil/pia" +website = "" + +[picsur] +name = "Picsur" +description = "Image hosting" +upstream = "https://github.com/rubikscraft/Picsur" +website = "https://picsur.org/" + +[pinry] +name = "Pinry" +description = "Tiling image board" +upstream = "https://github.com/pinry/pinry/" +website = "https://docs.getpinry.com/" + +[piped] +name = "Piped" +description = "An alternative frontend for YouTube which is efficient by design." +upstream = "https://github.com/TeamPiped/Piped" +website = "https://github.com/TeamPiped/Piped/wiki/Instances" + +[planka] +name = "Planka" +description = "Kanban board for workgroups." +upstream = "https://github.com/plankanban/planka" +website = "https://planka.app/" + +[plausible-analytics] +name = "Plausible Analytics" +description = "Privacy-friendly web analytics (alternative to Google Analytics)" +upstream = "https://github.com/plausible/analytics" +website = "https://plausible.io" + +[protonmails-webclient] +name = "ProtonMail’s WebClient" +description = "Monorepo hosting the proton web clients" +upstream = "https://github.com/ProtonMail/WebClient" +website = "" + +[psono] +name = "Psono" +description = "Password Manager for Teams" +upstream = "https://gitlab.com/psono/psono-server" +website = "https://psono.com/" + +[pterodactyl] +name = "Pterodactyl" +description = "" +upstream = "" +website = "https://pterodactyl.io/" + +[qgis-server] +name = "QGis server" +description = "Publish QGis desktop projets and maps as OGC-compliant services, that can be used in openlayers, leaflet etc." +upstream = "https://github.com/qgis/QGIS" +website = "https://qgis.org/fr/site/" + +[qwc2] +name = "QWC2" +description = "A react and openlayers-based web UI to publish and display QGIS desktop projects." +upstream = "https://github.com/qgis/qwc2" +website = "" + +[race-for-the-galaxy] +name = "Race for the galaxy" +description = "Play Race for the Galaxy against AI" +upstream = "https://github.com/bnordli/rftg" +website = "" + +[racktables] +name = "racktables" +description = "A datacenter asset management system" +upstream = "https://github.com/RackTables/racktables" +website = "https://racktables.org" + +[raindrop] +name = "Raindrop" +description = "All-in-one bookmark manager" +upstream = "https://github.com/raindropio/app" +website = "https://raindrop.io" + +[raspap] +name = "Raspap" +description = "Simple wireless AP setup & management for Debian-based devices" +upstream = "https://github.com/RaspAP/raspap-webgui" +website = "https://raspap.com/" + +[redash] +name = "Redash" +description = "Connect to any data source, easily visualize, dashboard and share your data." +upstream = "https://github.com/getredash/redash" +website = "" + +[renovate] +name = "Renovate" +description = "Bot for automating dependency updates on Gitlab / Gitea / Forgejo" +upstream = "https://github.com/renovatebot/renovate" +website = "https://www.mend.io/renovate/" + +[request-tracker] +name = "Request Tracker" +description = "An enterprise-grade issue tracking system" +upstream = "https://github.com/bestpractical/rt" +website = "https://bestpractical.com" + +[restya] +name = "Restya" +description = "Trello like kanban board. Based on Restya platform." +upstream = "https://github.com/RestyaPlatform/board/" +website = "https://restya.com" + +[retroshare] +name = "Retroshare" +description = "Friend-2-Friend, secure decentralised communication platform." +upstream = "https://github.com/RetroShare/RetroShare" +website = "https://retroshare.cc/" + +[revolt] +name = "Revolt" +description = "Chat software similar to Discord" +upstream = "https://github.com/revoltchat/self-hosted" +website = "https://revolt.chat/" + +[rss-proxy] +name = "RSS-proxy" +description = "Create an RSS or ATOM feed of almost any website, just by analyzing just the static HTML structure." +upstream = "https://github.com/damoeb/rss-proxy" +website = "" + +[rsshub] +name = "RSSHub" +description = "Extensible RSS feed generator, generate RSS feeds from pretty much everything" +upstream = "https://github.com/DIYgod/RSSHub" +website = "" + +[rustdesk] +name = "RustDesk" +description = "TeamViewer alternative" +upstream = "https://github.com/rustdesk/rustdesk-server" +website = "https://rustdesk.com/server" + +[sat] +name = "SAT" +description = "An all-in-one tool to manage all your communications" +upstream = "" +website = "https://salut-a-toi.org" + +[sabnzbd] +name = "SABnzbd" +descrition = "The automated Usenet download tool" +upstream = "https://github.com/sabnzbd/sabnzbd" +website = "https://sabnzbd.org/" + +[screego] +name = "Screego" +description = "Screen sharing webrtc" +upstream = "https://github.com/screego/server" +website = "https://screego.net/" + +[scribe] +name = "Scribe" +description = "An alternative frontend to Medium" +upstream = "https://git.sr.ht/~edwardloveall/scribe" +website = "https://scribe.rip/" + +[semantic-mediawiki] +name = "Semantic MediaWiki" +description = "Store and query data withxadin MediaWiki's pages" +upstream = "https://github.com/SemanticMediaWiki/SemanticMediaWiki" +website = "https://www.semantic-mediawiki.org/wiki/Semantic_MediaWiki" + +[semaphore] +name = "Semaphore" +description = "A fediverse (Mastodon-API compatible) accessible, simple and fast web client" +upstream = "https://github.com/NickColley/semaphore" +website = "" + +[shadowsocks] +name = "shadowsocks" +description = "A SOCKS5 proxy to protect your Internet traffic" +upstream = "" +website = "https://shadowsocks.org" + +[shinken] +name = "shinken" +description = "A flexible and scalable monitoring framework" +upstream = "https://github.com/naparuba/shinken" +website = "" + +[sickrage] +name = "sickrage" +description = "Automatic TV shows downloader" +upstream = "" +website = "https://sickchill.github.io/" + +[signal-proxy] +name = "Signal Proxy" +description = "Fight censorship and bypass traffic securely to the Signal service" +upstream = "https://github.com/signalapp/Signal-TLS-Proxy" +website = "https://signal.org/blog/help-iran-reconnect/" + +[simplelogin] +name = "SimpleLogin" +description = "Privacy-first e-mail forwarding and identity provider service" +upstream = "https://github.com/simple-login/app" +website = "https://simplelogin.io" + +[smokeping] +name = "smokeping" +description = "The Active Monitoring System" +upstream = "https://github.com/oetiker/SmokePing" +website = "https://oss.oetiker.ch/smokeping/" + +[socialhome] +name = "SocialHome" +description = "A federated personal profile" +upstream = "https://github.com/jaywink/socialhome" +website = "https://socialhome.network" + +[sphinx] +name = "sphinx" +description = "The Sphinx documentation generator" +upstream = "https://github.com/sphinx-doc/sphinx" +website = "" + +[spreed] +name = "Spreed" +description = "Standalone signaling server for Nextcloud Talk." +upstream = "https://github.com/strukturag/nextcloud-spreed-signaling" +website = "" + +[stackedit] +name = "Stackedit" +description = "In-browser Markdown editor" +upstream = "https://github.com/benweet/stackedit" +website = "https://stackedit.io" + +[storj] +name = "Storj" +description = "Ongoing Storj v3 development. Decentralized cloud object storage that is affordable, easy to use, private, and secure." +upstream = "https://github.com/storj/storj" +website = "https://www.storj.io/node" + +[strapi] +name = "Strapi" +description = "Node.js Headless CMS to easily build customisable APIs" +upstream = "https://github.com/strapi/strapi" +website = "https://strapi.io" + +[stremio] +name = "Stremio" +description = "A modern media center" +upstream = "https://github.com/Stremio/stremio-web" +website = "https://strem.io" + +[suitecrm] +name = "SuiteCRM" +description = "A CRM software" +upstream = "https://github.com/salesagility/SuiteCRM" +website = "https://suitecrm.com/" + +[superalgos] +name = "Superalgos" +description = "Crypto trading bot, automated bitcoin / cryptocurrency trading software." +upstream = "https://github.com/Superalgos/Superalgos" +website = "" + +[sympa] +name = "Sympa" +description = "Mailing List manager" +upstream = "" +website = "https://www.sympa.org/" + +[syspass] +name = "Syspass" +description = "Systems Password Manager" +upstream = "https://github.com/nuxsmin/sysPass" +website = "https://www.syspass.org/" + +[tahoe-lafs] +name = "Tahoe-LAFS" +description = "Decentralized cloud storage system" +upstream = "https://github.com/tahoe-lafs/tahoe-lafs" +website = "https://tahoe-lafs.org/" + +[taiga] +name = "Taiga" +description = "" +upstream = "https://github.com/kaleidos-ventures/taiga-back" +website = "https://taiga.io" + +[tailscale] +name = "Tailscale" +description = "Wireguard-based Mesh-VPN" +upstream = "https://github.com/tailscale/tailscale" +website = "https://tailscale.com/" + +[takahe] +name = "Takahē" +description = "An efficient ActivityPub Server, for small installs with multiple domains" +upstream = "https://github.com/jointakahe/takahe" +website = "https://jointakahe.org" + +[taskwarrior] +name = "Taskwarrior" +description = "Command line Task Management" +upstream = "https://github.com/GothenburgBitFactory/taskwarrior" +website = "https://taskwarrior.org" + +[teddy-io] +name = "Teddy.io" +description = "Document manager" +upstream = "https://github.com/sismics/docs" +website = "https://teedy.io/" + +[teleport] +name = "Teleport" +description = "Multi-protocol access proxy which understands SSH, HTTPS, RDP, Kubernetes API, MySQL, MongoDB and PostgreSQL wire protocols." +upstream = "https://github.com/gravitational/teleport" +website = "https://goteleport.com/" + +[theia-ide] +name = "Theia-IDE" +description = "VS Code-like cloud IDE" +upstream = "https://hub.docker.com/r/theiaide/theia-full" +website = "https://theia-ide.org/" + +[tileserver-gl] +name = "Tileserver-GL" +description = "Tile server light SVG for map service" +upstream = "https://github.com/maptiler/tileserver-gl" +website = "https://maps.earth/" + +[tmate] +name = "TMate" +description = "Instant Terminal Sharing" +upstream = "https://github.com/tmate-io/tmate" +website = "https://tmate.io/" + +[traccar] +name = "Traccar" +description = "Modern GPS Tracking Platform" +upstream = "https://github.com/traccar/traccar" +website = "" + +[trivy] +name = "trivy" +description = "OSS Vulnerability and Misconfiguration Scanning." +upstream = "https://github.com/aquasecurity/trivy" +website = "https://www.aquasec.com/products/trivy/" + +[tubesync] +name = "tubesync" +description = "Syncs YouTube channels and playlists to a locally hosted media server" +upstream = "https://github.com/meeb/tubesyn" +website = "" + +[tutao] +name = "tutao" +description = "End-to-end encrypted e-mail client" +upstream = "https://github.com/tutao/tutanota/" +website = "" + +[ultrasonics] +name = "ultrasonics" +description = "Sync music playlists between all your music services: Spotify, Deezer, Apple Music, Plex, etc." +upstream = "https://github.com/XDGFX/ultrasonics" +website = "" + +[umap] +name = "umap" +description = "Cartography software" +upstream = "" +website = "https://umap.openstreetmap.fr/" + +[upmpdcli] +name = "upmpdcli" +description = "" +upstream = "https://framagit.org/medoc92/upmpdcli" +website = "https://www.lesbonscomptes.com/upmpdcli/" + +[uwazi] +name = "Uwazi" +description = "Build and share document collections" +upstream = "https://github.com/huridocs/uwazi" +website = "https://www.uwazi.io/" + +[vpn-server] +name = "VPN server" +description = "Create/provide VPNs from your server" +upstream = "" +website = "https://openvpn.net" + +[webhook-site] +name = "Webhook.site" +description = "Easily test HTTP webhooks with this handy tool that displays requests instantly." +upstream = "https://github.com/fredsted/webhook.site" +website = "https://docs.webhook.site/" + +[webogram] +name = "webogram" +description = "A new era of messaging" +upstream = "https://github.com/zhukov/webogram" +website = "" + +[webterminal] +name = "Webterminal" +description = "A web-based Jump Host / Bastion, supports VNC, SSH, RDP, Telnet, SFTP..." +upstream = "https://github.com/jimmy201602/webterminal/" +website = "" + +[webthings-gateway] +name = "WebThings Gateway" +description = "WebThings Gateway" +upstream = "https://github.com/WebThingsIO/gateway" +website = "https://iot.mozilla.org/gateway/" + +[wg-access-server] +name = "wg-access-server" +description = "VPN Server OIDC ipv4 ipv6" +upstream = "https://github.com/freifunkMUC/wg-access-server" +website = "" + +[whoogle] +name = "Whoogle" +description = "A metasearch engine" +upstream = "https://github.com/benbusby/whoogle-search" +website = "" + +[wikiless] +name = "Wikiless" +description = "An alternative Wikipedia front-end focused on privacy." +upstream = "https://codeberg.org/orenom/wikiless" +website = "https://wikiless.org/" + +[wikisuite] +name = "WikiSuite" +description = "An integrated enterprise solution" +upstream = "https://gitlab.com/wikisuite" +website = "https://wikisuite.org/Software" + +[wildduck] +name = "WildDuck" +description = "Opinionated email server" +upstream = "https://github.com/nodemailer/wildduck" +website = "" + +[wisemapping] +name = "Wisemapping" +description = "An online mind mapping editor" +upstream = "https://bitbucket.org/wisemapping/wisemapping-open-source" +website = "" + +[workadventure] +name = "WorkAdventure" +description = "A web-based collaborative workspace for small to medium teams" +upstream = "https://github.com/thecodingmachine/workadventure" +website = "" + +[xbackbone] +name = "XBackBone" +description = "A file manager that support the instant sharing tool ShareX and NIX systems." +upstream = "https://github.com/SergiX44/XBackBone" +website = "https://xbackbone.app" + +[xbrowsersync] +name = "xBrowserSync" +description = "A bookmark sync tool, with browser plugins and mobile clients available" +upstream = "https://github.com/xbrowsersync/api" +website = "https://www.xbrowsersync.org/" + +[xibo] +name = "Xibo" +description = "A FLOSS digital signage solution" +upstream = "https://github.com/xibosignage/xibo-cms" +website = "" + +[xonotic] +name = "Xonotic" +description = "" +upstream = "https://gitlab.com/xonotic" +website = "https://xonotic.org" + +[yggdrasil] +name = "Yggdrasil" +description = "An experiment in scalable routing as an encrypted IPv6 overlay network" +upstream = "https://github.com/yggdrasil-network/yggdrasil-go" +website = "https://yggdrasil-network.github.io/" + +[your-spotify] +name = "your_spotify" +description = "Spotify tracking dashboard" +upstream = "https://github.com/Yooooomi/your_spotify" +website = "" + +[zammad] +name = "Zammad" +description = "Helpdesk/customer support system" +upstream = "https://github.com/zammad/zammad" +website = "" + +[zigbee2mqtt-io] +name = "zigbee2mqtt.io" +description = "Zigbee-to-MQTT software-bridge supporting more than 1000 Zigbee devices" +upstream = "https://github.com/koenkk/zigbee2mqtt" +website = "https://www.zigbee2mqtt.io/" + +[zoneminder] +name = "Zoneminder" +description = "Closed-circuit television software app supporting IP, USB and Analog cameras. " +upstream = "https://github.com/ZoneMinder/zoneminder" +website = "" + +[zulip] +name = "Zulip" +description = "Team chat that helps teams stay productive and focused." +upstream = "https://github.com/zulip/zulip" +website = "https://zulipchat.com/" From efb4555a5c996976a916e229315a11d25a71c9b3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 18 Aug 2023 03:32:22 +0200 Subject: [PATCH 05/51] appstore: change star+bookmark icons to diamond+star --- store/templates/catalog.html | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/store/templates/catalog.html b/store/templates/catalog.html index 08438f44..4e160d3f 100644 --- a/store/templates/catalog.html +++ b/store/templates/catalog.html @@ -85,14 +85,15 @@ {% if infos['level'] == 0 %} - + {% elif infos['level']|int <= 4 %} - + {% elif infos['level'] == 8 %} - + {% endif %} - - 123 + + 123 + From 1995213f52e3a6172abe858cd169e1df9c8af504 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 18 Aug 2023 03:33:01 +0200 Subject: [PATCH 06/51] appstore: draft add to wishlist form + process --- store/app.py | 118 ++++++++++++++++++++++++++++-- store/config.toml.example | 3 + store/requirements.txt | 3 + store/templates/wishlist.html | 12 +-- store/templates/wishlist_add.html | 68 +++++++++++++++++ 5 files changed, 190 insertions(+), 14 deletions(-) create mode 100644 store/templates/wishlist_add.html diff --git a/store/app.py b/store/app.py index 6837588f..d0cd86da 100644 --- a/store/app.py +++ b/store/app.py @@ -1,3 +1,4 @@ +import re import toml import base64 import hashlib @@ -7,7 +8,9 @@ import random import urllib import json import sys +from slugify import slugify from flask import Flask, send_from_directory, render_template, session, redirect, request +from github import Github, InputGitAuthor app = Flask(__name__) catalog = json.load(open("apps.json")) @@ -15,14 +18,24 @@ catalog['categories'] = {c['id']:c for c in catalog['categories']} try: config = toml.loads(open("config.toml").read()) - DISCOURSE_SSO_SECRET = config["DISCOURSE_SSO_SECRET"] - DISCOURSE_SSO_ENDPOINT = config["DISCOURSE_SSO_ENDPOINT"] - CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE = config["CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE"] except Exception as e: print("You should create a config.toml with the appropriate key/values, cf config.toml.example") - print(e) sys.exit(1) +mandatory_config_keys = [ + "DISCOURSE_SSO_SECRET", + "DISCOURSE_SSO_ENDPOINT", + "CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE", + "GITHUB_LOGIN", + "GITHUB_TOKEN", + "GITHUB_EMAIL", +] + +for key in mandatory_config_keys: + if key not in config: + print(f"Missing key in config.toml: {key}") + sys.exit(1) + if config.get("DEBUG"): app.debug = True app.config["DEBUG"] = True @@ -48,7 +61,7 @@ category_color = { for id_, category in catalog['categories'].items(): category["color"] = category_color[id_] -wishlist = json.load(open("wishlist.json")) +wishlist = toml.load(open("../wishlist.toml")) # This is the secret key used for session signing app.secret_key = ''.join([str(random.randint(0, 9)) for i in range(99)]) @@ -113,6 +126,95 @@ def browse_wishlist(): return render_template("wishlist.html", user=session.get('user', {}), wishlist=wishlist) +@app.route('/wishlist/add', methods=['GET', 'POST']) +def add_to_wishlist(): + if request.method == "POST": + + user = session.get('user', {}) + if not user: + errormsg = "You must be logged in to submit an app to the wishlist" + return render_template("wishlist_add.html", user=session.get('user', {}), errormsg=errormsg) + + name = request.form['name'].strip().replace("\n", "") + description = request.form['description'].strip().replace("\n", "") + upstream = request.form['upstream'].strip().replace("\n", "") + website = request.form['website'].strip().replace("\n", "") + + checks = [ + (len(name) >= 3, "App name should be at least 3 characters"), + (len(name) <= 30, "App name should be less than 30 characters"), + (len(description) >= 5, "App name should be at least 5 characters"), + (len(description) <= 100, "App name should be less than 100 characters"), + (len(upstream) >= 10, "Upstream code repo URL should be at least 10 characters"), + (len(upstream) <= 150, "Upstream code repo URL should be less than 150 characters"), + (len(website) <= 150, "Website URL should be less than 150 characters"), + (re.match(r"^[\w\.\-\(\)\ ]+$", name), "App name contains special characters"), + ] + + for check, errormsg in checks: + if not check: + return render_template("wishlist_add.html", user=session.get('user', {}), errormsg=errormsg) + + slug = slugify(name) + + github = Github((config["GITHUB_LOGIN"], config["GITHUB_TOKEN"])) + author = InputGitAuthor(config["GITHUB_LOGIN"], config["GITHUB_EMAIL"]) + repo = github.get_repo("Yunohost/apps") + current_wishlist_rawtoml = repo.get_contents("wishlist.toml", ref="app-store") # FIXME : ref=repo.default_branch) + current_wishlist_sha = current_wishlist_rawtoml.sha + current_wishlist_rawtoml = current_wishlist_rawtoml.decoded_content.decode() + new_wishlist = toml.loads(current_wishlist_rawtoml) + + if slug in new_wishlist: + return render_template("wishlist_add.html", user=session.get('user', {}), errormsg=f"An entry with the name {slug} already exists in the wishlist") + + new_wishlist[slug] = { + "name": name, + "description": description, + "upstream": upstream, + "website": website, + } + + new_wishlist = dict(sorted(new_wishlist.items())) + new_wishlist_rawtoml = toml.dumps(new_wishlist) + new_branch = f"add-to-wishlist-{slug}" + try: + # Get the commit base for the new branch, and create it + commit_sha = repo.get_branch("app-store").commit.sha # FIXME app-store -> repo.default_branch + repo.create_git_ref(ref=f"refs/heads/{new_branch}", sha=commit_sha) + except: + print("... Branch already exists, skipping") + return False + + message = f"Add {name} to wishlist" + repo.update_file( + "wishlist.toml", + message=message, + content=new_wishlist, + sha=current_wishlist_sha, + branch=new_branch, + author=author, + ) + + # Wait a bit to preserve the API rate limit + time.sleep(1.5) + + body = f""" +### Add {name} to wishlist + +- [ ] Confirm app is self-hostable and generally makes sense to possibly integrate in YunoHost +- [ ] Confirm app's license is opensource/free software (or not-totally-free, case by case TBD) +- [ ] Description describes concisely what the app is/does + """ + + # Open the PR + pr = self.repo.create_pull( + title=message, body=body, head=new_branch, base="app-store" # FIXME app-store -> repo.default_branch + ) + + else: + return render_template("wishlist_add.html", user=session.get('user', {}), errormsg=None) + ################################################ @@ -123,11 +225,11 @@ def create_nonce_and_build_url_to_login_on_discourse_sso(): nonce = ''.join([str(random.randint(0, 9)) for i in range(99)]) - url_data = {"nonce": nonce, "return_sso_url": CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE} + url_data = {"nonce": nonce, "return_sso_url": config["CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE"]} url_encoded = urllib.parse.urlencode(url_data) payload = base64.b64encode(url_encoded.encode()).decode() - sig = hmac.new(DISCOURSE_SSO_SECRET.encode(), msg=payload.encode(), digestmod=hashlib.sha256).hexdigest() + sig = hmac.new(config["DISCOURSE_SSO_SECRET"].encode(), msg=payload.encode(), digestmod=hashlib.sha256).hexdigest() data = {"sig": sig, "sso": payload} - url = f"{DISCOURSE_SSO_ENDPOINT}?{urllib.parse.urlencode(data)}" + url = f"{config['DISCOURSE_SSO_ENDPOINT']}?{urllib.parse.urlencode(data)}" return nonce, url diff --git a/store/config.toml.example b/store/config.toml.example index f6e56171..b029ef74 100644 --- a/store/config.toml.example +++ b/store/config.toml.example @@ -3,3 +3,6 @@ DISCOURSE_SSO_SECRET = "abcdefghijklmnopqrstuvwxyz1234567890" DISCOURSE_SSO_ENDPOINT = "https://forum.yunohost.org/session/sso_provider" CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE = "http://localhost:5000/sso_login_callback" DEBUG = false +GITHUB_LOGIN = "yunohost-bot" +GITHUB_EMAIL = "yunohost [at] yunohost.org" # Replace the [at] by actual @ +GITHUB_TOKEN = "superSecretToken" diff --git a/store/requirements.txt b/store/requirements.txt index 5aad892a..655dbae3 100644 --- a/store/requirements.txt +++ b/store/requirements.txt @@ -1 +1,4 @@ Flask==2.3.2 +python-slugify +PyGithub +toml diff --git a/store/templates/wishlist.html b/store/templates/wishlist.html index a4651183..28f0da7f 100644 --- a/store/templates/wishlist.html +++ b/store/templates/wishlist.html @@ -25,8 +25,8 @@ Add an app to the wishlist @@ -55,7 +55,7 @@ - {% for infos in wishlist %} + {% for app, infos in wishlist.items() %} {{ infos['name'] }} @@ -67,7 +67,7 @@ href="{{ infos['website'] }}" class="inline-block" > - + {% endif %} @@ -77,14 +77,14 @@ href="{{ infos['upstream'] }}" class="inline-block" > - + {% endif %} Vote diff --git a/store/templates/wishlist_add.html b/store/templates/wishlist_add.html new file mode 100644 index 00000000..6d298fb9 --- /dev/null +++ b/store/templates/wishlist_add.html @@ -0,0 +1,68 @@ +{% extends "base.html" %} +{% block main %} +
+

+ Add an application to the wishlist +

+
+ + +
+ + {% if not user %} + + {% endif %} + + + + {% if errormsg %} + + {% endif %} + + +
+ + + + + + + Please be concise and focus on what the app does. No need to repeat "[App] is ...". No need to state that it is free/open-source or self-hosted (otherwise it wouldn't be packaged for YunoHost). Avoid marketing stuff like 'the most', or vague properties like 'easy', 'simple', 'lightweight'. + + + + + + + Please do not just copy-paste the code repository URL. If the project has no proper website, then leave the field empty. + + + +
+
+ +{% endblock %} From 2721f8ae6384e65aa77d5a3bbc7f576297f8cfd2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 21 Aug 2023 15:21:17 +0200 Subject: [PATCH 07/51] appstore: fix add-to-wishlist PR mechanism after tests on the battlefield --- store/app.py | 28 +++++++++++++++++----------- store/templates/wishlist_add.html | 11 +++++++++++ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/store/app.py b/store/app.py index d0cd86da..dc597a42 100644 --- a/store/app.py +++ b/store/app.py @@ -1,3 +1,4 @@ +import time import re import toml import base64 @@ -133,7 +134,7 @@ def add_to_wishlist(): user = session.get('user', {}) if not user: errormsg = "You must be logged in to submit an app to the wishlist" - return render_template("wishlist_add.html", user=session.get('user', {}), errormsg=errormsg) + return render_template("wishlist_add.html", user=session.get('user', {}), successmsg=None, errormsg=errormsg) name = request.form['name'].strip().replace("\n", "") description = request.form['description'].strip().replace("\n", "") @@ -153,11 +154,10 @@ def add_to_wishlist(): for check, errormsg in checks: if not check: - return render_template("wishlist_add.html", user=session.get('user', {}), errormsg=errormsg) + return render_template("wishlist_add.html", user=session.get('user', {}), successmsg=None, errormsg=errormsg) slug = slugify(name) - - github = Github((config["GITHUB_LOGIN"], config["GITHUB_TOKEN"])) + github = Github(config["GITHUB_TOKEN"]) author = InputGitAuthor(config["GITHUB_LOGIN"], config["GITHUB_EMAIL"]) repo = github.get_repo("Yunohost/apps") current_wishlist_rawtoml = repo.get_contents("wishlist.toml", ref="app-store") # FIXME : ref=repo.default_branch) @@ -166,7 +166,7 @@ def add_to_wishlist(): new_wishlist = toml.loads(current_wishlist_rawtoml) if slug in new_wishlist: - return render_template("wishlist_add.html", user=session.get('user', {}), errormsg=f"An entry with the name {slug} already exists in the wishlist") + return render_template("wishlist_add.html", user=session.get('user', {}), successmsg=None, errormsg=f"An entry with the name {slug} already exists in the wishlist") new_wishlist[slug] = { "name": name, @@ -182,15 +182,17 @@ def add_to_wishlist(): # Get the commit base for the new branch, and create it commit_sha = repo.get_branch("app-store").commit.sha # FIXME app-store -> repo.default_branch repo.create_git_ref(ref=f"refs/heads/{new_branch}", sha=commit_sha) - except: - print("... Branch already exists, skipping") - return False + except exception as e: + print("... Failed to create branch ?") + print(e) + errormsg = "Failed to create the pull request to add the app to the wishlist ... please report the issue to the yunohost team" + return render_template("wishlist_add.html", user=session.get('user', {}), successmsg=None, errormsg=errormsg) message = f"Add {name} to wishlist" repo.update_file( "wishlist.toml", message=message, - content=new_wishlist, + content=new_wishlist_rawtoml, sha=current_wishlist_sha, branch=new_branch, author=author, @@ -202,18 +204,22 @@ def add_to_wishlist(): body = f""" ### Add {name} to wishlist +Proposed by **{session['user']['username']}** + - [ ] Confirm app is self-hostable and generally makes sense to possibly integrate in YunoHost - [ ] Confirm app's license is opensource/free software (or not-totally-free, case by case TBD) - [ ] Description describes concisely what the app is/does """ # Open the PR - pr = self.repo.create_pull( + pr = repo.create_pull( title=message, body=body, head=new_branch, base="app-store" # FIXME app-store -> repo.default_branch ) + successmsg = f"Your proposed app has succesfully been submitted. It must now be validated by the YunoHost team. You can track progress here: https://github.com/YunoHost/apps/pull/{pr.number}" + return render_template("wishlist_add.html", user=session.get('user', {}), successmsg=successmsg) else: - return render_template("wishlist_add.html", user=session.get('user', {}), errormsg=None) + return render_template("wishlist_add.html", user=session.get('user', {}), successmsg=None, errormsg=None) ################################################ diff --git a/store/templates/wishlist_add.html b/store/templates/wishlist_add.html index 6d298fb9..f8e2af18 100644 --- a/store/templates/wishlist_add.html +++ b/store/templates/wishlist_add.html @@ -9,6 +9,16 @@
+ {% if successmsg %} + + {% else %} + + {% if not user %} {% endblock %} From eeb4b9ef3af4139c71845dcee3f5fcc89a2a390b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 21 Aug 2023 18:22:45 +0200 Subject: [PATCH 08/51] appstore: serve assets from local --- store/app.py | 7 ++++++- store/assets/fetch_assets | 10 ++++++++++ store/templates/base.html | 8 ++++---- store/templates/catalog.html | 1 + store/templates/index.html | 2 +- 5 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 store/assets/fetch_assets diff --git a/store/app.py b/store/app.py index dc597a42..5f8dc988 100644 --- a/store/app.py +++ b/store/app.py @@ -13,7 +13,7 @@ from slugify import slugify from flask import Flask, send_from_directory, render_template, session, redirect, request from github import Github, InputGitAuthor -app = Flask(__name__) +app = Flask(__name__, static_url_path='/assets', static_folder="assets") catalog = json.load(open("apps.json")) catalog['categories'] = {c['id']:c for c in catalog['categories']} @@ -68,6 +68,11 @@ wishlist = toml.load(open("../wishlist.toml")) app.secret_key = ''.join([str(random.randint(0, 9)) for i in range(99)]) +@app.route('/favicon.ico') +def favicon(): + return send_from_directory('assets', 'ynh_logo_packaging.png') + + @app.route('/login_using_discourse') def login_using_discourse(): """ diff --git a/store/assets/fetch_assets b/store/assets/fetch_assets new file mode 100644 index 00000000..f418bab8 --- /dev/null +++ b/store/assets/fetch_assets @@ -0,0 +1,10 @@ +curl https://cdn.tailwindcss.com?plugins=forms -O > tailwindcss.js + +curl https://cdn.jsdelivr.net/npm/fork-awesome@1.2.0/css/fork-awesome.min.css > fork-awesome.min.css +sed -i 's@../fonts/@@g' ./fork-awesome.min.css +curl https://cdn.jsdelivr.net/npm/fork-awesome@1.2.0/fonts/forkawesome-webfont.woff2?v=1.2.0 > forkawesome-webfont.woff2 +curl https://cdn.jsdelivr.net/npm/fork-awesome@1.2.0/fonts/forkawesome-webfont.woff?v=1.2.0 > forkawesome-webfont.woff +curl https://cdn.jsdelivr.net/npm/fork-awesome@1.2.0/fonts/forkawesome-webfont.ttf?v=1.2.0 > forkawesome-webfont.ttf + +curl https://raw.githubusercontent.com/YunoHost/doc/master/images/logo_roundcorner.png > ynh_logo_roundcorner.png +curl https://raw.githubusercontent.com/YunoHost/doc/master/images/ynh_logo_black.svg > ynh_logo_black.svg diff --git a/store/templates/base.html b/store/templates/base.html index 5b2b69b3..887d5fb9 100644 --- a/store/templates/base.html +++ b/store/templates/base.html @@ -4,9 +4,9 @@ YunoHost app store - - - + + + @@ -16,7 +16,7 @@ > Home - +
diff --git a/store/templates/catalog.html b/store/templates/catalog.html index 4e160d3f..cba9e753 100644 --- a/store/templates/catalog.html +++ b/store/templates/catalog.html @@ -75,6 +75,7 @@
diff --git a/store/templates/index.html b/store/templates/index.html index 2691d726..e5f62672 100644 --- a/store/templates/index.html +++ b/store/templates/index.html @@ -3,7 +3,7 @@ {% set locale = 'en' %}
- +

Application Store

From 50a43c460e6f606f3146b4491fd73e4ccf57ae03 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 21 Aug 2023 19:14:26 +0200 Subject: [PATCH 09/51] appstore: misc cosmetics --- store/templates/base.html | 34 +++++----------------------------- store/templates/wishlist.html | 31 +++++++++++++------------------ 2 files changed, 18 insertions(+), 47 deletions(-) diff --git a/store/templates/base.html b/store/templates/base.html index 887d5fb9..5f96ad1b 100644 --- a/store/templates/base.html +++ b/store/templates/base.html @@ -4,7 +4,7 @@ YunoHost app store - + @@ -39,7 +39,7 @@
-
+
-
+