Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrated code lifecycle: Add email notifications for SSH key creation and expiry #9918

Open
wants to merge 23 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ac6e176
add mail notification
SimonEntholzer Nov 30, 2024
50506b7
add scheduled expiry warnings
SimonEntholzer Nov 30, 2024
9f935e6
Merge branch 'develop' into feature/ssh/email-notifications-2
SimonEntholzer Dec 3, 2024
bf99c21
let keys expire on 3:00
SimonEntholzer Dec 3, 2024
4bda922
execute every minute (todo remove this again)
SimonEntholzer Dec 3, 2024
1189383
set cron task to correct time (7am) again
SimonEntholzer Dec 3, 2024
62c454f
added integration tests
SimonEntholzer Dec 3, 2024
f6ed392
Merge branch 'develop' into feature/ssh/email-notifications-2
SimonEntholzer Dec 3, 2024
2232df3
move asserts from teardown into check method
SimonEntholzer Dec 3, 2024
2a72c76
Merge branch 'develop' into feature/ssh/email-notifications-2
SimonEntholzer Dec 4, 2024
27c5e24
Merge branch 'develop' into feature/ssh/email-notifications-2
SimonEntholzer Dec 6, 2024
cf1d060
remove notification placeholder
SimonEntholzer Dec 7, 2024
16dfe52
Merge branch 'develop' into feature/ssh/email-notifications-2
SimonEntholzer Dec 7, 2024
ea5c48b
Merge branch 'develop' into feature/ssh/email-notifications-2
SimonEntholzer Dec 7, 2024
2377382
added java docs
SimonEntholzer Dec 7, 2024
3e74892
Merge branch 'develop' into feature/ssh/email-notifications-2
SimonEntholzer Dec 8, 2024
da8822c
make mails consistent
SimonEntholzer Dec 10, 2024
3559e98
Merge branch 'develop' into feature/ssh/email-notifications-2
SimonEntholzer Dec 10, 2024
f9893c9
Merge branch 'develop' into feature/ssh/email-notifications-2
SimonEntholzer Dec 10, 2024
66da8a7
Update src/main/resources/i18n/messages_en.properties
SimonEntholzer Dec 10, 2024
5cf594d
Update src/main/resources/i18n/messages_de.properties
SimonEntholzer Dec 10, 2024
e073e2a
Merge branch 'develop' into feature/ssh/email-notifications-2
SimonEntholzer Dec 10, 2024
5f6cc15
Merge branch 'develop' into feature/ssh/email-notifications-2
SimonEntholzer Dec 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ public enum NotificationType {
TUTORIAL_GROUP_MULTIPLE_REGISTRATION_TUTOR, TUTORIAL_GROUP_DEREGISTRATION_TUTOR, TUTORIAL_GROUP_DELETED, TUTORIAL_GROUP_UPDATED, TUTORIAL_GROUP_ASSIGNED,
TUTORIAL_GROUP_UNASSIGNED, CONVERSATION_NEW_MESSAGE, CONVERSATION_NEW_REPLY_MESSAGE, CONVERSATION_USER_MENTIONED, CONVERSATION_CREATE_ONE_TO_ONE_CHAT,
CONVERSATION_CREATE_GROUP_CHAT, CONVERSATION_ADD_USER_GROUP_CHAT, CONVERSATION_ADD_USER_CHANNEL, CONVERSATION_REMOVE_USER_GROUP_CHAT, CONVERSATION_REMOVE_USER_CHANNEL,
CONVERSATION_DELETE_CHANNEL, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_BUILD_RUN_UPDATE
CONVERSATION_DELETE_CHANNEL, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_BUILD_RUN_UPDATE, SSH_KEY_ADDED, SSH_KEY_EXPIRES_SOON,
SSH_KEY_HAS_EXPIRED
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PROGRAMMING_REPOSITORY_LOCKS;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PROGRAMMING_TEST_CASES_CHANGED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.QUIZ_EXERCISE_STARTED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_ADDED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_EXPIRES_SOON;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_HAS_EXPIRED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_ASSIGNED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DELETED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_STUDENT;
Expand Down Expand Up @@ -147,6 +150,12 @@ public class NotificationConstants {

public static final String TUTORIAL_GROUP_UNASSIGNED_TITLE = "artemisApp.singleUserNotification.title.tutorialGroupUnassigned";

public static final String SSH_KEY_ADDED_TITLE = "artemisApp.singleUserNotification.title.sshKeyAdded";

public static final String SSH_KEY_EXPIRES_SOON_TITLE = "artemisApp.singleUserNotification.title.sshKeyExpiresSoon";

public static final String SSH_KEY_HAS_EXPIRED_TITLE = "artemisApp.singleUserNotification.title.sshKeyHasExpired";
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

// Texts
public static final String LIVE_EXAM_EXERCISE_UPDATE_NOTIFICATION_TEXT = "artemisApp.groupNotification.text.liveExamExerciseUpdate";

Expand Down Expand Up @@ -280,6 +289,12 @@ public class NotificationConstants {

public static final String CONVERSATION_DELETE_CHANNEL_TEXT = "artemisApp.singleUserNotification.text.deleteChannel";

public static final String SSH_KEY_ADDED_TEXT = "artemisApp.singleUserNotification.text.sshKeyAdded";

public static final String SSH_KEY_EXPIRES_SOON_TEXT = "artemisApp.singleUserNotification.text.sshKeyExpiresSoon";

public static final String SSH_KEY_HAS_EXPIRED_TEXT = "artemisApp.singleUserNotification.text.sshKeyHasExpired";

// bidirectional map
private static final BiMap<NotificationType, String> NOTIFICATION_TYPE_AND_TITLE_MAP = new ImmutableBiMap.Builder<NotificationType, String>()
.put(EXERCISE_SUBMISSION_ASSESSED, EXERCISE_SUBMISSION_ASSESSED_TITLE).put(ATTACHMENT_CHANGE, ATTACHMENT_CHANGE_TITLE).put(EXERCISE_RELEASED, EXERCISE_RELEASED_TITLE)
Expand All @@ -305,7 +320,8 @@ public class NotificationConstants {
.put(CONVERSATION_ADD_USER_GROUP_CHAT, CONVERSATION_ADD_USER_GROUP_CHAT_TITLE).put(CONVERSATION_REMOVE_USER_GROUP_CHAT, CONVERSATION_REMOVE_USER_GROUP_CHAT_TITLE)
.put(CONVERSATION_REMOVE_USER_CHANNEL, CONVERSATION_REMOVE_USER_CHANNEL_TITLE).put(CONVERSATION_DELETE_CHANNEL, CONVERSATION_DELETE_CHANNEL_TITLE)
.put(DATA_EXPORT_CREATED, DATA_EXPORT_CREATED_TITLE).put(DATA_EXPORT_FAILED, DATA_EXPORT_FAILED_TITLE)
.put(PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_REPOSITORY_LOCKS_TITLE).put(PROGRAMMING_BUILD_RUN_UPDATE, PROGRAMMING_BUILD_RUN_UPDATE_TITLE).build();
.put(PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_REPOSITORY_LOCKS_TITLE).put(PROGRAMMING_BUILD_RUN_UPDATE, PROGRAMMING_BUILD_RUN_UPDATE_TITLE)
.put(SSH_KEY_ADDED, SSH_KEY_ADDED_TITLE).put(SSH_KEY_EXPIRES_SOON, SSH_KEY_EXPIRES_SOON_TITLE).put(SSH_KEY_HAS_EXPIRED, SSH_KEY_HAS_EXPIRED_TITLE).build();

/**
* Finds the corresponding NotificationType for the provided notification title
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.NEW_REPLY_FOR_EXERCISE_POST;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.NEW_REPLY_FOR_LECTURE_POST;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PLAGIARISM_CASE_VERDICT_STUDENT;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_EXPIRES_SOON;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_HAS_EXPIRED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_ASSIGNED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_STUDENT;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_TUTOR;
Expand Down Expand Up @@ -49,6 +51,12 @@
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.NEW_PLAGIARISM_CASE_STUDENT_TITLE;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.PLAGIARISM_CASE_VERDICT_STUDENT_TEXT;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.PLAGIARISM_CASE_VERDICT_STUDENT_TITLE;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_ADDED_TEXT;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_ADDED_TITLE;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_EXPIRES_SOON_TEXT;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_EXPIRES_SOON_TITLE;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_HAS_EXPIRED_TEXT;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_HAS_EXPIRED_TITLE;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_ASSIGNED_TEXT;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_DEREGISTRATION_STUDENT_TEXT;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_DEREGISTRATION_TUTOR_TEXT;
Expand All @@ -67,6 +75,7 @@
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationTargetFactory.createPlagiarismCaseTarget;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationTargetFactory.createTutorialGroupTarget;

import java.time.format.DateTimeFormatter;
import java.util.Set;

import jakarta.validation.constraints.NotNull;
Expand All @@ -80,6 +89,7 @@
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.exercise.domain.Exercise;
import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismCase;
import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey;
import de.tum.cit.aet.artemis.tutorialgroup.domain.TutorialGroup;

public class SingleUserNotificationFactory {
Expand Down Expand Up @@ -158,6 +168,26 @@ public static String[] createPlaceholdersDataExport() {
return new String[] {};
}

public static SingleUserNotification createNotification(UserSshPublicKey key, NotificationType notificationType, User recipient) {
switch (notificationType) {
case SSH_KEY_ADDED -> {
return new SingleUserNotification(recipient, SSH_KEY_ADDED_TITLE, SSH_KEY_ADDED_TEXT, true, new String[] {});
}
case SSH_KEY_EXPIRES_SOON -> {
return new SingleUserNotification(recipient, SSH_KEY_EXPIRES_SOON_TITLE, SSH_KEY_EXPIRES_SOON_TEXT, true, createPlaceholdersSshKeyNotification(key));
}
case SSH_KEY_HAS_EXPIRED -> {
return new SingleUserNotification(recipient, SSH_KEY_HAS_EXPIRED_TITLE, SSH_KEY_HAS_EXPIRED_TEXT, true, createPlaceholdersSshKeyNotification(key));
}
default -> throw new UnsupportedOperationException("Unsupported NotificationType: " + notificationType);
}
}
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

@NotificationPlaceholderCreator(values = { SSH_KEY_EXPIRES_SOON, SSH_KEY_HAS_EXPIRED })
public static String[] createPlaceholdersSshKeyNotification(UserSshPublicKey key) {
return new String[] { key.getLabel(), key.getExpiryDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")) };
}
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

/**
* Creates an instance of SingleUserNotification based on plagiarisms.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.net.URL;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -33,6 +34,7 @@
import de.tum.cit.aet.artemis.exercise.domain.Exercise;
import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation;
import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismCase;
import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey;

/**
* Service for preparing and sending emails.
Expand Down Expand Up @@ -86,6 +88,10 @@ public class MailService implements InstantNotificationService {

private static final String NOTIFICATION_TYPE = "notificationType";

private static final String SSH_KEY = "sshKey";

private static final String SSH_KEY_EXPIRY_DATE = "expiryDate";

// time related variables
private static final String TIME_SERVICE = "timeService";

Expand Down Expand Up @@ -263,6 +269,12 @@ public void sendNotification(Notification notification, User user, Object notifi
if (notificationSubject instanceof PlagiarismCase plagiarismCase) {
subject = setPlagiarismContextAndSubject(context, notificationType, notification, plagiarismCase);
}
if (notificationSubject instanceof UserSshPublicKey userSshPublicKey) {
context.setVariable(SSH_KEY, userSshPublicKey);
if (userSshPublicKey.getExpiryDate() != null) {
context.setVariable(SSH_KEY_EXPIRY_DATE, userSshPublicKey.getExpiryDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")));
}
}

if (notificationSubject instanceof SingleUserNotificationService.TutorialGroupNotificationSubject tutorialGroupNotificationSubject) {
setContextForTutorialGroupNotifications(context, notificationType, tutorialGroupNotificationSubject);
Expand Down Expand Up @@ -393,6 +405,10 @@ private String createContentForNotificationEmailByType(NotificationType notifica
case TUTORIAL_GROUP_UPDATED -> templateEngine.process("mail/notification/tutorialGroupUpdatedEmail", context);
case DATA_EXPORT_CREATED -> templateEngine.process("mail/notification/dataExportCreatedEmail", context);
case DATA_EXPORT_FAILED -> templateEngine.process("mail/notification/dataExportFailedEmail", context);
case SSH_KEY_ADDED -> templateEngine.process("mail/notification/sshKeyAddedEmail", context);
case SSH_KEY_EXPIRES_SOON -> templateEngine.process("mail/notification/sshKeyExpiresSoonEmail", context);
case SSH_KEY_HAS_EXPIRED -> templateEngine.process("mail/notification/sshKeyHasExpiredEmail", context);

default -> throw new UnsupportedOperationException("Unsupported NotificationType: " + notificationType);
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PLAGIARISM_CASE_VERDICT_STUDENT;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PROGRAMMING_TEST_CASES_CHANGED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.QUIZ_EXERCISE_STARTED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_ADDED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_EXPIRES_SOON;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_HAS_EXPIRED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_ASSIGNED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DELETED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_STUDENT;
Expand Down Expand Up @@ -134,6 +137,13 @@ public class NotificationSettingsService {

public static final String NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_FAILED = "notification.user-notification.data-export-failed";

// ssh user notification settings group
public static final String NOTIFICATION_USER_NOTIFICATION_SSH_KEY_ADDED = "notification.user-notification.ssh-key-added";

public static final String NOTIFICATION_USER_NOTIFICATION_SSH_KEY_EXPIRES_SOON = "notification.user-notification.ssh-key-expires-soon";

public static final String NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED = "notification.user-notification.ssh-key-has-expired";

// if webapp or email is not explicitly set for a specific setting -> no support for this communication channel for this setting
// this has to match the properties in the notification settings structure file on the client that hides the related UI elements
public static final Set<NotificationSetting> DEFAULT_NOTIFICATION_SETTINGS = new HashSet<>(Arrays.asList(
Expand Down Expand Up @@ -173,9 +183,12 @@ public class NotificationSettingsService {
new NotificationSetting(true, false, true, NOTIFICATION__USER_NOTIFICATION__NEW_REPLY_IN_CONVERSATION_MESSAGE),
// user mention notification setting group
new NotificationSetting(true, false, true, NOTIFICATION__USER_NOTIFICATION__USER_MENTION),
// data export notification setting (cannot be overridden by user)
// data export and SSH notification setting (cannot be overridden by user)
new NotificationSetting(true, true, true, NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_FAILED),
new NotificationSetting(true, true, true, NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_CREATED)));
new NotificationSetting(true, true, true, NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_CREATED),
new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_ADDED),
new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_EXPIRES_SOON),
new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED)));

/**
* This is the place where the mapping between SettingId and NotificationTypes happens on the server side
Expand Down Expand Up @@ -209,7 +222,10 @@ public class NotificationSettingsService {
new NotificationType[] { CONVERSATION_NEW_MESSAGE, CONVERSATION_CREATE_ONE_TO_ONE_CHAT, CONVERSATION_CREATE_GROUP_CHAT, CONVERSATION_ADD_USER_GROUP_CHAT,
CONVERSATION_ADD_USER_CHANNEL, CONVERSATION_REMOVE_USER_GROUP_CHAT, CONVERSATION_REMOVE_USER_CHANNEL }),
Map.entry(NOTIFICATION__USER_NOTIFICATION__NEW_REPLY_IN_CONVERSATION_MESSAGE, new NotificationType[] { CONVERSATION_NEW_REPLY_MESSAGE }),
Map.entry(NOTIFICATION__USER_NOTIFICATION__USER_MENTION, new NotificationType[] { CONVERSATION_USER_MENTIONED }));
Map.entry(NOTIFICATION__USER_NOTIFICATION__USER_MENTION, new NotificationType[] { CONVERSATION_USER_MENTIONED }),
Map.entry(NOTIFICATION_USER_NOTIFICATION_SSH_KEY_ADDED, new NotificationType[] { SSH_KEY_ADDED }),
Map.entry(NOTIFICATION_USER_NOTIFICATION_SSH_KEY_EXPIRES_SOON, new NotificationType[] { SSH_KEY_EXPIRES_SOON }),
Map.entry(NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED, new NotificationType[] { SSH_KEY_HAS_EXPIRED }));

// This set has to equal the UI configuration in the client notification settings structure file!
// More information on supported notification types can be found here: https://docs.artemis.cit.tum.de/user/notifications/
Expand All @@ -219,7 +235,8 @@ public class NotificationSettingsService {
PLAGIARISM_CASE_VERDICT_STUDENT, TUTORIAL_GROUP_REGISTRATION_STUDENT, TUTORIAL_GROUP_REGISTRATION_TUTOR, TUTORIAL_GROUP_MULTIPLE_REGISTRATION_TUTOR,
TUTORIAL_GROUP_DEREGISTRATION_STUDENT, TUTORIAL_GROUP_DEREGISTRATION_TUTOR, TUTORIAL_GROUP_DELETED, TUTORIAL_GROUP_UPDATED, TUTORIAL_GROUP_ASSIGNED,
TUTORIAL_GROUP_UNASSIGNED, NEW_EXERCISE_POST, NEW_LECTURE_POST, NEW_REPLY_FOR_LECTURE_POST, NEW_COURSE_POST, NEW_REPLY_FOR_COURSE_POST, NEW_REPLY_FOR_EXERCISE_POST,
QUIZ_EXERCISE_STARTED, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, CONVERSATION_NEW_MESSAGE, CONVERSATION_NEW_REPLY_MESSAGE);
QUIZ_EXERCISE_STARTED, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, CONVERSATION_NEW_MESSAGE, CONVERSATION_NEW_REPLY_MESSAGE, SSH_KEY_ADDED, SSH_KEY_EXPIRES_SOON,
SSH_KEY_HAS_EXPIRED);

// More information on supported notification types can be found here: https://docs.artemis.cit.tum.de/user/notifications/
// Please adapt the above docs if you change the supported notification types
Expand Down
Loading
Loading