diff --git a/go.mod b/go.mod index 7f8861b..9763471 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module ircserve go 1.26.3 -require github.com/ergochat/irc-go v0.6.0 +require ( + github.com/ergochat/irc-go v0.6.0 + github.com/mattn/go-sqlite3 v1.14.48 +) diff --git a/go.sum b/go.sum index f1074c0..7e3a045 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ github.com/ergochat/irc-go v0.6.0 h1:Y0AGV76aeihJfCtLaQh+OyJKFiKGrYC0VTkeMZ6XW28= github.com/ergochat/irc-go v0.6.0/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0= +github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= +github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= diff --git a/main.go b/main.go index 8244080..15c8f47 100644 --- a/main.go +++ b/main.go @@ -171,19 +171,25 @@ func handleConn(conn net.Conn) { client.fwrite(":server 433 * %s :Nickname is already in use\r\n", newNick) } else { oldNick := client.nick + client.nick = newNick + + cliMu.Lock() if oldNick != "" { - // nick change - cliMu.Lock() delete(clients, oldNick) - clients[newNick] = client - cliMu.Unlock() - for _, ch := range client.channels { - ch.mu.Lock() + } + clients[newNick] = client + cliMu.Unlock() + for _, ch := range client.channels { + ch.mu.Lock() + if oldNick != "" { delete(ch.members, oldNick) - ch.members[newNick] = client - ch.mu.Unlock() + } else { + delete(ch.members, "") } - client.nick = newNick + ch.members[newNick] = client + ch.mu.Unlock() + } + if oldNick != "" { for _, ch := range client.channels { broadcastNames(ch) } @@ -192,15 +198,12 @@ func handleConn(conn net.Conn) { other.fwrite(":%s NICK %s\r\n", oldNick, newNick) } } - } else { - client.nick = newNick - cliMu.Lock() - clients[newNick] = client - cliMu.Unlock() } + client.checkRegistration() } } + case "CAP": if len(msg.Params) < 1 { client.fwrite(":server 461 %s CAP :Not enough parameters\r\n", client.nick) @@ -210,7 +213,7 @@ func handleConn(conn net.Conn) { switch subcmd { case "LS": // explicitly tell we only support PLAIN SASL, as well as chathistory - client.fwrite(":server CAP * LS :sasl=PLAIN chathistory\r\n") + client.fwrite(":server CAP * LS :sasl=PLAIN sasl chathistory\r\n") case "REQ": if len(msg.Params) >= 2 && strings.Contains(strings.ToLower(msg.Params[1]), "sasl") { @@ -335,6 +338,44 @@ func handleConn(conn net.Conn) { text := msg.Params[1] sendNotice(client, target, text) } + case "INVITE": + if !client.registered { + client.sendUnregisteredNotice() + continue + } + if len(msg.Params) < 2 { + client.fwrite(":server 461 %s INVITE :Not enough parameters\r\n", client.nick) + continue + } + targetNick := msg.Params[0] + channelName := normalizeChannelName(msg.Params[1]) + + cliMu.RLock() + target, ok := clients[targetNick] + cliMu.RUnlock() + if !ok { + client.fwrite(":server 401 %s %s :No such nick\r\n", client.nick, targetNick) + continue + } + + chMu.Lock() + ch, ok := channels[channelName] + chMu.Unlock() + if !ok { + client.fwrite(":server 403 %s %s :No such channel\r\n", client.nick, channelName) + continue + } + + ch.mu.RLock() + _, inChannel := ch.members[client.nick] + ch.mu.RUnlock() + if !inChannel { + client.fwrite(":server 442 %s %s :You're not on that channel\r\n", client.nick, channelName) + continue + } + client.fwrite(":server 341 %s %s %s\r\n", client.nick, targetNick, channelName) + target.fwrite(":%s INVITE %s :%s\r\n", client.nick, targetNick, channelName) + case "QUIT": if !client.registered { client.sendUnregisteredNotice() @@ -394,6 +435,7 @@ func handleConn(conn net.Conn) { } else { fmt.Fprintf(client.conn, ":server 461 %s NAMES :Not enough parameters\r\n", client.nick) } + // ircv3 stuff case "CHATHISTORY": if len(msg.Params) < 3 { client.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", client.nick) @@ -624,9 +666,10 @@ func handleMode(sender *Client, channelName, mode, targetNick string) { sender.fwrite(":server 501 %s :Unknown MODE flag\r\n", sender.nick) } } - func joinChannel(c *Client, name string) { name = normalizeChannelName(name) + + // get or create channel chMu.Lock() ch, ok := channels[name] if !ok { @@ -634,13 +677,9 @@ 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 - if globalOps[c.nick] { - ch.ops[c.nick] = true - } } if globalOps[c.nick] { ch.ops[c.nick] = true @@ -648,20 +687,47 @@ func joinChannel(c *Client, name string) { chMu.Unlock() ch.mu.Lock() + // reject duplicate join + if _, exists := ch.members[c.nick]; exists { + ch.mu.Unlock() + c.fwrite(":server 443 %s %s %s :is already on channel\r\n", + c.nick, c.nick, name) + return + } + // clean up stale empty nick + delete(ch.members, "") + // add new member ch.members[c.nick] = c c.channels[name] = ch ch.mu.Unlock() + + // echo JOIN to client + fmt.Fprintf(c.conn, ":%s JOIN %s\r\n", c.nick, name) + // broadcast JOIN to others for _, member := range ch.members { - fmt.Fprintf(member.conn, ":%s JOIN %s\r\n", c.nick, name) + if member != c { + fmt.Fprintf(member.conn, ":%s JOIN %s\r\n", c.nick, name) + } } + // topic + if ch.description != "" { + c.fwrite(":server 332 %s %s :%s\r\n", c.nick, name, ch.description) + } else { + c.fwrite(":server 331 %s %s :No topic is set\r\n", c.nick, name) + } + + // names list + c.sendNames(ch) + + // MODE if op 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) { chMu.Lock() ch, ok := channels[name] @@ -809,7 +875,7 @@ 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), + Prefix: fmt.Sprintf("%s!%s@%s", sender.nick, sender.user, "host"), // for privacy reasons Command: "PRIVMSG", Params: []string{target}, Text: text,