Skip to content
quarry Artifact [8e36b2daa4]
Login

Artifact [8e36b2daa4]

Artifact 8e36b2daa40db7b1c8e22235e50273b76c6864bbd88d43d547cc371f440076e0:


package web

import (
	"net/http"
	"net/url"
	"strings"

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

// chatBridgePanel is the Fossil chat bridge section of a repository's
// settings (RFC 0011 ยง9): the bridge, its state, and whether the
// channel is closed to named people, which a bridge needs. Who reads
// the channel is the roster's own rows, filtered on chat.
type chatBridgePanel struct {
	Bridged bool `json:"bridged"`
	Bridge  struct {
		URL     string `json:"url"`
		User    string `json:"user"`
		At      string `json:"at"`
		LastErr string `json:"lastErr"`
		Stopped string `json:"stopped"`
	} `json:"bridge"`
	State  string   `json:"state"`
	OpenTo []string `json:"openTo"`
	// Links are which accounts the bridged names are (chat.identities):
	// "Who is who".
	Links []oltp.ChatIdentity `json:"-"`
	// Err is this section's own refusal, back from its POST.
	Err string `json:"-"`
}

// Open says whether the channel's chat reaches people nobody named.
func (p *chatBridgePanel) Open() bool { return len(p.OpenTo) > 0 }

// OpenWords names who the channel is open to, in the settings page's
// words for the two categories.
func (p *chatBridgePanel) OpenWords() string {
	words := make([]string, 0, len(p.OpenTo))
	for _, c := range p.OpenTo {
		switch c {
		case "nobody":
			words = append(words, "every visitor")
		case "anonymous":
			words = append(words, "anyone signed in")
		}
	}
	return strings.Join(words, " and ")
}

// chatBridgePanelFor reads the bridge through `mirror.chatbridge.get`;
// nil, so no section, when the forge keeps no bridges.
func chatBridgePanelFor(r *ops.Registry, req *http.Request, repo string) *chatBridgePanel {
	out, err := invoke(req.Context(), r, "mirror.chatbridge.get", url.Values{"repo": {repo}})
	if err != nil {
		return nil
	}
	var p chatBridgePanel
	remarshal(out, &p)
	p.Err = req.URL.Query().Get("cbe")
	if p.Bridged {
		if out, err := invoke(req.Context(), r, "chat.identities", url.Values{"repo": {repo}}); err == nil {
			var v struct {
				Links []oltp.ChatIdentity `json:"links"`
			}
			remarshal(out, &v)
			p.Links = v.Links
		}
	}
	return &p
}

// mountChatBridge serves the section's two forms: bridge or detach,
// and close the channel to named people. Both are the repository's
// own admin's, and both go through the ops so the page refuses what
// the API refuses.
func mountChatBridge(mux *http.ServeMux, r *ops.Registry, capsPlane Caps, verify Verify, sessions Sessions) {
	back := func(w http.ResponseWriter, req *http.Request, repo string, err error) {
		to := "/r/" + repo + "/settings"
		if err != nil {
			to += "?" + url.Values{"cbe": {opRefusal(err)}}.Encode()
		} else {
			setFlash(w, req, "chat-bridge")
		}
		http.Redirect(w, req, to+"#chat-bridge", http.StatusSeeOther)
	}
	mux.HandleFunc("POST /r/{repo}/settings/chat-bridge", func(w http.ResponseWriter, req *http.Request) {
		repo := req.PathValue("repo")
		req, ok := gateCap(capsPlane, verify, sessions, repo, "a", ops.Admin, w, req)
		if !ok {
			return
		}
		form := url.Values{"repo": {repo}}
		if req.FormValue("do") == "detach" {
			form.Set("detach", "true")
		} else {
			form.Set("url", strings.TrimSpace(req.FormValue("url")))
			form.Set("user", strings.TrimSpace(req.FormValue("user")))
			form.Set("password", req.FormValue("password"))
		}
		_, err := invoke(req.Context(), r, "mirror.chatbridge", form)
		back(w, req, repo, err)
	})
	// Who is who: link a Fossil login to the account it is, or unlink
	// it. An administrator's statement, recorded as theirs.
	mux.HandleFunc("POST /r/{repo}/settings/chat-identity", func(w http.ResponseWriter, req *http.Request) {
		repo := req.PathValue("repo")
		req, ok := gateCap(capsPlane, verify, sessions, repo, "a", ops.Admin, w, req)
		if !ok {
			return
		}
		login := strings.TrimPrefix(strings.TrimSpace(req.FormValue("login")), "fossil:")
		_, err := invoke(req.Context(), r, "chat.identity.set", url.Values{"repo": {repo},
			"remote": {"fossil:" + login}, "account": {strings.TrimSpace(req.FormValue("account"))}})
		back(w, req, repo, err)
	})
	mux.HandleFunc("POST /r/{repo}/settings/chat-bridge/close", func(w http.ResponseWriter, req *http.Request) {
		repo := req.PathValue("repo")
		req, ok := gateCap(capsPlane, verify, sessions, repo, "a", ops.Admin, w, req)
		if !ok {
			return
		}
		admin, isAdmin := capsPlane.(CapsAdmin)
		if !isAdmin {
			back(w, req, repo, nil)
			return
		}
		// Chat leaves the two categories nobody is named into and
		// nothing else does: each keeps every other letter it held.
		for _, cat := range []string{"nobody", "anonymous"} {
			was := admin.CategoryCaps(repo, cat)
			if !oltp.CapSet(oltp.Closure(was)).HasAny("C") {
				continue
			}
			if _, err := invoke(req.Context(), r, "repo.category.set", url.Values{
				"repo": {repo}, "category": {cat}, "caps": {strings.ReplaceAll(was, "C", "")},
			}); err != nil {
				back(w, req, repo, err)
				return
			}
		}
		back(w, req, repo, nil)
	})
}