Compare commits
2 Commits
main
...
f57a373053
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f57a373053 | ||
| 260f798af4 |
@@ -1,7 +1,7 @@
|
||||
// Package main is the entry point for the neoirc-cli client.
|
||||
package main
|
||||
|
||||
import "sneak.berlin/go/neoirc/internal/cli"
|
||||
import "git.eeqj.de/sneak/neoirc/internal/cli"
|
||||
|
||||
func main() {
|
||||
cli.Run()
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.eeqj.de/sneak/neoirc/internal/broker"
|
||||
"git.eeqj.de/sneak/neoirc/internal/config"
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/internal/globals"
|
||||
"git.eeqj.de/sneak/neoirc/internal/handlers"
|
||||
"git.eeqj.de/sneak/neoirc/internal/healthcheck"
|
||||
"git.eeqj.de/sneak/neoirc/internal/ircserver"
|
||||
"git.eeqj.de/sneak/neoirc/internal/logger"
|
||||
"git.eeqj.de/sneak/neoirc/internal/middleware"
|
||||
"git.eeqj.de/sneak/neoirc/internal/server"
|
||||
"git.eeqj.de/sneak/neoirc/internal/service"
|
||||
"git.eeqj.de/sneak/neoirc/internal/stats"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/neoirc/internal/broker"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"sneak.berlin/go/neoirc/internal/globals"
|
||||
"sneak.berlin/go/neoirc/internal/handlers"
|
||||
"sneak.berlin/go/neoirc/internal/healthcheck"
|
||||
"sneak.berlin/go/neoirc/internal/ircserver"
|
||||
"sneak.berlin/go/neoirc/internal/logger"
|
||||
"sneak.berlin/go/neoirc/internal/middleware"
|
||||
"sneak.berlin/go/neoirc/internal/server"
|
||||
"sneak.berlin/go/neoirc/internal/service"
|
||||
"sneak.berlin/go/neoirc/internal/stats"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
2
go.mod
2
go.mod
@@ -1,4 +1,4 @@
|
||||
module sneak.berlin/go/neoirc
|
||||
module git.eeqj.de/sneak/neoirc
|
||||
|
||||
go 1.24.0
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/broker"
|
||||
"git.eeqj.de/sneak/neoirc/internal/broker"
|
||||
)
|
||||
|
||||
func TestNewBroker(t *testing.T) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/neoirc/pkg/irc"
|
||||
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/hashcash"
|
||||
"git.eeqj.de/sneak/neoirc/internal/hashcash"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
api "sneak.berlin/go/neoirc/internal/cli/api"
|
||||
"sneak.berlin/go/neoirc/pkg/irc"
|
||||
api "git.eeqj.de/sneak/neoirc/internal/cli/api"
|
||||
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/globals"
|
||||
"git.eeqj.de/sneak/neoirc/internal/logger"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/neoirc/internal/globals"
|
||||
"sneak.berlin/go/neoirc/internal/logger"
|
||||
|
||||
_ "github.com/joho/godotenv/autoload" // loads .env file
|
||||
)
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/config"
|
||||
"git.eeqj.de/sneak/neoirc/internal/logger"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/logger"
|
||||
|
||||
_ "github.com/joho/godotenv/autoload" // .env
|
||||
_ "modernc.org/sqlite" // driver
|
||||
@@ -135,21 +135,13 @@ type migration struct {
|
||||
func (database *Database) runMigrations(
|
||||
ctx context.Context,
|
||||
) error {
|
||||
bootstrap, err := SchemaFiles.ReadFile(
|
||||
"schema/000.sql",
|
||||
)
|
||||
_, err := database.conn.ExecContext(ctx,
|
||||
`CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP)`)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"read bootstrap migration: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
_, err = database.conn.ExecContext(
|
||||
ctx, string(bootstrap),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"execute bootstrap migration: %w", err,
|
||||
"create schema_migrations: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -278,11 +270,6 @@ func (database *Database) loadMigrations() (
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip bootstrap migration; it is executed separately.
|
||||
if version == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
content, readErr := SchemaFiles.ReadFile(
|
||||
"schema/" + entry.Name(),
|
||||
)
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
||||
"github.com/google/uuid"
|
||||
"sneak.berlin/go/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
-- Bootstrap: create the schema_migrations table itself.
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
INSERT OR IGNORE INTO schema_migrations (version) VALUES (0);
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/internal/hashcash"
|
||||
"git.eeqj.de/sneak/neoirc/internal/service"
|
||||
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"sneak.berlin/go/neoirc/internal/hashcash"
|
||||
"sneak.berlin/go/neoirc/internal/service"
|
||||
"sneak.berlin/go/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -18,21 +18,21 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/broker"
|
||||
"git.eeqj.de/sneak/neoirc/internal/config"
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/internal/globals"
|
||||
"git.eeqj.de/sneak/neoirc/internal/handlers"
|
||||
"git.eeqj.de/sneak/neoirc/internal/hashcash"
|
||||
"git.eeqj.de/sneak/neoirc/internal/healthcheck"
|
||||
"git.eeqj.de/sneak/neoirc/internal/logger"
|
||||
"git.eeqj.de/sneak/neoirc/internal/middleware"
|
||||
"git.eeqj.de/sneak/neoirc/internal/server"
|
||||
"git.eeqj.de/sneak/neoirc/internal/service"
|
||||
"git.eeqj.de/sneak/neoirc/internal/stats"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/fx/fxtest"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"sneak.berlin/go/neoirc/internal/broker"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"sneak.berlin/go/neoirc/internal/globals"
|
||||
"sneak.berlin/go/neoirc/internal/handlers"
|
||||
"sneak.berlin/go/neoirc/internal/hashcash"
|
||||
"sneak.berlin/go/neoirc/internal/healthcheck"
|
||||
"sneak.berlin/go/neoirc/internal/logger"
|
||||
"sneak.berlin/go/neoirc/internal/middleware"
|
||||
"sneak.berlin/go/neoirc/internal/server"
|
||||
"sneak.berlin/go/neoirc/internal/service"
|
||||
"sneak.berlin/go/neoirc/internal/stats"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"sneak.berlin/go/neoirc/pkg/irc"
|
||||
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
const minPasswordLength = 8
|
||||
|
||||
@@ -9,17 +9,17 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/broker"
|
||||
"git.eeqj.de/sneak/neoirc/internal/config"
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/internal/globals"
|
||||
"git.eeqj.de/sneak/neoirc/internal/hashcash"
|
||||
"git.eeqj.de/sneak/neoirc/internal/healthcheck"
|
||||
"git.eeqj.de/sneak/neoirc/internal/logger"
|
||||
"git.eeqj.de/sneak/neoirc/internal/ratelimit"
|
||||
"git.eeqj.de/sneak/neoirc/internal/service"
|
||||
"git.eeqj.de/sneak/neoirc/internal/stats"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/neoirc/internal/broker"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"sneak.berlin/go/neoirc/internal/globals"
|
||||
"sneak.berlin/go/neoirc/internal/hashcash"
|
||||
"sneak.berlin/go/neoirc/internal/healthcheck"
|
||||
"sneak.berlin/go/neoirc/internal/logger"
|
||||
"sneak.berlin/go/neoirc/internal/ratelimit"
|
||||
"sneak.berlin/go/neoirc/internal/service"
|
||||
"sneak.berlin/go/neoirc/internal/stats"
|
||||
)
|
||||
|
||||
var errUnauthorized = errors.New("unauthorized")
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/hashcash"
|
||||
"git.eeqj.de/sneak/neoirc/internal/hashcash"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/hashcash"
|
||||
"git.eeqj.de/sneak/neoirc/internal/hashcash"
|
||||
)
|
||||
|
||||
const testBits = 2
|
||||
|
||||
@@ -6,12 +6,12 @@ import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/config"
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/internal/globals"
|
||||
"git.eeqj.de/sneak/neoirc/internal/logger"
|
||||
"git.eeqj.de/sneak/neoirc/internal/stats"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"sneak.berlin/go/neoirc/internal/globals"
|
||||
"sneak.berlin/go/neoirc/internal/logger"
|
||||
"sneak.berlin/go/neoirc/internal/stats"
|
||||
)
|
||||
|
||||
// Params defines the dependencies for creating a Healthcheck.
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/service"
|
||||
"sneak.berlin/go/neoirc/pkg/irc"
|
||||
"git.eeqj.de/sneak/neoirc/internal/service"
|
||||
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
// sendIRCError maps a service.IRCError to an IRC numeric
|
||||
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/broker"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"sneak.berlin/go/neoirc/internal/service"
|
||||
"sneak.berlin/go/neoirc/pkg/irc"
|
||||
"git.eeqj.de/sneak/neoirc/internal/broker"
|
||||
"git.eeqj.de/sneak/neoirc/internal/config"
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/internal/service"
|
||||
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"log/slog"
|
||||
"net"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/broker"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"sneak.berlin/go/neoirc/internal/service"
|
||||
"git.eeqj.de/sneak/neoirc/internal/broker"
|
||||
"git.eeqj.de/sneak/neoirc/internal/config"
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/internal/service"
|
||||
)
|
||||
|
||||
// NewTestServer creates a Server suitable for testing.
|
||||
|
||||
@@ -1,913 +0,0 @@
|
||||
package ircserver_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestIntegrationTwoClients is a comprehensive integration
|
||||
// test that spawns the IRC server programmatically, connects
|
||||
// two real TCP clients, and verifies all major IRC features
|
||||
// including cross-client message delivery.
|
||||
//
|
||||
// The test runs sequentially through IRC features because
|
||||
// both clients share the same channel state. Each section
|
||||
// builds on the previous one (e.g. alice and bob must be
|
||||
// JOINed before PRIVMSG can be tested).
|
||||
//
|
||||
//nolint:cyclop,funlen,maintidx // integration test
|
||||
func TestIntegrationTwoClients(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
alice := env.dial(t)
|
||||
bob := env.dial(t)
|
||||
|
||||
// ── Registration ──────────────────────────────────
|
||||
|
||||
aliceWelcome := alice.register("alice")
|
||||
assertContains(
|
||||
t, aliceWelcome, " 001 ", "RPL_WELCOME alice",
|
||||
)
|
||||
assertContains(
|
||||
t, aliceWelcome, " 002 ", "RPL_YOURHOST alice",
|
||||
)
|
||||
assertContains(
|
||||
t, aliceWelcome, " 003 ", "RPL_CREATED alice",
|
||||
)
|
||||
assertContains(
|
||||
t, aliceWelcome, " 004 ", "RPL_MYINFO alice",
|
||||
)
|
||||
assertContains(
|
||||
t, aliceWelcome, "alice",
|
||||
"nick in welcome burst",
|
||||
)
|
||||
|
||||
bobWelcome := bob.register("bob")
|
||||
assertContains(
|
||||
t, bobWelcome, " 001 ", "RPL_WELCOME bob",
|
||||
)
|
||||
assertContains(
|
||||
t, bobWelcome, "bob",
|
||||
"nick in welcome burst",
|
||||
)
|
||||
|
||||
// ── JOIN and cross-client visibility ──────────────
|
||||
|
||||
alice.send("JOIN #integration")
|
||||
aliceJoinLines := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 366 ")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceJoinLines, "JOIN",
|
||||
"alice receives JOIN echo",
|
||||
)
|
||||
assertContains(
|
||||
t, aliceJoinLines, " 366 ",
|
||||
"RPL_ENDOFNAMES for alice",
|
||||
)
|
||||
|
||||
bob.send("JOIN #integration")
|
||||
bobJoinLines := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 366 ")
|
||||
})
|
||||
assertContains(
|
||||
t, bobJoinLines, "JOIN",
|
||||
"bob receives JOIN echo",
|
||||
)
|
||||
|
||||
// Alice should see bob's JOIN via relay.
|
||||
aliceSeesBob := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "JOIN") &&
|
||||
strings.Contains(l, "bob")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceSeesBob, "bob",
|
||||
"alice sees bob's JOIN",
|
||||
)
|
||||
|
||||
// ── PRIVMSG (channel) — alice to bob ──────────────
|
||||
|
||||
alice.send("PRIVMSG #integration :hello from alice")
|
||||
|
||||
bobGetsMsg := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "hello from alice")
|
||||
})
|
||||
assertContains(
|
||||
t, bobGetsMsg, "hello from alice",
|
||||
"bob receives alice's channel message",
|
||||
)
|
||||
|
||||
// ── PRIVMSG (channel) — bob to alice ──────────────
|
||||
|
||||
bob.send("PRIVMSG #integration :hello from bob")
|
||||
|
||||
aliceGetsMsg := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "hello from bob")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceGetsMsg, "hello from bob",
|
||||
"alice receives bob's channel message",
|
||||
)
|
||||
|
||||
// ── PRIVMSG (DM) — alice to bob ──────────────────
|
||||
|
||||
alice.send("PRIVMSG bob :secret message")
|
||||
|
||||
bobDM := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "secret message")
|
||||
})
|
||||
assertContains(
|
||||
t, bobDM, "secret message",
|
||||
"bob receives alice's DM",
|
||||
)
|
||||
assertContains(
|
||||
t, bobDM, "alice",
|
||||
"DM from field is alice",
|
||||
)
|
||||
|
||||
// ── PRIVMSG (DM) — bob to alice ──────────────────
|
||||
|
||||
bob.send("PRIVMSG alice :reply to you")
|
||||
|
||||
aliceDM := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "reply to you")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceDM, "reply to you",
|
||||
"alice receives bob's DM",
|
||||
)
|
||||
|
||||
// ── NOTICE (channel) ──────────────────────────────
|
||||
|
||||
alice.send("NOTICE #integration :notice msg")
|
||||
|
||||
bobNotice := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "notice msg")
|
||||
})
|
||||
assertContains(
|
||||
t, bobNotice, "NOTICE",
|
||||
"bob receives NOTICE command",
|
||||
)
|
||||
assertContains(
|
||||
t, bobNotice, "notice msg",
|
||||
"bob receives NOTICE text",
|
||||
)
|
||||
|
||||
// ── NOTICE (DM) ──────────────────────────────────
|
||||
|
||||
bob.send("NOTICE alice :dm notice")
|
||||
|
||||
aliceNotice := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "dm notice")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceNotice, "dm notice",
|
||||
"alice receives DM NOTICE",
|
||||
)
|
||||
|
||||
// ── TOPIC ─────────────────────────────────────────
|
||||
// alice is the channel creator so she is +o.
|
||||
|
||||
alice.send("TOPIC #integration :Integration Test Topic")
|
||||
|
||||
aliceTopic := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(
|
||||
l, "Integration Test Topic",
|
||||
)
|
||||
})
|
||||
assertContains(
|
||||
t, aliceTopic, "Integration Test Topic",
|
||||
"alice sees TOPIC echo",
|
||||
)
|
||||
|
||||
bobTopic := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(
|
||||
l, "Integration Test Topic",
|
||||
)
|
||||
})
|
||||
assertContains(
|
||||
t, bobTopic, "Integration Test Topic",
|
||||
"bob receives TOPIC change",
|
||||
)
|
||||
|
||||
// ── MODE (query) ──────────────────────────────────
|
||||
|
||||
alice.send("MODE #integration")
|
||||
|
||||
aliceMode := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 324 ")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceMode, " 324 ",
|
||||
"RPL_CHANNELMODEIS",
|
||||
)
|
||||
|
||||
// ── MODE (+m moderated, then -m) ──────────────────
|
||||
|
||||
alice.send("MODE #integration +m")
|
||||
|
||||
aliceModeM := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "MODE") &&
|
||||
strings.Contains(l, "+m")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceModeM, "+m",
|
||||
"alice sees MODE +m echo",
|
||||
)
|
||||
|
||||
bobModeM := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "+m")
|
||||
})
|
||||
assertContains(
|
||||
t, bobModeM, "+m",
|
||||
"bob sees MODE +m relay",
|
||||
)
|
||||
|
||||
// Revert moderated mode.
|
||||
alice.send("MODE #integration -m")
|
||||
alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "-m")
|
||||
})
|
||||
bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "-m")
|
||||
})
|
||||
|
||||
// ── MODE (+v voice, then -v) ──────────────────────
|
||||
|
||||
alice.send("MODE #integration +v bob")
|
||||
|
||||
aliceVoice := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "+v")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceVoice, "+v",
|
||||
"alice sees +v echo",
|
||||
)
|
||||
|
||||
bobVoice := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "+v")
|
||||
})
|
||||
assertContains(
|
||||
t, bobVoice, "+v",
|
||||
"bob receives +v relay",
|
||||
)
|
||||
|
||||
// Remove voice.
|
||||
alice.send("MODE #integration -v bob")
|
||||
alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "-v")
|
||||
})
|
||||
bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "-v")
|
||||
})
|
||||
|
||||
// ── NAMES ─────────────────────────────────────────
|
||||
|
||||
alice.send("NAMES #integration")
|
||||
|
||||
aliceNames := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 366 ")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceNames, " 353 ",
|
||||
"RPL_NAMREPLY",
|
||||
)
|
||||
assertContains(
|
||||
t, aliceNames, " 366 ",
|
||||
"RPL_ENDOFNAMES",
|
||||
)
|
||||
|
||||
// Both nicks should appear in the name list.
|
||||
foundBothNames := false
|
||||
for _, line := range aliceNames {
|
||||
if strings.Contains(line, " 353 ") &&
|
||||
strings.Contains(line, "alice") &&
|
||||
strings.Contains(line, "bob") {
|
||||
foundBothNames = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundBothNames {
|
||||
t.Error("NAMES reply should list both alice and bob")
|
||||
}
|
||||
|
||||
// ── LIST ──────────────────────────────────────────
|
||||
|
||||
alice.send("LIST")
|
||||
|
||||
aliceList := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 323 ")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceList, " 322 ",
|
||||
"RPL_LIST entry",
|
||||
)
|
||||
assertContains(
|
||||
t, aliceList, "#integration",
|
||||
"LIST includes #integration",
|
||||
)
|
||||
assertContains(
|
||||
t, aliceList, " 323 ", //nolint:misspell // IRC RPL_LISTEND
|
||||
"RPL_LISTEND", //nolint:misspell // IRC term
|
||||
)
|
||||
|
||||
// ── WHO ───────────────────────────────────────────
|
||||
|
||||
bob.send("WHO #integration")
|
||||
|
||||
bobWho := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 315 ")
|
||||
})
|
||||
assertContains(
|
||||
t, bobWho, " 352 ",
|
||||
"RPL_WHOREPLY",
|
||||
)
|
||||
assertContains(
|
||||
t, bobWho, " 315 ",
|
||||
"RPL_ENDOFWHO",
|
||||
)
|
||||
|
||||
// ── WHOIS ─────────────────────────────────────────
|
||||
|
||||
alice.send("WHOIS bob")
|
||||
|
||||
aliceWhois := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 318 ")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceWhois, " 311 ",
|
||||
"RPL_WHOISUSER",
|
||||
)
|
||||
assertContains(
|
||||
t, aliceWhois, " 312 ",
|
||||
"RPL_WHOISSERVER",
|
||||
)
|
||||
assertContains(
|
||||
t, aliceWhois, " 318 ",
|
||||
"RPL_ENDOFWHOIS",
|
||||
)
|
||||
|
||||
// ── WHOIS with channels ───────────────────────────
|
||||
|
||||
bob.send("WHOIS alice")
|
||||
|
||||
bobWhois := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 318 ")
|
||||
})
|
||||
assertContains(
|
||||
t, bobWhois, " 319 ",
|
||||
"RPL_WHOISCHANNELS",
|
||||
)
|
||||
assertContains(
|
||||
t, bobWhois, "#integration",
|
||||
"WHOIS shows #integration channel",
|
||||
)
|
||||
|
||||
// ── LUSERS ────────────────────────────────────────
|
||||
|
||||
alice.send("LUSERS")
|
||||
|
||||
aliceLusers := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 255 ")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceLusers, " 251 ",
|
||||
"RPL_LUSERCLIENT",
|
||||
)
|
||||
assertContains(
|
||||
t, aliceLusers, " 255 ",
|
||||
"RPL_LUSERME",
|
||||
)
|
||||
|
||||
// ── NICK change ───────────────────────────────────
|
||||
|
||||
bob.send("NICK bobby")
|
||||
|
||||
bobNick := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "NICK") &&
|
||||
strings.Contains(l, "bobby")
|
||||
})
|
||||
assertContains(
|
||||
t, bobNick, "bobby",
|
||||
"bob sees NICK change to bobby",
|
||||
)
|
||||
|
||||
// alice should see the nick change relayed.
|
||||
aliceNick := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "bobby")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceNick, "NICK",
|
||||
"alice sees NICK command",
|
||||
)
|
||||
assertContains(
|
||||
t, aliceNick, "bobby",
|
||||
"alice sees new nick bobby",
|
||||
)
|
||||
|
||||
// Change it back for remaining tests.
|
||||
bob.send("NICK bob")
|
||||
bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "bob")
|
||||
})
|
||||
alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "NICK") &&
|
||||
strings.Contains(l, "bob")
|
||||
})
|
||||
|
||||
// ── Duplicate NICK ────────────────────────────────
|
||||
|
||||
bob.send("NICK alice")
|
||||
|
||||
bobDupNick := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 433 ")
|
||||
})
|
||||
assertContains(
|
||||
t, bobDupNick, " 433 ",
|
||||
"ERR_NICKNAMEINUSE",
|
||||
)
|
||||
|
||||
// ── KICK ──────────────────────────────────────────
|
||||
// alice is op; she kicks bob.
|
||||
|
||||
alice.send("KICK #integration bob :testing kick")
|
||||
|
||||
aliceKick := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "KICK")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceKick, "KICK",
|
||||
"alice sees KICK echo",
|
||||
)
|
||||
assertContains(
|
||||
t, aliceKick, "bob",
|
||||
"KICK mentions bob",
|
||||
)
|
||||
|
||||
bobKick := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "KICK")
|
||||
})
|
||||
assertContains(
|
||||
t, bobKick, "KICK",
|
||||
"bob receives KICK",
|
||||
)
|
||||
assertContains(
|
||||
t, bobKick, "testing kick",
|
||||
"KICK reason is relayed",
|
||||
)
|
||||
|
||||
// bob rejoins.
|
||||
bob.joinAndDrain("#integration")
|
||||
|
||||
// Drain alice's view of the rejoin.
|
||||
alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "JOIN") &&
|
||||
strings.Contains(l, "bob")
|
||||
})
|
||||
|
||||
// ── KICK non-op should fail ───────────────────────
|
||||
|
||||
bob.send("KICK #integration alice :nope")
|
||||
|
||||
bobKickFail := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 482 ")
|
||||
})
|
||||
assertContains(
|
||||
t, bobKickFail, " 482 ",
|
||||
"ERR_CHANOPRIVSNEEDED",
|
||||
)
|
||||
|
||||
// ── TOPIC lock (+t default) ───────────────────────
|
||||
// +t is default, so bob should not be able to set
|
||||
// topic.
|
||||
|
||||
bob.send("TOPIC #integration :bob tries topic")
|
||||
|
||||
bobTopicFail := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 482 ")
|
||||
})
|
||||
assertContains(
|
||||
t, bobTopicFail, " 482 ",
|
||||
"ERR_CHANOPRIVSNEEDED for topic",
|
||||
)
|
||||
|
||||
// ── PING / PONG ───────────────────────────────────
|
||||
|
||||
alice.send("PING :testtoken")
|
||||
|
||||
alicePong := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "PONG")
|
||||
})
|
||||
assertContains(
|
||||
t, alicePong, "PONG",
|
||||
"PONG response received",
|
||||
)
|
||||
|
||||
// ── Unknown command ───────────────────────────────
|
||||
|
||||
bob.send("FOOBAR")
|
||||
|
||||
bobUnknown := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 421 ")
|
||||
})
|
||||
assertContains(
|
||||
t, bobUnknown, " 421 ",
|
||||
"ERR_UNKNOWNCOMMAND",
|
||||
)
|
||||
|
||||
// ── MOTD ──────────────────────────────────────────
|
||||
|
||||
alice.send("MOTD")
|
||||
|
||||
aliceMOTD := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 376 ")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceMOTD, " 376 ",
|
||||
"RPL_ENDOFMOTD",
|
||||
)
|
||||
|
||||
// ── AWAY (set, check via DM, clear) ───────────────
|
||||
|
||||
alice.send("AWAY :gone fishing")
|
||||
|
||||
aliceAway := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 306 ")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceAway, " 306 ",
|
||||
"RPL_NOWAWAY",
|
||||
)
|
||||
|
||||
// bob DMs alice — should get RPL_AWAY.
|
||||
bob.send("PRIVMSG alice :are you there?")
|
||||
|
||||
bobAwayReply := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 301 ")
|
||||
})
|
||||
assertContains(
|
||||
t, bobAwayReply, " 301 ",
|
||||
"RPL_AWAY for bob when messaging alice",
|
||||
)
|
||||
assertContains(
|
||||
t, bobAwayReply, "gone fishing",
|
||||
"away message relayed",
|
||||
)
|
||||
|
||||
// Clear away.
|
||||
alice.send("AWAY")
|
||||
alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 305 ")
|
||||
})
|
||||
|
||||
// ── PASS (set password post-registration) ─────────
|
||||
|
||||
alice.send("PASS :mypassword123")
|
||||
|
||||
alicePass := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "Password set")
|
||||
})
|
||||
assertContains(
|
||||
t, alicePass, "Password set",
|
||||
"password set confirmation",
|
||||
)
|
||||
|
||||
// ── MODE -t/+t topic lock toggle ──────────────────
|
||||
|
||||
alice.send("MODE #integration -t")
|
||||
alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "-t")
|
||||
})
|
||||
bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "-t")
|
||||
})
|
||||
|
||||
// Now bob should be able to set topic.
|
||||
bob.send("TOPIC #integration :bob sets topic now")
|
||||
|
||||
bobTopicOK := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "bob sets topic now")
|
||||
})
|
||||
assertContains(
|
||||
t, bobTopicOK, "bob sets topic now",
|
||||
"bob can set topic after -t",
|
||||
)
|
||||
|
||||
// alice sees the topic change.
|
||||
aliceTopicRelay := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "bob sets topic now")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceTopicRelay, "bob sets topic now",
|
||||
"alice sees bob's topic after -t",
|
||||
)
|
||||
|
||||
// Restore +t.
|
||||
alice.send("MODE #integration +t")
|
||||
alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "+t")
|
||||
})
|
||||
bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "+t")
|
||||
})
|
||||
|
||||
// ── DM to nonexistent nick ────────────────────────
|
||||
|
||||
alice.send("PRIVMSG nobody123 :hello")
|
||||
|
||||
aliceNoSuch := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 401 ")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceNoSuch, " 401 ",
|
||||
"ERR_NOSUCHNICK",
|
||||
)
|
||||
|
||||
// ── PART with reason ──────────────────────────────
|
||||
|
||||
bob.send("PART #integration :bye for now")
|
||||
|
||||
bobPart := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "PART")
|
||||
})
|
||||
assertContains(
|
||||
t, bobPart, "PART",
|
||||
"bob sees PART echo",
|
||||
)
|
||||
|
||||
// alice sees bob PART via relay.
|
||||
alicePart := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "PART") &&
|
||||
strings.Contains(l, "bob")
|
||||
})
|
||||
assertContains(
|
||||
t, alicePart, "bob",
|
||||
"alice sees bob's PART",
|
||||
)
|
||||
assertContains(
|
||||
t, alicePart, "bye for now",
|
||||
"PART reason is relayed",
|
||||
)
|
||||
|
||||
// bob rejoins for remaining tests.
|
||||
bob.joinAndDrain("#integration")
|
||||
alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "JOIN") &&
|
||||
strings.Contains(l, "bob")
|
||||
})
|
||||
|
||||
// ── PART non-existent channel ─────────────────────
|
||||
|
||||
bob.send("PART #nonexistent")
|
||||
|
||||
bobPartFail := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 403 ") ||
|
||||
strings.Contains(l, " 442 ")
|
||||
})
|
||||
|
||||
foundPartErr := false
|
||||
for _, line := range bobPartFail {
|
||||
if strings.Contains(line, " 403 ") ||
|
||||
strings.Contains(line, " 442 ") {
|
||||
foundPartErr = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundPartErr {
|
||||
t.Error(
|
||||
"expected ERR_NOSUCHCHANNEL or " +
|
||||
"ERR_NOTONCHANNEL",
|
||||
)
|
||||
}
|
||||
|
||||
// ── User MODE query ───────────────────────────────
|
||||
|
||||
alice.send("MODE alice")
|
||||
|
||||
aliceUMode := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 221 ")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceUMode, " 221 ",
|
||||
"RPL_UMODEIS",
|
||||
)
|
||||
|
||||
// ── Multiple channel operation ────────────────────
|
||||
|
||||
alice.send("JOIN #second")
|
||||
alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 366 ")
|
||||
})
|
||||
|
||||
bob.send("JOIN #second")
|
||||
bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 366 ")
|
||||
})
|
||||
|
||||
// Drain alice seeing bob join.
|
||||
alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "JOIN") &&
|
||||
strings.Contains(l, "bob")
|
||||
})
|
||||
|
||||
alice.send("PRIVMSG #second :cross-channel test")
|
||||
|
||||
bobCross := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "cross-channel test")
|
||||
})
|
||||
assertContains(
|
||||
t, bobCross, "cross-channel test",
|
||||
"bob receives message in #second",
|
||||
)
|
||||
|
||||
// Clean up #second.
|
||||
alice.send("PART #second")
|
||||
alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "PART")
|
||||
})
|
||||
bob.send("PART #second")
|
||||
bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "PART")
|
||||
})
|
||||
|
||||
// ── QUIT ──────────────────────────────────────────
|
||||
|
||||
bob.send("QUIT :integration test done")
|
||||
|
||||
bobQuit := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "ERROR")
|
||||
})
|
||||
assertContains(
|
||||
t, bobQuit, "integration test done",
|
||||
"QUIT reason echoed",
|
||||
)
|
||||
|
||||
// alice should see bob's QUIT via relay.
|
||||
aliceQuit := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "QUIT") &&
|
||||
strings.Contains(l, "bob")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceQuit, "bob",
|
||||
"alice sees bob's QUIT",
|
||||
)
|
||||
}
|
||||
|
||||
// TestIntegrationModeSecret tests +s (secret) channel
|
||||
// mode — verifies that +s can be set and the mode is
|
||||
// reflected in MODE queries.
|
||||
func TestIntegrationModeSecret(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
alice := env.dial(t)
|
||||
alice.register("alice")
|
||||
|
||||
alice.joinAndDrain("#secretroom")
|
||||
|
||||
// Set +s.
|
||||
alice.send("MODE #secretroom +s")
|
||||
aliceLines := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "+s")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceLines, "+s",
|
||||
"alice sees MODE +s confirmation",
|
||||
)
|
||||
|
||||
// Verify mode is reflected in query.
|
||||
alice.send("MODE #secretroom")
|
||||
modeLines := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 324 ")
|
||||
})
|
||||
assertContains(
|
||||
t, modeLines, "s",
|
||||
"channel mode includes s",
|
||||
)
|
||||
}
|
||||
|
||||
// TestIntegrationModeModerated tests +m (moderated) mode
|
||||
// — non-voiced users cannot send.
|
||||
func TestIntegrationModeModerated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
alice := env.dial(t)
|
||||
alice.register("alice")
|
||||
|
||||
bob := env.dial(t)
|
||||
bob.register("bob")
|
||||
|
||||
alice.joinAndDrain("#modtest")
|
||||
bob.joinAndDrain("#modtest")
|
||||
|
||||
// Drain alice's view of bob's join.
|
||||
alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "JOIN") &&
|
||||
strings.Contains(l, "bob")
|
||||
})
|
||||
|
||||
// Set +m.
|
||||
alice.send("MODE #modtest +m")
|
||||
alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "+m")
|
||||
})
|
||||
bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "+m")
|
||||
})
|
||||
|
||||
// bob should get an error trying to send.
|
||||
bob.send("PRIVMSG #modtest :should fail")
|
||||
bobLines := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " 404 ") ||
|
||||
strings.Contains(l, " 482 ")
|
||||
})
|
||||
|
||||
foundModErr := false
|
||||
for _, line := range bobLines {
|
||||
if strings.Contains(line, " 404 ") ||
|
||||
strings.Contains(line, " 482 ") {
|
||||
foundModErr = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundModErr {
|
||||
t.Error(
|
||||
"non-voiced user should not be able to send " +
|
||||
"in +m channel",
|
||||
)
|
||||
}
|
||||
|
||||
// Grant +v to bob, then he should be able to send.
|
||||
alice.send("MODE #modtest +v bob")
|
||||
alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "+v")
|
||||
})
|
||||
bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "+v")
|
||||
})
|
||||
|
||||
bob.send("PRIVMSG #modtest :voiced message")
|
||||
aliceLines := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "voiced message")
|
||||
})
|
||||
assertContains(
|
||||
t, aliceLines, "voiced message",
|
||||
"alice receives voiced bob's message",
|
||||
)
|
||||
}
|
||||
|
||||
// TestIntegrationThirdClientObserver verifies that a third
|
||||
// client observing the same channel receives messages from
|
||||
// the other two.
|
||||
func TestIntegrationThirdClientObserver(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
alice := env.dial(t)
|
||||
alice.register("alice")
|
||||
|
||||
bob := env.dial(t)
|
||||
bob.register("bob")
|
||||
|
||||
carol := env.dial(t)
|
||||
carol.register("carol")
|
||||
|
||||
alice.joinAndDrain("#trio")
|
||||
bob.joinAndDrain("#trio")
|
||||
carol.joinAndDrain("#trio")
|
||||
|
||||
// Drain join notifications.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// alice sends; both bob and carol should receive.
|
||||
alice.send("PRIVMSG #trio :hello trio")
|
||||
|
||||
bobLines := bob.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "hello trio")
|
||||
})
|
||||
assertContains(
|
||||
t, bobLines, "hello trio",
|
||||
"bob receives trio message",
|
||||
)
|
||||
|
||||
carolLines := carol.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "hello trio")
|
||||
})
|
||||
assertContains(
|
||||
t, carolLines, "hello trio",
|
||||
"carol receives trio message",
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package ircserver_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/ircserver"
|
||||
"git.eeqj.de/sneak/neoirc/internal/ircserver"
|
||||
)
|
||||
|
||||
//nolint:funlen // table-driven test
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"sneak.berlin/go/neoirc/pkg/irc"
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
// relayMessages polls the client output queue and delivers
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/broker"
|
||||
"git.eeqj.de/sneak/neoirc/internal/config"
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/internal/logger"
|
||||
"git.eeqj.de/sneak/neoirc/internal/service"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/neoirc/internal/broker"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"sneak.berlin/go/neoirc/internal/logger"
|
||||
"sneak.berlin/go/neoirc/internal/service"
|
||||
)
|
||||
|
||||
// Params defines the dependencies for creating an IRC
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/broker"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"sneak.berlin/go/neoirc/internal/ircserver"
|
||||
"git.eeqj.de/sneak/neoirc/internal/broker"
|
||||
"git.eeqj.de/sneak/neoirc/internal/config"
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/internal/ircserver"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/globals"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/neoirc/internal/globals"
|
||||
)
|
||||
|
||||
// Params defines the dependencies for creating a Logger.
|
||||
|
||||
@@ -7,6 +7,9 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/config"
|
||||
"git.eeqj.de/sneak/neoirc/internal/globals"
|
||||
"git.eeqj.de/sneak/neoirc/internal/logger"
|
||||
basicauth "github.com/99designs/basicauth-go"
|
||||
chimw "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
@@ -15,9 +18,6 @@ import (
|
||||
"github.com/slok/go-http-metrics/middleware/std"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/globals"
|
||||
"sneak.berlin/go/neoirc/internal/logger"
|
||||
)
|
||||
|
||||
const corsMaxAge = 300
|
||||
|
||||
@@ -3,7 +3,7 @@ package ratelimit_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/ratelimit"
|
||||
"git.eeqj.de/sneak/neoirc/internal/ratelimit"
|
||||
)
|
||||
|
||||
func TestNewCreatesLimiter(t *testing.T) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/neoirc/web"
|
||||
"git.eeqj.de/sneak/neoirc/web"
|
||||
|
||||
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
@@ -12,12 +12,12 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/config"
|
||||
"git.eeqj.de/sneak/neoirc/internal/globals"
|
||||
"git.eeqj.de/sneak/neoirc/internal/handlers"
|
||||
"git.eeqj.de/sneak/neoirc/internal/logger"
|
||||
"git.eeqj.de/sneak/neoirc/internal/middleware"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/globals"
|
||||
"sneak.berlin/go/neoirc/internal/handlers"
|
||||
"sneak.berlin/go/neoirc/internal/logger"
|
||||
"sneak.berlin/go/neoirc/internal/middleware"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/broker"
|
||||
"git.eeqj.de/sneak/neoirc/internal/config"
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/internal/logger"
|
||||
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/neoirc/internal/broker"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"sneak.berlin/go/neoirc/internal/logger"
|
||||
"sneak.berlin/go/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
// Params defines the dependencies for creating a Service.
|
||||
|
||||
@@ -12,16 +12,16 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/broker"
|
||||
"git.eeqj.de/sneak/neoirc/internal/config"
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/internal/globals"
|
||||
"git.eeqj.de/sneak/neoirc/internal/logger"
|
||||
"git.eeqj.de/sneak/neoirc/internal/service"
|
||||
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/fx/fxtest"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"sneak.berlin/go/neoirc/internal/broker"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"sneak.berlin/go/neoirc/internal/globals"
|
||||
"sneak.berlin/go/neoirc/internal/logger"
|
||||
"sneak.berlin/go/neoirc/internal/service"
|
||||
"sneak.berlin/go/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
@@ -3,7 +3,7 @@ package stats_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/stats"
|
||||
"git.eeqj.de/sneak/neoirc/internal/stats"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/neoirc/pkg/irc"
|
||||
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/commands/JOIN.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/JOIN.json",
|
||||
"title": "JOIN",
|
||||
"description": "Join a channel. C2S: request to join. S2C: notification that a user joined. RFC 1459 §4.2.1.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/commands/KICK.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/KICK.json",
|
||||
"title": "KICK",
|
||||
"description": "Kick a user from a channel. RFC 1459 §4.2.8.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/commands/MODE.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/MODE.json",
|
||||
"title": "MODE",
|
||||
"description": "Set or query channel/user modes. RFC 1459 §4.2.3.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/commands/NICK.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/NICK.json",
|
||||
"title": "NICK",
|
||||
"description": "Change nickname. C2S: request new nick. S2C: notification of nick change. RFC 1459 §4.1.2.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/commands/NOTICE.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/NOTICE.json",
|
||||
"title": "NOTICE",
|
||||
"description": "Send a notice. Like PRIVMSG but must not trigger automatic replies. RFC 1459 §4.4.2.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/commands/PART.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/PART.json",
|
||||
"title": "PART",
|
||||
"description": "Leave a channel. C2S: request to leave. S2C: notification that a user left. RFC 1459 §4.2.2.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/commands/PING.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/PING.json",
|
||||
"title": "PING",
|
||||
"description": "Keepalive. C2S or S2S. Server responds with PONG. RFC 1459 §4.6.2.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/commands/PONG.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/PONG.json",
|
||||
"title": "PONG",
|
||||
"description": "Keepalive response. S2C or S2S. RFC 1459 §4.6.3.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/commands/PRIVMSG.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/PRIVMSG.json",
|
||||
"title": "PRIVMSG",
|
||||
"description": "Send a message to a channel or user. C2S: client sends to server. S2C: server relays to recipients. RFC 1459 §4.4.1.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/commands/PUBKEY.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/PUBKEY.json",
|
||||
"title": "PUBKEY",
|
||||
"description": "Announce or relay a user's public signing key. C2S: client announces key to channel or server. S2C: server relays to channel members. Protocol extension (not in RFC 1459). Body is a structured object (not an array) containing the key material.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/commands/QUIT.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/QUIT.json",
|
||||
"title": "QUIT",
|
||||
"description": "User disconnected. S2C only. RFC 1459 §4.1.6.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/commands/TOPIC.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/TOPIC.json",
|
||||
"title": "TOPIC",
|
||||
"description": "Get or set channel topic. C2S: set topic (body present) or query (body absent). S2C: topic change notification. RFC 1459 §4.2.4.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/message.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/message.json",
|
||||
"title": "IRC Message Envelope",
|
||||
"description": "Base envelope for all messages. Mirrors IRC wire format (RFC 1459/2812) encoded as JSON over HTTP. The 'command' field carries either an IRC command name (PRIVMSG, JOIN, etc.) or a three-digit numeric reply code (001, 353, 433, etc.).",
|
||||
"type": "object",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/001.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/001.json",
|
||||
"title": "001 RPL_WELCOME",
|
||||
"description": "Welcome message sent after successful session creation. RFC 2812 \u00a75.1.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/002.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/002.json",
|
||||
"title": "002 RPL_YOURHOST",
|
||||
"description": "Server host info sent after session creation. RFC 2812 \u00a75.1.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/003.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/003.json",
|
||||
"title": "003 RPL_CREATED",
|
||||
"description": "Server creation date. RFC 2812 \u00a75.1.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/004.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/004.json",
|
||||
"title": "004 RPL_MYINFO",
|
||||
"description": "Server info (name, version, available modes). RFC 2812 \u00a75.1.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/322.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/322.json",
|
||||
"title": "322 RPL_LIST",
|
||||
"description": "Channel list entry. One per channel in response to LIST. RFC 1459 \u00a76.2.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/323.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/323.json",
|
||||
"title": "323 RPL_LISTEND",
|
||||
"description": "End of channel list. RFC 1459 \u00a76.2.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/332.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/332.json",
|
||||
"title": "332 RPL_TOPIC",
|
||||
"description": "Channel topic (sent on JOIN or TOPIC query). RFC 1459 \u00a76.2.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/353.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/353.json",
|
||||
"title": "353 RPL_NAMREPLY",
|
||||
"description": "Channel member list. Sent on JOIN or NAMES query. RFC 1459 \u00a76.2.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/366.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/366.json",
|
||||
"title": "366 RPL_ENDOFNAMES",
|
||||
"description": "End of NAMES list. RFC 1459 \u00a76.2.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/372.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/372.json",
|
||||
"title": "372 RPL_MOTD",
|
||||
"description": "MOTD line. One message per line of the MOTD. RFC 2812 \u00a75.1.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/375.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/375.json",
|
||||
"title": "375 RPL_MOTDSTART",
|
||||
"description": "Start of MOTD. RFC 2812 \u00a75.1.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/376.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/376.json",
|
||||
"title": "376 RPL_ENDOFMOTD",
|
||||
"description": "End of MOTD. RFC 2812 \u00a75.1.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/401.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/401.json",
|
||||
"title": "401 ERR_NOSUCHNICK",
|
||||
"description": "No such nick/channel. RFC 1459 \u00a76.1.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/403.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/403.json",
|
||||
"title": "403 ERR_NOSUCHCHANNEL",
|
||||
"description": "No such channel. RFC 1459 \u00a76.1.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/433.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/433.json",
|
||||
"title": "433 ERR_NICKNAMEINUSE",
|
||||
"description": "Nickname is already in use. RFC 1459 \u00a76.1.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/442.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/442.json",
|
||||
"title": "442 ERR_NOTONCHANNEL",
|
||||
"description": "You're not on that channel. RFC 1459 \u00a76.1.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/482.json",
|
||||
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/482.json",
|
||||
"title": "482 ERR_CHANOPRIVSNEEDED",
|
||||
"description": "You're not channel operator. RFC 1459 \u00a76.1.",
|
||||
"$ref": "../message.json",
|
||||
|
||||
Reference in New Issue
Block a user