diff --git a/allianceauth/templatetags/admin_status.py b/allianceauth/templatetags/admin_status.py
index d7084dc5..6768a9a7 100644
--- a/allianceauth/templatetags/admin_status.py
+++ b/allianceauth/templatetags/admin_status.py
@@ -1,4 +1,7 @@
import logging
+from dataclasses import dataclass
+from enum import Enum
+from urllib.parse import quote_plus
import requests
from packaging.version import InvalidVersion, Version as Pep440Version
@@ -11,6 +14,7 @@ from allianceauth import __version__
from allianceauth.authentication.task_statistics.counters import (
dashboard_results,
)
+from allianceauth.hooks import get_hooks
register = template.Library()
@@ -32,6 +36,72 @@ GITLAB_AUTH_ANNOUNCEMENT_ISSUES_URL = (
logger = logging.getLogger(__name__)
+@dataclass
+class AppAnnouncementHook:
+ """
+ A hook for an application to send GitHub/GitLab issues as announcements on the dashboard
+
+ Args:
+ - app_name: The name of your application
+ - repository_namespace: The namespace of the remote repository of your application source code.
+ It should look like `
/`.
+ - repository_kind: Enumeration to determine if your repository is a GitHub or GitLab repository.
+ - label: The label applied to issues that should be seen as announcements, case-sensitive.
+ Default value: `announcement`
+ """
+ class RepositoryKind(Enum):
+ """Simple enumeration to determine which api should be called to access issues"""
+ GITLAB = "gitlab"
+ GITHUB = "github"
+
+ app_name: str
+ repository_namespace: str
+ repository_kind: RepositoryKind
+ label: str = "announcement"
+
+
+ def get_announcement_list(self) -> list:
+ """
+ Checks the application repository to find issues with the `Announcement` tag and return their title and link to
+ be displayed.
+ """
+ match self.repository_kind:
+ case AppAnnouncementHook.RepositoryKind.GITHUB:
+ announcement_list = self._get_github_announcement_list()
+ case AppAnnouncementHook.RepositoryKind.GITLAB:
+ announcement_list = self._get_gitlab_announcement_list()
+ case _:
+ return []
+
+ for announcement in announcement_list:
+ announcement["app_name"] = self.app_name
+
+ return announcement_list
+
+
+ def _get_github_announcement_list(self) -> list:
+ """
+ Return the issue list for a GitHub repository
+ Will filter if the `pull_request` attribute is present
+ """
+ raw_list = _fetch_list_from_github(
+ f"https://api.github.com/repos/{self.repository_namespace}/issues"
+ f"?labels={self.label}"
+ )
+ # Translates GitHub attributes to GitLab and filters out pull requests
+ clean_list = []
+ for element in raw_list:
+ if not element.get("pull_request"):
+ element["web_url"] = element["html_url"]
+ element["iid"] = element["number"]
+ clean_list.append(element)
+ return clean_list
+
+ def _get_gitlab_announcement_list(self) -> list:
+ """Return the issues list for a GitLab repository"""
+ return _fetch_list_from_gitlab(
+ f"https://gitlab.com/api/v4/projects/{quote_plus(self.repository_namespace)}/issues"
+ f"?labels={self.label}&state=opened")
@register.simple_tag()
def decimal_widthratio(this_value, max_value, max_width) -> str:
@@ -89,8 +159,28 @@ def _current_notifications() -> dict:
else:
top_notifications = []
+ app_notifications = []
+ hooks = [fn() for fn in get_hooks("app_announcement_hook")]
+ for hook in hooks:
+ logger.debug(hook)
+ try:
+ app_notifications.extend(cache.get_or_set(
+ f"{hook.app_name}_notification_issues",
+ hook.get_announcement_list,
+ NOTIFICATION_CACHE_TIME,
+ ))
+ except requests.HTTPError:
+ logger.warning("Error when getting %s notifications", hook, exc_info=True)
+
+ if app_notifications:
+ logger.debug(app_notifications)
+ application_notifications = app_notifications[:10]
+ else:
+ application_notifications = []
+
response = {
'notifications': top_notifications,
+ 'application_notifications': application_notifications,
}
return response
@@ -199,3 +289,38 @@ def _fetch_list_from_gitlab(url: str, max_pages: int = MAX_PAGES) -> list:
break
return result
+
+def _fetch_list_from_github(url: str, max_pages: int = MAX_PAGES) -> list:
+ """returns a list from the GitHub API. Supports paging"""
+
+ result = []
+ for page in range(1, max_pages+1):
+ try:
+ request = requests.get(
+ url,
+ params={'page': page},
+ headers={
+ "Accept": "application/vnd.github+json",
+ "X-GitHub-Api-Version": "2022-11-28"
+ }
+ )
+ request.raise_for_status()
+ except requests.exceptions.RequestException as e:
+ error_str = str(e)
+
+ logger.warning(
+ f'Unable to fetch from GitHub API. Error: {error_str}',
+ exc_info=True,
+ )
+
+ return result
+
+ result += request.json()
+ logger.debug(request.json())
+
+ # https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api?apiVersion=2022-11-28
+ # See Example creating a pagination method
+ if not ('link' in request.headers and 'rel=\"next\"' in request.headers['link']):
+ break
+
+ return result
diff --git a/docs/development/custom/app-announcement-hooks.md b/docs/development/custom/app-announcement-hooks.md
new file mode 100644
index 00000000..03650a87
--- /dev/null
+++ b/docs/development/custom/app-announcement-hooks.md
@@ -0,0 +1,52 @@
+# Announcement Hooks
+
+This hook allows the issues opened on your application repository to be displayed on the alliance auth front page to
+administrators.
+
+
+
+To register an AppAnnouncementHook class, you would do the following:
+
+```python
+from allianceauth import hooks
+from allianceauth.services.hooks import AppAnnouncementHook
+
+@hooks.register('app_announcement_hook')
+def announcement_hook():
+ return AppAnnouncementHook("Your app name", "USERNAME/REPOSITORY_NAME", AppAnnouncementHook.RepositoryKind.GITLAB)
+```
+
+```{eval-rst}
+.. autoclass:: allianceauth.services.hooks.AppAnnouncementHook
+ :members: __init__
+ :undoc-members:
+```
+
+## Parameters
+
+### app_name
+
+The name of your application.
+
+### repository_namespace
+
+Here you should enter the namespace of your repository.
+The structure stays the same for both GitHub and GitLab repositories. \
+A repository with the url `https://gitlab.com/username/appname` will have a namespace of `username/appname`.
+
+### repository_kind
+
+This variable is an enumeration of the class `AppAnnouncemementHook.RepositoryKind`
+
+It is mandatory to specify this variable so alliance auth contacts the correct API when fetching your repository issues.
+
+```{eval-rst}
+.. autoclass:: allianceauth.services.hooks.AppAnnouncementHook.RepositoryKind
+ :members: GITLAB, GITHUB
+ :undoc-members:
+```
+
+### label
+
+The label that will determine if issues should be seen as an announcement.
+This value is case-sensitive and the default value is `"announcement"`.
diff --git a/docs/development/custom/img/app_announcement_hook_example.png b/docs/development/custom/img/app_announcement_hook_example.png
new file mode 100755
index 00000000..b57c6950
Binary files /dev/null and b/docs/development/custom/img/app_announcement_hook_example.png differ
diff --git a/docs/development/custom/index.md b/docs/development/custom/index.md
index 72be4d4d..3ed4008b 100644
--- a/docs/development/custom/index.md
+++ b/docs/development/custom/index.md
@@ -8,6 +8,7 @@ This section describes how to extend **Alliance Auth** with custom apps, service
integrating-services
menu-hooks
url-hooks
+app-announcement-hooks
logging
custom-themes
aa-framework