watch

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package watch holds the watcher registry, per-watcher state, the send logs and the gate that decides whether a watcher may send one reply.

Index

Constants

View Source
const (
	RulePausedAll       = "paused_all"
	RuleNotRegistered   = "not_registered"
	RuleInvalid         = "invalid_registration"
	RulePaused          = "paused"
	RuleSession         = "session_mismatch"
	RuleTextLength      = "text_length"
	RuleRateWatcher     = "rate_watcher"
	RuleRateGlobal      = "rate_global"
	RuleChatSpacing     = "chat_spacing"
	RuleChatType        = "chat_type"
	RuleScope           = "scope"
	RuleEmptyChat       = "empty_chat"
	RuleLatestFromMe    = "latest_from_me"
	RuleOwnerBackoff    = "owner_backoff"
	RuleUnverifiable    = "unverifiable"
	RuleReplyToRequired = "reply_to_required"
	RuleTargetMissing   = "target_missing"
	RuleTargetFromMe    = "target_from_me"
	RuleAlreadyAnswered = "already_answered"
	RuleAlreadyReplied  = "already_replied"
	RuleNotMentioned    = "not_mentioned"
)

Refusal rule ids, reported as error.details.rule.

View Source
const (
	ScopeListed   = "listed"   // chats in the registry entry
	ScopeContacts = "contacts" // mutual contacts, or listed chats
	ScopeDMFirst  = "dm-first" // 1:1 chats where the other side wrote first
)

Scopes: which chats a watcher may answer.

View Source
const (
	DefaultScope          = ScopeListed
	DefaultMentionsOnly   = true
	DefaultMaxChars       = 300
	DefaultPerHour        = 20
	DefaultPerChatSeconds = 20
	DefaultBackoffMinutes = 15
)

Defaults for omitted registry fields.

View Source
const (
	PauseFile = "PAUSE"
	LogFile   = "sends.jsonl"
)

File names: PAUSE in the watcher's own dir or its data-dir dir; the send log in the data dir.

View Source
const RegistryHelp = `` /* 1591-byte string literal not displayed */

RegistryHelp documents watchers.json for --help.

Variables

This section is empty.

Functions

func AddressedToMe

func AddressedToMe(ctx context.Context, t tgclient.Telegram, p *tgclient.Peer, known []tgclient.Message, m *tgclient.Message) (bool, error)

