From 9efc56368cfc7c88a92531520f70e92afe2ba5dd Mon Sep 17 00:00:00 2001 From: nbenedek Date: Mon, 6 Jun 2022 21:58:12 +0200 Subject: [PATCH] rssbridge: New app to generate RSS feeds for websites [sunil: Update description for simplicity, group info] [sunil: Indentation fixes] [sunil: End all URLs with a slash] [sunil: Update frontpage shortcut to be a simple one] [sunil: Enable single-sign-on for main interface only] [sunil: In copyright file, merge with public-domain section] [sunil: Simplify and vectorify the icon] Signed-off-by: Sunil Mohan Adapa Reviewed-by: Sunil Mohan Adapa --- debian/copyright | 3 + plinth/modules/rssbridge/__init__.py | 92 ++++++++++++++++ .../apache2/conf-available/rss-bridge.conf | 21 ++++ .../data/etc/plinth/modules-enabled/rssbridge | 1 + plinth/modules/rssbridge/manifest.py | 17 +++ plinth/modules/rssbridge/urls.py | 13 +++ pyproject.toml | 1 + static/themes/default/icons/rssbridge.png | Bin 0 -> 10672 bytes static/themes/default/icons/rssbridge.svg | 103 ++++++++++++++++++ 9 files changed, 251 insertions(+) create mode 100644 plinth/modules/rssbridge/__init__.py create mode 100644 plinth/modules/rssbridge/data/etc/apache2/conf-available/rss-bridge.conf create mode 100644 plinth/modules/rssbridge/data/etc/plinth/modules-enabled/rssbridge create mode 100644 plinth/modules/rssbridge/manifest.py create mode 100644 plinth/modules/rssbridge/urls.py create mode 100644 static/themes/default/icons/rssbridge.png create mode 100644 static/themes/default/icons/rssbridge.svg diff --git a/debian/copyright b/debian/copyright index e0a3a8c61..74a81f861 100644 --- a/debian/copyright +++ b/debian/copyright @@ -67,6 +67,8 @@ Files: static/themes/default/icons/ejabberd.png static/themes/default/icons/privoxy.png static/themes/default/icons/privoxy.svg static/themes/default/icons/radicale.svg + static/themes/default/icons/rssbridge.png + static/themes/default/icons/rssbridge.svg static/themes/default/icons/zoph.png static/themes/default/icons/zoph.svg static/themes/default/img/network-connection.svg @@ -82,6 +84,7 @@ Comment: Placed into public domain by authors (or) https://commons.wikimedia.org/wiki/File:Ejabberd_icon.png https://radicale.org/css/logo.svg https://github.com/resiprocate/resiprocate/blob/master/resip/stack/doc/reSIProcate-logo.svg + https://github.com/RSS-Bridge/rss-bridge/blob/master/static/logo_600px.png License: public-domain Files: doc/manual/en/images/icons/* diff --git a/plinth/modules/rssbridge/__init__.py b/plinth/modules/rssbridge/__init__.py new file mode 100644 index 000000000..e4c89bbb0 --- /dev/null +++ b/plinth/modules/rssbridge/__init__.py @@ -0,0 +1,92 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +FreedomBox app to configure RSS-Bridge. +""" +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ + +from plinth import app as app_module +from plinth import cfg, frontpage, menu +from plinth.modules.apache.components import Webserver +from plinth.modules.backups.components import BackupRestore +from plinth.modules.firewall.components import Firewall +from plinth.modules.users.components import UsersAndGroups +from plinth.package import Packages +from plinth.utils import format_lazy + +from . import manifest + +_description = [ + _('RSS-Bridge generates RSS and Atom feeds for websites that do not have ' + 'one. Generated feeds can be consumed by any feed reader.'), + format_lazy( + _('When enabled, RSS-Bridge can be accessed by ' + 'any user belonging to the feed-reader group.'), + users_url=reverse_lazy('users:index')), + format_lazy( + _('You can use RSS-Bridge with Tiny Tiny ' + 'RSS to follow various websites. When adding a feed, enable ' + 'authentication and use your {box_name} credentials.'), + ttrss_url=reverse_lazy('ttrss:index'), box_name=cfg.box_name), +] + +app = None + + +class RSSBridgeApp(app_module.App): + """FreedomBox app for RSS-Bridge.""" + + app_id = 'rssbridge' + + _version = 1 + + def __init__(self): + """Create components for the app.""" + super().__init__() + + groups = {'feed-reader': _('Read and subscribe to news feeds')} + + info = app_module.Info(app_id=self.app_id, version=self._version, + name=_('RSS-Bridge'), icon_filename='rssbridge', + short_description=_('RSS Feed Generator'), + description=_description, + manual_page='RSSBridge', donation_url=None, + clients=manifest.clients) + self.add(info) + + menu_item = menu.Menu('menu-rssbridge', info.name, + info.short_description, info.icon_filename, + 'rssbridge:index', parent_url_name='apps') + self.add(menu_item) + + shortcut = frontpage.Shortcut('shortcut-rssbridge', name=info.name, + short_description=info.short_description, + icon=info.icon_filename, + url='/rss-bridge/', login_required=True, + allowed_groups=list(groups)) + self.add(shortcut) + + packages = Packages('packages-rssbridge', ['rss-bridge']) + self.add(packages) + + firewall = Firewall('firewall-rssbridge', info.name, + ports=['http', 'https'], is_external=True) + self.add(firewall) + + webserver = Webserver('webserver-rssbridge', 'rss-bridge', + urls=['https://{host}/rss-bridge/']) + self.add(webserver) + + users_and_groups = UsersAndGroups('users-and-groups-rssbridge', + groups=groups) + self.add(users_and_groups) + + backup_restore = BackupRestore('backup-restore-rssbridge', + **manifest.backup) + self.add(backup_restore) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + app.setup(old_version) + helper.call('post', app.enable) diff --git a/plinth/modules/rssbridge/data/etc/apache2/conf-available/rss-bridge.conf b/plinth/modules/rssbridge/data/etc/apache2/conf-available/rss-bridge.conf new file mode 100644 index 000000000..e3aaf2059 --- /dev/null +++ b/plinth/modules/rssbridge/data/etc/apache2/conf-available/rss-bridge.conf @@ -0,0 +1,21 @@ +## +## On all sites, provide RSS-Bridge on a default path: /rss-bridge +## Allow valid LDAP users from groups 'feed-reader' and 'admin'. +## +Alias /rss-bridge /usr/share/rss-bridge + + + + # Formats: Atom, Json, Mrss and Plaintext + Include includes/freedombox-auth-ldap.conf + Require ldap-group cn=admin,ou=groups,dc=thisbox + Require ldap-group cn=feed-reader,ou=groups,dc=thisbox + + + # Formats: Html and all other pages + Include includes/freedombox-single-sign-on.conf + + TKTAuthToken "feed-reader" "admin" + + + diff --git a/plinth/modules/rssbridge/data/etc/plinth/modules-enabled/rssbridge b/plinth/modules/rssbridge/data/etc/plinth/modules-enabled/rssbridge new file mode 100644 index 000000000..47fe69ce8 --- /dev/null +++ b/plinth/modules/rssbridge/data/etc/plinth/modules-enabled/rssbridge @@ -0,0 +1 @@ +plinth.modules.rssbridge diff --git a/plinth/modules/rssbridge/manifest.py b/plinth/modules/rssbridge/manifest.py new file mode 100644 index 000000000..289bc65d3 --- /dev/null +++ b/plinth/modules/rssbridge/manifest.py @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later + +from django.utils.translation import gettext_lazy as _ + +""" +Application manifest for RSS-Bridge. +""" + +clients = [{ + 'name': _('RSS-Bridge'), + 'platforms': [{ + 'type': 'web', + 'url': '/rss-bridge/' + }] +}] + +backup = {} diff --git a/plinth/modules/rssbridge/urls.py b/plinth/modules/rssbridge/urls.py new file mode 100644 index 000000000..5df049814 --- /dev/null +++ b/plinth/modules/rssbridge/urls.py @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +URLs for the RSS-Bridge module. +""" + +from django.urls import re_path + +from plinth.views import AppView + +urlpatterns = [ + re_path(r'^apps/rssbridge/$', AppView.as_view(app_id='rssbridge'), + name='index'), +] diff --git a/pyproject.toml b/pyproject.toml index 3954de651..5ef7e8f2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ markers = [ "quassel", "radicale", "roundcube", + "rssbridge", "samba", "searx", "security", diff --git a/static/themes/default/icons/rssbridge.png b/static/themes/default/icons/rssbridge.png new file mode 100644 index 0000000000000000000000000000000000000000..a01e7a5daf6a1ac1389bc66fd919566bce4ee35f GIT binary patch literal 10672 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 h;eOr + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + +