From ce9d162ed5dd2cac2e1fef773f31cc5f2d79bfec Mon Sep 17 00:00:00 2001 From: Guido Flohr Date: Fri, 31 May 2024 10:15:01 +0300 Subject: [PATCH 1/8] views, comments: add /pending endpoint It works exactly like `/latest` but returns only posts waiting moderation. The endpoint has to be explicitly enabled and requests must be authorized with the admin password. The admin interface also has to be enabled to make sure that people have changed the password. --- contrib/isso-dev.cfg | 1 + isso/isso.cfg | 4 ++ isso/tests/test_comments.py | 78 ++++++++++++++++++++++++++++ isso/views/comments.py | 101 +++++++++++++++++++++++++++++++++++- 4 files changed, 183 insertions(+), 1 deletion(-) diff --git a/contrib/isso-dev.cfg b/contrib/isso-dev.cfg index 760494b4a..992415f70 100644 --- a/contrib/isso-dev.cfg +++ b/contrib/isso-dev.cfg @@ -18,6 +18,7 @@ notify = stdout reply-notifications = false log-file = latest-enabled = true +pending-enabled = true [admin] enabled = true diff --git a/isso/isso.cfg b/isso/isso.cfg index fec238296..1bcee5bb9 100644 --- a/isso/isso.cfg +++ b/isso/isso.cfg @@ -65,6 +65,10 @@ gravatar-url = https://www.gravatar.com/avatar/{}?d=identicon&s=55 # needing to previously know the posts URIs) latest-enabled = false +# enable the "/pending" endpoint, that works likes "/latest" but only +# for comments waiting moderation +pending-enabled = false + [admin] enabled = false diff --git a/isso/tests/test_comments.py b/isso/tests/test_comments.py index 8cc6360be..80ac53cbb 100644 --- a/isso/tests/test_comments.py +++ b/isso/tests/test_comments.py @@ -5,10 +5,12 @@ import re import tempfile import unittest +import base64 from urllib.parse import urlencode from werkzeug.wrappers import Response +from werkzeug.datastructures import Headers from isso import Isso, core, config from isso.utils import http @@ -705,6 +707,20 @@ def testLatestNotEnabled(self): response = self.get('/latest?limit=5') self.assertEqual(response.status_code, 404) + def testPendingNotFound(self): + # load some comments in a mix of posts + saved = [] + for idx, post_id in enumerate([1, 2, 2, 1, 2, 1, 3, 1, 4, 2, 3, 4, 1, 2]): + text = 'text-{}'.format(idx) + post_uri = 'test-{}'.format(post_id) + self.post('/new?uri=' + post_uri, data=json.dumps({'text': text})) + saved.append((post_uri, text)) + + response = self.get('/pending?limit=5') + + # If the admin interface was not enabled we should get a 404. + self.assertEqual(response.status_code, 404) + class TestHostDependent(unittest.TestCase): @@ -779,6 +795,8 @@ def setUp(self): conf.set("moderation", "enabled", "true") conf.set("guard", "enabled", "off") conf.set("hash", "algorithm", "none") + conf.set("admin", "enabled", "true") + self.conf = conf class App(Isso, core.Mixin): pass @@ -786,6 +804,8 @@ class App(Isso, core.Mixin): self.app = App(conf) self.app.wsgi_app = FakeIP(self.app.wsgi_app, "192.168.1.1") self.client = JSONClient(self.app, Response) + self.post = self.client.post + self.get = self.client.get def tearDown(self): os.unlink(self.path) @@ -860,6 +880,64 @@ def testModerateComment(self): # Comment should no longer exist self.assertEqual(self.app.db.comments.get(id_), None) + def testPendingWithoutAdmin(self): + self.conf.set("admin", "enabled", "false") + response = self.get('/pending?limit=5') + self.assertEqual(response.status_code, 404) + + def testPendingUnauthorized(self): + response = self.get('/pending?limit=5') + self.assertEqual(response.status_code, 401) + + def getAuthenticated(self, url, username, password): + credentials = f"{username}:{password}" + encoded_credentials = base64.b64encode(credentials.encode('utf-8')).decode('utf-8') + headers = Headers() + headers.add('Authorization', f'Basic {encoded_credentials}') + + return self.client.get(url, headers=headers) + + def testPendingNotEnabled(self): + password = "s3cr3t" + self.conf.set("admin", "enabled", "true") + self.conf.set("admin", "password", password) + response = self.getAuthenticated('/pending?limit=5', 'admin', password) + self.assertEqual(response.status_code, 404) + + def testPendingNotEnabled(self): + password = "s3cr3t" + self.conf.set("admin", "enabled", "true") + self.conf.set("admin", "password", password) + self.conf.set("general", "pending-enabled", "true") + response = self.getAuthenticated('/pending?limit=5', 'admin', password) + self.assertEqual(response.status_code, 200) + + body = loads(response.data) + self.assertEqual(body, []) + + def testPendingPosts(self): + # load some comments in a mix of posts + saved = [] + for idx, post_id in enumerate([1, 2, 2, 1, 2, 1, 3, 1, 4, 2, 3, 4, 1, 2]): + text = 'text-{}'.format(idx) + post_uri = 'test-{}'.format(post_id) + self.post('/new?uri=' + post_uri, data=json.dumps({'text': text})) + saved.append((post_uri, text)) + + password = "s3cr3t" + self.conf.set("admin", "enabled", "true") + self.conf.set("admin", "password", password) + self.conf.set("general", "pending-enabled", "true") + response = self.getAuthenticated('/pending?limit=5', 'admin', password) + self.assertEqual(response.status_code, 200) + + body = loads(response.data) + expected_items = saved[-5:] # latest 5 + for reply, expected in zip(body, expected_items): + expected_uri, expected_text = expected + self.assertIn(expected_text, reply['text']) + self.assertEqual(expected_uri, reply['uri']) + class TestUnsubscribe(unittest.TestCase): diff --git a/isso/views/comments.py b/isso/views/comments.py index 80fee5d9e..c6ba86356 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -132,6 +132,33 @@ def get_uri_from_url(url): return uri +def requires_auth(method): + def decorated(self, *args, **kwargs): + request = args[1] + auth = request.authorization + if not auth: + return Response( + "Unauthorized", 401, + {'WWW-Authenticate': 'Basic realm="Authentication Required"'}) + if not self.check_auth(auth.username, auth.password): + return Response( + "Wrong username or password", 401, + {'WWW-Authenticate': 'Basic realm="Authentication Required"'}) + return method(self, *args, **kwargs) + return decorated + + +def requires_admin(method): + def decorated(self, *args, **kwargs): + if not self.isso.conf.getboolean("admin", "enabled"): + return NotFound( + "Unavailable because 'admin' not enabled by site admin" + ) + + return method(self, *args, **kwargs) + return decorated + + class API(object): FIELDS = set(['id', 'parent', 'text', 'author', 'website', @@ -146,6 +173,7 @@ class API(object): ('counts', ('POST', '/count')), ('feed', ('GET', '/feed')), ('latest', ('GET', '/latest')), + ('pending', ('GET', '/pending')), ('view', ('GET', '/id/')), ('edit', ('PUT', '/id/')), ('delete', ('DELETE', '/id/')), @@ -1565,6 +1593,77 @@ def latest(self, environ, request): "Unavailable because 'latest-enabled' not set by site admin" ) + return self._latest(environ, request, "1") + + + def check_auth(self, username, password): + admin_password = self.isso.conf.get("admin", "password") + + return username == 'admin' and password == admin_password + + + """ + @api {get} /pending pending + @apiGroup Comment + @apiName pending + @apiVersion 0.13.0 + @apiDescription + Get the latest comments from the system waiting moderation, no matter which thread. Only available if `[general] pending-enabled` is set to `true` in server config. + + @apiQuery {Number} limit + The quantity of last comments to retrieve + + @apiExample {curl} Get the latest 5 pending comments + curl 'https://comments.example.com/pending?limit=5' + + @apiUse commentResponse + + @apiSuccessExample Example result: + [ + { + "website": null, + "uri": "/some", + "author": null, + "parent": null, + "created": 1464912312.123416, + "text": " <p>I want to use MySQL</p>", + "dislikes": 0, + "modified": null, + "mode": 2, + "id": 3, + "likes": 1 + }, + { + "website": null, + "uri": "/other", + "author": null, + "parent": null, + "created": 1464914341.312426, + "text": " <p>I want to use MySQL</p>", + "dislikes": 0, + "modified": null, + "mode": 2, + "id": 4, + "likes": 0 + } + ] + """ + # If the admin interface is not enabled, people may have not changed + # the default password. We therefore disallow the /pending endpoint, + # as well. + @requires_admin + @requires_auth + def pending(self, environ, request): + # if the feature is not allowed, don't present the endpoint + if not self.conf.getboolean("pending-enabled"): + return NotFound( + "Unavailable because 'pending-enabled' not set by site admin" + ) + + return self._latest(environ, request, "2") + + + def _latest(self, environ, request, mode): # get and check the limit bad_limit_msg = "Query parameter 'limit' is mandatory (integer, >0)" try: @@ -1575,7 +1674,7 @@ def latest(self, environ, request): return BadRequest(bad_limit_msg) # retrieve the latest N comments from the DB - all_comments_gen = self.comments.fetchall(limit=None, order_by='created', mode='1') + all_comments_gen = self.comments.fetchall(limit=None, order_by='created', mode=mode) comments = collections.deque(all_comments_gen, maxlen=limit) # prepare a special set of fields (except text which is rendered specifically) From 0f104366198af3847947342ed9a6e0fb67b9bf8e Mon Sep 17 00:00:00 2001 From: Guido Flohr Date: Fri, 31 May 2024 10:39:29 +0300 Subject: [PATCH 2/8] views, API docs: clarify /latest vs. /pending It should be mentioned that the endpoint /latest only returns accepted comments. --- isso/views/comments.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/isso/views/comments.py b/isso/views/comments.py index c6ba86356..29b7dc39f 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -1546,12 +1546,12 @@ def admin(self, env, req): @apiName latest @apiVersion 0.12.6 @apiDescription - Get the latest comments from the system, no matter which thread. Only available if `[general] latest-enabled` is set to `true` in server config. + Get the latest accepted comments from the system, no matter which thread. Only available if `[general] latest-enabled` is set to `true` in server config. @apiQuery {Number} limit The quantity of last comments to retrieve - @apiExample {curl} Get the latest 5 comments + @apiExample {curl} Get the latest 5 accepted comments curl 'https://comments.example.com/latest?limit=5' @apiUse commentResponse @@ -1608,13 +1608,15 @@ def check_auth(self, username, password): @apiName pending @apiVersion 0.13.0 @apiDescription - Get the latest comments from the system waiting moderation, no matter which thread. Only available if `[general] pending-enabled` is set to `true` in server config. + Get the latest comments waiting moderation from the system, no matter which thread. Only available if `[general] pending-enabled` is set to `true` and `[admin] enabled is set to `true` in server config. + + @apiHeader {String="Basic BASE64_CREDENTIALS"} authorization Base64 encoded "USERNAME:PASSWORD" @apiQuery {Number} limit The quantity of last comments to retrieve @apiExample {curl} Get the latest 5 pending comments - curl 'https://comments.example.com/pending?limit=5' + curl -u 'admin:ADMIN_PASSWORD' 'https://comments.example.com/pending?limit=5' @apiUse commentResponse From ab1af621a468fe96220c0319770b16c1f39bfd66 Mon Sep 17 00:00:00 2001 From: Guido Flohr Date: Fri, 31 May 2024 10:43:25 +0300 Subject: [PATCH 3/8] CHANGES.rst: Document new endpoint `/pending` --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 377eb0424..0e85c4308 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,6 +16,7 @@ New Features - admin: Add log out button (`#870`_, bbaovanc) - Add support for environment variables in config (`#1037`_, pkvach) - Add Japanese localisation (`#1051`_, zurukumo) +- Add server API endpoint /pending for moderation queue (`#1028`_, gflohr) .. _#870: https://github.com/posativ/isso/pull/870 .. _#966: https://github.com/posativ/isso/pull/966 @@ -24,6 +25,7 @@ New Features .. _#1001: https://github.com/isso-comments/isso/pull/1001 .. _#1020: https://github.com/isso-comments/isso/pull/1020 .. _#1005: https://github.com/isso-comments/isso/pull/1005 +.. _#1028: https://github.com/isso-comments/isso/pull/1028 .. _#1037: https://github.com/isso-comments/isso/pull/1037 .. _#1051: https://github.com/isso-comments/isso/pull/1051 From 000cd2490e435a4504914e9fda326bec17355afb Mon Sep 17 00:00:00 2001 From: Guido Flohr Date: Fri, 31 May 2024 11:45:06 +0300 Subject: [PATCH 4/8] views, comments: fix api version for /pending Version 0.13.0 is already released. . Change to the probably next version 0.13.1. --- isso/views/comments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isso/views/comments.py b/isso/views/comments.py index 29b7dc39f..9dfed1158 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -1606,7 +1606,7 @@ def check_auth(self, username, password): @api {get} /pending pending @apiGroup Comment @apiName pending - @apiVersion 0.13.0 + @apiVersion 0.13.1 @apiDescription Get the latest comments waiting moderation from the system, no matter which thread. Only available if `[general] pending-enabled` is set to `true` and `[admin] enabled is set to `true` in server config. From 8f52bf699783dc592b879d8f52ebf9ef0c1c9f29 Mon Sep 17 00:00:00 2001 From: Guido Flohr Date: Thu, 7 Aug 2025 23:05:07 +0300 Subject: [PATCH 5/8] re-implement moderation queue access Instead of a separate endpoint /pending, the existing endpoint /latest now has an optional mode parameter. --- contrib/isso-dev.cfg | 1 - isso/isso.cfg | 4 -- isso/tests/test_comments.py | 63 +++++++++++++++-------------- isso/views/comments.py | 80 +++++++------------------------------ 4 files changed, 49 insertions(+), 99 deletions(-) diff --git a/contrib/isso-dev.cfg b/contrib/isso-dev.cfg index 992415f70..760494b4a 100644 --- a/contrib/isso-dev.cfg +++ b/contrib/isso-dev.cfg @@ -18,7 +18,6 @@ notify = stdout reply-notifications = false log-file = latest-enabled = true -pending-enabled = true [admin] enabled = true diff --git a/isso/isso.cfg b/isso/isso.cfg index 1bcee5bb9..fec238296 100644 --- a/isso/isso.cfg +++ b/isso/isso.cfg @@ -65,10 +65,6 @@ gravatar-url = https://www.gravatar.com/avatar/{}?d=identicon&s=55 # needing to previously know the posts URIs) latest-enabled = false -# enable the "/pending" endpoint, that works likes "/latest" but only -# for comments waiting moderation -pending-enabled = false - [admin] enabled = false diff --git a/isso/tests/test_comments.py b/isso/tests/test_comments.py index 80ac53cbb..952e133d9 100644 --- a/isso/tests/test_comments.py +++ b/isso/tests/test_comments.py @@ -684,6 +684,37 @@ def testLatestOk(self): self.assertIn(expected_text, reply['text']) self.assertEqual(expected_uri, reply['uri']) + def testLatestWithMode(self): + # load some comments in a mix of posts + saved = [] + for idx, post_id in enumerate([1, 2, 2, 1, 2, 1, 3, 1, 4, 2, 3, 4, 1, 2]): + text = 'text-{}'.format(idx) + post_uri = 'test-{}'.format(post_id) + self.post('/new?uri=' + post_uri, data=json.dumps({'text': text})) + saved.append((post_uri, text)) + + response = self.get('/latest?limit=5&mode=1') + self.assertEqual(response.status_code, 200) + + body = loads(response.data) + expected_items = saved[-5:] # latest 5 + for reply, expected in zip(body, expected_items): + expected_uri, expected_text = expected + self.assertIn(expected_text, reply['text']) + self.assertEqual(expected_uri, reply['uri']) + + def testLatestWithInvalidMode(self): + # load some comments in a mix of posts + saved = [] + for idx, post_id in enumerate([1, 2, 2, 1, 2, 1, 3, 1, 4, 2, 3, 4, 1, 2]): + text = 'text-{}'.format(idx) + post_uri = 'test-{}'.format(post_id) + self.post('/new?uri=' + post_uri, data=json.dumps({'text': text})) + saved.append((post_uri, text)) + + response = self.get('/latest?limit=5&mode=3') + self.assertEqual(response.status_code, 400) + def testLatestWithoutLimit(self): response = self.get('/latest') self.assertEqual(response.status_code, 400) @@ -880,15 +911,6 @@ def testModerateComment(self): # Comment should no longer exist self.assertEqual(self.app.db.comments.get(id_), None) - def testPendingWithoutAdmin(self): - self.conf.set("admin", "enabled", "false") - response = self.get('/pending?limit=5') - self.assertEqual(response.status_code, 404) - - def testPendingUnauthorized(self): - response = self.get('/pending?limit=5') - self.assertEqual(response.status_code, 401) - def getAuthenticated(self, url, username, password): credentials = f"{username}:{password}" encoded_credentials = base64.b64encode(credentials.encode('utf-8')).decode('utf-8') @@ -897,24 +919,6 @@ def getAuthenticated(self, url, username, password): return self.client.get(url, headers=headers) - def testPendingNotEnabled(self): - password = "s3cr3t" - self.conf.set("admin", "enabled", "true") - self.conf.set("admin", "password", password) - response = self.getAuthenticated('/pending?limit=5', 'admin', password) - self.assertEqual(response.status_code, 404) - - def testPendingNotEnabled(self): - password = "s3cr3t" - self.conf.set("admin", "enabled", "true") - self.conf.set("admin", "password", password) - self.conf.set("general", "pending-enabled", "true") - response = self.getAuthenticated('/pending?limit=5', 'admin', password) - self.assertEqual(response.status_code, 200) - - body = loads(response.data) - self.assertEqual(body, []) - def testPendingPosts(self): # load some comments in a mix of posts saved = [] @@ -927,8 +931,9 @@ def testPendingPosts(self): password = "s3cr3t" self.conf.set("admin", "enabled", "true") self.conf.set("admin", "password", password) - self.conf.set("general", "pending-enabled", "true") - response = self.getAuthenticated('/pending?limit=5', 'admin', password) + self.conf.set("general", "latest-enabled", "true") + response = self.getAuthenticated('/latest?mode=2&limit=5', 'admin', password) + print(response.status) self.assertEqual(response.status_code, 200) body = loads(response.data) diff --git a/isso/views/comments.py b/isso/views/comments.py index 9dfed1158..cf7c24449 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -173,7 +173,6 @@ class API(object): ('counts', ('POST', '/count')), ('feed', ('GET', '/feed')), ('latest', ('GET', '/latest')), - ('pending', ('GET', '/pending')), ('view', ('GET', '/id/')), ('edit', ('PUT', '/id/')), ('delete', ('DELETE', '/id/')), @@ -1551,6 +1550,13 @@ def admin(self, env, req): @apiQuery {Number} limit The quantity of last comments to retrieve + @apiQuery {Number{1,2}} [mode=1] + The comments’ mode: + value | explanation + --- | --- + `1` | accepted: The comment was accepted by the server and is published. + `2` | in moderation queue: The comment was accepted by the server but awaits moderation. + @apiExample {curl} Get the latest 5 accepted comments curl 'https://comments.example.com/latest?limit=5' @@ -1593,7 +1599,14 @@ def latest(self, environ, request): "Unavailable because 'latest-enabled' not set by site admin" ) - return self._latest(environ, request, "1") + mode = request.args.get('mode', "1") + + if mode != "1" and mode != "2": + return BadRequest( + "Mode must either be '1' for accepted comments or '2' for pedning comments waiting moderation" + ) + + return self._latest(environ, request, mode) def check_auth(self, username, password): @@ -1602,69 +1615,6 @@ def check_auth(self, username, password): return username == 'admin' and password == admin_password - """ - @api {get} /pending pending - @apiGroup Comment - @apiName pending - @apiVersion 0.13.1 - @apiDescription - Get the latest comments waiting moderation from the system, no matter which thread. Only available if `[general] pending-enabled` is set to `true` and `[admin] enabled is set to `true` in server config. - - @apiHeader {String="Basic BASE64_CREDENTIALS"} authorization Base64 encoded "USERNAME:PASSWORD" - - @apiQuery {Number} limit - The quantity of last comments to retrieve - - @apiExample {curl} Get the latest 5 pending comments - curl -u 'admin:ADMIN_PASSWORD' 'https://comments.example.com/pending?limit=5' - - @apiUse commentResponse - - @apiSuccessExample Example result: - [ - { - "website": null, - "uri": "/some", - "author": null, - "parent": null, - "created": 1464912312.123416, - "text": " <p>I want to use MySQL</p>", - "dislikes": 0, - "modified": null, - "mode": 2, - "id": 3, - "likes": 1 - }, - { - "website": null, - "uri": "/other", - "author": null, - "parent": null, - "created": 1464914341.312426, - "text": " <p>I want to use MySQL</p>", - "dislikes": 0, - "modified": null, - "mode": 2, - "id": 4, - "likes": 0 - } - ] - """ - # If the admin interface is not enabled, people may have not changed - # the default password. We therefore disallow the /pending endpoint, - # as well. - @requires_admin - @requires_auth - def pending(self, environ, request): - # if the feature is not allowed, don't present the endpoint - if not self.conf.getboolean("pending-enabled"): - return NotFound( - "Unavailable because 'pending-enabled' not set by site admin" - ) - - return self._latest(environ, request, "2") - - def _latest(self, environ, request, mode): # get and check the limit bad_limit_msg = "Query parameter 'limit' is mandatory (integer, >0)" From 9dfe3761c7162100831340401f996dde83377741 Mon Sep 17 00:00:00 2001 From: Guido Flohr Date: Thu, 7 Aug 2025 23:08:55 +0300 Subject: [PATCH 6/8] document API access to moderation queue --- CHANGES.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0e85c4308..8bda38e19 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,7 +16,7 @@ New Features - admin: Add log out button (`#870`_, bbaovanc) - Add support for environment variables in config (`#1037`_, pkvach) - Add Japanese localisation (`#1051`_, zurukumo) -- Add server API endpoint /pending for moderation queue (`#1028`_, gflohr) +- Allow access moderation queue (`#1028`_, gflohr) .. _#870: https://github.com/posativ/isso/pull/870 .. _#966: https://github.com/posativ/isso/pull/966 @@ -59,6 +59,9 @@ Bugfixes & Improvements - Python 3.12 support (`#1015`_, ix5) - Disable Postbox submit button on click, enable after response (`#993`_, pkvach) - Document title parameter and improve error handling for /new API (`#1058`_, pkvach) +- The `/latest` endpoint now has an optional parameter `mode`. The default value + of '1' retrieves published comments, the mode '2' retrieves posts waiting + moderation. .. _#951: https://github.com/posativ/isso/pull/951 .. _#967: https://github.com/posativ/isso/pull/967 From b6fcad28e62b4422b3836967d9c7999aedada564 Mon Sep 17 00:00:00 2001 From: Guido Flohr Date: Mon, 18 Aug 2025 14:54:02 +0300 Subject: [PATCH 7/8] shut up linter --- isso/views/comments.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/isso/views/comments.py b/isso/views/comments.py index cf7c24449..9d170ad11 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -153,7 +153,7 @@ def decorated(self, *args, **kwargs): if not self.isso.conf.getboolean("admin", "enabled"): return NotFound( "Unavailable because 'admin' not enabled by site admin" - ) + ) return method(self, *args, **kwargs) return decorated @@ -1608,13 +1608,11 @@ def latest(self, environ, request): return self._latest(environ, request, mode) - def check_auth(self, username, password): admin_password = self.isso.conf.get("admin", "password") return username == 'admin' and password == admin_password - def _latest(self, environ, request, mode): # get and check the limit bad_limit_msg = "Query parameter 'limit' is mandatory (integer, >0)" From 2b2bceb5786d4b9709eda1252e9adfb304d8eede Mon Sep 17 00:00:00 2001 From: Guido Flohr Date: Mon, 18 Aug 2025 14:57:30 +0300 Subject: [PATCH 8/8] clarify semantics of /latest The mode parameter switches between accepted comments and comments pending moderation. --- isso/views/comments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isso/views/comments.py b/isso/views/comments.py index 9d170ad11..909620512 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -1545,7 +1545,7 @@ def admin(self, env, req): @apiName latest @apiVersion 0.12.6 @apiDescription - Get the latest accepted comments from the system, no matter which thread. Only available if `[general] latest-enabled` is set to `true` in server config. + Get the latest comments from the system, no matter which thread. Only available if `[general] latest-enabled` is set to `true` in server config. @apiQuery {Number} limit The quantity of last comments to retrieve