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)
})
}