Adding new service¶
A service is any class that:
- Implements
INotifier
- Have a public constructor that takes a
ServiceConfig
- Have a
NapriseNotificationService
attribute
Services can then be added to the ServiceRegistry
var registry = new ServiceRegistry().AddDefaultServices();
registry.Add<MyService>();
// or
registry.Add(typeof(MyService));
In addition, services provided by Naprise should:
- Be sealed
- Inherit from
NotificationService
- Have a
NotificationServiceWebsite
attribute - Have a
NotificationServiceApiDoc
attribute
If a service requires another library, consider creating a new project for it so that the dependency is not required for users don't use the service.
Designing the service URL¶
The service URL is the way to configure a service.
Here are some guidelines for designing the URL:
- IMPORTANT: Host MUST NOT be used for passing tokens or api keys, as it is case insensitive, all uppercase characters will be converted to lowercase.
- Prefer using the service's full name as the URL scheme. For example, use
discord
instead ofdc
. - For self-hostable services, add
s
to the scheme for requests over https/tls. For example, useapprise
for calling API over http andapprises
for calling API over https. - Prefer using host and path for required arguments.
- Prefer using query parameters for optional arguments.
Implementing the service¶
Steps to implement a service:
- Create a new file in
src/Naprise/Service
. - Copy the content of
src/Naprise/Service/Template.cs
to the new file. - Rename the class name to the service name.
- Fill in all the attributes.
- Parse the URL in the constructor.
- Implement the
SendAsync
method.
Some considerations when implementing the constructor:
- Use
Flurl.Url
instead ofSystem.Uri
for parsing and building URLs. - Throw
NapriseInvalidUrlException
if the URL is invalid, e.g. missing required arguments, or contains invalid arguments. - Check the token format if applicable, but do not send network requests in the constructor.
- Store the parsed arguments in readonly fields.
Some considerations when implementing the SendAsync method:
- Use
Flurl.Url
for parsing and building URLs. - If the service supports setting color, convert the message type to a color using
this.Asset.GetColor(type)
. - If the service does not support setting color, prepend the message with the string returned by
this.Asset.GetAscii(type)
. - If the service only support one message format (e.g. markdown), convert the message to the supported format using
message.Prefer<Format>Body()
. - If the service supports multiple message formats, it's up to you to decide which format to use.
- Check the response and throw
NapriseNotifyFailedException
if the request failed.
Adding tests¶
If you're adding a new service for Naprise:
Please also add tests for the new service in src/Naprise.Tests/Service/
. Please add test cases for all valid URLs.
You can optionally add tests for invalid URLs and tests for sending messages, see DiscordTests.cs
for an example.
Adding documentation¶
Please add documentation for the new service in docs/docs/services/
.
Add link to the new page in nav
section of docs/mkdocs.yml
.
Generate README.md and the json file for documentation website by running
Template for implementing new service¶
- Check GitHub for latest version of this template.
- If you are not adding the service to Naprise, the namespace should also be changed.
using Flurl;
using System;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Naprise.Service
{
[NapriseNotificationService("Template", "", SupportText = true, SupportMarkdown = true, SupportHtml = false)] // TODO fill in blank
[NotificationServiceWebsite("")] // TODO fill in blank
[NotificationServiceApiDoc("")] // TODO fill in blank
// TODO change visibility and class name
internal sealed class Template : NotificationService
{
// TODO change class name
public Template(ServiceConfig config) : base(config: config, bypassChecks: false)
{
var url = config.Url;
var segment = url.PathSegments;
var query = url.QueryParams;
// fill in all the blanks and change visibility to public
throw new InvalidProgramException("this is a template for adding new notification services");
}
public override async Task NotifyAsync(Message message, CancellationToken cancellationToken = default)
{
message.ThrowIfEmpty();
// TODO build the message body
var payload = new Payload
{
// TODO fill payload
// TODO check message.Type
};
var url = new Url($"{(true ? "https" : "http")}://{"localhost"}").AppendPathSegments("example");
var content = JsonContent.Create(payload, options: SharedJsonOptions.SnakeCaseNamingOptions);
cancellationToken.ThrowIfCancellationRequested();
var resp = await this.HttpClientFactory().PostAsync(url, content, cancellationToken);
var respText = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
{
throw new NapriseNotifyFailedException($"Failed to send notification to {nameof(Template)}: {resp.StatusCode}") // TODO change class name
{
Notifier = this,
Notification = message,
ResponseStatus = resp.StatusCode,
ResponseBody = respText,
};
}
try
{
var jobj = JsonDocument.Parse(respText);
// TODO parse response and check if it's successful
var status = jobj.RootElement.GetProperty("status").GetString();
if (status != "ok")
{
var respMessage = jobj.RootElement.GetProperty("message").GetString();
throw new NapriseNotifyFailedException($"Failed to send notification to {nameof(Template)}: \"{respMessage}\"")
{
Notifier = this,
Notification = message,
ResponseStatus = resp.StatusCode,
ResponseBody = respText,
};
}
}
catch (Exception ex)
{
throw new NapriseNotifyFailedException($"Failed to send notification to {nameof(Template)}", ex)
{
Notifier = this,
Notification = message,
ResponseStatus = resp.StatusCode,
ResponseBody = respText,
};
}
}
private class Payload
{
// TODO add payload
}
}
}