neu mit benachrichtigungslink

This commit is contained in:
Jörg Mühlberger 2026-07-21 11:42:11 +02:00
parent d0b1141648
commit 67a735654a
1 changed files with 134 additions and 43 deletions

View File

@ -12,6 +12,10 @@ use Flarum\User\User;
class NotificationPayloadFactory
{
private const DEFAULT_TEAMS_APP_ID = 'd92b1be6-ea92-46b1-bcb0-c08ffdd1a61a';
private const TEAMS_ENTITY_ID = '8ffe419f-ca44-4627-a4b2-f223da2d07aa';
private const LANDING_PAGE_PATH = '/static/flarum-notify-v2.html';
public function __construct(
protected SettingsRepositoryInterface $settings
) {}
@ -19,41 +23,30 @@ class NotificationPayloadFactory
/**
* Build the payload for Microsoft Teams Activity Feed.
*
* Informational notification that opens the Teams app side pane
* and marks the activity item as read.
* The activity opens the personal Teams tab. The tab loads the static
* landing page, which receives the exact Flarum discussion/post URL as a
* validated query parameter and presents it as an external link.
*
* @return array<string, mixed>
*/
public function make(User $recipient, BlueprintInterface $blueprint): array
{
// Activity Feed type (informational, but clickable via /l/entity)
$activityType = 'systemDefault';
$forumName = trim((string) $this->settings->get('forum_title', 'sbp forum')) ?: 'sbp forum';
// Resolve dynamic data
$discussionTitle = $this->resolveDiscussionTitle($blueprint) ?? $forumName;
$previewSnippet = $this->buildPreviewText($blueprint);
$reasonText = $this->buildReasonTextEn($blueprint);
// Teams App + Personal Tab (THIS is what makes it clickable & read)
$teamsAppId = 'd92b1be6-ea92-46b1-bcb0-c08ffdd1a61a';
$teamsEntityId = '8ffe419f-ca44-4627-a4b2-f223da2d07aa';
$landingPageUrl = $this->buildLandingPageUrl($blueprint);
$teamsTabLink = $this->buildTeamsTabLink($landingPageUrl);
// Opens the app side pane and marks the activity as read
$teamsTabLink = sprintf(
'https://teams.microsoft.com/l/entity/%s/%s?webUrl=%s',
$teamsAppId,
$teamsEntityId,
rawurlencode('https://forum.sbp.de/static/flarum-notify.html?v=2')
);
return [
'topic' => [
'source' => 'text',
'value' => $discussionTitle,
'webUrl' => $teamsTabLink,
],
'activityType' => $activityType,
'activityType' => 'systemDefault',
'previewText' => [
'content' => mb_substr($previewSnippet, 0, 150),
],
@ -66,6 +59,87 @@ class NotificationPayloadFactory
];
}
/**
* Build the URL of the static Teams landing page.
*
* The exact Flarum target is passed as an absolute URL. The landing page
* validates that it belongs to the same origin before exposing the link.
*/
protected function buildLandingPageUrl(BlueprintInterface $blueprint): string
{
$baseUrl = $this->resolveForumBaseUrl();
if ($baseUrl === null) {
return 'https://forum.sbp.de' . self::LANDING_PAGE_PATH;
}
$landingPageUrl = $baseUrl . self::LANDING_PAGE_PATH;
$subjectUrl = $this->resolveSubjectUrl($blueprint);
if ($subjectUrl === null) {
return $landingPageUrl;
}
return $landingPageUrl . '?' . http_build_query(
['target' => $subjectUrl],
'',
'&',
PHP_QUERY_RFC3986
);
}
/**
* Build a Teams deep link to the personal app tab.
*/
protected function buildTeamsTabLink(string $webUrl): string
{
$teamsAppId = trim((string) $this->settings->get(
'sbp-jm-msteams-webhook.teams_app_id',
self::DEFAULT_TEAMS_APP_ID
));
if ($teamsAppId === '') {
$teamsAppId = self::DEFAULT_TEAMS_APP_ID;
}
return sprintf(
'https://teams.microsoft.com/l/entity/%s/%s?webUrl=%s',
rawurlencode($teamsAppId),
rawurlencode(self::TEAMS_ENTITY_ID),
rawurlencode($webUrl)
);
}
/**
* Resolve and validate the configured public forum base URL.
*/
protected function resolveForumBaseUrl(): ?string
{
$baseUrl = rtrim(
trim((string) $this->settings->get(
'sbp-jm-msteams-webhook.forum_base_url',
''
)),
'/'
);
if ($baseUrl === '') {
return null;
}
$parts = parse_url($baseUrl);
if (
!is_array($parts)
|| ($parts['scheme'] ?? null) !== 'https'
|| empty($parts['host'])
) {
return null;
}
return $baseUrl;
}
/**
* Resolve the discussion title from the notification subject.
*/
@ -85,24 +159,28 @@ class NotificationPayloadFactory
}
/**
* Resolve the URL to the discussion or specific post.
* (Not used for navigation in Teams, but kept for completeness.)
* Resolve the URL to the discussion or exact post.
*/
protected function resolveSubjectUrl(BlueprintInterface $blueprint): ?string
{
$baseUrl = rtrim(
(string) $this->settings->get('sbp-jm-msteams-webhook.forum_base_url', ''),
'/'
);
$baseUrl = $this->resolveForumBaseUrl();
if ($baseUrl === '') {
if ($baseUrl === null) {
return null;
}
$subject = $blueprint->getSubject();
if ($subject instanceof Discussion) {
return sprintf('%s/d/%s', $baseUrl, $subject->id);
if ($subject->id === null) {
return $baseUrl;
}
return sprintf(
'%s/d/%s',
$baseUrl,
rawurlencode((string) $subject->id)
);
}
if ($subject instanceof Post) {
@ -110,7 +188,20 @@ class NotificationPayloadFactory
$postNumber = $subject->number;
if ($discussionId !== null && $postNumber !== null) {
return sprintf('%s/d/%s/%s', $baseUrl, $discussionId, $postNumber);
return sprintf(
'%s/d/%s/%s',
$baseUrl,
rawurlencode((string) $discussionId),
rawurlencode((string) $postNumber)
);
}
if ($discussionId !== null) {
return sprintf(
'%s/d/%s',
$baseUrl,
rawurlencode((string) $discussionId)
);
}
}