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

feat: Add notification support #1381

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft

feat: Add notification support #1381

wants to merge 19 commits into from

Conversation

jorenn92
Copy link
Owner

This PR introduces notification support to Maintainerr. You can now add notification providers via the settings. When creating a rule, you’ll be able to configure which notification provider to use for that rule.

Please note: I’ve only tested this with the Discord and Pushover notification providers. The others have not yet been tested.

@jorenn92 jorenn92 requested a review from benscobie November 18, 2024 15:04
@jorenn92
Copy link
Owner Author

/release-pr

Copy link
Contributor

‼️ There was an error trying to release the PR.

@jorenn92
Copy link
Owner Author

/release-pr

Copy link
Contributor

Released to jorenn92/maintainerr:pr-1381 🚀

@jorenn92
Copy link
Owner Author

The "About to be Handled" notification type is still a work in progress. I’m still getting the cron job fully set up. Once it’s ready, I’ll update this PR.

@jorenn92
Copy link
Owner Author

/release-pr

Copy link
Contributor

Released to jorenn92/maintainerr:pr-1381 🚀

@jorenn92
Copy link
Owner Author

jorenn92 commented Nov 19, 2024

Todo:

  • Remove the 'none' notification type
  • Add the cron timer of the 'Notification timer'-task to the settings (maybe)
  • Configurable messages (Later in a follow up PR)
  • Testing and probably fixing untested notification agents

@benscobie
Copy link
Collaborator

I'll try and get to reviewing this sometime this week.

@jorenn92
Copy link
Owner Author

TODO: There might be a logic issue in how notifications are being sent. In certain parts of the code, I call a method to send a notification to all agents active for a specific notification type. However, it might be possible that the notification is also being sent out for rules that don't have that notification agent configured.

This is not tested though, it's just an afterthought I just had.

@jorenn92
Copy link
Owner Author

/release-pr

Copy link
Contributor

Released to jorenn92/maintainerr:pr-1381 🚀

Copy link
Collaborator

@benscobie benscobie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just had a quick pass through of the code, nice work! I'll try and make some time to test a few of the agents soon as I use a few of them myself.

Most of the comments are around missing awaits. I believe this might have been on purpose, so that you can fire off notifications without slowing down the workers? You also don't really want to explode a worker if the notification code fails for whatever reason as it's not that important. If that's the case I'd suggest using a queue: https://docs.nestjs.com/techniques/queues. Or attaching . catch() to them so unhandledrejections aren't thrown.

handleNotification could simply become await this.notificationQueue.add(), and the existing handling code moved to a different function to be called by a consumer? Though the agent is being passed in a few places so might need a little refactor, or you don't queue those? You could also have another queue that takes a constructed message and the agent to send to, that would mean you can retry individual notifications if the service is having an issue.

  1. Add item to queue with the required data (type, mediaItems, collectionName, dayAmount)
  2. Consumer takes from that queue, runs transformMessageContent and adds to another queue, one queue message per agent.
  3. Consumer takes from that queue and calls agent.send().

EDIT:
I've just realized this will require redis which might be a pain in the ass for some users (seedbox etc). We could do something similar with an in-process task runner though, or possibly events: https://docs.nestjs.com/techniques/events. I need to experiment with nestjs error handling, from what I've read they've got a global handler for exceptions but I've seen it crash from unhandledrejections.


// wait 5 seconds to make sure we're not executing together with the rule handler
await new Promise((resolve) => setTimeout(resolve, 5000));
this.notificationService.registerConfiguredAgents(true); // re-register notification agents, to avoid flukes
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.notificationService.registerConfiguredAgents(true); // re-register notification agents, to avoid flukes
await this.notificationService.registerConfiguredAgents(true); // re-register notification agents, to avoid flukes

