]> git.ipfire.org Git - ipfire.org.git/commitdiff
lists: Implement some basic code to talk to Mailman
authorMichael Tremer <michael.tremer@ipfire.org>
Tue, 28 Nov 2023 18:43:56 +0000 (18:43 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Tue, 28 Nov 2023 18:43:56 +0000 (18:43 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Makefile.am
src/backend/accounts.py
src/backend/base.py
src/backend/lists.py [new file with mode: 0644]
src/templates/lists/index.html [new file with mode: 0644]
src/web/__init__.py
src/web/lists.py [new file with mode: 0644]

index 1ed1a4983f29af37f403159e80cd2f567c43f353..92e494ad156357f1b463428e169984df4b66b332 100644 (file)
@@ -62,6 +62,7 @@ backend_PYTHON = \
        src/backend/httpclient.py \
        src/backend/hwdata.py \
        src/backend/iuse.py \
+       src/backend/lists.py \
        src/backend/messages.py \
        src/backend/mirrors.py \
        src/backend/misc.py \
@@ -92,6 +93,7 @@ web_PYTHON = \
        src/web/fireinfo.py \
        src/web/handlers.py \
        src/web/iuse.py \
+       src/web/lists.py \
        src/web/location.py \
        src/web/nopaste.py \
        src/web/ui_modules.py \
@@ -256,6 +258,11 @@ templates_location_DATA = \
 
 templates_locationdir = $(templatesdir)/location
 
+templates_lists_DATA = \
+       src/templates/lists/index.html
+
+templates_listsdir = $(templatesdir)/lists
+
 templates_messages_DATA = \
        src/templates/messages/base.html \
        src/templates/messages/base-promo.html \
index a9942085fcbb1a18d0df619284b339df8f3b824b..1634e45f6b2f86fc999cc54bac312329a46e6fdd 100644 (file)
@@ -1309,6 +1309,11 @@ class Account(LDAPObject):
                # Disable the user
                await user.disable(text)
 
+       # Mailman
+
+       async def get_lists(self):
+               return await self.backend.lists.get_subscribed_lists(self)
+
 
 class Groups(Object):
        hidden_groups = (
index f5d267f4a68277dbb42803820c6823d8477c29ef..00dcefcbf4b576099d2fbaa3382e0f53be1d3970 100644 (file)
@@ -17,6 +17,7 @@ from . import database
 from . import fireinfo
 from . import httpclient
 from . import iuse
+from . import lists
 from . import messages
 from . import mirrors
 from . import netboot
@@ -176,6 +177,10 @@ class Backend(object):
        def groups(self):
                return accounts.Groups(self)
 
+       @lazy_property
+       def lists(self):
+               return lists.Lists(self)
+
        @lazy_property
        def messages(self):
                return messages.Messages(self)
diff --git a/src/backend/lists.py b/src/backend/lists.py
new file mode 100644 (file)
index 0000000..f5d1692
--- /dev/null
@@ -0,0 +1,131 @@
+#!/usr/bin/python3
+
+import json
+import urllib.parse
+
+from . import accounts
+from . import misc
+
+class Lists(misc.Object):
+       @property
+       def url(self):
+               """
+                       Returns the base URL of a Mailman instance
+               """
+               return self.settings.get("mailman-url")
+
+       @property
+       def username(self):
+               return self.settings.get("mailman-username")
+
+       @property
+       def password(self):
+               return self.settings.get("mailman-password")
+
+       async def _request(self, method, url, data=None):
+               headers, body = {}, None
+
+               # URL
+               url = urllib.parse.urljoin(self.url, url)
+
+               # For GET requests, append query arguments
+               if method == "GET":
+                       if data:
+                               url = "%s?%s" % (url, urllib.parse.urlencode(data))
+
+               # For POST/PUT encode all arguments as JSON
+               elif method in ("POST", "PUT", "PATCH"):
+                       headers |= {
+                               "Content-Type" : "application/json",
+                       }
+
+                       body = json.dumps(data)
+
+               # Send the request and wait for a response
+               res = await self.backend.http_client.fetch(url, method=method,
+                       headers=headers, body=body,
+
+                       # Authentication
+                       auth_username=self.username, auth_password=self.password,
+               )
+
+               # Decode JSON response
+               body = json.loads(res.body)
+
+               # XXX handle errors
+
+               return body
+
+       async def _get_lists(self, *args, **kwargs):
+               lists = []
+
+               # Fetch the response
+               response = await self._request(*args, **kwargs)
+
+               # Fetch entries
+               for entry in response.get("entries", []):
+                       list = List(self.backend, **entry)
+                       lists.append(list)
+
+               return lists
+
+       async def get_lists(self):
+               """
+                       Fetches all available lists
+               """
+               data = {
+                       "advertised" : True,
+               }
+
+               return await self._get_lists("GET", "/3.1/lists", data=data)
+
+       async def get_subscribed_lists(self, account):
+               data = {
+                       "subscriber" : account.email,
+                       "role"       : "member",
+               }
+
+               return await self._get_lists("GET", "/3.1/members/find", data=data)
+
+
+class List(misc.Object):
+       def init(self, list_id, **kwargs):
+               self.list_id = list_id
+
+               # Store all other data
+               self.data = kwargs
+
+       def __repr__(self):
+               return "<List %s>" % self.list_id
+
+       def __str__(self):
+               return self.display_name
+
+       def __eq__(self, other):
+               if isinstance(other, self.__class__):
+                       return self.list_id == other.list_id
+
+               return NotImplemented
+
+       def __lt__(self, other):
+               if isinstance(other, self.__class__):
+                       return self.list_id < other.list_id
+
+               return NotImplemented
+
+       def __len__(self):
+               return self.data.get("member_count")
+
+       @property
+       def display_name(self):
+               return self.data.get("display_name")
+
+       @property
+       def description(self):
+               return self.data.get("description")
+
+       async def subscribe(self, account):
+               pass # XXX TODO
+
+       async def unsubscribe(self, account):
+               pass # XXX TODO
diff --git a/src/templates/lists/index.html b/src/templates/lists/index.html
new file mode 100644 (file)
index 0000000..5be27f8
--- /dev/null
@@ -0,0 +1,42 @@
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Lists") }}{% end block %}
+
+{% block container %}
+       <section class="hero is-primary">
+               <div class="hero-body">
+                       <div class="container">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
+                                       <ul>
+                                               <li>
+                                                       <a href="/">
+                                                               {{ _("Home") }}
+                                                       </a>
+                                               </li>
+                                               <li class="is-active">
+                                                       <a href="#" aria-current="page">{{ _("Mailing Lists") }}</a>
+                                               </li>
+                                       </ul>
+                               </nav>
+
+                               <h1 class="title">{{ _("Mailing Lists") }}</h1>
+                       </div>
+               </div>
+       </section>
+
+       <section class="section">
+               <div class="container">
+                       <nav class="panel">
+                               {% for list in sorted(lists) %}
+                                       <a class="panel-block {% if list in subscribed_lists %}is-active{% end %}">
+                                               <span class="panel-icon">
+                                                       <i class="fa-solid fa-envelope" aria-hidden="true"></i>
+                                               </span>
+
+                                               {{ list }}{% if list.description %} &dash; {{ list.description }}{% end %}
+                                       </a>
+                               {% end %}
+                       </nav>
+               </div>
+       </section>
+{% end block %}
index 94dc506919c001289c8020b1aec0f315c79e58a8..04e71e9f9c06720559fa47c567b68c37efa69d1c 100644 (file)
@@ -22,6 +22,7 @@ from . import donate
 from . import downloads
 from . import fireinfo
 from . import iuse
+from . import lists
 from . import location
 from . import nopaste
 from . import ui_modules
@@ -164,6 +165,9 @@ class Application(tornado.web.Application):
                        (r"/donate/thank-you", donate.ThankYouHandler),
                        (r"/donate/error", donate.ErrorHandler),
 
+                       # Lists
+                       (r"/lists", lists.IndexHandler),
+
                        # Password Reset
                        (r"/password\-reset", auth.PasswordResetInitiationHandler),
                        (r"/password\-reset/([a-z_][a-z0-9_-]{0,31})/(\w+)", auth.PasswordResetHandler),
diff --git a/src/web/lists.py b/src/web/lists.py
new file mode 100644 (file)
index 0000000..1425603
--- /dev/null
@@ -0,0 +1,16 @@
+#!/usr/bin/python3
+
+import tornado.web
+
+from . import base
+
+class IndexHandler(base.BaseHandler):
+       @tornado.web.authenticated
+       async def get(self):
+               # Fetch all available lists
+               lists = await self.backend.lists.get_lists()
+
+               # Fetch all subscribed lists
+               subscribed_lists = await self.current_user.get_lists()
+
+               self.render("lists/index.html", lists=lists, subscribed_lists=subscribed_lists)