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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% block main %}
+ {% endblock %}
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for app, infos in catalog['apps'].items() %}
+
+ {% endfor %}
+
+{% 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+ |
+
+ Description
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+ {% for infos in wishlist %}
+
+
+ {{ infos['name'] }}
+ |
+ {{ infos['description'] }} |
+
+ {% if infos['website'] %}
+
+
+
+ {% endif %}
+ |
+
+ {% if infos['upstream'] %}
+
+
+
+ {% endif %}
+ |
+
+
+
+ Vote
+
+ |
+
+
+ 123
+
+ |
+
+ {% endfor %}
+
+
+
+{% 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 %}
+
+
+
+ You must first login to be allowed to submit an app to the wishlist
+
+
+ {% endif %}
+
+
+
+
+ Please check the license of the app your are proposing
+
+
+ The YunoHost project will only package free/open-source software (with possible case-by-case exceptions for apps which are not-totally-free)
+
+
+
+ {% if errormsg %}
+
+ {% endif %}
+
+
+
+
+
+{% 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 %}
+
+
+
+ {{ successmsg }}
+
+
+ {% else %}
+
+
{% if not user %}
@@ -63,6 +73,7 @@
+ {% endif %}
{% 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 @@
|