package main import ( "encoding/json" "fmt" "net/http" ) type ModeResponse struct { Action string `json:"action"` Mode string `json:"mode"` Target string `json:"target"` Channel string `json:"channel"` Status string `json:"status"` } type KickResponse struct { Action string `json:"action"` Target string `json:"target"` Channel string `json:"channel"` Reason string `json:"reason"` Status string `json:"status"` } func startHTTP(cfg Config) { htport := cfg.Webapiport http.HandleFunc("/admin/kick", handleKick) http.HandleFunc("/admin/mode", handleModeAPI) http.HandleFunc("/admin/list", handleListAPI) http.HandleFunc("/admin/reloadmotd", handleMOTDreload) fmt.Printf("[web] starting API on port %d\n", htport) http.ListenAndServe(fmt.Sprintf(":%d", htport), nil) } func handleKick(w http.ResponseWriter, r *http.Request) { user, pass, ok := r.BasicAuth() if !ok || !checkCredentials(user, pass) || !globalOps[user] { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } if r.Method != http.MethodPost { http.Error(w, "POST required", http.StatusMethodNotAllowed) return } channelName := normalizeChannelName(r.FormValue("channel")) targetNick := r.FormValue("nick") reason := r.FormValue("reason") if reason == "" { reason = "Kicked via API" } chMu.Lock() ch, ok := channels[channelName] chMu.Unlock() if !ok { http.Error(w, "No such channel", http.StatusNotFound) return } ch.mu.Lock() defer ch.mu.Unlock() target, ok := ch.members[targetNick] if !ok { http.Error(w, "Target not in channel", http.StatusNotFound) return } if globalOps[targetNick] { http.Error(w, "Cannot kick global operator", http.StatusForbidden) return } for _, member := range ch.members { member.fwrite(":server KICK %s %s :%s\r\n", channelName, targetNick, reason) } // Remove target delete(ch.members, targetNick) delete(target.channels, channelName) resp := KickResponse{ Action: "kick", Target: targetNick, Channel: channelName, Reason: reason, Status: "ok", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } func handleModeAPI(w http.ResponseWriter, r *http.Request) { user, pass, ok := r.BasicAuth() if !ok || !checkCredentials(user, pass) || !globalOps[user] { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } if r.Method != http.MethodPost { http.Error(w, "POST required", http.StatusMethodNotAllowed) return } channelName := normalizeChannelName(r.FormValue("channel")) mode := r.FormValue("mode") targetNick := r.FormValue("nick") chMu.Lock() ch, ok := channels[channelName] chMu.Unlock() if !ok { http.Error(w, "No such channel", http.StatusNotFound) return } ch.mu.Lock() defer ch.mu.Unlock() switch mode { case "+o": if targetNick != "" { ch.ops[targetNick] = true for _, member := range ch.members { member.fwrite(":api MODE %s +o %s\r\n", channelName, targetNick) } } case "-o": if targetNick != "" { if globalOps[targetNick] { http.Error(w, "Cannot remove global operator", http.StatusForbidden) return } delete(ch.ops, targetNick) for _, member := range ch.members { member.fwrite(":api MODE %s -o %s\r\n", channelName, targetNick) } } default: http.Error(w, "Unknown MODE flag", http.StatusBadRequest) return } resp := ModeResponse{ Action: "mode", Mode: mode, Target: targetNick, Channel: channelName, Status: "ok", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } type ChannelInfo struct { Name string `json:"name"` Description string `json:"description"` Users int `json:"users"` } func handleListAPI(w http.ResponseWriter, r *http.Request) { user, pass, ok := r.BasicAuth() if !ok || !checkCredentials(user, pass) || !globalOps[user] { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } chMu.Lock() defer chMu.Unlock() var list []ChannelInfo for _, ch := range channels { ch.mu.Lock() count := len(ch.members) desc := ch.description name := ch.name ch.mu.Unlock() list = append(list, ChannelInfo{ Name: name, Description: desc, Users: count, }) } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(list); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func handleMOTDreload(w http.ResponseWriter, r *http.Request) { user, pass, ok := r.BasicAuth() if !ok || !checkCredentials(user, pass) || !globalOps[user] { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } loadMOTD(motdFile) type htresp struct { Status string `json:"status"` } resp := htresp{ Status: "ok", } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(resp); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }