Add telegram and the desk as channels of their own, each judging nothing and holding only its own secret (hq ADR 0259)

This commit is contained in:
jochen
2026-10-09 17:08:17 +02:00
committed by jschoubben
parent 8754de747d
commit a412389725
16 changed files with 2220 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
// telegram: a channel of the operator's conversation, kind `telegram` of the `channel` and `intake` benches
// (novox/hq ADR 0234, ADR 0259). A Go bundle the node's runtime launches as this module's own account, never
// root and never the operator's: the bot token and this channel's state are that account's alone. stdout is
// the MCP channel; what this module says, it says on stderr.
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) }
// Worker is the work queue of this kind, as the controller names it.
const Worker = "SEAT_CHANNEL_TELEGRAM_worker"
// busStore keeps the update offset and where each message is in this module's state on the bus.
type busStore struct{}
func (busStore) Offset() (int64, error) {
e, err := stdio.State("offset").Get("updates")
if err != nil || e == nil {
return 0, err
}
var o struct {
Next int64 `json:"next"`
}
return o.Next, json.Unmarshal(e.Value, &o)
}
func (busStore) SetOffset(next int64) error {
_, err := stdio.State("offset").Put("updates", map[string]int64{"next": next})
return err
}
func (busStore) Placed(handle string) (*Placed, error) {
e, err := stdio.State("messages").Get(handle)
if err != nil || e == nil {
return nil, err
}
var p Placed
return &p, json.Unmarshal(e.Value, &p)
}
func (busStore) Place(handle string, p Placed) error {
_, err := stdio.State("messages").Put(handle, p)
return err
}
func main() {
c := &Channel{
Bot: &Bot{TokenFile: os.Getenv("MESH_TELEGRAM_TOKEN_FILE"), Client: &http.Client{Timeout: PollWait + 15*time.Second}},
Store: busStore{},
Publish: func(subject string, body any, id string) error { return stdio.SeatPublish(subject, body, id) },
Prove: stdio.SeatProve,
Logf: logf,
}
c.init()
go run(c)
tools := []stdio.Tool{{Name: "telegram_status",
Description: "Whether this channel can reach the operator on Telegram, and why not: whether the bot token works " +
"(never the token), the bot's name, when updates last arrived, the last delivery and the last error, how " +
"many it delivered and failed since it started, and the rules it keeps. Linking an account as the " +
"operator is the router's (messenger_identities).",
Run: func(map[string]any) (any, error) { return c.Status(), nil }}}
if err := stdio.Serve("", tools); err != nil {
logf("%v", err)
os.Exit(1)
}
}
// run takes this kind's work, long-polls the updates and says its standing — each retried, each failure said.
func run(c *Channel) {
time.Sleep(500 * time.Millisecond) // Serve first: the bus is reached through it
go func() {
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
err := stdio.SeatTake(Worker, func(w stdio.Work) error { return c.Take(w.Subject, w.Body) })
if err == nil {
logf("[telegram] taking the router's messages for this kind")
return
}
logf("[telegram] not taking messages yet (%v); asking again in %s", err, wait)
time.Sleep(wait)
}
}()
go func() {
for {
c.SayStanding()
time.Sleep(time.Minute)
}
}()
said := ""
for {
if _, err := c.Bot.token(); err != nil {
if err.Error() != said {
logf("[telegram] %v", err)
said = err.Error()
}
time.Sleep(time.Minute)
continue
}
if err := c.Poll(PollWait); err != nil {
if err.Error() != said {
logf("[telegram] the updates do not arrive: %v", err)
said = err.Error()
}
c.SayStanding()
time.Sleep(10 * time.Second)
continue
}
said = ""
}
}