notify

package
v1.8.0 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: GPL-3.0 Imports: 18 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultLang = "en"

DefaultLang is what an unset or unknown notifyLang falls back to.

Variables

AllKinds is the order the settings page lists the toggles in.

View Source
var Langs = []string{"en", "fa", "ru", "vi", "zhHans", "zhHant"}

Langs are the languages notifications can be written in, using the same keys as the panel's own locales (frontend/src/locales/index.ts) so the settings page can offer one list for both.

Functions

func DeliverNow

func DeliverNow(text string) error

DeliverNow sends a pre-composed body to every configured channel, bypassing both the enabled-events filter and the suppressor.

The scheduled digests use it rather than Publish: they are not events. Each has its own schedule setting, which is already the operator's decision about how often to hear from it, and running them through the suppressor would mean a daily report silently swallowed by a cooldown.

func Host

func Host() string

Host is the name that goes at the top of every message. Without it, an operator running more than one panel cannot tell which one is reporting.

func HumanBytes

func HumanBytes(b int64) string

HumanBytes renders a byte count the way both halves of the Telegram integration show one.

Exported because service/tgbot needs the same rendering: the operator reads a client card from the bot and an expiry alert about that same client, and two copies of this would eventually disagree about the figure. Alongside Translate, Label, Paginate and Host, which are exported for the same reason.

func Label

func Label(lang, key string) string

Label translates one alert-table key. The digest labels live in this table rather than the bot's because the scheduled report is an alert, and the two callers (that report and the bot's /status) must not word it differently.

func LabelWith

func LabelWith(lang, key string, params map[string]string) string

LabelWith is Label for the few digest lines that carry a value, such as the count of names a truncated list dropped.

func Paginate

func Paginate(msg string, limit int) []string

Paginate splits a message into chunks no longer than limit runes, preferring to break at blank lines, then at line ends, and cutting mid-line only when a single line is itself too long.

That last fallback is the point of this function. 3x-ui splits on blank lines alone, so a long message that happens to contain none comes back unsplit and Telegram rejects the whole thing with "400: message is too long" -- which reads as the bot silently doing nothing. Any batch message here (a depletion pass naming every disabled client) is exactly that shape.

func Publish

func Publish(e Event)

Publish is what every event source calls. It never blocks and never returns an error: a notification that cannot be delivered must not change what the caller does, and every caller here is a cron job or the login path.

func Render

func Render(e Event, lang string) string

Render turns an event into the message body for the given language.

func RenderClient

func RenderClient(e Event, target ClientTarget, lang string) string

RenderClient turns an event into the message its own client is sent, which is a different message from the operator's rather than a translation of it: the operator is told which of their clients is running out, the client is told what has happened to their own account.

Nothing identifying the panel goes in -- no Host() prefix, no counts of other clients. This is the only notification that leaves the operator's own circle.

A kind with nothing to say to a client renders empty and the caller skips it, so adding a kind does not silently start messaging customers.

func SendBackup

func SendBackup(filename string, data []byte, caption string) error

SendBackup uploads a database backup to the configured Telegram chats.

Telegram only: a webhook receiver has nowhere to put a file, and mailing a database around is a different decision from alerting.

func Start

func Start(cfg func() Config)

Start brings the notifier up. It follows service.StartHub's shape: a package-level singleton with no-op entry points once it is down, so callers never have to check whether notifications are running before publishing.

func Stop

func Stop()

Stop tears the notifier down. Safe to call when it was never started.

func TestDeliver

func TestDeliver(cfg Config) error

TestDeliver sends one event straight to every configured channel, skipping both the enabled-events filter and the suppressor.

It backs the settings page's "send a test message" button, which has to reach the operator even when nothing is switched on yet -- that is the whole point of pressing it. Without this button a wrong chat id fails silently, which is the most common support question these panels get.

It uses the saved settings, not whatever is in the form: the credentials are write-only, so the page has no token to submit even if it wanted to. Save first, then test.

func Translate

func Translate(table map[string]map[string]string, lang, key string, params map[string]string) string

Translate looks key up in table[lang], falling back to English and finally to the key itself, then substitutes the {name} placeholders.

Exported because service/tgbot keeps its own table: the bot's wording is not the alert wording, and neither package should carry the other's strings. What should not be written twice is this -- the fallback rules and the placeholder syntax.

Falling back per key rather than per language matters: a key added in English but not yet translated should render in English everywhere, not turn the whole message into a bare identifier.

Types

type Bus

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

Bus is an in-process fan-out: Publish never blocks, and every subscriber gets every event on its own goroutine.

The two-level queueing is the whole point. Senders here block on network I/O for as long as their timeout allows, and a single shared worker would let a Telegram call that is waiting on a dead connection hold up the Webhook delivery of the same alert -- which is exactly the delivery that was supposed to cover for Telegram being unreachable. Per-subscriber queues also bound the damage: a wedged subscriber fills its own 64 slots and drops, instead of backing up into the bus or spawning a goroutine per event.

Filtering is the subscriber's job. The bus does not know which events an operator enabled.

func NewBus

func NewBus() *Bus

NewBus starts the dispatch loop. Callers own the returned bus and must Stop it.

func (*Bus) Publish

func (b *Bus) Publish(e Event)

Publish hands an event to the bus and returns immediately.

It is called from cron jobs and from the login path, so it must never block: a full buffer drops the event and logs it. Notifications are not a ledger -- losing one is better than stalling a login behind an unreachable Telegram.

func (*Bus) Stop

func (b *Bus) Stop()

Stop shuts the bus down. Buffered and queued events may be dropped; handlers already running are waited for. After Stop, Subscribe is a no-op -- which is also what keeps its wg.Add from racing Wait, since both go through b.mu.

func (*Bus) Subscribe

func (b *Bus) Subscribe(name string, handle func(Event))

Subscribe registers handle under name, replacing any subscriber already using that name. Each subscriber is driven by its own worker, so handle is never called concurrently with itself and sees events in publication order.

func (*Bus) Unsubscribe

func (b *Bus) Unsubscribe(name string)

Unsubscribe stops and drops the named subscriber. Unknown names are ignored, which is what lets the reload path unsubscribe unconditionally.

type ClientData

type ClientData struct {
	Names []string
	// DaysLeft / BytesLeft are only set for ClientExpiring, and only the one
	// that actually tripped is non-zero.
	DaysLeft  int
	BytesLeft int64
	// Targets are the clients this event is about that have a Telegram binding,
	// so they can be warned directly as well as the operator. Empty whenever
	// nobody involved has one, which is the common case -- the binding is
	// optional and set through the bot, not the panel's client form.
	//
	// The operator's message is still rendered from the fields above; this only
	// adds recipients, it does not change what the operator is told.
	Targets []ClientTarget
}

ClientData accompanies ClientDepleted / ClientExpiring. Names is plural because DepleteJob disables a whole batch in one pass and sends one event for the batch rather than one per client.

type ClientTarget

type ClientTarget struct {
	Name      string
	TgId      int64
	DaysLeft  int
	BytesLeft int64
}

ClientTarget is one client to warn on their own Telegram chat, carrying the figures that client's message needs. They are repeated here rather than read off ClientData because a batched event (ClientDepleted) stands for many clients at once, each with its own numbers.

type Config

type Config struct {
	Enable bool
	Proxy  string
	Lang   string
	Events map[Kind]bool

	Telegram TelegramConfig
	Webhook  WebhookConfig
	SMTP     SMTPConfig
}

Config is the whole notification setup, read fresh for every event.

It is supplied as a callback rather than captured at Start, so a settings change takes effect on the next event with no reload step to remember. The events are rare enough (minutes apart at worst) that the extra settings read does not matter.

func (Config) Wants

func (c Config) Wants(k Kind) bool

Wants reports whether the operator asked to hear about this kind.

Exported because an event source may be expensive enough to be worth skipping entirely: the outbound probe dials every outbound through the core, which is not something to do every five minutes for an alert nobody enabled.

type CoreData

type CoreData struct {
	Err string
}

CoreData accompanies CoreCrash.

type Decision

type Decision struct {
	Send bool
	// Failures is how many attempts this single notification stands for, and
	// is only set for LoginFailed. It is 1 for the first alert in a window and
	// higher when attempts were folded into it.
	Failures int
}

