major: added CHATHISTORY, fixed bug in sendmessage
This commit is contained in:
parent
e49701af15
commit
44da1e17a6
50
http.go
50
http.go
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@ -12,6 +13,7 @@ type ModeResponse struct {
|
||||
Target string `json:"target"`
|
||||
Channel string `json:"channel"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
type KickResponse struct {
|
||||
Action string `json:"action"`
|
||||
@ -19,24 +21,33 @@ type KickResponse struct {
|
||||
Channel string `json:"channel"`
|
||||
Reason string `json:"reason"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
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)
|
||||
addAuthMiddleware := func(path string, next http.HandlerFunc) {
|
||||
http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
guardHandlerAuth(w, r, next)
|
||||
})
|
||||
}
|
||||
addAuthMiddleware("/admin/kick", handleKick)
|
||||
addAuthMiddleware("/admin/mode", handleModeAPI)
|
||||
addAuthMiddleware("/admin/list", handleListAPI)
|
||||
addAuthMiddleware("/admin/reloadmotd", handleMOTDreload)
|
||||
log.Printf("[web] starting API on port %d\n", htport)
|
||||
http.ListenAndServe(fmt.Sprintf(":%d", htport), nil)
|
||||
}
|
||||
func handleKick(w http.ResponseWriter, r *http.Request) {
|
||||
func guardHandlerAuth(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || !checkCredentials(user, pass) || !globalOps[user] {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="API"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
func handleKick(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@ -82,17 +93,12 @@ func handleKick(w http.ResponseWriter, r *http.Request) {
|
||||
Channel: channelName,
|
||||
Reason: reason,
|
||||
Status: "ok",
|
||||
Error: "",
|
||||
}
|
||||
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] {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="API"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@ -142,6 +148,7 @@ func handleModeAPI(w http.ResponseWriter, r *http.Request) {
|
||||
Target: targetNick,
|
||||
Channel: channelName,
|
||||
Status: "ok",
|
||||
Error: "",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
enc := json.NewEncoder(w)
|
||||
@ -156,12 +163,6 @@ type ChannelInfo struct {
|
||||
}
|
||||
|
||||
func handleListAPI(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || !checkCredentials(user, pass) || !globalOps[user] {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="API"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
chMu.Lock()
|
||||
defer chMu.Unlock()
|
||||
@ -189,18 +190,9 @@ func handleListAPI(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
func handleMOTDreload(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || !checkCredentials(user, pass) || !globalOps[user] {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="API"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
loadMOTD(motdFile)
|
||||
type htresp struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
resp := htresp{
|
||||
Status: "ok",
|
||||
resp := map[string]any{
|
||||
"status": "ok",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
|
||||
183
main.go
183
main.go
@ -14,6 +14,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@ -38,8 +39,18 @@ type Channel struct {
|
||||
description string
|
||||
members map[string]*Client
|
||||
ops map[string]bool
|
||||
history []HistoryMessage
|
||||
mu sync.RWMutex
|
||||
}
|
||||
type HistoryMessage struct {
|
||||
MsgID string
|
||||
Time time.Time
|
||||
Prefix string
|
||||
Command string
|
||||
Params []string
|
||||
Text string
|
||||
}
|
||||
|
||||
type Channelconfig struct {
|
||||
Channel string `json:"channel"`
|
||||
Description string `json:"description"`
|
||||
@ -162,6 +173,12 @@ func handleConn(conn net.Conn) {
|
||||
delete(clients, oldNick)
|
||||
clients[newNick] = client
|
||||
cliMu.Unlock()
|
||||
for _, ch := range client.channels {
|
||||
ch.mu.Lock()
|
||||
delete(ch.members, oldNick)
|
||||
ch.members[newNick] = client
|
||||
ch.mu.Unlock()
|
||||
}
|
||||
client.nick = newNick
|
||||
for _, ch := range client.channels {
|
||||
broadcastNames(ch)
|
||||
@ -188,8 +205,8 @@ func handleConn(conn net.Conn) {
|
||||
subcmd := strings.ToUpper(msg.Params[0])
|
||||
switch subcmd {
|
||||
case "LS":
|
||||
// explicitly tell we only support PLAIN SASL
|
||||
client.fwrite(":server CAP * LS :sasl=PLAIN\r\n")
|
||||
// explicitly tell we only support PLAIN SASL, as well as chathistory
|
||||
client.fwrite(":server CAP * LS :sasl=PLAIN chathistory\r\n")
|
||||
|
||||
case "REQ":
|
||||
if len(msg.Params) >= 2 && strings.Contains(strings.ToLower(msg.Params[1]), "sasl") {
|
||||
@ -373,6 +390,15 @@ func handleConn(conn net.Conn) {
|
||||
} else {
|
||||
fmt.Fprintf(client.conn, ":server 461 %s NAMES :Not enough parameters\r\n", client.nick)
|
||||
}
|
||||
case "CHATHISTORY":
|
||||
if len(msg.Params) < 3 {
|
||||
client.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", client.nick)
|
||||
continue
|
||||
}
|
||||
subcmd := strings.ToUpper(msg.Params[0])
|
||||
target := normalizeChannelName(msg.Params[1])
|
||||
count, _ := strconv.Atoi(msg.Params[2])
|
||||
client.serveHistory(subcmd, target, count, msg.Params)
|
||||
default:
|
||||
if client.nick != "" {
|
||||
fmt.Fprintf(conn, ":server 421 %s %s :Unknown command or command is not implemented\r\n", client.nick, msg.Command)
|
||||
@ -398,6 +424,110 @@ func (c *Client) sendNames(ch *Channel) {
|
||||
fmt.Fprintf(c.conn, ":server 353 %s = %s :%s\r\n", c.nick, ch.name, strings.Join(nicks, " "))
|
||||
fmt.Fprintf(c.conn, ":server 366 %s %s :End of /NAMES list.\r\n", c.nick, ch.name)
|
||||
}
|
||||
func (c *Client) serveHistory(subcmd, target string, count int, params []string) {
|
||||
chMu.Lock()
|
||||
ch, ok := channels[target]
|
||||
chMu.Unlock()
|
||||
if !ok {
|
||||
c.fwrite(":server 403 %s %s :No such channel\r\n", c.nick, target)
|
||||
return
|
||||
}
|
||||
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
var msgs []HistoryMessage
|
||||
|
||||
switch subcmd {
|
||||
case "LATEST":
|
||||
if count > len(ch.history) {
|
||||
count = len(ch.history)
|
||||
}
|
||||
msgs = ch.history[len(ch.history)-count:]
|
||||
|
||||
case "BEFORE":
|
||||
if len(params) < 4 {
|
||||
c.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", c.nick)
|
||||
return
|
||||
}
|
||||
msgid := params[2] // format: msgid=12345
|
||||
msgid = strings.TrimPrefix(msgid, "msgid=")
|
||||
idx := findMsgIndex(ch.history, msgid)
|
||||
if idx == -1 {
|
||||
c.fwrite(":server 718 %s %s :No history available\r\n", c.nick, target)
|
||||
return
|
||||
}
|
||||
start := idx - count
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
msgs = ch.history[start:idx]
|
||||
|
||||
case "AFTER":
|
||||
if len(params) < 4 {
|
||||
c.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", c.nick)
|
||||
return
|
||||
}
|
||||
msgid := strings.TrimPrefix(params[2], "msgid=")
|
||||
idx := findMsgIndex(ch.history, msgid)
|
||||
if idx == -1 {
|
||||
c.fwrite(":server 718 %s %s :No history available\r\n", c.nick, target)
|
||||
return
|
||||
}
|
||||
end := idx + 1 + count
|
||||
if end > len(ch.history) {
|
||||
end = len(ch.history)
|
||||
}
|
||||
msgs = ch.history[idx+1 : end]
|
||||
|
||||
case "AROUND":
|
||||
if len(params) < 4 {
|
||||
c.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", c.nick)
|
||||
return
|
||||
}
|
||||
msgid := strings.TrimPrefix(params[2], "msgid=")
|
||||
idx := findMsgIndex(ch.history, msgid)
|
||||
if idx == -1 {
|
||||
c.fwrite(":server 718 %s %s :No history available\r\n", c.nick, target)
|
||||
return
|
||||
}
|
||||
half := count / 2
|
||||
start := idx - half
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := idx + half + 1
|
||||
if end > len(ch.history) {
|
||||
end = len(ch.history)
|
||||
}
|
||||
msgs = ch.history[start:end]
|
||||
}
|
||||
batchID := fmt.Sprintf("hist-%d", time.Now().UnixNano())
|
||||
c.fwrite(":server BATCH +%s chathistory %s\r\n", batchID, target)
|
||||
|
||||
for _, m := range msgs {
|
||||
tags := fmt.Sprintf("@msgid=%s;time=%s",
|
||||
m.MsgID,
|
||||
m.Time.UTC().Format(time.RFC3339))
|
||||
c.fwrite("%s :%s %s %s :%s\r\n",
|
||||
tags,
|
||||
m.Prefix,
|
||||
m.Command,
|
||||
strings.Join(m.Params, " "),
|
||||
m.Text)
|
||||
}
|
||||
|
||||
c.fwrite(":server BATCH -%s\r\n", batchID)
|
||||
}
|
||||
func findMsgIndex(history []HistoryMessage, msgid string) int {
|
||||
for i, m := range history {
|
||||
if m.MsgID == msgid {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func kickUser(sender *Client, channelName, targetNick, reason string) {
|
||||
chMu.Lock()
|
||||
ch, ok := channels[channelName]
|
||||
@ -482,6 +612,7 @@ func joinChannel(c *Client, name string) {
|
||||
name: name,
|
||||
members: make(map[string]*Client),
|
||||
ops: make(map[string]bool),
|
||||
history: nil,
|
||||
description: "none",
|
||||
}
|
||||
channels[name] = ch
|
||||
@ -501,6 +632,12 @@ func joinChannel(c *Client, name string) {
|
||||
for _, member := range ch.members {
|
||||
fmt.Fprintf(member.conn, ":%s JOIN %s\r\n", c.nick, name)
|
||||
}
|
||||
|
||||
if ch.ops[c.nick] {
|
||||
for _, member := range ch.members {
|
||||
member.fwrite(":server MODE %s +o %s\r\n", name, c.nick)
|
||||
}
|
||||
}
|
||||
c.sendNames(ch)
|
||||
}
|
||||
func (c *Client) partChannel(name string) {
|
||||
@ -518,7 +655,7 @@ func (c *Client) partChannel(name string) {
|
||||
fmt.Fprintf(member.conn, ":%s PART %s\r\n", c.nick, name)
|
||||
}
|
||||
ch.mu.Unlock()
|
||||
broadcastNames(ch)
|
||||
// broadcastNames(ch)
|
||||
}
|
||||
func addChannel(name string, description string) {
|
||||
name = normalizeChannelName(name)
|
||||
@ -535,6 +672,7 @@ func addChannel(name string, description string) {
|
||||
}
|
||||
}
|
||||
func sendMessage(sender *Client, target, text string) {
|
||||
// TODO: handle when a user sends a message to a channel theyre not in
|
||||
cliMu.Lock()
|
||||
if c, ok := clients[target]; ok {
|
||||
c.fwrite(":%s!%s@%s PRIVMSG %s :%s\r\n", sender.nick, sender.user, sender.host, target, text)
|
||||
@ -548,6 +686,13 @@ func sendMessage(sender *Client, target, text string) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_, member := ch.members[sender.nick]
|
||||
ch.mu.RUnlock()
|
||||
if !member {
|
||||
sender.fwrite(":server 404 %s %s :Cannot send to channel\r\n",
|
||||
sender.nick, target)
|
||||
return
|
||||
}
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
for _, member := range ch.members {
|
||||
@ -555,9 +700,9 @@ func sendMessage(sender *Client, target, text string) {
|
||||
fmt.Fprintf(member.conn, ":%s!%s@%s PRIVMSG %s :%s\r\n", sender.nick, sender.user, sender.host, target, text)
|
||||
}
|
||||
}
|
||||
storeMessage(ch, sender, target, text)
|
||||
}
|
||||
func (c *Client) checkRegistration() {
|
||||
//log.Printf("checkRegistration called: nick=%q user=%q registered=%v", c.nick, c.user, c.registered)
|
||||
if c.nick != "" && c.user != "" && !c.registered {
|
||||
if c.saslRequired && !c.saslComplete {
|
||||
return
|
||||
@ -568,8 +713,12 @@ func (c *Client) checkRegistration() {
|
||||
c.fwrite(":server 003 %s :This server was created just now\r\n", c.nick)
|
||||
c.fwrite(":server 004 %s server irc 0.1\r\n", c.nick)
|
||||
fmt.Fprintf(c.conn, ":server 375 %s :- Message of the Day -\r\n", c.nick)
|
||||
for _, i := range motdString {
|
||||
c.fwrite(":server 372 %s :- %s\r\n", c.nick, i)
|
||||
if motdString != nil {
|
||||
for _, i := range motdString {
|
||||
c.fwrite(":server 372 %s :- %s\r\n", c.nick, i)
|
||||
}
|
||||
} else {
|
||||
c.fwrite("%s :MOTD File is missing", c.nick)
|
||||
}
|
||||
fmt.Fprintf(c.conn, ":server 376 %s :End of MOTD\r\n", c.nick)
|
||||
}
|
||||
@ -633,6 +782,20 @@ func (c *Client) handleList() {
|
||||
// End of list
|
||||
c.fwrite(":server 323 %s :End of /LIST\r\n", c.nick)
|
||||
}
|
||||
func storeMessage(ch *Channel, sender *Client, target, text string) {
|
||||
msg := HistoryMessage{
|
||||
MsgID: fmt.Sprintf("%d", time.Now().UnixNano()),
|
||||
Time: time.Now(),
|
||||
Prefix: fmt.Sprintf("%s!%s@%s", sender.nick, sender.user, sender.host),
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{target},
|
||||
Text: text,
|
||||
}
|
||||
ch.history = append(ch.history, msg)
|
||||
if len(ch.history) > 1000 {
|
||||
ch.history = ch.history[1:] // drop oldest
|
||||
}
|
||||
}
|
||||
func nicknameExists(nick string) bool {
|
||||
cliMu.RLock()
|
||||
defer cliMu.RUnlock()
|
||||
@ -646,11 +809,13 @@ func (c *Client) cleanup() {
|
||||
for name, ch := range c.channels {
|
||||
ch.mu.Lock()
|
||||
delete(ch.members, c.nick)
|
||||
for _, member := range ch.members {
|
||||
for nick, member := range ch.members {
|
||||
member.fwrite(":%s PART %s\r\n", c.nick, name)
|
||||
if member == c {
|
||||
delete(ch.members, nick)
|
||||
}
|
||||
}
|
||||
ch.mu.Unlock()
|
||||
broadcastNames(ch)
|
||||
delete(c.channels, name)
|
||||
}
|
||||
cliMu.RLock()
|
||||
@ -702,7 +867,7 @@ func normalizeChannelName(name string) string {
|
||||
func checkCredentials(user, pass string) bool {
|
||||
file, err := os.Open("accounts.txt")
|
||||
if err != nil {
|
||||
log.Printf("checkCredentials: open error: %v", err)
|
||||
log.Printf("open error: %v", err)
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user