major: added CHATHISTORY, fixed bug in sendmessage

This commit is contained in:
meera 2026-07-16 01:09:25 +03:00
parent e49701af15
commit 44da1e17a6
3 changed files with 196 additions and 39 deletions

50
http.go
View File

@ -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
View File

@ -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()

View File

@ -1,4 +1,4 @@
welcome to thinIRC
welcome to Gocirc!
plz follow the rules:
no discriminating by religion
no racial slurs allowed