diff --git a/internal/handlers/api.go b/internal/handlers/api.go index 74f9f9a..1d959b0 100644 --- a/internal/handlers/api.go +++ b/internal/handlers/api.go @@ -1636,6 +1636,32 @@ func (hdlr *Handlers) handleTopic( return } + isMember, err := hdlr.params.Database.IsChannelMember( + request.Context(), chID, sessionID, + ) + if err != nil { + hdlr.log.Error( + "check membership failed", "error", err, + ) + hdlr.respondError( + writer, request, + "internal error", + http.StatusInternalServerError, + ) + + return + } + + if !isMember { + hdlr.respondIRCError( + writer, request, clientID, sessionID, + irc.ErrNotOnChannel, nick, []string{channel}, + "You're not on that channel", + ) + + return + } + hdlr.executeTopic( writer, request, sessionID, clientID, nick, diff --git a/internal/handlers/api_test.go b/internal/handlers/api_test.go index e4da9cb..a482757 100644 --- a/internal/handlers/api_test.go +++ b/internal/handlers/api_test.go @@ -1134,6 +1134,42 @@ func TestTopicMissingBody(t *testing.T) { } } +func TestTopicNonMember(t *testing.T) { + tserver := newTestServer(t) + aliceToken := tserver.createSession("alice_topic") + bobToken := tserver.createSession("bob_topic") + + // Only alice joins the channel. + tserver.sendCommand(aliceToken, map[string]any{ + commandKey: joinCmd, toKey: "#topicpriv", + }) + + // Drain bob's initial messages. + _, lastID := tserver.pollMessages(bobToken, 0) + + // Bob tries to set topic without joining. + status, _ := tserver.sendCommand( + bobToken, + map[string]any{ + commandKey: "TOPIC", + toKey: "#topicpriv", + bodyKey: []string{"Hijacked topic"}, + }, + ) + if status != http.StatusOK { + t.Fatalf("expected 200, got %d", status) + } + + msgs, _ := tserver.pollMessages(bobToken, lastID) + + if !findNumeric(msgs, "442") { + t.Fatalf( + "expected ERR_NOTONCHANNEL (442), got %v", + msgs, + ) + } +} + func TestPing(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("ping_user")