AddressedToMe: m mentions the user (Telegram's flag) or replies to one of their messages. known saves a fetch when it holds the parent.

func AppendJSONL

func AppendJSONL(path string, v any) error

AppendJSONL appends one line (0600, parent dir created 0700) and syncs it. Callers hold the watch lock.

func ReadJSONL

func ReadJSONL[T any](path string) (out []T, bad int, err error)

ReadJSONL reads a jsonl file; a missing file is empty. bad counts unparsable lines.

func Refuse

func Refuse(rule, format string, a ...any) *output.Error

Refuse builds a watcher refusal (exit 7) with a stable rule id.

Types

type Answer

type Answer struct {
	T       float64 `json:"t"`
	At      string  `json:"at"`
	Watcher string  `json:"watcher"`
	ChatID  int64   `json:"chat_id"`
	MsgID   int     `json:"msg_id"`
	SentID  int     `json:"sent_id"`
}

Answer is one answered.jsonl line: a message some watcher answered, and the reply.

func (Answer) Time

func (a Answer) Time() time.Time

type Gate

type Gate struct {
	Paths         config.Paths
	Name          string
	Caller        string // the caller's session id; never printed
	Registry      *Registry
	W             *Watcher
	GlobalPerHour int
	Now           func() time.Time
}

Gate runs the checks for one watcher's send. Everything it trusts comes from the user-owned registry and the data dir, re-read by Reload.

func Open

func Open(p config.Paths, name, caller string, globalPerHour int, now func() time.Time) (*Gate, error)

Open loads the watcher and checks pauses, registration and the session binding.

func (*Gate) CheckChat

func (g *Gate) CheckChat(ctx context.Context, t tgclient.Telegram, p *tgclient.Peer, self *tgclient.User,
	l *Ledger, replyTo int, text string) (*Plan, error)

CheckChat runs the chat-type, scope and conversation rules against the live chat.

func (*Gate) Limits

func (g *Gate) Limits(chat int64, l *Ledger) error

Limits checks the per-watcher and global hourly limits and per-chat spacing. Every logged attempt counts, failed ones too.

func (*Gate) LoadLedger

func (g *Gate) LoadLedger() (*Ledger, error)

LoadLedger reads the shared ledger.

func (*Gate) Recheck

func (g *Gate) Recheck(ctx context.Context, t tgclient.Telegram, p *tgclient.Peer, self *tgclient.User, text string) error

Recheck runs right before the send: the registry is re-read and the entry, binding, text length, chat type and scope must still allow this reply.

func (*Gate) Record

func (g *Gate) Record(plan *Plan, sent *tgclient.Message, sendErr error) error

Record appends the attempt to the data-dir send log and, on success, the answer to the ledger. Callers hold the watch lock.

func (*Gate) Reload

func (g *Gate) Reload() error

Reload re-reads watchers.json and re-checks, in order: PAUSE-ALL, registration, a valid entry, the watcher's PAUSE files, and the caller's session binding.

func (*Gate) TextLength

func (g *Gate) TextLength(text string) error

TextLength checks the reply length against the current entry.

type Ledger

type Ledger struct {
	Answers []Answer
}

Ledger is answered.jsonl, read once per send.

func (*Ledger) Answered

func (l *Ledger) Answered(chat int64, msg int) bool

Answered reports whether (chat, msg) was answered by any watcher.

func (*Ledger) SentByWatcher

func (l *Ledger) SentByWatcher(chat int64, msg int) bool

SentByWatcher reports whether (chat, msg) is a reply some watcher sent.

type Plan

type Plan struct {
	Chat    tgclient.ChatRef `json:"chat"`
	ReplyTo int              `json:"reply_to,omitempty"`
	Answers int              `json:"answers"` // the incoming message being answered
	Group   bool             `json:"group"`
	Text    string           `json:"text"`
	Watcher string           `json:"watcher"`
}

Plan is what a passing check would send.

type Registry

type Registry struct {
	Path    string
	Missing bool
	// contains filtered or unexported fields
}

Registry is watchers.json; entries are decoded on demand so one bad entry does not hide the others.

func LoadRegistry

func LoadRegistry(p config.Paths) (*Registry, error)

LoadRegistry reads the data dir's watchers.json; a missing file is an empty registry.

func (*Registry) Has

func (r *Registry) Has(name string) bool

Has reports whether name is registered.

func (*Registry) Names

func (r *Registry) Names() []string

Names returns the registered watcher names, sorted.

func (*Registry) Watcher

func (r *Registry) Watcher(name string) (*Watcher, []string)

Watcher decodes one entry strictly. It returns the best-effort watcher and every problem found; a watcher with problems must not send.

type Send

type Send struct {
	T        float64 `json:"t"`
	At       string  `json:"at"`
	Watcher  string  `json:"watcher"`
	ChatID   int64   `json:"chat_id"`
	ReplyTo  int     `json:"reply_to,omitempty"`
	Answered int     `json:"answered,omitempty"`
	Text     string  `json:"text"`
	OK       bool    `json:"ok"`
	MsgID    int     `json:"msg_id,omitempty"`
	Err      string  `json:"err,omitempty"`
}

Send is one sends.jsonl line: an attempt that reached Telegram.

func (Send) Time

func (s Send) Time() time.Time

Time returns the record's time.

type Watcher

type Watcher struct {
	Name           string           `json:"name"`
	Session        string           `json:"-"`
	Chats          map[int64]string `json:"-"`
	Dir            string           `json:"dir,omitempty"`
	Scope          string           `json:"scope"`
	Groups         bool             `json:"groups"`
	MentionsOnly   bool             `json:"mentions_only"`
	MaxChars       int              `json:"max_chars"`
	PerHour        int              `json:"per_hour"`
	PerChatSeconds int              `json:"per_chat_seconds"`
	BackoffMinutes int              `json:"backoff_minutes"`
	Approved       string           `json:"approved"`
	// contains filtered or unexported fields
}

Watcher is a registry entry with defaults applied. Session is never printed.

func (*Watcher) DataDir

func (w *Watcher) DataDir() string

DataDir is watchers/NAME in the data dir ("" for an invalid name).

func (*Watcher) Listed

func (w *Watcher) Listed(chat int64) bool

Listed reports whether chat is on the allowlist.

func (*Watcher) LogPath

func (w *Watcher) LogPath() string

LogPath is the send log in the data dir.

func (*Watcher) PausePaths

func (w *Watcher) PausePaths() []string

PausePaths are the PAUSE files that pause this watcher.

Jump to

Keyboard shortcuts

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