for (const media of collectionMedia) {
// handle media addate <= due date
if (new Date(media.addDate) <= dangerDate) {
await this.handleMedia(collection, media);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I brought up in Discord here about a possible handleMedia improvement. We may want to notify on individual media handling failures at some point once we've figured out what to do there.

}, 7000);
// handle notification
if (handledMediaForNotification.length > 0) {
this.notificationService.handleNotification(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.notificationService.handleNotification(
await this.notificationService.handleNotification(

);

// notify
this.notificationService.handleNotification(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.notificationService.handleNotification(
await this.notificationService.handleNotification(

this.logger.debug(e);

// notify
this.notificationService.handleNotification(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.notificationService.handleNotification(
await this.notificationService.handleNotification(

@@ -118,6 +122,10 @@ export class RuleExecutorService extends TaskBase {
this.logger.log(
'Not all applications are reachable.. Skipped rule execution.',
);
this.notificationService.handleNotification(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.notificationService.handleNotification(
await this.notificationService.handleNotification(

@@ -264,6 +272,12 @@ export class RuleExecutorService extends TaskBase {
: collection.title
}'.`,
);

this.notificationService.handleNotification(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.notificationService.handleNotification(
await this.notificationService.handleNotification(

@@ -274,6 +288,13 @@ export class RuleExecutorService extends TaskBase {
: collection.title
}'.`,
);

this.notificationService.handleNotification(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.notificationService.handleNotification(
await this.notificationService.handleNotification(

@@ -295,9 +316,17 @@ export class RuleExecutorService extends TaskBase {
return collection;
} else {
this.logInfo(`collection not found with id ${rulegroup.collectionId}`);
this.notificationService.handleNotification(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.notificationService.handleNotification(
await this.notificationService.handleNotification(

}
} catch (err) {
this.logger.warn(`Execption occurred whild handling rule: `, err);
this.logger.error(`Execption occurred while handling rule: `, err);
this.notificationService.handleNotification(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.notificationService.handleNotification(
await this.notificationService.handleNotification(


<div className="form-row">
<label htmlFor="about-scale" className="text-label">
Days for About To Be Handled
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a bit confused at first when I saw this as I hadn't read the types. Perhaps move this after Types and only make it appear when selecting Media About To Be Handled?

Copy link
Owner Author

@jorenn92 jorenn92 Dec 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spent too much time considering how to name this and eventually settled on this option, though I wasn't entirely satisfied with it.

You're right that showing this only when the type is selected is a better option. I'll try to disable the option when that type is unchecked.

}}
className="rounded-l-only"
>
{availableAgents?.map((agent, index) => (
Copy link
Collaborator

@benscobie benscobie Dec 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the 'none' option need filtering out here, or just be made into an empty string? Having a mapping from the enum value to a friendly type would be nice too. discord -> Discord etc.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was planning to remove the 'none' option entirely since it's not being used anywhere.

Adding a friendly name mapping sounds like a great idea.


class DiscordAgent implements NotificationAgent {
constructor(
private readonly settings: NotificationAgentConfig,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private readonly settings: NotificationAgentConfig,
private readonly settings: NotificationAgentDiscord,

You can use the specific config types in all of the agents to get type checking on the options.

</div>
<div className="m-auto mb-5 flex">
<div className="ml-2 mr-auto sm:mr-0">
<ExecuteButton
Copy link
Collaborator

@benscobie benscobie Dec 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first time around I found the test notification thing confusing. I added a notification, smashed Test Notifications a bunch of times and went on a entrypoint spree because nothing was happening. Only when I went back to the modal did I see the 'Test Notification' type and then it clicked in my head. Also the flow of edit notification -> save -> close -> test -> repeat seems awkward.

I think a friendlier version would be having the Test button within the notification modal that just uses the Test Notification type in the background. Looking at the code it might just be a case of passing the non-saved settings up, create a new agent (maybe can extract the factory logic from registerConfiguredAgents) and calling sendNotificationToAgent?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also no feedback after testing currently, and for Gotify (what I've tested so far) the error & debug don't really help much (I'll add a separate comment for that).

Exceptions in the agents send() could bubble up (or throw a new one) with the actual exception message, that could be returned to the user when they hit the /test endpoint so they don't have to dig through logs.

Copy link
Owner Author

@jorenn92 jorenn92 Dec 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I hadn't really considered that. Looking at it now, it does seem odd. I took the easiest route at the time.
Moving the test button to the creation modal seems like the best approach, and we could also remove the 'test' type in that case.

As for the error, I'll try to return the exception, or at least a part of it to the user.

const settings = this.getSettings();

if (
!payload.notifySystem ||
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's notifySystem used for? From what I can see it's always set to false so this and a few other agents will always return early.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a leftover i still need to remove. I copied the notification agents from Overseerr and adapted them to something Nestjs understands. I believe they use it for notifications regarding the Overseerr application itself (like errors).

return true;
} catch (e) {
this.logger.error('Error sending Gotify notification');
this.logger.debug(e);
Copy link
Collaborator

@benscobie benscobie Dec 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL could be included here, e.g.:

      const url = axios.getUri({ url: endpoint });
      this.logger.debug(`GET request to ${url} failed: ${e}`);


<p className="text-gray-300 space-x-2 mb-4 truncate">
<span className="font-semibold">Agent</span>
<a href={config.agent} className="hover:underline">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having a link here was a leftover from where it was copied.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, I mindlessly borrowed it from your multi-arr page. I should have caught that.

@@ -383,6 +386,7 @@ export class RulesService {
params.isActive !== undefined ? params.isActive : true,
params.dataType !== undefined ? params.dataType : undefined,
group.id,
params.notifications,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also needs adding in setRules when calling createOrUpdateGroup, I can't save new collections at the moment due to the error below, but it also means it won't save any notifications I've selected as well.

The notifications param on createOrUpdateGroup is optional but .map() is called on it later. This is throwing causing groupId to be null in setRules.


<div>
{/* Load fields */}
{targetAgent?.options.map((option) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should have a different input for the Json Payload option. The monaco-editor would be perfect here.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, good suggestion!

notifySystem: boolean;
image?: string;
message?: string;
extra?: { name: string; value: string }[];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a leftover from overseerr as I can't see it being used.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, also something that still needs cleaning up

export interface NotificationPayload {
event?: string;
subject: string;
notifySystem: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with this and the leftover conditions

NotificationType,
} from '../notifications-interfaces';

export interface NotificationPayload {
Copy link
Collaborator

@benscobie benscobie Dec 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to include some metadata in this payload where relevant, such as the collection name, and/or the media item name. The webhook agent only has access to the subject and message which isn't super useful.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's worth an exploration, i'll look into it.

const settings = this.getSettings();

if (
!payload.notifySystem ||
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
!payload.notifySystem ||

return true;
} catch (e) {
this.logger.error('Error sending webhook notification');
this.logger.debug(e);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as comment before around including the URL in the message.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants