2 Commits

Author SHA1 Message Date
f829f9e3da Change module path to sneak.berlin/go/neoirc (#99)
Some checks failed
check / check (push) Failing after 1m21s
Changes the Go module path from `git.eeqj.de/sneak/neoirc` to `sneak.berlin/go/neoirc`.

All occurrences updated:
- `go.mod` module directive
- All Go import paths across 35 `.go` files (107 import statements)
- All JSON schema `$id` URIs across 30 `.json` files in `schema/`

No functional changes — this is a pure rename of the module path.

`docker build .` passes clean (formatting, linting, all tests, binary build).

closes #98

Co-authored-by: clawbot <clawbot@users.noreply.git.eeqj.de>
Reviewed-on: #99
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-04-01 23:05:41 +02:00
02279b2654 test: add comprehensive IRC integration test with two clients (#100)
All checks were successful
check / check (push) Successful in 6s
Adds `integration_test.go` with four test functions that exercise all major IRC features using real TCP connections.

## Tests

**TestIntegrationTwoClients** — sequential two-client test covering:
- NICK/USER registration (001-004 welcome burst)
- JOIN with cross-client visibility
- PRIVMSG channel (both directions)
- PRIVMSG DM (both directions)
- NOTICE channel and DM
- TOPIC set/get/lock/unlock
- MODE query, +m (moderated), +v (voice), -t/+t (topic lock)
- NAMES (with both nicks listed)
- LIST
- WHO
- WHOIS (with channels)
- LUSERS
- NICK change (with relay to other client)
- Duplicate NICK (ERR_NICKNAMEINUSE)
- KICK (with relay + reason)
- KICK non-op error (ERR_CHANOPRIVSNEEDED)
- PING/PONG
- Unknown command (ERR_UNKNOWNCOMMAND)
- MOTD
- AWAY set/clear/RPL_AWAY on DM
- PASS post-registration
- PART with reason + relay
- PART non-existent channel error
- User MODE query
- Multi-channel messaging
- QUIT with relay

**TestIntegrationModeSecret** — verifies +s mode set and query.

**TestIntegrationModeModerated** — verifies +m blocks non-voiced users and +v enables sending.

**TestIntegrationThirdClientObserver** — verifies three-client channel message fanout.

`docker build --no-cache .` passes clean (formatting, linting, all tests, build).

closes sneak/chat#97

Co-authored-by: user <user@Mac.lan guest wan>
Reviewed-on: #100
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-04-01 12:47:49 +02:00
66 changed files with 1051 additions and 138 deletions

View File

@@ -1,7 +1,7 @@
// Package main is the entry point for the neoirc-cli client.
package main
import "git.eeqj.de/sneak/neoirc/internal/cli"
import "sneak.berlin/go/neoirc/internal/cli"
func main() {
cli.Run()

View File

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

@@ -1,4 +1,4 @@
module git.eeqj.de/sneak/neoirc
module sneak.berlin/go/neoirc
go 1.24.0

View File

@@ -5,7 +5,7 @@ import (
"testing"
"time"
"git.eeqj.de/sneak/neoirc/internal/broker"
"sneak.berlin/go/neoirc/internal/broker"
)
func TestNewBroker(t *testing.T) {

View File

@@ -15,7 +15,7 @@ import (
"strings"
"time"
"git.eeqj.de/sneak/neoirc/pkg/irc"
"sneak.berlin/go/neoirc/pkg/irc"
)
const (

View File

@@ -8,7 +8,7 @@ import (
"math/big"
"time"
"git.eeqj.de/sneak/neoirc/internal/hashcash"
"sneak.berlin/go/neoirc/internal/hashcash"
)
const (

View File

@@ -8,8 +8,8 @@ import (
"sync"
"time"
api "git.eeqj.de/sneak/neoirc/internal/cli/api"
"git.eeqj.de/sneak/neoirc/pkg/irc"
api "sneak.berlin/go/neoirc/internal/cli/api"
"sneak.berlin/go/neoirc/pkg/irc"
)
const (

View File

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

View 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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import (
"encoding/json"
"testing"
"git.eeqj.de/sneak/neoirc/internal/db"
"sneak.berlin/go/neoirc/internal/db"
_ "modernc.org/sqlite"
)

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import (
"net/http"
"strings"
"git.eeqj.de/sneak/neoirc/pkg/irc"
"sneak.berlin/go/neoirc/pkg/irc"
)
const minPasswordLength = 8

View File

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

View File

@@ -5,7 +5,7 @@ import (
"encoding/hex"
"testing"
"git.eeqj.de/sneak/neoirc/internal/hashcash"
"sneak.berlin/go/neoirc/internal/hashcash"
)
const (

View File

@@ -9,7 +9,7 @@ import (
"testing"
"time"
"git.eeqj.de/sneak/neoirc/internal/hashcash"
"sneak.berlin/go/neoirc/internal/hashcash"
)
const testBits = 2

View File

@@ -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.

View File

@@ -8,8 +8,8 @@ import (
"strings"
"time"
"git.eeqj.de/sneak/neoirc/internal/service"
"git.eeqj.de/sneak/neoirc/pkg/irc"
"sneak.berlin/go/neoirc/internal/service"
"sneak.berlin/go/neoirc/pkg/irc"
)
// sendIRCError maps a service.IRCError to an IRC numeric

View File

@@ -11,11 +11,11 @@ import (
"sync"
"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/service"
"git.eeqj.de/sneak/neoirc/pkg/irc"
"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"
)
const (

View File

@@ -5,10 +5,10 @@ import (
"log/slog"
"net"
"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"
"sneak.berlin/go/neoirc/internal/broker"
"sneak.berlin/go/neoirc/internal/config"
"sneak.berlin/go/neoirc/internal/db"
"sneak.berlin/go/neoirc/internal/service"
)
// NewTestServer creates a Server suitable for testing.

View File

@@ -0,0 +1,913 @@
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",
)
}

View File

@@ -3,7 +3,7 @@ package ircserver_test
import (
"testing"
"git.eeqj.de/sneak/neoirc/internal/ircserver"
"sneak.berlin/go/neoirc/internal/ircserver"
)
//nolint:funlen // table-driven test

View File

@@ -6,8 +6,8 @@ import (
"strings"
"time"
"git.eeqj.de/sneak/neoirc/internal/db"
"git.eeqj.de/sneak/neoirc/pkg/irc"
"sneak.berlin/go/neoirc/internal/db"
"sneak.berlin/go/neoirc/pkg/irc"
)
// relayMessages polls the client output queue and delivers

View File

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

View File

@@ -11,10 +11,10 @@ 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/ircserver"
"sneak.berlin/go/neoirc/internal/broker"
"sneak.berlin/go/neoirc/internal/config"
"sneak.berlin/go/neoirc/internal/db"
"sneak.berlin/go/neoirc/internal/ircserver"
_ "modernc.org/sqlite"
)

View File

@@ -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.

View File

@@ -7,9 +7,6 @@ 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"
@@ -18,6 +15,9 @@ 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

View File

@@ -3,7 +3,7 @@ package ratelimit_test
import (
"testing"
"git.eeqj.de/sneak/neoirc/internal/ratelimit"
"sneak.berlin/go/neoirc/internal/ratelimit"
)
func TestNewCreatesLimiter(t *testing.T) {

View File

@@ -5,7 +5,7 @@ import (
"net/http"
"time"
"git.eeqj.de/sneak/neoirc/web"
"sneak.berlin/go/neoirc/web"
sentryhttp "github.com/getsentry/sentry-go/http"
"github.com/go-chi/chi/v5"

View File

@@ -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"

View File

@@ -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.

View File

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

View File

@@ -3,7 +3,7 @@ package stats_test
import (
"testing"
"git.eeqj.de/sneak/neoirc/internal/stats"
"sneak.berlin/go/neoirc/internal/stats"
)
func TestNew(t *testing.T) {

View File

@@ -4,7 +4,7 @@ import (
"errors"
"testing"
"git.eeqj.de/sneak/neoirc/pkg/irc"
"sneak.berlin/go/neoirc/pkg/irc"
)
func TestName(t *testing.T) {

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/JOIN.json",
"$id": "https://sneak.berlin/go/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",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/KICK.json",
"$id": "https://sneak.berlin/go/neoirc/schema/commands/KICK.json",
"title": "KICK",
"description": "Kick a user from a channel. RFC 1459 §4.2.8.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/MODE.json",
"$id": "https://sneak.berlin/go/neoirc/schema/commands/MODE.json",
"title": "MODE",
"description": "Set or query channel/user modes. RFC 1459 §4.2.3.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/NICK.json",
"$id": "https://sneak.berlin/go/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",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/NOTICE.json",
"$id": "https://sneak.berlin/go/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",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/PART.json",
"$id": "https://sneak.berlin/go/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",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/PING.json",
"$id": "https://sneak.berlin/go/neoirc/schema/commands/PING.json",
"title": "PING",
"description": "Keepalive. C2S or S2S. Server responds with PONG. RFC 1459 §4.6.2.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/PONG.json",
"$id": "https://sneak.berlin/go/neoirc/schema/commands/PONG.json",
"title": "PONG",
"description": "Keepalive response. S2C or S2S. RFC 1459 §4.6.3.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/PRIVMSG.json",
"$id": "https://sneak.berlin/go/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",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/PUBKEY.json",
"$id": "https://sneak.berlin/go/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",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/QUIT.json",
"$id": "https://sneak.berlin/go/neoirc/schema/commands/QUIT.json",
"title": "QUIT",
"description": "User disconnected. S2C only. RFC 1459 §4.1.6.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/commands/TOPIC.json",
"$id": "https://sneak.berlin/go/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",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/message.json",
"$id": "https://sneak.berlin/go/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",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/001.json",
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/001.json",
"title": "001 RPL_WELCOME",
"description": "Welcome message sent after successful session creation. RFC 2812 \u00a75.1.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/002.json",
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/002.json",
"title": "002 RPL_YOURHOST",
"description": "Server host info sent after session creation. RFC 2812 \u00a75.1.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/003.json",
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/003.json",
"title": "003 RPL_CREATED",
"description": "Server creation date. RFC 2812 \u00a75.1.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/004.json",
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/004.json",
"title": "004 RPL_MYINFO",
"description": "Server info (name, version, available modes). RFC 2812 \u00a75.1.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/322.json",
"$id": "https://sneak.berlin/go/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",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/323.json",
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/323.json",
"title": "323 RPL_LISTEND",
"description": "End of channel list. RFC 1459 \u00a76.2.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/332.json",
"$id": "https://sneak.berlin/go/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",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/353.json",
"$id": "https://sneak.berlin/go/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",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/366.json",
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/366.json",
"title": "366 RPL_ENDOFNAMES",
"description": "End of NAMES list. RFC 1459 \u00a76.2.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/372.json",
"$id": "https://sneak.berlin/go/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",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/375.json",
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/375.json",
"title": "375 RPL_MOTDSTART",
"description": "Start of MOTD. RFC 2812 \u00a75.1.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/376.json",
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/376.json",
"title": "376 RPL_ENDOFMOTD",
"description": "End of MOTD. RFC 2812 \u00a75.1.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/401.json",
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/401.json",
"title": "401 ERR_NOSUCHNICK",
"description": "No such nick/channel. RFC 1459 \u00a76.1.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/403.json",
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/403.json",
"title": "403 ERR_NOSUCHCHANNEL",
"description": "No such channel. RFC 1459 \u00a76.1.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/433.json",
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/433.json",
"title": "433 ERR_NICKNAMEINUSE",
"description": "Nickname is already in use. RFC 1459 \u00a76.1.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/442.json",
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/442.json",
"title": "442 ERR_NOTONCHANNEL",
"description": "You're not on that channel. RFC 1459 \u00a76.1.",
"$ref": "../message.json",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/neoirc/schema/numerics/482.json",
"$id": "https://sneak.berlin/go/neoirc/schema/numerics/482.json",
"title": "482 ERR_CHANOPRIVSNEEDED",
"description": "You're not channel operator. RFC 1459 \u00a76.1.",
"$ref": "../message.json",