Decision is what the suppressor concluded about one event.

type Event

type Event struct {
	Kind Kind
	// Subject is what the event is about: a node name, a client name, a source
	// IP. It is half of the suppression key, so events about different subjects
	// never suppress one another.
	Subject string
	// Data carries the kind-specific payload, one of the types below. Renderers
	// type-assert it and must tolerate a nil or mismatched value rather than
	// panicking -- an event source that forgets to attach it should degrade to
	// a plainer message, not take the panel down.
	Data any
	// Text, when set, is delivered verbatim instead of rendering Kind. The
	// scheduled digests use it: their body is assembled from client and node
	// data this package cannot reach without depending on service, which would
	// close an import cycle.
	Text string
	At   time.Time
}

Event is one thing worth telling the operator about.

type Kind

type Kind string

Kind identifies what happened.

The string form is what the notifyEvents setting stores, so these values are part of the panel's persisted configuration: renaming one silently turns that event off for every operator who had it enabled. Add new kinds, do not rename existing ones.

const (
	// State events. Delivery is gated on a transition -- see Suppressor.
	NodeDown     Kind = "node.down"
	NodeUp       Kind = "node.up"
	CoreCrash    Kind = "core.crash"
	CoreUp       Kind = "core.up"
	OutboundDown Kind = "outbound.down"
	OutboundUp   Kind = "outbound.up"

	// One-shot events, rate limited per subject.
	ClientDepleted Kind = "client.depleted"
	ClientExpiring Kind = "client.expiring"
	CPUHigh        Kind = "cpu.high"
	MemoryHigh     Kind = "memory.high"

	// Login events, each handled differently -- see Suppressor.Decide.
	LoginSuccess Kind = "login.success"
	LoginFailed  Kind = "login.failed"
	LoginBanned  Kind = "login.banned"
)

type LoginData

type LoginData struct {
	Username   string
	IP         string
	Failures   int
	BanMinutes int
}

LoginData accompanies the three login kinds. Failures is the number of attempts folded into this notification, which is 1 for the first alert in a window and higher when the limiter merged some -- see Suppressor.Decide.

type MetricData

type MetricData struct {
	Percent   float64
	Threshold int
}

MetricData accompanies CPUHigh / MemoryHigh.

type NodeData

type NodeData struct {
	LatencyMs int64
	Err       string
}

NodeData accompanies NodeDown / NodeUp.

type Notifier

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

Notifier ties the suppressor and the bus to a config source.

type OutboundData

type OutboundData struct {
	LatencyMs uint16
	Err       string
}

OutboundData accompanies OutboundDown / OutboundUp. Separate from NodeData despite the identical shape: the two describe different things, and merging them would make a later field that only one of them has read as if it applied to both.

type SMTPConfig

type SMTPConfig struct {
	Host string
	Port int
	User string
	Pass string
	From string
	To   []string
	// Security is none, starttls or tls.
	Security string
}

type Suppressor

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

Suppressor decides which events actually reach the channels.

It sits in front of the bus rather than inside each subscriber, so every channel is told about the same events -- an operator comparing their Telegram history against a webhook receiver should not find them disagreeing.

All of its state is in memory. A restart therefore re-arms everything: a still-relevant expiry warning goes out a second time, and a node that was already known down announces itself again. Persisting it would mean a SQLite write per event to save at most one duplicate message per restart, which is the wrong trade -- but it does mean event sources whose underlying condition repeats every few seconds (the core restart loop, node probes) must debounce on their own rather than relying on this to be the only guard.

func NewSuppressor

func NewSuppressor() *Suppressor

func (*Suppressor) Decide

func (s *Suppressor) Decide(e Event, now time.Time) Decision

Decide reports whether e should be delivered. now is a parameter so the decision is testable without sleeping.

type TelegramConfig

type TelegramConfig struct {
	Token   string
	ChatIDs []string
	// APIServer overrides https://api.telegram.org, for operators running their
	// own Bot API server. On a censored network that is a steadier route than
	// an HTTP proxy, which still has to reach Telegram's own edge.
	APIServer string
}

type WebhookConfig

type WebhookConfig struct {
	URL string
}

Jump to

Keyboard shortcuts

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