From df41ecbd306df8a989ab3359faf8143779138132 Mon Sep 17 00:00:00 2001 From: clawbot Date: Fri, 6 Mar 2026 03:49:59 -0800 Subject: [PATCH] Rename app from chat to neoirc, binary to neoircd (closes #46) - Rename Go module path: git.eeqj.de/sneak/chat -> git.eeqj.de/sneak/neoirc - Rename binary: chatd -> neoircd, chat-cli -> neoirc-cli - Rename cmd directories: cmd/chatd -> cmd/neoircd, cmd/chat-cli -> cmd/neoirc-cli - Rename Go package: chatapi -> neoircapi - Update Makefile: binary name, build targets, docker image tag, clean target - Update Dockerfile: binary paths, user/group names, ENTRYPOINT - Update .gitignore and .dockerignore - Update all Go imports and doc comments - Update default server name fallback: chat -> neoirc - Update web client: localStorage keys, page title, default server name - Update all schema $id URLs and example hostnames - Update README.md: project name, binary references, examples, directory tree - Update AGENTS.md: build command reference - Update test fixtures: app name and channel names --- .dockerignore | 4 +- .gitignore | 4 +- AGENTS.md | 2 +- Dockerfile | 14 ++--- Makefile | 10 ++-- README.md | 62 +++++++++++----------- cmd/{chat-cli => neoirc-cli}/api/client.go | 6 +-- cmd/{chat-cli => neoirc-cli}/api/types.go | 4 +- cmd/{chat-cli => neoirc-cli}/main.go | 8 +-- cmd/{chat-cli => neoirc-cli}/ui.go | 0 cmd/{chatd => neoircd}/main.go | 18 +++---- go.mod | 2 +- internal/broker/broker_test.go | 2 +- internal/config/config.go | 4 +- internal/db/db.go | 4 +- internal/db/queries_test.go | 2 +- internal/handlers/api.go | 2 +- internal/handlers/api_test.go | 28 +++++----- internal/handlers/handlers.go | 14 ++--- internal/healthcheck/healthcheck.go | 8 +-- internal/logger/logger.go | 2 +- internal/middleware/middleware.go | 8 +-- internal/server/routes.go | 2 +- internal/server/server.go | 12 ++--- schema/README.md | 2 +- schema/commands/JOIN.json | 2 +- schema/commands/KICK.json | 2 +- schema/commands/MODE.json | 2 +- schema/commands/NICK.json | 2 +- schema/commands/NOTICE.json | 2 +- schema/commands/PART.json | 2 +- schema/commands/PING.json | 2 +- schema/commands/PONG.json | 2 +- schema/commands/PRIVMSG.json | 2 +- schema/commands/PUBKEY.json | 2 +- schema/commands/QUIT.json | 2 +- schema/commands/TOPIC.json | 4 +- schema/message.json | 2 +- schema/numerics/001.json | 2 +- schema/numerics/002.json | 4 +- schema/numerics/003.json | 2 +- schema/numerics/004.json | 4 +- schema/numerics/322.json | 2 +- schema/numerics/323.json | 2 +- schema/numerics/332.json | 4 +- schema/numerics/353.json | 2 +- schema/numerics/366.json | 2 +- schema/numerics/372.json | 2 +- schema/numerics/375.json | 2 +- schema/numerics/376.json | 2 +- schema/numerics/401.json | 2 +- schema/numerics/403.json | 2 +- schema/numerics/433.json | 2 +- schema/numerics/442.json | 2 +- schema/numerics/482.json | 2 +- web/dist/app.js | 4 +- web/dist/index.html | 2 +- web/src/app.jsx | 14 ++--- web/src/index.html | 2 +- 59 files changed, 157 insertions(+), 157 deletions(-) rename cmd/{chat-cli => neoirc-cli}/api/client.go (97%) rename cmd/{chat-cli => neoirc-cli}/api/types.go (96%) rename cmd/{chat-cli => neoirc-cli}/main.go (98%) rename cmd/{chat-cli => neoirc-cli}/ui.go (100%) rename cmd/{chatd => neoircd}/main.go (55%) diff --git a/.dockerignore b/.dockerignore index 5004937..9b75277 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,8 @@ .git *.md !README.md -chatd -chat-cli +neoircd +neoirc-cli data.db data.db-wal data.db-shm diff --git a/.gitignore b/.gitignore index 95d8357..2adb002 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ node_modules/ *.key # Build artifacts -/chatd +/neoircd /bin/ *.exe *.dll @@ -34,5 +34,5 @@ vendor/ # Project data.db debug.log -/chat-cli +/neoirc-cli web/node_modules/ diff --git a/AGENTS.md b/AGENTS.md index 88bbae9..69ff0a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ 1. **Format**: `gofmt -s -w .` and `goimports -w .` 2. **Lint**: `golangci-lint run --config .golangci.yml ./...` — zero issues 3. **Test**: `go test -race ./...` — all passing -4. **Build**: `go build ./cmd/chatd` — compiles clean +4. **Build**: `go build ./cmd/neoircd` — compiles clean No commit lands on main with lint errors, test failures, or formatting issues. diff --git a/Dockerfile b/Dockerfile index 68667fa..4fd5b15 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,20 +26,20 @@ RUN make test # Build static binaries (no cgo needed at runtime — modernc.org/sqlite is pure Go) ARG VERSION=dev -RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.Version=${VERSION}" -o /chatd ./cmd/chatd/ -RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /chat-cli ./cmd/chat-cli/ +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.Version=${VERSION}" -o /neoircd ./cmd/neoircd/ +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /neoirc-cli ./cmd/neoirc-cli/ # Runtime stage # alpine:3.21, 2026-02-26 FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709 RUN apk add --no-cache ca-certificates \ - && addgroup -S chat && adduser -S chat -G chat \ + && addgroup -S neoirc && adduser -S neoirc -G neoirc \ && mkdir -p /var/lib/neoirc \ - && chown chat:chat /var/lib/neoirc -COPY --from=builder /chatd /usr/local/bin/chatd + && chown neoirc:neoirc /var/lib/neoirc +COPY --from=builder /neoircd /usr/local/bin/neoircd -USER chat +USER neoirc EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget -qO- http://localhost:8080/.well-known/healthcheck.json || exit 1 -ENTRYPOINT ["chatd"] +ENTRYPOINT ["neoircd"] diff --git a/Makefile b/Makefile index a25d89e..6f70538 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: all build lint fmt fmt-check test check clean run debug docker hooks -BINARY := chatd +BINARY := neoircd VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") BUILDARCH := $(shell go env GOARCH) LDFLAGS := -X main.Version=$(VERSION) -X main.Buildarch=$(BUILDARCH) @@ -8,7 +8,7 @@ LDFLAGS := -X main.Version=$(VERSION) -X main.Buildarch=$(BUILDARCH) all: check build build: - go build -ldflags "$(LDFLAGS)" -o bin/$(BINARY) ./cmd/chatd + go build -ldflags "$(LDFLAGS)" -o bin/$(BINARY) ./cmd/neoircd lint: golangci-lint run --config .golangci.yml ./... @@ -27,7 +27,7 @@ test: # Used by CI and Docker build — fails if anything is wrong check: test lint fmt-check @echo "==> Building..." - go build -ldflags "$(LDFLAGS)" -o /dev/null ./cmd/chatd + go build -ldflags "$(LDFLAGS)" -o /dev/null ./cmd/neoircd @echo "==> All checks passed!" run: build @@ -37,10 +37,10 @@ debug: build DEBUG=1 GOTRACEBACK=all ./bin/$(BINARY) clean: - rm -rf bin/ chatd + rm -rf bin/ neoircd docker: - docker build -t chat . + docker build -t neoirc . hooks: @printf '#!/bin/sh\nset -e\n' > .git/hooks/pre-commit diff --git a/README.md b/README.md index 8315bb2..937b233 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# chat +# neoirc **IRC semantics, structured message metadata, cryptographic signing, and server-held session state with per-client delivery queues. All over HTTP+JSON.** -A chat server written in Go that decouples session state from transport +An IRC-inspired server written in Go that decouples session state from transport connections, enabling mobile-friendly persistent sessions over plain HTTP. The **HTTP API is the primary interface**. It's designed to be simple enough @@ -44,7 +44,7 @@ IRC is in decline because session state is tied to the TCP connection. In a mobile-first world, that's a nonstarter. Not everyone wants to run a bouncer or pay for IRCCloud. -This project builds a chat server that: +This project builds a server that: - Holds session state server-side (message queues, presence, channel membership) - Exposes a minimal, clean HTTP+JSON API — easy to build clients against @@ -132,7 +132,7 @@ makes signing consistent — you sign the same structure you send. ### Why not XMPP or Matrix? -XMPP is XML-based, overengineered for chat, and the ecosystem is fragmented +XMPP is XML-based, overengineered for messaging, and the ecosystem is fragmented across incompatible extensions (XEPs). Matrix is a federated append-only event graph with a spec that runs to hundreds of pages. Both are fine protocols, but they're solving different problems at different scales. @@ -828,16 +828,16 @@ the server to the client (never C2S) and use 3-digit string codes in the | Code | Name | When Sent | Example | |------|----------------------|-----------|---------| | `001` | RPL_WELCOME | After session creation | `{"command":"001","to":"alice","body":["Welcome to the network, alice"]}` | -| `002` | RPL_YOURHOST | After session creation | `{"command":"002","to":"alice","body":["Your host is chatserver, running version 0.1"]}` | +| `002` | RPL_YOURHOST | After session creation | `{"command":"002","to":"alice","body":["Your host is neoirc-server, running version 0.1"]}` | | `003` | RPL_CREATED | After session creation | `{"command":"003","to":"alice","body":["This server was created 2026-02-10"]}` | -| `004` | RPL_MYINFO | After session creation | `{"command":"004","to":"alice","params":["chatserver","0.1","","imnst"]}` | -| `322` | RPL_LIST | In response to LIST | `{"command":"322","to":"alice","params":["#general","5"],"body":["General chat"]}` | +| `004` | RPL_MYINFO | After session creation | `{"command":"004","to":"alice","params":["neoirc-server","0.1","","imnst"]}` | +| `322` | RPL_LIST | In response to LIST | `{"command":"322","to":"alice","params":["#general","5"],"body":["General discussion"]}` | | `323` | RPL_LISTEND | End of LIST response | `{"command":"323","to":"alice","body":["End of /LIST"]}` | | `332` | RPL_TOPIC | On JOIN or TOPIC query | `{"command":"332","to":"alice","params":["#general"],"body":["Welcome!"]}` | | `353` | RPL_NAMREPLY | On JOIN or NAMES query | `{"command":"353","to":"alice","params":["=","#general"],"body":["@op1 alice bob +voiced1"]}` | | `366` | RPL_ENDOFNAMES | End of NAMES response | `{"command":"366","to":"alice","params":["#general"],"body":["End of /NAMES list"]}` | | `372` | RPL_MOTD | MOTD line | `{"command":"372","to":"alice","body":["Welcome to the server"]}` | -| `375` | RPL_MOTDSTART | Start of MOTD | `{"command":"375","to":"alice","body":["- chatserver Message of the Day -"]}` | +| `375` | RPL_MOTDSTART | Start of MOTD | `{"command":"375","to":"alice","body":["- neoirc-server Message of the Day -"]}` | | `376` | RPL_ENDOFMOTD | End of MOTD | `{"command":"376","to":"alice","body":["End of /MOTD command"]}` | | `401` | ERR_NOSUCHNICK | DM to nonexistent nick | `{"command":"401","to":"alice","params":["bob"],"body":["No such nick/channel"]}` | | `403` | ERR_NOSUCHCHANNEL | Action on nonexistent channel | `{"command":"403","to":"alice","params":["#nope"],"body":["No such channel"]}` | @@ -1214,7 +1214,7 @@ Return server metadata. No authentication required. **Response:** `200 OK` ```json { - "name": "My Chat Server", + "name": "My NeoIRC Server", "motd": "Welcome! Be nice.", "users": 42 } @@ -1468,7 +1468,7 @@ authenticity. ## Federation (Server-to-Server) -Federation allows multiple chat servers to link together, forming a network +Federation allows multiple neoirc servers to link together, forming a network where users on different servers can share channels — similar to IRC server linking. @@ -1664,7 +1664,7 @@ directory is also loaded automatically via ```bash PORT=8080 -SERVER_NAME=My Chat Server +SERVER_NAME=My NeoIRC Server MOTD=Welcome! Be excellent to each other. DEBUG=false DBURL=file:///var/lib/neoirc/state.db?_journal_mode=WAL @@ -1677,24 +1677,24 @@ SESSION_IDLE_TIMEOUT=24h ### Docker (Recommended) -The Docker image contains a single static binary (`chatd`) and nothing else. +The Docker image contains a single static binary (`neoircd`) and nothing else. ```bash # Build -docker build -t chat . +docker build -t neoirc . # Run docker run -p 8080:8080 \ - -v chat-data:/var/lib/neoirc \ + -v neoirc-data:/var/lib/neoirc \ -e SERVER_NAME="My Server" \ -e MOTD="Welcome!" \ - chat + neoirc ``` The Dockerfile is a multi-stage build: -1. **Build stage**: Compiles `chatd` and `chat-cli` (CLI built to verify +1. **Build stage**: Compiles `neoircd` and `neoirc-cli` (CLI built to verify compilation, not included in final image) -2. **Final stage**: Alpine Linux + `chatd` binary only +2. **Final stage**: Alpine Linux + `neoircd` binary only ```dockerfile FROM golang:1.24-alpine AS builder @@ -1703,13 +1703,13 @@ RUN apk add --no-cache make COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN go build -o /chatd ./cmd/chatd/ -RUN go build -o /chat-cli ./cmd/chat-cli/ +RUN go build -o /neoircd ./cmd/neoircd/ +RUN go build -o /neoirc-cli ./cmd/neoirc-cli/ FROM alpine:latest -COPY --from=builder /chatd /usr/local/bin/chatd +COPY --from=builder /neoircd /usr/local/bin/neoircd EXPOSE 8080 -CMD ["chatd"] +CMD ["neoircd"] ``` ### Binary @@ -1717,10 +1717,10 @@ CMD ["chatd"] ```bash # Build from source make build -# Binary at ./bin/chatd +# Binary at ./bin/neoircd # Run -./bin/chatd +./bin/neoircd # Listens on :8080, writes to /var/lib/neoirc/state.db ``` @@ -1730,7 +1730,7 @@ For production, run behind a TLS-terminating reverse proxy. **Caddy:** ``` -chat.example.com { +neoirc.example.com { reverse_proxy localhost:8080 } ``` @@ -1739,7 +1739,7 @@ chat.example.com { ```nginx server { listen 443 ssl; - server_name chat.example.com; + server_name neoirc.example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; @@ -1770,7 +1770,7 @@ seconds to accommodate long-poll connections. ## Client Development Guide -This section explains how to write a client against the chat API. The API is +This section explains how to write a client against the neoirc API. The API is designed to be simple enough that a basic client can be written in any language with an HTTP client library. @@ -2060,7 +2060,7 @@ GET /api/v1/challenge - [x] NICK change with broadcast - [x] QUIT with broadcast and cleanup - [x] Embedded web SPA client -- [x] CLI client (chat-cli) +- [x] CLI client (neoirc-cli) - [x] SQLite storage with WAL mode - [x] Docker deployment - [x] Prometheus metrics endpoint @@ -2113,11 +2113,11 @@ GET /api/v1/challenge Following [gohttpserver CONVENTIONS.md](https://git.eeqj.de/sneak/gohttpserver/src/branch/main/CONVENTIONS.md): ``` -chat/ +neoirc/ ├── cmd/ -│ ├── chatd/ # Server binary entry point +│ ├── neoircd/ # Server binary entry point │ │ └── main.go -│ └── chat-cli/ # TUI client +│ └── neoirc-cli/ # TUI client │ ├── main.go # Command handling, poll loop │ ├── ui.go # tview-based terminal UI │ └── api/ @@ -2248,7 +2248,7 @@ chat/ - IRC message envelope format with per-client queue fan-out - Long-polling with in-memory broker - Embedded web SPA client -- TUI client (chat-cli) +- TUI client (neoirc-cli) - Docker image - Prometheus metrics diff --git a/cmd/chat-cli/api/client.go b/cmd/neoirc-cli/api/client.go similarity index 97% rename from cmd/chat-cli/api/client.go rename to cmd/neoirc-cli/api/client.go index ca22506..98eea62 100644 --- a/cmd/chat-cli/api/client.go +++ b/cmd/neoirc-cli/api/client.go @@ -1,5 +1,5 @@ -// Package chatapi provides a client for the chat server API. -package chatapi +// Package neoircapi provides a client for the neoirc server API. +package neoircapi import ( "bytes" @@ -23,7 +23,7 @@ const ( var errHTTP = errors.New("HTTP error") -// Client wraps HTTP calls to the chat server API. +// Client wraps HTTP calls to the neoirc server API. type Client struct { BaseURL string Token string diff --git a/cmd/chat-cli/api/types.go b/cmd/neoirc-cli/api/types.go similarity index 96% rename from cmd/chat-cli/api/types.go rename to cmd/neoirc-cli/api/types.go index 718bf76..96a0dc6 100644 --- a/cmd/chat-cli/api/types.go +++ b/cmd/neoirc-cli/api/types.go @@ -1,4 +1,4 @@ -package chatapi +package neoircapi import "time" @@ -21,7 +21,7 @@ type StateResponse struct { Channels []string `json:"channels"` } -// Message represents a chat message envelope. +// Message represents a neoirc message envelope. type Message struct { Command string `json:"command"` From string `json:"from,omitempty"` diff --git a/cmd/chat-cli/main.go b/cmd/neoirc-cli/main.go similarity index 98% rename from cmd/chat-cli/main.go rename to cmd/neoirc-cli/main.go index f5b22f3..1fb3fb1 100644 --- a/cmd/chat-cli/main.go +++ b/cmd/neoirc-cli/main.go @@ -1,4 +1,4 @@ -// Package main is the entry point for the chat-cli client. +// Package main is the entry point for the neoirc-cli client. package main import ( @@ -8,7 +8,7 @@ import ( "sync" "time" - api "git.eeqj.de/sneak/chat/cmd/chat-cli/api" + api "git.eeqj.de/sneak/neoirc/cmd/neoirc-cli/api" ) const ( @@ -41,7 +41,7 @@ func main() { app.ui.SetStatus(app.nick, "", "disconnected") app.ui.AddStatus( - "Welcome to chat-cli — an IRC-style client", + "Welcome to neoirc-cli — an IRC-style client", ) app.ui.AddStatus( "Type [yellow]/connect " + @@ -564,7 +564,7 @@ func (a *App) cmdQuit() { func (a *App) cmdHelp() { help := []string{ - "[cyan]*** chat-cli commands:", + "[cyan]*** neoirc-cli commands:", " /connect — Connect to server", " /nick — Change nickname", " /join #channel — Join channel", diff --git a/cmd/chat-cli/ui.go b/cmd/neoirc-cli/ui.go similarity index 100% rename from cmd/chat-cli/ui.go rename to cmd/neoirc-cli/ui.go diff --git a/cmd/chatd/main.go b/cmd/neoircd/main.go similarity index 55% rename from cmd/chatd/main.go rename to cmd/neoircd/main.go index 779594a..e7b0445 100644 --- a/cmd/chatd/main.go +++ b/cmd/neoircd/main.go @@ -1,15 +1,15 @@ -// Package main is the entry point for the chatd server. +// Package main is the entry point for the neoircd server. package main import ( - "git.eeqj.de/sneak/chat/internal/config" - "git.eeqj.de/sneak/chat/internal/db" - "git.eeqj.de/sneak/chat/internal/globals" - "git.eeqj.de/sneak/chat/internal/handlers" - "git.eeqj.de/sneak/chat/internal/healthcheck" - "git.eeqj.de/sneak/chat/internal/logger" - "git.eeqj.de/sneak/chat/internal/middleware" - "git.eeqj.de/sneak/chat/internal/server" + "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/logger" + "git.eeqj.de/sneak/neoirc/internal/middleware" + "git.eeqj.de/sneak/neoirc/internal/server" "go.uber.org/fx" ) diff --git a/go.mod b/go.mod index 53d7d54..af218ee 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module git.eeqj.de/sneak/chat +module git.eeqj.de/sneak/neoirc go 1.24.0 diff --git a/internal/broker/broker_test.go b/internal/broker/broker_test.go index 2d35013..2259c3a 100644 --- a/internal/broker/broker_test.go +++ b/internal/broker/broker_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "git.eeqj.de/sneak/chat/internal/broker" + "git.eeqj.de/sneak/neoirc/internal/broker" ) func TestNewBroker(t *testing.T) { diff --git a/internal/config/config.go b/internal/config/config.go index f2b4308..df94550 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,8 +5,8 @@ import ( "errors" "log/slog" - "git.eeqj.de/sneak/chat/internal/globals" - "git.eeqj.de/sneak/chat/internal/logger" + "git.eeqj.de/sneak/neoirc/internal/globals" + "git.eeqj.de/sneak/neoirc/internal/logger" "github.com/spf13/viper" "go.uber.org/fx" diff --git a/internal/db/db.go b/internal/db/db.go index 41aeee5..074f365 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -12,8 +12,8 @@ import ( "strconv" "strings" - "git.eeqj.de/sneak/chat/internal/config" - "git.eeqj.de/sneak/chat/internal/logger" + "git.eeqj.de/sneak/neoirc/internal/config" + "git.eeqj.de/sneak/neoirc/internal/logger" "go.uber.org/fx" _ "github.com/joho/godotenv/autoload" // .env diff --git a/internal/db/queries_test.go b/internal/db/queries_test.go index 6afc9c7..f750baf 100644 --- a/internal/db/queries_test.go +++ b/internal/db/queries_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "testing" - "git.eeqj.de/sneak/chat/internal/db" + "git.eeqj.de/sneak/neoirc/internal/db" _ "modernc.org/sqlite" ) diff --git a/internal/handlers/api.go b/internal/handlers/api.go index ad31dd3..5ed1e18 100644 --- a/internal/handlers/api.go +++ b/internal/handlers/api.go @@ -229,7 +229,7 @@ func (hdlr *Handlers) deliverMOTD( serverName := hdlr.params.Config.ServerName if serverName == "" { - serverName = "chat" + serverName = "neoirc" } if motd == "" { diff --git a/internal/handlers/api_test.go b/internal/handlers/api_test.go index 1a5ce9e..adfc77e 100644 --- a/internal/handlers/api_test.go +++ b/internal/handlers/api_test.go @@ -17,14 +17,14 @@ import ( "testing" "time" - "git.eeqj.de/sneak/chat/internal/config" - "git.eeqj.de/sneak/chat/internal/db" - "git.eeqj.de/sneak/chat/internal/globals" - "git.eeqj.de/sneak/chat/internal/handlers" - "git.eeqj.de/sneak/chat/internal/healthcheck" - "git.eeqj.de/sneak/chat/internal/logger" - "git.eeqj.de/sneak/chat/internal/middleware" - "git.eeqj.de/sneak/chat/internal/server" + "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/logger" + "git.eeqj.de/sneak/neoirc/internal/middleware" + "git.eeqj.de/sneak/neoirc/internal/server" "go.uber.org/fx" "go.uber.org/fx/fxtest" ) @@ -115,7 +115,7 @@ func newTestServer( func newTestGlobals() *globals.Globals { return &globals.Globals{ - Appname: "chat-test", + Appname: "neoirc-test", Version: "test", } } @@ -682,10 +682,10 @@ func TestChannelMessage(t *testing.T) { bobToken := tserver.createSession("bob_msg") tserver.sendCommand(aliceToken, map[string]any{ - commandKey: joinCmd, toKey: "#chat", + commandKey: joinCmd, toKey: "#test", }) tserver.sendCommand(bobToken, map[string]any{ - commandKey: joinCmd, toKey: "#chat", + commandKey: joinCmd, toKey: "#test", }) _, _ = tserver.pollMessages(aliceToken, 0) @@ -695,7 +695,7 @@ func TestChannelMessage(t *testing.T) { aliceToken, map[string]any{ commandKey: privmsgCmd, - toKey: "#chat", + toKey: "#test", bodyKey: []string{"hello world"}, }, ) @@ -725,11 +725,11 @@ func TestMessageMissingBody(t *testing.T) { token := tserver.createSession("nobody") tserver.sendCommand(token, map[string]any{ - commandKey: joinCmd, toKey: "#chat", + commandKey: joinCmd, toKey: "#test", }) status, _ := tserver.sendCommand(token, map[string]any{ - commandKey: privmsgCmd, toKey: "#chat", + commandKey: privmsgCmd, toKey: "#test", }) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index ed6ef04..72ef994 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -1,4 +1,4 @@ -// Package handlers provides HTTP request handlers for the chat server. +// Package handlers provides HTTP request handlers for the neoirc server. package handlers import ( @@ -9,12 +9,12 @@ import ( "net/http" "time" - "git.eeqj.de/sneak/chat/internal/broker" - "git.eeqj.de/sneak/chat/internal/config" - "git.eeqj.de/sneak/chat/internal/db" - "git.eeqj.de/sneak/chat/internal/globals" - "git.eeqj.de/sneak/chat/internal/healthcheck" - "git.eeqj.de/sneak/chat/internal/logger" + "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/healthcheck" + "git.eeqj.de/sneak/neoirc/internal/logger" "go.uber.org/fx" ) diff --git a/internal/healthcheck/healthcheck.go b/internal/healthcheck/healthcheck.go index 2aacc84..4f4f2ac 100644 --- a/internal/healthcheck/healthcheck.go +++ b/internal/healthcheck/healthcheck.go @@ -6,10 +6,10 @@ import ( "log/slog" "time" - "git.eeqj.de/sneak/chat/internal/config" - "git.eeqj.de/sneak/chat/internal/db" - "git.eeqj.de/sneak/chat/internal/globals" - "git.eeqj.de/sneak/chat/internal/logger" + "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" "go.uber.org/fx" ) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 518c86a..075b376 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -5,7 +5,7 @@ import ( "log/slog" "os" - "git.eeqj.de/sneak/chat/internal/globals" + "git.eeqj.de/sneak/neoirc/internal/globals" "go.uber.org/fx" ) diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index a69c58c..e8260ca 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -1,4 +1,4 @@ -// Package middleware provides HTTP middleware for the chat server. +// Package middleware provides HTTP middleware for the neoirc server. package middleware import ( @@ -7,9 +7,9 @@ import ( "net/http" "time" - "git.eeqj.de/sneak/chat/internal/config" - "git.eeqj.de/sneak/chat/internal/globals" - "git.eeqj.de/sneak/chat/internal/logger" + "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/middleware" "github.com/go-chi/cors" diff --git a/internal/server/routes.go b/internal/server/routes.go index 9e945e7..9cc0103 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -5,7 +5,7 @@ import ( "net/http" "time" - "git.eeqj.de/sneak/chat/web" + "git.eeqj.de/sneak/neoirc/web" sentryhttp "github.com/getsentry/sentry-go/http" "github.com/go-chi/chi" diff --git a/internal/server/server.go b/internal/server/server.go index b6d04c5..0adccaf 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,4 +1,4 @@ -// Package server implements the main HTTP server for the chat application. +// Package server implements the main HTTP server for the neoirc application. package server import ( @@ -12,11 +12,11 @@ import ( "syscall" "time" - "git.eeqj.de/sneak/chat/internal/config" - "git.eeqj.de/sneak/chat/internal/globals" - "git.eeqj.de/sneak/chat/internal/handlers" - "git.eeqj.de/sneak/chat/internal/logger" - "git.eeqj.de/sneak/chat/internal/middleware" + "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" "github.com/getsentry/sentry-go" diff --git a/schema/README.md b/schema/README.md index 250010b..6aaa5fb 100644 --- a/schema/README.md +++ b/schema/README.md @@ -1,6 +1,6 @@ # Message Schemas -JSON Schema definitions (draft 2020-12) for the chat protocol. Messages use +JSON Schema definitions (draft 2020-12) for the neoirc protocol. Messages use **IRC command names and numeric reply codes** (RFC 1459/2812) encoded as JSON over HTTP. diff --git a/schema/commands/JOIN.json b/schema/commands/JOIN.json index ca7f6c4..92af9cb 100644 --- a/schema/commands/JOIN.json +++ b/schema/commands/JOIN.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/commands/KICK.json b/schema/commands/KICK.json index 25af8a8..e788fd1 100644 --- a/schema/commands/KICK.json +++ b/schema/commands/KICK.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/commands/MODE.json b/schema/commands/MODE.json index e2da7f0..dc96af9 100644 --- a/schema/commands/MODE.json +++ b/schema/commands/MODE.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/commands/NICK.json b/schema/commands/NICK.json index e6e9bef..e45fbba 100644 --- a/schema/commands/NICK.json +++ b/schema/commands/NICK.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/commands/NOTICE.json b/schema/commands/NOTICE.json index 092e825..eee9474 100644 --- a/schema/commands/NOTICE.json +++ b/schema/commands/NOTICE.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/commands/PART.json b/schema/commands/PART.json index a2bd9ca..5e11d08 100644 --- a/schema/commands/PART.json +++ b/schema/commands/PART.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/commands/PING.json b/schema/commands/PING.json index b911dc8..b831d6b 100644 --- a/schema/commands/PING.json +++ b/schema/commands/PING.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/commands/PONG.json b/schema/commands/PONG.json index 6e04620..4c0bb92 100644 --- a/schema/commands/PONG.json +++ b/schema/commands/PONG.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/commands/PRIVMSG.json b/schema/commands/PRIVMSG.json index 9204705..6256daa 100644 --- a/schema/commands/PRIVMSG.json +++ b/schema/commands/PRIVMSG.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/commands/PUBKEY.json b/schema/commands/PUBKEY.json index 90fdc30..668e9e9 100644 --- a/schema/commands/PUBKEY.json +++ b/schema/commands/PUBKEY.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/commands/QUIT.json b/schema/commands/QUIT.json index cfb3a45..5d86f6f 100644 --- a/schema/commands/QUIT.json +++ b/schema/commands/QUIT.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/commands/TOPIC.json b/schema/commands/TOPIC.json index 8ba1365..d3aee57 100644 --- a/schema/commands/TOPIC.json +++ b/schema/commands/TOPIC.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", @@ -17,6 +17,6 @@ }, "required": ["command", "to"], "examples": [ - { "command": "TOPIC", "from": "alice", "to": "#general", "body": ["Welcome to the chat"] } + { "command": "TOPIC", "from": "alice", "to": "#general", "body": ["Welcome to the channel"] } ] } diff --git a/schema/message.json b/schema/message.json index 875a492..ff39be0 100644 --- a/schema/message.json +++ b/schema/message.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/numerics/001.json b/schema/numerics/001.json index feb2ba0..4dfd7f1 100644 --- a/schema/numerics/001.json +++ b/schema/numerics/001.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/numerics/002.json b/schema/numerics/002.json index 13fc73c..cf78d4d 100644 --- a/schema/numerics/002.json +++ b/schema/numerics/002.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", @@ -29,7 +29,7 @@ "command": "002", "to": "alice", "body": [ - "Your host is chat.example.com, running version 0.1.0" + "Your host is neoirc.example.com, running version 0.1.0" ] } ] diff --git a/schema/numerics/003.json b/schema/numerics/003.json index d448a8c..1cff7c9 100644 --- a/schema/numerics/003.json +++ b/schema/numerics/003.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/numerics/004.json b/schema/numerics/004.json index 2aaf116..b7f28ca 100644 --- a/schema/numerics/004.json +++ b/schema/numerics/004.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", @@ -29,7 +29,7 @@ "command": "004", "to": "alice", "params": [ - "chat.example.com", + "neoirc.example.com", "0.1.0", "o", "imnst+ov" diff --git a/schema/numerics/322.json b/schema/numerics/322.json index 3f4d288..759a4f1 100644 --- a/schema/numerics/322.json +++ b/schema/numerics/322.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/numerics/323.json b/schema/numerics/323.json index 310a061..5886ac4 100644 --- a/schema/numerics/323.json +++ b/schema/numerics/323.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/numerics/332.json b/schema/numerics/332.json index aebb9c6..7f6a4bc 100644 --- a/schema/numerics/332.json +++ b/schema/numerics/332.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", @@ -40,7 +40,7 @@ "#general" ], "body": [ - "Welcome to the chat" + "Welcome to the channel" ] } ] diff --git a/schema/numerics/353.json b/schema/numerics/353.json index ddf7f4e..4ea85ee 100644 --- a/schema/numerics/353.json +++ b/schema/numerics/353.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/numerics/366.json b/schema/numerics/366.json index 2b5d17a..50e0c6f 100644 --- a/schema/numerics/366.json +++ b/schema/numerics/366.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/numerics/372.json b/schema/numerics/372.json index 58ddb05..34efb6b 100644 --- a/schema/numerics/372.json +++ b/schema/numerics/372.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/numerics/375.json b/schema/numerics/375.json index 4fbe45a..d866edc 100644 --- a/schema/numerics/375.json +++ b/schema/numerics/375.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/numerics/376.json b/schema/numerics/376.json index 5082517..ad24150 100644 --- a/schema/numerics/376.json +++ b/schema/numerics/376.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/numerics/401.json b/schema/numerics/401.json index 3213156..ca473e8 100644 --- a/schema/numerics/401.json +++ b/schema/numerics/401.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/numerics/403.json b/schema/numerics/403.json index bf06774..5a39171 100644 --- a/schema/numerics/403.json +++ b/schema/numerics/403.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/numerics/433.json b/schema/numerics/433.json index 1f4930e..a77dd61 100644 --- a/schema/numerics/433.json +++ b/schema/numerics/433.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/numerics/442.json b/schema/numerics/442.json index a36518b..2a772d3 100644 --- a/schema/numerics/442.json +++ b/schema/numerics/442.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/schema/numerics/482.json b/schema/numerics/482.json index fb03924..2cea333 100644 --- a/schema/numerics/482.json +++ b/schema/numerics/482.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.eeqj.de/sneak/chat/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", diff --git a/web/dist/app.js b/web/dist/app.js index 0cd5a41..3205565 100644 --- a/web/dist/app.js +++ b/web/dist/app.js @@ -1,2 +1,2 @@ -(()=>{var Y=(a=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(a,{get:(f,p)=>(typeof require<"u"?require:f)[p]}):a)(function(a){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+a+'" is not supported')});var o=Y("preact"),s=Y("preact/hooks"),se="/api/v1",oe=15,ae=3e3,ce=1e4;function u(a,f={}){let p=localStorage.getItem("chat_token"),y={"Content-Type":"application/json",...f.headers||{}};p&&(y.Authorization=`Bearer ${p}`);let{signal:i,...h}=f;return fetch(se+a,{...h,headers:y,signal:i}).then(async d=>{let v=await d.json().catch(()=>null);if(!d.ok)throw{status:d.status,data:v};return v})}function X(a){return new Date(a).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"})}function Z(a){let f=0;for(let y=0;y{u("/server").then(m=>{m.name&&$(m.name),m.motd&&d(m.motd)}).catch(()=>{}),localStorage.getItem("chat_token")&&u("/state").then(m=>a(m.nick)).catch(()=>localStorage.removeItem("chat_token")),g.current?.focus()},[]),(0,o.h)("div",{class:"login-screen"},(0,o.h)("h1",null,v),h&&(0,o.h)("div",{class:"motd"},h),(0,o.h)("form",{onSubmit:async T=>{T.preventDefault(),i("");try{let m=await u("/session",{method:"POST",body:JSON.stringify({nick:f.trim()})});localStorage.setItem("chat_token",m.token),a(m.nick)}catch(m){i(m.data?.error||"Connection failed")}}},(0,o.h)("input",{ref:g,type:"text",placeholder:"Choose a nickname...",value:f,onInput:T=>p(T.target.value),maxLength:32,autoFocus:!0}),(0,o.h)("button",{type:"submit"},"Connect")),y&&(0,o.h)("div",{class:"error"},y))}function de({msg:a}){return a.system?(0,o.h)("div",{class:"message system"},(0,o.h)("span",{class:"timestamp"},X(a.ts)),(0,o.h)("span",{class:"content"},a.text)):(0,o.h)("div",{class:"message"},(0,o.h)("span",{class:"timestamp"},X(a.ts)),(0,o.h)("span",{class:"nick",style:{color:Z(a.from)}},a.from),(0,o.h)("span",{class:"content"},a.text))}function le(){let[a,f]=(0,s.useState)(!1),[p,y]=(0,s.useState)(""),[i,h]=(0,s.useState)([{type:"server",name:"Server"}]),[d,v]=(0,s.useState)(0),[$,g]=(0,s.useState)({Server:[]}),[N,T]=(0,s.useState)({}),[m,x]=(0,s.useState)({}),[J,_]=(0,s.useState)({}),[j,L]=(0,s.useState)(""),[C,D]=(0,s.useState)(""),[ee,U]=(0,s.useState)(!0),M=(0,s.useRef)(0),V=(0,s.useRef)(new Set),W=(0,s.useRef)(null),w=(0,s.useRef)(i),K=(0,s.useRef)(d),R=(0,s.useRef)(p),B=(0,s.useRef)(),F=(0,s.useRef)();(0,s.useEffect)(()=>{w.current=i},[i]),(0,s.useEffect)(()=>{K.current=d},[d]),(0,s.useEffect)(()=>{R.current=p},[p]),(0,s.useEffect)(()=>{let e=i.filter(t=>t.type==="channel").map(t=>t.name);localStorage.setItem("chat_channels",JSON.stringify(e))},[i]),(0,s.useEffect)(()=>{let e=i[d];e&&_(t=>({...t,[e.name]:0}))},[d,i]);let b=(0,s.useCallback)((e,t)=>{if(t.id&&V.current.has(t.id))return;t.id&&V.current.add(t.id),g(r=>({...r,[e]:[...r[e]||[],t]}));let n=w.current[K.current];(!n||n.name!==e)&&_(r=>({...r,[e]:(r[e]||0)+1}))},[]),S=(0,s.useCallback)((e,t)=>{g(n=>({...n,[e]:[...n[e]||[],{id:"sys-"+Date.now()+"-"+Math.random(),ts:new Date().toISOString(),text:t,system:!0}]}))},[]),I=(0,s.useCallback)(e=>{let t=e.replace("#","");u(`/channels/${t}/members`).then(n=>{T(r=>({...r,[e]:n}))}).catch(()=>{})},[]),E=(0,s.useCallback)(e=>{let t=Array.isArray(e.body)?e.body.join(` -`):"",n={id:e.id,ts:e.ts,from:e.from,to:e.to,command:e.command};switch(e.command){case"PRIVMSG":case"NOTICE":{let r={...n,text:t,system:!1},c=e.to;if(c&&c.startsWith("#"))b(c,r);else{let l=e.from===R.current?e.to:e.from;h(O=>O.find(Q=>Q.type==="dm"&&Q.name===l)?O:[...O,{type:"dm",name:l}]),b(l,r)}break}case"JOIN":{let r=`${e.from} has joined ${e.to}`;e.to&&b(e.to,{...n,text:r,system:!0}),e.to&&e.to.startsWith("#")&&I(e.to);break}case"PART":{let r=t?": "+t:"",c=`${e.from} has left ${e.to}${r}`;e.to&&b(e.to,{...n,text:c,system:!0}),e.to&&e.to.startsWith("#")&&I(e.to);break}case"QUIT":{let r=t?": "+t:"",c=`${e.from} has quit${r}`;w.current.forEach(l=>{l.type==="channel"&&b(l.name,{...n,text:c,system:!0})});break}case"NICK":{let r=Array.isArray(e.body)?e.body[0]:t,c=`${e.from} is now known as ${r}`;w.current.forEach(l=>{l.type==="channel"&&b(l.name,{...n,text:c,system:!0})}),e.from===R.current&&r&&y(r),w.current.forEach(l=>{l.type==="channel"&&I(l.name)});break}case"TOPIC":{let r=`${e.from} set the topic: ${t}`;e.to&&(b(e.to,{...n,text:r,system:!0}),x(c=>({...c,[e.to]:t})));break}case"375":case"372":case"376":b("Server",{...n,text:t,system:!0});break;default:b("Server",{...n,text:t||e.command,system:!0})}},[b,I]);(0,s.useEffect)(()=>{if(!a)return;let e=!0;return(async()=>{for(;e;)try{let n=new AbortController;W.current=n;let r=await u(`/messages?after=${M.current}&timeout=${oe}`,{signal:n.signal});if(!e)break;if(U(!0),r.messages)for(let c of r.messages)E(c);r.last_id>M.current&&(M.current=r.last_id)}catch(n){if(!e)break;if(n.name==="AbortError")continue;U(!1),await new Promise(r=>setTimeout(r,ae))}})(),()=>{e=!1,W.current?.abort()}},[a,E]),(0,s.useEffect)(()=>{if(!a)return;let e=i[d];if(!e||e.type!=="channel")return;I(e.name);let t=setInterval(()=>I(e.name),ce);return()=>clearInterval(t)},[a,d,i,I]),(0,s.useEffect)(()=>{B.current?.scrollIntoView({behavior:"smooth"})},[$,d]),(0,s.useEffect)(()=>{F.current?.focus()},[d]),(0,s.useEffect)(()=>{if(!a)return;let e=i[d];!e||e.type!=="channel"||u("/channels").then(t=>{let n=t.find(r=>r.name===e.name);n&&n.topic&&x(r=>({...r,[e.name]:n.topic}))}).catch(()=>{})},[a,d,i]);let te=(0,s.useCallback)(async e=>{y(e),f(!0),S("Server",`Connected as ${e}`);let t=JSON.parse(localStorage.getItem("chat_channels")||"[]");for(let n of t)try{await u("/messages",{method:"POST",body:JSON.stringify({command:"JOIN",to:n})}),h(r=>r.find(c=>c.type==="channel"&&c.name===n)?r:[...r,{type:"channel",name:n}])}catch{}},[S]),P=async e=>{if(e){e=e.trim(),e.startsWith("#")||(e="#"+e);try{await u("/messages",{method:"POST",body:JSON.stringify({command:"JOIN",to:e})}),h(t=>t.find(n=>n.type==="channel"&&n.name===e)?t:[...t,{type:"channel",name:e}]),v(i.length);try{let t=await u(`/history?target=${encodeURIComponent(e)}&limit=50`);if(Array.isArray(t))for(let n of t)E(n)}catch{}D("")}catch(t){S("Server",`Failed to join ${e}: ${t.data?.error||"error"}`)}}},G=async e=>{try{await u("/messages",{method:"POST",body:JSON.stringify({command:"PART",to:e})})}catch{}h(t=>t.filter(n=>!(n.type==="channel"&&n.name===e))),v(0)},ne=e=>{let t=i[e];t.type==="channel"?G(t.name):t.type==="dm"&&(h(n=>n.filter((r,c)=>c!==e)),d>=e&&v(Math.max(0,d-1)))},q=e=>{h(n=>n.find(r=>r.type==="dm"&&r.name===e)?n:[...n,{type:"dm",name:e}]);let t=i.findIndex(n=>n.type==="dm"&&n.name===e);v(t>=0?t:i.length)},z=async()=>{let e=j.trim();if(!e)return;L("");let t=i[d];if(!(!t||t.type==="server")){if(e.startsWith("/")){let n=e.split(" "),r=n[0].toLowerCase();if(r==="/join"&&n[1]){P(n[1]);return}if(r==="/part"){t.type==="channel"&&G(t.name);return}if(r==="/msg"&&n[1]&&n.slice(2).join(" ")){let c=n[1],l=n.slice(2).join(" ");try{await u("/messages",{method:"POST",body:JSON.stringify({command:"PRIVMSG",to:c,body:[l]})}),q(c)}catch(O){S("Server",`DM failed: ${O.data?.error||"error"}`)}return}if(r==="/nick"&&n[1]){try{await u("/messages",{method:"POST",body:JSON.stringify({command:"NICK",body:[n[1]]})})}catch(c){S("Server",`Nick change failed: ${c.data?.error||"error"}`)}return}if(r==="/topic"&&t.type==="channel"){let c=n.slice(1).join(" ");try{await u("/messages",{method:"POST",body:JSON.stringify({command:"TOPIC",to:t.name,body:[c]})})}catch(l){S("Server",`Topic failed: ${l.data?.error||"error"}`)}return}S("Server",`Unknown command: ${r}`);return}try{await u("/messages",{method:"POST",body:JSON.stringify({command:"PRIVMSG",to:t.name,body:[e]})})}catch(n){S(t.name,`Send failed: ${n.data?.error||"error"}`)}}};if(!a)return(0,o.h)(ie,{onLogin:te});let k=i[d]||i[0],re=$[k.name]||[],H=N[k.name]||[],A=m[k.name]||"";return(0,o.h)("div",{class:"app"},(0,o.h)("div",{class:"tab-bar"},!ee&&(0,o.h)("div",{class:"connection-status"},"\u26A0 Reconnecting..."),i.map((e,t)=>(0,o.h)("div",{class:`tab ${t===d?"active":""}`,onClick:()=>v(t)},e.type==="dm"?`\u2192${e.name}`:e.name,J[e.name]>0&&t!==d&&(0,o.h)("span",{class:"unread-badge"},J[e.name]),e.type!=="server"&&(0,o.h)("span",{class:"close-btn",onClick:n=>{n.stopPropagation(),ne(t)}},"\xD7"))),(0,o.h)("div",{class:"join-dialog"},(0,o.h)("input",{placeholder:"#channel",value:C,onInput:e=>D(e.target.value),onKeyDown:e=>e.key==="Enter"&&P(C)}),(0,o.h)("button",{onClick:()=>P(C)},"Join"))),k.type==="channel"&&A&&(0,o.h)("div",{class:"topic-bar",title:A},A),(0,o.h)("div",{class:"content"},(0,o.h)("div",{class:"messages-pane"},(0,o.h)("div",{class:k.type==="server"?"server-messages":"messages"},re.map(e=>(0,o.h)(de,{msg:e})),(0,o.h)("div",{ref:B})),k.type!=="server"&&(0,o.h)("div",{class:"input-bar"},(0,o.h)("input",{ref:F,placeholder:`Message ${k.name}...`,value:j,onInput:e=>L(e.target.value),onKeyDown:e=>e.key==="Enter"&&z()}),(0,o.h)("button",{onClick:z},"Send"))),k.type==="channel"&&(0,o.h)("div",{class:"user-list"},(0,o.h)("h3",null,"Users (",H.length,")"),H.map(e=>(0,o.h)("div",{class:"user",onClick:()=>q(e.nick),style:{color:Z(e.nick)}},e.nick)))))}(0,o.render)((0,o.h)(le,null),document.getElementById("root"));})(); +(()=>{var Y=(a=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(a,{get:(f,p)=>(typeof require<"u"?require:f)[p]}):a)(function(a){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+a+'" is not supported')});var o=Y("preact"),s=Y("preact/hooks"),se="/api/v1",oe=15,ae=3e3,ce=1e4;function u(a,f={}){let p=localStorage.getItem("neoirc_token"),y={"Content-Type":"application/json",...f.headers||{}};p&&(y.Authorization=`Bearer ${p}`);let{signal:i,...h}=f;return fetch(se+a,{...h,headers:y,signal:i}).then(async d=>{let v=await d.json().catch(()=>null);if(!d.ok)throw{status:d.status,data:v};return v})}function X(a){return new Date(a).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"})}function Z(a){let f=0;for(let y=0;y{u("/server").then(m=>{m.name&&$(m.name),m.motd&&d(m.motd)}).catch(()=>{}),localStorage.getItem("neoirc_token")&&u("/state").then(m=>a(m.nick)).catch(()=>localStorage.removeItem("neoirc_token")),g.current?.focus()},[]),(0,o.h)("div",{class:"login-screen"},(0,o.h)("h1",null,v),h&&(0,o.h)("div",{class:"motd"},h),(0,o.h)("form",{onSubmit:async T=>{T.preventDefault(),i("");try{let m=await u("/session",{method:"POST",body:JSON.stringify({nick:f.trim()})});localStorage.setItem("neoirc_token",m.token),a(m.nick)}catch(m){i(m.data?.error||"Connection failed")}}},(0,o.h)("input",{ref:g,type:"text",placeholder:"Choose a nickname...",value:f,onInput:T=>p(T.target.value),maxLength:32,autoFocus:!0}),(0,o.h)("button",{type:"submit"},"Connect")),y&&(0,o.h)("div",{class:"error"},y))}function de({msg:a}){return a.system?(0,o.h)("div",{class:"message system"},(0,o.h)("span",{class:"timestamp"},X(a.ts)),(0,o.h)("span",{class:"content"},a.text)):(0,o.h)("div",{class:"message"},(0,o.h)("span",{class:"timestamp"},X(a.ts)),(0,o.h)("span",{class:"nick",style:{color:Z(a.from)}},a.from),(0,o.h)("span",{class:"content"},a.text))}function le(){let[a,f]=(0,s.useState)(!1),[p,y]=(0,s.useState)(""),[i,h]=(0,s.useState)([{type:"server",name:"Server"}]),[d,v]=(0,s.useState)(0),[$,g]=(0,s.useState)({Server:[]}),[N,T]=(0,s.useState)({}),[m,x]=(0,s.useState)({}),[J,_]=(0,s.useState)({}),[j,L]=(0,s.useState)(""),[C,D]=(0,s.useState)(""),[ee,U]=(0,s.useState)(!0),M=(0,s.useRef)(0),V=(0,s.useRef)(new Set),W=(0,s.useRef)(null),w=(0,s.useRef)(i),K=(0,s.useRef)(d),R=(0,s.useRef)(p),B=(0,s.useRef)(),F=(0,s.useRef)();(0,s.useEffect)(()=>{w.current=i},[i]),(0,s.useEffect)(()=>{K.current=d},[d]),(0,s.useEffect)(()=>{R.current=p},[p]),(0,s.useEffect)(()=>{let e=i.filter(t=>t.type==="channel").map(t=>t.name);localStorage.setItem("neoirc_channels",JSON.stringify(e))},[i]),(0,s.useEffect)(()=>{let e=i[d];e&&_(t=>({...t,[e.name]:0}))},[d,i]);let b=(0,s.useCallback)((e,t)=>{if(t.id&&V.current.has(t.id))return;t.id&&V.current.add(t.id),g(r=>({...r,[e]:[...r[e]||[],t]}));let n=w.current[K.current];(!n||n.name!==e)&&_(r=>({...r,[e]:(r[e]||0)+1}))},[]),S=(0,s.useCallback)((e,t)=>{g(n=>({...n,[e]:[...n[e]||[],{id:"sys-"+Date.now()+"-"+Math.random(),ts:new Date().toISOString(),text:t,system:!0}]}))},[]),I=(0,s.useCallback)(e=>{let t=e.replace("#","");u(`/channels/${t}/members`).then(n=>{T(r=>({...r,[e]:n}))}).catch(()=>{})},[]),E=(0,s.useCallback)(e=>{let t=Array.isArray(e.body)?e.body.join(` +`):"",n={id:e.id,ts:e.ts,from:e.from,to:e.to,command:e.command};switch(e.command){case"PRIVMSG":case"NOTICE":{let r={...n,text:t,system:!1},c=e.to;if(c&&c.startsWith("#"))b(c,r);else{let l=e.from===R.current?e.to:e.from;h(O=>O.find(Q=>Q.type==="dm"&&Q.name===l)?O:[...O,{type:"dm",name:l}]),b(l,r)}break}case"JOIN":{let r=`${e.from} has joined ${e.to}`;e.to&&b(e.to,{...n,text:r,system:!0}),e.to&&e.to.startsWith("#")&&I(e.to);break}case"PART":{let r=t?": "+t:"",c=`${e.from} has left ${e.to}${r}`;e.to&&b(e.to,{...n,text:c,system:!0}),e.to&&e.to.startsWith("#")&&I(e.to);break}case"QUIT":{let r=t?": "+t:"",c=`${e.from} has quit${r}`;w.current.forEach(l=>{l.type==="channel"&&b(l.name,{...n,text:c,system:!0})});break}case"NICK":{let r=Array.isArray(e.body)?e.body[0]:t,c=`${e.from} is now known as ${r}`;w.current.forEach(l=>{l.type==="channel"&&b(l.name,{...n,text:c,system:!0})}),e.from===R.current&&r&&y(r),w.current.forEach(l=>{l.type==="channel"&&I(l.name)});break}case"TOPIC":{let r=`${e.from} set the topic: ${t}`;e.to&&(b(e.to,{...n,text:r,system:!0}),x(c=>({...c,[e.to]:t})));break}case"375":case"372":case"376":b("Server",{...n,text:t,system:!0});break;default:b("Server",{...n,text:t||e.command,system:!0})}},[b,I]);(0,s.useEffect)(()=>{if(!a)return;let e=!0;return(async()=>{for(;e;)try{let n=new AbortController;W.current=n;let r=await u(`/messages?after=${M.current}&timeout=${oe}`,{signal:n.signal});if(!e)break;if(U(!0),r.messages)for(let c of r.messages)E(c);r.last_id>M.current&&(M.current=r.last_id)}catch(n){if(!e)break;if(n.name==="AbortError")continue;U(!1),await new Promise(r=>setTimeout(r,ae))}})(),()=>{e=!1,W.current?.abort()}},[a,E]),(0,s.useEffect)(()=>{if(!a)return;let e=i[d];if(!e||e.type!=="channel")return;I(e.name);let t=setInterval(()=>I(e.name),ce);return()=>clearInterval(t)},[a,d,i,I]),(0,s.useEffect)(()=>{B.current?.scrollIntoView({behavior:"smooth"})},[$,d]),(0,s.useEffect)(()=>{F.current?.focus()},[d]),(0,s.useEffect)(()=>{if(!a)return;let e=i[d];!e||e.type!=="channel"||u("/channels").then(t=>{let n=t.find(r=>r.name===e.name);n&&n.topic&&x(r=>({...r,[e.name]:n.topic}))}).catch(()=>{})},[a,d,i]);let te=(0,s.useCallback)(async e=>{y(e),f(!0),S("Server",`Connected as ${e}`);let t=JSON.parse(localStorage.getItem("neoirc_channels")||"[]");for(let n of t)try{await u("/messages",{method:"POST",body:JSON.stringify({command:"JOIN",to:n})}),h(r=>r.find(c=>c.type==="channel"&&c.name===n)?r:[...r,{type:"channel",name:n}])}catch{}},[S]),P=async e=>{if(e){e=e.trim(),e.startsWith("#")||(e="#"+e);try{await u("/messages",{method:"POST",body:JSON.stringify({command:"JOIN",to:e})}),h(t=>t.find(n=>n.type==="channel"&&n.name===e)?t:[...t,{type:"channel",name:e}]),v(i.length);try{let t=await u(`/history?target=${encodeURIComponent(e)}&limit=50`);if(Array.isArray(t))for(let n of t)E(n)}catch{}D("")}catch(t){S("Server",`Failed to join ${e}: ${t.data?.error||"error"}`)}}},G=async e=>{try{await u("/messages",{method:"POST",body:JSON.stringify({command:"PART",to:e})})}catch{}h(t=>t.filter(n=>!(n.type==="channel"&&n.name===e))),v(0)},ne=e=>{let t=i[e];t.type==="channel"?G(t.name):t.type==="dm"&&(h(n=>n.filter((r,c)=>c!==e)),d>=e&&v(Math.max(0,d-1)))},q=e=>{h(n=>n.find(r=>r.type==="dm"&&r.name===e)?n:[...n,{type:"dm",name:e}]);let t=i.findIndex(n=>n.type==="dm"&&n.name===e);v(t>=0?t:i.length)},z=async()=>{let e=j.trim();if(!e)return;L("");let t=i[d];if(!(!t||t.type==="server")){if(e.startsWith("/")){let n=e.split(" "),r=n[0].toLowerCase();if(r==="/join"&&n[1]){P(n[1]);return}if(r==="/part"){t.type==="channel"&&G(t.name);return}if(r==="/msg"&&n[1]&&n.slice(2).join(" ")){let c=n[1],l=n.slice(2).join(" ");try{await u("/messages",{method:"POST",body:JSON.stringify({command:"PRIVMSG",to:c,body:[l]})}),q(c)}catch(O){S("Server",`DM failed: ${O.data?.error||"error"}`)}return}if(r==="/nick"&&n[1]){try{await u("/messages",{method:"POST",body:JSON.stringify({command:"NICK",body:[n[1]]})})}catch(c){S("Server",`Nick change failed: ${c.data?.error||"error"}`)}return}if(r==="/topic"&&t.type==="channel"){let c=n.slice(1).join(" ");try{await u("/messages",{method:"POST",body:JSON.stringify({command:"TOPIC",to:t.name,body:[c]})})}catch(l){S("Server",`Topic failed: ${l.data?.error||"error"}`)}return}S("Server",`Unknown command: ${r}`);return}try{await u("/messages",{method:"POST",body:JSON.stringify({command:"PRIVMSG",to:t.name,body:[e]})})}catch(n){S(t.name,`Send failed: ${n.data?.error||"error"}`)}}};if(!a)return(0,o.h)(ie,{onLogin:te});let k=i[d]||i[0],re=$[k.name]||[],H=N[k.name]||[],A=m[k.name]||"";return(0,o.h)("div",{class:"app"},(0,o.h)("div",{class:"tab-bar"},!ee&&(0,o.h)("div",{class:"connection-status"},"\u26A0 Reconnecting..."),i.map((e,t)=>(0,o.h)("div",{class:`tab ${t===d?"active":""}`,onClick:()=>v(t)},e.type==="dm"?`\u2192${e.name}`:e.name,J[e.name]>0&&t!==d&&(0,o.h)("span",{class:"unread-badge"},J[e.name]),e.type!=="server"&&(0,o.h)("span",{class:"close-btn",onClick:n=>{n.stopPropagation(),ne(t)}},"\xD7"))),(0,o.h)("div",{class:"join-dialog"},(0,o.h)("input",{placeholder:"#channel",value:C,onInput:e=>D(e.target.value),onKeyDown:e=>e.key==="Enter"&&P(C)}),(0,o.h)("button",{onClick:()=>P(C)},"Join"))),k.type==="channel"&&A&&(0,o.h)("div",{class:"topic-bar",title:A},A),(0,o.h)("div",{class:"content"},(0,o.h)("div",{class:"messages-pane"},(0,o.h)("div",{class:k.type==="server"?"server-messages":"messages"},re.map(e=>(0,o.h)(de,{msg:e})),(0,o.h)("div",{ref:B})),k.type!=="server"&&(0,o.h)("div",{class:"input-bar"},(0,o.h)("input",{ref:F,placeholder:`Message ${k.name}...`,value:j,onInput:e=>L(e.target.value),onKeyDown:e=>e.key==="Enter"&&z()}),(0,o.h)("button",{onClick:z},"Send"))),k.type==="channel"&&(0,o.h)("div",{class:"user-list"},(0,o.h)("h3",null,"Users (",H.length,")"),H.map(e=>(0,o.h)("div",{class:"user",onClick:()=>q(e.nick),style:{color:Z(e.nick)}},e.nick)))))}(0,o.render)((0,o.h)(le,null),document.getElementById("root"));})(); diff --git a/web/dist/index.html b/web/dist/index.html index fee4d65..282793f 100644 --- a/web/dist/index.html +++ b/web/dist/index.html @@ -3,7 +3,7 @@ - Chat + NeoIRC diff --git a/web/src/app.jsx b/web/src/app.jsx index 41ed141..47ef442 100644 --- a/web/src/app.jsx +++ b/web/src/app.jsx @@ -7,7 +7,7 @@ const RECONNECT_DELAY = 3000; const MEMBER_REFRESH_INTERVAL = 10000; function api(path, opts = {}) { - const token = localStorage.getItem('chat_token'); + const token = localStorage.getItem('neoirc_token'); const headers = { 'Content-Type': 'application/json', ...(opts.headers || {}) }; if (token) headers['Authorization'] = `Bearer ${token}`; const { signal, ...rest } = opts; @@ -34,7 +34,7 @@ function LoginScreen({ onLogin }) { const [nick, setNick] = useState(''); const [error, setError] = useState(''); const [motd, setMotd] = useState(''); - const [serverName, setServerName] = useState('Chat'); + const [serverName, setServerName] = useState('NeoIRC'); const inputRef = useRef(); useEffect(() => { @@ -42,9 +42,9 @@ function LoginScreen({ onLogin }) { if (s.name) setServerName(s.name); if (s.motd) setMotd(s.motd); }).catch(() => {}); - const saved = localStorage.getItem('chat_token'); + const saved = localStorage.getItem('neoirc_token'); if (saved) { - api('/state').then(u => onLogin(u.nick)).catch(() => localStorage.removeItem('chat_token')); + api('/state').then(u => onLogin(u.nick)).catch(() => localStorage.removeItem('neoirc_token')); } inputRef.current?.focus(); }, []); @@ -57,7 +57,7 @@ function LoginScreen({ onLogin }) { method: 'POST', body: JSON.stringify({ nick: nick.trim() }) }); - localStorage.setItem('chat_token', res.token); + localStorage.setItem('neoirc_token', res.token); onLogin(res.nick); } catch (err) { setError(err.data?.error || 'Connection failed'); @@ -132,7 +132,7 @@ function App() { // Persist joined channels useEffect(() => { const channels = tabs.filter(t => t.type === 'channel').map(t => t.name); - localStorage.setItem('chat_channels', JSON.stringify(channels)); + localStorage.setItem('neoirc_channels', JSON.stringify(channels)); }, [tabs]); // Clear unread on tab switch @@ -321,7 +321,7 @@ function App() { setLoggedIn(true); addSystemMessage('Server', `Connected as ${userNick}`); // Auto-rejoin saved channels - const saved = JSON.parse(localStorage.getItem('chat_channels') || '[]'); + const saved = JSON.parse(localStorage.getItem('neoirc_channels') || '[]'); for (const ch of saved) { try { await api('/messages', { method: 'POST', body: JSON.stringify({ command: 'JOIN', to: ch }) }); diff --git a/web/src/index.html b/web/src/index.html index fee4d65..282793f 100644 --- a/web/src/index.html +++ b/web/src/index.html @@ -3,7 +3,7 @@ - Chat + NeoIRC