notifiers

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 12, 2026 License: AGPL-3.0 Imports: 28 Imported by: 0

Documentation

Overview

Package notifiers runs the notify-only channels (email, ntfy, webhooks, Web Push): it owns their instance settings and hands the channels to the action layer.

Settings come from the environment (EXHALE_SMTP_*, EXHALE_NTFY_*, EXHALE_ALLOW_PRIVATE_DESTINATIONS) or, more usually, from the web, where they are saved in the settings table like the Telegram token. The environment wins. Every message reads the current settings, so a change applies at once, without a restart. Secrets go in and never come back out: Status only says whether one is saved.

Index

Constants

View Source
const (
	SourceEnv      = "env"
	SourceSettings = "settings"
)

Sources of a setting.

View Source
const MaxDevices = 10

MaxDevices is how many browsers one member can turn push on in.

Variables

View Source
var ErrActionExpired = webpush.ErrBadToken

ErrActionExpired is returned for a notification button whose token is not valid any more; the web opens the charge instead.

Functions

This section is empty.

Types

type ChannelsStatus

type ChannelsStatus struct {
	PublicURL          string     `json:"publicUrl,omitempty" doc:"address used for links in messages; empty when the server was started without --public-url"`
	AllowPrivate       bool       `json:"allowPrivate" doc:"webhooks and ntfy may reach private-network addresses"`
	AllowPrivateSource string     `json:"allowPrivateSource,omitempty" enum:"env,settings"`
	SMTP               SMTPStatus `json:"smtp"`
	Ntfy               NtfyStatus `json:"ntfy"`
}

ChannelsStatus is what the web shows about the notify-only channels.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager configures the channels and sends through them.

func New

func New(st *store.Store, opts Options, log *slog.Logger) (*Manager, error)

New checks the options and returns a manager.

func (*Manager) AddDevice

func (m *Manager) AddDevice(ctx context.Context, memberID string, in PushDevice) (string, error)

AddDevice saves a browser's subscription for a member and returns its link id. The same browser signed in as someone else moves to them.

func (*Manager) Channels

func (m *Manager) Channels() []actions.Channel

Channels returns the email, ntfy, webhook and Web Push channels for the action layer.

func (*Manager) Describe

func (m *Manager) Describe(ctx context.Context, channel, destination string) (text string, hasTemplate bool)

Describe returns how a destination is shown to its member: the address, the topic, or only the host of a webhook (the rest of its URL is a secret), and whether it has a body template.

func (*Manager) Forget

func (m *Manager) Forget(ctx context.Context, channel, destination string) error

Forget clears what is kept about a destination besides its link.

func (*Manager) Notifies

func (m *Manager) Notifies(channel string) bool

Notifies reports whether channel is one of the notify-only channels.

func (*Manager) PushAction

func (m *Manager) PushAction(ctx context.Context, token string) (actions.PushResult, error)

PushAction runs the button of a notification from its token. The device it was sent to must still be the member's.

func (*Manager) PushStatus

func (m *Manager) PushStatus(ctx context.Context) (PushStatus, error)

PushStatus returns the public key, making the keys if needed.

func (*Manager) RemoveDevice

func (m *Manager) RemoveDevice(ctx context.Context, memberID, endpoint string) error

RemoveDevice forgets a browser by its endpoint, if it is the member's (turning push off in that browser).

func (*Manager) RemoveNtfy

func (m *Manager) RemoveNtfy(ctx context.Context) error

RemoveNtfy goes back to the default public server without a token.

func (*Manager) RemoveSMTP

func (m *Manager) RemoveSMTP(ctx context.Context) error

RemoveSMTP forgets the email settings saved from the web.

func (*Manager) SetAllowPrivate

func (m *Manager) SetAllowPrivate(ctx context.Context, allow bool) error

SetAllowPrivate lets webhooks and ntfy reach private-network addresses (a server on the LAN), or stops them. Loopback, link-local and cloud metadata addresses stay out of reach either way.

func (*Manager) SetNtfy

func (m *Manager) SetNtfy(ctx context.Context, in NtfyInput) error

SetNtfy saves the ntfy server after checking its address against the network rules.

func (*Manager) SetPushActions

func (m *Manager) SetPushActions(a PushActions)

SetPushActions wires the action layer, which is built after the manager.

func (*Manager) SetSMTP

func (m *Manager) SetSMTP(ctx context.Context, in SMTPInput) error

SetSMTP checks the settings by connecting and signing in, then saves them.

func (*Manager) SetTemplate

func (m *Manager) SetTemplate(ctx context.Context, url, template string) error

SetTemplate saves the body template of a webhook ("" for the default body). Call it once the destination is linked to the member.

