This commit is contained in:
meera 2026-07-19 01:57:38 +03:00
parent 330bd96cdf
commit c2551972a5

108
main.go
View File

@ -236,12 +236,14 @@ func handleConn(conn net.Conn) {
switch subcmd { switch subcmd {
case "LS": case "LS":
// explicitly tell we only support PLAIN SASL, as well as chathistory // explicitly tell we only support PLAIN SASL, as well as chathistory
client.fwrite(":server CAP * LS :sasl=PLAIN sasl chathistory\r\n") client.fwrite(":server CAP * LS :sasl=PLAIN sasl chathistory server-time message-tags draft/chathistory batch\r\n")
case "REQ": case "REQ":
if len(msg.Params) >= 2 && strings.Contains(strings.ToLower(msg.Params[1]), "sasl") { if len(msg.Params) >= 2 && strings.Contains(strings.ToLower(msg.Params[1]), "sasl") {
client.saslRequired = true client.saslRequired = true
client.fwrite(":server CAP * ACK :sasl\r\n") client.fwrite(":server CAP * ACK :sasl\r\n")
} else if len(msg.Params) >= 2 {
client.fwrite(":server CAP * ACK :%s\r\n", msg.Params[1])
} else { } else {
client.fwrite(":server CAP * NAK :%s\r\n", msg.Params[1]) client.fwrite(":server CAP * NAK :%s\r\n", msg.Params[1])
} }
@ -461,13 +463,17 @@ func handleConn(conn net.Conn) {
} }
// ircv3 stuff // ircv3 stuff
case "CHATHISTORY": case "CHATHISTORY":
if len(msg.Params) < 3 { if len(msg.Params) < 4 {
client.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", client.nick) client.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", client.nick)
continue continue
} }
subcmd := strings.ToUpper(msg.Params[0]) subcmd := strings.ToUpper(msg.Params[0])
target := normalizeChannelName(msg.Params[1]) target := historyTargetName(msg.Params[1])
count, _ := strconv.Atoi(msg.Params[2]) count, convErr := strconv.Atoi(msg.Params[3])
if convErr != nil || count <= 0 {
client.fwrite(":server 461 %s CHATHISTORY :Invalid message count\r\n", client.nick)
continue
}
client.serveHistory(subcmd, target, count, msg.Params) client.serveHistory(subcmd, target, count, msg.Params)
default: default:
if client.nick != "" { if client.nick != "" {
@ -478,6 +484,17 @@ func handleConn(conn net.Conn) {
} }
} }
} }
func historyTargetName(name string) string {
if strings.HasPrefix(name, "#") || strings.HasPrefix(name, "&") {
return name
}
if nicknameExists(name) {
return name
}
return name
}
func (c *Client) sendNames(ch *Channel) { func (c *Client) sendNames(ch *Channel) {
ch.mu.Lock() ch.mu.Lock()
defer ch.mu.Unlock() defer ch.mu.Unlock()
@ -496,24 +513,29 @@ func (c *Client) sendNames(ch *Channel) {
} }
func (c *Client) serveHistory(subcmd, target string, count int, params []string) { func (c *Client) serveHistory(subcmd, target string, count int, params []string) {
chMu.Lock() chMu.Lock()
ch, ok := channels[target] ch, isChannel := channels[target]
chMu.Unlock() chMu.Unlock()
if !ok {
c.fwrite(":server 403 %s %s :No such channel\r\n", c.nick, target)
return
}
ch.mu.RLock() var lockCh func()
defer ch.mu.RUnlock() var unlockCh func()
if isChannel {
lockCh = ch.mu.RLock
unlockCh = ch.mu.RUnlock
lockCh()
defer unlockCh()
}
var msgs []HistoryMessage var msgs []HistoryMessage
switch subcmd { switch subcmd {
case "LATEST": case "LATEST":
if count <= len(ch.history) { if isChannel && count <= len(ch.history) {
msgs = ch.history[len(ch.history)-count:] msgs = ch.history[len(ch.history)-count:]
} else { } else {
extra := count - len(ch.history) extra := count
if isChannel {
extra = count - len(ch.history)
}
rows, err := db.Query(`SELECT msgid, timestamp, prefix, command, msg rows, err := db.Query(`SELECT msgid, timestamp, prefix, command, msg
FROM messages FROM messages
WHERE channel=? WHERE channel=?
@ -525,7 +547,14 @@ func (c *Client) serveHistory(subcmd, target string, count int, params []string)
} }
defer rows.Close() defer rows.Close()
dbMsgs := scanRows(rows, target) dbMsgs := scanRows(rows, target)
msgs = append(dbMsgs, ch.history...) if isChannel {
msgs = append(dbMsgs, ch.history...)
} else {
for i, j := 0, len(dbMsgs)-1; i < j; i, j = i+1, j-1 {
dbMsgs[i], dbMsgs[j] = dbMsgs[j], dbMsgs[i]
}
msgs = dbMsgs
}
} }
case "BEFORE": case "BEFORE":
@ -570,7 +599,7 @@ func (c *Client) serveHistory(subcmd, target string, count int, params []string)
c.fwrite(":server 461 %s CHATHISTORY :Invalid timestamp\r\n", c.nick) c.fwrite(":server 461 %s CHATHISTORY :Invalid timestamp\r\n", c.nick)
return return
} }
rows, err = db.Query(`SELECT msgid, nick, timestamp, channel, prefix, command, msg rows, err = db.Query(`SELECT msgid, timestamp, prefix, command, msg
FROM messages FROM messages
WHERE channel=? AND timestamp > ? WHERE channel=? AND timestamp > ?
ORDER BY timestamp ASC LIMIT ?`, ORDER BY timestamp ASC LIMIT ?`,
@ -662,7 +691,7 @@ func kickUser(sender *Client, channelName, targetNick, reason string) {
ch.mu.Lock() ch.mu.Lock()
defer ch.mu.Unlock() defer ch.mu.Unlock()
if !ch.ops[sender.nick] { if !ch.ops[sender.nick] && !globalOps[sender.nick] {
sender.fwrite(":server 482 %s %s :You're not channel operator\r\n", sender.nick, channelName) sender.fwrite(":server 482 %s %s :You're not channel operator\r\n", sender.nick, channelName)
return return
} }
@ -696,7 +725,7 @@ func handleMode(sender *Client, channelName, mode, targetNick string) {
ch.mu.Lock() ch.mu.Lock()
defer ch.mu.Unlock() defer ch.mu.Unlock()
if !ch.ops[sender.nick] { if !ch.ops[sender.nick] && !globalOps[sender.nick] {
sender.fwrite(":server 482 %s %s :You're not channel operator\r\n", sender.nick, channelName) sender.fwrite(":server 482 %s %s :You're not channel operator\r\n", sender.nick, channelName)
return return
} }
@ -787,6 +816,7 @@ func joinChannel(c *Client, name string) {
} }
func (c *Client) partChannel(name string) { func (c *Client) partChannel(name string) {
name = normalizeChannelName(name)
chMu.Lock() chMu.Lock()
ch, ok := channels[name] ch, ok := channels[name]
chMu.Unlock() chMu.Unlock()
@ -801,7 +831,7 @@ func (c *Client) partChannel(name string) {
fmt.Fprintf(member.conn, ":%s PART %s\r\n", c.nick, name) fmt.Fprintf(member.conn, ":%s PART %s\r\n", c.nick, name)
} }
ch.mu.Unlock() ch.mu.Unlock()
// broadcastNames(ch) broadcastNames(ch)
} }
func addChannel(name string, description string) { func addChannel(name string, description string) {
name = normalizeChannelName(name) name = normalizeChannelName(name)
@ -818,11 +848,11 @@ func addChannel(name string, description string) {
} }
} }
func sendMessage(sender *Client, target, text string) { func sendMessage(sender *Client, target, text string) {
// TODO: handle when a user sends a message to a channel theyre not in
cliMu.Lock() cliMu.Lock()
if c, ok := clients[target]; ok { if c, ok := clients[target]; ok {
c.fwrite(":%s!%s@%s PRIVMSG %s :%s\r\n", sender.nick, sender.user, sender.host, target, text) c.fwrite(":%s!%s@%s PRIVMSG %s :%s\r\n", sender.nick, sender.user, sender.host, target, text)
cliMu.Unlock() cliMu.Unlock()
storeDirectMessage(sender, target, text)
return return
} }
cliMu.Unlock() cliMu.Unlock()
@ -830,6 +860,7 @@ func sendMessage(sender *Client, target, text string) {
ch, ok := channels[target] ch, ok := channels[target]
chMu.Unlock() chMu.Unlock()
if !ok { if !ok {
sender.fwrite(":server 401 %s %s :No such nick/channel\r\n", sender.nick, target)
return return
} }
ch.mu.RLock() ch.mu.RLock()
@ -851,7 +882,8 @@ func sendMessage(sender *Client, target, text string) {
} }
func (c *Client) checkRegistration() { func (c *Client) checkRegistration() {
if c.nick != "" && c.user != "" && !c.registered { if c.nick != "" && c.user != "" && !c.registered {
if c.saslRequired && !c.saslComplete { if !c.saslComplete {
c.fwrite(":server 464 %s :You must authenticate before registering\r\n", c.nick)
return return
} }
c.registered = true c.registered = true
@ -896,7 +928,7 @@ func (c *Client) performWhois(nick string) {
c.fwrite(":server 318 %s %s :End of WHOIS list\r\n", c.nick, nick) c.fwrite(":server 318 %s %s :End of WHOIS list\r\n", c.nick, nick)
} }
func (c *Client) sendUnregisteredNotice() { func (c *Client) sendUnregisteredNotice() {
fmt.Fprintf(c.conn, ":server 451 * :You have not registered\r\n") c.fwrite(":server 451 * :You have not registered\r\n")
} }
func showWho(c *Client, name string) { func showWho(c *Client, name string) {
name = normalizeChannelName(name) name = normalizeChannelName(name)
@ -909,8 +941,7 @@ func showWho(c *Client, name string) {
ch.mu.Lock() ch.mu.Lock()
defer ch.mu.Unlock() defer ch.mu.Unlock()
for _, member := range ch.members { for _, member := range ch.members {
fmt.Fprintf(c.conn, ":server 352 %s %s %s server %s H :0 %s\r\n", c.fwrite(":server 352 %s %s %s server %s H :0 %s\r\n", c.nick, name, member.host, member.nick, member.nick)
c.nick, name, member.host, member.nick, member.nick)
} }
fmt.Fprintf(c.conn, ":server 315 %s %s :End of WHO list\r\n", c.nick, name) fmt.Fprintf(c.conn, ":server 315 %s %s :End of WHO list\r\n", c.nick, name)
} }
@ -931,9 +962,9 @@ func (c *Client) handleList() {
} }
func storeMessage(ch *Channel, sender *Client, target, text string) { func storeMessage(ch *Channel, sender *Client, target, text string) {
msg := HistoryMessage{ msg := HistoryMessage{
MsgID: fmt.Sprintf("%d", time.Now().UnixNano()), MsgID: fmt.Sprintf("%019d", time.Now().UnixNano()),
Time: time.Now(), Time: time.Now(),
Prefix: fmt.Sprintf("%s!%s@%s", sender.nick, sender.user, "host"), // for privacy reasons Prefix: fmt.Sprintf("%s!%s@%s", sender.nick, sender.user, sender.host),
Command: "PRIVMSG", Command: "PRIVMSG",
Params: []string{target}, Params: []string{target},
Text: text, Text: text,
@ -942,10 +973,26 @@ func storeMessage(ch *Channel, sender *Client, target, text string) {
if len(ch.history) > 1000 { if len(ch.history) > 1000 {
ch.history = ch.history[1:] // drop oldest ch.history = ch.history[1:] // drop oldest
} }
persistMessage(msg, sender.nick, target)
}
func storeDirectMessage(sender *Client, target, text string) {
msg := HistoryMessage{
MsgID: fmt.Sprintf("%019d", 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,
}
persistMessage(msg, sender.nick, target)
}
func persistMessage(msg HistoryMessage, nick, target string) {
_, err := db.Exec(`INSERT INTO messages(msgid, nick, timestamp, channel, prefix, command, msg) _, err := db.Exec(`INSERT INTO messages(msgid, nick, timestamp, channel, prefix, command, msg)
VALUES(?, ?, ?, ?, ?, ?, ?)`, VALUES(?, ?, ?, ?, ?, ?, ?)`,
msg.MsgID, msg.MsgID,
sender.nick, nick,
msg.Time.UnixNano(), msg.Time.UnixNano(),
target, target,
msg.Prefix, msg.Prefix,
@ -953,7 +1000,7 @@ func storeMessage(ch *Channel, sender *Client, target, text string) {
msg.Text, msg.Text,
) )
if err != nil { if err != nil {
log.Printf("storeMessage error: %v", err) log.Printf("persistMessage error: %v", err)
} }
} }
func nicknameExists(nick string) bool { func nicknameExists(nick string) bool {
@ -969,13 +1016,12 @@ func (c *Client) cleanup() {
for name, ch := range c.channels { for name, ch := range c.channels {
ch.mu.Lock() ch.mu.Lock()
delete(ch.members, c.nick) delete(ch.members, c.nick)
ch.mu.Unlock()
for nick, member := range ch.members { for nick, member := range ch.members {
member.fwrite(":%s PART %s\r\n", c.nick, name) if nick != c.nick {
if member == c { member.fwrite(":%s PART %s\r\n", c.nick, name)
delete(ch.members, nick)
} }
} }
ch.mu.Unlock()
delete(c.channels, name) delete(c.channels, name)
} }
cliMu.RLock() cliMu.RLock()