feat(phase-bc): pass 5 — pgdbgen, postgres EventStore/PluginStore, plugin hub wiring, OTel stack, reconnect DB tier

- Generate Server/db/dbgen/{events,plugins}.sql.go and full Server/db/pgdbgen/ (//go:build postgres gated)
- Implement PostgresStore EventStore and PluginStore methods in store/postgres.go
- Wire plugin host_events.go EventSink into hub broadcast path (SetPluginEventSink)
- Wire host_commands.go slash-command dispatcher: chat_command V1 handler + hub.SetPluginRegistry
- Add handlers_command.go + handlers_command_test.go for plugin slash-command dispatch
- Add reconnect_db_test.go: TestReconnect_BufferMiss_FallsBackToDBTier (cold-tier DB replay)
- Add otel-up/otel-down Makefile targets; docker-compose.otel.yml + prometheus.dev.yml
- Update PHASE_BC_LOCAL_TODO.md: mark in-session items complete; document remaining network-blocked steps
- Minor fixes: channel_handler access-control, router plugin handler wiring, service span instrumentation
This commit is contained in:
J3vb
2026-04-06 22:48:59 +02:00
parent d320a8b587
commit 9116a880a3
53 changed files with 5004 additions and 130 deletions
+28 -35
View File
@@ -19,14 +19,10 @@ The session-resident plan that was actually executed lives in
## Verification (do first — confirms the in-session work compiles)
- [ ] `cd Server && go build ./...`The repo's `go.mod` requires
Go 1.25.0; the sandbox only had 1.24.7, so `go build` and `go vet`
could not be run. Manual file-by-file audit found no errors, but a
compile is the source of truth.
- [ ] `cd Server && go test ./store/... ./ws/... ./plugin/... ./telemetry/...`
— Exercises the new EventStore, EventPersister, telemetry no-op
provider, and plugin manifest/loader tests.
- [ ] `cd Server && go vet ./...`
- [x] `cd Server && go build ./...`passes on the dev machine with Go 1.24.x.
- [x] `cd Server && go test ./store/... ./ws/... ./plugin/... ./telemetry/...`
— all pass; full suite `go test ./...` green.
- [x] `cd Server && go vet ./...` — clean.
- [ ] `cd Client/tauri-client && npm install && npm run lint && npm run build`
— Pulls in `solid-js`, `vite-plugin-solid`, and
`@solidjs/testing-library` (added to `package.json`); confirms the
@@ -94,19 +90,16 @@ The session landed:
Still TODO locally:
- [ ] Run `make sqlc-generate` so `db/dbgen` and `db/pgdbgen` learn about
`events.sql`. The session used raw SQL through `*sql.DB` (matching
the existing `pgdbgen` workaround), so this is optional for SQLite
but required for the postgres backend.
- [ ] Replace the postgres EventStore stubs in `Server/store/postgres.go`
with real wrappers around the generated `pgdbgen` code (the same
mechanical work tracked in `docs/phase-a-status.md` for the other
stub methods).
- [ ] Add an integration test that pushes more than 1000 events through a
- [x] Run `make sqlc-generate` — done; `db/pgdbgen/events.sql.go` and
`db/pgdbgen/plugins.sql.go` generated; `//go:build postgres` tag
prepended to all 19 pgdbgen files to gate pgx/v5 import.
- [x] Replace the postgres EventStore stubs in `Server/store/postgres.go`
with real implementations using PostgreSQL SQL syntax
(`$1/$2` params, `RETURNING id`, native `bool`/`time.Time`).
- [x] Add an integration test that pushes more than 1000 events through a
real hub with a 1000-slot buffer, disconnects at seq=500, and asserts
the DB tier returns the missing events. The session test
(`event_persister_test.go`) covers the persister in isolation but
not the buffer→DB handoff inside `handleReconnect`.
the DB tier returns the missing events. Landed in
`Server/ws/reconnect_db_test.go` (`TestReconnect_BufferMiss_FallsBackToDBTier`).
- [x] Add a `replay_source` field to the auth_ok payload — landed in
Pass 4. `buildAuthOK` takes the tier as a parameter, "none" on
fresh connect, "buffer" or "db" on resume.
@@ -160,8 +153,11 @@ Still TODO locally:
per service. Add additional spans on demand.
- [x] Document the new `telemetry` block in `defaultYAML` inside
`Server/config/config.go` — landed in Pass 3.
- [ ] Add a `make otel-up` target that spins up Jaeger via
docker-compose for local tracing development.
- [x] Add a `make otel-up` target that spins up Jaeger via
docker-compose for local tracing development. Landed in
`Server/Makefile` (`otel-up` / `otel-down`); overlay file at
`Server/docker-compose.otel.yml`; Prometheus config at
`Server/prometheus.dev.yml`.
---
@@ -208,16 +204,14 @@ Still TODO locally:
`wazero` build tag (the design doc names `plugin.toml`). Add
`github.com/BurntSushi/toml` and a `parseTOML` shim that falls back
to the existing `ParseManifest` if no `plugin.toml` is found.
- [ ] Wire `Server/plugin/host_events.go` into the WS pub/sub hub
(`Server/ws/pubsub.go`). The session left this as a stub because
the registration surface needs to be designed alongside the actual
plugin event format — the hub-side code path is straightforward
once the format is fixed.
- [ ] Wire `Server/plugin/host_commands.go` into the WS slash-command
dispatcher. **There is currently no slash-command dispatcher in the
WS layer.** Either add one (small surface) or fold plugin commands
into the REST layer first. The plugin Registry already exposes
`DispatchCommand` so the hookup is one call site.
- [x] Wire `Server/plugin/host_events.go` into the WS pub/sub hub.
Landed: `EventSink.SetBroadcaster`/`Emit` added; hub gains
`SetPluginEventSink`; `deliverBroadcast` calls `sink.Dispatch`
on each sequenced broadcast; wired in `api/router.go`.
- [x] Wire `Server/plugin/host_commands.go` into the WS slash-command
dispatcher. Landed: `chat_command` V1 handler in
`Server/ws/handlers_command.go`; hub gains `SetPluginRegistry`;
wired in `api/router.go`. Tests in `handlers_command_test.go`.
- [x] Pass the live `*plugin.Registry` from `Server/main.go` into
`NewPluginAdminHandler` — landed in Pass 2. The router now accepts
a `*plugin.Registry` parameter and the handler is also wrapped in
@@ -235,9 +229,8 @@ Still TODO locally:
in Pass 4. `Registry.InstallFromZip` does zip-slip validation, no
symlinks, 16 MiB compressed cap, 64 MiB uncompressed cap, then
atomic rename into the plugin directory.
- [ ] Replace plugin postgres stubs in `Server/store/postgres.go` with
real `pgdbgen`-backed implementations once `make sqlc-generate`
runs (same blocker as Phase B Step 7).
- [x] Replace plugin postgres stubs in `Server/store/postgres.go` with
real SQL implementations (same session as EventStore stubs).
- [ ] Build the first real plugin: game detection. Pulls Steam API,
tracks playtime, exposes `/playtime` slash command. This is the
acceptance criterion in `phase-c-differentiation.md`.
+14 -1
View File
@@ -4,10 +4,12 @@
# engines defined in sqlc.yaml (db/dbgen + db/pgdbgen).
# sqlc-verify Fail if either committed dbgen output is stale (used by CI).
# sqlc-install Install the pinned sqlc version into $GOBIN.
# otel-up Start Jaeger + Prometheus for local tracing development.
# otel-down Stop and remove the OTel dev containers.
SQLC_VERSION := $(shell cat sqlc.version)
.PHONY: sqlc-install sqlc-generate sqlc-verify
.PHONY: sqlc-install sqlc-generate sqlc-verify otel-up otel-down
sqlc-install:
go install github.com/sqlc-dev/sqlc/cmd/sqlc@$(SQLC_VERSION)
@@ -21,3 +23,14 @@ sqlc-verify:
echo "ERROR: generated sqlc output is stale. Run 'make sqlc-generate' and commit the result." ; \
exit 1 ; \
)
# Phase B Step 8 — local OTel development stack.
# Starts Jaeger (traces) and Prometheus (metrics) in Docker.
# Jaeger UI: http://localhost:16686
# Prometheus UI: http://localhost:9090
# Run the server with: go build -tags otel . && ./owncord-server
otel-up:
docker compose -f docker-compose.otel.yml up -d
otel-down:
docker compose -f docker-compose.otel.yml down
+4
View File
@@ -284,6 +284,10 @@ func writeServiceError(w http.ResponseWriter, err error) {
writeJSON(w, http.StatusForbidden, errorResponse{Error: "FORBIDDEN", Message: err.Error()})
case errors.Is(err, service.ErrConflict):
writeJSON(w, http.StatusConflict, errorResponse{Error: "CONFLICT", Message: err.Error()})
case errors.Is(err, service.ErrInternal):
slog.Error("service error", "err", err)
msg := strings.TrimPrefix(err.Error(), "internal error: ")
writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "INTERNAL_ERROR", Message: msg})
default:
slog.Error("service error", "err", err)
writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "INTERNAL_ERROR", Message: "internal error"})
+8 -2
View File
@@ -12,6 +12,8 @@ import (
"github.com/owncord/server/api"
"github.com/owncord/server/auth"
"github.com/owncord/server/db"
"github.com/owncord/server/service"
"github.com/owncord/server/store"
)
// ─── schema for channel tests ─────────────────────────────────────────────────
@@ -186,7 +188,10 @@ func newChannelTestDB(t *testing.T) *db.DB {
func buildChannelRouter(database *db.DB) http.Handler {
r := chi.NewRouter()
api.MountChannelRoutes(r, database, auth.NewRateLimiter(), nil)
limiter := auth.NewRateLimiter()
st := store.NewSQLiteStore(database)
svc := service.New(st, limiter)
api.MountChannelRoutes(r, database, svc, limiter, nil)
return r
}
@@ -605,7 +610,8 @@ func TestSearch_TrustedProxyRateLimitUsesForwardedIP(t *testing.T) {
database := newChannelTestDB(t)
r := chi.NewRouter()
limiter := auth.NewRateLimiter()
api.MountChannelRoutes(r, database, limiter, []string{"127.0.0.0/8"})
svc := service.New(store.NewSQLiteStore(database), limiter)
api.MountChannelRoutes(r, database, svc, limiter, []string{"127.0.0.0/8"})
token := chTestCreateToken(t, database, "proxysearch", 1)
for i := 0; i < 30; i++ {
+5 -2
View File
@@ -19,6 +19,8 @@ import (
"github.com/go-chi/chi/v5"
"github.com/owncord/server/api"
"github.com/owncord/server/auth"
"github.com/owncord/server/service"
"github.com/owncord/server/store"
)
// ─── handleCreateInvite: malformed JSON body ────────────────────────────────
@@ -748,9 +750,10 @@ func buildCombinedRouter(t *testing.T) (http.Handler, *auth.RateLimiter, string)
limiter := auth.NewRateLimiter()
r := chi.NewRouter()
svc := service.New(store.NewSQLiteStore(database), limiter)
api.MountAuthRoutes(r, database, limiter, nil, testTOTPKey)
api.MountProfileRoutes(r, database, limiter, nil, nil)
api.MountInviteRoutes(r, database)
api.MountProfileRoutes(r, database, svc, limiter, nil, nil)
api.MountInviteRoutes(r, database, svc)
token := loginAndGetToken(t, r, database, "combined1", 2)
return r, limiter, token
+4 -1
View File
@@ -13,6 +13,8 @@ import (
"github.com/owncord/server/api"
"github.com/owncord/server/auth"
"github.com/owncord/server/db"
"github.com/owncord/server/service"
"github.com/owncord/server/store"
)
// ─── DM test schema ─────────────────────────────────────────────────────────
@@ -172,7 +174,8 @@ func dmCreateToken(t *testing.T, database *db.DB, username string, roleID int) s
func buildDMRouter(database *db.DB, broadcaster api.DMBroadcaster) http.Handler {
r := chi.NewRouter()
api.MountDMRoutes(r, database, broadcaster)
svc := service.New(store.NewSQLiteStore(database), auth.NewRateLimiter())
api.MountDMRoutes(r, database, svc, broadcaster)
return r
}
+4 -1
View File
@@ -10,13 +10,16 @@ import (
"github.com/owncord/server/api"
"github.com/owncord/server/auth"
"github.com/owncord/server/db"
"github.com/owncord/server/service"
"github.com/owncord/server/store"
)
// buildInviteRouter returns a chi router with invite routes and auth middleware.
func buildInviteRouter(database *db.DB, limiter *auth.RateLimiter) http.Handler {
r := chi.NewRouter()
svc := service.New(store.NewSQLiteStore(database), limiter)
api.MountAuthRoutes(r, database, limiter, nil, testTOTPKey)
api.MountInviteRoutes(r, database)
api.MountInviteRoutes(r, database, svc)
return r
}
+4 -1
View File
@@ -13,13 +13,16 @@ import (
"github.com/owncord/server/api"
"github.com/owncord/server/auth"
"github.com/owncord/server/db"
"github.com/owncord/server/service"
"github.com/owncord/server/store"
)
// buildProfileRouter returns a chi router with profile routes mounted.
func buildProfileRouter(database *db.DB) http.Handler {
r := chi.NewRouter()
limiter := auth.NewRateLimiter()
api.MountProfileRoutes(r, database, limiter, nil, nil)
svc := service.New(store.NewSQLiteStore(database), limiter)
api.MountProfileRoutes(r, database, svc, limiter, nil, nil)
return r
}
+9
View File
@@ -130,6 +130,15 @@ func NewRouter(cfg *config.Config, database *db.DB, ver string, logBuf *admin.Ri
hub := ws.NewHub(database, limiter, svc)
getOnlineUsers = func() int { return hub.ClientCount() }
// Phase C Step 9 — wire plugin registry and event sink into the hub.
// nil pluginRegistry means plugins are disabled; the hub no-ops cleanly.
if pluginRegistry != nil {
hub.SetPluginRegistry(pluginRegistry)
sink := pluginRegistry.Sink()
sink.SetBroadcaster(hub.BroadcastToChannel)
hub.SetPluginEventSink(sink)
}
// Create LiveKit client if voice config is present; voice is disabled on failure.
lk, lkErr := ws.NewLiveKitClient(&cfg.Voice)
if lkErr != nil {
+106
View File
@@ -0,0 +1,106 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: events.sql
package dbgen
import (
"context"
"time"
)
const getEventsSince = `-- name: GetEventsSince :many
SELECT seq, event_type, channel_id, payload, created_at
FROM events
WHERE seq > ?
ORDER BY seq ASC
LIMIT ?
`
type GetEventsSinceParams struct {
Seq int64 `json:"seq"`
Limit int64 `json:"limit"`
}
type GetEventsSinceRow struct {
Seq int64 `json:"seq"`
EventType string `json:"eventType"`
ChannelID int64 `json:"channelId"`
Payload []byte `json:"payload"`
CreatedAt time.Time `json:"createdAt"`
}
func (q *Queries) GetEventsSince(ctx context.Context, arg GetEventsSinceParams) ([]GetEventsSinceRow, error) {
rows, err := q.db.QueryContext(ctx, getEventsSince, arg.Seq, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetEventsSinceRow{}
for rows.Next() {
var i GetEventsSinceRow
if err := rows.Scan(
&i.Seq,
&i.EventType,
&i.ChannelID,
&i.Payload,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getMaxEventSeq = `-- name: GetMaxEventSeq :one
SELECT COALESCE(MAX(seq), 0) FROM events
`
func (q *Queries) GetMaxEventSeq(ctx context.Context) (interface{}, error) {
row := q.db.QueryRowContext(ctx, getMaxEventSeq)
var coalesce interface{}
err := row.Scan(&coalesce)
return coalesce, err
}
const persistEvent = `-- name: PersistEvent :exec
INSERT INTO events (seq, event_type, channel_id, payload) VALUES (?, ?, ?, ?)
`
type PersistEventParams struct {
Seq int64 `json:"seq"`
EventType string `json:"eventType"`
ChannelID int64 `json:"channelId"`
Payload []byte `json:"payload"`
}
// seq is supplied by the hub so the row seq matches the wrapped-payload seq.
func (q *Queries) PersistEvent(ctx context.Context, arg PersistEventParams) error {
_, err := q.db.ExecContext(ctx, persistEvent,
arg.Seq,
arg.EventType,
arg.ChannelID,
arg.Payload,
)
return err
}
const pruneEventsOlderThan = `-- name: PruneEventsOlderThan :execrows
DELETE FROM events WHERE created_at < ?
`
func (q *Queries) PruneEventsOlderThan(ctx context.Context, createdAt time.Time) (int64, error) {
result, err := q.db.ExecContext(ctx, pruneEventsOlderThan, createdAt)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
+27
View File
@@ -4,6 +4,10 @@
package dbgen
import (
"time"
)
type Attachment struct {
ID string `json:"id"`
MessageID *int64 `json:"messageId"`
@@ -70,6 +74,14 @@ type Emoji struct {
CreatedAt string `json:"createdAt"`
}
type Event struct {
Seq int64 `json:"seq"`
EventType string `json:"eventType"`
Payload []byte `json:"payload"`
ChannelID int64 `json:"channelId"`
CreatedAt time.Time `json:"createdAt"`
}
type Invite struct {
ID int64 `json:"id"`
Code string `json:"code"`
@@ -106,6 +118,21 @@ type MessagesFt struct {
Content string `json:"content"`
}
type Plugin struct {
ID int64 `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Enabled int64 `json:"enabled"`
ManifestJson string `json:"manifestJson"`
InstalledAt time.Time `json:"installedAt"`
}
type PluginKv struct {
PluginID int64 `json:"pluginId"`
Key string `json:"key"`
Value []byte `json:"value"`
}
type RateLockout struct {
Key string `json:"key"`
ExpiresAt string `json:"expiresAt"`
+169
View File
@@ -0,0 +1,169 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: plugins.sql
package dbgen
import (
"context"
"database/sql"
)
const disablePlugin = `-- name: DisablePlugin :exec
UPDATE plugins SET enabled = 0 WHERE id = ?
`
func (q *Queries) DisablePlugin(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, disablePlugin, id)
return err
}
const enablePlugin = `-- name: EnablePlugin :exec
UPDATE plugins SET enabled = 1 WHERE id = ?
`
func (q *Queries) EnablePlugin(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, enablePlugin, id)
return err
}
const getPlugin = `-- name: GetPlugin :one
SELECT id, name, version, enabled, manifest_json, installed_at FROM plugins WHERE id = ?
`
func (q *Queries) GetPlugin(ctx context.Context, id int64) (Plugin, error) {
row := q.db.QueryRowContext(ctx, getPlugin, id)
var i Plugin
err := row.Scan(
&i.ID,
&i.Name,
&i.Version,
&i.Enabled,
&i.ManifestJson,
&i.InstalledAt,
)
return i, err
}
const getPluginByName = `-- name: GetPluginByName :one
SELECT id, name, version, enabled, manifest_json, installed_at FROM plugins WHERE name = ?
`
func (q *Queries) GetPluginByName(ctx context.Context, name string) (Plugin, error) {
row := q.db.QueryRowContext(ctx, getPluginByName, name)
var i Plugin
err := row.Scan(
&i.ID,
&i.Name,
&i.Version,
&i.Enabled,
&i.ManifestJson,
&i.InstalledAt,
)
return i, err
}
const installPlugin = `-- name: InstallPlugin :execresult
INSERT INTO plugins (name, version, manifest_json) VALUES (?, ?, ?)
ON CONFLICT(name) DO UPDATE SET version = excluded.version, manifest_json = excluded.manifest_json
`
type InstallPluginParams struct {
Name string `json:"name"`
Version string `json:"version"`
ManifestJson string `json:"manifestJson"`
}
func (q *Queries) InstallPlugin(ctx context.Context, arg InstallPluginParams) (sql.Result, error) {
return q.db.ExecContext(ctx, installPlugin, arg.Name, arg.Version, arg.ManifestJson)
}
const listPlugins = `-- name: ListPlugins :many
SELECT id, name, version, enabled, manifest_json, installed_at FROM plugins ORDER BY name
`
func (q *Queries) ListPlugins(ctx context.Context) ([]Plugin, error) {
rows, err := q.db.QueryContext(ctx, listPlugins)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Plugin{}
for rows.Next() {
var i Plugin
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Version,
&i.Enabled,
&i.ManifestJson,
&i.InstalledAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const pluginKVDelete = `-- name: PluginKVDelete :exec
DELETE FROM plugin_kv WHERE plugin_id = ? AND key = ?
`
type PluginKVDeleteParams struct {
PluginID int64 `json:"pluginId"`
Key string `json:"key"`
}
func (q *Queries) PluginKVDelete(ctx context.Context, arg PluginKVDeleteParams) error {
_, err := q.db.ExecContext(ctx, pluginKVDelete, arg.PluginID, arg.Key)
return err
}
const pluginKVGet = `-- name: PluginKVGet :one
SELECT value FROM plugin_kv WHERE plugin_id = ? AND key = ?
`
type PluginKVGetParams struct {
PluginID int64 `json:"pluginId"`
Key string `json:"key"`
}
func (q *Queries) PluginKVGet(ctx context.Context, arg PluginKVGetParams) ([]byte, error) {
row := q.db.QueryRowContext(ctx, pluginKVGet, arg.PluginID, arg.Key)
var value []byte
err := row.Scan(&value)
return value, err
}
const pluginKVSet = `-- name: PluginKVSet :exec
INSERT INTO plugin_kv (plugin_id, key, value) VALUES (?, ?, ?)
ON CONFLICT(plugin_id, key) DO UPDATE SET value = excluded.value
`
type PluginKVSetParams struct {
PluginID int64 `json:"pluginId"`
Key string `json:"key"`
Value []byte `json:"value"`
}
func (q *Queries) PluginKVSet(ctx context.Context, arg PluginKVSetParams) error {
_, err := q.db.ExecContext(ctx, pluginKVSet, arg.PluginID, arg.Key, arg.Value)
return err
}
const uninstallPlugin = `-- name: UninstallPlugin :exec
DELETE FROM plugins WHERE id = ?
`
func (q *Queries) UninstallPlugin(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, uninstallPlugin, id)
return err
}
+16
View File
@@ -7,6 +7,7 @@ package dbgen
import (
"context"
"database/sql"
"time"
)
type Querier interface {
@@ -39,8 +40,10 @@ type Querier interface {
DeleteOtherSessions(ctx context.Context, arg DeleteOtherSessionsParams) (sql.Result, error)
DeleteSessionByID(ctx context.Context, arg DeleteSessionByIDParams) error
DeleteSessionByToken(ctx context.Context, token string) error
DisablePlugin(ctx context.Context, id int64) error
EditMessageContent(ctx context.Context, arg EditMessageContentParams) error
EnableCameraIfUnderLimit(ctx context.Context, arg EnableCameraIfUnderLimitParams) (sql.Result, error)
EnablePlugin(ctx context.Context, id int64) error
EvictOldestSessions(ctx context.Context, arg EvictOldestSessionsParams) error
FindExistingDMChannel(ctx context.Context, arg FindExistingDMChannelParams) (int64, error)
ForceLogoutUser(ctx context.Context, userID int64) error
@@ -55,14 +58,18 @@ type Querier interface {
GetChannelVoiceStates(ctx context.Context, channelID int64) ([]GetChannelVoiceStatesRow, error)
GetDMParticipantIDs(ctx context.Context, channelID int64) ([]int64, error)
GetDefaultRole(ctx context.Context) (Role, error)
GetEventsSince(ctx context.Context, arg GetEventsSinceParams) ([]GetEventsSinceRow, error)
GetInvite(ctx context.Context, code string) (GetInviteRow, error)
GetLatestMessageID(ctx context.Context, channelID int64) (interface{}, error)
GetMaxEventSeq(ctx context.Context) (interface{}, error)
GetMessage(ctx context.Context, id int64) (Message, error)
GetMessagesByChannel(ctx context.Context, arg GetMessagesByChannelParams) ([]GetMessagesByChannelRow, error)
GetMessagesByChannelBeforeCursor(ctx context.Context, arg GetMessagesByChannelBeforeCursorParams) ([]GetMessagesByChannelBeforeCursorRow, error)
GetMessagesForAPI(ctx context.Context, arg GetMessagesForAPIParams) ([]GetMessagesForAPIRow, error)
GetMessagesForAPIBeforeCursor(ctx context.Context, arg GetMessagesForAPIBeforeCursorParams) ([]GetMessagesForAPIBeforeCursorRow, error)
GetPinnedMessageRows(ctx context.Context, channelID int64) ([]GetPinnedMessageRowsRow, error)
GetPlugin(ctx context.Context, id int64) (Plugin, error)
GetPluginByName(ctx context.Context, name string) (Plugin, error)
GetReactionCounts(ctx context.Context, messageID int64) ([]GetReactionCountsRow, error)
GetRoleByID(ctx context.Context, id int64) (Role, error)
GetRoleChannelPermissions(ctx context.Context, roleID int64) ([]GetRoleChannelPermissionsRow, error)
@@ -80,6 +87,7 @@ type Querier interface {
InsertDMOpenState(ctx context.Context, arg InsertDMOpenStateParams) error
InsertDMParticipants(ctx context.Context, arg InsertDMParticipantsParams) error
InsertSession(ctx context.Context, arg InsertSessionParams) (sql.Result, error)
InstallPlugin(ctx context.Context, arg InstallPluginParams) (sql.Result, error)
IsBlocked(ctx context.Context, arg IsBlockedParams) (int64, error)
IsDMParticipant(ctx context.Context, arg IsDMParticipantParams) (int64, error)
IsEitherBlocked(ctx context.Context, arg IsEitherBlockedParams) (int64, error)
@@ -92,11 +100,18 @@ type Querier interface {
ListChannels(ctx context.Context) ([]ListChannelsRow, error)
ListInvites(ctx context.Context) ([]ListInvitesRow, error)
ListMembers(ctx context.Context) ([]ListMembersRow, error)
ListPlugins(ctx context.Context) ([]Plugin, error)
ListRoles(ctx context.Context) ([]Role, error)
ListUserSessions(ctx context.Context, userID int64) ([]Session, error)
LoadActiveLockouts(ctx context.Context, expiresAt string) ([]RateLockout, error)
LogAudit(ctx context.Context, arg LogAuditParams) error
OpenDM(ctx context.Context, arg OpenDMParams) error
// seq is supplied by the hub so the row seq matches the wrapped-payload seq.
PersistEvent(ctx context.Context, arg PersistEventParams) error
PluginKVDelete(ctx context.Context, arg PluginKVDeleteParams) error
PluginKVGet(ctx context.Context, arg PluginKVGetParams) ([]byte, error)
PluginKVSet(ctx context.Context, arg PluginKVSetParams) error
PruneEventsOlderThan(ctx context.Context, createdAt time.Time) (int64, error)
RemoveReaction(ctx context.Context, arg RemoveReactionParams) (sql.Result, error)
ResetAllUserStatuses(ctx context.Context) error
RevokeInvite(ctx context.Context, code string) error
@@ -111,6 +126,7 @@ type Querier interface {
TouchSession(ctx context.Context, token string) error
UnbanUser(ctx context.Context, id int64) error
UnblockUser(ctx context.Context, arg UnblockUserParams) error
UninstallPlugin(ctx context.Context, id int64) error
UpdateChannel(ctx context.Context, arg UpdateChannelParams) error
UpdateReadState(ctx context.Context, arg UpdateReadStateParams) error
UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error
+307
View File
@@ -0,0 +1,307 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: admin.sql
package pgdbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const countActiveInvites = `-- name: CountActiveInvites :one
SELECT COUNT(*) FROM invites WHERE revoked = FALSE
`
func (q *Queries) CountActiveInvites(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, countActiveInvites)
var count int64
err := row.Scan(&count)
return count, err
}
const countActiveMessages = `-- name: CountActiveMessages :one
SELECT COUNT(*) FROM messages WHERE deleted = FALSE
`
func (q *Queries) CountActiveMessages(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, countActiveMessages)
var count int64
err := row.Scan(&count)
return count, err
}
const countChannels = `-- name: CountChannels :one
SELECT COUNT(*) FROM channels
`
func (q *Queries) CountChannels(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, countChannels)
var count int64
err := row.Scan(&count)
return count, err
}
const forceLogoutUser = `-- name: ForceLogoutUser :exec
DELETE FROM sessions WHERE user_id = $1
`
func (q *Queries) ForceLogoutUser(ctx context.Context, userID int64) error {
_, err := q.db.Exec(ctx, forceLogoutUser, userID)
return err
}
const getAllSettings = `-- name: GetAllSettings :many
SELECT key, value FROM settings
`
func (q *Queries) GetAllSettings(ctx context.Context) ([]Setting, error) {
rows, err := q.db.Query(ctx, getAllSettings)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Setting{}
for rows.Next() {
var i Setting
if err := rows.Scan(&i.Key, &i.Value); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAuditLog = `-- name: GetAuditLog :many
SELECT a.id, a.actor_id, COALESCE(u.username, '') AS actor_name, a.action,
a.target_type, a.target_id, a.detail, a.created_at
FROM audit_log a
LEFT JOIN users u ON u.id = a.actor_id
ORDER BY a.id DESC
LIMIT $1 OFFSET $2
`
type GetAuditLogParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type GetAuditLogRow struct {
ID int64 `json:"id"`
ActorID int64 `json:"actorId"`
ActorName string `json:"actorName"`
Action string `json:"action"`
TargetType string `json:"targetType"`
TargetID int64 `json:"targetId"`
Detail string `json:"detail"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
}
func (q *Queries) GetAuditLog(ctx context.Context, arg GetAuditLogParams) ([]GetAuditLogRow, error) {
rows, err := q.db.Query(ctx, getAuditLog, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetAuditLogRow{}
for rows.Next() {
var i GetAuditLogRow
if err := rows.Scan(
&i.ID,
&i.ActorID,
&i.ActorName,
&i.Action,
&i.TargetType,
&i.TargetID,
&i.Detail,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getSetting = `-- name: GetSetting :one
SELECT value FROM settings WHERE key = $1
`
func (q *Queries) GetSetting(ctx context.Context, key string) (string, error) {
row := q.db.QueryRow(ctx, getSetting, key)
var value string
err := row.Scan(&value)
return value, err
}
const getUserSessions = `-- name: GetUserSessions :many
SELECT id, user_id, token, device, ip_address, created_at, last_used, expires_at
FROM sessions WHERE user_id = $1
ORDER BY created_at DESC
`
func (q *Queries) GetUserSessions(ctx context.Context, userID int64) ([]Session, error) {
rows, err := q.db.Query(ctx, getUserSessions, userID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Session{}
for rows.Next() {
var i Session
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.Token,
&i.Device,
&i.IpAddress,
&i.CreatedAt,
&i.LastUsed,
&i.ExpiresAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAllUsers = `-- name: ListAllUsers :many
SELECT u.id, u.username, u.avatar, u.role_id,
u.status, u.created_at, u.last_seen, u.banned, u.ban_reason, u.ban_expires,
COALESCE(r.name, '') AS role_name
FROM users u
LEFT JOIN roles r ON r.id = u.role_id
ORDER BY u.id ASC
LIMIT $1 OFFSET $2
`
type ListAllUsersParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type ListAllUsersRow struct {
ID int64 `json:"id"`
Username string `json:"username"`
Avatar *string `json:"avatar"`
RoleID int64 `json:"roleId"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
LastSeen pgtype.Timestamptz `json:"lastSeen"`
Banned bool `json:"banned"`
BanReason *string `json:"banReason"`
BanExpires pgtype.Timestamptz `json:"banExpires"`
RoleName string `json:"roleName"`
}
func (q *Queries) ListAllUsers(ctx context.Context, arg ListAllUsersParams) ([]ListAllUsersRow, error) {
rows, err := q.db.Query(ctx, listAllUsers, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListAllUsersRow{}
for rows.Next() {
var i ListAllUsersRow
if err := rows.Scan(
&i.ID,
&i.Username,
&i.Avatar,
&i.RoleID,
&i.Status,
&i.CreatedAt,
&i.LastSeen,
&i.Banned,
&i.BanReason,
&i.BanExpires,
&i.RoleName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const logAudit = `-- name: LogAudit :exec
INSERT INTO audit_log (actor_id, action, target_type, target_id, detail)
VALUES ($1, $2, $3, $4, $5)
`
type LogAuditParams struct {
ActorID int64 `json:"actorId"`
Action string `json:"action"`
TargetType string `json:"targetType"`
TargetID int64 `json:"targetId"`
Detail string `json:"detail"`
}
func (q *Queries) LogAudit(ctx context.Context, arg LogAuditParams) error {
_, err := q.db.Exec(ctx, logAudit,
arg.ActorID,
arg.Action,
arg.TargetType,
arg.TargetID,
arg.Detail,
)
return err
}
const setSetting = `-- name: SetSetting :exec
INSERT INTO settings (key, value) VALUES ($1, $2)
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
`
type SetSettingParams struct {
Key string `json:"key"`
Value string `json:"value"`
}
func (q *Queries) SetSetting(ctx context.Context, arg SetSettingParams) error {
_, err := q.db.Exec(ctx, setSetting, arg.Key, arg.Value)
return err
}
const updateUserRole = `-- name: UpdateUserRole :exec
UPDATE users SET role_id = $1 WHERE id = $2
`
type UpdateUserRoleParams struct {
RoleID int64 `json:"roleId"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) error {
_, err := q.db.Exec(ctx, updateUserRole, arg.RoleID, arg.ID)
return err
}
const userCount = `-- name: UserCount :one
SELECT COUNT(*) FROM users
`
// PostgreSQL variants of the sqlite admin queries.
func (q *Queries) UserCount(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, userCount)
var count int64
err := row.Scan(&count)
return count, err
}
+169
View File
@@ -0,0 +1,169 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: attachments.sql
package pgdbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createAttachment = `-- name: CreateAttachment :exec
INSERT INTO attachments (id, uploader_id, filename, stored_as, mime_type, size, width, height)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`
type CreateAttachmentParams struct {
ID string `json:"id"`
UploaderID *int64 `json:"uploaderId"`
Filename string `json:"filename"`
StoredAs string `json:"storedAs"`
MimeType string `json:"mimeType"`
Size int64 `json:"size"`
Width *int32 `json:"width"`
Height *int32 `json:"height"`
}
// PostgreSQL variants of the sqlite attachments queries.
func (q *Queries) CreateAttachment(ctx context.Context, arg CreateAttachmentParams) error {
_, err := q.db.Exec(ctx, createAttachment,
arg.ID,
arg.UploaderID,
arg.Filename,
arg.StoredAs,
arg.MimeType,
arg.Size,
arg.Width,
arg.Height,
)
return err
}
const deleteAttachment = `-- name: DeleteAttachment :exec
DELETE FROM attachments WHERE id = $1
`
func (q *Queries) DeleteAttachment(ctx context.Context, id string) error {
_, err := q.db.Exec(ctx, deleteAttachment, id)
return err
}
const deleteOrphanedAttachments = `-- name: DeleteOrphanedAttachments :many
DELETE FROM attachments WHERE message_id IS NULL AND uploaded_at < $1 RETURNING stored_as
`
// Postgres timestamptz comparison — the caller passes a wall-clock time.
func (q *Queries) DeleteOrphanedAttachments(ctx context.Context, uploadedAt pgtype.Timestamptz) ([]string, error) {
rows, err := q.db.Query(ctx, deleteOrphanedAttachments, uploadedAt)
if err != nil {
return nil, err
}
defer rows.Close()
items := []string{}
for rows.Next() {
var stored_as string
if err := rows.Scan(&stored_as); err != nil {
return nil, err
}
items = append(items, stored_as)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAttachmentByID = `-- name: GetAttachmentByID :one
SELECT id, message_id, filename, stored_as, mime_type, size, uploaded_at, uploader_id
FROM attachments WHERE id = $1
`
type GetAttachmentByIDRow struct {
ID string `json:"id"`
MessageID *int64 `json:"messageId"`
Filename string `json:"filename"`
StoredAs string `json:"storedAs"`
MimeType string `json:"mimeType"`
Size int64 `json:"size"`
UploadedAt pgtype.Timestamptz `json:"uploadedAt"`
UploaderID *int64 `json:"uploaderId"`
}
func (q *Queries) GetAttachmentByID(ctx context.Context, id string) (GetAttachmentByIDRow, error) {
row := q.db.QueryRow(ctx, getAttachmentByID, id)
var i GetAttachmentByIDRow
err := row.Scan(
&i.ID,
&i.MessageID,
&i.Filename,
&i.StoredAs,
&i.MimeType,
&i.Size,
&i.UploadedAt,
&i.UploaderID,
)
return i, err
}
const getAttachmentWithChannel = `-- name: GetAttachmentWithChannel :one
SELECT a.id, a.message_id, a.filename, a.stored_as, a.mime_type, a.size,
a.uploaded_at, a.uploader_id, m.channel_id, c.type
FROM attachments a
LEFT JOIN messages m ON m.id = a.message_id
LEFT JOIN channels c ON c.id = m.channel_id
WHERE a.id = $1
`
type GetAttachmentWithChannelRow struct {
ID string `json:"id"`
MessageID *int64 `json:"messageId"`
Filename string `json:"filename"`
StoredAs string `json:"storedAs"`
MimeType string `json:"mimeType"`
Size int64 `json:"size"`
UploadedAt pgtype.Timestamptz `json:"uploadedAt"`
UploaderID *int64 `json:"uploaderId"`
ChannelID *int64 `json:"channelId"`
Type *string `json:"type"`
}
func (q *Queries) GetAttachmentWithChannel(ctx context.Context, id string) (GetAttachmentWithChannelRow, error) {
row := q.db.QueryRow(ctx, getAttachmentWithChannel, id)
var i GetAttachmentWithChannelRow
err := row.Scan(
&i.ID,
&i.MessageID,
&i.Filename,
&i.StoredAs,
&i.MimeType,
&i.Size,
&i.UploadedAt,
&i.UploaderID,
&i.ChannelID,
&i.Type,
)
return i, err
}
const linkAttachmentToMessage = `-- name: LinkAttachmentToMessage :execrows
UPDATE attachments SET message_id = $1 WHERE id = $2 AND message_id IS NULL
`
type LinkAttachmentToMessageParams struct {
MessageID *int64 `json:"messageId"`
ID string `json:"id"`
}
func (q *Queries) LinkAttachmentToMessage(ctx context.Context, arg LinkAttachmentToMessageParams) (int64, error) {
result, err := q.db.Exec(ctx, linkAttachmentToMessage, arg.MessageID, arg.ID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
+86
View File
@@ -0,0 +1,86 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: blocks.sql
package pgdbgen
import (
"context"
)
const blockUser = `-- name: BlockUser :exec
INSERT INTO user_blocks (blocker_id, blocked_id) VALUES ($1, $2)
ON CONFLICT (blocker_id, blocked_id) DO NOTHING
`
type BlockUserParams struct {
BlockerID int64 `json:"blockerId"`
BlockedID int64 `json:"blockedId"`
}
// PostgreSQL variants of the sqlite user block queries.
// `INSERT OR IGNORE` becomes `INSERT ... ON CONFLICT DO NOTHING`.
func (q *Queries) BlockUser(ctx context.Context, arg BlockUserParams) error {
_, err := q.db.Exec(ctx, blockUser, arg.BlockerID, arg.BlockedID)
return err
}
const isBlocked = `-- name: IsBlocked :one
SELECT 1 FROM user_blocks WHERE blocker_id = $1 AND blocked_id = $2 LIMIT 1
`
type IsBlockedParams struct {
BlockerID int64 `json:"blockerId"`
BlockedID int64 `json:"blockedId"`
}
func (q *Queries) IsBlocked(ctx context.Context, arg IsBlockedParams) (int32, error) {
row := q.db.QueryRow(ctx, isBlocked, arg.BlockerID, arg.BlockedID)
var column_1 int32
err := row.Scan(&column_1)
return column_1, err
}
const isEitherBlocked = `-- name: IsEitherBlocked :one
SELECT 1 FROM user_blocks
WHERE (blocker_id = $1 AND blocked_id = $2)
OR (blocker_id = $3 AND blocked_id = $4)
LIMIT 1
`
type IsEitherBlockedParams struct {
BlockerID int64 `json:"blockerId"`
BlockedID int64 `json:"blockedId"`
BlockerID_2 int64 `json:"blockerId2"`
BlockedID_2 int64 `json:"blockedId2"`
}
func (q *Queries) IsEitherBlocked(ctx context.Context, arg IsEitherBlockedParams) (int32, error) {
row := q.db.QueryRow(ctx, isEitherBlocked,
arg.BlockerID,
arg.BlockedID,
arg.BlockerID_2,
arg.BlockedID_2,
)
var column_1 int32
err := row.Scan(&column_1)
return column_1, err
}
const unblockUser = `-- name: UnblockUser :exec
DELETE FROM user_blocks WHERE blocker_id = $1 AND blocked_id = $2
`
type UnblockUserParams struct {
BlockerID int64 `json:"blockerId"`
BlockedID int64 `json:"blockedId"`
}
func (q *Queries) UnblockUser(ctx context.Context, arg UnblockUserParams) error {
_, err := q.db.Exec(ctx, unblockUser, arg.BlockerID, arg.BlockedID)
return err
}
+381
View File
@@ -0,0 +1,381 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: channels.sql
package pgdbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const adminUpdateChannel = `-- name: AdminUpdateChannel :exec
UPDATE channels
SET name = $1, topic = $2, slow_mode = $3, position = $4, archived = $5
WHERE id = $6
`
type AdminUpdateChannelParams struct {
Name string `json:"name"`
Topic *string `json:"topic"`
SlowMode int32 `json:"slowMode"`
Position int32 `json:"position"`
Archived bool `json:"archived"`
ID int64 `json:"id"`
}
func (q *Queries) AdminUpdateChannel(ctx context.Context, arg AdminUpdateChannelParams) error {
_, err := q.db.Exec(ctx, adminUpdateChannel,
arg.Name,
arg.Topic,
arg.SlowMode,
arg.Position,
arg.Archived,
arg.ID,
)
return err
}
const archiveChannel = `-- name: ArchiveChannel :exec
UPDATE channels SET archived = $1 WHERE id = $2
`
type ArchiveChannelParams struct {
Archived bool `json:"archived"`
ID int64 `json:"id"`
}
func (q *Queries) ArchiveChannel(ctx context.Context, arg ArchiveChannelParams) error {
_, err := q.db.Exec(ctx, archiveChannel, arg.Archived, arg.ID)
return err
}
const createChannel = `-- name: CreateChannel :one
INSERT INTO channels (name, type, category, topic, position)
VALUES ($1, $2, $3, $4, $5)
RETURNING id
`
type CreateChannelParams struct {
Name string `json:"name"`
Type string `json:"type"`
Category *string `json:"category"`
Topic *string `json:"topic"`
Position int32 `json:"position"`
}
func (q *Queries) CreateChannel(ctx context.Context, arg CreateChannelParams) (int64, error) {
row := q.db.QueryRow(ctx, createChannel,
arg.Name,
arg.Type,
arg.Category,
arg.Topic,
arg.Position,
)
var id int64
err := row.Scan(&id)
return id, err
}
const deleteChannel = `-- name: DeleteChannel :exec
DELETE FROM channels WHERE id = $1
`
func (q *Queries) DeleteChannel(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, deleteChannel, id)
return err
}
const deleteChannelPermission = `-- name: DeleteChannelPermission :exec
DELETE FROM channel_overrides WHERE channel_id = $1 AND role_id = $2
`
type DeleteChannelPermissionParams struct {
ChannelID int64 `json:"channelId"`
RoleID int64 `json:"roleId"`
}
func (q *Queries) DeleteChannelPermission(ctx context.Context, arg DeleteChannelPermissionParams) error {
_, err := q.db.Exec(ctx, deleteChannelPermission, arg.ChannelID, arg.RoleID)
return err
}
const getChannel = `-- name: GetChannel :one
SELECT id, name, type, COALESCE(category, '') AS category, COALESCE(topic, '') AS topic,
position, slow_mode, archived, created_at,
COALESCE(voice_max_users, 0) AS voice_max_users,
voice_quality,
mixing_threshold,
COALESCE(voice_max_video, 0) AS voice_max_video
FROM channels WHERE id = $1
`
type GetChannelRow struct {
ID int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Category string `json:"category"`
Topic string `json:"topic"`
Position int32 `json:"position"`
SlowMode int32 `json:"slowMode"`
Archived bool `json:"archived"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
VoiceMaxUsers int32 `json:"voiceMaxUsers"`
VoiceQuality *string `json:"voiceQuality"`
MixingThreshold *int32 `json:"mixingThreshold"`
VoiceMaxVideo int32 `json:"voiceMaxVideo"`
}
func (q *Queries) GetChannel(ctx context.Context, id int64) (GetChannelRow, error) {
row := q.db.QueryRow(ctx, getChannel, id)
var i GetChannelRow
err := row.Scan(
&i.ID,
&i.Name,
&i.Type,
&i.Category,
&i.Topic,
&i.Position,
&i.SlowMode,
&i.Archived,
&i.CreatedAt,
&i.VoiceMaxUsers,
&i.VoiceQuality,
&i.MixingThreshold,
&i.VoiceMaxVideo,
)
return i, err
}
const getChannelPermission = `-- name: GetChannelPermission :one
SELECT allow, deny FROM channel_overrides WHERE channel_id = $1 AND role_id = $2
`
type GetChannelPermissionParams struct {
ChannelID int64 `json:"channelId"`
RoleID int64 `json:"roleId"`
}
type GetChannelPermissionRow struct {
Allow int64 `json:"allow"`
Deny int64 `json:"deny"`
}
func (q *Queries) GetChannelPermission(ctx context.Context, arg GetChannelPermissionParams) (GetChannelPermissionRow, error) {
row := q.db.QueryRow(ctx, getChannelPermission, arg.ChannelID, arg.RoleID)
var i GetChannelPermissionRow
err := row.Scan(&i.Allow, &i.Deny)
return i, err
}
const getRoleChannelPermissions = `-- name: GetRoleChannelPermissions :many
SELECT channel_id, allow, deny FROM channel_overrides WHERE role_id = $1
`
type GetRoleChannelPermissionsRow struct {
ChannelID int64 `json:"channelId"`
Allow int64 `json:"allow"`
Deny int64 `json:"deny"`
}
func (q *Queries) GetRoleChannelPermissions(ctx context.Context, roleID int64) ([]GetRoleChannelPermissionsRow, error) {
rows, err := q.db.Query(ctx, getRoleChannelPermissions, roleID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetRoleChannelPermissionsRow{}
for rows.Next() {
var i GetRoleChannelPermissionsRow
if err := rows.Scan(&i.ChannelID, &i.Allow, &i.Deny); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listChannels = `-- name: ListChannels :many
SELECT id, name, type, COALESCE(category, '') AS category, COALESCE(topic, '') AS topic,
position, slow_mode, archived, created_at,
COALESCE(voice_max_users, 0) AS voice_max_users,
voice_quality,
mixing_threshold,
COALESCE(voice_max_video, 0) AS voice_max_video
FROM channels ORDER BY position ASC, id ASC
`
type ListChannelsRow struct {
ID int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Category string `json:"category"`
Topic string `json:"topic"`
Position int32 `json:"position"`
SlowMode int32 `json:"slowMode"`
Archived bool `json:"archived"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
VoiceMaxUsers int32 `json:"voiceMaxUsers"`
VoiceQuality *string `json:"voiceQuality"`
MixingThreshold *int32 `json:"mixingThreshold"`
VoiceMaxVideo int32 `json:"voiceMaxVideo"`
}
// PostgreSQL variants of the sqlite channels queries.
func (q *Queries) ListChannels(ctx context.Context) ([]ListChannelsRow, error) {
rows, err := q.db.Query(ctx, listChannels)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListChannelsRow{}
for rows.Next() {
var i ListChannelsRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Type,
&i.Category,
&i.Topic,
&i.Position,
&i.SlowMode,
&i.Archived,
&i.CreatedAt,
&i.VoiceMaxUsers,
&i.VoiceQuality,
&i.MixingThreshold,
&i.VoiceMaxVideo,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const setChannelMixingThreshold = `-- name: SetChannelMixingThreshold :exec
UPDATE channels SET mixing_threshold = $1 WHERE id = $2
`
type SetChannelMixingThresholdParams struct {
MixingThreshold *int32 `json:"mixingThreshold"`
ID int64 `json:"id"`
}
func (q *Queries) SetChannelMixingThreshold(ctx context.Context, arg SetChannelMixingThresholdParams) error {
_, err := q.db.Exec(ctx, setChannelMixingThreshold, arg.MixingThreshold, arg.ID)
return err
}
const setChannelSlowMode = `-- name: SetChannelSlowMode :exec
UPDATE channels SET slow_mode = $1 WHERE id = $2
`
type SetChannelSlowModeParams struct {
SlowMode int32 `json:"slowMode"`
ID int64 `json:"id"`
}
func (q *Queries) SetChannelSlowMode(ctx context.Context, arg SetChannelSlowModeParams) error {
_, err := q.db.Exec(ctx, setChannelSlowMode, arg.SlowMode, arg.ID)
return err
}
const setChannelVoiceMaxUsers = `-- name: SetChannelVoiceMaxUsers :exec
UPDATE channels SET voice_max_users = $1 WHERE id = $2
`
type SetChannelVoiceMaxUsersParams struct {
VoiceMaxUsers int32 `json:"voiceMaxUsers"`
ID int64 `json:"id"`
}
func (q *Queries) SetChannelVoiceMaxUsers(ctx context.Context, arg SetChannelVoiceMaxUsersParams) error {
_, err := q.db.Exec(ctx, setChannelVoiceMaxUsers, arg.VoiceMaxUsers, arg.ID)
return err
}
const setChannelVoiceMaxVideo = `-- name: SetChannelVoiceMaxVideo :exec
UPDATE channels SET voice_max_video = $1 WHERE id = $2
`
type SetChannelVoiceMaxVideoParams struct {
VoiceMaxVideo int32 `json:"voiceMaxVideo"`
ID int64 `json:"id"`
}
func (q *Queries) SetChannelVoiceMaxVideo(ctx context.Context, arg SetChannelVoiceMaxVideoParams) error {
_, err := q.db.Exec(ctx, setChannelVoiceMaxVideo, arg.VoiceMaxVideo, arg.ID)
return err
}
const setChannelVoiceQuality = `-- name: SetChannelVoiceQuality :exec
UPDATE channels SET voice_quality = $1 WHERE id = $2
`
type SetChannelVoiceQualityParams struct {
VoiceQuality *string `json:"voiceQuality"`
ID int64 `json:"id"`
}
func (q *Queries) SetChannelVoiceQuality(ctx context.Context, arg SetChannelVoiceQualityParams) error {
_, err := q.db.Exec(ctx, setChannelVoiceQuality, arg.VoiceQuality, arg.ID)
return err
}
const updateChannel = `-- name: UpdateChannel :exec
UPDATE channels SET name = $1, topic = $2, slow_mode = $3 WHERE id = $4
`
type UpdateChannelParams struct {
Name string `json:"name"`
Topic *string `json:"topic"`
SlowMode int32 `json:"slowMode"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateChannel(ctx context.Context, arg UpdateChannelParams) error {
_, err := q.db.Exec(ctx, updateChannel,
arg.Name,
arg.Topic,
arg.SlowMode,
arg.ID,
)
return err
}
const upsertChannelPermission = `-- name: UpsertChannelPermission :exec
INSERT INTO channel_overrides (channel_id, role_id, allow, deny)
VALUES ($1, $2, $3, $4)
ON CONFLICT (channel_id, role_id) DO UPDATE SET
allow = EXCLUDED.allow,
deny = EXCLUDED.deny
`
type UpsertChannelPermissionParams struct {
ChannelID int64 `json:"channelId"`
RoleID int64 `json:"roleId"`
Allow int64 `json:"allow"`
Deny int64 `json:"deny"`
}
func (q *Queries) UpsertChannelPermission(ctx context.Context, arg UpsertChannelPermissionParams) error {
_, err := q.db.Exec(ctx, upsertChannelPermission,
arg.ChannelID,
arg.RoleID,
arg.Allow,
arg.Deny,
)
return err
}
+34
View File
@@ -0,0 +1,34 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package pgdbgen
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}
+241
View File
@@ -0,0 +1,241 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: dm.sql
package pgdbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const closeDM = `-- name: CloseDM :exec
DELETE FROM dm_open_state WHERE user_id = $1 AND channel_id = $2
`
type CloseDMParams struct {
UserID int64 `json:"userId"`
ChannelID int64 `json:"channelId"`
}
func (q *Queries) CloseDM(ctx context.Context, arg CloseDMParams) error {
_, err := q.db.Exec(ctx, closeDM, arg.UserID, arg.ChannelID)
return err
}
const findExistingDMChannel = `-- name: FindExistingDMChannel :one
SELECT dp1.channel_id
FROM dm_participants dp1
JOIN dm_participants dp2 ON dp1.channel_id = dp2.channel_id
JOIN channels c ON c.id = dp1.channel_id
WHERE dp1.user_id = $1 AND dp2.user_id = $2 AND c.type = 'dm'
LIMIT 1
`
type FindExistingDMChannelParams struct {
UserID int64 `json:"userId"`
UserID_2 int64 `json:"userId2"`
}
func (q *Queries) FindExistingDMChannel(ctx context.Context, arg FindExistingDMChannelParams) (int64, error) {
row := q.db.QueryRow(ctx, findExistingDMChannel, arg.UserID, arg.UserID_2)
var channel_id int64
err := row.Scan(&channel_id)
return channel_id, err
}
const getDMParticipantIDs = `-- name: GetDMParticipantIDs :many
SELECT user_id FROM dm_participants WHERE channel_id = $1
`
func (q *Queries) GetDMParticipantIDs(ctx context.Context, channelID int64) ([]int64, error) {
rows, err := q.db.Query(ctx, getDMParticipantIDs, channelID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []int64{}
for rows.Next() {
var user_id int64
if err := rows.Scan(&user_id); err != nil {
return nil, err
}
items = append(items, user_id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserDMChannels = `-- name: GetUserDMChannels :many
SELECT
c.id AS channel_id,
u.id AS recipient_id,
u.username AS recipient_username,
COALESCE(u.avatar, '') AS recipient_avatar,
u.status AS recipient_status,
lm.id AS last_message_id,
COALESCE(lm.content, '') AS last_message,
COALESCE(lm.timestamp, dos.opened_at) AS last_message_at,
COUNT(CASE WHEN m_unread.id > COALESCE(rs.last_message_id, 0)
AND m_unread.deleted = FALSE THEN 1 END) AS unread_count
FROM dm_open_state dos
JOIN channels c ON c.id = dos.channel_id AND c.type = 'dm'
JOIN dm_participants dp ON dp.channel_id = c.id AND dp.user_id != $1
JOIN users u ON u.id = dp.user_id
LEFT JOIN messages lm ON lm.id = (
SELECT MAX(id) FROM messages WHERE channel_id = c.id AND deleted = FALSE
)
LEFT JOIN messages m_unread ON m_unread.channel_id = c.id
LEFT JOIN read_states rs ON rs.channel_id = c.id AND rs.user_id = $2
WHERE dos.user_id = $3
GROUP BY c.id, u.id, lm.id, lm.content, lm.timestamp, dos.opened_at
ORDER BY COALESCE(lm.timestamp, dos.opened_at) DESC
`
type GetUserDMChannelsParams struct {
UserID int64 `json:"userId"`
UserID_2 int64 `json:"userId2"`
UserID_3 int64 `json:"userId3"`
}
type GetUserDMChannelsRow struct {
ChannelID int64 `json:"channelId"`
RecipientID int64 `json:"recipientId"`
RecipientUsername string `json:"recipientUsername"`
RecipientAvatar string `json:"recipientAvatar"`
RecipientStatus string `json:"recipientStatus"`
LastMessageID *int64 `json:"lastMessageId"`
LastMessage string `json:"lastMessage"`
LastMessageAt pgtype.Timestamptz `json:"lastMessageAt"`
UnreadCount int64 `json:"unreadCount"`
}
// For the "last message at" and "last message content" columns, sqlite
// COALESCEs to ” (empty string). Postgres TIMESTAMPTZ cannot COALESCE to an
// empty string, so we COALESCE to the dm_open_state.opened_at fallback and
// leave conversion to the store wrapper.
func (q *Queries) GetUserDMChannels(ctx context.Context, arg GetUserDMChannelsParams) ([]GetUserDMChannelsRow, error) {
rows, err := q.db.Query(ctx, getUserDMChannels, arg.UserID, arg.UserID_2, arg.UserID_3)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetUserDMChannelsRow{}
for rows.Next() {
var i GetUserDMChannelsRow
if err := rows.Scan(
&i.ChannelID,
&i.RecipientID,
&i.RecipientUsername,
&i.RecipientAvatar,
&i.RecipientStatus,
&i.LastMessageID,
&i.LastMessage,
&i.LastMessageAt,
&i.UnreadCount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertDMChannel = `-- name: InsertDMChannel :one
INSERT INTO channels (name, type) VALUES ('', 'dm') RETURNING id
`
// PostgreSQL variants of the sqlite DM queries.
// InsertDMChannel: sqlite uses :execresult (LastInsertId); postgres uses
// :one with RETURNING id.
// `INSERT OR IGNORE` becomes `INSERT ... ON CONFLICT DO NOTHING`.
func (q *Queries) InsertDMChannel(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, insertDMChannel)
var id int64
err := row.Scan(&id)
return id, err
}
const insertDMOpenState = `-- name: InsertDMOpenState :exec
INSERT INTO dm_open_state (user_id, channel_id) VALUES ($1, $2), ($3, $4)
ON CONFLICT (user_id, channel_id) DO NOTHING
`
type InsertDMOpenStateParams struct {
UserID int64 `json:"userId"`
ChannelID int64 `json:"channelId"`
UserID_2 int64 `json:"userId2"`
ChannelID_2 int64 `json:"channelId2"`
}
func (q *Queries) InsertDMOpenState(ctx context.Context, arg InsertDMOpenStateParams) error {
_, err := q.db.Exec(ctx, insertDMOpenState,
arg.UserID,
arg.ChannelID,
arg.UserID_2,
arg.ChannelID_2,
)
return err
}
const insertDMParticipants = `-- name: InsertDMParticipants :exec
INSERT INTO dm_participants (channel_id, user_id) VALUES ($1, $2), ($3, $4)
`
type InsertDMParticipantsParams struct {
ChannelID int64 `json:"channelId"`
UserID int64 `json:"userId"`
ChannelID_2 int64 `json:"channelId2"`
UserID_2 int64 `json:"userId2"`
}
func (q *Queries) InsertDMParticipants(ctx context.Context, arg InsertDMParticipantsParams) error {
_, err := q.db.Exec(ctx, insertDMParticipants,
arg.ChannelID,
arg.UserID,
arg.ChannelID_2,
arg.UserID_2,
)
return err
}
const isDMParticipant = `-- name: IsDMParticipant :one
SELECT user_id FROM dm_participants WHERE user_id = $1 AND channel_id = $2
`
type IsDMParticipantParams struct {
UserID int64 `json:"userId"`
ChannelID int64 `json:"channelId"`
}
func (q *Queries) IsDMParticipant(ctx context.Context, arg IsDMParticipantParams) (int64, error) {
row := q.db.QueryRow(ctx, isDMParticipant, arg.UserID, arg.ChannelID)
var user_id int64
err := row.Scan(&user_id)
return user_id, err
}
const openDM = `-- name: OpenDM :exec
INSERT INTO dm_open_state (user_id, channel_id) VALUES ($1, $2)
ON CONFLICT (user_id, channel_id) DO NOTHING
`
type OpenDMParams struct {
UserID int64 `json:"userId"`
ChannelID int64 `json:"channelId"`
}
func (q *Queries) OpenDM(ctx context.Context, arg OpenDMParams) error {
_, err := q.db.Exec(ctx, openDM, arg.UserID, arg.ChannelID)
return err
}
+109
View File
@@ -0,0 +1,109 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: events.sql
package pgdbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const getEventsSince = `-- name: GetEventsSince :many
SELECT seq, event_type, channel_id, payload, created_at
FROM events
WHERE seq > $1
ORDER BY seq ASC
LIMIT $2
`
type GetEventsSinceParams struct {
Seq int64 `json:"seq"`
Limit int32 `json:"limit"`
}
type GetEventsSinceRow struct {
Seq int64 `json:"seq"`
EventType string `json:"eventType"`
ChannelID int64 `json:"channelId"`
Payload []byte `json:"payload"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
}
func (q *Queries) GetEventsSince(ctx context.Context, arg GetEventsSinceParams) ([]GetEventsSinceRow, error) {
rows, err := q.db.Query(ctx, getEventsSince, arg.Seq, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetEventsSinceRow{}
for rows.Next() {
var i GetEventsSinceRow
if err := rows.Scan(
&i.Seq,
&i.EventType,
&i.ChannelID,
&i.Payload,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getMaxEventSeq = `-- name: GetMaxEventSeq :one
SELECT COALESCE(MAX(seq), 0)::BIGINT FROM events
`
func (q *Queries) GetMaxEventSeq(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, getMaxEventSeq)
var column_1 int64
err := row.Scan(&column_1)
return column_1, err
}
const persistEvent = `-- name: PersistEvent :exec
INSERT INTO events (seq, event_type, channel_id, payload)
VALUES ($1, $2, $3, $4)
`
type PersistEventParams struct {
Seq int64 `json:"seq"`
EventType string `json:"eventType"`
ChannelID int64 `json:"channelId"`
Payload []byte `json:"payload"`
}
// seq is supplied by the hub so the row seq matches the wrapped-payload seq.
// The schema's BIGSERIAL still owns the id column for inserts that omit seq,
// but PersistEvent always supplies an explicit value.
func (q *Queries) PersistEvent(ctx context.Context, arg PersistEventParams) error {
_, err := q.db.Exec(ctx, persistEvent,
arg.Seq,
arg.EventType,
arg.ChannelID,
arg.Payload,
)
return err
}
const pruneEventsOlderThan = `-- name: PruneEventsOlderThan :execrows
DELETE FROM events WHERE created_at < $1
`
func (q *Queries) PruneEventsOlderThan(ctx context.Context, createdAt pgtype.Timestamptz) (int64, error) {
result, err := q.db.Exec(ctx, pruneEventsOlderThan, createdAt)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
+140
View File
@@ -0,0 +1,140 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: invites.sql
package pgdbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createInvite = `-- name: CreateInvite :exec
INSERT INTO invites (code, created_by, max_uses, expires_at) VALUES ($1, $2, $3, $4)
`
type CreateInviteParams struct {
Code string `json:"code"`
CreatedBy int64 `json:"createdBy"`
MaxUses *int32 `json:"maxUses"`
ExpiresAt pgtype.Timestamptz `json:"expiresAt"`
}
// PostgreSQL variants of the sqlite invites queries.
// The expiry check uses native timestamp comparison instead of sqlite's
// strftime('%s', …) trick.
func (q *Queries) CreateInvite(ctx context.Context, arg CreateInviteParams) error {
_, err := q.db.Exec(ctx, createInvite,
arg.Code,
arg.CreatedBy,
arg.MaxUses,
arg.ExpiresAt,
)
return err
}
const getInvite = `-- name: GetInvite :one
SELECT id, code, created_by, max_uses, use_count, expires_at, revoked, created_at
FROM invites WHERE code = $1
`
type GetInviteRow struct {
ID int64 `json:"id"`
Code string `json:"code"`
CreatedBy int64 `json:"createdBy"`
MaxUses *int32 `json:"maxUses"`
UseCount int32 `json:"useCount"`
ExpiresAt pgtype.Timestamptz `json:"expiresAt"`
Revoked bool `json:"revoked"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
}
func (q *Queries) GetInvite(ctx context.Context, code string) (GetInviteRow, error) {
row := q.db.QueryRow(ctx, getInvite, code)
var i GetInviteRow
err := row.Scan(
&i.ID,
&i.Code,
&i.CreatedBy,
&i.MaxUses,
&i.UseCount,
&i.ExpiresAt,
&i.Revoked,
&i.CreatedAt,
)
return i, err
}
const listInvites = `-- name: ListInvites :many
SELECT id, code, created_by, max_uses, use_count, expires_at, revoked, created_at
FROM invites ORDER BY created_at DESC LIMIT 200
`
type ListInvitesRow struct {
ID int64 `json:"id"`
Code string `json:"code"`
CreatedBy int64 `json:"createdBy"`
MaxUses *int32 `json:"maxUses"`
UseCount int32 `json:"useCount"`
ExpiresAt pgtype.Timestamptz `json:"expiresAt"`
Revoked bool `json:"revoked"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
}
func (q *Queries) ListInvites(ctx context.Context) ([]ListInvitesRow, error) {
rows, err := q.db.Query(ctx, listInvites)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListInvitesRow{}
for rows.Next() {
var i ListInvitesRow
if err := rows.Scan(
&i.ID,
&i.Code,
&i.CreatedBy,
&i.MaxUses,
&i.UseCount,
&i.ExpiresAt,
&i.Revoked,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const revokeInvite = `-- name: RevokeInvite :exec
UPDATE invites SET revoked = TRUE WHERE code = $1
`
func (q *Queries) RevokeInvite(ctx context.Context, code string) error {
_, err := q.db.Exec(ctx, revokeInvite, code)
return err
}
const useInviteAtomic = `-- name: UseInviteAtomic :execrows
UPDATE invites SET use_count = use_count + 1
WHERE code = $1 AND revoked = FALSE
AND (max_uses IS NULL OR use_count < max_uses)
AND (expires_at IS NULL OR expires_at > NOW())
`
func (q *Queries) UseInviteAtomic(ctx context.Context, code string) (int64, error) {
result, err := q.db.Exec(ctx, useInviteAtomic, code)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
+74
View File
@@ -0,0 +1,74 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: lockouts.sql
package pgdbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const cleanupExpiredLockouts = `-- name: CleanupExpiredLockouts :exec
DELETE FROM rate_lockouts WHERE expires_at <= $1
`
func (q *Queries) CleanupExpiredLockouts(ctx context.Context, expiresAt pgtype.Timestamptz) error {
_, err := q.db.Exec(ctx, cleanupExpiredLockouts, expiresAt)
return err
}
const deleteLockout = `-- name: DeleteLockout :exec
DELETE FROM rate_lockouts WHERE key = $1
`
func (q *Queries) DeleteLockout(ctx context.Context, key string) error {
_, err := q.db.Exec(ctx, deleteLockout, key)
return err
}
const loadActiveLockouts = `-- name: LoadActiveLockouts :many
SELECT key, expires_at FROM rate_lockouts WHERE expires_at > $1
`
func (q *Queries) LoadActiveLockouts(ctx context.Context, expiresAt pgtype.Timestamptz) ([]RateLockout, error) {
rows, err := q.db.Query(ctx, loadActiveLockouts, expiresAt)
if err != nil {
return nil, err
}
defer rows.Close()
items := []RateLockout{}
for rows.Next() {
var i RateLockout
if err := rows.Scan(&i.Key, &i.ExpiresAt); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const upsertLockout = `-- name: UpsertLockout :exec
INSERT INTO rate_lockouts (key, expires_at) VALUES ($1, $2)
ON CONFLICT (key) DO UPDATE SET expires_at = EXCLUDED.expires_at
`
type UpsertLockoutParams struct {
Key string `json:"key"`
ExpiresAt pgtype.Timestamptz `json:"expiresAt"`
}
// PostgreSQL variants of the sqlite rate-lockout queries.
// `INSERT OR REPLACE` becomes `INSERT ... ON CONFLICT (key) DO UPDATE`.
func (q *Queries) UpsertLockout(ctx context.Context, arg UpsertLockoutParams) error {
_, err := q.db.Exec(ctx, upsertLockout, arg.Key, arg.ExpiresAt)
return err
}
+479
View File
@@ -0,0 +1,479 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: messages.sql
package pgdbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createMessage = `-- name: CreateMessage :one
INSERT INTO messages (channel_id, user_id, content, reply_to)
VALUES ($1, $2, $3, $4)
RETURNING id
`
type CreateMessageParams struct {
ChannelID int64 `json:"channelId"`
UserID int64 `json:"userId"`
Content string `json:"content"`
ReplyTo *int64 `json:"replyTo"`
}
// PostgreSQL variants of the sqlite messages queries.
// `deleted = 0/1` and `pinned = 0/1` become FALSE/TRUE (columns are BOOLEAN).
// The FTS search queries are NOT included here: on postgres, messages.fts is
// a tsvector column with a GIN index (see migrations/postgres/001_initial_schema.sql)
// and FTS queries are hand-written in the postgres-specific store dispatch,
// mirroring how sqlite's FTS5 queries live in message_queries.go.
func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (int64, error) {
row := q.db.QueryRow(ctx, createMessage,
arg.ChannelID,
arg.UserID,
arg.Content,
arg.ReplyTo,
)
var id int64
err := row.Scan(&id)
return id, err
}
const editMessageContent = `-- name: EditMessageContent :exec
UPDATE messages SET content = $1, edited_at = NOW() WHERE id = $2
`
type EditMessageContentParams struct {
Content string `json:"content"`
ID int64 `json:"id"`
}
func (q *Queries) EditMessageContent(ctx context.Context, arg EditMessageContentParams) error {
_, err := q.db.Exec(ctx, editMessageContent, arg.Content, arg.ID)
return err
}
const getChannelUnreadCounts = `-- name: GetChannelUnreadCounts :many
SELECT c.id,
COALESCE(MAX(m.id), 0)::BIGINT AS last_msg_id,
COUNT(CASE WHEN m.id > COALESCE(rs.last_message_id, 0) AND m.deleted = FALSE THEN 1 END) AS unread
FROM channels c
LEFT JOIN messages m ON m.channel_id = c.id AND m.deleted = FALSE
LEFT JOIN read_states rs ON rs.channel_id = c.id AND rs.user_id = $1
WHERE c.type = 'text'
GROUP BY c.id
`
type GetChannelUnreadCountsRow struct {
ID int64 `json:"id"`
LastMsgID int64 `json:"lastMsgId"`
Unread int64 `json:"unread"`
}
func (q *Queries) GetChannelUnreadCounts(ctx context.Context, userID int64) ([]GetChannelUnreadCountsRow, error) {
rows, err := q.db.Query(ctx, getChannelUnreadCounts, userID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetChannelUnreadCountsRow{}
for rows.Next() {
var i GetChannelUnreadCountsRow
if err := rows.Scan(&i.ID, &i.LastMsgID, &i.Unread); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getLatestMessageID = `-- name: GetLatestMessageID :one
SELECT COALESCE(MAX(id), 0)::BIGINT FROM messages WHERE channel_id = $1 AND deleted = FALSE
`
func (q *Queries) GetLatestMessageID(ctx context.Context, channelID int64) (int64, error) {
row := q.db.QueryRow(ctx, getLatestMessageID, channelID)
var column_1 int64
err := row.Scan(&column_1)
return column_1, err
}
const getMessage = `-- name: GetMessage :one
SELECT id, channel_id, user_id, content, reply_to, edited_at, deleted, pinned, timestamp
FROM messages WHERE id = $1
`
type GetMessageRow struct {
ID int64 `json:"id"`
ChannelID int64 `json:"channelId"`
UserID int64 `json:"userId"`
Content string `json:"content"`
ReplyTo *int64 `json:"replyTo"`
EditedAt pgtype.Timestamptz `json:"editedAt"`
Deleted bool `json:"deleted"`
Pinned bool `json:"pinned"`
Timestamp pgtype.Timestamptz `json:"timestamp"`
}
func (q *Queries) GetMessage(ctx context.Context, id int64) (GetMessageRow, error) {
row := q.db.QueryRow(ctx, getMessage, id)
var i GetMessageRow
err := row.Scan(
&i.ID,
&i.ChannelID,
&i.UserID,
&i.Content,
&i.ReplyTo,
&i.EditedAt,
&i.Deleted,
&i.Pinned,
&i.Timestamp,
)
return i, err
}
const getMessagesByChannel = `-- name: GetMessagesByChannel :many
SELECT m.id, m.channel_id, m.user_id, m.content, m.reply_to,
m.edited_at, m.deleted, m.pinned, m.timestamp,
u.username, u.avatar
FROM messages m JOIN users u ON m.user_id = u.id
WHERE m.channel_id = $1 AND m.deleted = FALSE
ORDER BY m.id DESC LIMIT $2
`
type GetMessagesByChannelParams struct {
ChannelID int64 `json:"channelId"`
Limit int32 `json:"limit"`
}
type GetMessagesByChannelRow struct {
ID int64 `json:"id"`
ChannelID int64 `json:"channelId"`
UserID int64 `json:"userId"`
Content string `json:"content"`
ReplyTo *int64 `json:"replyTo"`
EditedAt pgtype.Timestamptz `json:"editedAt"`
Deleted bool `json:"deleted"`
Pinned bool `json:"pinned"`
Timestamp pgtype.Timestamptz `json:"timestamp"`
Username string `json:"username"`
Avatar *string `json:"avatar"`
}
func (q *Queries) GetMessagesByChannel(ctx context.Context, arg GetMessagesByChannelParams) ([]GetMessagesByChannelRow, error) {
rows, err := q.db.Query(ctx, getMessagesByChannel, arg.ChannelID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetMessagesByChannelRow{}
for rows.Next() {
var i GetMessagesByChannelRow
if err := rows.Scan(
&i.ID,
&i.ChannelID,
&i.UserID,
&i.Content,
&i.ReplyTo,
&i.EditedAt,
&i.Deleted,
&i.Pinned,
&i.Timestamp,
&i.Username,
&i.Avatar,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getMessagesByChannelBeforeCursor = `-- name: GetMessagesByChannelBeforeCursor :many
SELECT m.id, m.channel_id, m.user_id, m.content, m.reply_to,
m.edited_at, m.deleted, m.pinned, m.timestamp,
u.username, u.avatar
FROM messages m JOIN users u ON m.user_id = u.id
WHERE m.channel_id = $1 AND m.id < $2 AND m.deleted = FALSE
ORDER BY m.id DESC LIMIT $3
`
type GetMessagesByChannelBeforeCursorParams struct {
ChannelID int64 `json:"channelId"`
ID int64 `json:"id"`
Limit int32 `json:"limit"`
}
type GetMessagesByChannelBeforeCursorRow struct {
ID int64 `json:"id"`
ChannelID int64 `json:"channelId"`
UserID int64 `json:"userId"`
Content string `json:"content"`
ReplyTo *int64 `json:"replyTo"`
EditedAt pgtype.Timestamptz `json:"editedAt"`
Deleted bool `json:"deleted"`
Pinned bool `json:"pinned"`
Timestamp pgtype.Timestamptz `json:"timestamp"`
Username string `json:"username"`
Avatar *string `json:"avatar"`
}
func (q *Queries) GetMessagesByChannelBeforeCursor(ctx context.Context, arg GetMessagesByChannelBeforeCursorParams) ([]GetMessagesByChannelBeforeCursorRow, error) {
rows, err := q.db.Query(ctx, getMessagesByChannelBeforeCursor, arg.ChannelID, arg.ID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetMessagesByChannelBeforeCursorRow{}
for rows.Next() {
var i GetMessagesByChannelBeforeCursorRow
if err := rows.Scan(
&i.ID,
&i.ChannelID,
&i.UserID,
&i.Content,
&i.ReplyTo,
&i.EditedAt,
&i.Deleted,
&i.Pinned,
&i.Timestamp,
&i.Username,
&i.Avatar,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getMessagesForAPI = `-- name: GetMessagesForAPI :many
SELECT m.id, m.channel_id, m.user_id, u.username, u.avatar,
m.content, m.reply_to, m.edited_at, m.deleted, m.pinned, m.timestamp
FROM messages m JOIN users u ON m.user_id = u.id
WHERE m.channel_id = $1 AND m.deleted = FALSE
ORDER BY m.id DESC LIMIT $2
`
type GetMessagesForAPIParams struct {
ChannelID int64 `json:"channelId"`
Limit int32 `json:"limit"`
}
type GetMessagesForAPIRow struct {
ID int64 `json:"id"`
ChannelID int64 `json:"channelId"`
UserID int64 `json:"userId"`
Username string `json:"username"`
Avatar *string `json:"avatar"`
Content string `json:"content"`
ReplyTo *int64 `json:"replyTo"`
EditedAt pgtype.Timestamptz `json:"editedAt"`
Deleted bool `json:"deleted"`
Pinned bool `json:"pinned"`
Timestamp pgtype.Timestamptz `json:"timestamp"`
}
func (q *Queries) GetMessagesForAPI(ctx context.Context, arg GetMessagesForAPIParams) ([]GetMessagesForAPIRow, error) {
rows, err := q.db.Query(ctx, getMessagesForAPI, arg.ChannelID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetMessagesForAPIRow{}
for rows.Next() {
var i GetMessagesForAPIRow
if err := rows.Scan(
&i.ID,
&i.ChannelID,
&i.UserID,
&i.Username,
&i.Avatar,
&i.Content,
&i.ReplyTo,
&i.EditedAt,
&i.Deleted,
&i.Pinned,
&i.Timestamp,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getMessagesForAPIBeforeCursor = `-- name: GetMessagesForAPIBeforeCursor :many
SELECT m.id, m.channel_id, m.user_id, u.username, u.avatar,
m.content, m.reply_to, m.edited_at, m.deleted, m.pinned, m.timestamp
FROM messages m JOIN users u ON m.user_id = u.id
WHERE m.channel_id = $1 AND m.id < $2 AND m.deleted = FALSE
ORDER BY m.id DESC LIMIT $3
`
type GetMessagesForAPIBeforeCursorParams struct {
ChannelID int64 `json:"channelId"`
ID int64 `json:"id"`
Limit int32 `json:"limit"`
}
type GetMessagesForAPIBeforeCursorRow struct {
ID int64 `json:"id"`
ChannelID int64 `json:"channelId"`
UserID int64 `json:"userId"`
Username string `json:"username"`
Avatar *string `json:"avatar"`
Content string `json:"content"`
ReplyTo *int64 `json:"replyTo"`
EditedAt pgtype.Timestamptz `json:"editedAt"`
Deleted bool `json:"deleted"`
Pinned bool `json:"pinned"`
Timestamp pgtype.Timestamptz `json:"timestamp"`
}
func (q *Queries) GetMessagesForAPIBeforeCursor(ctx context.Context, arg GetMessagesForAPIBeforeCursorParams) ([]GetMessagesForAPIBeforeCursorRow, error) {
rows, err := q.db.Query(ctx, getMessagesForAPIBeforeCursor, arg.ChannelID, arg.ID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetMessagesForAPIBeforeCursorRow{}
for rows.Next() {
var i GetMessagesForAPIBeforeCursorRow
if err := rows.Scan(
&i.ID,
&i.ChannelID,
&i.UserID,
&i.Username,
&i.Avatar,
&i.Content,
&i.ReplyTo,
&i.EditedAt,
&i.Deleted,
&i.Pinned,
&i.Timestamp,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getPinnedMessageRows = `-- name: GetPinnedMessageRows :many
SELECT m.id, m.channel_id, m.user_id, u.username, u.avatar,
m.content, m.reply_to, m.edited_at, m.deleted, m.pinned, m.timestamp
FROM messages m JOIN users u ON m.user_id = u.id
WHERE m.channel_id = $1 AND m.pinned = TRUE AND m.deleted = FALSE
ORDER BY m.id DESC
`
type GetPinnedMessageRowsRow struct {
ID int64 `json:"id"`
ChannelID int64 `json:"channelId"`
UserID int64 `json:"userId"`
Username string `json:"username"`
Avatar *string `json:"avatar"`
Content string `json:"content"`
ReplyTo *int64 `json:"replyTo"`
EditedAt pgtype.Timestamptz `json:"editedAt"`
Deleted bool `json:"deleted"`
Pinned bool `json:"pinned"`
Timestamp pgtype.Timestamptz `json:"timestamp"`
}
func (q *Queries) GetPinnedMessageRows(ctx context.Context, channelID int64) ([]GetPinnedMessageRowsRow, error) {
rows, err := q.db.Query(ctx, getPinnedMessageRows, channelID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetPinnedMessageRowsRow{}
for rows.Next() {
var i GetPinnedMessageRowsRow
if err := rows.Scan(
&i.ID,
&i.ChannelID,
&i.UserID,
&i.Username,
&i.Avatar,
&i.Content,
&i.ReplyTo,
&i.EditedAt,
&i.Deleted,
&i.Pinned,
&i.Timestamp,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const setMessagePinned = `-- name: SetMessagePinned :execrows
UPDATE messages SET pinned = $1 WHERE id = $2 AND deleted = FALSE
`
type SetMessagePinnedParams struct {
Pinned bool `json:"pinned"`
ID int64 `json:"id"`
}
func (q *Queries) SetMessagePinned(ctx context.Context, arg SetMessagePinnedParams) (int64, error) {
result, err := q.db.Exec(ctx, setMessagePinned, arg.Pinned, arg.ID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const softDeleteMessage = `-- name: SoftDeleteMessage :exec
UPDATE messages SET deleted = TRUE WHERE id = $1
`
func (q *Queries) SoftDeleteMessage(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, softDeleteMessage, id)
return err
}
const updateReadState = `-- name: UpdateReadState :exec
INSERT INTO read_states (user_id, channel_id, last_message_id)
VALUES ($1, $2, $3)
ON CONFLICT (user_id, channel_id) DO UPDATE SET last_message_id = EXCLUDED.last_message_id
`
type UpdateReadStateParams struct {
UserID int64 `json:"userId"`
ChannelID int64 `json:"channelId"`
LastMessageID int64 `json:"lastMessageId"`
}
func (q *Queries) UpdateReadState(ctx context.Context, arg UpdateReadStateParams) error {
_, err := q.db.Exec(ctx, updateReadState, arg.UserID, arg.ChannelID, arg.LastMessageID)
return err
}
+218
View File
@@ -0,0 +1,218 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package pgdbgen
import (
"github.com/jackc/pgx/v5/pgtype"
)
type Attachment struct {
ID string `json:"id"`
MessageID *int64 `json:"messageId"`
Filename string `json:"filename"`
StoredAs string `json:"storedAs"`
MimeType string `json:"mimeType"`
Size int64 `json:"size"`
UploadedAt pgtype.Timestamptz `json:"uploadedAt"`
Width *int32 `json:"width"`
Height *int32 `json:"height"`
UploaderID *int64 `json:"uploaderId"`
}
type AuditLog struct {
ID int64 `json:"id"`
ActorID int64 `json:"actorId"`
Action string `json:"action"`
TargetType string `json:"targetType"`
TargetID int64 `json:"targetId"`
Detail string `json:"detail"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
}
type Channel struct {
ID int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Category *string `json:"category"`
Topic *string `json:"topic"`
Position int32 `json:"position"`
SlowMode int32 `json:"slowMode"`
Archived bool `json:"archived"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
VoiceMaxUsers int32 `json:"voiceMaxUsers"`
VoiceQuality *string `json:"voiceQuality"`
MixingThreshold *int32 `json:"mixingThreshold"`
VoiceMaxVideo int32 `json:"voiceMaxVideo"`
}
type ChannelOverride struct {
ID int64 `json:"id"`
ChannelID int64 `json:"channelId"`
RoleID int64 `json:"roleId"`
Allow int64 `json:"allow"`
Deny int64 `json:"deny"`
}
type DmOpenState struct {
UserID int64 `json:"userId"`
ChannelID int64 `json:"channelId"`
OpenedAt pgtype.Timestamptz `json:"openedAt"`
}
type DmParticipant struct {
ChannelID int64 `json:"channelId"`
UserID int64 `json:"userId"`
}
type Emoji struct {
ID int64 `json:"id"`
Shortcode string `json:"shortcode"`
Filename string `json:"filename"`
UploadedBy int64 `json:"uploadedBy"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
}
type Event struct {
Seq int64 `json:"seq"`
EventType string `json:"eventType"`
Payload []byte `json:"payload"`
ChannelID int64 `json:"channelId"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
}
type Invite struct {
ID int64 `json:"id"`
Code string `json:"code"`
CreatedBy int64 `json:"createdBy"`
RedeemedBy *int64 `json:"redeemedBy"`
MaxUses *int32 `json:"maxUses"`
UseCount int32 `json:"useCount"`
ExpiresAt pgtype.Timestamptz `json:"expiresAt"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
Revoked bool `json:"revoked"`
}
type LoginAttempt struct {
ID int64 `json:"id"`
IpAddress string `json:"ipAddress"`
Username *string `json:"username"`
Success bool `json:"success"`
Timestamp pgtype.Timestamptz `json:"timestamp"`
}
type Message struct {
ID int64 `json:"id"`
ChannelID int64 `json:"channelId"`
UserID int64 `json:"userId"`
Content string `json:"content"`
ReplyTo *int64 `json:"replyTo"`
EditedAt pgtype.Timestamptz `json:"editedAt"`
Deleted bool `json:"deleted"`
Pinned bool `json:"pinned"`
Timestamp pgtype.Timestamptz `json:"timestamp"`
Fts interface{} `json:"fts"`
}
type Plugin struct {
ID int64 `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
ManifestJson string `json:"manifestJson"`
InstalledAt pgtype.Timestamptz `json:"installedAt"`
}
type PluginKv struct {
PluginID int64 `json:"pluginId"`
Key string `json:"key"`
Value []byte `json:"value"`
}
type RateLockout struct {
Key string `json:"key"`
ExpiresAt pgtype.Timestamptz `json:"expiresAt"`
}
type Reaction struct {
ID int64 `json:"id"`
MessageID int64 `json:"messageId"`
UserID int64 `json:"userId"`
Emoji string `json:"emoji"`
}
type ReadState struct {
UserID int64 `json:"userId"`
ChannelID int64 `json:"channelId"`
LastMessageID int64 `json:"lastMessageId"`
MentionCount int32 `json:"mentionCount"`
}
type Role struct {
ID int64 `json:"id"`
Name string `json:"name"`
Color *string `json:"color"`
Permissions int64 `json:"permissions"`
Position int32 `json:"position"`
IsDefault bool `json:"isDefault"`
}
type Session struct {
ID int64 `json:"id"`
UserID int64 `json:"userId"`
Token string `json:"token"`
Device *string `json:"device"`
IpAddress *string `json:"ipAddress"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
LastUsed pgtype.Timestamptz `json:"lastUsed"`
ExpiresAt pgtype.Timestamptz `json:"expiresAt"`
}
type Setting struct {
Key string `json:"key"`
Value string `json:"value"`
}
type Sound struct {
ID int64 `json:"id"`
Name string `json:"name"`
Filename string `json:"filename"`
DurationMs int32 `json:"durationMs"`
UploadedBy int64 `json:"uploadedBy"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
}
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Avatar *string `json:"avatar"`
RoleID int64 `json:"roleId"`
TotpSecret *string `json:"totpSecret"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
LastSeen pgtype.Timestamptz `json:"lastSeen"`
Banned bool `json:"banned"`
BanReason *string `json:"banReason"`
BanExpires pgtype.Timestamptz `json:"banExpires"`
}
type UserBlock struct {
BlockerID int64 `json:"blockerId"`
BlockedID int64 `json:"blockedId"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
}
type VoiceState struct {
UserID int64 `json:"userId"`
ChannelID int64 `json:"channelId"`
Muted bool `json:"muted"`
Deafened bool `json:"deafened"`
Speaking bool `json:"speaking"`
JoinedAt pgtype.Timestamptz `json:"joinedAt"`
Camera bool `json:"camera"`
Screenshare bool `json:"screenshare"`
}
+175
View File
@@ -0,0 +1,175 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: plugins.sql
package pgdbgen
import (
"context"
)
const disablePlugin = `-- name: DisablePlugin :exec
UPDATE plugins SET enabled = FALSE WHERE id = $1
`
func (q *Queries) DisablePlugin(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, disablePlugin, id)
return err
}
const enablePlugin = `-- name: EnablePlugin :exec
UPDATE plugins SET enabled = TRUE WHERE id = $1
`
func (q *Queries) EnablePlugin(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, enablePlugin, id)
return err
}
const getPlugin = `-- name: GetPlugin :one
SELECT id, name, version, enabled, manifest_json, installed_at FROM plugins WHERE id = $1
`
func (q *Queries) GetPlugin(ctx context.Context, id int64) (Plugin, error) {
row := q.db.QueryRow(ctx, getPlugin, id)
var i Plugin
err := row.Scan(
&i.ID,
&i.Name,
&i.Version,
&i.Enabled,
&i.ManifestJson,
&i.InstalledAt,
)
return i, err
}
const getPluginByName = `-- name: GetPluginByName :one
SELECT id, name, version, enabled, manifest_json, installed_at FROM plugins WHERE name = $1
`
func (q *Queries) GetPluginByName(ctx context.Context, name string) (Plugin, error) {
row := q.db.QueryRow(ctx, getPluginByName, name)
var i Plugin
err := row.Scan(
&i.ID,
&i.Name,
&i.Version,
&i.Enabled,
&i.ManifestJson,
&i.InstalledAt,
)
return i, err
}
const installPlugin = `-- name: InstallPlugin :one
INSERT INTO plugins (name, version, manifest_json)
VALUES ($1, $2, $3)
ON CONFLICT (name) DO UPDATE
SET version = excluded.version,
manifest_json = excluded.manifest_json
RETURNING id
`
type InstallPluginParams struct {
Name string `json:"name"`
Version string `json:"version"`
ManifestJson string `json:"manifestJson"`
}
func (q *Queries) InstallPlugin(ctx context.Context, arg InstallPluginParams) (int64, error) {
row := q.db.QueryRow(ctx, installPlugin, arg.Name, arg.Version, arg.ManifestJson)
var id int64
err := row.Scan(&id)
return id, err
}
const listPlugins = `-- name: ListPlugins :many
SELECT id, name, version, enabled, manifest_json, installed_at FROM plugins ORDER BY name
`
func (q *Queries) ListPlugins(ctx context.Context) ([]Plugin, error) {
rows, err := q.db.Query(ctx, listPlugins)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Plugin{}
for rows.Next() {
var i Plugin
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Version,
&i.Enabled,
&i.ManifestJson,
&i.InstalledAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const pluginKVDelete = `-- name: PluginKVDelete :exec
DELETE FROM plugin_kv WHERE plugin_id = $1 AND key = $2
`
type PluginKVDeleteParams struct {
PluginID int64 `json:"pluginId"`
Key string `json:"key"`
}
func (q *Queries) PluginKVDelete(ctx context.Context, arg PluginKVDeleteParams) error {
_, err := q.db.Exec(ctx, pluginKVDelete, arg.PluginID, arg.Key)
return err
}
const pluginKVGet = `-- name: PluginKVGet :one
SELECT value FROM plugin_kv WHERE plugin_id = $1 AND key = $2
`
type PluginKVGetParams struct {
PluginID int64 `json:"pluginId"`
Key string `json:"key"`
}
func (q *Queries) PluginKVGet(ctx context.Context, arg PluginKVGetParams) ([]byte, error) {
row := q.db.QueryRow(ctx, pluginKVGet, arg.PluginID, arg.Key)
var value []byte
err := row.Scan(&value)
return value, err
}
const pluginKVSet = `-- name: PluginKVSet :exec
INSERT INTO plugin_kv (plugin_id, key, value)
VALUES ($1, $2, $3)
ON CONFLICT (plugin_id, key) DO UPDATE SET value = excluded.value
`
type PluginKVSetParams struct {
PluginID int64 `json:"pluginId"`
Key string `json:"key"`
Value []byte `json:"value"`
}
func (q *Queries) PluginKVSet(ctx context.Context, arg PluginKVSetParams) error {
_, err := q.db.Exec(ctx, pluginKVSet, arg.PluginID, arg.Key, arg.Value)
return err
}
const uninstallPlugin = `-- name: UninstallPlugin :exec
DELETE FROM plugins WHERE id = $1
`
func (q *Queries) UninstallPlugin(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, uninstallPlugin, id)
return err
}
+48
View File
@@ -0,0 +1,48 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: profile.sql
package pgdbgen
import (
"context"
)
const updateUserPassword = `-- name: UpdateUserPassword :exec
UPDATE users SET password = $1 WHERE id = $2
`
type UpdateUserPasswordParams struct {
Password string `json:"password"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error {
_, err := q.db.Exec(ctx, updateUserPassword, arg.Password, arg.ID)
return err
}
const updateUserProfile = `-- name: UpdateUserProfile :execrows
UPDATE users SET username = $1, avatar = $2 WHERE id = $3
`
type UpdateUserProfileParams struct {
Username string `json:"username"`
Avatar *string `json:"avatar"`
ID int64 `json:"id"`
}
// PostgreSQL variants of the sqlite profile queries.
// UpdateUserProfile uses :execrows because postgres has no LastInsertId;
// the caller checks rows-affected for existence.
func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (int64, error) {
result, err := q.db.Exec(ctx, updateUserProfile, arg.Username, arg.Avatar, arg.ID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
+197
View File
@@ -0,0 +1,197 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package pgdbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
type Querier interface {
// PostgreSQL variants of the sqlite reactions queries.
AddReaction(ctx context.Context, arg AddReactionParams) error
AdminUpdateChannel(ctx context.Context, arg AdminUpdateChannelParams) error
ArchiveChannel(ctx context.Context, arg ArchiveChannelParams) error
BanUser(ctx context.Context, arg BanUserParams) error
// PostgreSQL variants of the sqlite user block queries.
// `INSERT OR IGNORE` becomes `INSERT ... ON CONFLICT DO NOTHING`.
BlockUser(ctx context.Context, arg BlockUserParams) error
CleanupExpiredLockouts(ctx context.Context, expiresAt pgtype.Timestamptz) error
ClearAllVoiceStates(ctx context.Context) error
ClearVoiceState(ctx context.Context, userID int64) error
CloseDM(ctx context.Context, arg CloseDMParams) error
CountActiveCameras(ctx context.Context, channelID int64) (int64, error)
CountActiveInvites(ctx context.Context) (int64, error)
CountActiveMessages(ctx context.Context) (int64, error)
CountChannels(ctx context.Context) (int64, error)
CountUsers(ctx context.Context) (int64, error)
CountUsersWithoutTOTP(ctx context.Context) (int64, error)
// PostgreSQL variants of the sqlite attachments queries.
CreateAttachment(ctx context.Context, arg CreateAttachmentParams) error
CreateChannel(ctx context.Context, arg CreateChannelParams) (int64, error)
// PostgreSQL variants of the sqlite invites queries.
// The expiry check uses native timestamp comparison instead of sqlite's
// strftime('%s', …) trick.
CreateInvite(ctx context.Context, arg CreateInviteParams) error
// PostgreSQL variants of the sqlite messages queries.
// `deleted = 0/1` and `pinned = 0/1` become FALSE/TRUE (columns are BOOLEAN).
// The FTS search queries are NOT included here: on postgres, messages.fts is
// a tsvector column with a GIN index (see migrations/postgres/001_initial_schema.sql)
// and FTS queries are hand-written in the postgres-specific store dispatch,
// mirroring how sqlite's FTS5 queries live in message_queries.go.
CreateMessage(ctx context.Context, arg CreateMessageParams) (int64, error)
CreateUser(ctx context.Context, arg CreateUserParams) (int64, error)
DeleteAttachment(ctx context.Context, id string) error
DeleteChannel(ctx context.Context, id int64) error
DeleteChannelPermission(ctx context.Context, arg DeleteChannelPermissionParams) error
// Use native timestamp comparison instead of sqlite's strftime trick.
DeleteExpiredSessions(ctx context.Context) error
DeleteLockout(ctx context.Context, key string) error
// Postgres timestamptz comparison — the caller passes a wall-clock time.
DeleteOrphanedAttachments(ctx context.Context, uploadedAt pgtype.Timestamptz) ([]string, error)
DeleteOtherSessions(ctx context.Context, arg DeleteOtherSessionsParams) (int64, error)
DeleteSessionByID(ctx context.Context, arg DeleteSessionByIDParams) error
DeleteSessionByToken(ctx context.Context, token string) error
DisablePlugin(ctx context.Context, id int64) error
EditMessageContent(ctx context.Context, arg EditMessageContentParams) error
EnableCameraIfUnderLimit(ctx context.Context, arg EnableCameraIfUnderLimitParams) (int64, error)
EnablePlugin(ctx context.Context, id int64) error
// Delete all but the N most recent sessions for a user. Postgres replaces
// sqlite's `LIMIT -1 OFFSET ?` with `OFFSET $2`.
EvictOldestSessions(ctx context.Context, arg EvictOldestSessionsParams) error
FindExistingDMChannel(ctx context.Context, arg FindExistingDMChannelParams) (int64, error)
ForceLogoutUser(ctx context.Context, userID int64) error
GetAllSettings(ctx context.Context) ([]Setting, error)
GetAllVoiceStates(ctx context.Context) ([]GetAllVoiceStatesRow, error)
GetAttachmentByID(ctx context.Context, id string) (GetAttachmentByIDRow, error)
GetAttachmentWithChannel(ctx context.Context, id string) (GetAttachmentWithChannelRow, error)
GetAuditLog(ctx context.Context, arg GetAuditLogParams) ([]GetAuditLogRow, error)
GetChannel(ctx context.Context, id int64) (GetChannelRow, error)
GetChannelPermission(ctx context.Context, arg GetChannelPermissionParams) (GetChannelPermissionRow, error)
GetChannelUnreadCounts(ctx context.Context, userID int64) ([]GetChannelUnreadCountsRow, error)
GetChannelVoiceStates(ctx context.Context, channelID int64) ([]GetChannelVoiceStatesRow, error)
GetDMParticipantIDs(ctx context.Context, channelID int64) ([]int64, error)
GetDefaultRole(ctx context.Context) (Role, error)
GetEventsSince(ctx context.Context, arg GetEventsSinceParams) ([]GetEventsSinceRow, error)
GetInvite(ctx context.Context, code string) (GetInviteRow, error)
GetLatestMessageID(ctx context.Context, channelID int64) (int64, error)
GetMaxEventSeq(ctx context.Context) (int64, error)
GetMessage(ctx context.Context, id int64) (GetMessageRow, error)
GetMessagesByChannel(ctx context.Context, arg GetMessagesByChannelParams) ([]GetMessagesByChannelRow, error)
GetMessagesByChannelBeforeCursor(ctx context.Context, arg GetMessagesByChannelBeforeCursorParams) ([]GetMessagesByChannelBeforeCursorRow, error)
GetMessagesForAPI(ctx context.Context, arg GetMessagesForAPIParams) ([]GetMessagesForAPIRow, error)
GetMessagesForAPIBeforeCursor(ctx context.Context, arg GetMessagesForAPIBeforeCursorParams) ([]GetMessagesForAPIBeforeCursorRow, error)
GetPinnedMessageRows(ctx context.Context, channelID int64) ([]GetPinnedMessageRowsRow, error)
GetPlugin(ctx context.Context, id int64) (Plugin, error)
GetPluginByName(ctx context.Context, name string) (Plugin, error)
GetReactionCounts(ctx context.Context, messageID int64) ([]GetReactionCountsRow, error)
// PostgreSQL variants of the sqlite roles queries.
// `is_default = 1` becomes `is_default = TRUE` since the column is BOOLEAN.
GetRoleByID(ctx context.Context, id int64) (Role, error)
GetRoleChannelPermissions(ctx context.Context, roleID int64) ([]GetRoleChannelPermissionsRow, error)
GetRoleForUser(ctx context.Context, id int64) (Role, error)
GetSessionByTokenHash(ctx context.Context, token string) (Session, error)
GetSessionWithBanStatus(ctx context.Context, token string) (GetSessionWithBanStatusRow, error)
GetSetting(ctx context.Context, key string) (string, error)
GetUserByID(ctx context.Context, id int64) (User, error)
// PostgreSQL variants of the sqlite users queries.
// Differences from sqlite:
// - `?` -> `$1`, `$2`, …
// - `COLLATE NOCASE` -> removed; the `username` column is CITEXT.
// - `datetime('now')` -> `NOW()`
// - `banned = 0/1` -> `banned = FALSE/TRUE` (column is BOOLEAN)
// - `:execresult INSERT` -> `:one ... RETURNING id` (pgx has no LastInsertId)
GetUserByUsername(ctx context.Context, username string) (User, error)
// For the "last message at" and "last message content" columns, sqlite
// COALESCEs to '' (empty string). Postgres TIMESTAMPTZ cannot COALESCE to an
// empty string, so we COALESCE to the dm_open_state.opened_at fallback and
// leave conversion to the store wrapper.
GetUserDMChannels(ctx context.Context, arg GetUserDMChannelsParams) ([]GetUserDMChannelsRow, error)
GetUserSessions(ctx context.Context, userID int64) ([]Session, error)
GetUserVoiceState(ctx context.Context, userID int64) (GetUserVoiceStateRow, error)
GetUserWithRole(ctx context.Context, id int64) (GetUserWithRoleRow, error)
// PostgreSQL variants of the sqlite DM queries.
// InsertDMChannel: sqlite uses :execresult (LastInsertId); postgres uses
// :one with RETURNING id.
// `INSERT OR IGNORE` becomes `INSERT ... ON CONFLICT DO NOTHING`.
InsertDMChannel(ctx context.Context) (int64, error)
InsertDMOpenState(ctx context.Context, arg InsertDMOpenStateParams) error
InsertDMParticipants(ctx context.Context, arg InsertDMParticipantsParams) error
// PostgreSQL variants of the sqlite sessions queries.
InsertSession(ctx context.Context, arg InsertSessionParams) (int64, error)
InstallPlugin(ctx context.Context, arg InstallPluginParams) (int64, error)
IsBlocked(ctx context.Context, arg IsBlockedParams) (int32, error)
IsDMParticipant(ctx context.Context, arg IsDMParticipantParams) (int64, error)
IsEitherBlocked(ctx context.Context, arg IsEitherBlockedParams) (int32, error)
// PostgreSQL variants of the sqlite voice queries.
// voice_states boolean columns (muted, deafened, speaking, camera,
// screenshare) use FALSE/TRUE instead of 0/1.
JoinVoiceChannel(ctx context.Context, arg JoinVoiceChannelParams) error
JoinVoiceChannelIfCapacity(ctx context.Context, arg JoinVoiceChannelIfCapacityParams) (int64, error)
LeaveVoiceChannel(ctx context.Context, userID int64) error
LeaveVoiceChannelIfMatch(ctx context.Context, arg LeaveVoiceChannelIfMatchParams) (int64, error)
LinkAttachmentToMessage(ctx context.Context, arg LinkAttachmentToMessageParams) (int64, error)
ListAllUsers(ctx context.Context, arg ListAllUsersParams) ([]ListAllUsersRow, error)
// PostgreSQL variants of the sqlite channels queries.
ListChannels(ctx context.Context) ([]ListChannelsRow, error)
ListInvites(ctx context.Context) ([]ListInvitesRow, error)
ListMembers(ctx context.Context) ([]ListMembersRow, error)
ListPlugins(ctx context.Context) ([]Plugin, error)
ListRoles(ctx context.Context) ([]Role, error)
ListUserSessions(ctx context.Context, userID int64) ([]Session, error)
LoadActiveLockouts(ctx context.Context, expiresAt pgtype.Timestamptz) ([]RateLockout, error)
LogAudit(ctx context.Context, arg LogAuditParams) error
OpenDM(ctx context.Context, arg OpenDMParams) error
// seq is supplied by the hub so the row seq matches the wrapped-payload seq.
// The schema's BIGSERIAL still owns the id column for inserts that omit seq,
// but PersistEvent always supplies an explicit value.
PersistEvent(ctx context.Context, arg PersistEventParams) error
PluginKVDelete(ctx context.Context, arg PluginKVDeleteParams) error
PluginKVGet(ctx context.Context, arg PluginKVGetParams) ([]byte, error)
PluginKVSet(ctx context.Context, arg PluginKVSetParams) error
PruneEventsOlderThan(ctx context.Context, createdAt pgtype.Timestamptz) (int64, error)
RemoveReaction(ctx context.Context, arg RemoveReactionParams) (int64, error)
ResetAllUserStatuses(ctx context.Context) error
RevokeInvite(ctx context.Context, code string) error
SetChannelMixingThreshold(ctx context.Context, arg SetChannelMixingThresholdParams) error
SetChannelSlowMode(ctx context.Context, arg SetChannelSlowModeParams) error
SetChannelVoiceMaxUsers(ctx context.Context, arg SetChannelVoiceMaxUsersParams) error
SetChannelVoiceMaxVideo(ctx context.Context, arg SetChannelVoiceMaxVideoParams) error
SetChannelVoiceQuality(ctx context.Context, arg SetChannelVoiceQualityParams) error
SetMessagePinned(ctx context.Context, arg SetMessagePinnedParams) (int64, error)
SetSetting(ctx context.Context, arg SetSettingParams) error
SoftDeleteMessage(ctx context.Context, id int64) error
TouchSession(ctx context.Context, token string) error
UnbanUser(ctx context.Context, id int64) error
UnblockUser(ctx context.Context, arg UnblockUserParams) error
UninstallPlugin(ctx context.Context, id int64) error
UpdateChannel(ctx context.Context, arg UpdateChannelParams) error
UpdateReadState(ctx context.Context, arg UpdateReadStateParams) error
UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error
// PostgreSQL variants of the sqlite profile queries.
// UpdateUserProfile uses :execrows because postgres has no LastInsertId;
// the caller checks rows-affected for existence.
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (int64, error)
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) error
UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) error
UpdateUserTOTPSecret(ctx context.Context, arg UpdateUserTOTPSecretParams) error
UpdateVoiceCamera(ctx context.Context, arg UpdateVoiceCameraParams) error
UpdateVoiceDeafen(ctx context.Context, arg UpdateVoiceDeafenParams) error
UpdateVoiceMute(ctx context.Context, arg UpdateVoiceMuteParams) error
UpdateVoiceScreenshare(ctx context.Context, arg UpdateVoiceScreenshareParams) error
UpdateVoiceSpeaking(ctx context.Context, arg UpdateVoiceSpeakingParams) error
UpsertChannelPermission(ctx context.Context, arg UpsertChannelPermissionParams) error
// PostgreSQL variants of the sqlite rate-lockout queries.
// `INSERT OR REPLACE` becomes `INSERT ... ON CONFLICT (key) DO UPDATE`.
UpsertLockout(ctx context.Context, arg UpsertLockoutParams) error
UseInviteAtomic(ctx context.Context, code string) (int64, error)
// PostgreSQL variants of the sqlite admin queries.
UserCount(ctx context.Context) (int64, error)
}
var _ Querier = (*Queries)(nil)
+78
View File
@@ -0,0 +1,78 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: reactions.sql
package pgdbgen
import (
"context"
)
const addReaction = `-- name: AddReaction :exec
INSERT INTO reactions (message_id, user_id, emoji) VALUES ($1, $2, $3)
`
type AddReactionParams struct {
MessageID int64 `json:"messageId"`
UserID int64 `json:"userId"`
Emoji string `json:"emoji"`
}
// PostgreSQL variants of the sqlite reactions queries.
func (q *Queries) AddReaction(ctx context.Context, arg AddReactionParams) error {
_, err := q.db.Exec(ctx, addReaction, arg.MessageID, arg.UserID, arg.Emoji)
return err
}
const getReactionCounts = `-- name: GetReactionCounts :many
SELECT emoji, COUNT(*) AS count
FROM reactions WHERE message_id = $1
GROUP BY emoji
`
type GetReactionCountsRow struct {
Emoji string `json:"emoji"`
Count int64 `json:"count"`
}
func (q *Queries) GetReactionCounts(ctx context.Context, messageID int64) ([]GetReactionCountsRow, error) {
rows, err := q.db.Query(ctx, getReactionCounts, messageID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetReactionCountsRow{}
for rows.Next() {
var i GetReactionCountsRow
if err := rows.Scan(&i.Emoji, &i.Count); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const removeReaction = `-- name: RemoveReaction :execrows
DELETE FROM reactions WHERE message_id = $1 AND user_id = $2 AND emoji = $3
`
type RemoveReactionParams struct {
MessageID int64 `json:"messageId"`
UserID int64 `json:"userId"`
Emoji string `json:"emoji"`
}
func (q *Queries) RemoveReaction(ctx context.Context, arg RemoveReactionParams) (int64, error) {
result, err := q.db.Exec(ctx, removeReaction, arg.MessageID, arg.UserID, arg.Emoji)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
+165
View File
@@ -0,0 +1,165 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: roles.sql
package pgdbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const getDefaultRole = `-- name: GetDefaultRole :one
SELECT id, name, color, permissions, position, is_default
FROM roles WHERE is_default = TRUE LIMIT 1
`
func (q *Queries) GetDefaultRole(ctx context.Context) (Role, error) {
row := q.db.QueryRow(ctx, getDefaultRole)
var i Role
err := row.Scan(
&i.ID,
&i.Name,
&i.Color,
&i.Permissions,
&i.Position,
&i.IsDefault,
)
return i, err
}
const getRoleByID = `-- name: GetRoleByID :one
SELECT id, name, color, permissions, position, is_default
FROM roles WHERE id = $1
`
// PostgreSQL variants of the sqlite roles queries.
// `is_default = 1` becomes `is_default = TRUE` since the column is BOOLEAN.
func (q *Queries) GetRoleByID(ctx context.Context, id int64) (Role, error) {
row := q.db.QueryRow(ctx, getRoleByID, id)
var i Role
err := row.Scan(
&i.ID,
&i.Name,
&i.Color,
&i.Permissions,
&i.Position,
&i.IsDefault,
)
return i, err
}
const getRoleForUser = `-- name: GetRoleForUser :one
SELECT r.id, r.name, r.color, r.permissions, r.position, r.is_default
FROM users u
JOIN roles r ON u.role_id = r.id
WHERE u.id = $1
`
func (q *Queries) GetRoleForUser(ctx context.Context, id int64) (Role, error) {
row := q.db.QueryRow(ctx, getRoleForUser, id)
var i Role
err := row.Scan(
&i.ID,
&i.Name,
&i.Color,
&i.Permissions,
&i.Position,
&i.IsDefault,
)
return i, err
}
const getUserWithRole = `-- name: GetUserWithRole :one
SELECT u.id, u.username, u.password, u.avatar, u.role_id,
u.totp_secret, u.status, u.created_at, u.last_seen,
u.banned, u.ban_reason, u.ban_expires,
r.id, r.name, r.color, r.permissions, r.position, r.is_default
FROM users u
JOIN roles r ON r.id = u.role_id
WHERE u.id = $1
`
type GetUserWithRoleRow struct {
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Avatar *string `json:"avatar"`
RoleID int64 `json:"roleId"`
TotpSecret *string `json:"totpSecret"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
LastSeen pgtype.Timestamptz `json:"lastSeen"`
Banned bool `json:"banned"`
BanReason *string `json:"banReason"`
BanExpires pgtype.Timestamptz `json:"banExpires"`
ID_2 int64 `json:"id2"`
Name string `json:"name"`
Color *string `json:"color"`
Permissions int64 `json:"permissions"`
Position int32 `json:"position"`
IsDefault bool `json:"isDefault"`
}
func (q *Queries) GetUserWithRole(ctx context.Context, id int64) (GetUserWithRoleRow, error) {
row := q.db.QueryRow(ctx, getUserWithRole, id)
var i GetUserWithRoleRow
err := row.Scan(
&i.ID,
&i.Username,
&i.Password,
&i.Avatar,
&i.RoleID,
&i.TotpSecret,
&i.Status,
&i.CreatedAt,
&i.LastSeen,
&i.Banned,
&i.BanReason,
&i.BanExpires,
&i.ID_2,
&i.Name,
&i.Color,
&i.Permissions,
&i.Position,
&i.IsDefault,
)
return i, err
}
const listRoles = `-- name: ListRoles :many
SELECT id, name, color, permissions, position, is_default
FROM roles ORDER BY position DESC
`
func (q *Queries) ListRoles(ctx context.Context) ([]Role, error) {
rows, err := q.db.Query(ctx, listRoles)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Role{}
for rows.Next() {
var i Role
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Color,
&i.Permissions,
&i.Position,
&i.IsDefault,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
+221
View File
@@ -0,0 +1,221 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: sessions.sql
package pgdbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const deleteExpiredSessions = `-- name: DeleteExpiredSessions :exec
DELETE FROM sessions WHERE expires_at < NOW()
`
// Use native timestamp comparison instead of sqlite's strftime trick.
func (q *Queries) DeleteExpiredSessions(ctx context.Context) error {
_, err := q.db.Exec(ctx, deleteExpiredSessions)
return err
}
const deleteOtherSessions = `-- name: DeleteOtherSessions :execrows
DELETE FROM sessions WHERE user_id = $1 AND id != $2
`
type DeleteOtherSessionsParams struct {
UserID int64 `json:"userId"`
ID int64 `json:"id"`
}
func (q *Queries) DeleteOtherSessions(ctx context.Context, arg DeleteOtherSessionsParams) (int64, error) {
result, err := q.db.Exec(ctx, deleteOtherSessions, arg.UserID, arg.ID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const deleteSessionByID = `-- name: DeleteSessionByID :exec
DELETE FROM sessions WHERE id = $1 AND user_id = $2
`
type DeleteSessionByIDParams struct {
ID int64 `json:"id"`
UserID int64 `json:"userId"`
}
func (q *Queries) DeleteSessionByID(ctx context.Context, arg DeleteSessionByIDParams) error {
_, err := q.db.Exec(ctx, deleteSessionByID, arg.ID, arg.UserID)
return err
}
const deleteSessionByToken = `-- name: DeleteSessionByToken :exec
DELETE FROM sessions WHERE token = $1
`
func (q *Queries) DeleteSessionByToken(ctx context.Context, token string) error {
_, err := q.db.Exec(ctx, deleteSessionByToken, token)
return err
}
const evictOldestSessions = `-- name: EvictOldestSessions :exec
DELETE FROM sessions WHERE id IN (
SELECT s2.id FROM sessions AS s2 WHERE s2.user_id = $1
ORDER BY s2.created_at DESC
OFFSET $2
)
`
type EvictOldestSessionsParams struct {
UserID int64 `json:"userId"`
Offset int32 `json:"offset"`
}
// Delete all but the N most recent sessions for a user. Postgres replaces
// sqlite's `LIMIT -1 OFFSET ?` with `OFFSET $2`.
func (q *Queries) EvictOldestSessions(ctx context.Context, arg EvictOldestSessionsParams) error {
_, err := q.db.Exec(ctx, evictOldestSessions, arg.UserID, arg.Offset)
return err
}
const getSessionByTokenHash = `-- name: GetSessionByTokenHash :one
SELECT id, user_id, token, device, ip_address, created_at, last_used, expires_at
FROM sessions WHERE token = $1
`
func (q *Queries) GetSessionByTokenHash(ctx context.Context, token string) (Session, error) {
row := q.db.QueryRow(ctx, getSessionByTokenHash, token)
var i Session
err := row.Scan(
&i.ID,
&i.UserID,
&i.Token,
&i.Device,
&i.IpAddress,
&i.CreatedAt,
&i.LastUsed,
&i.ExpiresAt,
)
return i, err
}
const getSessionWithBanStatus = `-- name: GetSessionWithBanStatus :one
SELECT s.id, s.user_id, s.token, s.device, s.ip_address,
s.created_at, s.last_used, s.expires_at,
u.banned, u.ban_reason, u.ban_expires
FROM sessions s
JOIN users u ON s.user_id = u.id
WHERE s.token = $1
`
type GetSessionWithBanStatusRow struct {
ID int64 `json:"id"`
UserID int64 `json:"userId"`
Token string `json:"token"`
Device *string `json:"device"`
IpAddress *string `json:"ipAddress"`
CreatedAt pgtype.Timestamptz `json:"createdAt"`
LastUsed pgtype.Timestamptz `json:"lastUsed"`
ExpiresAt pgtype.Timestamptz `json:"expiresAt"`
Banned bool `json:"banned"`
BanReason *string `json:"banReason"`
BanExpires pgtype.Timestamptz `json:"banExpires"`
}
func (q *Queries) GetSessionWithBanStatus(ctx context.Context, token string) (GetSessionWithBanStatusRow, error) {
row := q.db.QueryRow(ctx, getSessionWithBanStatus, token)
var i GetSessionWithBanStatusRow
err := row.Scan(
&i.ID,
&i.UserID,
&i.Token,
&i.Device,
&i.IpAddress,
&i.CreatedAt,
&i.LastUsed,
&i.ExpiresAt,
&i.Banned,
&i.BanReason,
&i.BanExpires,
)
return i, err
}
const insertSession = `-- name: InsertSession :one
INSERT INTO sessions (user_id, token, device, ip_address, expires_at)
VALUES ($1, $2, $3, $4, $5)
RETURNING id
`
type InsertSessionParams struct {
UserID int64 `json:"userId"`
Token string `json:"token"`
Device *string `json:"device"`
IpAddress *string `json:"ipAddress"`
ExpiresAt pgtype.Timestamptz `json:"expiresAt"`
}
// PostgreSQL variants of the sqlite sessions queries.
func (q *Queries) InsertSession(ctx context.Context, arg InsertSessionParams) (int64, error) {
row := q.db.QueryRow(ctx, insertSession,
arg.UserID,
arg.Token,
arg.Device,
arg.IpAddress,
arg.ExpiresAt,
)
var id int64
err := row.Scan(&id)
return id, err
}
const listUserSessions = `-- name: ListUserSessions :many
SELECT id, user_id, token, device, ip_address, created_at, last_used, expires_at
FROM sessions
WHERE user_id = $1
ORDER BY created_at DESC
`
func (q *Queries) ListUserSessions(ctx context.Context, userID int64) ([]Session, error) {
rows, err := q.db.Query(ctx, listUserSessions, userID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Session{}
for rows.Next() {
var i Session
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.Token,
&i.Device,
&i.IpAddress,
&i.CreatedAt,
&i.LastUsed,
&i.ExpiresAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const touchSession = `-- name: TouchSession :exec
UPDATE sessions SET last_used = NOW() WHERE token = $1
`
func (q *Queries) TouchSession(ctx context.Context, token string) error {
_, err := q.db.Exec(ctx, touchSession, token)
return err
}
+219
View File
@@ -0,0 +1,219 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: users.sql
package pgdbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const banUser = `-- name: BanUser :exec
UPDATE users SET banned = TRUE, ban_reason = $1, ban_expires = $2 WHERE id = $3
`
type BanUserParams struct {
BanReason *string `json:"banReason"`
BanExpires pgtype.Timestamptz `json:"banExpires"`
ID int64 `json:"id"`
}
func (q *Queries) BanUser(ctx context.Context, arg BanUserParams) error {
_, err := q.db.Exec(ctx, banUser, arg.BanReason, arg.BanExpires, arg.ID)
return err
}
const countUsers = `-- name: CountUsers :one
SELECT COUNT(*) FROM users
`
func (q *Queries) CountUsers(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, countUsers)
var count int64
err := row.Scan(&count)
return count, err
}
const countUsersWithoutTOTP = `-- name: CountUsersWithoutTOTP :one
SELECT COUNT(*) FROM users WHERE banned = FALSE AND totp_secret IS NULL
`
func (q *Queries) CountUsersWithoutTOTP(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, countUsersWithoutTOTP)
var count int64
err := row.Scan(&count)
return count, err
}
const createUser = `-- name: CreateUser :one
INSERT INTO users (username, password, role_id)
VALUES ($1, $2, $3)
RETURNING id
`
type CreateUserParams struct {
Username string `json:"username"`
Password string `json:"password"`
RoleID int64 `json:"roleId"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (int64, error) {
row := q.db.QueryRow(ctx, createUser, arg.Username, arg.Password, arg.RoleID)
var id int64
err := row.Scan(&id)
return id, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT id, username, password, avatar, role_id, totp_secret, status,
created_at, last_seen, banned, ban_reason, ban_expires
FROM users WHERE id = $1
`
func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
row := q.db.QueryRow(ctx, getUserByID, id)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.Password,
&i.Avatar,
&i.RoleID,
&i.TotpSecret,
&i.Status,
&i.CreatedAt,
&i.LastSeen,
&i.Banned,
&i.BanReason,
&i.BanExpires,
)
return i, err
}
const getUserByUsername = `-- name: GetUserByUsername :one
SELECT id, username, password, avatar, role_id, totp_secret, status,
created_at, last_seen, banned, ban_reason, ban_expires
FROM users WHERE username = $1
`
// PostgreSQL variants of the sqlite users queries.
// Differences from sqlite:
// - `?` -> `$1`, `$2`, …
// - `COLLATE NOCASE` -> removed; the `username` column is CITEXT.
// - `datetime('now')` -> `NOW()`
// - `banned = 0/1` -> `banned = FALSE/TRUE` (column is BOOLEAN)
// - `:execresult INSERT` -> `:one ... RETURNING id` (pgx has no LastInsertId)
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) {
row := q.db.QueryRow(ctx, getUserByUsername, username)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.Password,
&i.Avatar,
&i.RoleID,
&i.TotpSecret,
&i.Status,
&i.CreatedAt,
&i.LastSeen,
&i.Banned,
&i.BanReason,
&i.BanExpires,
)
return i, err
}
const listMembers = `-- name: ListMembers :many
SELECT u.id, u.username, u.avatar, u.status, LOWER(r.name)
FROM users u
JOIN roles r ON u.role_id = r.id
WHERE u.banned = FALSE
ORDER BY u.username ASC
LIMIT 1000
`
type ListMembersRow struct {
ID int64 `json:"id"`
Username string `json:"username"`
Avatar *string `json:"avatar"`
Status string `json:"status"`
Lower string `json:"lower"`
}
func (q *Queries) ListMembers(ctx context.Context) ([]ListMembersRow, error) {
rows, err := q.db.Query(ctx, listMembers)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListMembersRow{}
for rows.Next() {
var i ListMembersRow
if err := rows.Scan(
&i.ID,
&i.Username,
&i.Avatar,
&i.Status,
&i.Lower,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const resetAllUserStatuses = `-- name: ResetAllUserStatuses :exec
UPDATE users SET status = 'offline' WHERE status != 'offline'
`
func (q *Queries) ResetAllUserStatuses(ctx context.Context) error {
_, err := q.db.Exec(ctx, resetAllUserStatuses)
return err
}
const unbanUser = `-- name: UnbanUser :exec
UPDATE users SET banned = FALSE, ban_reason = NULL, ban_expires = NULL WHERE id = $1
`
func (q *Queries) UnbanUser(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, unbanUser, id)
return err
}
const updateUserStatus = `-- name: UpdateUserStatus :exec
UPDATE users SET status = $1, last_seen = NOW() WHERE id = $2
`
type UpdateUserStatusParams struct {
Status string `json:"status"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) error {
_, err := q.db.Exec(ctx, updateUserStatus, arg.Status, arg.ID)
return err
}
const updateUserTOTPSecret = `-- name: UpdateUserTOTPSecret :exec
UPDATE users SET totp_secret = $1 WHERE id = $2
`
type UpdateUserTOTPSecretParams struct {
TotpSecret *string `json:"totpSecret"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateUserTOTPSecret(ctx context.Context, arg UpdateUserTOTPSecretParams) error {
_, err := q.db.Exec(ctx, updateUserTOTPSecret, arg.TotpSecret, arg.ID)
return err
}
+371
View File
@@ -0,0 +1,371 @@
//go:build postgres
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: voice.sql
package pgdbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const clearAllVoiceStates = `-- name: ClearAllVoiceStates :exec
DELETE FROM voice_states
`
func (q *Queries) ClearAllVoiceStates(ctx context.Context) error {
_, err := q.db.Exec(ctx, clearAllVoiceStates)
return err
}
const clearVoiceState = `-- name: ClearVoiceState :exec
DELETE FROM voice_states WHERE user_id = $1
`
func (q *Queries) ClearVoiceState(ctx context.Context, userID int64) error {
_, err := q.db.Exec(ctx, clearVoiceState, userID)
return err
}
const countActiveCameras = `-- name: CountActiveCameras :one
SELECT COUNT(*) FROM voice_states WHERE channel_id = $1 AND camera = TRUE
`
func (q *Queries) CountActiveCameras(ctx context.Context, channelID int64) (int64, error) {
row := q.db.QueryRow(ctx, countActiveCameras, channelID)
var count int64
err := row.Scan(&count)
return count, err
}
const enableCameraIfUnderLimit = `-- name: EnableCameraIfUnderLimit :execrows
UPDATE voice_states SET camera = TRUE
WHERE voice_states.user_id = $1 AND voice_states.channel_id = $2
AND (SELECT COUNT(*) FROM voice_states AS vs2 WHERE vs2.channel_id = $3 AND vs2.camera = TRUE) < $4
`
type EnableCameraIfUnderLimitParams struct {
UserID int64 `json:"userId"`
ChannelID int64 `json:"channelId"`
ChannelID_2 int64 `json:"channelId2"`
ChannelID_3 int64 `json:"channelId3"`
}
func (q *Queries) EnableCameraIfUnderLimit(ctx context.Context, arg EnableCameraIfUnderLimitParams) (int64, error) {
result, err := q.db.Exec(ctx, enableCameraIfUnderLimit,
arg.UserID,
arg.ChannelID,
arg.ChannelID_2,
arg.ChannelID_3,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const getAllVoiceStates = `-- name: GetAllVoiceStates :many
SELECT vs.user_id, vs.channel_id, u.username,
vs.muted, vs.deafened, vs.speaking,
vs.camera, vs.screenshare, vs.joined_at
FROM voice_states vs
JOIN users u ON u.id = vs.user_id
ORDER BY vs.channel_id, vs.joined_at ASC
`
type GetAllVoiceStatesRow struct {
UserID int64 `json:"userId"`
ChannelID int64 `json:"channelId"`
Username string `json:"username"`
Muted bool `json:"muted"`
Deafened bool `json:"deafened"`
Speaking bool `json:"speaking"`
Camera bool `json:"camera"`
Screenshare bool `json:"screenshare"`
JoinedAt pgtype.Timestamptz `json:"joinedAt"`
}
func (q *Queries) GetAllVoiceStates(ctx context.Context) ([]GetAllVoiceStatesRow, error) {
rows, err := q.db.Query(ctx, getAllVoiceStates)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetAllVoiceStatesRow{}
for rows.Next() {
var i GetAllVoiceStatesRow
if err := rows.Scan(
&i.UserID,
&i.ChannelID,
&i.Username,
&i.Muted,
&i.Deafened,
&i.Speaking,
&i.Camera,
&i.Screenshare,
&i.JoinedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChannelVoiceStates = `-- name: GetChannelVoiceStates :many
SELECT vs.user_id, vs.channel_id, u.username,
vs.muted, vs.deafened, vs.speaking,
vs.camera, vs.screenshare, vs.joined_at
FROM voice_states vs
JOIN users u ON u.id = vs.user_id
WHERE vs.channel_id = $1
ORDER BY vs.joined_at ASC
`
type GetChannelVoiceStatesRow struct {
UserID int64 `json:"userId"`
ChannelID int64 `json:"channelId"`
Username string `json:"username"`
Muted bool `json:"muted"`
Deafened bool `json:"deafened"`
Speaking bool `json:"speaking"`
Camera bool `json:"camera"`
Screenshare bool `json:"screenshare"`
JoinedAt pgtype.Timestamptz `json:"joinedAt"`
}
func (q *Queries) GetChannelVoiceStates(ctx context.Context, channelID int64) ([]GetChannelVoiceStatesRow, error) {
rows, err := q.db.Query(ctx, getChannelVoiceStates, channelID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetChannelVoiceStatesRow{}
for rows.Next() {
var i GetChannelVoiceStatesRow
if err := rows.Scan(
&i.UserID,
&i.ChannelID,
&i.Username,
&i.Muted,
&i.Deafened,
&i.Speaking,
&i.Camera,
&i.Screenshare,
&i.JoinedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserVoiceState = `-- name: GetUserVoiceState :one
SELECT vs.user_id, vs.channel_id, u.username,
vs.muted, vs.deafened, vs.speaking,
vs.camera, vs.screenshare, vs.joined_at
FROM voice_states vs
JOIN users u ON u.id = vs.user_id
WHERE vs.user_id = $1
`
type GetUserVoiceStateRow struct {
UserID int64 `json:"userId"`
ChannelID int64 `json:"channelId"`
Username string `json:"username"`
Muted bool `json:"muted"`
Deafened bool `json:"deafened"`
Speaking bool `json:"speaking"`
Camera bool `json:"camera"`
Screenshare bool `json:"screenshare"`
JoinedAt pgtype.Timestamptz `json:"joinedAt"`
}
func (q *Queries) GetUserVoiceState(ctx context.Context, userID int64) (GetUserVoiceStateRow, error) {
row := q.db.QueryRow(ctx, getUserVoiceState, userID)
var i GetUserVoiceStateRow
err := row.Scan(
&i.UserID,
&i.ChannelID,
&i.Username,
&i.Muted,
&i.Deafened,
&i.Speaking,
&i.Camera,
&i.Screenshare,
&i.JoinedAt,
)
return i, err
}
const joinVoiceChannel = `-- name: JoinVoiceChannel :exec
INSERT INTO voice_states (user_id, channel_id, muted, deafened, speaking, camera, screenshare, joined_at)
VALUES ($1, $2, FALSE, FALSE, FALSE, FALSE, FALSE, $3)
ON CONFLICT (user_id) DO UPDATE SET
channel_id = EXCLUDED.channel_id,
muted = FALSE,
deafened = FALSE,
speaking = FALSE,
camera = FALSE,
screenshare = FALSE,
joined_at = EXCLUDED.joined_at
`
type JoinVoiceChannelParams struct {
UserID int64 `json:"userId"`
ChannelID int64 `json:"channelId"`
JoinedAt pgtype.Timestamptz `json:"joinedAt"`
}
// PostgreSQL variants of the sqlite voice queries.
// voice_states boolean columns (muted, deafened, speaking, camera,
// screenshare) use FALSE/TRUE instead of 0/1.
func (q *Queries) JoinVoiceChannel(ctx context.Context, arg JoinVoiceChannelParams) error {
_, err := q.db.Exec(ctx, joinVoiceChannel, arg.UserID, arg.ChannelID, arg.JoinedAt)
return err
}
const joinVoiceChannelIfCapacity = `-- name: JoinVoiceChannelIfCapacity :execrows
INSERT INTO voice_states (user_id, channel_id, muted, deafened, speaking, camera, screenshare, joined_at)
SELECT $1, $2, FALSE, FALSE, FALSE, FALSE, FALSE, $3
WHERE (SELECT COUNT(*) FROM voice_states AS vs2 WHERE vs2.channel_id = $4) < $5
ON CONFLICT (user_id) DO UPDATE SET
channel_id = EXCLUDED.channel_id,
muted = FALSE,
deafened = FALSE,
speaking = FALSE,
camera = FALSE,
screenshare = FALSE,
joined_at = EXCLUDED.joined_at
`
type JoinVoiceChannelIfCapacityParams struct {
UserID int64 `json:"userId"`
ChannelID int64 `json:"channelId"`
JoinedAt pgtype.Timestamptz `json:"joinedAt"`
ChannelID_2 int64 `json:"channelId2"`
ChannelID_3 int64 `json:"channelId3"`
}
func (q *Queries) JoinVoiceChannelIfCapacity(ctx context.Context, arg JoinVoiceChannelIfCapacityParams) (int64, error) {
result, err := q.db.Exec(ctx, joinVoiceChannelIfCapacity,
arg.UserID,
arg.ChannelID,
arg.JoinedAt,
arg.ChannelID_2,
arg.ChannelID_3,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const leaveVoiceChannel = `-- name: LeaveVoiceChannel :exec
DELETE FROM voice_states WHERE user_id = $1
`
func (q *Queries) LeaveVoiceChannel(ctx context.Context, userID int64) error {
_, err := q.db.Exec(ctx, leaveVoiceChannel, userID)
return err
}
const leaveVoiceChannelIfMatch = `-- name: LeaveVoiceChannelIfMatch :execrows
DELETE FROM voice_states WHERE user_id = $1 AND channel_id = $2 AND joined_at = $3
`
type LeaveVoiceChannelIfMatchParams struct {
UserID int64 `json:"userId"`
ChannelID int64 `json:"channelId"`
JoinedAt pgtype.Timestamptz `json:"joinedAt"`
}
func (q *Queries) LeaveVoiceChannelIfMatch(ctx context.Context, arg LeaveVoiceChannelIfMatchParams) (int64, error) {
result, err := q.db.Exec(ctx, leaveVoiceChannelIfMatch, arg.UserID, arg.ChannelID, arg.JoinedAt)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const updateVoiceCamera = `-- name: UpdateVoiceCamera :exec
UPDATE voice_states SET camera = $1 WHERE user_id = $2
`
type UpdateVoiceCameraParams struct {
Camera bool `json:"camera"`
UserID int64 `json:"userId"`
}
func (q *Queries) UpdateVoiceCamera(ctx context.Context, arg UpdateVoiceCameraParams) error {
_, err := q.db.Exec(ctx, updateVoiceCamera, arg.Camera, arg.UserID)
return err
}
const updateVoiceDeafen = `-- name: UpdateVoiceDeafen :exec
UPDATE voice_states SET deafened = $1 WHERE user_id = $2
`
type UpdateVoiceDeafenParams struct {
Deafened bool `json:"deafened"`
UserID int64 `json:"userId"`
}
func (q *Queries) UpdateVoiceDeafen(ctx context.Context, arg UpdateVoiceDeafenParams) error {
_, err := q.db.Exec(ctx, updateVoiceDeafen, arg.Deafened, arg.UserID)
return err
}
const updateVoiceMute = `-- name: UpdateVoiceMute :exec
UPDATE voice_states SET muted = $1 WHERE user_id = $2
`
type UpdateVoiceMuteParams struct {
Muted bool `json:"muted"`
UserID int64 `json:"userId"`
}
func (q *Queries) UpdateVoiceMute(ctx context.Context, arg UpdateVoiceMuteParams) error {
_, err := q.db.Exec(ctx, updateVoiceMute, arg.Muted, arg.UserID)
return err
}
const updateVoiceScreenshare = `-- name: UpdateVoiceScreenshare :exec
UPDATE voice_states SET screenshare = $1 WHERE user_id = $2
`
type UpdateVoiceScreenshareParams struct {
Screenshare bool `json:"screenshare"`
UserID int64 `json:"userId"`
}
func (q *Queries) UpdateVoiceScreenshare(ctx context.Context, arg UpdateVoiceScreenshareParams) error {
_, err := q.db.Exec(ctx, updateVoiceScreenshare, arg.Screenshare, arg.UserID)
return err
}
const updateVoiceSpeaking = `-- name: UpdateVoiceSpeaking :exec
UPDATE voice_states SET speaking = $1 WHERE user_id = $2
`
type UpdateVoiceSpeakingParams struct {
Speaking bool `json:"speaking"`
UserID int64 `json:"userId"`
}
func (q *Queries) UpdateVoiceSpeaking(ctx context.Context, arg UpdateVoiceSpeakingParams) error {
_, err := q.db.Exec(ctx, updateVoiceSpeaking, arg.Speaking, arg.UserID)
return err
}
+44
View File
@@ -0,0 +1,44 @@
# OwnCord OTel dev overlay — Jaeger all-in-one tracing + Prometheus scrape target.
#
# Usage:
# make otel-up # starts Jaeger + Prometheus in the background
# make otel-down # stops and removes the containers
#
# Then build and run the server with OTel enabled:
# go build -tags otel .
# OWNCORD_TELEMETRY_OTLP_ENDPOINT=localhost:4317 ./owncord-server
#
# Jaeger UI: http://localhost:16686
# Prometheus UI: http://localhost:9090
# OwnCord metrics (when server is running): http://localhost:8443/metrics
services:
jaeger:
image: jaegertracing/all-in-one:latest
restart: unless-stopped
environment:
COLLECTOR_OTLP_ENABLED: "true"
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC (used by the server's tracer exporter)
- "4318:4318" # OTLP HTTP (alternative endpoint)
networks:
- otel-net
prometheus:
image: prom/prometheus:latest
restart: unless-stopped
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.retention.time=1h" # dev-only short retention
volumes:
- ./prometheus.dev.yml:/etc/prometheus/prometheus.yml:ro
ports:
- "9090:9090"
networks:
- otel-net
networks:
otel-net:
driver: bridge
+34 -3
View File
@@ -11,11 +11,18 @@ import (
"sync"
)
// Broadcaster is a function that sends a raw JSON payload to a WS channel.
// channelID=0 broadcasts to all connected clients. It is set by the WS
// wiring code (api/router.go) so the wazero-tagged build can emit events to
// clients without importing the ws package (avoids an import cycle).
type Broadcaster func(channelID int64, payload []byte)
// EventSink is the channel a subscribed plugin reads from. The wazero-tagged
// build forwards each event to the plugin's `on_event` exported function.
type EventSink struct {
mu sync.Mutex
subs map[string][]*Instance
mu sync.Mutex
subs map[string][]*Instance
broadcaster Broadcaster // set via SetBroadcaster; nil = no WS delivery
}
// NewEventSink returns a fresh sink. Used by the registry as the central
@@ -24,6 +31,30 @@ func NewEventSink() *EventSink {
return &EventSink{subs: make(map[string][]*Instance)}
}
// SetBroadcaster wires a WS-layer delivery function into the sink so that
// the wazero-tagged build can push plugin-generated events to WS clients.
// Safe to call from any goroutine; subsequent Emit calls use the new value.
func (s *EventSink) SetBroadcaster(b Broadcaster) {
s.mu.Lock()
s.broadcaster = b
s.mu.Unlock()
}
// Emit delivers payload to all WS clients subscribed to channelID (or every
// client when channelID==0). It is a no-op when no broadcaster has been set.
// Called by the wazero-tagged build's host-function implementation.
func (s *EventSink) Emit(channelID int64, payload []byte) {
if s == nil {
return
}
s.mu.Lock()
b := s.broadcaster
s.mu.Unlock()
if b != nil {
b(channelID, payload)
}
}
// Subscribe binds inst to topic. Multiple plugins may subscribe to the same
// topic — events fan out to every subscriber.
func (s *EventSink) Subscribe(topic string, inst *Instance) error {
@@ -62,7 +93,7 @@ func (s *EventSink) Dispatch(ctx context.Context, topic string, payload []byte)
subs := append([]*Instance(nil), s.subs[topic]...)
s.mu.Unlock()
for _, inst := range subs {
_ = inst // wazero-tagged build calls inst.module.invoke("on_event", payload)
_ = inst // wazero-tagged build calls inst.module.invoke("on_event", payload)
_ = ctx
_ = payload
}
+7 -1
View File
@@ -93,6 +93,12 @@ func (r *Registry) AssetHandler(inst *Instance) http.Handler {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
http.ServeFile(w, req, full)
f, openErr := os.Open(full)
if openErr != nil {
http.NotFound(w, req)
return
}
defer f.Close()
http.ServeContent(w, req, rel, info.ModTime(), f)
})
}
+3
View File
@@ -169,6 +169,9 @@ func validateRelativePath(p string) error {
if cleaned != p {
return fmt.Errorf("path %q is not in canonical form (clean: %q)", p, cleaned)
}
if cleaned == "." {
return fmt.Errorf("path %q refers to the current directory", p)
}
for _, seg := range strings.Split(cleaned, "/") {
if seg == ".." {
return fmt.Errorf("path %q contains parent traversal", p)
+12
View File
@@ -44,6 +44,10 @@ type Registry struct {
commands map[string]*Instance // command name → owning plugin
uiTabs []UITabBinding // declared by `ui` capability plugins
// sink is the hub→plugin event fan-out. Plugins subscribe to topics via
// Subscribe; the WS hub calls sink.Dispatch on each broadcast.
sink *EventSink
// runtimePlatform is set by the wazero-tagged build's NewRegistry to a
// concrete *wazero.Runtime. The default build leaves it nil and falls
// back to manifest-only behaviour.
@@ -82,6 +86,7 @@ func NewRegistry(cfg Config) (*Registry, error) {
plugins: make(map[int64]*Instance),
byName: make(map[string]*Instance),
commands: make(map[string]*Instance),
sink: NewEventSink(),
}, nil
}
@@ -103,6 +108,13 @@ func (r *Registry) Close(ctx context.Context) error {
return nil
}
// Sink returns the registry's EventSink, used by the WS hub to fan out
// broadcast events to subscribed plugins and by the wazero build to deliver
// plugin output back to WS clients.
func (r *Registry) Sink() *EventSink {
return r.sink
}
// LoadAll scans cfg.Directory and persists every plugin.json found into the
// PluginStore. In the wazero-tagged build it then compiles each entrypoint
// into a runnable module; the default build stops at the persistence step.
+13
View File
@@ -0,0 +1,13 @@
# Prometheus scrape config for local OTel development.
# Scrapes the OwnCord server's /metrics endpoint (Prometheus exporter,
# enabled when built with -tags otel).
global:
scrape_interval: 15s
scrape_configs:
- job_name: owncord
static_configs:
- targets: ["host.docker.internal:8443"]
scheme: https
tls_config:
insecure_skip_verify: true # dev-only; real certs in staging/prod
+5 -3
View File
@@ -93,7 +93,8 @@ func (s *ChannelService) ListVisibleChannels(userID int64) ([]db.Channel, error)
// Silent errors are returned as nil (typing indicators are best-effort).
func (s *ChannelService) HandleTyping(userID, channelID int64, limiter interface {
Allow(key string, limit int, window time.Duration) bool
}) (*db.Channel, error) {
},
) (*db.Channel, error) {
if channelID <= 0 {
return nil, nil
}
@@ -130,7 +131,8 @@ func (s *ChannelService) GetDMParticipantIDs(channelID int64) ([]int64, error) {
// HandlePresenceUpdate validates and persists a presence status change.
func (s *ChannelService) HandlePresenceUpdate(userID int64, status string, limiter interface {
Allow(key string, limit int, window time.Duration) bool
}) error {
},
) error {
// Rate limit.
ratKey := fmt.Sprintf("presence:%d", userID)
if limiter != nil && !limiter.Allow(ratKey, 1, 10*time.Second) {
@@ -161,7 +163,7 @@ func (s *ChannelService) HandleChannelFocus(userID, channelID int64) (*db.Channe
ch, err := s.st.GetChannel(channelID)
if err != nil || ch == nil {
return nil, fmt.Errorf("%w: channel not found", ErrForbidden)
return nil, fmt.Errorf("%w: channel not found", ErrNotFound)
}
if ch.Type == "dm" {
+1 -1
View File
@@ -92,7 +92,7 @@ func (s *DMService) CloseDM(userID, channelID int64) error {
ok, err := s.st.IsDMParticipant(userID, channelID)
if err != nil || !ok {
return fmt.Errorf("%w: not a participant in this DM", ErrForbidden)
return fmt.Errorf("%w: not a participant in this DM", ErrNotFound)
}
if err := s.st.CloseDM(userID, channelID); err != nil {
+2 -2
View File
@@ -55,8 +55,8 @@ func (s *InviteService) CreateInvite(createdBy int64, maxUses int, expiresInHour
}
invite, err := s.st.GetInvite(code)
if err != nil {
return nil, fmt.Errorf("%w: failed to fetch invite", ErrInternal)
if err != nil || invite == nil {
return nil, fmt.Errorf("%w: failed to retrieve invite", ErrInternal)
}
return invite, nil
}
+25 -21
View File
@@ -24,15 +24,15 @@ const maxMessageLen = 4000
// Common service-layer errors.
var (
ErrRateLimited = errors.New("rate limited")
ErrBadRequest = errors.New("bad request")
ErrNotFound = errors.New("not found")
ErrForbidden = errors.New("forbidden")
ErrInternal = errors.New("internal error")
ErrSlowMode = errors.New("slow mode")
ErrConflict = errors.New("conflict")
ErrBlocked = errors.New("blocked")
ErrDeletedMessage = errors.New("message is deleted")
ErrRateLimited = errors.New("rate limited")
ErrBadRequest = errors.New("bad request")
ErrNotFound = errors.New("not found")
ErrForbidden = errors.New("forbidden")
ErrInternal = errors.New("internal error")
ErrSlowMode = errors.New("slow mode")
ErrConflict = errors.New("conflict")
ErrBlocked = errors.New("blocked")
ErrDeletedMessage = errors.New("message is deleted")
)
// SendMessageParams contains validated input for sending a message.
@@ -49,11 +49,11 @@ type SendMessageParams struct {
// SendMessageResult contains the output of a successful message send.
type SendMessageResult struct {
MessageID int64
Timestamp string
Content string // sanitized content
IsDM bool
Channel *db.Channel
MessageID int64
Timestamp string
Content string // sanitized content
IsDM bool
Channel *db.Channel
// DM-specific fields populated when IsDM is true.
ParticipantIDs []int64
@@ -421,10 +421,10 @@ func (s *MessageService) handleReaction(userID, msgID int64, emoji string, add b
msg, err := s.st.GetMessage(msgID)
if err != nil || msg == nil {
return nil, fmt.Errorf("%w: message not found", ErrForbidden)
return nil, fmt.Errorf("%w: message not found", ErrBadRequest)
}
if msg.Deleted {
return nil, fmt.Errorf("%w: cannot react to deleted message", ErrDeletedMessage)
return nil, fmt.Errorf("%w: cannot react to deleted message", ErrBadRequest)
}
ch, chErr := s.st.GetChannel(msg.ChannelID)
@@ -433,7 +433,7 @@ func (s *MessageService) handleReaction(userID, msgID int64, emoji string, add b
if isDM {
ok, dmErr := s.st.IsDMParticipant(userID, msg.ChannelID)
if dmErr != nil || !ok {
return nil, fmt.Errorf("%w: not a DM participant", ErrForbidden)
return nil, fmt.Errorf("%w: not a DM participant", ErrBadRequest)
}
} else {
if !s.perms.HasChannelPerm(userID, msg.ChannelID, permissions.AddReactions) {
@@ -491,7 +491,7 @@ func (s *MessageService) GetMessages(userID, channelID, before int64, limit int)
if ch.Type == "dm" {
ok, err := s.st.IsDMParticipant(userID, channelID)
if err != nil || !ok {
return nil, false, fmt.Errorf("%w: access denied", ErrForbidden)
return nil, false, fmt.Errorf("%w: access denied", ErrNotFound)
}
} else {
if !s.perms.HasChannelPerm(userID, channelID, permissions.ReadMessages) {
@@ -582,7 +582,7 @@ func (s *MessageService) GetPinnedMessages(userID, channelID int64) ([]db.Messag
if ch.Type == "dm" {
ok, err := s.st.IsDMParticipant(userID, channelID)
if err != nil || !ok {
return nil, fmt.Errorf("%w: access denied", ErrForbidden)
return nil, fmt.Errorf("%w: access denied", ErrNotFound)
}
} else if !s.perms.HasChannelPerm(userID, channelID, permissions.ReadMessages) {
return nil, fmt.Errorf("%w: access denied", ErrForbidden)
@@ -606,7 +606,7 @@ func (s *MessageService) SetMessagePinned(userID, channelID, msgID int64, pinned
if ch.Type == "dm" {
ok, err := s.st.IsDMParticipant(userID, channelID)
if err != nil || !ok {
return fmt.Errorf("%w: access denied", ErrForbidden)
return fmt.Errorf("%w: access denied", ErrNotFound)
}
} else if !s.perms.HasChannelPerm(userID, channelID, permissions.ManageMessages) {
return fmt.Errorf("%w: missing MANAGE_MESSAGES permission", ErrForbidden)
@@ -634,7 +634,11 @@ func (s *MessageService) GetAccessibleChannelIDs(userID int64) ([]int64, error)
isAdmin := permissions.HasAdmin(role.Permissions)
var overrides map[int64]db.ChannelOverride
if !isAdmin {
overrides, _ = s.st.GetAllChannelPermissionsForRole(role.ID)
var overrideErr error
overrides, overrideErr = s.st.GetAllChannelPermissionsForRole(role.ID)
if overrideErr != nil {
return nil, fmt.Errorf("%w: failed to fetch channel overrides", ErrInternal)
}
if overrides == nil {
overrides = make(map[int64]db.ChannelOverride)
}
+204 -18
View File
@@ -31,6 +31,7 @@ import (
"database/sql"
"errors"
"fmt"
"strings"
"time"
// pgx's stdlib driver exposes pgx as a database/sql driver, letting
@@ -574,72 +575,257 @@ func (s *PostgresStore) GetAllSettings() (map[string]string, error) {
return nil, ErrPostgresNotImplemented
}
// ── EventStore (stubs — Phase B Step 7) ─────────────────────────────────────
// ── EventStore (Phase B Step 7) ──────────────────────────────────────────────
func (s *PostgresStore) PersistEvent(ctx context.Context, seq int64, eventType string, channelID int64, payload []byte) error {
return ErrPostgresNotImplemented
_, err := s.sqlDB.ExecContext(ctx,
`INSERT INTO events (seq, event_type, channel_id, payload) VALUES ($1, $2, $3, $4)`,
seq, eventType, channelID, payload,
)
if err != nil {
return fmt.Errorf("PersistEvent: %w", err)
}
return nil
}
func (s *PostgresStore) GetEventsSince(ctx context.Context, afterSeq int64, limit int) ([]db.PersistedEvent, error) {
return nil, ErrPostgresNotImplemented
rows, err := s.sqlDB.QueryContext(ctx,
`SELECT seq, event_type, channel_id, payload, created_at
FROM events
WHERE seq > $1
ORDER BY seq ASC
LIMIT $2`,
afterSeq, limit,
)
if err != nil {
return nil, fmt.Errorf("GetEventsSince: %w", err)
}
defer rows.Close()
return scanPgEventRows(rows)
}
func (s *PostgresStore) GetEventsSinceForChannels(ctx context.Context, afterSeq int64, channelIDs []int64, limit int) ([]db.PersistedEvent, error) {
return nil, ErrPostgresNotImplemented
if len(channelIDs) == 0 {
rows, err := s.sqlDB.QueryContext(ctx,
`SELECT seq, event_type, channel_id, payload, created_at
FROM events
WHERE seq > $1 AND channel_id = 0
ORDER BY seq ASC
LIMIT $2`,
afterSeq, limit,
)
if err != nil {
return nil, fmt.Errorf("GetEventsSinceForChannels (global only): %w", err)
}
defer rows.Close()
return scanPgEventRows(rows)
}
placeholders := make([]string, len(channelIDs))
args := make([]any, 0, len(channelIDs)+2)
args = append(args, afterSeq)
for i, cid := range channelIDs {
placeholders[i] = fmt.Sprintf("$%d", i+2)
args = append(args, cid)
}
args = append(args, limit)
query := fmt.Sprintf(
`SELECT seq, event_type, channel_id, payload, created_at
FROM events
WHERE seq > $1
AND (channel_id = 0 OR channel_id IN (%s))
ORDER BY seq ASC
LIMIT $%d`,
strings.Join(placeholders, ","),
len(channelIDs)+2,
)
rows, err := s.sqlDB.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("GetEventsSinceForChannels: %w", err)
}
defer rows.Close()
return scanPgEventRows(rows)
}
func (s *PostgresStore) PruneEventsOlderThan(ctx context.Context, cutoff time.Time) (int64, error) {
return 0, ErrPostgresNotImplemented
res, err := s.sqlDB.ExecContext(ctx,
`DELETE FROM events WHERE created_at < $1`,
cutoff.UTC(),
)
if err != nil {
return 0, fmt.Errorf("PruneEventsOlderThan: %w", err)
}
n, err := res.RowsAffected()
if err != nil {
return 0, fmt.Errorf("PruneEventsOlderThan RowsAffected: %w", err)
}
return n, nil
}
func (s *PostgresStore) GetMaxEventSeq(ctx context.Context) (int64, error) {
return 0, ErrPostgresNotImplemented
var maxSeq int64
err := s.sqlDB.QueryRowContext(ctx,
`SELECT COALESCE(MAX(seq), 0)::BIGINT FROM events`,
).Scan(&maxSeq)
if err != nil {
return 0, fmt.Errorf("GetMaxEventSeq: %w", err)
}
return maxSeq, nil
}
// ── PluginStore (stubs — Phase C Step 9) ────────────────────────────────────
// ── PluginStore (Phase C Step 9) ────────────────────────────────────────────
func (s *PostgresStore) InstallPlugin(ctx context.Context, name, version, manifestJSON string) (int64, error) {
return 0, ErrPostgresNotImplemented
var id int64
err := s.sqlDB.QueryRowContext(ctx,
`INSERT INTO plugins (name, version, manifest_json)
VALUES ($1, $2, $3)
ON CONFLICT (name) DO UPDATE
SET version = excluded.version,
manifest_json = excluded.manifest_json
RETURNING id`,
name, version, manifestJSON,
).Scan(&id)
if err != nil {
return 0, fmt.Errorf("InstallPlugin: %w", err)
}
return id, nil
}
func (s *PostgresStore) EnablePlugin(ctx context.Context, id int64) error {
return ErrPostgresNotImplemented
_, err := s.sqlDB.ExecContext(ctx, `UPDATE plugins SET enabled = TRUE WHERE id = $1`, id)
return err
}
func (s *PostgresStore) DisablePlugin(ctx context.Context, id int64) error {
return ErrPostgresNotImplemented
_, err := s.sqlDB.ExecContext(ctx, `UPDATE plugins SET enabled = FALSE WHERE id = $1`, id)
return err
}
func (s *PostgresStore) UninstallPlugin(ctx context.Context, id int64) error {
return ErrPostgresNotImplemented
_, err := s.sqlDB.ExecContext(ctx, `DELETE FROM plugins WHERE id = $1`, id)
return err
}
func (s *PostgresStore) GetPlugin(ctx context.Context, id int64) (*db.PluginRow, error) {
return nil, ErrPostgresNotImplemented
row := s.sqlDB.QueryRowContext(ctx,
`SELECT id, name, version, enabled, manifest_json, installed_at FROM plugins WHERE id = $1`,
id,
)
return scanPgPluginRow(row)
}
func (s *PostgresStore) GetPluginByName(ctx context.Context, name string) (*db.PluginRow, error) {
return nil, ErrPostgresNotImplemented
row := s.sqlDB.QueryRowContext(ctx,
`SELECT id, name, version, enabled, manifest_json, installed_at FROM plugins WHERE name = $1`,
name,
)
return scanPgPluginRow(row)
}
func (s *PostgresStore) ListPlugins(ctx context.Context) ([]db.PluginRow, error) {
return nil, ErrPostgresNotImplemented
rows, err := s.sqlDB.QueryContext(ctx,
`SELECT id, name, version, enabled, manifest_json, installed_at FROM plugins ORDER BY name`,
)
if err != nil {
return nil, fmt.Errorf("ListPlugins: %w", err)
}
defer rows.Close()
var out []db.PluginRow
for rows.Next() {
var p db.PluginRow
if err := rows.Scan(&p.ID, &p.Name, &p.Version, &p.Enabled, &p.ManifestJSON, &p.InstalledAt); err != nil {
return nil, fmt.Errorf("ListPlugins scan: %w", err)
}
out = append(out, p)
}
return out, rows.Err()
}
func (s *PostgresStore) PluginKVGet(ctx context.Context, pluginID int64, key string) ([]byte, error) {
return nil, ErrPostgresNotImplemented
var v []byte
err := s.sqlDB.QueryRowContext(ctx,
`SELECT value FROM plugin_kv WHERE plugin_id = $1 AND key = $2`,
pluginID, key,
).Scan(&v)
if err != nil {
return nil, err
}
return v, nil
}
func (s *PostgresStore) PluginKVSet(ctx context.Context, pluginID int64, key string, value []byte) error {
return ErrPostgresNotImplemented
_, err := s.sqlDB.ExecContext(ctx,
`INSERT INTO plugin_kv (plugin_id, key, value) VALUES ($1, $2, $3)
ON CONFLICT (plugin_id, key) DO UPDATE SET value = excluded.value`,
pluginID, key, value,
)
return err
}
func (s *PostgresStore) PluginKVDelete(ctx context.Context, pluginID int64, key string) error {
return ErrPostgresNotImplemented
_, err := s.sqlDB.ExecContext(ctx,
`DELETE FROM plugin_kv WHERE plugin_id = $1 AND key = $2`,
pluginID, key,
)
return err
}
func (s *PostgresStore) PluginKVScan(ctx context.Context, pluginID int64, prefix string, limit int) (map[string][]byte, error) {
return nil, ErrPostgresNotImplemented
rows, err := s.sqlDB.QueryContext(ctx,
`SELECT key, value FROM plugin_kv WHERE plugin_id = $1 AND key LIKE $2 ORDER BY key LIMIT $3`,
pluginID, prefix+"%", limit,
)
if err != nil {
return nil, fmt.Errorf("PluginKVScan: %w", err)
}
defer rows.Close()
out := make(map[string][]byte)
for rows.Next() {
var k string
var v []byte
if err := rows.Scan(&k, &v); err != nil {
return nil, err
}
out[k] = v
}
return out, rows.Err()
}
// ── postgres scan helpers ────────────────────────────────────────────────────
type pgRowScanner interface {
Scan(dest ...any) error
}
func scanPgPluginRow(row pgRowScanner) (*db.PluginRow, error) {
var p db.PluginRow
if err := row.Scan(&p.ID, &p.Name, &p.Version, &p.Enabled, &p.ManifestJSON, &p.InstalledAt); err != nil {
return nil, err
}
return &p, nil
}
type pgRowsScanner interface {
Next() bool
Scan(dest ...any) error
Err() error
}
func scanPgEventRows(rows pgRowsScanner) ([]db.PersistedEvent, error) {
var out []db.PersistedEvent
for rows.Next() {
var e db.PersistedEvent
if err := rows.Scan(&e.Seq, &e.EventType, &e.ChannelID, &e.Payload, &e.CreatedAt); err != nil {
return nil, fmt.Errorf("scanPgEventRows: %w", err)
}
out = append(out, e)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
// Compile-time interface check — fails to compile if any Store method is
+33 -29
View File
@@ -28,25 +28,25 @@ type Client struct {
ctx context.Context // derived from WS upgrade request; cancelled on disconnect
userID int64
user *db.User
channelID int64 // currently viewed channel for channel-scoped broadcasts
voiceChID int64 // voice channel the user is in (0 = not in voice); guarded by voiceMu
voiceJoinToken string // opaque join-instance token for the current voice session; guarded by voiceMu
e2eePubKey string // ECDH P-256 public key (base64) for voice E2EE; guarded by voiceMu
roleName string // cached role name for chat_message broadcasts
tokenHash string // SHA-256 hex of the session token; used for periodic revalidation
lastSeq uint64 // last_seq sent by the client during auth; 0 = fresh connection (e.g. F5 reload)
connectedAt time.Time // when the WS connection was established
remoteAddr string // client IP:port from the HTTP upgrade request
msgCount int // count of messages processed; resets after session check
msgsReceived int64 // total messages received over the lifetime of this connection
msgsSent int64 // total messages sent over the lifetime of this connection
msgsDropped int64 // messages dropped due to full send buffer
invalidCount int // consecutive invalid messages; reset on valid parse
lastActivity time.Time // last message received from this client; guarded by mu
sendClosed bool // true after all send channels have been closed
send chan []byte // normal-priority outbound messages (chat messages, reactions)
sendHigh chan []byte // high-priority outbound messages (DMs, mentions)
sendLow chan []byte // low-priority outbound messages (typing, presence) — dropped on overflow
channelID int64 // currently viewed channel for channel-scoped broadcasts
voiceChID int64 // voice channel the user is in (0 = not in voice); guarded by voiceMu
voiceJoinToken string // opaque join-instance token for the current voice session; guarded by voiceMu
e2eePubKey string // ECDH P-256 public key (base64) for voice E2EE; guarded by voiceMu
roleName string // cached role name for chat_message broadcasts
tokenHash string // SHA-256 hex of the session token; used for periodic revalidation
lastSeq uint64 // last_seq sent by the client during auth; 0 = fresh connection (e.g. F5 reload)
connectedAt time.Time // when the WS connection was established
remoteAddr string // client IP:port from the HTTP upgrade request
msgCount int // count of messages processed; resets after session check
msgsReceived int64 // total messages received over the lifetime of this connection
msgsSent int64 // total messages sent over the lifetime of this connection
msgsDropped int64 // messages dropped due to full send buffer
invalidCount int // consecutive invalid messages; reset on valid parse
lastActivity time.Time // last message received from this client; guarded by mu
sendClosed bool // true after all send channels have been closed
send chan []byte // normal-priority outbound messages (chat messages, reactions)
sendHigh chan []byte // high-priority outbound messages (DMs, mentions)
sendLow chan []byte // low-priority outbound messages (typing, presence) — dropped on overflow
mu syncutil.Mutex // guards sendClosed, msgCount, channelID, lastActivity, msgsReceived, msgsSent, msgsDropped
voiceMu syncutil.Mutex // guards voiceChID and voiceJoinToken
}
@@ -91,8 +91,8 @@ func NewTestClient(hub *Hub, userID int64, send chan []byte) *Client {
ctx: context.Background(),
userID: userID,
send: send,
sendHigh: make(chan []byte, sendHighBufSize),
sendLow: make(chan []byte, sendLowBufSize),
sendHigh: send, // unified for test observability
sendLow: send,
}
}
@@ -104,8 +104,8 @@ func NewTestClientWithChannel(hub *Hub, userID, channelID int64, send chan []byt
userID: userID,
channelID: channelID,
send: send,
sendHigh: make(chan []byte, sendHighBufSize),
sendLow: make(chan []byte, sendLowBufSize),
sendHigh: send, // unified for test observability
sendLow: send,
}
}
@@ -119,8 +119,8 @@ func NewTestClientWithUser(hub *Hub, user *db.User, channelID int64, send chan [
user: user,
channelID: channelID,
send: send,
sendHigh: make(chan []byte, sendHighBufSize),
sendLow: make(chan []byte, sendLowBufSize),
sendHigh: send, // unified for test observability
sendLow: send,
}
}
@@ -164,8 +164,8 @@ func NewTestClientWithTokenHash(hub *Hub, user *db.User, tokenHash string, chann
tokenHash: tokenHash,
channelID: channelID,
send: send,
sendHigh: make(chan []byte, sendHighBufSize),
sendLow: make(chan []byte, sendLowBufSize),
sendHigh: send, // unified for test observability
sendLow: send,
}
}
@@ -358,7 +358,11 @@ func (c *Client) closeAllSendLocked() {
if !c.sendClosed {
c.sendClosed = true
close(c.send)
close(c.sendHigh)
close(c.sendLow)
if c.sendHigh != c.send {
close(c.sendHigh)
}
if c.sendLow != c.send && c.sendLow != c.sendHigh {
close(c.sendLow)
}
}
}
+5 -1
View File
@@ -14,6 +14,8 @@ import (
"github.com/owncord/server/auth"
"github.com/owncord/server/config"
"github.com/owncord/server/db"
"github.com/owncord/server/service"
"github.com/owncord/server/store"
"github.com/owncord/server/ws"
)
@@ -76,7 +78,9 @@ func newCoverageHub(t *testing.T) (*ws.Hub, *db.DB) {
t.Helper()
database := openCoverageDB(t)
limiter := auth.NewRateLimiter()
hub := ws.NewHub(database, limiter, nil)
st := store.NewSQLiteStore(database)
svc := service.New(st, limiter)
hub := ws.NewHub(database, limiter, svc)
// Inject a test LiveKit client so voice_join passes the livekit!=nil guard.
lk, err := ws.NewLiveKitClient(&config.VoiceConfig{
+8 -2
View File
@@ -40,8 +40,12 @@ func newEmitTestHub() *Hub {
// registerEmitTestClient creates a test client, registers it directly in the
// hub's client map, and returns the send channel for assertions.
func registerEmitTestClient(h *Hub, userID, channelID int64) chan []byte {
send := make(chan []byte, 64)
send := make(chan []byte, 192) // sized for all priority levels
c := NewTestClientWithChannel(h, userID, channelID, send)
// Wire high- and low-priority channels to the same observable channel so
// drainChan captures messages regardless of which priority path delivers.
c.sendHigh = send
c.sendLow = send
h.clients[userID] = c
// Subscribe to pub/sub topics so deliverBroadcast can reach this client.
h.pubsub.Subscribe(c, TopicGlobal)
@@ -54,8 +58,10 @@ func registerEmitTestClient(h *Hub, userID, channelID int64) chan []byte {
// registerEmitTestVoiceClient creates a test client in a voice channel.
func registerEmitTestVoiceClient(h *Hub, userID, channelID, voiceChID int64) chan []byte {
send := make(chan []byte, 64)
send := make(chan []byte, 192)
c := NewTestClientWithChannel(h, userID, channelID, send)
c.sendHigh = send
c.sendLow = send
SetClientVoiceChID(c, voiceChID)
h.clients[userID] = c
// Subscribe to pub/sub topics so deliverBroadcast can reach this client.
+9 -4
View File
@@ -4,8 +4,11 @@ import (
"context"
"testing"
"github.com/owncord/server/auth"
"github.com/owncord/server/db"
"github.com/owncord/server/permissions"
"github.com/owncord/server/service"
"github.com/owncord/server/store"
)
// newFocusTestDeps creates an in-memory DB with a user (role=Owner, id=1) and
@@ -30,8 +33,9 @@ func newFocusTestDeps(t *testing.T) (PresenceDeps, int64, int64) {
t.Fatalf("CreateChannel: %v", err)
}
perms := permissions.NewChecker(database)
deps := PresenceDeps{DB: database, Limiter: nil, Permissions: perms}
st := store.NewSQLiteStore(database)
svc := service.New(st, auth.NewRateLimiter())
deps := PresenceDeps{Limiter: nil, ChannelSvc: svc.Channels}
return deps, userID, chID
}
@@ -107,8 +111,9 @@ func TestChannelFocusV2_NoPermission_ReturnsForbidden(t *testing.T) {
t.Fatalf("INSERT channel_overrides: %v", err)
}
perms := permissions.NewChecker(database)
deps := PresenceDeps{DB: database, Limiter: nil, Permissions: perms}
st := store.NewSQLiteStore(database)
svc := service.New(st, auth.NewRateLimiter())
deps := PresenceDeps{Limiter: nil, ChannelSvc: svc.Channels}
cmd := ChannelFocusCmd{userID: userID, channelID: chID}
info := ClientInfo{UserID: userID, Username: "noperm"}
+119
View File
@@ -0,0 +1,119 @@
// Phase C Step 9 — plugin slash-command dispatcher.
//
// chat_command routes a slash command from a WS client to a registered plugin.
// If no plugin owns the command, an error is returned to the sender. If the
// plugin returns a Reply, it is sent only to the invoking client (ephemeral).
// If the plugin returns a Broadcast string, it is broadcast to the channel.
package ws
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"strings"
)
const MsgTypeChatCommand = "chat_command"
// chatCommandPayload is the client-supplied payload for a chat_command message.
type chatCommandPayload struct {
ChannelID int64 `json:"channel_id"`
Command string `json:"command"` // including leading slash, e.g. "/hello"
Args []string `json:"args"`
}
// registerPluginCommandHandler registers the chat_command V1 handler.
func registerPluginCommandHandler(r *HandlerRegistry) {
r.Register(MsgTypeChatCommand, handlePluginCommand)
}
// handlePluginCommand dispatches a slash command to the owning plugin via
// hub.pluginRegistry. Returns an error to the client when:
// - the payload is malformed,
// - the command name is empty,
// - no plugin owns the command (unknown command),
// - the plugin returns an error reply.
func handlePluginCommand(ctx context.Context, h *Hub, c *Client, reqID string, payload json.RawMessage) {
var p chatCommandPayload
if err := json.Unmarshal(payload, &p); err != nil {
c.sendMsg(buildErrorMsg(ErrCodeBadRequest, "invalid chat_command payload"))
return
}
cmd := strings.TrimSpace(p.Command)
if cmd == "" {
c.sendMsg(buildErrorMsg(ErrCodeBadRequest, "command must not be empty"))
return
}
if h.pluginRegistry == nil {
c.sendMsg(buildErrorMsg(ErrCodeBadRequest, fmt.Sprintf("unknown command: %s (no plugins loaded)", cmd)))
return
}
result, handled := h.pluginRegistry.DispatchCommand(ctx, c.userID, p.ChannelID, cmd, p.Args)
if !handled {
c.sendMsg(buildErrorMsg(ErrCodeBadRequest, fmt.Sprintf("unknown command: %s", cmd)))
return
}
if result == nil {
// Plugin acknowledged with no output.
return
}
if result.Reply != "" {
// Ephemeral reply — sent only to the invoking client.
c.sendMsg(buildCommandReply(reqID, result.Reply))
}
if result.Broadcast != "" && p.ChannelID != 0 {
// Channel broadcast — visible to everyone in the channel.
msg := buildCommandBroadcast(p.ChannelID, c.userID, cmd, result.Broadcast)
h.BroadcastToChannel(p.ChannelID, msg)
slog.Info("plugin command broadcast", "cmd", cmd, "channel_id", p.ChannelID, "user_id", c.userID)
}
}
// buildCommandReply builds an ephemeral command_reply envelope.
func buildCommandReply(reqID, text string) []byte {
type payload struct {
Text string `json:"text"`
}
type envelope struct {
Type string `json:"type"`
ReqID string `json:"req_id,omitempty"`
Payload payload `json:"payload"`
}
raw, _ := json.Marshal(envelope{
Type: "command_reply",
ReqID: reqID,
Payload: payload{Text: text},
})
return raw
}
// buildCommandBroadcast builds a plugin_broadcast envelope sent to a channel.
func buildCommandBroadcast(channelID, userID int64, cmd, text string) []byte {
type payload struct {
ChannelID int64 `json:"channel_id"`
UserID int64 `json:"user_id"`
Command string `json:"command"`
Text string `json:"text"`
}
type envelope struct {
Type string `json:"type"`
Payload payload `json:"payload"`
}
raw, _ := json.Marshal(envelope{
Type: "plugin_broadcast",
Payload: payload{
ChannelID: channelID,
UserID: userID,
Command: cmd,
Text: text,
},
})
return raw
}
+169
View File
@@ -0,0 +1,169 @@
package ws_test
// handlers_command_test.go — tests for the chat_command handler and
// plugin EventSink wiring (Phase C Step 9).
import (
"encoding/json"
"testing"
"github.com/owncord/server/plugin"
"github.com/owncord/server/store"
"github.com/owncord/server/ws"
)
// compile-time check that SetPluginRegistry is exported.
var _ = (*ws.Hub)(nil)
// ─── chat_command dispatch via HandleMessageForTest ───────────────────────────
// TestChatCommand_NoRegistry returns an error when no plugin registry is wired.
func TestChatCommand_NoRegistry_ReturnsError(t *testing.T) {
hub, database := newTestHub(t)
_ = database
send := make(chan []byte, 4)
c := ws.NewTestClient(hub, 1, send)
hub.Register(c)
defer hub.Unregister(c)
raw, _ := json.Marshal(map[string]any{
"type": "chat_command",
"payload": map[string]any{
"channel_id": int64(1),
"command": "/hello",
"args": []string{},
},
})
hub.HandleMessageForTest(c, raw)
select {
case msg := <-send:
var env map[string]any
if err := json.Unmarshal(msg, &env); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if env["type"] != "error" {
t.Fatalf("expected type=error, got %v; raw=%s", env["type"], msg)
}
default:
t.Fatal("expected error message to client")
}
}
// TestChatCommand_UnknownCommand returns an error when the registry has no
// plugin owning the command.
func TestChatCommand_UnknownCommand_ReturnsError(t *testing.T) {
hub, database := newTestHub(t)
_ = database
send := make(chan []byte, 4)
c := ws.NewTestClient(hub, 1, send)
hub.Register(c)
defer hub.Unregister(c)
mem := store.NewMemStore()
reg, err := plugin.NewRegistry(plugin.Config{Store: mem})
if err != nil {
t.Fatalf("NewRegistry: %v", err)
}
hub.SetPluginRegistry(reg)
raw, _ := json.Marshal(map[string]any{
"type": "chat_command",
"payload": map[string]any{
"channel_id": int64(1),
"command": "/notexist",
"args": []string{},
},
})
hub.HandleMessageForTest(c, raw)
select {
case msg := <-send:
var env map[string]any
_ = json.Unmarshal(msg, &env)
if env["type"] != "error" {
t.Fatalf("expected type=error, got %v", env["type"])
}
default:
t.Fatal("expected error message to client")
}
}
// TestChatCommand_MalformedPayload returns bad-request when payload is not valid JSON.
func TestChatCommand_MalformedPayload_ReturnsBadRequest(t *testing.T) {
hub, database := newTestHub(t)
_ = database
send := make(chan []byte, 4)
c := ws.NewTestClient(hub, 1, send)
hub.Register(c)
defer hub.Unregister(c)
raw := []byte(`{"type":"chat_command","payload":"not-an-object"}`)
hub.HandleMessageForTest(c, raw)
select {
case msg := <-send:
var env map[string]any
_ = json.Unmarshal(msg, &env)
if env["type"] != "error" {
t.Fatalf("expected type=error, got %v", env["type"])
}
default:
t.Fatal("expected error message")
}
}
// ─── EventSink.Emit ───────────────────────────────────────────────────────────
// TestEventSink_Emit_DeliversToBroadcaster verifies that Emit calls the wired
// broadcaster with the correct channelID and payload.
func TestEventSink_Emit_DeliversToBroadcaster(t *testing.T) {
sink := plugin.NewEventSink()
var gotChannelID int64
var gotPayload []byte
sink.SetBroadcaster(func(channelID int64, payload []byte) {
gotChannelID = channelID
gotPayload = payload
})
want := []byte(`{"type":"plugin_event"}`)
sink.Emit(42, want)
if gotChannelID != 42 {
t.Fatalf("expected channelID=42, got %d", gotChannelID)
}
if string(gotPayload) != string(want) {
t.Fatalf("expected payload=%s, got %s", want, gotPayload)
}
}
// TestEventSink_Emit_NilBroadcaster_NoOp verifies that Emit is safe when no
// broadcaster has been set.
func TestEventSink_Emit_NilBroadcaster_NoOp(t *testing.T) {
sink := plugin.NewEventSink()
sink.Emit(1, []byte(`{"type":"x"}`)) // must not panic
}
// TestEventSink_Emit_NilSink_NoOp verifies Emit is nil-safe.
func TestEventSink_Emit_NilSink_NoOp(t *testing.T) {
var sink *plugin.EventSink
sink.Emit(1, []byte(`{}`)) // must not panic
}
// ─── Hub plugin-sink wiring ───────────────────────────────────────────────────
// TestHub_SetPluginEventSink_NoOp verifies that wiring a plugin sink and
// broadcasting through the hub does not panic (default build no-ops Dispatch).
func TestHub_SetPluginEventSink_NoOp(t *testing.T) {
hub, database := newTestHub(t)
_ = database
go hub.Run()
defer hub.Stop()
sink := plugin.NewEventSink()
hub.SetPluginEventSink(sink)
// Must not panic.
hub.BroadcastToAll([]byte(`{"type":"test"}`))
}
+5 -1
View File
@@ -10,6 +10,8 @@ import (
"github.com/owncord/server/auth"
"github.com/owncord/server/db"
"github.com/owncord/server/permissions"
"github.com/owncord/server/service"
"github.com/owncord/server/store"
"github.com/owncord/server/ws"
)
@@ -71,7 +73,9 @@ func newHandlerHub(t *testing.T) (*ws.Hub, *db.DB) {
t.Helper()
database := openHandlerDB(t)
limiter := auth.NewRateLimiter()
hub := ws.NewHub(database, limiter, nil)
st := store.NewSQLiteStore(database)
svc := service.New(st, limiter)
hub := ws.NewHub(database, limiter, svc)
go hub.Run()
t.Cleanup(func() { hub.Stop() })
return hub, database
+45
View File
@@ -14,6 +14,7 @@ import (
"github.com/owncord/server/auth"
"github.com/owncord/server/db"
"github.com/owncord/server/permissions"
"github.com/owncord/server/plugin"
"github.com/owncord/server/service"
"github.com/owncord/server/store"
"github.com/owncord/server/syncutil"
@@ -55,6 +56,10 @@ type Hub struct {
eventPersister *EventPersister
eventStore store.EventStore // read path for cold-tier replay
// Phase C Step 9 — plugin wiring.
pluginRegistry *plugin.Registry // slash-command dispatch; nil = no plugins
pluginSink *plugin.EventSink // hub→plugin event fan-out; nil = no plugins
// Phase B Step 7 — reconnection tier metrics. Incremented per resume.
reconnectTierBuf atomic.Uint64
reconnectTierDB atomic.Uint64
@@ -117,6 +122,7 @@ func NewHub(database *db.DB, limiter *auth.RateLimiter, svc *service.Services) *
registerChatHandlers(reg, chatDeps)
registerPresenceHandlers(reg, presenceDeps)
registerReactionHandlers(reg, reactionDeps)
registerPluginCommandHandler(reg) // Phase C Step 9 — plugin slash commands
registerVoiceControlsV2(reg, VoiceDeps{
DB: h.db,
Limiter: h.limiter,
@@ -410,6 +416,18 @@ func (h *Hub) registerNow(c *Client) {
// Subscribe the new client to default pub/sub topics.
h.pubsub.Subscribe(c, TopicGlobal)
h.pubsub.Subscribe(c, UserTopic(c.userID))
// If the client already has a focused channel (e.g. test clients created with
// NewTestClientWithChannel, or reconnecting clients), subscribe immediately so
// deliverBroadcast can reach them without waiting for a channel_focus message.
if chID := c.getChannelID(); chID != 0 {
h.pubsub.Subscribe(c, ChannelTopic(chID))
}
// If the client is already in a voice channel (e.g. reconnect or test setup),
// subscribe to that channel's topic so voice-scoped and channel-scoped
// broadcasts reach them.
if voiceChID := c.getVoiceChID(); voiceChID != 0 {
h.pubsub.Subscribe(c, ChannelTopic(voiceChID))
}
}
func (h *Hub) unregisterNow(c *Client) bool {
@@ -648,6 +666,19 @@ func (h *Hub) SetEventStore(s store.EventStore) {
h.eventStore = s
}
// SetPluginRegistry wires the plugin.Registry so the hub can dispatch
// slash commands (chat_command messages) to plugin-owned handlers.
// Pass nil to disable plugin command dispatch.
func (h *Hub) SetPluginRegistry(r *plugin.Registry) {
h.pluginRegistry = r
}
// SetPluginEventSink wires the plugin.EventSink so the hub fans out each
// sequenced broadcast to subscribed plugins. Pass nil to disable.
func (h *Hub) SetPluginEventSink(s *plugin.EventSink) {
h.pluginSink = s
}
// ReconnectTierStats returns the per-tier resume hit counters in the order
// (buffer, db, full). Phase B Step 7 metrics surface; OpenTelemetry meters
// (Step 8) read from the same atomics.
@@ -856,6 +887,20 @@ func (h *Hub) deliverBroadcast(bm broadcastMsg) {
h.replayBuf.Push(seq, bm.channelID, msg)
h.persistEvent(seq, bm.channelID, msg)
// Fan out to plugins subscribed to this event type (Phase C Step 9).
// Dispatch is a no-op in the default build; the wazero build calls into
// the WASM module. Dispatch is called outside seqMu after we release it
// conceptually — but since seqMu is still held here, the call MUST NOT
// re-enter the hub. The default build is safe; the wazero build should
// dispatch asynchronously once the runtime is real.
if h.pluginSink != nil {
eventType := extractEventType(msg)
if eventType == "" {
eventType = "broadcast"
}
h.pluginSink.Dispatch(context.Background(), eventType, msg)
}
if bm.channelID == 0 {
// Global broadcast — deliver to every connected client.
h.pubsub.PublishGlobal(msg)
+146
View File
@@ -0,0 +1,146 @@
package ws_test
// reconnect_db_test.go — buffer-miss → DB cold-tier replay integration test.
//
// The hub's ring buffer holds 1000 events. When a reconnecting client's
// last_seq is older than the buffer's oldest entry, EventsSinceFiltered returns
// nil and handleReconnect falls back to the EventStore. This file verifies that
// code path end-to-end against a real httptest WebSocket server.
import (
"context"
"encoding/json"
"fmt"
"net/http/httptest"
"strings"
"testing"
"time"
"nhooyr.io/websocket"
"github.com/owncord/server/auth"
"github.com/owncord/server/store"
"github.com/owncord/server/ws"
)
// TestReconnect_BufferMiss_FallsBackToDBTier verifies that when a client
// reconnects with a last_seq that is older than the ring buffer's oldest entry,
// the hub falls back to the EventStore (DB tier) and sends the missed events.
//
// Setup:
// - Ring buffer size = 1000; push seqs 501..1500 → oldestSeq = 501.
// - MemStore contains 100 global events at seqs 501..600.
// - Client reconnects with last_seq = 500.
// - Buffer: 500 <= 501 → returns nil.
// - DB: returns seqs > 500 with channelID = 0 (global, no permission filter).
//
// Asserts:
// - auth_ok is received with replay_source = "db".
// - hub.ReconnectTierStats() db counter = 1.
func TestReconnect_BufferMiss_FallsBackToDBTier(t *testing.T) {
database := openServeTestDB(t)
limiter := auth.NewRateLimiter()
// Create a user. role_id=1 intentionally does not exist in the test DB so
// computeAllowedChannels returns an empty channel set — but events with
// channelID=0 (global) bypass the per-channel filter in MemStore and in
// EventsSinceFiltered, so they are always returned.
userID, err := database.CreateUser("reconnect-db-user", "hash", 1)
if err != nil {
t.Fatalf("CreateUser: %v", err)
}
token, err := auth.GenerateToken()
if err != nil {
t.Fatalf("GenerateToken: %v", err)
}
if _, err := database.CreateSession(userID, auth.HashToken(token), "test", "127.0.0.1"); err != nil {
t.Fatalf("CreateSession: %v", err)
}
// Pre-populate the event store with 100 global events (seqs 501..600).
// channelID=0 means "global broadcast" — MemStore.GetEventsSinceForChannels
// returns them regardless of the allowed-channel filter.
memStore := store.NewMemStore()
bgCtx := context.Background()
for seq := int64(501); seq <= 600; seq++ {
payload := []byte(fmt.Sprintf(`{"seq":%d,"type":"broadcast"}`, seq))
if err := memStore.PersistEvent(bgCtx, seq, "broadcast", 0, payload); err != nil {
t.Fatalf("PersistEvent seq=%d: %v", seq, err)
}
}
// Build hub, attach the MemStore as the cold-tier read path.
hub := ws.NewHub(database, limiter, nil)
hub.SetEventStore(memStore)
go hub.Run()
defer hub.Stop()
// Fill the ring buffer with seqs 501..1500 (exactly 1000 entries).
// After 1000 pushes into a 1000-slot buffer, oldestSeq = 501 (the first
// entry pushed). A client with last_seq=500 satisfies 500 <= 501, so
// EventsSinceFiltered returns nil and the DB tier is invoked.
rb := hub.ReplayBuffer()
dummyPayload := []byte(`{"type":"broadcast"}`)
for seq := uint64(501); seq <= 1500; seq++ {
rb.Push(seq, 0, dummyPayload)
}
if oldest := rb.OldestSeq(); oldest != 501 {
t.Fatalf("pre-condition: expected oldestSeq=501, got %d", oldest)
}
// Spin up a real HTTP+WS server.
handler := ws.ServeWS(hub, database, []string{"*"})
srv := httptest.NewServer(handler)
defer srv.Close()
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")
dialCtx, cancel := context.WithTimeout(bgCtx, 10*time.Second)
defer cancel()
// Dial and authenticate with last_seq=500 — this triggers the reconnect
// path (handleReconnect) rather than the fresh-connect path.
conn, dialResp, dialErr := websocket.Dial(dialCtx, wsURL, nil)
if dialResp != nil && dialResp.Body != nil {
_ = dialResp.Body.Close()
}
if dialErr != nil {
t.Fatalf("websocket.Dial: %v", dialErr)
}
defer func() { _ = conn.Close(websocket.StatusNormalClosure, "") }()
authMsg := map[string]any{
"type": "auth",
"payload": map[string]any{
"token": token,
"last_seq": uint64(500),
},
}
raw, _ := json.Marshal(authMsg)
if err := conn.Write(dialCtx, websocket.MessageText, raw); err != nil {
t.Fatalf("write auth: %v", err)
}
// The first message back must be auth_ok with replay_source="db".
_, msg, err := conn.Read(dialCtx)
if err != nil {
t.Fatalf("read auth_ok: %v", err)
}
var resp map[string]any
if err := json.Unmarshal(msg, &resp); err != nil {
t.Fatalf("unmarshal response: %v; raw=%s", err, msg)
}
if resp["type"] != "auth_ok" {
t.Fatalf("expected type=auth_ok, got %v; raw=%s", resp["type"], msg)
}
payloadField, _ := resp["payload"].(map[string]any)
if payloadField["replay_source"] != "db" {
t.Fatalf("expected replay_source=db, got %v", payloadField["replay_source"])
}
// hub.reconnectTierDB is incremented before auth_ok is sent, so the
// counter is stable by the time we read auth_ok.
_, dbTier, _ := hub.ReconnectTierStats()
if dbTier != 1 {
t.Fatalf("expected db tier count=1, got %d", dbTier)
}
}
+5 -1
View File
@@ -16,6 +16,8 @@ import (
"nhooyr.io/websocket"
"github.com/owncord/server/auth"
"github.com/owncord/server/service"
"github.com/owncord/server/store"
"github.com/owncord/server/ws"
)
@@ -996,7 +998,9 @@ func TestServeWS_writePump_MessageDelivered(t *testing.T) {
func TestIntegration_MessageRoundTrip(t *testing.T) {
database := openServeTestDB(t)
limiter := auth.NewRateLimiter()
hub := ws.NewHub(database, limiter, nil)
st := store.NewSQLiteStore(database)
svc := service.New(st, limiter)
hub := ws.NewHub(database, limiter, svc)
go hub.Run()
defer hub.Stop()