aa-rss-to-discord 2.4.0__tar.gz → 2.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/PKG-INFO +4 -4
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/README.md +2 -2
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/__init__.py +1 -1
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/cs_CZ/LC_MESSAGES/django.po +1 -1
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/de/LC_MESSAGES/django.po +1 -1
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/django.pot +2 -2
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/es/LC_MESSAGES/django.mo +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/es/LC_MESSAGES/django.po +7 -5
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/fr_FR/LC_MESSAGES/django.po +1 -1
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/it_IT/LC_MESSAGES/django.po +1 -1
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/ja/LC_MESSAGES/django.po +1 -1
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/ko_KR/LC_MESSAGES/django.po +1 -1
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/nl_NL/LC_MESSAGES/django.po +1 -1
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/pl_PL/LC_MESSAGES/django.po +1 -1
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/ru/LC_MESSAGES/django.po +1 -1
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/sk/LC_MESSAGES/django.po +1 -1
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/uk/LC_MESSAGES/django.mo +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/uk/LC_MESSAGES/django.po +6 -5
- aa_rss_to_discord-2.5.0/aa_rss_to_discord/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/zh_Hans/LC_MESSAGES/django.po +11 -12
- aa_rss_to_discord-2.5.0/aa_rss_to_discord/tasks.py +154 -0
- aa_rss_to_discord-2.5.0/aa_rss_to_discord/tests/test_tasks.py +236 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/pyproject.toml +1 -1
- aa_rss_to_discord-2.4.0/aa_rss_to_discord/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- aa_rss_to_discord-2.4.0/aa_rss_to_discord/tasks.py +0 -131
- aa_rss_to_discord-2.4.0/aa_rss_to_discord/tests/test_tasks.py +0 -176
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/.gitignore +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/LICENSE +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/admin.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/apps.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/auth_hooks.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/constants.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/discordbot/cogs/__init__.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/discordbot/cogs/rss.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/de/LC_MESSAGES/django.mo +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/fr_FR/LC_MESSAGES/django.mo +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/it_IT/LC_MESSAGES/django.mo +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/ja/LC_MESSAGES/django.mo +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/ko_KR/LC_MESSAGES/django.mo +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/nl_NL/LC_MESSAGES/django.mo +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/ru/LC_MESSAGES/django.mo +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/locale/sk/LC_MESSAGES/django.mo +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/managers.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/migrations/0001_initial.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/migrations/0002_enable_disable_rss_feeds.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/migrations/__init__.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/models.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/providers.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/tests/__init__.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/tests/test_auth_hooks.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/tests/test_managers.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/tests/test_models.py +0 -0
- {aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/tests/test_providers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aa-rss-to-discord
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: Alliance Auth module to post news from RSS feeds to your Discord
|
|
5
5
|
Project-URL: Changelog, https://github.com/ppfeufer/aa-rss-to-discord/blob/master/CHANGELOG.md
|
|
6
6
|
Project-URL: Codecov, https://codecov.io/gh/ppfeufer/aa-rss-to-discord
|
|
@@ -705,7 +705,7 @@ Requires-Python: <3.14,>=3.10
|
|
|
705
705
|
Requires-Dist: allianceauth-discordbot>=3.0.5
|
|
706
706
|
Requires-Dist: allianceauth<5,>=4.3.1
|
|
707
707
|
Requires-Dist: feedparser
|
|
708
|
-
Requires-Dist: py-cord>=2.7.0rc2; python_version
|
|
708
|
+
Requires-Dist: py-cord>=2.7.0rc2; python_version >= '3.13'
|
|
709
709
|
Provides-Extra: tests-allianceauth-latest
|
|
710
710
|
Requires-Dist: coverage; extra == 'tests-allianceauth-latest'
|
|
711
711
|
Requires-Dist: django-webtest; extra == 'tests-allianceauth-latest'
|
|
@@ -773,7 +773,7 @@ Make sure you're in the virtual environment (venv) of your Alliance Auth
|
|
|
773
773
|
installation Then install the latest release directly from PyPi.
|
|
774
774
|
|
|
775
775
|
```shell
|
|
776
|
-
pip install aa-rss-to-discord==2.
|
|
776
|
+
pip install aa-rss-to-discord==2.5.0
|
|
777
777
|
```
|
|
778
778
|
|
|
779
779
|
### Step 2: Configure Alliance Auth<a name="step-2-configure-alliance-auth"></a>
|
|
@@ -828,7 +828,7 @@ To update your existing installation of Alliance Auth RSS to Discord, first enab
|
|
|
828
828
|
virtual environment (venv) of your Alliance Auth installation.
|
|
829
829
|
|
|
830
830
|
```bash
|
|
831
|
-
pip install aa-rss-to-discord==2.
|
|
831
|
+
pip install aa-rss-to-discord==2.5.0
|
|
832
832
|
|
|
833
833
|
python manage.py migrate
|
|
834
834
|
```
|
|
@@ -60,7 +60,7 @@ Make sure you're in the virtual environment (venv) of your Alliance Auth
|
|
|
60
60
|
installation Then install the latest release directly from PyPi.
|
|
61
61
|
|
|
62
62
|
```shell
|
|
63
|
-
pip install aa-rss-to-discord==2.
|
|
63
|
+
pip install aa-rss-to-discord==2.5.0
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
### Step 2: Configure Alliance Auth<a name="step-2-configure-alliance-auth"></a>
|
|
@@ -115,7 +115,7 @@ To update your existing installation of Alliance Auth RSS to Discord, first enab
|
|
|
115
115
|
virtual environment (venv) of your Alliance Auth installation.
|
|
116
116
|
|
|
117
117
|
```bash
|
|
118
|
-
pip install aa-rss-to-discord==2.
|
|
118
|
+
pip install aa-rss-to-discord==2.5.0
|
|
119
119
|
|
|
120
120
|
python manage.py migrate
|
|
121
121
|
```
|
|
@@ -7,7 +7,7 @@ msgid ""
|
|
|
7
7
|
msgstr ""
|
|
8
8
|
"Project-Id-Version: AA RSS to Discord 2.2.0\n"
|
|
9
9
|
"Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-rss-to-discord/issues\n"
|
|
10
|
-
"POT-Creation-Date:
|
|
10
|
+
"POT-Creation-Date: 2026-01-05 22:28+0100\n"
|
|
11
11
|
"PO-Revision-Date: 2024-05-10 14:10+0000\n"
|
|
12
12
|
"Last-Translator: Anonymous <noreply@weblate.org>\n"
|
|
13
13
|
"Language-Team: Czech <https://weblate.ppfeufer.de/projects/alliance-auth-apps/aa-rss-to-discord/cs/>\n"
|
|
@@ -6,7 +6,7 @@ msgid ""
|
|
|
6
6
|
msgstr ""
|
|
7
7
|
"Project-Id-Version: AA RSS to Discord 2.2.0\n"
|
|
8
8
|
"Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-rss-to-discord/issues\n"
|
|
9
|
-
"POT-Creation-Date:
|
|
9
|
+
"POT-Creation-Date: 2026-01-05 22:28+0100\n"
|
|
10
10
|
"PO-Revision-Date: 2025-04-25 14:24+0000\n"
|
|
11
11
|
"Last-Translator: Peter Pfeufer <info@ppfeufer.de>\n"
|
|
12
12
|
"Language-Team: German <https://weblate.ppfeufer.de/projects/alliance-auth-apps/aa-rss-to-discord/de/>\n"
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
#, fuzzy
|
|
7
7
|
msgid ""
|
|
8
8
|
msgstr ""
|
|
9
|
-
"Project-Id-Version: AA RSS to Discord 2.
|
|
9
|
+
"Project-Id-Version: AA RSS to Discord 2.5.0\n"
|
|
10
10
|
"Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-rss-to-discord/issues\n"
|
|
11
|
-
"POT-Creation-Date:
|
|
11
|
+
"POT-Creation-Date: 2026-01-05 22:28+0100\n"
|
|
12
12
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
13
13
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
14
14
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
Binary file
|
|
@@ -2,24 +2,26 @@
|
|
|
2
2
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
3
3
|
# This file is distributed under the same license as the PACKAGE package.
|
|
4
4
|
# Zigor Fernandez Moreno <sietehierros@gmail.com>, 2023, 2024.
|
|
5
|
+
# Ivan <isangar2000@gmail.com>, 2025.
|
|
5
6
|
msgid ""
|
|
6
7
|
msgstr ""
|
|
7
8
|
"Project-Id-Version: AA RSS to Discord 2.2.0\n"
|
|
8
9
|
"Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-rss-to-discord/issues\n"
|
|
9
|
-
"POT-Creation-Date:
|
|
10
|
-
"PO-Revision-Date:
|
|
11
|
-
"Last-Translator:
|
|
10
|
+
"POT-Creation-Date: 2026-01-05 22:28+0100\n"
|
|
11
|
+
"PO-Revision-Date: 2025-12-18 23:47+0000\n"
|
|
12
|
+
"Last-Translator: Ivan <isangar2000@gmail.com>\n"
|
|
12
13
|
"Language-Team: Spanish <https://weblate.ppfeufer.de/projects/alliance-auth-apps/aa-rss-to-discord/es/>\n"
|
|
13
14
|
"Language: es\n"
|
|
14
15
|
"MIME-Version: 1.0\n"
|
|
15
16
|
"Content-Type: text/plain; charset=UTF-8\n"
|
|
16
17
|
"Content-Transfer-Encoding: 8bit\n"
|
|
17
18
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
|
18
|
-
"X-Generator: Weblate 5.
|
|
19
|
+
"X-Generator: Weblate 5.14.3\n"
|
|
19
20
|
|
|
20
21
|
#: aa_rss_to_discord/__init__.py:10
|
|
22
|
+
#, fuzzy
|
|
21
23
|
msgid "RSS to Discord"
|
|
22
|
-
msgstr ""
|
|
24
|
+
msgstr "RSS ir discord"
|
|
23
25
|
|
|
24
26
|
#: aa_rss_to_discord/models.py:44
|
|
25
27
|
msgid "RSS Feed"
|
|
@@ -9,7 +9,7 @@ msgid ""
|
|
|
9
9
|
msgstr ""
|
|
10
10
|
"Project-Id-Version: AA RSS to Discord 2.2.0\n"
|
|
11
11
|
"Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-rss-to-discord/issues\n"
|
|
12
|
-
"POT-Creation-Date:
|
|
12
|
+
"POT-Creation-Date: 2026-01-05 22:28+0100\n"
|
|
13
13
|
"PO-Revision-Date: 2025-08-03 04:24+0000\n"
|
|
14
14
|
"Last-Translator: The “Devcutter” Guy <mick162534@gmail.com>\n"
|
|
15
15
|
"Language-Team: French <https://weblate.ppfeufer.de/projects/alliance-auth-apps/aa-rss-to-discord/fr/>\n"
|
|
@@ -7,7 +7,7 @@ msgid ""
|
|
|
7
7
|
msgstr ""
|
|
8
8
|
"Project-Id-Version: AA RSS to Discord 2.2.0\n"
|
|
9
9
|
"Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-rss-to-discord/issues\n"
|
|
10
|
-
"POT-Creation-Date:
|
|
10
|
+
"POT-Creation-Date: 2026-01-05 22:28+0100\n"
|
|
11
11
|
"PO-Revision-Date: 2024-05-10 14:10+0000\n"
|
|
12
12
|
"Last-Translator: Anonymous <noreply@weblate.org>\n"
|
|
13
13
|
"Language-Team: Italian <https://weblate.ppfeufer.de/projects/alliance-auth-apps/aa-rss-to-discord/it/>\n"
|
|
@@ -6,7 +6,7 @@ msgid ""
|
|
|
6
6
|
msgstr ""
|
|
7
7
|
"Project-Id-Version: AA RSS to Discord 2.2.0\n"
|
|
8
8
|
"Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-rss-to-discord/issues\n"
|
|
9
|
-
"POT-Creation-Date:
|
|
9
|
+
"POT-Creation-Date: 2026-01-05 22:28+0100\n"
|
|
10
10
|
"PO-Revision-Date: 2024-08-05 10:10+0000\n"
|
|
11
11
|
"Last-Translator: Anata_no_Usiro <yt23542354m@gmail.com>\n"
|
|
12
12
|
"Language-Team: Japanese <https://weblate.ppfeufer.de/projects/alliance-auth-apps/aa-rss-to-discord/ja/>\n"
|
|
@@ -7,7 +7,7 @@ msgid ""
|
|
|
7
7
|
msgstr ""
|
|
8
8
|
"Project-Id-Version: AA RSS to Discord 2.2.0\n"
|
|
9
9
|
"Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-rss-to-discord/issues\n"
|
|
10
|
-
"POT-Creation-Date:
|
|
10
|
+
"POT-Creation-Date: 2026-01-05 22:28+0100\n"
|
|
11
11
|
"PO-Revision-Date: 2024-05-10 14:10+0000\n"
|
|
12
12
|
"Last-Translator: Hue Radient <seataoji@gmail.com>\n"
|
|
13
13
|
"Language-Team: Korean <https://weblate.ppfeufer.de/projects/alliance-auth-apps/aa-rss-to-discord/ko/>\n"
|
|
@@ -7,7 +7,7 @@ msgid ""
|
|
|
7
7
|
msgstr ""
|
|
8
8
|
"Project-Id-Version: AA RSS to Discord 2.2.0\n"
|
|
9
9
|
"Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-rss-to-discord/issues\n"
|
|
10
|
-
"POT-Creation-Date:
|
|
10
|
+
"POT-Creation-Date: 2026-01-05 22:28+0100\n"
|
|
11
11
|
"PO-Revision-Date: 2024-05-10 14:10+0000\n"
|
|
12
12
|
"Last-Translator: Anonymous <noreply@weblate.org>\n"
|
|
13
13
|
"Language-Team: Dutch <https://weblate.ppfeufer.de/projects/alliance-auth-apps/aa-rss-to-discord/nl/>\n"
|
|
@@ -7,7 +7,7 @@ msgid ""
|
|
|
7
7
|
msgstr ""
|
|
8
8
|
"Project-Id-Version: AA RSS to Discord 2.2.0\n"
|
|
9
9
|
"Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-rss-to-discord/issues\n"
|
|
10
|
-
"POT-Creation-Date:
|
|
10
|
+
"POT-Creation-Date: 2026-01-05 22:28+0100\n"
|
|
11
11
|
"PO-Revision-Date: 2024-05-10 14:10+0000\n"
|
|
12
12
|
"Last-Translator: Anonymous <noreply@weblate.org>\n"
|
|
13
13
|
"Language-Team: Polish <https://weblate.ppfeufer.de/projects/alliance-auth-apps/aa-rss-to-discord/pl/>\n"
|
|
@@ -7,7 +7,7 @@ msgid ""
|
|
|
7
7
|
msgstr ""
|
|
8
8
|
"Project-Id-Version: AA RSS to Discord 2.2.0\n"
|
|
9
9
|
"Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-rss-to-discord/issues\n"
|
|
10
|
-
"POT-Creation-Date:
|
|
10
|
+
"POT-Creation-Date: 2026-01-05 22:28+0100\n"
|
|
11
11
|
"PO-Revision-Date: 2024-05-10 14:10+0000\n"
|
|
12
12
|
"Last-Translator: Dromiel <dimhry@yandex.ru>\n"
|
|
13
13
|
"Language-Team: Russian <https://weblate.ppfeufer.de/projects/alliance-auth-apps/aa-rss-to-discord/ru/>\n"
|
|
@@ -7,7 +7,7 @@ msgid ""
|
|
|
7
7
|
msgstr ""
|
|
8
8
|
"Project-Id-Version: AA RSS to Discord 2.2.0\n"
|
|
9
9
|
"Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-rss-to-discord/issues\n"
|
|
10
|
-
"POT-Creation-Date:
|
|
10
|
+
"POT-Creation-Date: 2026-01-05 22:28+0100\n"
|
|
11
11
|
"PO-Revision-Date: 2024-05-10 14:10+0000\n"
|
|
12
12
|
"Last-Translator: Anonymous <noreply@weblate.org>\n"
|
|
13
13
|
"Language-Team: Slovak <https://weblate.ppfeufer.de/projects/alliance-auth-apps/aa-rss-to-discord/sk/>\n"
|
|
Binary file
|
|
@@ -3,20 +3,21 @@
|
|
|
3
3
|
# This file is distributed under the same license as the PACKAGE package.
|
|
4
4
|
# Peter Pfeufer <info@ppfeufer.de>, 2023, 2024.
|
|
5
5
|
# s0k0l -_- <salarysalo@gmail.com>, 2025.
|
|
6
|
+
# Варрус <horowiserus@gmail.com>, 2025.
|
|
6
7
|
msgid ""
|
|
7
8
|
msgstr ""
|
|
8
9
|
"Project-Id-Version: AA RSS to Discord 2.2.0\n"
|
|
9
10
|
"Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-rss-to-discord/issues\n"
|
|
10
|
-
"POT-Creation-Date:
|
|
11
|
-
"PO-Revision-Date: 2025-
|
|
12
|
-
"Last-Translator:
|
|
11
|
+
"POT-Creation-Date: 2026-01-05 22:28+0100\n"
|
|
12
|
+
"PO-Revision-Date: 2025-12-15 14:17+0000\n"
|
|
13
|
+
"Last-Translator: Варрус <horowiserus@gmail.com>\n"
|
|
13
14
|
"Language-Team: Ukrainian <https://weblate.ppfeufer.de/projects/alliance-auth-apps/aa-rss-to-discord/uk/>\n"
|
|
14
15
|
"Language: uk\n"
|
|
15
16
|
"MIME-Version: 1.0\n"
|
|
16
17
|
"Content-Type: text/plain; charset=UTF-8\n"
|
|
17
18
|
"Content-Transfer-Encoding: 8bit\n"
|
|
18
19
|
"Plural-Forms: nplurals=4; plural=n==1 ? 3 : (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
|
19
|
-
"X-Generator: Weblate 5.
|
|
20
|
+
"X-Generator: Weblate 5.14.3\n"
|
|
20
21
|
|
|
21
22
|
#: aa_rss_to_discord/__init__.py:10
|
|
22
23
|
msgid "RSS to Discord"
|
|
@@ -24,7 +25,7 @@ msgstr "RSS до Діскорду"
|
|
|
24
25
|
|
|
25
26
|
#: aa_rss_to_discord/models.py:44
|
|
26
27
|
msgid "RSS Feed"
|
|
27
|
-
msgstr "RSS
|
|
28
|
+
msgstr "RSS-стрічка"
|
|
28
29
|
|
|
29
30
|
#: aa_rss_to_discord/models.py:45
|
|
30
31
|
msgid "RSS Feeds"
|
|
@@ -1,39 +1,38 @@
|
|
|
1
1
|
# SOME DESCRIPTIVE TITLE.
|
|
2
2
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
3
3
|
# This file is distributed under the same license as the PACKAGE package.
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# AKA Patrick <hsc844766246@gmail.com>, 2025.
|
|
6
5
|
msgid ""
|
|
7
6
|
msgstr ""
|
|
8
7
|
"Project-Id-Version: AA RSS to Discord 2.2.0\n"
|
|
9
8
|
"Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-rss-to-discord/issues\n"
|
|
10
|
-
"POT-Creation-Date:
|
|
11
|
-
"PO-Revision-Date:
|
|
12
|
-
"Last-Translator:
|
|
13
|
-
"Language-Team: Chinese (Simplified) <https://weblate.ppfeufer.de/projects/alliance-auth-apps/aa-rss-to-discord/zh_Hans/>\n"
|
|
9
|
+
"POT-Creation-Date: 2026-01-05 22:28+0100\n"
|
|
10
|
+
"PO-Revision-Date: 2025-12-08 09:17+0000\n"
|
|
11
|
+
"Last-Translator: AKA Patrick <hsc844766246@gmail.com>\n"
|
|
12
|
+
"Language-Team: Chinese (Simplified Han script) <https://weblate.ppfeufer.de/projects/alliance-auth-apps/aa-rss-to-discord/zh_Hans/>\n"
|
|
14
13
|
"Language: zh_Hans\n"
|
|
15
14
|
"MIME-Version: 1.0\n"
|
|
16
15
|
"Content-Type: text/plain; charset=UTF-8\n"
|
|
17
16
|
"Content-Transfer-Encoding: 8bit\n"
|
|
18
17
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
|
19
|
-
"X-Generator: Weblate 5.
|
|
18
|
+
"X-Generator: Weblate 5.14.3\n"
|
|
20
19
|
|
|
21
20
|
#: aa_rss_to_discord/__init__.py:10
|
|
22
21
|
msgid "RSS to Discord"
|
|
23
|
-
msgstr ""
|
|
22
|
+
msgstr "链接Discord的Rss"
|
|
24
23
|
|
|
25
24
|
#: aa_rss_to_discord/models.py:44
|
|
26
25
|
msgid "RSS Feed"
|
|
27
|
-
msgstr ""
|
|
26
|
+
msgstr "RSS源"
|
|
28
27
|
|
|
29
28
|
#: aa_rss_to_discord/models.py:45
|
|
30
29
|
msgid "RSS Feeds"
|
|
31
|
-
msgstr ""
|
|
30
|
+
msgstr "RSS源"
|
|
32
31
|
|
|
33
32
|
#: aa_rss_to_discord/models.py:72
|
|
34
33
|
msgid "Last Item"
|
|
35
|
-
msgstr ""
|
|
34
|
+
msgstr "最后一项"
|
|
36
35
|
|
|
37
36
|
#: aa_rss_to_discord/models.py:73
|
|
38
37
|
msgid "Last Items"
|
|
39
|
-
msgstr ""
|
|
38
|
+
msgstr "最后几项"
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AA RSS To Discord Tasks
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Standard Library
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
# Third Party
|
|
9
|
+
import feedparser
|
|
10
|
+
from aadiscordbot.tasks import send_message
|
|
11
|
+
from celery import group, shared_task
|
|
12
|
+
|
|
13
|
+
# Alliance Auth
|
|
14
|
+
from allianceauth.services.hooks import get_extension_logger
|
|
15
|
+
from allianceauth.services.tasks import QueueOnce
|
|
16
|
+
|
|
17
|
+
# AA RSS to Discord
|
|
18
|
+
from aa_rss_to_discord import __title__
|
|
19
|
+
from aa_rss_to_discord.constants import USER_AGENT
|
|
20
|
+
from aa_rss_to_discord.models import LastItem, RssFeeds
|
|
21
|
+
from aa_rss_to_discord.providers import AppLogger
|
|
22
|
+
|
|
23
|
+
logger = AppLogger(get_extension_logger(__name__), __title__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def remove_emoji(string: str) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Removing these dumb as fuck emojis from the title string.
|
|
29
|
+
Like honestly, who in the hell needs that shit?
|
|
30
|
+
|
|
31
|
+
:param string: Input string
|
|
32
|
+
:type string: str
|
|
33
|
+
:return: String without emojis
|
|
34
|
+
:rtype: str
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
emoji_pattern = re.compile(
|
|
38
|
+
pattern="["
|
|
39
|
+
"\U0001f600-\U0001f64f" # Emoticons
|
|
40
|
+
"\U0001f300-\U0001f5ff" # Symbols & pictographs
|
|
41
|
+
"\U0001f680-\U0001f6ff" # Transport & map symbols
|
|
42
|
+
"\U0001f1e0-\U0001f1ff" # Flags (iOS)
|
|
43
|
+
"\U00002500-\U00002bef" # Chinese char
|
|
44
|
+
"\U00002702-\U000027b0"
|
|
45
|
+
"\U00002702-\U000027b0"
|
|
46
|
+
"\U000024c2-\U0001f251"
|
|
47
|
+
"\U0001f926-\U0001f937"
|
|
48
|
+
"\U00010000-\U0010ffff"
|
|
49
|
+
"\u2640-\u2642"
|
|
50
|
+
"\u2600-\u2b55"
|
|
51
|
+
"\u200d"
|
|
52
|
+
"\u23cf"
|
|
53
|
+
"\u23e9"
|
|
54
|
+
"\u231a"
|
|
55
|
+
"\ufe0f" # Dingbats
|
|
56
|
+
"\u3030"
|
|
57
|
+
"]+",
|
|
58
|
+
flags=re.UNICODE,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return emoji_pattern.sub(repl=r"", string=string)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@shared_task(**{"base": QueueOnce})
|
|
65
|
+
def fetch_rss() -> None:
|
|
66
|
+
"""
|
|
67
|
+
Fetch all enabled RSS feeds and dispatch processing tasks.
|
|
68
|
+
|
|
69
|
+
:return: None
|
|
70
|
+
:rtype: None
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
rss_feeds = RssFeeds.objects.select_enabled()
|
|
74
|
+
if not rss_feeds:
|
|
75
|
+
logger.debug("No RSS feeds found to parse.")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
feed_ids = [f.id for f in rss_feeds]
|
|
79
|
+
if not feed_ids:
|
|
80
|
+
logger.debug("No RSS feed ids to dispatch.")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
group(_process_feed.s(fid) for fid in feed_ids).apply_async()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@shared_task
|
|
87
|
+
def _process_feed(rss_feed_id: int) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Process a single RSS feed by fetching its latest entry and posting to Discord if it's new.
|
|
90
|
+
|
|
91
|
+
:param rss_feed_id: ID of the RSS feed to process
|
|
92
|
+
:type rss_feed_id: int
|
|
93
|
+
:return: None
|
|
94
|
+
:rtype: None
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
rss_feed = RssFeeds.objects.select_related("discord_channel").get(
|
|
99
|
+
id=rss_feed_id
|
|
100
|
+
)
|
|
101
|
+
except RssFeeds.DoesNotExist:
|
|
102
|
+
logger.debug("RSS feed %s not found", rss_feed_id)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
logger.info(f'Fetching RSS Feed "{rss_feed.name}"')
|
|
106
|
+
feed = feedparser.parse(rss_feed.url, agent=USER_AGENT)
|
|
107
|
+
|
|
108
|
+
latest_entry = next(iter(getattr(feed, "entries", [])), None)
|
|
109
|
+
if not latest_entry:
|
|
110
|
+
logger.debug(f'No entries found for feed "{rss_feed.name}".')
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
feed_entry_title = remove_emoji(latest_entry.get("title", "No title"))
|
|
114
|
+
feed_entry_link = latest_entry.get("link")
|
|
115
|
+
feed_entry_time = latest_entry.get("published", latest_entry.get("updated"))
|
|
116
|
+
feed_entry_guid = latest_entry.get("id")
|
|
117
|
+
|
|
118
|
+
last_item = LastItem.objects.filter(rss_feed=rss_feed).first()
|
|
119
|
+
if last_item and (
|
|
120
|
+
last_item.rss_item_time,
|
|
121
|
+
last_item.rss_item_title,
|
|
122
|
+
last_item.rss_item_link,
|
|
123
|
+
last_item.rss_item_guid,
|
|
124
|
+
) == (feed_entry_time, feed_entry_title, feed_entry_link, feed_entry_guid):
|
|
125
|
+
logger.debug(
|
|
126
|
+
'News item "%s" for RSS Feed "%s" has already been posted to your Discord',
|
|
127
|
+
feed_entry_title,
|
|
128
|
+
rss_feed.name,
|
|
129
|
+
)
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
if not last_item:
|
|
133
|
+
logger.debug("This seems to be a completely new RSS feed: %s", rss_feed.name)
|
|
134
|
+
|
|
135
|
+
logger.info(
|
|
136
|
+
'New entry found for RSS feed "%s", posting to Discord channel %s',
|
|
137
|
+
rss_feed.name,
|
|
138
|
+
rss_feed.discord_channel,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
LastItem.objects.update_or_create(
|
|
142
|
+
rss_feed=rss_feed,
|
|
143
|
+
defaults={
|
|
144
|
+
"rss_item_time": feed_entry_time,
|
|
145
|
+
"rss_item_title": feed_entry_title,
|
|
146
|
+
"rss_item_link": feed_entry_link,
|
|
147
|
+
"rss_item_guid": feed_entry_guid,
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
send_message(
|
|
152
|
+
channel_id=rss_feed.discord_channel.channel,
|
|
153
|
+
message=f"**{rss_feed.name}**\n{feed_entry_link}",
|
|
154
|
+
)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test cases for the fetch_rss task
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Standard Library
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
from unittest.mock import MagicMock, patch
|
|
8
|
+
|
|
9
|
+
# AA RSS to Discord
|
|
10
|
+
from aa_rss_to_discord.constants import USER_AGENT
|
|
11
|
+
from aa_rss_to_discord.models import RssFeeds
|
|
12
|
+
from aa_rss_to_discord.tasks import _process_feed, fetch_rss
|
|
13
|
+
from aa_rss_to_discord.tests import BaseTestCase
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestFetchRss(BaseTestCase):
|
|
17
|
+
"""
|
|
18
|
+
Test cases for the fetch_rss task
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def test_fetch_rss_logs_message_when_no_rss_feeds_found(self):
|
|
22
|
+
"""
|
|
23
|
+
Test that fetch_rss logs a message when no RSS feeds are found.
|
|
24
|
+
|
|
25
|
+
:return:
|
|
26
|
+
:rtype:
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
with (
|
|
30
|
+
patch(
|
|
31
|
+
"aa_rss_to_discord.tasks.RssFeeds.objects.select_enabled"
|
|
32
|
+
) as mock_select_enabled,
|
|
33
|
+
patch("aa_rss_to_discord.tasks.logger.debug") as mock_logger_debug,
|
|
34
|
+
):
|
|
35
|
+
mock_select_enabled.return_value = []
|
|
36
|
+
|
|
37
|
+
fetch_rss()
|
|
38
|
+
|
|
39
|
+
mock_logger_debug.assert_called_once_with("No RSS feeds found to parse.")
|
|
40
|
+
|
|
41
|
+
def test_fetch_rss_dispatches_tasks_for_enabled_feeds(self):
|
|
42
|
+
"""
|
|
43
|
+
Test that fetch_rss dispatches tasks for enabled RSS feeds.
|
|
44
|
+
|
|
45
|
+
:return:
|
|
46
|
+
:rtype:
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
with (
|
|
50
|
+
patch(
|
|
51
|
+
"aa_rss_to_discord.tasks.RssFeeds.objects.select_enabled"
|
|
52
|
+
) as mock_select_enabled,
|
|
53
|
+
patch("aa_rss_to_discord.tasks.group") as mock_group,
|
|
54
|
+
):
|
|
55
|
+
mock_rss_feed_1 = MagicMock(id=1)
|
|
56
|
+
mock_rss_feed_2 = MagicMock(id=2)
|
|
57
|
+
mock_select_enabled.return_value = [mock_rss_feed_1, mock_rss_feed_2]
|
|
58
|
+
|
|
59
|
+
fetch_rss()
|
|
60
|
+
|
|
61
|
+
mock_group.assert_called_once()
|
|
62
|
+
mock_group.return_value.apply_async.assert_called_once()
|
|
63
|
+
dispatched_tasks = mock_group.call_args[0][0]
|
|
64
|
+
dispatched_list = list(dispatched_tasks)
|
|
65
|
+
self.assertEqual(len(dispatched_list), 2)
|
|
66
|
+
self.assertEqual(dispatched_list[0].args[0], 1)
|
|
67
|
+
self.assertEqual(dispatched_list[1].args[0], 2)
|
|
68
|
+
|
|
69
|
+
def test_handles_empty_rss_feed_ids_gracefully(self):
|
|
70
|
+
"""
|
|
71
|
+
Test that fetch_rss handles an empty list of RSS feed IDs gracefully.
|
|
72
|
+
|
|
73
|
+
:return:
|
|
74
|
+
:rtype:
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
with (
|
|
78
|
+
patch(
|
|
79
|
+
"aa_rss_to_discord.tasks.RssFeeds.objects.select_enabled"
|
|
80
|
+
) as mock_select_enabled,
|
|
81
|
+
patch("aa_rss_to_discord.tasks.group") as mock_group,
|
|
82
|
+
patch("aa_rss_to_discord.tasks.logger.debug") as mock_logger_debug,
|
|
83
|
+
):
|
|
84
|
+
mock_select_enabled.return_value = iter([])
|
|
85
|
+
|
|
86
|
+
fetch_rss()
|
|
87
|
+
|
|
88
|
+
mock_group.assert_not_called()
|
|
89
|
+
mock_logger_debug.assert_called_once_with("No RSS feed ids to dispatch.")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestHelperProcessRssFeed(BaseTestCase):
|
|
93
|
+
"""
|
|
94
|
+
Test cases for the _process_feed task
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def test_handles_missing_rss_feed_gracefully(self):
|
|
98
|
+
"""
|
|
99
|
+
Test that _process_feed handles a missing RSS feed gracefully.
|
|
100
|
+
|
|
101
|
+
:return:
|
|
102
|
+
:rtype:
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
with patch(
|
|
106
|
+
"aa_rss_to_discord.tasks.RssFeeds.objects.select_related"
|
|
107
|
+
) as mock_select_related:
|
|
108
|
+
mock_select_related.return_value.get.side_effect = RssFeeds.DoesNotExist
|
|
109
|
+
|
|
110
|
+
_process_feed(1)
|
|
111
|
+
|
|
112
|
+
mock_select_related.return_value.get.assert_called_once_with(id=1)
|
|
113
|
+
|
|
114
|
+
def test_logs_and_skips_when_no_entries_in_feed(self):
|
|
115
|
+
"""
|
|
116
|
+
Test that _process_feed logs and skips processing when no entries are found in the feed.
|
|
117
|
+
|
|
118
|
+
:return:
|
|
119
|
+
:rtype:
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
with (
|
|
123
|
+
patch(
|
|
124
|
+
"aa_rss_to_discord.tasks.RssFeeds.objects.select_related"
|
|
125
|
+
) as mock_select_related,
|
|
126
|
+
patch(
|
|
127
|
+
"aa_rss_to_discord.tasks.feedparser.parse", return_value={}
|
|
128
|
+
) as mock_parse,
|
|
129
|
+
):
|
|
130
|
+
mock_rss_feed = MagicMock(url="http://example.com", name="No Entries Feed")
|
|
131
|
+
mock_select_related.return_value.get.return_value = mock_rss_feed
|
|
132
|
+
|
|
133
|
+
_process_feed(1)
|
|
134
|
+
|
|
135
|
+
mock_parse.assert_called_once_with(mock_rss_feed.url, agent=USER_AGENT)
|
|
136
|
+
|
|
137
|
+
def test_posts_new_entry_to_discord(self):
|
|
138
|
+
"""
|
|
139
|
+
Test that _process_feed posts a new entry to Discord when it's not a duplicate.
|
|
140
|
+
|
|
141
|
+
:return:
|
|
142
|
+
:rtype:
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
entry = {"title": "New Entry", "link": "http://example.com", "id": "123"}
|
|
146
|
+
|
|
147
|
+
with (
|
|
148
|
+
patch(
|
|
149
|
+
"aa_rss_to_discord.tasks.RssFeeds.objects.select_related"
|
|
150
|
+
) as mock_select_related,
|
|
151
|
+
patch(
|
|
152
|
+
"aa_rss_to_discord.tasks.feedparser.parse",
|
|
153
|
+
return_value=SimpleNamespace(entries=[entry]),
|
|
154
|
+
) as mock_parse,
|
|
155
|
+
patch("aa_rss_to_discord.tasks.LastItem.objects.filter") as mock_filter,
|
|
156
|
+
patch(
|
|
157
|
+
"aa_rss_to_discord.tasks.LastItem.objects.update_or_create"
|
|
158
|
+
) as mock_update_or_create,
|
|
159
|
+
patch("aa_rss_to_discord.tasks.send_message") as mock_send_message,
|
|
160
|
+
):
|
|
161
|
+
mock_rss_feed = MagicMock(
|
|
162
|
+
discord_channel=MagicMock(channel=12345),
|
|
163
|
+
name="Test Feed",
|
|
164
|
+
url="http://example.com",
|
|
165
|
+
)
|
|
166
|
+
mock_select_related.return_value.get.return_value = mock_rss_feed
|
|
167
|
+
mock_filter.return_value.first.return_value = None
|
|
168
|
+
mock_update_or_create.return_value = (MagicMock(), True)
|
|
169
|
+
|
|
170
|
+
_process_feed(1)
|
|
171
|
+
|
|
172
|
+
mock_parse.assert_called_once_with(mock_rss_feed.url, agent=USER_AGENT)
|
|
173
|
+
mock_update_or_create.assert_called_once()
|
|
174
|
+
self.assertTrue(
|
|
175
|
+
mock_send_message.called,
|
|
176
|
+
f"send_message not called. calls: {mock_send_message.call_args_list}",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
call_args, call_kwargs = mock_send_message.call_args
|
|
180
|
+
called_channel = (
|
|
181
|
+
call_kwargs.get("channel_id")
|
|
182
|
+
if "channel_id" in call_kwargs
|
|
183
|
+
else (call_args[0] if len(call_args) > 0 else None)
|
|
184
|
+
)
|
|
185
|
+
called_message = (
|
|
186
|
+
call_kwargs.get("message")
|
|
187
|
+
if "message" in call_kwargs
|
|
188
|
+
else (
|
|
189
|
+
call_args[1]
|
|
190
|
+
if len(call_args) > 1
|
|
191
|
+
else (call_args[0] if len(call_args) == 1 else None)
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
self.assertEqual(called_channel, 12345)
|
|
196
|
+
self.assertIsNotNone(called_message)
|
|
197
|
+
self.assertIn(mock_rss_feed.url, called_message)
|
|
198
|
+
|
|
199
|
+
def test_skips_posting_duplicate_entry(self):
|
|
200
|
+
"""
|
|
201
|
+
Test that _process_feed skips posting a duplicate entry to Discord.
|
|
202
|
+
|
|
203
|
+
:return:
|
|
204
|
+
:rtype:
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
entry = {"title": "Duplicate Entry", "link": "http://example.com", "id": "123"}
|
|
208
|
+
|
|
209
|
+
with (
|
|
210
|
+
patch(
|
|
211
|
+
"aa_rss_to_discord.tasks.RssFeeds.objects.select_related"
|
|
212
|
+
) as mock_select_related,
|
|
213
|
+
patch(
|
|
214
|
+
"aa_rss_to_discord.tasks.feedparser.parse",
|
|
215
|
+
return_value=SimpleNamespace(entries=[entry]),
|
|
216
|
+
),
|
|
217
|
+
patch("aa_rss_to_discord.tasks.LastItem.objects.filter") as mock_filter,
|
|
218
|
+
patch("aa_rss_to_discord.tasks.send_message") as mock_send_message,
|
|
219
|
+
):
|
|
220
|
+
mock_rss_feed = MagicMock(
|
|
221
|
+
discord_channel=MagicMock(channel=12345),
|
|
222
|
+
name="Dup Feed",
|
|
223
|
+
url="http://example.com",
|
|
224
|
+
)
|
|
225
|
+
mock_select_related.return_value.get.return_value = mock_rss_feed
|
|
226
|
+
mock_existing = MagicMock(
|
|
227
|
+
rss_item_time=None,
|
|
228
|
+
rss_item_title="Duplicate Entry",
|
|
229
|
+
rss_item_link="http://example.com",
|
|
230
|
+
rss_item_guid="123",
|
|
231
|
+
)
|
|
232
|
+
mock_filter.return_value.first.return_value = mock_existing
|
|
233
|
+
|
|
234
|
+
_process_feed(1)
|
|
235
|
+
|
|
236
|
+
mock_send_message.assert_not_called()
|
|
@@ -42,7 +42,7 @@ dependencies = [
|
|
|
42
42
|
"allianceauth>=4.3.1,<5",
|
|
43
43
|
"allianceauth-discordbot>=3.0.5",
|
|
44
44
|
"feedparser",
|
|
45
|
-
"py-cord>=2.7.0rc2; python_version
|
|
45
|
+
"py-cord>=2.7.0rc2; python_version>='3.13'",
|
|
46
46
|
]
|
|
47
47
|
optional-dependencies.tests-allianceauth-latest = [
|
|
48
48
|
"coverage",
|
|
Binary file
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
AA RSS To Discord Tasks
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
# Standard Library
|
|
6
|
-
import re
|
|
7
|
-
|
|
8
|
-
# Third Party
|
|
9
|
-
import feedparser
|
|
10
|
-
from aadiscordbot.tasks import send_message
|
|
11
|
-
from celery import shared_task
|
|
12
|
-
|
|
13
|
-
# Alliance Auth
|
|
14
|
-
from allianceauth.services.hooks import get_extension_logger
|
|
15
|
-
from allianceauth.services.tasks import QueueOnce
|
|
16
|
-
|
|
17
|
-
# AA RSS to Discord
|
|
18
|
-
from aa_rss_to_discord import __title__
|
|
19
|
-
from aa_rss_to_discord.constants import USER_AGENT
|
|
20
|
-
from aa_rss_to_discord.models import LastItem, RssFeeds
|
|
21
|
-
from aa_rss_to_discord.providers import AppLogger
|
|
22
|
-
|
|
23
|
-
logger = AppLogger(get_extension_logger(__name__), __title__)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def remove_emoji(string):
|
|
27
|
-
"""
|
|
28
|
-
Removing these dumb as fuck emojis from the title string.
|
|
29
|
-
Like honestly, who in the hell needs that shit?
|
|
30
|
-
|
|
31
|
-
:param string:
|
|
32
|
-
:type string:
|
|
33
|
-
:return:
|
|
34
|
-
:rtype:
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
emoji_pattern = re.compile(
|
|
38
|
-
pattern="["
|
|
39
|
-
"\U0001f600-\U0001f64f" # Emoticons
|
|
40
|
-
"\U0001f300-\U0001f5ff" # Symbols & pictographs
|
|
41
|
-
"\U0001f680-\U0001f6ff" # Transport & map symbols
|
|
42
|
-
"\U0001f1e0-\U0001f1ff" # Flags (iOS)
|
|
43
|
-
"\U00002500-\U00002bef" # Chinese char
|
|
44
|
-
"\U00002702-\U000027b0"
|
|
45
|
-
"\U00002702-\U000027b0"
|
|
46
|
-
"\U000024c2-\U0001f251"
|
|
47
|
-
"\U0001f926-\U0001f937"
|
|
48
|
-
"\U00010000-\U0010ffff"
|
|
49
|
-
"\u2640-\u2642"
|
|
50
|
-
"\u2600-\u2b55"
|
|
51
|
-
"\u200d"
|
|
52
|
-
"\u23cf"
|
|
53
|
-
"\u23e9"
|
|
54
|
-
"\u231a"
|
|
55
|
-
"\ufe0f" # Dingbats
|
|
56
|
-
"\u3030"
|
|
57
|
-
"]+",
|
|
58
|
-
flags=re.UNICODE,
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
return emoji_pattern.sub(repl=r"", string=string)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@shared_task(**{"base": QueueOnce})
|
|
65
|
-
def fetch_rss() -> None:
|
|
66
|
-
"""
|
|
67
|
-
Fetch RSS feeds and post new entries to Discord channels.
|
|
68
|
-
|
|
69
|
-
:return:
|
|
70
|
-
:rtype:
|
|
71
|
-
"""
|
|
72
|
-
|
|
73
|
-
rss_feeds = RssFeeds.objects.select_enabled()
|
|
74
|
-
|
|
75
|
-
if not rss_feeds:
|
|
76
|
-
logger.debug("No RSS feeds found to parse.")
|
|
77
|
-
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
for rss_feed in rss_feeds:
|
|
81
|
-
logger.info(f'Fetching RSS Feed "{rss_feed.name}"')
|
|
82
|
-
feed = feedparser.parse(rss_feed.url, agent=USER_AGENT)
|
|
83
|
-
|
|
84
|
-
try:
|
|
85
|
-
latest_entry = feed.entries[0]
|
|
86
|
-
feed_entry_title = remove_emoji(latest_entry.get("title", "No title"))
|
|
87
|
-
feed_entry_link = latest_entry.get("link")
|
|
88
|
-
feed_entry_time = latest_entry.get("published", latest_entry.updated)
|
|
89
|
-
feed_entry_guid = latest_entry.get("id")
|
|
90
|
-
except (AttributeError, IndexError) as exc:
|
|
91
|
-
logger.debug(f'Error processing feed "{rss_feed.name}": {exc}')
|
|
92
|
-
|
|
93
|
-
continue
|
|
94
|
-
|
|
95
|
-
try:
|
|
96
|
-
last_item = LastItem.objects.get(rss_feed=rss_feed)
|
|
97
|
-
is_duplicate = (
|
|
98
|
-
last_item.rss_item_time == feed_entry_time
|
|
99
|
-
and last_item.rss_item_title == feed_entry_title
|
|
100
|
-
and last_item.rss_item_link == feed_entry_link
|
|
101
|
-
and last_item.rss_item_guid == feed_entry_guid
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
if is_duplicate:
|
|
105
|
-
logger.debug(
|
|
106
|
-
f'News item "{feed_entry_title}" for RSS Feed "{rss_feed.name}" '
|
|
107
|
-
"has already been posted to your Discord"
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
continue
|
|
111
|
-
except LastItem.DoesNotExist:
|
|
112
|
-
logger.debug("This seems to be a completely new RSS feed.")
|
|
113
|
-
|
|
114
|
-
logger.info(
|
|
115
|
-
f"New entry found, posting to Discord channel {rss_feed.discord_channel}"
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
LastItem.objects.update_or_create(
|
|
119
|
-
rss_feed=rss_feed,
|
|
120
|
-
defaults={
|
|
121
|
-
"rss_item_time": feed_entry_time,
|
|
122
|
-
"rss_item_title": feed_entry_title,
|
|
123
|
-
"rss_item_link": feed_entry_link,
|
|
124
|
-
"rss_item_guid": feed_entry_guid,
|
|
125
|
-
},
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
send_message(
|
|
129
|
-
channel_id=rss_feed.discord_channel.channel,
|
|
130
|
-
message=f"**{rss_feed.name}**\n{feed_entry_link}",
|
|
131
|
-
)
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Test cases for the fetch_rss task
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
# Standard Library
|
|
6
|
-
from unittest.mock import MagicMock, patch
|
|
7
|
-
|
|
8
|
-
# AA RSS to Discord
|
|
9
|
-
from aa_rss_to_discord.models import LastItem
|
|
10
|
-
from aa_rss_to_discord.tasks import fetch_rss
|
|
11
|
-
from aa_rss_to_discord.tests import BaseTestCase
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class TestFetchRss(BaseTestCase):
|
|
15
|
-
"""
|
|
16
|
-
Test fetch_rss task
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
def test_logs_warning_if_no_rss_feeds_found(self):
|
|
20
|
-
"""
|
|
21
|
-
Test that a warning is logged if no RSS feeds are found.
|
|
22
|
-
|
|
23
|
-
:return:
|
|
24
|
-
:rtype:
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
with (
|
|
28
|
-
patch(
|
|
29
|
-
"aa_rss_to_discord.tasks.RssFeeds.objects.select_enabled",
|
|
30
|
-
return_value=[],
|
|
31
|
-
),
|
|
32
|
-
patch("aa_rss_to_discord.tasks.logger.debug") as mock_debug,
|
|
33
|
-
):
|
|
34
|
-
fetch_rss()
|
|
35
|
-
mock_debug.assert_called_once_with("No RSS feeds found to parse.")
|
|
36
|
-
|
|
37
|
-
def test_processes_valid_rss_feed_entry(self):
|
|
38
|
-
"""
|
|
39
|
-
Test that a valid RSS feed entry is processed and sent to Discord.
|
|
40
|
-
|
|
41
|
-
:return:
|
|
42
|
-
:rtype:
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
discord_channel = MagicMock()
|
|
46
|
-
discord_channel.channel = 12345
|
|
47
|
-
|
|
48
|
-
rss_feed = MagicMock()
|
|
49
|
-
rss_feed.name = "Valid Feed"
|
|
50
|
-
rss_feed.url = "http://example.com/rss"
|
|
51
|
-
rss_feed.discord_channel = discord_channel
|
|
52
|
-
|
|
53
|
-
# Use a MagicMock that acts like feedparser entry with both dict and attribute access
|
|
54
|
-
feed_entry = MagicMock()
|
|
55
|
-
feed_entry.get = lambda key, default=None: {
|
|
56
|
-
"title": "Valid Entry",
|
|
57
|
-
"link": "http://example.com/valid-entry",
|
|
58
|
-
"published": "2023-10-01T12:00:00Z",
|
|
59
|
-
"id": "entry-1",
|
|
60
|
-
}.get(key, default)
|
|
61
|
-
feed_entry.updated = (
|
|
62
|
-
"2023-10-01T12:00:00Z" # Fallback if 'published' is missing
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
parsed_feed = MagicMock()
|
|
66
|
-
parsed_feed.entries = [feed_entry]
|
|
67
|
-
|
|
68
|
-
with (
|
|
69
|
-
patch(
|
|
70
|
-
"aa_rss_to_discord.tasks.RssFeeds.objects.select_enabled",
|
|
71
|
-
return_value=[rss_feed],
|
|
72
|
-
),
|
|
73
|
-
patch(
|
|
74
|
-
"aa_rss_to_discord.tasks.feedparser.parse",
|
|
75
|
-
return_value=parsed_feed,
|
|
76
|
-
),
|
|
77
|
-
patch(
|
|
78
|
-
"aa_rss_to_discord.tasks.LastItem.objects.get",
|
|
79
|
-
side_effect=LastItem.DoesNotExist,
|
|
80
|
-
),
|
|
81
|
-
patch(
|
|
82
|
-
"aa_rss_to_discord.tasks.LastItem.objects.update_or_create",
|
|
83
|
-
),
|
|
84
|
-
patch(
|
|
85
|
-
"aa_rss_to_discord.tasks.send_message",
|
|
86
|
-
) as mock_send_message,
|
|
87
|
-
):
|
|
88
|
-
fetch_rss()
|
|
89
|
-
|
|
90
|
-
mock_send_message.assert_called_once()
|
|
91
|
-
call_args = mock_send_message.call_args
|
|
92
|
-
self.assertEqual(call_args.kwargs.get("channel_id"), 12345)
|
|
93
|
-
message = call_args.kwargs.get("message", "")
|
|
94
|
-
self.assertIn("http://example.com/valid-entry", message)
|
|
95
|
-
self.assertIn("Valid Feed", message)
|
|
96
|
-
|
|
97
|
-
def test_skips_duplicate_rss_feed_entry(self):
|
|
98
|
-
"""
|
|
99
|
-
Test that a duplicate RSS feed entry is skipped.
|
|
100
|
-
|
|
101
|
-
:return:
|
|
102
|
-
:rtype:
|
|
103
|
-
"""
|
|
104
|
-
|
|
105
|
-
rss_feed = MagicMock()
|
|
106
|
-
rss_feed.name = "Duplicate Feed"
|
|
107
|
-
rss_feed.url = "http://example.com/rss"
|
|
108
|
-
|
|
109
|
-
# Create feed entry that supports both dict-style and attribute access
|
|
110
|
-
feed_entry = MagicMock()
|
|
111
|
-
feed_entry.get = lambda key, default=None: {
|
|
112
|
-
"title": "Duplicate Entry",
|
|
113
|
-
"link": "http://example.com/duplicate-entry",
|
|
114
|
-
"published": "2023-10-01T12:00:00Z",
|
|
115
|
-
"id": "entry-1",
|
|
116
|
-
}.get(key, default)
|
|
117
|
-
feed_entry.updated = "2023-10-01T12:00:00Z"
|
|
118
|
-
|
|
119
|
-
last_item = MagicMock(
|
|
120
|
-
rss_item_time="2023-10-01T12:00:00Z",
|
|
121
|
-
rss_item_title="Duplicate Entry",
|
|
122
|
-
rss_item_link="http://example.com/duplicate-entry",
|
|
123
|
-
rss_item_guid="entry-1",
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
with (
|
|
127
|
-
patch(
|
|
128
|
-
"aa_rss_to_discord.tasks.RssFeeds.objects.select_enabled",
|
|
129
|
-
return_value=[rss_feed],
|
|
130
|
-
),
|
|
131
|
-
patch(
|
|
132
|
-
"aa_rss_to_discord.tasks.feedparser.parse",
|
|
133
|
-
return_value=MagicMock(entries=[feed_entry]),
|
|
134
|
-
),
|
|
135
|
-
patch(
|
|
136
|
-
"aa_rss_to_discord.tasks.LastItem.objects.get",
|
|
137
|
-
return_value=last_item,
|
|
138
|
-
),
|
|
139
|
-
patch("aa_rss_to_discord.tasks.logger.debug") as mock_debug,
|
|
140
|
-
patch("aa_rss_to_discord.tasks.send_message") as mock_send_message,
|
|
141
|
-
):
|
|
142
|
-
fetch_rss()
|
|
143
|
-
|
|
144
|
-
mock_send_message.assert_not_called()
|
|
145
|
-
mock_debug.assert_any_call(
|
|
146
|
-
'News item "Duplicate Entry" for RSS Feed "Duplicate Feed" has already been posted to your Discord'
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
def test_skips_rss_feed_with_no_entries(self):
|
|
150
|
-
"""
|
|
151
|
-
Test that an RSS feed with no entries is skipped.
|
|
152
|
-
|
|
153
|
-
:return:
|
|
154
|
-
:rtype:
|
|
155
|
-
"""
|
|
156
|
-
|
|
157
|
-
rss_feed = MagicMock()
|
|
158
|
-
rss_feed.name = "Empty Feed"
|
|
159
|
-
rss_feed.url = "http://example.com/rss"
|
|
160
|
-
|
|
161
|
-
with (
|
|
162
|
-
patch(
|
|
163
|
-
"aa_rss_to_discord.tasks.RssFeeds.objects.select_enabled",
|
|
164
|
-
return_value=[rss_feed],
|
|
165
|
-
),
|
|
166
|
-
patch(
|
|
167
|
-
"aa_rss_to_discord.tasks.feedparser.parse",
|
|
168
|
-
return_value=MagicMock(entries=[]),
|
|
169
|
-
),
|
|
170
|
-
patch("aa_rss_to_discord.tasks.logger.debug") as mock_debug,
|
|
171
|
-
):
|
|
172
|
-
fetch_rss()
|
|
173
|
-
|
|
174
|
-
mock_debug.assert_any_call(
|
|
175
|
-
'Error processing feed "Empty Feed": list index out of range'
|
|
176
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/discordbot/cogs/__init__.py
RENAMED
|
File without changes
|
{aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/discordbot/cogs/rss.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
{aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/migrations/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/tests/test_auth_hooks.py
RENAMED
|
File without changes
|
{aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/tests/test_managers.py
RENAMED
|
File without changes
|
|
File without changes
|
{aa_rss_to_discord-2.4.0 → aa_rss_to_discord-2.5.0}/aa_rss_to_discord/tests/test_providers.py
RENAMED
|
File without changes
|