A module that cannot set Betterbird up is of little use (the operator, 2026-10-12). The module now declares the community package absent and unpacks the pinned upstream release where that package had it, so the same profile opens; places the enterprise policy that turns Betterbird's updater off and force-installs Owl from the xpi the mesh pinned by its vendor's digest (never in a repository); and takes the operator's accounts from the trusted setting accounts, without any password or token: an account the profile already holds is adopted under its own keys, a new one is created under the module's mesh_ keys, an ambiguous one is refused with the remedy, and the account lists are only written into. betterbird_accounts says the profile's accounts by count and proposes the setting built from them for the operator's Approve. Every new build waits for the operator's push. The reminder test is visibly a test (MESH TEST band, event name) and its description says it opens a window on the operator's screen.
225 lines
7.6 KiB
Go
225 lines
7.6 KiB
Go
package main
|
|
|
|
// What the mesh installs (novox/hq ADR 0316 §1–§9, to-be 56 §1–§3): Betterbird from its pinned upstream release
|
|
// in /opt/betterbird, where the community package betterbird-bin put it, so Betterbird keeps choosing the same
|
|
// profile; the enterprise policy in its distribution directory, which turns Betterbird's own updater off and
|
|
// installs Owl into every profile from the xpi the mesh placed beside it; and a defaults file naming the
|
|
// system's spelling dictionaries. The tools read these files and say what they find: the version against the
|
|
// pin and the newest published build, whether the policy is placed, Owl's version against its pin.
|
|
//
|
|
// **The pin on the machine is what the mesh placed.** The manifest pins the release and the xpi by sha256; the
|
|
// node-engine places exactly those bytes, so the version in /opt/betterbird/application.ini (when the mesh
|
|
// placed it) and the version in the placed xpi are the pins. The community package's own copy is said as
|
|
// such, and the switch from it waits for the operator (ADR 0316 §4).
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
installDir = "/opt/betterbird"
|
|
// policyFile is the enterprise policy the module places (ADR 0316 §7).
|
|
policyFile = installDir + "/distribution/policies.json"
|
|
// owlFile is Owl's xpi as the mesh placed it, and the URL the policy installs it from.
|
|
owlFile = installDir + "/owl/owl.xpi"
|
|
owlID = "owl@beonex.com"
|
|
// preparation is the tools' program as the module places it for its start (betterbird-tools prepare).
|
|
preparation = "/usr/local/lib/mesh-betterbird/betterbird-tools"
|
|
// versionsURL is where Betterbird publishes its newest build per major version: what its own updater
|
|
// reads (MailGlue's BetterbirdUpdateCheck), and what the status compares the installed build with.
|
|
versionsURL = "https://www.betterbird.eu/start/versions.txt"
|
|
)
|
|
|
|
// publishedBuilds fetches Betterbird's list of newest builds: a variable so a test answers.
|
|
var publishedBuilds = func() (string, error) {
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
resp, err := client.Get(versionsURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("%s answered %s", versionsURL, resp.Status)
|
|
}
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
return string(body), err
|
|
}
|
|
|
|
// Installation is what is installed, and from where.
|
|
type Installation struct {
|
|
// From is the mesh, the community package (betterbird-bin, before the switch), or nothing.
|
|
From string `json:"from"`
|
|
Version string `json:"version,omitempty"`
|
|
Build string `json:"build,omitempty"`
|
|
// Newest is the newest build Betterbird published for this major version, and whether this one is behind.
|
|
Newest string `json:"newest_published_build,omitempty"`
|
|
Behind bool `json:"behind,omitempty"`
|
|
// Policy says whether the module's enterprise policy is placed, and what it does.
|
|
Policy string `json:"policy"`
|
|
// Owl is the add-on's version as the mesh placed it (the pin), and as the profile runs it.
|
|
Owl *OwlState `json:"owl,omitempty"`
|
|
}
|
|
|
|
// OwlState is Owl against its pin.
|
|
type OwlState struct {
|
|
Pinned string `json:"pinned,omitempty"`
|
|
InProfile string `json:"in_profile,omitempty"`
|
|
Active bool `json:"active"`
|
|
AtThePin bool `json:"at_the_pin"`
|
|
}
|
|
|
|
// installation reads what is installed. profile may be empty when none is found yet.
|
|
func (m *Machine) installation(profile string) Installation {
|
|
var in Installation
|
|
community, _ := m.installed(packageFor)
|
|
raw, err := readBounded(m.path(appIni))
|
|
switch {
|
|
case community != "":
|
|
in.From = "the community package " + packageFor + " " + community + ": the switch to the mesh's release waits for the operator's push, with Betterbird quit (novox/hq ADR 0316 §4)"
|
|
case err == nil:
|
|
in.From = "the mesh"
|
|
default:
|
|
in.From = "not installed"
|
|
}
|
|
if err == nil {
|
|
_, sec := iniSections(raw)
|
|
in.Version, in.Build = sec["App"]["Version"], sec["App"]["BuildID"]
|
|
in.Newest, in.Behind = newestBuild(in.Version, in.Build)
|
|
}
|
|
in.Policy = m.policyState()
|
|
pinned := xpiVersion(m.path(owlFile))
|
|
owl := &OwlState{Pinned: pinned}
|
|
if profile != "" {
|
|
for _, a := range addOns(profile) {
|
|
if a.ID == owlID {
|
|
owl.InProfile, owl.Active = a.Version, a.Active
|
|
}
|
|
}
|
|
}
|
|
owl.AtThePin = owl.Pinned != "" && owl.Pinned == owl.InProfile
|
|
if owl.Pinned != "" || owl.InProfile != "" {
|
|
in.Owl = owl
|
|
}
|
|
return in
|
|
}
|
|
|
|
// newestBuild is the newest build Betterbird published for a version's major, and whether build is older.
|
|
func newestBuild(version, build string) (string, bool) {
|
|
major, _, _ := strings.Cut(version, ".")
|
|
if major == "" || build == "" {
|
|
return "", false
|
|
}
|
|
list, err := publishedBuilds()
|
|
if err != nil {
|
|
return "unknown: " + err.Error(), false
|
|
}
|
|
for _, l := range strings.Split(list, "\n") {
|
|
f := strings.Split(strings.TrimSpace(l), ",")
|
|
if len(f) == 3 && f[0] == "Linux" && f[1] == major {
|
|
return f[2], f[2] > build
|
|
}
|
|
}
|
|
return "unknown: " + versionsURL + " names no build for " + major, false
|
|
}
|
|
|
|
// policyState says whether the module's policy is placed and does what ADR 0316 §5 and §7 say.
|
|
func (m *Machine) policyState() string {
|
|
raw, err := readBounded(m.path(policyFile))
|
|
if err != nil {
|
|
return "not placed: " + policyFile + " is missing"
|
|
}
|
|
if why := policyProblem(raw); why != "" {
|
|
return "placed, and " + why
|
|
}
|
|
return "placed: Betterbird's updater off, Owl installed into every profile from the mesh's xpi, its updates off"
|
|
}
|
|
|
|
// policyProblem is what the policy fails to say of what ADR 0316 asks, or "".
|
|
func policyProblem(raw []byte) string {
|
|
var p struct {
|
|
Policies struct {
|
|
DisableAppUpdate bool `json:"DisableAppUpdate"`
|
|
ExtensionSettings map[string]struct {
|
|
Mode string `json:"installation_mode"`
|
|
URL string `json:"install_url"`
|
|
Disabled bool `json:"updates_disabled"`
|
|
} `json:"ExtensionSettings"`
|
|
} `json:"policies"`
|
|
}
|
|
if err := json.Unmarshal(raw, &p); err != nil {
|
|
return "it is not JSON: " + err.Error()
|
|
}
|
|
owl, ok := p.Policies.ExtensionSettings[owlID]
|
|
switch {
|
|
case !p.Policies.DisableAppUpdate:
|
|
return "it does not turn Betterbird's updater off (DisableAppUpdate)"
|
|
case !ok || owl.Mode != "force_installed":
|
|
return "it does not install Owl into every profile (force_installed)"
|
|
case owl.URL != "file://"+owlFile:
|
|
return "it installs Owl from " + owl.URL + ", not from the mesh's xpi"
|
|
case !owl.Disabled:
|
|
return "it leaves Owl's updates on"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// xpiVersion is the version an add-on's manifest.json says, or "" when it cannot be read.
|
|
func xpiVersion(path string) string {
|
|
r, err := zip.OpenReader(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer r.Close()
|
|
for _, f := range r.File {
|
|
if f.Name != "manifest.json" {
|
|
continue
|
|
}
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer rc.Close()
|
|
var mf struct {
|
|
Version string `json:"version"`
|
|
}
|
|
if json.NewDecoder(io.LimitReader(rc, 1<<20)).Decode(&mf) != nil {
|
|
return ""
|
|
}
|
|
return mf.Version
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// addOns reads a profile's extensions.json: each add-on's id, version, whether it is on, and where from.
|
|
func addOns(profile string) []AddOn {
|
|
raw, err := readBounded(filepath.Join(profile, "extensions.json"))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var ext struct {
|
|
Addons []struct {
|
|
ID string `json:"id"`
|
|
Version string `json:"version"`
|
|
Active bool `json:"active"`
|
|
Location string `json:"location"`
|
|
} `json:"addons"`
|
|
}
|
|
if json.Unmarshal(raw, &ext) != nil {
|
|
return nil
|
|
}
|
|
var out []AddOn
|
|
for _, a := range ext.Addons {
|
|
if a.Location == "app-profile" {
|
|
out = append(out, AddOn{ID: a.ID, Version: a.Version, Active: a.Active})
|
|
}
|
|
}
|
|
return out
|
|
}
|