irchandlers

package
v1.5.1-0...-475df33 Latest Latest
Warning

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

Go to latest
Published: Aug 10, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Overview

Package irchandlers ports server/plugins/irc-events/*.ts: the reactive logic that turns events off an ircbridge.Bridge into Network/Chan/User state changes, constructed Msg values pushed through Chan.PushMessage, and any additional hub broadcasts a Node handler emits directly via `client.emit(...)`.

Node registers each irc-events/*.ts module once per network connection as `handler.apply(client, [network.irc, network])`, and each module attaches its own irc-framework event listeners closing over that `client`/`network` pair. ircbridge.Bridge already collapses the underlying connection's many callback types into one fan-out Event stream, so the Go port collapses Node's ~29 independent listener registrations into one handler struct (constructed per network connection, exactly like Node's closures) with one dispatch method per ircbridge.EventType - individual *.go files below still mirror the *.ts files 1:1 for the logic itself, so a reviewer can match either side file-for-file.

What's deliberately NOT ported yet, consistently across every file here: client.save() (ClientManager persistence isn't wired to a live connection until later stages) and chan.loadMessages (Stage 8 message storage). Push notifications, client.mentions bookkeeping, and custom-highlight testing are wired via Deps's Notify/Mentions/CustomHighlightRegex/ CustomHighlightExceptionRegex/IsOpen seams (internal/session supplies the closures once Client owns the data), not deferred anymore.

link.go ports server/plugins/irc-events/link.ts - one of the two files the stage plan calls out for byte-for-byte care, specifically its SSRF hardening: isDisallowedPrefetchAddress and the resolved-address recheck on every connection (safeDialContext below), which defeats DNS rebinding by validating the address actually being dialed, not just the URL's hostname.

The full Node pipeline (cheerio-based OpenGraph scraping, media/thumbnail type sniffing) is scoped down here to: fetch through the SSRF-hardened client, sniff content-type, and for text/html extract <title>/description via a small regex-based scan (not a full HTML parse - documented divergence from cheerio) rather than pulling in a new HTML parsing dependency. Image thumbnails are cached locally via internal/prefetchstore (Deps.PrefetchStore) when Config.values.prefetchStorage is on, matching storage.ts; otherwise (or if storage isn't wired) they link directly to the already-fetched-and-validated source URL.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func PrefetchStoredMessage

func PrefetchStoredMessage(hub *wsproto.Hub, cfg *config.Loaded, store func(data []byte, extension string) (string, error), c *model.Chan, msg *model.Msg)

PrefetchStoredMessage rebuilds previews for a message restored from persistent history. SQLite deliberately omits previews because the web server normally rebuilds them while its process stays alive. The native desktop shell restarts that process when the app is relaunched, so the desktop history loader calls this wrapper after restoring messages. Keep the behavior behind that caller rather than changing the normal web/PWA history path.

func Register

func Register(bridge *ircbridge.Bridge, network *model.Network, conn *irc.Connection, deps Deps) int

Register wires up every ported irc-events handler for one network connection: subscribes a single dispatch listener to bridge and returns its subscription id (for Unsubscribe on disconnect/network removal). conn is bridge.Conn - passed separately since ircbridge.Bridge does not expose it as an interface, only the concrete *irc.Connection callers already hold.

Types

type Deps

type Deps struct {
	Hub    *wsproto.Hub
	Cfg    *config.Loaded
	NextID func() int

	// NextChanID mirrors Client.createChannel's `chan.id = this.idChan++`
	// counter - every Chan an irc-events handler opens (self-join, a new
	// incoming query, list/ban/invite special windows) needs an id from
	// this same per-account sequence, or it's left at Go's zero value and
	// collides with every other unassigned channel.
	NextChanID func() int

	STS STSUpdater

	// Index mirrors Chan.pushMessage's own `this.writeUserLog(client, msg)`
	// call - a Client-level concern (the message-storage backend list)
	// this package doesn't own, wired in by internal/session. nil is
	// accepted (messages simply aren't indexed) via the same seam pattern
	// STS uses.
	Index func(network *model.Network, channel *model.Chan, msg *model.Msg)

	// Update persists mutable fields on an already-indexed message, such as
	// reactions. nil means the configured storage has no update capability.
	Update func(network *model.Network, channel *model.Chan, msg *model.Msg)

	// IsOpen mirrors chan.pushMessage's own `_.find(client.attachedClients,
	// {openChannel: chanId})` lookup (Client.IsChannelOpen) - whether the
	// target channel is the currently-focused one in any attached session,
	// which suppresses unread/highlight counter increments. nil is treated
	// as "never open".
	IsOpen func(chanID int) bool

	// CustomHighlightRegex/CustomHighlightExceptionRegex mirror
	// Client.highlightRegex/highlightExceptionRegex - read fresh on every
	// call (rather than captured once) since setting:set recompiles them
	// mid-session. nil, or a nil returned regex, both mean "not
	// configured".
	CustomHighlightRegex          func() *regexp.Regexp
	CustomHighlightExceptionRegex func() *regexp.Regexp

	// Mentions appends one entry to the owning client's Mentions list
	// (capped at 100), mirroring `client.mentions.push(...)` in
	// message.ts. nil skips the bookkeeping.
	Mentions func(entry MentionEntry)

	// Notify sends a push notification for an eligible highlighted/reply
	// event, mirroring notifications.ts's sendPushNotification (which
	// itself checks the per-category clientSettings.notify* toggle before
	// calling WebPush.Push). nil skips push entirely.
	Notify func(categories []string, chanID int, timestampMillis int64, title, body string)

	// ClientAwayFallback mirrors connection.ts's `client.awayMessage &&
	// _.size(client.attachedClients) === 0` check: the account-level away
	// message to fall back to when this network has none of its own and no
	// browser session is currently attached. Returns "" when there's no
	// fallback to apply (no away message set, or a session is attached);
	// nil is treated the same as always returning "".
	ClientAwayFallback func() string

	// PrefetchStore mirrors storage.ts's Storage.store(), used by link.go
	// to cache a fetched thumbnail locally instead of showing a direct link
	// to it, when Config.values.prefetchStorage is on. nil (or a nil
	// return) falls back to linking directly to the original remote URL,
	// matching Node's own `!Config.values.prefetchStorage` branch.
	PrefetchStore func(data []byte, extension string) (string, error)

	// LoadHistory mirrors chan.ts's `channel.loadMessages(client, network)`
	// call, made right after join.ts/message.ts/whois.ts each open a new
	// channel/query window (self-rejoin, an incoming DM's auto-opened
	// query, a classic (non-popup) WHOIS lookup) - see internal/session's
	// loadHistoryForChannel, the real implementation this seam calls into
	// (this package can't import internal/session directly: session
	// already imports irchandlers to wire up Register). nil skips the
	// load, leaving the new channel empty exactly like before this seam
	// existed.
	LoadHistory func(network *model.Network, chn *model.Chan)

	// Decrypt examines recognized Relay ciphertext before the message passes
	// through highlighting, previews, notifications, persistence, and the
	// WebSocket payload. It returns the displayed text, whether the message
	// was encrypted, and a stable non-sensitive error code when unavailable.
	Decrypt func(network *model.Network, kind, target, peer, message string) (string, bool, string)
}

Deps carries the dependencies every handler needs, gathered in one place so Register's signature doesn't grow a parameter per handler file.

type MentionEntry

type MentionEntry struct {
	ChanID int
	MsgID  int
	Type   model.MessageType
	Time   time.Time
	Text   string
	From   model.UserInMessage
}

MentionEntry mirrors one entry Client.mentions.push(...) appends - duplicated here (rather than importing internal/auth) to keep this package a leaf with no dependency on account/session storage, matching the STSUpdater/Index seam pattern above.

type STSUpdater

type STSUpdater interface {
	Update(host string, port, durationSeconds int)
}

STSUpdater persists an IRCv3 STS policy learned during CAP negotiation. internal/sts (Stage 10) will implement it; nil is accepted (the STS upgrade message is still shown, the policy just isn't remembered across reconnects until that stage lands) via the same interface-seam pattern model.Network.Validate uses for model.STSLookup.

Jump to

Keyboard shortcuts

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