func (*Manager) Status

func (m *Manager) Status(ctx context.Context, owner bool) ChannelsStatus

Status reports the setup. Members who are not owners see whether email works and which ntfy server to use, not how the mail server is reached.

func (*Manager) Test

func (m *Manager) Test(ctx context.Context, channel, destination string) error

Test sends a sample reminder to a destination now and reports what went wrong, if anything.

func (*Manager) Validate

func (m *Manager) Validate(ctx context.Context, channel, destination, template string) (string, error)

Validate checks a destination a member typed (an email address, an ntfy topic, a webhook URL with an optional body template) and returns it in the form it is saved in.

type NtfyInput

type NtfyInput struct {
	Server      string `json:"server,omitempty" doc:"base address of the ntfy server; default https://ntfy.sh"`
	Token       string `json:"token,omitempty" doc:"access token (tk_…), for servers that need one to publish; leave empty to keep the saved one"`
	RemoveToken bool   `json:"removeToken,omitempty" doc:"forget the saved access token"`
}

NtfyInput is the ntfy form. An empty token keeps the saved one unless RemoveToken is set or the server changes.

type NtfyStatus

type NtfyStatus struct {
	Source   string `json:"source,omitempty" enum:"env,settings" doc:"empty: the default public server"`
	Server   string `json:"server" doc:"where members subscribe to their topic"`
	HasToken bool   `json:"hasToken" doc:"an access token is saved; it is never returned"`
}

NtfyStatus describes the ntfy setup without its access token.

type Options

type Options struct {
	// PublicURL is the address people open Exhale at (--public-url). Links
	// in messages need it; without it they are left out.
	PublicURL string
	// SMTP comes from EXHALE_SMTP_*; it applies when Host is set.
	SMTP email.Config
	// NtfyServer and NtfyToken come from EXHALE_NTFY_URL and _TOKEN.
	NtfyServer, NtfyToken string
	// AllowPrivate comes from EXHALE_ALLOW_PRIVATE_DESTINATIONS; nil when
	// it is not set.
	AllowPrivate *bool
	// Version goes in the User-Agent of webhooks.
	Version string

	// Guard replaces the network rules' resolver and dialer (tests).
	Guard *netguard.Guard
	// SMTPRoots replaces the system certificates for SMTP (tests).
	SMTPRoots *x509.CertPool
	// PushRoots replaces the system certificates for push services (tests).
	PushRoots *x509.CertPool
}

Options is what the server was started with.

type PushActions

type PushActions interface {
	HandlePush(ctx context.Context, memberID, device, verb, chargeID string) (actions.PushResult, error)
}

PushActions runs a notification button (the action layer implements it).

type PushDevice

type PushDevice struct {
	Endpoint string `json:"endpoint" doc:"the push service address from PushSubscription.endpoint"`
	P256dh   string `json:"p256dh" doc:"keys.p256dh, base64url"`
	Auth     string `json:"auth" doc:"keys.auth, base64url"`
	Device   string `json:"device,omitempty" doc:"how to show it, e.g. Chrome on Android"`
}

PushDevice is a browser's PushSubscription (its toJSON()) and a name to show for it.

type PushStatus

type PushStatus struct {
	PublicKey string `json:"publicKey" doc:"VAPID public key, base64url: the applicationServerKey to subscribe with"`
}

PushStatus is what the web needs to turn push on.

type SMTPInput

type SMTPInput struct {
	Host     string `json:"host" doc:"SMTP server, e.g. smtp.example.com"`
	Port     int    `json:"port,omitempty" doc:"default: 587 with starttls, 465 with tls, 25 with none"`
	Username string `json:"username,omitempty"`
	Password string `json:"password,omitempty" doc:"leave empty to keep the saved password"`
	From     string `json:"from" doc:"sender address, e.g. reminders@example.com"`
	TLS      string `json:"tls,omitempty" enum:"starttls,tls,none" doc:"starttls (default), tls (implicit, port 465) or none (local relays)"`
}

SMTPInput is the email form. An empty password keeps the saved one.

type SMTPStatus

type SMTPStatus struct {
	Configured  bool   `json:"configured"`
	Source      string `json:"source,omitempty" enum:"env,settings" doc:"env: set by EXHALE_SMTP_* on the server and not editable in the web"`
	Host        string `json:"host,omitempty"`
	Port        int    `json:"port,omitempty"`
	Username    string `json:"username,omitempty"`
	From        string `json:"from,omitempty"`
	TLS         string `json:"tls,omitempty" enum:"starttls,tls,none"`
	HasPassword bool   `json:"hasPassword" doc:"a password is saved; it is never returned"`
}

SMTPStatus describes the email setup without its password.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL