Skip to content
quarry Artifact [69c9802b61]
Login

Artifact [69c9802b61]

Artifact 69c9802b61e19b65edb6eb76d132fbbee14e8aa7e8c86053d03f647e8823e19b:


// chatbridge.go is RFC 0011 ยง9's configuration surface: which
// repository's channel is bridged to which Fossil server's chat, and
// under whose login.
//
// The bridge is repository configuration, so it is `a` and Admin, the
// same standing every other mirror op takes. The credential goes in
// and never comes back: there is no field on any Out here that could
// carry it, which is the only way to be sure a page never renders it.
package api

import (
	"context"
	"fmt"

	"craftdesign.group/quarry/internal/ops"
)

// ChatBridge is one bridged channel as every caller but the poller
// sees it. No secret, by construction.
type ChatBridge struct {
	Repo string `json:"repo"`
	URL  string `json:"url"`
	User string `json:"user"`
	// LastMsgID is the upstream chat message the poller has read
	// through; At is when one last arrived and LastErr why the last
	// poll failed, kept apart so "quiet" and "broken" read differently.
	LastMsgID int64  `json:"lastMsgId"`
	At        string `json:"at,omitempty"`
	LastErr   string `json:"lastErr,omitempty"`
}

// State is the one word a status line needs: a bridge that has never
// heard anything and has no error is still connecting.
func (b ChatBridge) State() string {
	switch {
	case b.LastErr != "":
		return "failing"
	case b.At != "":
		return "connected"
	default:
		return "connecting"
	}
}

// ChatBridgeStore is the bridge's configuration. Stated in this
// package's own terms for the same reason MirrorStore is: the op layer
// names what it needs. nil disables the op, which is a forge with no
// OLTP โ€” and therefore no bridge to configure.
type ChatBridgeStore interface {
	SetChatBridge(repo, url, user, secret string) error
	DetachChatBridge(repo string) error
	ChatBridgeFor(repo string) (ChatBridge, bool, error)
	ChatBridges() ([]ChatBridge, error)
}

type chatBridgeIn struct {
	Repo string `json:"repo" validate:"required,max=64"`
	// URL is the upstream fossil's base URL โ€” the one its /chat page
	// lives under, not the /chat-poll endpoint.
	URL      string `json:"url" validate:"max=512"`
	User     string `json:"user" validate:"max=64"`
	Password string `json:"password" validate:"max=256"`
	// Detach removes the bridge and forgets its cursor.
	Detach bool `json:"detach"`
}

type chatBridgeOut struct {
	// Bridge is the configuration as stored โ€” a value rather than a
	// pointer because the ops spec reflector documents only concrete
	// shapes, and Detached already says when there is nothing to read.
	Bridge   ChatBridge `json:"bridge"`
	Detached bool       `json:"detached,omitempty"`
}

func registerChatBridge(r *ops.Registry, d Deps) error {
	return ops.Register(r, ops.Op[chatBridgeIn, chatBridgeOut]{
		Name:  "mirror.chatbridge",
		Caps:  "a",
		Doc:   "Bridge a repository's chat channel to a Fossil server's chat, or detach it. The password is stored and never returned.",
		Kind:  ops.Command,
		Auth:  ops.Admin,
		Route: "POST /api/v1/r/{repo}/mirror/chatbridge",
		Run: func(ctx context.Context, in chatBridgeIn) (chatBridgeOut, error) {
			if d.ChatBridges == nil {
				return chatBridgeOut{}, fmt.Errorf("the chat bridge is not configured: %w", ops.ErrUnavailable)
			}
			// The store must be one quarry serves, for the reason
			// mirror.set checks the same thing: a row for a name that
			// resolves to nothing is a bridge the scheduler skips for
			// ever, which reads as "bridging is broken".
			if _, err := d.repoFor(ctx, in.Repo); err != nil {
				return chatBridgeOut{}, err
			}
			if in.Detach {
				if err := d.ChatBridges.DetachChatBridge(in.Repo); err != nil {
					return chatBridgeOut{}, err
				}
				return chatBridgeOut{Detached: true}, nil
			}
			if in.URL == "" || in.User == "" || in.Password == "" {
				return chatBridgeOut{}, fmt.Errorf(
					"a chat bridge needs a url, a user and a password: %w", ops.ErrInvalid)
			}
			if err := d.ChatBridges.SetChatBridge(in.Repo, in.URL, in.User, in.Password); err != nil {
				return chatBridgeOut{}, fmt.Errorf("%w: %v", ops.ErrInvalid, err)
			}
			b, ok, err := d.ChatBridges.ChatBridgeFor(in.Repo)
			if err != nil || !ok {
				return chatBridgeOut{}, err
			}
			return chatBridgeOut{Bridge: b}, nil
		},
	})
}

// bridgeOf answers one repository's bridge for a listing, false when
// it has none or the forge keeps no bridges at all.
func (d Deps) bridgeOf(repo string) (ChatBridge, bool) {
	if d.ChatBridges == nil {
		return ChatBridge{}, false
	}
	b, ok, err := d.ChatBridges.ChatBridgeFor(repo)
	if err != nil || !ok {
		return ChatBridge{}, false
	}
	return b, true
}