From 46a6bd02d95efab3595f6eb45a0c89fa648ff23f Mon Sep 17 00:00:00 2001 From: noelorin Date: Fri, 21 Aug 2026 00:31:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=85=A5=20sync-live=20?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E9=87=8D=E6=9E=84=EF=BC=8C=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=85=A8=E9=9D=A2=E5=AF=B9=E9=BD=90=E6=96=B0=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E4=B8=8E=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - proto: live_sfu.proto -> sync_live.proto (synclive.v1/SyncLive),重生成 gen/sync_live.* - module: example.com/live-sfu -> sync-live,go.mod/go.sum 同步 - 后端: 新增 internal/room (role/member/room/service/store/permission),manage 治理面 internal/server/manage.go,auth(jwt/policy) 与 gateway/server 重构,适配 token_type/access+refresh 与 HttpOnly Cookie - 前端: types/api/auth/permissions 全量对齐,rooms 优先 query 分页、manage 创建,login 双模式+角色降级,用户管理封禁/解封,新增 /manage 5 Tab 治理页,主题与路由同步 - 构建: web 产物直出 internal/server/static,role 字符串序列化兼容数字,libsql 链接告警忽略 验证: web tsc --noEmit / go test / vite build / go build 常驻联调通过 (role:"creator" / manage/overview/rooms/query/create/members/refresh) --- .env.example | 17 +- .gitignore | 3 + README.md | 120 ++- api/{live_sfu.proto => sync_live.proto} | 8 +- cmd/server/main.go | 11 +- deploy/docker-compose.yml | 2 +- design.md | 40 +- docs/streaming-pipeline.md | 18 +- gen/live_sfu_grpc.pb.go | 517 ---------- gen/{live_sfu.pb.go => sync_live.pb.go} | 488 +++++----- gen/sync_live_grpc.pb.go | 517 ++++++++++ go.mod | 27 +- go.sum | 70 ++ internal/auth/auth.go | 139 ++- internal/auth/auth_test.go | 12 +- internal/auth/jwt.go | 85 +- internal/auth/policy.csv | 34 +- internal/auth/store.go | 124 ++- internal/config/config.go | 130 ++- internal/db/db.go | 102 +- internal/room/member.go | 95 ++ internal/room/permission.go | 211 ++++ internal/room/role.go | 156 +++ internal/room/room.go | 281 ++++++ internal/room/service.go | 263 +++++ internal/room/store.go | 415 ++++++++ internal/server/auth_handler.go | 185 +++- internal/server/gateway.go | 82 +- internal/server/grpc.go | 6 +- internal/server/grpc_test.go | 12 +- internal/server/manage.go | 485 +++++++++ internal/server/rooms.go | 20 +- internal/server/server.go | 62 +- internal/server/service.go | 50 +- ...ht-CYxcOjf4.js => arrow-right-BaPiev--.js} | 2 +- .../server/static/assets/cloud-DZf51awA.js | 1 + internal/server/static/assets/eye-DaP7VxYZ.js | 1 + .../server/static/assets/hash-ORiZzY9V.js | 1 + .../server/static/assets/index-BTRwyDO1.js | 2 + .../server/static/assets/index-BjOqv38F.js | 2 - .../server/static/assets/index-OrVzMh6m.css | 2 + .../server/static/assets/index-iV_C3-n9.css | 2 - .../server/static/assets/log-out-BSFDyM4m.js | 1 + .../server/static/assets/login-B-50707a.js | 1 + .../server/static/assets/login-DBhoyV9Z.js | 1 - .../server/static/assets/manage-BUOThicK.js | 1 + .../static/assets/message-square-1ZGuav88.js | 1 + ...y-Br3jNYD4.js => monitor-play-Bo4SAxB6.js} | 2 +- .../server/static/assets/publish-D-rS1-js.js | 1 + .../server/static/assets/publish-ukhzw-Kl.js | 1 - ...er-CN-xjSIh.js => radio-tower-UMy9OSpZ.js} | 2 +- .../server/static/assets/rooms-BN1WD1kf.js | 1 - .../server/static/assets/rooms-DcveOed7.js | 1 + .../server/static/assets/routes-BsH3d8_E.js | 1 + .../server/static/assets/routes-VQMbZFXp.js | 1 - ...{server-DmfY9b1N.js => server-eXVtBH0Y.js} | 2 +- ...k-DH7IfQwC.js => shield-check-PAk2q1NC.js} | 2 +- internal/server/static/assets/ui-BEZm0Fjb.js | 1 - internal/server/static/assets/ui-CzB-gjVP.js | 1 + .../static/assets/user-round-BkRPX9Ri.js | 1 + .../server/static/assets/users-CHP7RP4h.js | 1 - .../server/static/assets/users-D7ZZ6mzL.js | 1 + .../server/static/assets/users-DA_5fDuj.js | 1 + .../server/static/assets/video-1nOl5FpE.js | 1 - .../server/static/assets/video-C1co3G53.js | 1 + .../{watch-DT7UCu76.js => watch-sfB79fT6.js} | 32 +- .../server/static/assets/webrtc-Baz_BzLq.js | 1 - .../server/static/assets/webrtc-CWAdwVhe.js | 1 + internal/server/static/index.html | 12 +- internal/server/token_test.go | 6 +- internal/sfu/cloudflare/provider.go | 2 +- internal/sfu/srs/provider.go | 2 +- web/index.html | 6 +- web/package.json | 2 +- web/src/app/router.tsx | 10 +- web/src/app/routes/index.tsx | 6 +- web/src/app/routes/layout.tsx | 51 +- web/src/app/routes/login.tsx | 224 +++-- web/src/app/routes/manage.tsx | 919 ++++++++++++++++++ web/src/app/routes/rooms.tsx | 295 ++++-- web/src/app/routes/users.tsx | 213 ++-- web/src/lib/api.ts | 103 +- web/src/lib/auth.ts | 12 +- web/src/lib/permissions.ts | 15 + web/src/lib/theme.ts | 2 +- web/src/lib/types.ts | 114 ++- 86 files changed, 5589 insertions(+), 1266 deletions(-) rename api/{live_sfu.proto => sync_live.proto} (95%) delete mode 100644 gen/live_sfu_grpc.pb.go rename gen/{live_sfu.pb.go => sync_live.pb.go} (71%) create mode 100644 gen/sync_live_grpc.pb.go create mode 100644 internal/room/member.go create mode 100644 internal/room/permission.go create mode 100644 internal/room/role.go create mode 100644 internal/room/room.go create mode 100644 internal/room/service.go create mode 100644 internal/room/store.go create mode 100644 internal/server/manage.go rename internal/server/static/assets/{arrow-right-CYxcOjf4.js => arrow-right-BaPiev--.js} (64%) create mode 100644 internal/server/static/assets/cloud-DZf51awA.js create mode 100644 internal/server/static/assets/eye-DaP7VxYZ.js create mode 100644 internal/server/static/assets/hash-ORiZzY9V.js create mode 100644 internal/server/static/assets/index-BTRwyDO1.js delete mode 100644 internal/server/static/assets/index-BjOqv38F.js create mode 100644 internal/server/static/assets/index-OrVzMh6m.css delete mode 100644 internal/server/static/assets/index-iV_C3-n9.css create mode 100644 internal/server/static/assets/log-out-BSFDyM4m.js create mode 100644 internal/server/static/assets/login-B-50707a.js delete mode 100644 internal/server/static/assets/login-DBhoyV9Z.js create mode 100644 internal/server/static/assets/manage-BUOThicK.js create mode 100644 internal/server/static/assets/message-square-1ZGuav88.js rename internal/server/static/assets/{monitor-play-Br3jNYD4.js => monitor-play-Bo4SAxB6.js} (81%) create mode 100644 internal/server/static/assets/publish-D-rS1-js.js delete mode 100644 internal/server/static/assets/publish-ukhzw-Kl.js rename internal/server/static/assets/{radio-tower-CN-xjSIh.js => radio-tower-UMy9OSpZ.js} (78%) delete mode 100644 internal/server/static/assets/rooms-BN1WD1kf.js create mode 100644 internal/server/static/assets/rooms-DcveOed7.js create mode 100644 internal/server/static/assets/routes-BsH3d8_E.js delete mode 100644 internal/server/static/assets/routes-VQMbZFXp.js rename internal/server/static/assets/{server-DmfY9b1N.js => server-eXVtBH0Y.js} (68%) rename internal/server/static/assets/{shield-check-DH7IfQwC.js => shield-check-PAk2q1NC.js} (72%) delete mode 100644 internal/server/static/assets/ui-BEZm0Fjb.js create mode 100644 internal/server/static/assets/ui-CzB-gjVP.js create mode 100644 internal/server/static/assets/user-round-BkRPX9Ri.js delete mode 100644 internal/server/static/assets/users-CHP7RP4h.js create mode 100644 internal/server/static/assets/users-D7ZZ6mzL.js create mode 100644 internal/server/static/assets/users-DA_5fDuj.js delete mode 100644 internal/server/static/assets/video-1nOl5FpE.js create mode 100644 internal/server/static/assets/video-C1co3G53.js rename internal/server/static/assets/{watch-DT7UCu76.js => watch-sfB79fT6.js} (68%) delete mode 100644 internal/server/static/assets/webrtc-Baz_BzLq.js create mode 100644 internal/server/static/assets/webrtc-CWAdwVhe.js create mode 100644 web/src/app/routes/manage.tsx diff --git a/.env.example b/.env.example index f767d88..d49c72f 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,11 @@ HTTP_PORT=8088 GRPC_PORT=9090 +# 鉴权密钥(正式环境必填;缺失将拒绝启动) +JWT_SECRET= +# 可选:单独指定推流 JWT 签名密钥;留空则复用 JWT_SECRET +SFU_TOKEN_SECRET= + # 后端顺序(主 SFU 在前):cloudflare 优先,srs 本地对照 SFU_PROVIDER=cloudflare,srs @@ -18,12 +23,12 @@ CF_APP_SECRET= CF_BASE_URL=https://rtc.live.cloudflare.com/v1 CF_STUN_URL=stun:stun.cloudflare.com:3478 -# 推流 JWT(SRS 入口鉴权,可选;设为 1 后 WHIP 必须带有效 token) -DEMO_TOKEN_SECRET= -SFU_TOKEN_REQUIRED=0 +# 冷启动首个管理员(仅在用户库为空时生效;正式部署建议显式配置) +BOOTSTRAP_ADMIN_USER= +BOOTSTRAP_ADMIN_PASS= -# Turso 嵌入式(默认 DB,房间分发拓扑持久化) +# 嵌入式 libSQL(默认 DB,房间分发拓扑持久化) # 仅支持嵌入 file: 模式,开箱即用,无需远端凭证 -TURSO_DATABASE_URL=file:./data/live-sfu.db?cache=shared&_journal_mode=WAL +TURSO_DATABASE_URL=file:./data/sync-live.db?cache=shared&_journal_mode=WAL # 兼容 DATABASE_URL -# DATABASE_URL=file:./data/live-sfu.db?cache=shared&_journal_mode=WAL \ No newline at end of file +# DATABASE_URL=file:./data/sync-live.db?cache=shared&_journal_mode=WAL \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3823a9d..431afcf 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ data/* # 前端工程 web/node_modules + +server +/tmp/sync-live diff --git a/README.md b/README.md index 490126d..a5c6e7f 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,113 @@ -# 直播 SFU 分流 Demo(Cloudflare Realtime · SRS) +# SyncLive — 同步直播 · SFU 分流服务(对标 SyncTV) -参考 GOSpeak 技术栈的直播 SFU 分流演示。控制面用 **protobuf / gRPC** 定义,媒体面走 -**Cloudflare Realtime**(主 SFU)与 **SRS**(本地对照)的 WebRTC 扇出。 +> **对标 SyncTV**:SyncTV 是 *Sync + TV*(一起看),**SyncLive 是 *Sync + Live*(一起播·一起看直播)**。本项目是 SyncTV 理念在直播场景的姐妹实现:一路推流,多端同步扇出。 + +基于 **Cloudflare Realtime + SRS** 双 SFU 的同步直播分流服务。控制面以 **protobuf / gRPC** 定义,媒体面走 +**Cloudflare Realtime**(主 SFU)与 **SRS**(本地/对照)的 WebRTC 扇出。 - 一路推流 → SFU 扇出 → 多路拉流。 - 同一路发布可同时落到 **Cloudflare Realtime** 与 **SRS** 两条分发链路(分流)。 - 浏览器经服务端反向代理直连 SFU,凭证不出服务端。 -- 默认 DB 使用 Turso 嵌入式 (libSQL file):房间分发拓扑持久化到本地文件。 +- 默认 DB 使用嵌入式 libSQL (file) 持久化房间分发拓扑,零外部依赖。 - 前端为 **SolidJS + TanStack Router/Query + Ark UI + Tailwind v4** 单页应用,构建产物嵌入 Go 二进制。 ## 目录结构 ``` -live-sfu-demo/ - api/live_sfu.proto # protobuf 控制面契约 +sync-live/ + api/sync_live.proto # protobuf 控制面契约 gen/ # buf generate 产出(Go) internal/ - config/ # 运行参数 - db/ # Turso 嵌入式 (libSQL file) 持久化 + config/ # 运行参数 + 正式环境密钥校验 + db/ # 嵌入式 libSQL 持久化 sfu/cloudflare/ # Cloudflare Realtime REST 客户端 + Provider sfu/srs/ # SRS Provider - server/ # gRPC 服务 + JSON 网关 + SRS/CF 媒体反代 + 房间扇出状态 + server/ # gRPC 服务 + JSON 网关 + SRS/CF 媒体反代 + 房间拓扑 static/ # 前端构建产物(由 web/ 构建生成,Go embed) - auth/ # Casbin RBAC + JWT + bcrypt 用户存储 + auth/ # Casbin RBAC + JWT + 用户存储 cmd/server/main.go deploy/ # docker-compose.yml + srs.conf web/ # 前端工程(Solid + TanStack + Ark UI + Tailwind v4) - src/ # 页面 / 组件 / 库 + src/ vite.config.ts # 构建输出到 ../internal/server/static ``` -## 运行(SRS 链路,开箱即跑) +## 运行 + +### 1. 配置环境变量 + +复制示例并填写鉴权密钥(正式环境必填): ```bash -# 1) 起本地 SRS(WHIP/WHEP/HLS 三协议后端) -cd deploy && SRS_CANDIDATE=127.0.0.1 docker compose up -d srs - -# 2) 起 Demo 控制面(默认 Turso 嵌入式,无需额外配置) -cd live-sfu-demo cp .env.example .env -go run ./cmd/server - -# 3) 打开浏览器(发布/订阅需先登录,见下方鉴权说明) -# 首页: http://localhost:8088/ -# 发布: http://localhost:8088/publish?room=demo -# 观看: http://localhost:8088/watch?room=demo +# 必填:JWT_SECRET(与可选 SFU_TOKEN_SECRET) +# 可选:BOOTSTRAP_ADMIN_USER / BOOTSTRAP_ADMIN_PASS 冷启动首个管理员 ``` -两个标签页用同一房间名即可配对。发布页勾选的分发后端会在「分发状态」中实时显示; -SRS 链路同时产出 HLS/FLV,分发状态面板可直接获取 `/live/.m3u8` / `.flv` 链接, -经 Go 网关 (`/live/*`) 反代,无需浏览器直连 8080。 +未配置 `JWT_SECRET` 时服务将拒绝启动,避免以不安全默认密钥上线。 + +### 2. 起本地 SRS(可选,对照后端) + +```bash +cd deploy && SRS_CANDIDATE=127.0.0.1 docker compose up -d srs +``` + +### 3. 起控制面 + +```bash +go run ./cmd/server +# 首页: http://localhost:8088/ +# 发布: http://localhost:8088/publish?room=<房间名> +# 观看: http://localhost:8088/watch?room=<房间名> +# 登录: http://localhost:8088/login +``` + +发布/订阅需先登录;房间名通过 URL `?room=` 指定,不依赖默认占位。 ## 鉴权(Casbin RBAC + JWT) -控制面已启用登录与 RBAC: +控制面启用登录与 RBAC: -- 发布 / 订阅 / 房间管理需要登录;`guest` 仅可读取配置与 SRS 切片流(HLS/FLV)。 -- 默认账号(bcrypt,落盘 `data/users.json`): - - `admin / Admin123!`(admin) - - `publisher / Publisher123!`(publisher) - - `viewer / Viewer123!`(viewer) -- 登录后服务端下发 HttpOnly Cookie,前端同源请求自动携带;在 `/login` 登录。 +- 发布 / 订阅 / 房间管理需登录;`guest` 仅可读取配置与 SRS 切片流(HLS/FLV)。 +- 用户存储于 `data/users.json`(首次启动为空,需通过 `BOOTSTRAP_ADMIN_*` 或注册创建首个管理员)。 +- 登录后服务端下发 HttpOnly Cookie,前端同源请求自动携带。 -## 默认 DB:Turso 嵌入式 +角色:`admin` / `publisher` / `viewer` / `guest`,策略见 `internal/auth/policy.csv`。 -房间“分发目标”拓扑(`stream_targets`)默认持久化到 **Turso 嵌入式** -(`github.com/tursodatabase/go-libsql` 的 SQLite 兼容 file 模式),而非纯内存。 +## 持久化(嵌入式 libSQL) -- **DSN**:`TURSO_DATABASE_URL=file:./data/live-sfu.db?cache=shared&_journal_mode=WAL`(默认) - - 兼容 `DATABASE_URL` 覆盖 - - 仅支持嵌入 file: / :memory:,拒绝 libsql:// 远程(嵌入适配专注单机持久化) - - 自动建表 `(stream_targets, rooms)`,`SetMaxOpenConns(1)` 适配 SQLite 单写模型 - - 重启后自动 `LoadAll` 恢复房间 +房间「分发目标」拓扑(`stream_targets`)默认持久化到嵌入式 libSQL file,而非纯内存: + +- DSN:`file:./data/sync-live.db?cache=shared&_journal_mode=WAL`(默认) +- 兼容 `DATABASE_URL` 覆盖;仅支持 file: / :memory:,拒绝远程 libsql:// +- 自动建表,重启后自动恢复房间拓扑 ```bash -# 默认即嵌入文件 -TURSO_DATABASE_URL=file:./data/live-sfu.db?cache=shared&_journal_mode=WAL - -# 验证持久化:发布后重启,/api/rooms 仍在 curl -b cookie.txt http://localhost:8088/api/rooms | jq -sqlite3 data/live-sfu.db "select room, backend, stream from stream_targets;" ``` -## 运行(Cloudflare Realtime 链路,主 SFU) +## 运行(Cloudflare Realtime,主 SFU) -在 `.env` 填入 Cloudflare Realtime 凭证后,`go run ./cmd/server` 即启用主 SFU: +在 `.env` 填入 Cloudflare Realtime 凭证后启用主 SFU: ``` CF_APP_ID=xxxxxxxxxxxx CF_APP_SECRET=xxxxxxxxxxxx ``` -- 发布:服务端用 `CF_APP_SECRET` 创建 Cloudflare session,浏览器只交换 SDP(`/api/cf/...` 反代注入 Bearer)。 -- 观看:服务端为观众创建 viewer session,并订阅发布者 session 的轨道(`location=remote`)。 +- 发布:服务端用 `CF_APP_SECRET` 创建 Cloudflare session,浏览器只交换 SDP(反代注入 Bearer)。 +- 观看:服务端为观众创建 viewer session,并订阅发布者 session 的轨道。 -Cloudflare 未配置时,UI 会标注「未配置」,SRS 链路不受影响。 +Cloudflare 未配置时 UI 标注「未配置」,SRS 链路不受影响。 ## protobuf / gRPC ```bash -# 修改 api/live_sfu.proto 后重新生成 export PATH="$HOME/go/bin:$PATH" buf generate api ``` -契约(`LiveSFU` 服务):`GetConfig` / `ListRooms` / `Publish` / `Subscribe` / `StopStream` / +契约(`SyncLive` 服务):`GetConfig` / `ListRooms` / `Publish` / `Subscribe` / `StopStream` / `WatchRoom`(服务端流式推送房间分发拓扑)以及 `Login` / `Register` / `GetMe` / `ListUsers` / `UpdateUserRole`。 gRPC 监听 `GRPC_PORT`(默认 9090),浏览器走同端口的 JSON 网关(`protojson`)。 @@ -117,10 +120,6 @@ pnpm dev # http://localhost:5173 热更新 pnpm build # 构建进 ../internal/server/static(被 Go embed) ``` -技术栈:SolidJS(`vite-plugin-solid`)、TanStack Router + Query、Ark UI(无样式原语)、 -Tailwind v4(CSS-first `@theme` 做 design token 体系化驱动)。design token 集中在 -`web/src/index.css` 的 `@theme`,组件语义类在 `web/src/components/ui.tsx` 中以 token 派生。 - ## 分流拓扑 ``` @@ -128,14 +127,13 @@ Tailwind v4(CSS-first `@theme` 做 design token 体系化驱动)。design to └────WHIP────────────▶ SRS SFU ─┬─▶ WHEP viewer(s) (低延时 0.2-0.5s) ├─▶ HLS viewer(s) (全端兼容,/live/*.m3u8) └─▶ FLV viewer(s) - 一路 WHIP 推流,SRS 自动 remux 三协议同出;房间分发目标由 WatchRoom 实时广播 ``` > SRS 推流链路详解见 [`docs/streaming-pipeline.md`](docs/streaming-pipeline.md)。 ## 主播外部推送 API(弹幕广播) -除观众可发送的 `POST /api/room/{room}/chat` 外,额外提供**仅主播可用**的独立推送端点,供 OBS 脚本 / 机器人 / 管理工具以主播身份推送弹幕: +除观众可发送的 `POST /api/room/{room}/chat` 外,额外提供**仅主播可用**的独立推送端点: ``` POST /api/room/{room}/broadcast @@ -146,6 +144,4 @@ Content-Type: application/json ``` - 权限:需 `room:publish`(仅 `publisher` / `admin`),未登录返回 `401`,越权返回 `403`。 -- 推送的弹幕自动标记 `host: true`,经同一 `chatHub` 广播,主播端与观众端实时可见;前端在消息列表与弹幕叠加层以「主播」标识区分。 -- 同样受滑动窗口限流(房间 + 发送者 + IP,3s 内 5 条)保护。 -- 外部工具用 `Authorization: Bearer ` 调用;浏览器端同源请求自动携带登录 Cookie。 +- 推送的弹幕自动标记 `host: true`,经同一 `chatHub` 广播;同样受滑动窗口限流保护。 diff --git a/api/live_sfu.proto b/api/sync_live.proto similarity index 95% rename from api/live_sfu.proto rename to api/sync_live.proto index e744043..7c2aef7 100644 --- a/api/live_sfu.proto +++ b/api/sync_live.proto @@ -1,15 +1,15 @@ syntax = "proto3"; -package gospeak.livedemo.v1; +package synclive.v1; -option go_package = "gospeak-live-sfu-demo/gen"; +option go_package = "sync-live/gen"; -// LiveSFU 是直播 SFU 分流 Demo 的控制面契约(protobuf / gRPC)。 +// SyncLive 是直播 SFU 分流服务的控制面契约(protobuf / gRPC)。 // // 媒体面(WebRTC SDP 交换)不在此契约内:浏览器与 Cloudflare Realtime / SRS // 直连,经本服务反向代理转发(服务端注入 AppSecret / stream token)。 // 本契约只负责房间、后端、发布/订阅拓扑的协调与扇出状态广播。 -service LiveSFU { +service SyncLive { rpc GetConfig(GetConfigRequest) returns (GetConfigResponse); rpc ListRooms(ListRoomsRequest) returns (ListRoomsResponse); rpc Publish(PublishRequest) returns (PublishResponse); diff --git a/cmd/server/main.go b/cmd/server/main.go index 2e46034..d8170b9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -4,12 +4,15 @@ import ( "log" "net/http" - "gospeak-live-sfu-demo/internal/config" - "gospeak-live-sfu-demo/internal/server" + "sync-live/internal/config" + "sync-live/internal/server" ) func main() { cfg := config.Load() + if err := cfg.Validate(); err != nil { + log.Fatalf("config invalid: %v", err) + } srv := server.New(cfg) defer func() { if err := srv.Close(); err != nil { @@ -20,10 +23,10 @@ func main() { log.Printf("[warn] grpc control plane failed to start: %v", err) } addr := ":" + cfg.HTTPPort - log.Printf("live-sfu demo ready: http://localhost:%s (grpc :%s)", cfg.HTTPPort, cfg.GRPCPort) + log.Printf("sync-live ready: http://localhost:%s (grpc :%s)", cfg.HTTPPort, cfg.GRPCPort) log.Printf("backends (primary first): %v", cfg.ProviderList()) log.Printf("cloudflare configured: %v", cfg.CFAppID != "" && cfg.CFAppSecret != "") - log.Printf("turso embedded db: %s", cfg.DatabaseURL) + log.Printf("embedded db: %s", cfg.DatabaseURL) if err := http.ListenAndServe(addr, srv.Handler()); err != nil { log.Fatalf("http server error: %v", err) } diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index ed22981..84e5554 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -2,7 +2,7 @@ services: # SRS:本地对照 SFU(WHIP/WHEP)。媒体走 :8000,信令/HTTP API 走 :1985。 srs: image: ossrs/srs:6 - container_name: live-sfu-srs + container_name: sync-live-srs ports: - "1935:1935" - "1985:1985" diff --git a/design.md b/design.md index 4dae4f0..caa2002 100644 --- a/design.md +++ b/design.md @@ -1,13 +1,39 @@ -# Design Spec — 设计规范 +# 架构设计规范 — SyncLive -## 设计标准 +SyncLive(同步直播,对标 SyncTV)的整体架构与模块划分说明。 -对标 Awwwards 顶级网站水准,达到 Awwwards、FWA、CSS Design Awards 每日最佳网站同等设计品质。 +## 分层架构 -## 创意自由度 +``` +浏览器 (SolidJS SPA) + │ HTTP / SSE / WebRTC (信令经服务端反代,媒体直连 SFU) + ▼ +控制面 (Go) + ├─ gRPC + JSON 网关 # protobuf 定义的统一控制契约 + ├─ 鉴权 (Casbin RBAC + JWT) # 登录、权限校验、用户管理 + ├─ 房间拓扑 (roomHub) # 内存态 + 嵌入式 libSQL 持久化 + └─ 媒体面反代 (httputil) # SRS / Cloudflare 信令与 HLS 反代 + │ + ▼ +媒体面 (SFU Provider 抽象) + ├─ Cloudflare Realtime # 主 SFU(托管,低延迟扇出) + └─ SRS # 本地/对照 SFU(WHIP/WHEP + HLS/FLV 同出) +``` -将浏览器视作交互式艺术画布,跳出传统布局框架,追求先锋视觉风格、实验性排版、流畅物理动效、极具冲击力的文字版式。 +## 模块职责 -## 沉浸式体验 +- `api/`:protobuf 控制面契约(`SyncLive` 服务),单一事实来源,经 `buf generate` 生成 Go 代码到 `gen/`。 +- `internal/config/`:运行参数与环境变量加载,含正式环境密钥校验(`Validate`)。 +- `internal/db/`:嵌入式 libSQL 持久化(房间分发拓扑),仅 file:/:memory: 模式。 +- `internal/auth/`:Casbin RBAC 模型与策略、JWT 签发校验、用户存储。 +- `internal/sfu/`:SFU 后端 Provider 抽象(Cloudflare / SRS),统一 `BackendInfo` 与能力查询。 +- `internal/server/`:HTTP/gRPC 服务装配、JSON 网关、房间与聊天 SSE、媒体反代、静态 UI 嵌入。 +- `cmd/server/`:进程入口,装配配置、服务与优雅关闭。 +- `web/`:前端单页应用(SolidJS + TanStack + Ark UI + Tailwind v4),构建产物由 Go embed 内联。 -融合代码、高级渲染逻辑,打造统一完整的精品页面,做出突破常规 UI 认知、令人惊艳的数字交互体验。 +## 设计约束 + +- 控制面与媒体面分离:浏览器只与服务端交换 SDP/信令,SFU 凭证不出服务端。 +- 多 SFU 后端可同时承载同一路发布(分流),拓扑由 `WatchRoom` / SSE 实时广播。 +- 持久化默认嵌入式、零外部依赖;远程 libSQL 仅作可选项。 +- 正式环境密钥必须显式配置,禁止内置弱密码与不安全默认密钥。 diff --git a/docs/streaming-pipeline.md b/docs/streaming-pipeline.md index 67a4a1b..818e82d 100644 --- a/docs/streaming-pipeline.md +++ b/docs/streaming-pipeline.md @@ -1,4 +1,4 @@ -# 推流链路文档 · PC → WHIP → SRS → HLS / WHEP / FLV +# SyncLive 推流链路文档 · PC → WHIP → SRS → HLS / WHEP / FLV > 默认推流链路已升级为 **一路 WHIP 推流,SRS 三协议同出**。本文档沉淀该链路的拓扑、配置与验证方法,便于后续维护与排查。 @@ -107,7 +107,7 @@ services: | `SRS_APP` | `live` | 推流 app,决定 URL 路径 `/live/` | | `SRS_CANDIDATE` | `127.0.0.1` | ICE candidate,容器部署填宿主机/公网 IP | | `SFU_PROVIDER` | `cloudflare,srs` | 后端顺序,(`GetConfig` 返回) | -| `TURSO_DATABASE_URL` | `file:./data/live-sfu.db?cache=shared&_journal_mode=WAL` | 房间拓扑持久化(本次未改) | +| `TURSO_DATABASE_URL` | `file:./data/sync-live.db?cache=shared&_journal_mode=WAL` | 房间拓扑持久化(本次未改) | ### 2.4 反向代理 · `internal/server/proxy.go` + `internal/server/server.go` @@ -174,15 +174,15 @@ Body: offer.sdp → 200 answer.sdp → pc.setRemoteDescription(answer) ```bash # 1) 起 SRS(含 HLS) -cd deploy && SRS_CANDIDATE=127.0.0.1 docker compose up -d srs && docker logs -f live-sfu-srs +cd deploy && SRS_CANDIDATE=127.0.0.1 docker compose up -d srs && docker logs -f sync-live-srs # 2) 起控制面 cd .. && go run ./cmd/server -# 日志:live-sfu demo ready: http://localhost:8088 +# 日志:sync-live ready: http://localhost:8088 # 3) 浏览器 -# 发布: http://localhost:8088/publish?room=demo → 勾选 SRS → 开始推流 -# 观看 WHEP: http://localhost:8088/watch?room=demo → 选 SRS WebRTC → 开始观看(<1s) +# 发布: http://localhost:8088/publish?room=<房间名> → 勾选 SRS → 开始推流 +# 观看 WHEP: http://localhost:8088/watch?room=<房间名> → 选 SRS WebRTC → 开始观看(<1s) # 观看 HLS : 同页切 SRS HLS → 停止后重新开始观看(约 5–10s 后出画面) # 裸 HLS: http://localhost:8088/live/live-demo.m3u8 (VLC / ffplay / curl 均可) curl -i http://localhost:8088/live/live-demo.m3u8 @@ -192,7 +192,7 @@ ffplay http://localhost:8088/live/live-demo.m3u8 排障: -* `m3u8 404`:推流后等待 10–20s 再试;检查 `docker exec live-sfu-srs ls /usr/local/srs/objs/nginx/html/live/` 是否有切片。 +* `m3u8 404`:推流后等待 10–20s 再试;检查 `docker exec sync-live-srs ls /usr/local/srs/objs/nginx/html/live/` 是否有切片。 * `candidate` 不通:容器内 `ip` 与浏览器不在同一网段时,`SRS_CANDIDATE` 改为宿主机公网/局域网 IP 后 `docker compose up -d --force-recreate`。 * HLS 跨域:已由 `srsHlsProxyHandler` 统一加 CORS,若直连 8080 需自行在 `srs.conf` 加 `crossdomain`(http_server 无此指令,建议走网关)。 @@ -206,7 +206,7 @@ ffplay http://localhost:8088/live/live-demo.m3u8 | 单向大并发/回看友好 | HLS | 全端兼容,CDN 友好,成本最低 | | 演示/对比 | WHEP + HLS 双出(默认) | 一路推流兼顾低延时与兼容性 | -> 本 Demos 默认即为双出:无需额外推流或配置,开箱获得两条观看链路。 +> 默认即为双出:无需额外推流或配置,开箱获得两条观看链路。 --- @@ -215,7 +215,7 @@ ffplay http://localhost:8088/live/live-demo.m3u8 * 服务端配置:`deploy/srs.conf`、`deploy/docker-compose.yml`、`internal/config/config.go`、`.env.example` * 网关:`internal/server/proxy.go`(`srsProxyHandler` / `srsHlsProxyHandler`)、`internal/server/server.go`(`Handler` 路由)、`internal/server/gateway.go`、`internal/server/service.go` * 前端:`internal/server/static/app.js`(`publishSRS`/`watchSRS`/`watchHLS`)、`internal/server/static/publish.html`、`internal/server/static/watch.html` -* 协议:`api/live_sfu.proto`(`BackendKind` / `StreamTarget`) +* 协议:`api/sync_live.proto`(`BackendKind` / `StreamTarget`) --- diff --git a/gen/live_sfu_grpc.pb.go b/gen/live_sfu_grpc.pb.go deleted file mode 100644 index 881432e..0000000 --- a/gen/live_sfu_grpc.pb.go +++ /dev/null @@ -1,517 +0,0 @@ -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.5.1 -// - protoc (unknown) -// source: live_sfu.proto - -package gen - -import ( - context "context" - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" -) - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.64.0 or later. -const _ = grpc.SupportPackageIsVersion9 - -const ( - LiveSFU_GetConfig_FullMethodName = "/gospeak.livedemo.v1.LiveSFU/GetConfig" - LiveSFU_ListRooms_FullMethodName = "/gospeak.livedemo.v1.LiveSFU/ListRooms" - LiveSFU_Publish_FullMethodName = "/gospeak.livedemo.v1.LiveSFU/Publish" - LiveSFU_Subscribe_FullMethodName = "/gospeak.livedemo.v1.LiveSFU/Subscribe" - LiveSFU_StopStream_FullMethodName = "/gospeak.livedemo.v1.LiveSFU/StopStream" - LiveSFU_WatchRoom_FullMethodName = "/gospeak.livedemo.v1.LiveSFU/WatchRoom" - LiveSFU_Login_FullMethodName = "/gospeak.livedemo.v1.LiveSFU/Login" - LiveSFU_Register_FullMethodName = "/gospeak.livedemo.v1.LiveSFU/Register" - LiveSFU_GetMe_FullMethodName = "/gospeak.livedemo.v1.LiveSFU/GetMe" - LiveSFU_ListUsers_FullMethodName = "/gospeak.livedemo.v1.LiveSFU/ListUsers" - LiveSFU_UpdateUserRole_FullMethodName = "/gospeak.livedemo.v1.LiveSFU/UpdateUserRole" -) - -// LiveSFUClient is the client API for LiveSFU service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -// -// LiveSFU 是直播 SFU 分流 Demo 的控制面契约(protobuf / gRPC)。 -// -// 媒体面(WebRTC SDP 交换)不在此契约内:浏览器与 Cloudflare Realtime / SRS -// 直连,经本服务反向代理转发(服务端注入 AppSecret / stream token)。 -// 本契约只负责房间、后端、发布/订阅拓扑的协调与扇出状态广播。 -type LiveSFUClient interface { - GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) - ListRooms(ctx context.Context, in *ListRoomsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error) - Publish(ctx context.Context, in *PublishRequest, opts ...grpc.CallOption) (*PublishResponse, error) - Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (*SubscribeResponse, error) - StopStream(ctx context.Context, in *StopStreamRequest, opts ...grpc.CallOption) (*StopStreamResponse, error) - WatchRoom(ctx context.Context, in *WatchRoomRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[RoomEvent], error) - // ---- Auth & Casbin ---- - Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) - Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) - GetMe(ctx context.Context, in *GetMeRequest, opts ...grpc.CallOption) (*GetMeResponse, error) - ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) - UpdateUserRole(ctx context.Context, in *UpdateUserRoleRequest, opts ...grpc.CallOption) (*UpdateUserRoleResponse, error) -} - -type liveSFUClient struct { - cc grpc.ClientConnInterface -} - -func NewLiveSFUClient(cc grpc.ClientConnInterface) LiveSFUClient { - return &liveSFUClient{cc} -} - -func (c *liveSFUClient) GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(GetConfigResponse) - err := c.cc.Invoke(ctx, LiveSFU_GetConfig_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *liveSFUClient) ListRooms(ctx context.Context, in *ListRoomsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(ListRoomsResponse) - err := c.cc.Invoke(ctx, LiveSFU_ListRooms_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *liveSFUClient) Publish(ctx context.Context, in *PublishRequest, opts ...grpc.CallOption) (*PublishResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(PublishResponse) - err := c.cc.Invoke(ctx, LiveSFU_Publish_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *liveSFUClient) Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (*SubscribeResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(SubscribeResponse) - err := c.cc.Invoke(ctx, LiveSFU_Subscribe_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *liveSFUClient) StopStream(ctx context.Context, in *StopStreamRequest, opts ...grpc.CallOption) (*StopStreamResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(StopStreamResponse) - err := c.cc.Invoke(ctx, LiveSFU_StopStream_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *liveSFUClient) WatchRoom(ctx context.Context, in *WatchRoomRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[RoomEvent], error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &LiveSFU_ServiceDesc.Streams[0], LiveSFU_WatchRoom_FullMethodName, cOpts...) - if err != nil { - return nil, err - } - x := &grpc.GenericClientStream[WatchRoomRequest, RoomEvent]{ClientStream: stream} - if err := x.ClientStream.SendMsg(in); err != nil { - return nil, err - } - if err := x.ClientStream.CloseSend(); err != nil { - return nil, err - } - return x, nil -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type LiveSFU_WatchRoomClient = grpc.ServerStreamingClient[RoomEvent] - -func (c *liveSFUClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(LoginResponse) - err := c.cc.Invoke(ctx, LiveSFU_Login_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *liveSFUClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(RegisterResponse) - err := c.cc.Invoke(ctx, LiveSFU_Register_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *liveSFUClient) GetMe(ctx context.Context, in *GetMeRequest, opts ...grpc.CallOption) (*GetMeResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(GetMeResponse) - err := c.cc.Invoke(ctx, LiveSFU_GetMe_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *liveSFUClient) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(ListUsersResponse) - err := c.cc.Invoke(ctx, LiveSFU_ListUsers_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *liveSFUClient) UpdateUserRole(ctx context.Context, in *UpdateUserRoleRequest, opts ...grpc.CallOption) (*UpdateUserRoleResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(UpdateUserRoleResponse) - err := c.cc.Invoke(ctx, LiveSFU_UpdateUserRole_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -// LiveSFUServer is the server API for LiveSFU service. -// All implementations should embed UnimplementedLiveSFUServer -// for forward compatibility. -// -// LiveSFU 是直播 SFU 分流 Demo 的控制面契约(protobuf / gRPC)。 -// -// 媒体面(WebRTC SDP 交换)不在此契约内:浏览器与 Cloudflare Realtime / SRS -// 直连,经本服务反向代理转发(服务端注入 AppSecret / stream token)。 -// 本契约只负责房间、后端、发布/订阅拓扑的协调与扇出状态广播。 -type LiveSFUServer interface { - GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error) - ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) - Publish(context.Context, *PublishRequest) (*PublishResponse, error) - Subscribe(context.Context, *SubscribeRequest) (*SubscribeResponse, error) - StopStream(context.Context, *StopStreamRequest) (*StopStreamResponse, error) - WatchRoom(*WatchRoomRequest, grpc.ServerStreamingServer[RoomEvent]) error - // ---- Auth & Casbin ---- - Login(context.Context, *LoginRequest) (*LoginResponse, error) - Register(context.Context, *RegisterRequest) (*RegisterResponse, error) - GetMe(context.Context, *GetMeRequest) (*GetMeResponse, error) - ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) - UpdateUserRole(context.Context, *UpdateUserRoleRequest) (*UpdateUserRoleResponse, error) -} - -// UnimplementedLiveSFUServer should be embedded to have -// forward compatible implementations. -// -// NOTE: this should be embedded by value instead of pointer to avoid a nil -// pointer dereference when methods are called. -type UnimplementedLiveSFUServer struct{} - -func (UnimplementedLiveSFUServer) GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetConfig not implemented") -} -func (UnimplementedLiveSFUServer) ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListRooms not implemented") -} -func (UnimplementedLiveSFUServer) Publish(context.Context, *PublishRequest) (*PublishResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Publish not implemented") -} -func (UnimplementedLiveSFUServer) Subscribe(context.Context, *SubscribeRequest) (*SubscribeResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Subscribe not implemented") -} -func (UnimplementedLiveSFUServer) StopStream(context.Context, *StopStreamRequest) (*StopStreamResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method StopStream not implemented") -} -func (UnimplementedLiveSFUServer) WatchRoom(*WatchRoomRequest, grpc.ServerStreamingServer[RoomEvent]) error { - return status.Errorf(codes.Unimplemented, "method WatchRoom not implemented") -} -func (UnimplementedLiveSFUServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") -} -func (UnimplementedLiveSFUServer) Register(context.Context, *RegisterRequest) (*RegisterResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Register not implemented") -} -func (UnimplementedLiveSFUServer) GetMe(context.Context, *GetMeRequest) (*GetMeResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetMe not implemented") -} -func (UnimplementedLiveSFUServer) ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListUsers not implemented") -} -func (UnimplementedLiveSFUServer) UpdateUserRole(context.Context, *UpdateUserRoleRequest) (*UpdateUserRoleResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateUserRole not implemented") -} -func (UnimplementedLiveSFUServer) testEmbeddedByValue() {} - -// UnsafeLiveSFUServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to LiveSFUServer will -// result in compilation errors. -type UnsafeLiveSFUServer interface { - mustEmbedUnimplementedLiveSFUServer() -} - -func RegisterLiveSFUServer(s grpc.ServiceRegistrar, srv LiveSFUServer) { - // If the following call pancis, it indicates UnimplementedLiveSFUServer was - // embedded by pointer and is nil. This will cause panics if an - // unimplemented method is ever invoked, so we test this at initialization - // time to prevent it from happening at runtime later due to I/O. - if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { - t.testEmbeddedByValue() - } - s.RegisterService(&LiveSFU_ServiceDesc, srv) -} - -func _LiveSFU_GetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetConfigRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(LiveSFUServer).GetConfig(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: LiveSFU_GetConfig_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(LiveSFUServer).GetConfig(ctx, req.(*GetConfigRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _LiveSFU_ListRooms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ListRoomsRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(LiveSFUServer).ListRooms(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: LiveSFU_ListRooms_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(LiveSFUServer).ListRooms(ctx, req.(*ListRoomsRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _LiveSFU_Publish_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(PublishRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(LiveSFUServer).Publish(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: LiveSFU_Publish_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(LiveSFUServer).Publish(ctx, req.(*PublishRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _LiveSFU_Subscribe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(SubscribeRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(LiveSFUServer).Subscribe(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: LiveSFU_Subscribe_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(LiveSFUServer).Subscribe(ctx, req.(*SubscribeRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _LiveSFU_StopStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(StopStreamRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(LiveSFUServer).StopStream(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: LiveSFU_StopStream_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(LiveSFUServer).StopStream(ctx, req.(*StopStreamRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _LiveSFU_WatchRoom_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(WatchRoomRequest) - if err := stream.RecvMsg(m); err != nil { - return err - } - return srv.(LiveSFUServer).WatchRoom(m, &grpc.GenericServerStream[WatchRoomRequest, RoomEvent]{ServerStream: stream}) -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type LiveSFU_WatchRoomServer = grpc.ServerStreamingServer[RoomEvent] - -func _LiveSFU_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(LoginRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(LiveSFUServer).Login(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: LiveSFU_Login_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(LiveSFUServer).Login(ctx, req.(*LoginRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _LiveSFU_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(RegisterRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(LiveSFUServer).Register(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: LiveSFU_Register_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(LiveSFUServer).Register(ctx, req.(*RegisterRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _LiveSFU_GetMe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetMeRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(LiveSFUServer).GetMe(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: LiveSFU_GetMe_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(LiveSFUServer).GetMe(ctx, req.(*GetMeRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _LiveSFU_ListUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ListUsersRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(LiveSFUServer).ListUsers(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: LiveSFU_ListUsers_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(LiveSFUServer).ListUsers(ctx, req.(*ListUsersRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _LiveSFU_UpdateUserRole_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(UpdateUserRoleRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(LiveSFUServer).UpdateUserRole(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: LiveSFU_UpdateUserRole_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(LiveSFUServer).UpdateUserRole(ctx, req.(*UpdateUserRoleRequest)) - } - return interceptor(ctx, in, info, handler) -} - -// LiveSFU_ServiceDesc is the grpc.ServiceDesc for LiveSFU service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var LiveSFU_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "gospeak.livedemo.v1.LiveSFU", - HandlerType: (*LiveSFUServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "GetConfig", - Handler: _LiveSFU_GetConfig_Handler, - }, - { - MethodName: "ListRooms", - Handler: _LiveSFU_ListRooms_Handler, - }, - { - MethodName: "Publish", - Handler: _LiveSFU_Publish_Handler, - }, - { - MethodName: "Subscribe", - Handler: _LiveSFU_Subscribe_Handler, - }, - { - MethodName: "StopStream", - Handler: _LiveSFU_StopStream_Handler, - }, - { - MethodName: "Login", - Handler: _LiveSFU_Login_Handler, - }, - { - MethodName: "Register", - Handler: _LiveSFU_Register_Handler, - }, - { - MethodName: "GetMe", - Handler: _LiveSFU_GetMe_Handler, - }, - { - MethodName: "ListUsers", - Handler: _LiveSFU_ListUsers_Handler, - }, - { - MethodName: "UpdateUserRole", - Handler: _LiveSFU_UpdateUserRole_Handler, - }, - }, - Streams: []grpc.StreamDesc{ - { - StreamName: "WatchRoom", - Handler: _LiveSFU_WatchRoom_Handler, - ServerStreams: true, - }, - }, - Metadata: "live_sfu.proto", -} diff --git a/gen/live_sfu.pb.go b/gen/sync_live.pb.go similarity index 71% rename from gen/live_sfu.pb.go rename to gen/sync_live.pb.go index e72ded7..628047b 100644 --- a/gen/live_sfu.pb.go +++ b/gen/sync_live.pb.go @@ -1,8 +1,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: live_sfu.proto +// protoc-gen-go v1.36.12 +// protoc v7.35.1 +// source: sync_live.proto package gen @@ -54,11 +54,11 @@ func (x BackendKind) String() string { } func (BackendKind) Descriptor() protoreflect.EnumDescriptor { - return file_live_sfu_proto_enumTypes[0].Descriptor() + return file_sync_live_proto_enumTypes[0].Descriptor() } func (BackendKind) Type() protoreflect.EnumType { - return &file_live_sfu_proto_enumTypes[0] + return &file_sync_live_proto_enumTypes[0] } func (x BackendKind) Number() protoreflect.EnumNumber { @@ -67,7 +67,7 @@ func (x BackendKind) Number() protoreflect.EnumNumber { // Deprecated: Use BackendKind.Descriptor instead. func (BackendKind) EnumDescriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{0} + return file_sync_live_proto_rawDescGZIP(), []int{0} } type IceServer struct { @@ -81,7 +81,7 @@ type IceServer struct { func (x *IceServer) Reset() { *x = IceServer{} - mi := &file_live_sfu_proto_msgTypes[0] + mi := &file_sync_live_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -93,7 +93,7 @@ func (x *IceServer) String() string { func (*IceServer) ProtoMessage() {} func (x *IceServer) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[0] + mi := &file_sync_live_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -106,7 +106,7 @@ func (x *IceServer) ProtoReflect() protoreflect.Message { // Deprecated: Use IceServer.ProtoReflect.Descriptor instead. func (*IceServer) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{0} + return file_sync_live_proto_rawDescGZIP(), []int{0} } func (x *IceServer) GetUrls() []string { @@ -138,7 +138,7 @@ type GetConfigRequest struct { func (x *GetConfigRequest) Reset() { *x = GetConfigRequest{} - mi := &file_live_sfu_proto_msgTypes[1] + mi := &file_sync_live_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -150,7 +150,7 @@ func (x *GetConfigRequest) String() string { func (*GetConfigRequest) ProtoMessage() {} func (x *GetConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[1] + mi := &file_sync_live_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -163,12 +163,12 @@ func (x *GetConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetConfigRequest.ProtoReflect.Descriptor instead. func (*GetConfigRequest) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{1} + return file_sync_live_proto_rawDescGZIP(), []int{1} } type BackendInfo struct { state protoimpl.MessageState `protogen:"open.v1"` - Kind BackendKind `protobuf:"varint,1,opt,name=kind,proto3,enum=gospeak.livedemo.v1.BackendKind" json:"kind,omitempty"` + Kind BackendKind `protobuf:"varint,1,opt,name=kind,proto3,enum=synclive.v1.BackendKind" json:"kind,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` Configured bool `protobuf:"varint,3,opt,name=configured,proto3" json:"configured,omitempty"` Primary bool `protobuf:"varint,4,opt,name=primary,proto3" json:"primary,omitempty"` @@ -178,7 +178,7 @@ type BackendInfo struct { func (x *BackendInfo) Reset() { *x = BackendInfo{} - mi := &file_live_sfu_proto_msgTypes[2] + mi := &file_sync_live_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -190,7 +190,7 @@ func (x *BackendInfo) String() string { func (*BackendInfo) ProtoMessage() {} func (x *BackendInfo) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[2] + mi := &file_sync_live_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -203,7 +203,7 @@ func (x *BackendInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use BackendInfo.ProtoReflect.Descriptor instead. func (*BackendInfo) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{2} + return file_sync_live_proto_rawDescGZIP(), []int{2} } func (x *BackendInfo) GetKind() BackendKind { @@ -245,7 +245,7 @@ type GetConfigResponse struct { func (x *GetConfigResponse) Reset() { *x = GetConfigResponse{} - mi := &file_live_sfu_proto_msgTypes[3] + mi := &file_sync_live_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -257,7 +257,7 @@ func (x *GetConfigResponse) String() string { func (*GetConfigResponse) ProtoMessage() {} func (x *GetConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[3] + mi := &file_sync_live_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -270,7 +270,7 @@ func (x *GetConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetConfigResponse.ProtoReflect.Descriptor instead. func (*GetConfigResponse) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{3} + return file_sync_live_proto_rawDescGZIP(), []int{3} } func (x *GetConfigResponse) GetBackends() []*BackendInfo { @@ -296,7 +296,7 @@ func (x *GetConfigResponse) GetTokenRequired() bool { type StreamTarget struct { state protoimpl.MessageState `protogen:"open.v1"` - Backend BackendKind `protobuf:"varint,1,opt,name=backend,proto3,enum=gospeak.livedemo.v1.BackendKind" json:"backend,omitempty"` + Backend BackendKind `protobuf:"varint,1,opt,name=backend,proto3,enum=synclive.v1.BackendKind" json:"backend,omitempty"` SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` Stream string `protobuf:"bytes,3,opt,name=stream,proto3" json:"stream,omitempty"` PublishToken string `protobuf:"bytes,4,opt,name=publish_token,json=publishToken,proto3" json:"publish_token,omitempty"` @@ -308,7 +308,7 @@ type StreamTarget struct { func (x *StreamTarget) Reset() { *x = StreamTarget{} - mi := &file_live_sfu_proto_msgTypes[4] + mi := &file_sync_live_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -320,7 +320,7 @@ func (x *StreamTarget) String() string { func (*StreamTarget) ProtoMessage() {} func (x *StreamTarget) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[4] + mi := &file_sync_live_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -333,7 +333,7 @@ func (x *StreamTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamTarget.ProtoReflect.Descriptor instead. func (*StreamTarget) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{4} + return file_sync_live_proto_rawDescGZIP(), []int{4} } func (x *StreamTarget) GetBackend() BackendKind { @@ -388,7 +388,7 @@ type Room struct { func (x *Room) Reset() { *x = Room{} - mi := &file_live_sfu_proto_msgTypes[5] + mi := &file_sync_live_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -400,7 +400,7 @@ func (x *Room) String() string { func (*Room) ProtoMessage() {} func (x *Room) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[5] + mi := &file_sync_live_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -413,7 +413,7 @@ func (x *Room) ProtoReflect() protoreflect.Message { // Deprecated: Use Room.ProtoReflect.Descriptor instead. func (*Room) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{5} + return file_sync_live_proto_rawDescGZIP(), []int{5} } func (x *Room) GetName() string { @@ -438,7 +438,7 @@ type ListRoomsRequest struct { func (x *ListRoomsRequest) Reset() { *x = ListRoomsRequest{} - mi := &file_live_sfu_proto_msgTypes[6] + mi := &file_sync_live_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -450,7 +450,7 @@ func (x *ListRoomsRequest) String() string { func (*ListRoomsRequest) ProtoMessage() {} func (x *ListRoomsRequest) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[6] + mi := &file_sync_live_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -463,7 +463,7 @@ func (x *ListRoomsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomsRequest.ProtoReflect.Descriptor instead. func (*ListRoomsRequest) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{6} + return file_sync_live_proto_rawDescGZIP(), []int{6} } type ListRoomsResponse struct { @@ -475,7 +475,7 @@ type ListRoomsResponse struct { func (x *ListRoomsResponse) Reset() { *x = ListRoomsResponse{} - mi := &file_live_sfu_proto_msgTypes[7] + mi := &file_sync_live_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -487,7 +487,7 @@ func (x *ListRoomsResponse) String() string { func (*ListRoomsResponse) ProtoMessage() {} func (x *ListRoomsResponse) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[7] + mi := &file_sync_live_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -500,7 +500,7 @@ func (x *ListRoomsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomsResponse.ProtoReflect.Descriptor instead. func (*ListRoomsResponse) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{7} + return file_sync_live_proto_rawDescGZIP(), []int{7} } func (x *ListRoomsResponse) GetRooms() []*Room { @@ -513,7 +513,7 @@ func (x *ListRoomsResponse) GetRooms() []*Room { type PublishRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Room string `protobuf:"bytes,1,opt,name=room,proto3" json:"room,omitempty"` - Backend BackendKind `protobuf:"varint,2,opt,name=backend,proto3,enum=gospeak.livedemo.v1.BackendKind" json:"backend,omitempty"` + Backend BackendKind `protobuf:"varint,2,opt,name=backend,proto3,enum=synclive.v1.BackendKind" json:"backend,omitempty"` Identity string `protobuf:"bytes,3,opt,name=identity,proto3" json:"identity,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -521,7 +521,7 @@ type PublishRequest struct { func (x *PublishRequest) Reset() { *x = PublishRequest{} - mi := &file_live_sfu_proto_msgTypes[8] + mi := &file_sync_live_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -533,7 +533,7 @@ func (x *PublishRequest) String() string { func (*PublishRequest) ProtoMessage() {} func (x *PublishRequest) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[8] + mi := &file_sync_live_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -546,7 +546,7 @@ func (x *PublishRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PublishRequest.ProtoReflect.Descriptor instead. func (*PublishRequest) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{8} + return file_sync_live_proto_rawDescGZIP(), []int{8} } func (x *PublishRequest) GetRoom() string { @@ -583,7 +583,7 @@ type PublishResponse struct { func (x *PublishResponse) Reset() { *x = PublishResponse{} - mi := &file_live_sfu_proto_msgTypes[9] + mi := &file_sync_live_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -595,7 +595,7 @@ func (x *PublishResponse) String() string { func (*PublishResponse) ProtoMessage() {} func (x *PublishResponse) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[9] + mi := &file_sync_live_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -608,7 +608,7 @@ func (x *PublishResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PublishResponse.ProtoReflect.Descriptor instead. func (*PublishResponse) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{9} + return file_sync_live_proto_rawDescGZIP(), []int{9} } func (x *PublishResponse) GetSessionId() string { @@ -649,7 +649,7 @@ func (x *PublishResponse) GetTarget() *StreamTarget { type SubscribeRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Room string `protobuf:"bytes,1,opt,name=room,proto3" json:"room,omitempty"` - Backend BackendKind `protobuf:"varint,2,opt,name=backend,proto3,enum=gospeak.livedemo.v1.BackendKind" json:"backend,omitempty"` + Backend BackendKind `protobuf:"varint,2,opt,name=backend,proto3,enum=synclive.v1.BackendKind" json:"backend,omitempty"` Identity string `protobuf:"bytes,3,opt,name=identity,proto3" json:"identity,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -657,7 +657,7 @@ type SubscribeRequest struct { func (x *SubscribeRequest) Reset() { *x = SubscribeRequest{} - mi := &file_live_sfu_proto_msgTypes[10] + mi := &file_sync_live_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -669,7 +669,7 @@ func (x *SubscribeRequest) String() string { func (*SubscribeRequest) ProtoMessage() {} func (x *SubscribeRequest) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[10] + mi := &file_sync_live_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -682,7 +682,7 @@ func (x *SubscribeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubscribeRequest.ProtoReflect.Descriptor instead. func (*SubscribeRequest) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{10} + return file_sync_live_proto_rawDescGZIP(), []int{10} } func (x *SubscribeRequest) GetRoom() string { @@ -718,7 +718,7 @@ type SubscribeResponse struct { func (x *SubscribeResponse) Reset() { *x = SubscribeResponse{} - mi := &file_live_sfu_proto_msgTypes[11] + mi := &file_sync_live_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -730,7 +730,7 @@ func (x *SubscribeResponse) String() string { func (*SubscribeResponse) ProtoMessage() {} func (x *SubscribeResponse) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[11] + mi := &file_sync_live_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -743,7 +743,7 @@ func (x *SubscribeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SubscribeResponse.ProtoReflect.Descriptor instead. func (*SubscribeResponse) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{11} + return file_sync_live_proto_rawDescGZIP(), []int{11} } func (x *SubscribeResponse) GetSessionId() string { @@ -777,14 +777,14 @@ func (x *SubscribeResponse) GetIceServers() []*IceServer { type StopStreamRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Room string `protobuf:"bytes,1,opt,name=room,proto3" json:"room,omitempty"` - Backend BackendKind `protobuf:"varint,2,opt,name=backend,proto3,enum=gospeak.livedemo.v1.BackendKind" json:"backend,omitempty"` + Backend BackendKind `protobuf:"varint,2,opt,name=backend,proto3,enum=synclive.v1.BackendKind" json:"backend,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *StopStreamRequest) Reset() { *x = StopStreamRequest{} - mi := &file_live_sfu_proto_msgTypes[12] + mi := &file_sync_live_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -796,7 +796,7 @@ func (x *StopStreamRequest) String() string { func (*StopStreamRequest) ProtoMessage() {} func (x *StopStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[12] + mi := &file_sync_live_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -809,7 +809,7 @@ func (x *StopStreamRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StopStreamRequest.ProtoReflect.Descriptor instead. func (*StopStreamRequest) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{12} + return file_sync_live_proto_rawDescGZIP(), []int{12} } func (x *StopStreamRequest) GetRoom() string { @@ -835,7 +835,7 @@ type StopStreamResponse struct { func (x *StopStreamResponse) Reset() { *x = StopStreamResponse{} - mi := &file_live_sfu_proto_msgTypes[13] + mi := &file_sync_live_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -847,7 +847,7 @@ func (x *StopStreamResponse) String() string { func (*StopStreamResponse) ProtoMessage() {} func (x *StopStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[13] + mi := &file_sync_live_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -860,7 +860,7 @@ func (x *StopStreamResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StopStreamResponse.ProtoReflect.Descriptor instead. func (*StopStreamResponse) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{13} + return file_sync_live_proto_rawDescGZIP(), []int{13} } func (x *StopStreamResponse) GetOk() bool { @@ -879,7 +879,7 @@ type WatchRoomRequest struct { func (x *WatchRoomRequest) Reset() { *x = WatchRoomRequest{} - mi := &file_live_sfu_proto_msgTypes[14] + mi := &file_sync_live_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -891,7 +891,7 @@ func (x *WatchRoomRequest) String() string { func (*WatchRoomRequest) ProtoMessage() {} func (x *WatchRoomRequest) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[14] + mi := &file_sync_live_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -904,7 +904,7 @@ func (x *WatchRoomRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchRoomRequest.ProtoReflect.Descriptor instead. func (*WatchRoomRequest) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{14} + return file_sync_live_proto_rawDescGZIP(), []int{14} } func (x *WatchRoomRequest) GetRoom() string { @@ -924,7 +924,7 @@ type RoomEvent struct { func (x *RoomEvent) Reset() { *x = RoomEvent{} - mi := &file_live_sfu_proto_msgTypes[15] + mi := &file_sync_live_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -936,7 +936,7 @@ func (x *RoomEvent) String() string { func (*RoomEvent) ProtoMessage() {} func (x *RoomEvent) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[15] + mi := &file_sync_live_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -949,7 +949,7 @@ func (x *RoomEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomEvent.ProtoReflect.Descriptor instead. func (*RoomEvent) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{15} + return file_sync_live_proto_rawDescGZIP(), []int{15} } func (x *RoomEvent) GetRoom() string { @@ -978,7 +978,7 @@ type UserInfo struct { func (x *UserInfo) Reset() { *x = UserInfo{} - mi := &file_live_sfu_proto_msgTypes[16] + mi := &file_sync_live_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -990,7 +990,7 @@ func (x *UserInfo) String() string { func (*UserInfo) ProtoMessage() {} func (x *UserInfo) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[16] + mi := &file_sync_live_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1003,7 +1003,7 @@ func (x *UserInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use UserInfo.ProtoReflect.Descriptor instead. func (*UserInfo) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{16} + return file_sync_live_proto_rawDescGZIP(), []int{16} } func (x *UserInfo) GetUsername() string { @@ -1037,7 +1037,7 @@ type LoginRequest struct { func (x *LoginRequest) Reset() { *x = LoginRequest{} - mi := &file_live_sfu_proto_msgTypes[17] + mi := &file_sync_live_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1049,7 +1049,7 @@ func (x *LoginRequest) String() string { func (*LoginRequest) ProtoMessage() {} func (x *LoginRequest) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[17] + mi := &file_sync_live_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1062,7 +1062,7 @@ func (x *LoginRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LoginRequest.ProtoReflect.Descriptor instead. func (*LoginRequest) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{17} + return file_sync_live_proto_rawDescGZIP(), []int{17} } func (x *LoginRequest) GetUsername() string { @@ -1090,7 +1090,7 @@ type LoginResponse struct { func (x *LoginResponse) Reset() { *x = LoginResponse{} - mi := &file_live_sfu_proto_msgTypes[18] + mi := &file_sync_live_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1102,7 +1102,7 @@ func (x *LoginResponse) String() string { func (*LoginResponse) ProtoMessage() {} func (x *LoginResponse) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[18] + mi := &file_sync_live_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1115,7 +1115,7 @@ func (x *LoginResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead. func (*LoginResponse) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{18} + return file_sync_live_proto_rawDescGZIP(), []int{18} } func (x *LoginResponse) GetToken() string { @@ -1150,7 +1150,7 @@ type RegisterRequest struct { func (x *RegisterRequest) Reset() { *x = RegisterRequest{} - mi := &file_live_sfu_proto_msgTypes[19] + mi := &file_sync_live_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1162,7 +1162,7 @@ func (x *RegisterRequest) String() string { func (*RegisterRequest) ProtoMessage() {} func (x *RegisterRequest) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[19] + mi := &file_sync_live_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1175,7 +1175,7 @@ func (x *RegisterRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RegisterRequest.ProtoReflect.Descriptor instead. func (*RegisterRequest) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{19} + return file_sync_live_proto_rawDescGZIP(), []int{19} } func (x *RegisterRequest) GetUsername() string { @@ -1210,7 +1210,7 @@ type RegisterResponse struct { func (x *RegisterResponse) Reset() { *x = RegisterResponse{} - mi := &file_live_sfu_proto_msgTypes[20] + mi := &file_sync_live_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1222,7 +1222,7 @@ func (x *RegisterResponse) String() string { func (*RegisterResponse) ProtoMessage() {} func (x *RegisterResponse) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[20] + mi := &file_sync_live_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1235,7 +1235,7 @@ func (x *RegisterResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RegisterResponse.ProtoReflect.Descriptor instead. func (*RegisterResponse) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{20} + return file_sync_live_proto_rawDescGZIP(), []int{20} } func (x *RegisterResponse) GetToken() string { @@ -1267,7 +1267,7 @@ type GetMeRequest struct { func (x *GetMeRequest) Reset() { *x = GetMeRequest{} - mi := &file_live_sfu_proto_msgTypes[21] + mi := &file_sync_live_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1279,7 +1279,7 @@ func (x *GetMeRequest) String() string { func (*GetMeRequest) ProtoMessage() {} func (x *GetMeRequest) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[21] + mi := &file_sync_live_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1292,7 +1292,7 @@ func (x *GetMeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetMeRequest.ProtoReflect.Descriptor instead. func (*GetMeRequest) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{21} + return file_sync_live_proto_rawDescGZIP(), []int{21} } type GetMeResponse struct { @@ -1306,7 +1306,7 @@ type GetMeResponse struct { func (x *GetMeResponse) Reset() { *x = GetMeResponse{} - mi := &file_live_sfu_proto_msgTypes[22] + mi := &file_sync_live_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1318,7 +1318,7 @@ func (x *GetMeResponse) String() string { func (*GetMeResponse) ProtoMessage() {} func (x *GetMeResponse) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[22] + mi := &file_sync_live_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1331,7 +1331,7 @@ func (x *GetMeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetMeResponse.ProtoReflect.Descriptor instead. func (*GetMeResponse) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{22} + return file_sync_live_proto_rawDescGZIP(), []int{22} } func (x *GetMeResponse) GetUser() *UserInfo { @@ -1363,7 +1363,7 @@ type ListUsersRequest struct { func (x *ListUsersRequest) Reset() { *x = ListUsersRequest{} - mi := &file_live_sfu_proto_msgTypes[23] + mi := &file_sync_live_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1375,7 +1375,7 @@ func (x *ListUsersRequest) String() string { func (*ListUsersRequest) ProtoMessage() {} func (x *ListUsersRequest) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[23] + mi := &file_sync_live_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1388,7 +1388,7 @@ func (x *ListUsersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListUsersRequest.ProtoReflect.Descriptor instead. func (*ListUsersRequest) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{23} + return file_sync_live_proto_rawDescGZIP(), []int{23} } type ListUsersResponse struct { @@ -1400,7 +1400,7 @@ type ListUsersResponse struct { func (x *ListUsersResponse) Reset() { *x = ListUsersResponse{} - mi := &file_live_sfu_proto_msgTypes[24] + mi := &file_sync_live_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1412,7 +1412,7 @@ func (x *ListUsersResponse) String() string { func (*ListUsersResponse) ProtoMessage() {} func (x *ListUsersResponse) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[24] + mi := &file_sync_live_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1425,7 +1425,7 @@ func (x *ListUsersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListUsersResponse.ProtoReflect.Descriptor instead. func (*ListUsersResponse) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{24} + return file_sync_live_proto_rawDescGZIP(), []int{24} } func (x *ListUsersResponse) GetUsers() []*UserInfo { @@ -1445,7 +1445,7 @@ type UpdateUserRoleRequest struct { func (x *UpdateUserRoleRequest) Reset() { *x = UpdateUserRoleRequest{} - mi := &file_live_sfu_proto_msgTypes[25] + mi := &file_sync_live_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1457,7 +1457,7 @@ func (x *UpdateUserRoleRequest) String() string { func (*UpdateUserRoleRequest) ProtoMessage() {} func (x *UpdateUserRoleRequest) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[25] + mi := &file_sync_live_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1470,7 +1470,7 @@ func (x *UpdateUserRoleRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateUserRoleRequest.ProtoReflect.Descriptor instead. func (*UpdateUserRoleRequest) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{25} + return file_sync_live_proto_rawDescGZIP(), []int{25} } func (x *UpdateUserRoleRequest) GetUsername() string { @@ -1497,7 +1497,7 @@ type UpdateUserRoleResponse struct { func (x *UpdateUserRoleResponse) Reset() { *x = UpdateUserRoleResponse{} - mi := &file_live_sfu_proto_msgTypes[26] + mi := &file_sync_live_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1509,7 +1509,7 @@ func (x *UpdateUserRoleResponse) String() string { func (*UpdateUserRoleResponse) ProtoMessage() {} func (x *UpdateUserRoleResponse) ProtoReflect() protoreflect.Message { - mi := &file_live_sfu_proto_msgTypes[26] + mi := &file_sync_live_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1522,7 +1522,7 @@ func (x *UpdateUserRoleResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateUserRoleResponse.ProtoReflect.Descriptor instead. func (*UpdateUserRoleResponse) Descriptor() ([]byte, []int) { - return file_live_sfu_proto_rawDescGZIP(), []int{26} + return file_sync_live_proto_rawDescGZIP(), []int{26} } func (x *UpdateUserRoleResponse) GetOk() bool { @@ -1539,76 +1539,76 @@ func (x *UpdateUserRoleResponse) GetUser() *UserInfo { return nil } -var File_live_sfu_proto protoreflect.FileDescriptor +var File_sync_live_proto protoreflect.FileDescriptor -const file_live_sfu_proto_rawDesc = "" + +const file_sync_live_proto_rawDesc = "" + "\n" + - "\x0elive_sfu.proto\x12\x13gospeak.livedemo.v1\"[\n" + + "\x0fsync_live.proto\x12\vsynclive.v1\"[\n" + "\tIceServer\x12\x12\n" + "\x04urls\x18\x01 \x03(\tR\x04urls\x12\x1a\n" + "\busername\x18\x02 \x01(\tR\busername\x12\x1e\n" + "\n" + "credential\x18\x03 \x01(\tR\n" + "credential\"\x12\n" + - "\x10GetConfigRequest\"\x91\x01\n" + - "\vBackendInfo\x124\n" + - "\x04kind\x18\x01 \x01(\x0e2 .gospeak.livedemo.v1.BackendKindR\x04kind\x12\x12\n" + + "\x10GetConfigRequest\"\x89\x01\n" + + "\vBackendInfo\x12,\n" + + "\x04kind\x18\x01 \x01(\x0e2\x18.synclive.v1.BackendKindR\x04kind\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1e\n" + "\n" + "configured\x18\x03 \x01(\bR\n" + "configured\x12\x18\n" + - "\aprimary\x18\x04 \x01(\bR\aprimary\"\x96\x01\n" + - "\x11GetConfigResponse\x12<\n" + - "\bbackends\x18\x01 \x03(\v2 .gospeak.livedemo.v1.BackendInfoR\bbackends\x12\x1c\n" + + "\aprimary\x18\x04 \x01(\bR\aprimary\"\x8e\x01\n" + + "\x11GetConfigResponse\x124\n" + + "\bbackends\x18\x01 \x03(\v2\x18.synclive.v1.BackendInfoR\bbackends\x12\x1c\n" + "\tcandidate\x18\x02 \x01(\tR\tcandidate\x12%\n" + - "\x0etoken_required\x18\x03 \x01(\bR\rtokenRequired\"\xdb\x01\n" + - "\fStreamTarget\x12:\n" + - "\abackend\x18\x01 \x01(\x0e2 .gospeak.livedemo.v1.BackendKindR\abackend\x12\x1d\n" + + "\x0etoken_required\x18\x03 \x01(\bR\rtokenRequired\"\xd3\x01\n" + + "\fStreamTarget\x122\n" + + "\abackend\x18\x01 \x01(\x0e2\x18.synclive.v1.BackendKindR\abackend\x12\x1d\n" + "\n" + "session_id\x18\x02 \x01(\tR\tsessionId\x12\x16\n" + "\x06stream\x18\x03 \x01(\tR\x06stream\x12#\n" + "\rpublish_token\x18\x04 \x01(\tR\fpublishToken\x12\x10\n" + "\x03url\x18\x05 \x01(\tR\x03url\x12!\n" + - "\fpublished_at\x18\x06 \x01(\x03R\vpublishedAt\"W\n" + + "\fpublished_at\x18\x06 \x01(\x03R\vpublishedAt\"O\n" + "\x04Room\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12;\n" + - "\atargets\x18\x02 \x03(\v2!.gospeak.livedemo.v1.StreamTargetR\atargets\"\x12\n" + - "\x10ListRoomsRequest\"D\n" + - "\x11ListRoomsResponse\x12/\n" + - "\x05rooms\x18\x01 \x03(\v2\x19.gospeak.livedemo.v1.RoomR\x05rooms\"|\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x123\n" + + "\atargets\x18\x02 \x03(\v2\x19.synclive.v1.StreamTargetR\atargets\"\x12\n" + + "\x10ListRoomsRequest\"<\n" + + "\x11ListRoomsResponse\x12'\n" + + "\x05rooms\x18\x01 \x03(\v2\x11.synclive.v1.RoomR\x05rooms\"t\n" + "\x0ePublishRequest\x12\x12\n" + - "\x04room\x18\x01 \x01(\tR\x04room\x12:\n" + - "\abackend\x18\x02 \x01(\x0e2 .gospeak.livedemo.v1.BackendKindR\abackend\x12\x1a\n" + - "\bidentity\x18\x03 \x01(\tR\bidentity\"\xe9\x01\n" + + "\x04room\x18\x01 \x01(\tR\x04room\x122\n" + + "\abackend\x18\x02 \x01(\x0e2\x18.synclive.v1.BackendKindR\abackend\x12\x1a\n" + + "\bidentity\x18\x03 \x01(\tR\bidentity\"\xd9\x01\n" + "\x0fPublishResponse\x12\x1d\n" + "\n" + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x16\n" + "\x06stream\x18\x02 \x01(\tR\x06stream\x12#\n" + - "\rpublish_token\x18\x03 \x01(\tR\fpublishToken\x12?\n" + - "\vice_servers\x18\x04 \x03(\v2\x1e.gospeak.livedemo.v1.IceServerR\n" + - "iceServers\x129\n" + - "\x06target\x18\x05 \x01(\v2!.gospeak.livedemo.v1.StreamTargetR\x06target\"~\n" + + "\rpublish_token\x18\x03 \x01(\tR\fpublishToken\x127\n" + + "\vice_servers\x18\x04 \x03(\v2\x16.synclive.v1.IceServerR\n" + + "iceServers\x121\n" + + "\x06target\x18\x05 \x01(\v2\x19.synclive.v1.StreamTargetR\x06target\"v\n" + "\x10SubscribeRequest\x12\x12\n" + - "\x04room\x18\x01 \x01(\tR\x04room\x12:\n" + - "\abackend\x18\x02 \x01(\x0e2 .gospeak.livedemo.v1.BackendKindR\abackend\x12\x1a\n" + - "\bidentity\x18\x03 \x01(\tR\bidentity\"\xbd\x01\n" + + "\x04room\x18\x01 \x01(\tR\x04room\x122\n" + + "\abackend\x18\x02 \x01(\x0e2\x18.synclive.v1.BackendKindR\abackend\x12\x1a\n" + + "\bidentity\x18\x03 \x01(\tR\bidentity\"\xb5\x01\n" + "\x11SubscribeResponse\x12\x1d\n" + "\n" + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x16\n" + "\x06stream\x18\x02 \x01(\tR\x06stream\x120\n" + - "\x14publisher_session_id\x18\x03 \x01(\tR\x12publisherSessionId\x12?\n" + - "\vice_servers\x18\x04 \x03(\v2\x1e.gospeak.livedemo.v1.IceServerR\n" + - "iceServers\"c\n" + + "\x14publisher_session_id\x18\x03 \x01(\tR\x12publisherSessionId\x127\n" + + "\vice_servers\x18\x04 \x03(\v2\x16.synclive.v1.IceServerR\n" + + "iceServers\"[\n" + "\x11StopStreamRequest\x12\x12\n" + - "\x04room\x18\x01 \x01(\tR\x04room\x12:\n" + - "\abackend\x18\x02 \x01(\x0e2 .gospeak.livedemo.v1.BackendKindR\abackend\"$\n" + + "\x04room\x18\x01 \x01(\tR\x04room\x122\n" + + "\abackend\x18\x02 \x01(\x0e2\x18.synclive.v1.BackendKindR\abackend\"$\n" + "\x12StopStreamResponse\x12\x0e\n" + "\x02ok\x18\x01 \x01(\bR\x02ok\"&\n" + "\x10WatchRoomRequest\x12\x12\n" + - "\x04room\x18\x01 \x01(\tR\x04room\"\\\n" + + "\x04room\x18\x01 \x01(\tR\x04room\"T\n" + "\tRoomEvent\x12\x12\n" + - "\x04room\x18\x01 \x01(\tR\x04room\x12;\n" + - "\atargets\x18\x02 \x03(\v2!.gospeak.livedemo.v1.StreamTargetR\atargets\"Y\n" + + "\x04room\x18\x01 \x01(\tR\x04room\x123\n" + + "\atargets\x18\x02 \x03(\v2\x19.synclive.v1.StreamTargetR\atargets\"Y\n" + "\bUserInfo\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12\x12\n" + "\x04role\x18\x02 \x01(\tR\x04role\x12\x1d\n" + @@ -1616,138 +1616,138 @@ const file_live_sfu_proto_rawDesc = "" + "created_at\x18\x03 \x01(\x03R\tcreatedAt\"F\n" + "\fLoginRequest\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + - "\bpassword\x18\x02 \x01(\tR\bpassword\"w\n" + + "\bpassword\x18\x02 \x01(\tR\bpassword\"o\n" + "\rLoginResponse\x12\x14\n" + - "\x05token\x18\x01 \x01(\tR\x05token\x121\n" + - "\x04user\x18\x02 \x01(\v2\x1d.gospeak.livedemo.v1.UserInfoR\x04user\x12\x1d\n" + + "\x05token\x18\x01 \x01(\tR\x05token\x12)\n" + + "\x04user\x18\x02 \x01(\v2\x15.synclive.v1.UserInfoR\x04user\x12\x1d\n" + "\n" + "expires_at\x18\x03 \x01(\x03R\texpiresAt\"]\n" + "\x0fRegisterRequest\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + "\bpassword\x18\x02 \x01(\tR\bpassword\x12\x12\n" + - "\x04role\x18\x03 \x01(\tR\x04role\"z\n" + + "\x04role\x18\x03 \x01(\tR\x04role\"r\n" + "\x10RegisterResponse\x12\x14\n" + - "\x05token\x18\x01 \x01(\tR\x05token\x121\n" + - "\x04user\x18\x02 \x01(\v2\x1d.gospeak.livedemo.v1.UserInfoR\x04user\x12\x1d\n" + + "\x05token\x18\x01 \x01(\tR\x05token\x12)\n" + + "\x04user\x18\x02 \x01(\v2\x15.synclive.v1.UserInfoR\x04user\x12\x1d\n" + "\n" + "expires_at\x18\x03 \x01(\x03R\texpiresAt\"\x0e\n" + - "\fGetMeRequest\"~\n" + - "\rGetMeResponse\x121\n" + - "\x04user\x18\x01 \x01(\v2\x1d.gospeak.livedemo.v1.UserInfoR\x04user\x12\x1d\n" + + "\fGetMeRequest\"v\n" + + "\rGetMeResponse\x12)\n" + + "\x04user\x18\x01 \x01(\v2\x15.synclive.v1.UserInfoR\x04user\x12\x1d\n" + "\n" + "expires_at\x18\x02 \x01(\x03R\texpiresAt\x12\x1b\n" + "\tissued_at\x18\x03 \x01(\x03R\bissuedAt\"\x12\n" + - "\x10ListUsersRequest\"H\n" + - "\x11ListUsersResponse\x123\n" + - "\x05users\x18\x01 \x03(\v2\x1d.gospeak.livedemo.v1.UserInfoR\x05users\"G\n" + + "\x10ListUsersRequest\"@\n" + + "\x11ListUsersResponse\x12+\n" + + "\x05users\x18\x01 \x03(\v2\x15.synclive.v1.UserInfoR\x05users\"G\n" + "\x15UpdateUserRoleRequest\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12\x12\n" + - "\x04role\x18\x02 \x01(\tR\x04role\"[\n" + + "\x04role\x18\x02 \x01(\tR\x04role\"S\n" + "\x16UpdateUserRoleResponse\x12\x0e\n" + - "\x02ok\x18\x01 \x01(\bR\x02ok\x121\n" + - "\x04user\x18\x02 \x01(\v2\x1d.gospeak.livedemo.v1.UserInfoR\x04user*^\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\x12)\n" + + "\x04user\x18\x02 \x01(\v2\x15.synclive.v1.UserInfoR\x04user*^\n" + "\vBackendKind\x12\x1c\n" + "\x18BACKEND_KIND_UNSPECIFIED\x10\x00\x12\x1b\n" + "\x17BACKEND_KIND_CLOUDFLARE\x10\x01\x12\x14\n" + - "\x10BACKEND_KIND_SRS\x10\x022\xe8\a\n" + - "\aLiveSFU\x12Z\n" + - "\tGetConfig\x12%.gospeak.livedemo.v1.GetConfigRequest\x1a&.gospeak.livedemo.v1.GetConfigResponse\x12Z\n" + - "\tListRooms\x12%.gospeak.livedemo.v1.ListRoomsRequest\x1a&.gospeak.livedemo.v1.ListRoomsResponse\x12T\n" + - "\aPublish\x12#.gospeak.livedemo.v1.PublishRequest\x1a$.gospeak.livedemo.v1.PublishResponse\x12Z\n" + - "\tSubscribe\x12%.gospeak.livedemo.v1.SubscribeRequest\x1a&.gospeak.livedemo.v1.SubscribeResponse\x12]\n" + + "\x10BACKEND_KIND_SRS\x10\x022\xb9\x06\n" + + "\bSyncLive\x12J\n" + + "\tGetConfig\x12\x1d.synclive.v1.GetConfigRequest\x1a\x1e.synclive.v1.GetConfigResponse\x12J\n" + + "\tListRooms\x12\x1d.synclive.v1.ListRoomsRequest\x1a\x1e.synclive.v1.ListRoomsResponse\x12D\n" + + "\aPublish\x12\x1b.synclive.v1.PublishRequest\x1a\x1c.synclive.v1.PublishResponse\x12J\n" + + "\tSubscribe\x12\x1d.synclive.v1.SubscribeRequest\x1a\x1e.synclive.v1.SubscribeResponse\x12M\n" + "\n" + - "StopStream\x12&.gospeak.livedemo.v1.StopStreamRequest\x1a'.gospeak.livedemo.v1.StopStreamResponse\x12T\n" + - "\tWatchRoom\x12%.gospeak.livedemo.v1.WatchRoomRequest\x1a\x1e.gospeak.livedemo.v1.RoomEvent0\x01\x12N\n" + - "\x05Login\x12!.gospeak.livedemo.v1.LoginRequest\x1a\".gospeak.livedemo.v1.LoginResponse\x12W\n" + - "\bRegister\x12$.gospeak.livedemo.v1.RegisterRequest\x1a%.gospeak.livedemo.v1.RegisterResponse\x12N\n" + - "\x05GetMe\x12!.gospeak.livedemo.v1.GetMeRequest\x1a\".gospeak.livedemo.v1.GetMeResponse\x12Z\n" + - "\tListUsers\x12%.gospeak.livedemo.v1.ListUsersRequest\x1a&.gospeak.livedemo.v1.ListUsersResponse\x12i\n" + - "\x0eUpdateUserRole\x12*.gospeak.livedemo.v1.UpdateUserRoleRequest\x1a+.gospeak.livedemo.v1.UpdateUserRoleResponseB\x1bZ\x19gospeak-live-sfu-demo/genb\x06proto3" + "StopStream\x12\x1e.synclive.v1.StopStreamRequest\x1a\x1f.synclive.v1.StopStreamResponse\x12D\n" + + "\tWatchRoom\x12\x1d.synclive.v1.WatchRoomRequest\x1a\x16.synclive.v1.RoomEvent0\x01\x12>\n" + + "\x05Login\x12\x19.synclive.v1.LoginRequest\x1a\x1a.synclive.v1.LoginResponse\x12G\n" + + "\bRegister\x12\x1c.synclive.v1.RegisterRequest\x1a\x1d.synclive.v1.RegisterResponse\x12>\n" + + "\x05GetMe\x12\x19.synclive.v1.GetMeRequest\x1a\x1a.synclive.v1.GetMeResponse\x12J\n" + + "\tListUsers\x12\x1d.synclive.v1.ListUsersRequest\x1a\x1e.synclive.v1.ListUsersResponse\x12Y\n" + + "\x0eUpdateUserRole\x12\".synclive.v1.UpdateUserRoleRequest\x1a#.synclive.v1.UpdateUserRoleResponseB\x0fZ\rsync-live/genb\x06proto3" var ( - file_live_sfu_proto_rawDescOnce sync.Once - file_live_sfu_proto_rawDescData []byte + file_sync_live_proto_rawDescOnce sync.Once + file_sync_live_proto_rawDescData []byte ) -func file_live_sfu_proto_rawDescGZIP() []byte { - file_live_sfu_proto_rawDescOnce.Do(func() { - file_live_sfu_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_live_sfu_proto_rawDesc), len(file_live_sfu_proto_rawDesc))) +func file_sync_live_proto_rawDescGZIP() []byte { + file_sync_live_proto_rawDescOnce.Do(func() { + file_sync_live_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_sync_live_proto_rawDesc), len(file_sync_live_proto_rawDesc))) }) - return file_live_sfu_proto_rawDescData + return file_sync_live_proto_rawDescData } -var file_live_sfu_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_live_sfu_proto_msgTypes = make([]protoimpl.MessageInfo, 27) -var file_live_sfu_proto_goTypes = []any{ - (BackendKind)(0), // 0: gospeak.livedemo.v1.BackendKind - (*IceServer)(nil), // 1: gospeak.livedemo.v1.IceServer - (*GetConfigRequest)(nil), // 2: gospeak.livedemo.v1.GetConfigRequest - (*BackendInfo)(nil), // 3: gospeak.livedemo.v1.BackendInfo - (*GetConfigResponse)(nil), // 4: gospeak.livedemo.v1.GetConfigResponse - (*StreamTarget)(nil), // 5: gospeak.livedemo.v1.StreamTarget - (*Room)(nil), // 6: gospeak.livedemo.v1.Room - (*ListRoomsRequest)(nil), // 7: gospeak.livedemo.v1.ListRoomsRequest - (*ListRoomsResponse)(nil), // 8: gospeak.livedemo.v1.ListRoomsResponse - (*PublishRequest)(nil), // 9: gospeak.livedemo.v1.PublishRequest - (*PublishResponse)(nil), // 10: gospeak.livedemo.v1.PublishResponse - (*SubscribeRequest)(nil), // 11: gospeak.livedemo.v1.SubscribeRequest - (*SubscribeResponse)(nil), // 12: gospeak.livedemo.v1.SubscribeResponse - (*StopStreamRequest)(nil), // 13: gospeak.livedemo.v1.StopStreamRequest - (*StopStreamResponse)(nil), // 14: gospeak.livedemo.v1.StopStreamResponse - (*WatchRoomRequest)(nil), // 15: gospeak.livedemo.v1.WatchRoomRequest - (*RoomEvent)(nil), // 16: gospeak.livedemo.v1.RoomEvent - (*UserInfo)(nil), // 17: gospeak.livedemo.v1.UserInfo - (*LoginRequest)(nil), // 18: gospeak.livedemo.v1.LoginRequest - (*LoginResponse)(nil), // 19: gospeak.livedemo.v1.LoginResponse - (*RegisterRequest)(nil), // 20: gospeak.livedemo.v1.RegisterRequest - (*RegisterResponse)(nil), // 21: gospeak.livedemo.v1.RegisterResponse - (*GetMeRequest)(nil), // 22: gospeak.livedemo.v1.GetMeRequest - (*GetMeResponse)(nil), // 23: gospeak.livedemo.v1.GetMeResponse - (*ListUsersRequest)(nil), // 24: gospeak.livedemo.v1.ListUsersRequest - (*ListUsersResponse)(nil), // 25: gospeak.livedemo.v1.ListUsersResponse - (*UpdateUserRoleRequest)(nil), // 26: gospeak.livedemo.v1.UpdateUserRoleRequest - (*UpdateUserRoleResponse)(nil), // 27: gospeak.livedemo.v1.UpdateUserRoleResponse +var file_sync_live_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_sync_live_proto_msgTypes = make([]protoimpl.MessageInfo, 27) +var file_sync_live_proto_goTypes = []any{ + (BackendKind)(0), // 0: synclive.v1.BackendKind + (*IceServer)(nil), // 1: synclive.v1.IceServer + (*GetConfigRequest)(nil), // 2: synclive.v1.GetConfigRequest + (*BackendInfo)(nil), // 3: synclive.v1.BackendInfo + (*GetConfigResponse)(nil), // 4: synclive.v1.GetConfigResponse + (*StreamTarget)(nil), // 5: synclive.v1.StreamTarget + (*Room)(nil), // 6: synclive.v1.Room + (*ListRoomsRequest)(nil), // 7: synclive.v1.ListRoomsRequest + (*ListRoomsResponse)(nil), // 8: synclive.v1.ListRoomsResponse + (*PublishRequest)(nil), // 9: synclive.v1.PublishRequest + (*PublishResponse)(nil), // 10: synclive.v1.PublishResponse + (*SubscribeRequest)(nil), // 11: synclive.v1.SubscribeRequest + (*SubscribeResponse)(nil), // 12: synclive.v1.SubscribeResponse + (*StopStreamRequest)(nil), // 13: synclive.v1.StopStreamRequest + (*StopStreamResponse)(nil), // 14: synclive.v1.StopStreamResponse + (*WatchRoomRequest)(nil), // 15: synclive.v1.WatchRoomRequest + (*RoomEvent)(nil), // 16: synclive.v1.RoomEvent + (*UserInfo)(nil), // 17: synclive.v1.UserInfo + (*LoginRequest)(nil), // 18: synclive.v1.LoginRequest + (*LoginResponse)(nil), // 19: synclive.v1.LoginResponse + (*RegisterRequest)(nil), // 20: synclive.v1.RegisterRequest + (*RegisterResponse)(nil), // 21: synclive.v1.RegisterResponse + (*GetMeRequest)(nil), // 22: synclive.v1.GetMeRequest + (*GetMeResponse)(nil), // 23: synclive.v1.GetMeResponse + (*ListUsersRequest)(nil), // 24: synclive.v1.ListUsersRequest + (*ListUsersResponse)(nil), // 25: synclive.v1.ListUsersResponse + (*UpdateUserRoleRequest)(nil), // 26: synclive.v1.UpdateUserRoleRequest + (*UpdateUserRoleResponse)(nil), // 27: synclive.v1.UpdateUserRoleResponse } -var file_live_sfu_proto_depIdxs = []int32{ - 0, // 0: gospeak.livedemo.v1.BackendInfo.kind:type_name -> gospeak.livedemo.v1.BackendKind - 3, // 1: gospeak.livedemo.v1.GetConfigResponse.backends:type_name -> gospeak.livedemo.v1.BackendInfo - 0, // 2: gospeak.livedemo.v1.StreamTarget.backend:type_name -> gospeak.livedemo.v1.BackendKind - 5, // 3: gospeak.livedemo.v1.Room.targets:type_name -> gospeak.livedemo.v1.StreamTarget - 6, // 4: gospeak.livedemo.v1.ListRoomsResponse.rooms:type_name -> gospeak.livedemo.v1.Room - 0, // 5: gospeak.livedemo.v1.PublishRequest.backend:type_name -> gospeak.livedemo.v1.BackendKind - 1, // 6: gospeak.livedemo.v1.PublishResponse.ice_servers:type_name -> gospeak.livedemo.v1.IceServer - 5, // 7: gospeak.livedemo.v1.PublishResponse.target:type_name -> gospeak.livedemo.v1.StreamTarget - 0, // 8: gospeak.livedemo.v1.SubscribeRequest.backend:type_name -> gospeak.livedemo.v1.BackendKind - 1, // 9: gospeak.livedemo.v1.SubscribeResponse.ice_servers:type_name -> gospeak.livedemo.v1.IceServer - 0, // 10: gospeak.livedemo.v1.StopStreamRequest.backend:type_name -> gospeak.livedemo.v1.BackendKind - 5, // 11: gospeak.livedemo.v1.RoomEvent.targets:type_name -> gospeak.livedemo.v1.StreamTarget - 17, // 12: gospeak.livedemo.v1.LoginResponse.user:type_name -> gospeak.livedemo.v1.UserInfo - 17, // 13: gospeak.livedemo.v1.RegisterResponse.user:type_name -> gospeak.livedemo.v1.UserInfo - 17, // 14: gospeak.livedemo.v1.GetMeResponse.user:type_name -> gospeak.livedemo.v1.UserInfo - 17, // 15: gospeak.livedemo.v1.ListUsersResponse.users:type_name -> gospeak.livedemo.v1.UserInfo - 17, // 16: gospeak.livedemo.v1.UpdateUserRoleResponse.user:type_name -> gospeak.livedemo.v1.UserInfo - 2, // 17: gospeak.livedemo.v1.LiveSFU.GetConfig:input_type -> gospeak.livedemo.v1.GetConfigRequest - 7, // 18: gospeak.livedemo.v1.LiveSFU.ListRooms:input_type -> gospeak.livedemo.v1.ListRoomsRequest - 9, // 19: gospeak.livedemo.v1.LiveSFU.Publish:input_type -> gospeak.livedemo.v1.PublishRequest - 11, // 20: gospeak.livedemo.v1.LiveSFU.Subscribe:input_type -> gospeak.livedemo.v1.SubscribeRequest - 13, // 21: gospeak.livedemo.v1.LiveSFU.StopStream:input_type -> gospeak.livedemo.v1.StopStreamRequest - 15, // 22: gospeak.livedemo.v1.LiveSFU.WatchRoom:input_type -> gospeak.livedemo.v1.WatchRoomRequest - 18, // 23: gospeak.livedemo.v1.LiveSFU.Login:input_type -> gospeak.livedemo.v1.LoginRequest - 20, // 24: gospeak.livedemo.v1.LiveSFU.Register:input_type -> gospeak.livedemo.v1.RegisterRequest - 22, // 25: gospeak.livedemo.v1.LiveSFU.GetMe:input_type -> gospeak.livedemo.v1.GetMeRequest - 24, // 26: gospeak.livedemo.v1.LiveSFU.ListUsers:input_type -> gospeak.livedemo.v1.ListUsersRequest - 26, // 27: gospeak.livedemo.v1.LiveSFU.UpdateUserRole:input_type -> gospeak.livedemo.v1.UpdateUserRoleRequest - 4, // 28: gospeak.livedemo.v1.LiveSFU.GetConfig:output_type -> gospeak.livedemo.v1.GetConfigResponse - 8, // 29: gospeak.livedemo.v1.LiveSFU.ListRooms:output_type -> gospeak.livedemo.v1.ListRoomsResponse - 10, // 30: gospeak.livedemo.v1.LiveSFU.Publish:output_type -> gospeak.livedemo.v1.PublishResponse - 12, // 31: gospeak.livedemo.v1.LiveSFU.Subscribe:output_type -> gospeak.livedemo.v1.SubscribeResponse - 14, // 32: gospeak.livedemo.v1.LiveSFU.StopStream:output_type -> gospeak.livedemo.v1.StopStreamResponse - 16, // 33: gospeak.livedemo.v1.LiveSFU.WatchRoom:output_type -> gospeak.livedemo.v1.RoomEvent - 19, // 34: gospeak.livedemo.v1.LiveSFU.Login:output_type -> gospeak.livedemo.v1.LoginResponse - 21, // 35: gospeak.livedemo.v1.LiveSFU.Register:output_type -> gospeak.livedemo.v1.RegisterResponse - 23, // 36: gospeak.livedemo.v1.LiveSFU.GetMe:output_type -> gospeak.livedemo.v1.GetMeResponse - 25, // 37: gospeak.livedemo.v1.LiveSFU.ListUsers:output_type -> gospeak.livedemo.v1.ListUsersResponse - 27, // 38: gospeak.livedemo.v1.LiveSFU.UpdateUserRole:output_type -> gospeak.livedemo.v1.UpdateUserRoleResponse +var file_sync_live_proto_depIdxs = []int32{ + 0, // 0: synclive.v1.BackendInfo.kind:type_name -> synclive.v1.BackendKind + 3, // 1: synclive.v1.GetConfigResponse.backends:type_name -> synclive.v1.BackendInfo + 0, // 2: synclive.v1.StreamTarget.backend:type_name -> synclive.v1.BackendKind + 5, // 3: synclive.v1.Room.targets:type_name -> synclive.v1.StreamTarget + 6, // 4: synclive.v1.ListRoomsResponse.rooms:type_name -> synclive.v1.Room + 0, // 5: synclive.v1.PublishRequest.backend:type_name -> synclive.v1.BackendKind + 1, // 6: synclive.v1.PublishResponse.ice_servers:type_name -> synclive.v1.IceServer + 5, // 7: synclive.v1.PublishResponse.target:type_name -> synclive.v1.StreamTarget + 0, // 8: synclive.v1.SubscribeRequest.backend:type_name -> synclive.v1.BackendKind + 1, // 9: synclive.v1.SubscribeResponse.ice_servers:type_name -> synclive.v1.IceServer + 0, // 10: synclive.v1.StopStreamRequest.backend:type_name -> synclive.v1.BackendKind + 5, // 11: synclive.v1.RoomEvent.targets:type_name -> synclive.v1.StreamTarget + 17, // 12: synclive.v1.LoginResponse.user:type_name -> synclive.v1.UserInfo + 17, // 13: synclive.v1.RegisterResponse.user:type_name -> synclive.v1.UserInfo + 17, // 14: synclive.v1.GetMeResponse.user:type_name -> synclive.v1.UserInfo + 17, // 15: synclive.v1.ListUsersResponse.users:type_name -> synclive.v1.UserInfo + 17, // 16: synclive.v1.UpdateUserRoleResponse.user:type_name -> synclive.v1.UserInfo + 2, // 17: synclive.v1.SyncLive.GetConfig:input_type -> synclive.v1.GetConfigRequest + 7, // 18: synclive.v1.SyncLive.ListRooms:input_type -> synclive.v1.ListRoomsRequest + 9, // 19: synclive.v1.SyncLive.Publish:input_type -> synclive.v1.PublishRequest + 11, // 20: synclive.v1.SyncLive.Subscribe:input_type -> synclive.v1.SubscribeRequest + 13, // 21: synclive.v1.SyncLive.StopStream:input_type -> synclive.v1.StopStreamRequest + 15, // 22: synclive.v1.SyncLive.WatchRoom:input_type -> synclive.v1.WatchRoomRequest + 18, // 23: synclive.v1.SyncLive.Login:input_type -> synclive.v1.LoginRequest + 20, // 24: synclive.v1.SyncLive.Register:input_type -> synclive.v1.RegisterRequest + 22, // 25: synclive.v1.SyncLive.GetMe:input_type -> synclive.v1.GetMeRequest + 24, // 26: synclive.v1.SyncLive.ListUsers:input_type -> synclive.v1.ListUsersRequest + 26, // 27: synclive.v1.SyncLive.UpdateUserRole:input_type -> synclive.v1.UpdateUserRoleRequest + 4, // 28: synclive.v1.SyncLive.GetConfig:output_type -> synclive.v1.GetConfigResponse + 8, // 29: synclive.v1.SyncLive.ListRooms:output_type -> synclive.v1.ListRoomsResponse + 10, // 30: synclive.v1.SyncLive.Publish:output_type -> synclive.v1.PublishResponse + 12, // 31: synclive.v1.SyncLive.Subscribe:output_type -> synclive.v1.SubscribeResponse + 14, // 32: synclive.v1.SyncLive.StopStream:output_type -> synclive.v1.StopStreamResponse + 16, // 33: synclive.v1.SyncLive.WatchRoom:output_type -> synclive.v1.RoomEvent + 19, // 34: synclive.v1.SyncLive.Login:output_type -> synclive.v1.LoginResponse + 21, // 35: synclive.v1.SyncLive.Register:output_type -> synclive.v1.RegisterResponse + 23, // 36: synclive.v1.SyncLive.GetMe:output_type -> synclive.v1.GetMeResponse + 25, // 37: synclive.v1.SyncLive.ListUsers:output_type -> synclive.v1.ListUsersResponse + 27, // 38: synclive.v1.SyncLive.UpdateUserRole:output_type -> synclive.v1.UpdateUserRoleResponse 28, // [28:39] is the sub-list for method output_type 17, // [17:28] is the sub-list for method input_type 17, // [17:17] is the sub-list for extension type_name @@ -1755,27 +1755,27 @@ var file_live_sfu_proto_depIdxs = []int32{ 0, // [0:17] is the sub-list for field type_name } -func init() { file_live_sfu_proto_init() } -func file_live_sfu_proto_init() { - if File_live_sfu_proto != nil { +func init() { file_sync_live_proto_init() } +func file_sync_live_proto_init() { + if File_sync_live_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_live_sfu_proto_rawDesc), len(file_live_sfu_proto_rawDesc)), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_sync_live_proto_rawDesc), len(file_sync_live_proto_rawDesc)), NumEnums: 1, NumMessages: 27, NumExtensions: 0, NumServices: 1, }, - GoTypes: file_live_sfu_proto_goTypes, - DependencyIndexes: file_live_sfu_proto_depIdxs, - EnumInfos: file_live_sfu_proto_enumTypes, - MessageInfos: file_live_sfu_proto_msgTypes, + GoTypes: file_sync_live_proto_goTypes, + DependencyIndexes: file_sync_live_proto_depIdxs, + EnumInfos: file_sync_live_proto_enumTypes, + MessageInfos: file_sync_live_proto_msgTypes, }.Build() - File_live_sfu_proto = out.File - file_live_sfu_proto_goTypes = nil - file_live_sfu_proto_depIdxs = nil + File_sync_live_proto = out.File + file_sync_live_proto_goTypes = nil + file_sync_live_proto_depIdxs = nil } diff --git a/gen/sync_live_grpc.pb.go b/gen/sync_live_grpc.pb.go new file mode 100644 index 0000000..d7dba4a --- /dev/null +++ b/gen/sync_live_grpc.pb.go @@ -0,0 +1,517 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc v7.35.1 +// source: sync_live.proto + +package gen + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + SyncLive_GetConfig_FullMethodName = "/synclive.v1.SyncLive/GetConfig" + SyncLive_ListRooms_FullMethodName = "/synclive.v1.SyncLive/ListRooms" + SyncLive_Publish_FullMethodName = "/synclive.v1.SyncLive/Publish" + SyncLive_Subscribe_FullMethodName = "/synclive.v1.SyncLive/Subscribe" + SyncLive_StopStream_FullMethodName = "/synclive.v1.SyncLive/StopStream" + SyncLive_WatchRoom_FullMethodName = "/synclive.v1.SyncLive/WatchRoom" + SyncLive_Login_FullMethodName = "/synclive.v1.SyncLive/Login" + SyncLive_Register_FullMethodName = "/synclive.v1.SyncLive/Register" + SyncLive_GetMe_FullMethodName = "/synclive.v1.SyncLive/GetMe" + SyncLive_ListUsers_FullMethodName = "/synclive.v1.SyncLive/ListUsers" + SyncLive_UpdateUserRole_FullMethodName = "/synclive.v1.SyncLive/UpdateUserRole" +) + +// SyncLiveClient is the client API for SyncLive service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// SyncLive 是直播 SFU 分流服务的控制面契约(protobuf / gRPC)。 +// +// 媒体面(WebRTC SDP 交换)不在此契约内:浏览器与 Cloudflare Realtime / SRS +// 直连,经本服务反向代理转发(服务端注入 AppSecret / stream token)。 +// 本契约只负责房间、后端、发布/订阅拓扑的协调与扇出状态广播。 +type SyncLiveClient interface { + GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) + ListRooms(ctx context.Context, in *ListRoomsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error) + Publish(ctx context.Context, in *PublishRequest, opts ...grpc.CallOption) (*PublishResponse, error) + Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (*SubscribeResponse, error) + StopStream(ctx context.Context, in *StopStreamRequest, opts ...grpc.CallOption) (*StopStreamResponse, error) + WatchRoom(ctx context.Context, in *WatchRoomRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[RoomEvent], error) + // ---- Auth & Casbin ---- + Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) + Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) + GetMe(ctx context.Context, in *GetMeRequest, opts ...grpc.CallOption) (*GetMeResponse, error) + ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) + UpdateUserRole(ctx context.Context, in *UpdateUserRoleRequest, opts ...grpc.CallOption) (*UpdateUserRoleResponse, error) +} + +type syncLiveClient struct { + cc grpc.ClientConnInterface +} + +func NewSyncLiveClient(cc grpc.ClientConnInterface) SyncLiveClient { + return &syncLiveClient{cc} +} + +func (c *syncLiveClient) GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetConfigResponse) + err := c.cc.Invoke(ctx, SyncLive_GetConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *syncLiveClient) ListRooms(ctx context.Context, in *ListRoomsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListRoomsResponse) + err := c.cc.Invoke(ctx, SyncLive_ListRooms_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *syncLiveClient) Publish(ctx context.Context, in *PublishRequest, opts ...grpc.CallOption) (*PublishResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PublishResponse) + err := c.cc.Invoke(ctx, SyncLive_Publish_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *syncLiveClient) Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (*SubscribeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SubscribeResponse) + err := c.cc.Invoke(ctx, SyncLive_Subscribe_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *syncLiveClient) StopStream(ctx context.Context, in *StopStreamRequest, opts ...grpc.CallOption) (*StopStreamResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StopStreamResponse) + err := c.cc.Invoke(ctx, SyncLive_StopStream_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *syncLiveClient) WatchRoom(ctx context.Context, in *WatchRoomRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[RoomEvent], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &SyncLive_ServiceDesc.Streams[0], SyncLive_WatchRoom_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[WatchRoomRequest, RoomEvent]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SyncLive_WatchRoomClient = grpc.ServerStreamingClient[RoomEvent] + +func (c *syncLiveClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LoginResponse) + err := c.cc.Invoke(ctx, SyncLive_Login_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *syncLiveClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RegisterResponse) + err := c.cc.Invoke(ctx, SyncLive_Register_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *syncLiveClient) GetMe(ctx context.Context, in *GetMeRequest, opts ...grpc.CallOption) (*GetMeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetMeResponse) + err := c.cc.Invoke(ctx, SyncLive_GetMe_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *syncLiveClient) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListUsersResponse) + err := c.cc.Invoke(ctx, SyncLive_ListUsers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *syncLiveClient) UpdateUserRole(ctx context.Context, in *UpdateUserRoleRequest, opts ...grpc.CallOption) (*UpdateUserRoleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateUserRoleResponse) + err := c.cc.Invoke(ctx, SyncLive_UpdateUserRole_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SyncLiveServer is the server API for SyncLive service. +// All implementations should embed UnimplementedSyncLiveServer +// for forward compatibility. +// +// SyncLive 是直播 SFU 分流服务的控制面契约(protobuf / gRPC)。 +// +// 媒体面(WebRTC SDP 交换)不在此契约内:浏览器与 Cloudflare Realtime / SRS +// 直连,经本服务反向代理转发(服务端注入 AppSecret / stream token)。 +// 本契约只负责房间、后端、发布/订阅拓扑的协调与扇出状态广播。 +type SyncLiveServer interface { + GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error) + ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) + Publish(context.Context, *PublishRequest) (*PublishResponse, error) + Subscribe(context.Context, *SubscribeRequest) (*SubscribeResponse, error) + StopStream(context.Context, *StopStreamRequest) (*StopStreamResponse, error) + WatchRoom(*WatchRoomRequest, grpc.ServerStreamingServer[RoomEvent]) error + // ---- Auth & Casbin ---- + Login(context.Context, *LoginRequest) (*LoginResponse, error) + Register(context.Context, *RegisterRequest) (*RegisterResponse, error) + GetMe(context.Context, *GetMeRequest) (*GetMeResponse, error) + ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) + UpdateUserRole(context.Context, *UpdateUserRoleRequest) (*UpdateUserRoleResponse, error) +} + +// UnimplementedSyncLiveServer should be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedSyncLiveServer struct{} + +func (UnimplementedSyncLiveServer) GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetConfig not implemented") +} +func (UnimplementedSyncLiveServer) ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListRooms not implemented") +} +func (UnimplementedSyncLiveServer) Publish(context.Context, *PublishRequest) (*PublishResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Publish not implemented") +} +func (UnimplementedSyncLiveServer) Subscribe(context.Context, *SubscribeRequest) (*SubscribeResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Subscribe not implemented") +} +func (UnimplementedSyncLiveServer) StopStream(context.Context, *StopStreamRequest) (*StopStreamResponse, error) { + return nil, status.Error(codes.Unimplemented, "method StopStream not implemented") +} +func (UnimplementedSyncLiveServer) WatchRoom(*WatchRoomRequest, grpc.ServerStreamingServer[RoomEvent]) error { + return status.Error(codes.Unimplemented, "method WatchRoom not implemented") +} +func (UnimplementedSyncLiveServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Login not implemented") +} +func (UnimplementedSyncLiveServer) Register(context.Context, *RegisterRequest) (*RegisterResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Register not implemented") +} +func (UnimplementedSyncLiveServer) GetMe(context.Context, *GetMeRequest) (*GetMeResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetMe not implemented") +} +func (UnimplementedSyncLiveServer) ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListUsers not implemented") +} +func (UnimplementedSyncLiveServer) UpdateUserRole(context.Context, *UpdateUserRoleRequest) (*UpdateUserRoleResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateUserRole not implemented") +} +func (UnimplementedSyncLiveServer) testEmbeddedByValue() {} + +// UnsafeSyncLiveServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SyncLiveServer will +// result in compilation errors. +type UnsafeSyncLiveServer interface { + mustEmbedUnimplementedSyncLiveServer() +} + +func RegisterSyncLiveServer(s grpc.ServiceRegistrar, srv SyncLiveServer) { + // If the following call panics, it indicates UnimplementedSyncLiveServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&SyncLive_ServiceDesc, srv) +} + +func _SyncLive_GetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SyncLiveServer).GetConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SyncLive_GetConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SyncLiveServer).GetConfig(ctx, req.(*GetConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SyncLive_ListRooms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListRoomsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SyncLiveServer).ListRooms(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SyncLive_ListRooms_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SyncLiveServer).ListRooms(ctx, req.(*ListRoomsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SyncLive_Publish_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PublishRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SyncLiveServer).Publish(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SyncLive_Publish_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SyncLiveServer).Publish(ctx, req.(*PublishRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SyncLive_Subscribe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SubscribeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SyncLiveServer).Subscribe(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SyncLive_Subscribe_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SyncLiveServer).Subscribe(ctx, req.(*SubscribeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SyncLive_StopStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StopStreamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SyncLiveServer).StopStream(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SyncLive_StopStream_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SyncLiveServer).StopStream(ctx, req.(*StopStreamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SyncLive_WatchRoom_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(WatchRoomRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(SyncLiveServer).WatchRoom(m, &grpc.GenericServerStream[WatchRoomRequest, RoomEvent]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SyncLive_WatchRoomServer = grpc.ServerStreamingServer[RoomEvent] + +func _SyncLive_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LoginRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SyncLiveServer).Login(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SyncLive_Login_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SyncLiveServer).Login(ctx, req.(*LoginRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SyncLive_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RegisterRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SyncLiveServer).Register(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SyncLive_Register_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SyncLiveServer).Register(ctx, req.(*RegisterRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SyncLive_GetMe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetMeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SyncLiveServer).GetMe(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SyncLive_GetMe_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SyncLiveServer).GetMe(ctx, req.(*GetMeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SyncLive_ListUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListUsersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SyncLiveServer).ListUsers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SyncLive_ListUsers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SyncLiveServer).ListUsers(ctx, req.(*ListUsersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SyncLive_UpdateUserRole_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateUserRoleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SyncLiveServer).UpdateUserRole(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SyncLive_UpdateUserRole_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SyncLiveServer).UpdateUserRole(ctx, req.(*UpdateUserRoleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// SyncLive_ServiceDesc is the grpc.ServiceDesc for SyncLive service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var SyncLive_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "synclive.v1.SyncLive", + HandlerType: (*SyncLiveServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetConfig", + Handler: _SyncLive_GetConfig_Handler, + }, + { + MethodName: "ListRooms", + Handler: _SyncLive_ListRooms_Handler, + }, + { + MethodName: "Publish", + Handler: _SyncLive_Publish_Handler, + }, + { + MethodName: "Subscribe", + Handler: _SyncLive_Subscribe_Handler, + }, + { + MethodName: "StopStream", + Handler: _SyncLive_StopStream_Handler, + }, + { + MethodName: "Login", + Handler: _SyncLive_Login_Handler, + }, + { + MethodName: "Register", + Handler: _SyncLive_Register_Handler, + }, + { + MethodName: "GetMe", + Handler: _SyncLive_GetMe_Handler, + }, + { + MethodName: "ListUsers", + Handler: _SyncLive_ListUsers_Handler, + }, + { + MethodName: "UpdateUserRole", + Handler: _SyncLive_UpdateUserRole_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "WatchRoom", + Handler: _SyncLive_WatchRoom_Handler, + ServerStreams: true, + }, + }, + Metadata: "sync_live.proto", +} diff --git a/go.mod b/go.mod index 6911d34..dcd4824 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module gospeak-live-sfu-demo +module sync-live go 1.25.0 @@ -12,14 +12,39 @@ require ( ) require ( + cloud.google.com/go/compute/metadata v0.9.0 // indirect + github.com/aarondl/authboss/v3 v3.5.3 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect github.com/casbin/govaluate v1.3.0 // indirect + github.com/dghubble/oauth1 v0.7.3 // indirect + github.com/friendsofgo/errors v0.9.2 // indirect + github.com/go-oauth2/oauth2/v4 v4.5.4 // indirect + github.com/go-pkgz/auth v1.26.0 // indirect + github.com/go-pkgz/repeater/v2 v2.2.0 // indirect + github.com/go-pkgz/rest v1.24.0 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang/snappy v1.0.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.19.2 // indirect github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect + github.com/markbates/goth v1.82.0 // indirect + github.com/montanaflynn/stats v0.12.4 // indirect + github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d // indirect + github.com/wneessen/go-mail v0.8.1 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.2.0 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + go.etcd.io/bbolt v1.5.0 // indirect + go.mongodb.org/mongo-driver v1.17.9 // indirect golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect + golang.org/x/image v0.45.0 // indirect golang.org/x/net v0.57.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.41.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect ) diff --git a/go.sum b/go.sum index 50724f6..0ef9580 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/aarondl/authboss/v3 v3.5.3 h1:eF2G85mGjADsRQnBekMSsgqvW5Naec6kew5B0PRT1nE= +github.com/aarondl/authboss/v3 v3.5.3/go.mod h1:Q/t4SEfVVI+i/JPVuB6gB5LBSkqBnsf4JezLekg5X/g= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= @@ -8,26 +12,65 @@ github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9my github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dghubble/oauth1 v0.7.3 h1:EkEM/zMDMp3zOsX2DC/ZQ2vnEX3ELK0/l9kb+vs4ptE= +github.com/dghubble/oauth1 v0.7.3/go.mod h1:oxTe+az9NSMIucDPDCCtzJGsPhciJV33xocHfcR2sVY= +github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzjtgk= +github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-oauth2/oauth2/v4 v4.5.4 h1:YjI0tmGW8oxVhn9QSBIxlr641QugWrJY5UWa6XmLcW0= +github.com/go-oauth2/oauth2/v4 v4.5.4/go.mod h1:BXiOY+QZtZy2ewbsGk2B5P8TWmtz/Rf7ES5ZttQFxfQ= +github.com/go-pkgz/auth v1.26.0 h1:2CL91EO8ntGWcHkXj2AXDx2NUfZ//qgo+DTX6ZLyffM= +github.com/go-pkgz/auth v1.26.0/go.mod h1:z4stIOtrnxMumDchQ3BP7sDOP36Qyq7+xgT9+E/E+/c= +github.com/go-pkgz/repeater/v2 v2.2.0 h1:8nZR/NaknmLfx2YMHbr78u9OL4Xj+8+romm9dz4FpMg= +github.com/go-pkgz/repeater/v2 v2.2.0/go.mod h1:RgX5vUbLKq7PV82QUDP5pFbQS1os4Z+U9XzKymK23A8= +github.com/go-pkgz/rest v1.24.0 h1:GAUCgx7U8xCOC2OynLjhCRMhtnMQH4d1mTdKpQyX2yI= +github.com/go-pkgz/rest v1.24.0/go.mod h1:dl3EWiuFB4hRTo2Sknj6UrQGFRAYvANK6/NyW8qQPxc= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8= +github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM= github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM= +github.com/markbates/goth v1.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ= +github.com/markbates/goth v1.82.0/go.mod h1:/DRlcq0pyqkKToyZjsL2KgiA1zbF1HIjE7u2uC79rUk= +github.com/montanaflynn/stats v0.12.4 h1:amtNRsti20yIhcrkfUJGwoYqBR82jKQFE8SNNYVgGn0= +github.com/montanaflynn/stats v0.12.4/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d h1:l3+2LWCbVxn5itfvXAfH9n4YL9jh8l1g5zcncbIc1cs= +github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d/go.mod h1:TbpErkob6SY7cyozRVSGoB3OlO2qOAgVN8O3KAJ4fMI= github.com/tursodatabase/go-libsql v0.0.0-20260424063416-3051e37e6e04 h1:9nlqEMruvXDPynGbZ0RE67kKnkkg3NdnjGccvRABefc= github.com/tursodatabase/go-libsql v0.0.0-20260424063416-3051e37e6e04/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8= +github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pcM= +github.com/wneessen/go-mail v0.8.1/go.mod h1:dWZ61zadzCIyvB4y1/YzC5O7MrbbzBfPkARmbosdf8w= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= +github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU= +go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk= +go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= @@ -41,23 +84,50 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5w go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0= +golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= diff --git a/internal/auth/auth.go b/internal/auth/auth.go index f750063..f95b2fb 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -2,6 +2,7 @@ package auth import ( "fmt" + "os" "time" ) @@ -16,6 +17,17 @@ func NewManager(jwtSecret string, jwtTTL time.Duration, modelPath, policyPath, u if err != nil { return nil, fmt.Errorf("init user store: %w", err) } + if bu, bp := os.Getenv("BOOTSTRAP_ADMIN_USER"), os.Getenv("BOOTSTRAP_ADMIN_PASS"); bu != "" && bp != "" { + if len(store.List()) == 0 { + if err := store.BootstrapAdmin(bu, bp); err != nil { + return nil, fmt.Errorf("bootstrap admin: %w", err) + } + // 若配置为 root 引导,则提升首个用户为 root(对标 SyncTV Root 超管) + if br := os.Getenv("BOOTSTRAP_ADMIN_ROLE"); br == RoleRoot { + _ = store.UpdateRole(bu, RoleRoot) + } + } + } jwtMgr := NewJWTManager(jwtSecret, jwtTTL) enc, err := NewEnforcer(modelPath, policyPath, store) if err != nil { @@ -38,19 +50,44 @@ func (m *Manager) Login(username, password string) (string, *User, error) { if !ok { return "", nil, fmt.Errorf("invalid credentials") } + if u.Status == StatusBanned { + return "", nil, fmt.Errorf("user is banned") + } _ = m.Enforcer.AddUserRole(u.Username, u.Role) token, err := m.JWT.Sign(u.Username, u.Role) if err != nil { return "", nil, err } - ret := &User{Username: u.Username, Role: u.Role, CreatedAt: u.CreatedAt} + ret := &User{Username: u.Username, Role: u.Role, Status: u.Status, CreatedAt: u.CreatedAt} return token, ret, nil } +func (m *Manager) LoginWithRefresh(username, password string) (access, refresh string, user *User, err error) { + u, ok := m.Store.Verify(username, password) + if !ok { + return "", "", nil, fmt.Errorf("invalid credentials") + } + if u.Status == StatusBanned { + return "", "", nil, fmt.Errorf("user is banned") + } + _ = m.Enforcer.AddUserRole(u.Username, u.Role) + access, refresh, err = m.JWT.IssuePair(u.Username, u.Role) + if err != nil { + return "", "", nil, err + } + ret := &User{Username: u.Username, Role: u.Role, Status: u.Status, CreatedAt: u.CreatedAt} + return access, refresh, ret, nil +} + func (m *Manager) Register(username, password, role string) (string, *User, error) { if role == "" { role = RoleViewer } + role = NormalizeRole(role) + // 非 root/admin 不可自行注册高权限角色 + if role == RoleRoot { + return "", nil, fmt.Errorf("cannot register as root") + } u, err := m.Store.Create(username, password, role) if err != nil { return "", nil, err @@ -69,19 +106,51 @@ func (m *Manager) VerifyToken(token string) (*Claims, *User, error) { return nil, nil, err } if u, ok := m.Store.Get(claims.Username); ok { + if u.Status == StatusBanned { + return nil, nil, fmt.Errorf("user is banned") + } claims.Role = u.Role - return claims, &User{Username: u.Username, Role: u.Role, CreatedAt: u.CreatedAt}, nil + return claims, &User{Username: u.Username, Role: u.Role, Status: u.Status, CreatedAt: u.CreatedAt}, nil } - return claims, &User{Username: claims.Username, Role: claims.Role}, nil + return claims, &User{Username: claims.Username, Role: claims.Role, Status: StatusActive}, nil +} + +func (m *Manager) RefreshAccess(refreshToken string) (string, *Claims, error) { + claims, err := m.JWT.VerifyRefresh(refreshToken) + if err != nil { + return "", nil, fmt.Errorf("invalid refresh token: %w", err) + } + if u, ok := m.Store.Get(claims.Username); ok { + if u.Status == StatusBanned { + return "", nil, fmt.Errorf("user is banned") + } + claims.Role = u.Role + } + newAccess, err := m.JWT.Sign(claims.Username, claims.Role) + if err != nil { + return "", nil, err + } + newClaims, err := m.JWT.Verify(newAccess) + if err != nil { + return "", nil, err + } + return newAccess, newClaims, nil } func (m *Manager) Check(username, role, obj, act string) (bool, error) { if role == "" { if u, ok := m.Store.Get(username); ok { + if u.Status == StatusBanned { + return false, nil + } role = u.Role } else { role = RoleGuest } + } else { + if u, ok := m.Store.Get(username); ok && u.Status == StatusBanned { + return false, nil + } } sub := username if sub == "" { @@ -91,9 +160,73 @@ func (m *Manager) Check(username, role, obj, act string) (bool, error) { } func (m *Manager) UpdateUserRole(targetUser, newRole string) error { + return m.UpdateUserRoleAs("", targetUser, newRole) +} + +func (m *Manager) UpdateUserRoleAs(actor, targetUser, newRole string) error { + newRole = NormalizeRole(newRole) + if newRole == RoleRoot && actor != targetUser { + // 仅 root 可授予 root,对标 SyncTV can_manage(Root, _) + if actorRole := m.getRole(actor); actorRole != RoleRoot { + return fmt.Errorf("only root can grant root role") + } + } + if !isValidRole(newRole) { + return fmt.Errorf("invalid role %q", newRole) + } + actorRole := m.getRole(actor) + targetRole := m.getRole(targetUser) + if actor != "" && !CanManage(actorRole, newRole) && actorRole != RoleRoot { + // Admin 只能管理 viewer/publisher,不能提权为 admin/root + if targetRole == newRole { + return fmt.Errorf("permission denied: %s cannot manage %s", actorRole, newRole) + } + if !CanManage(actorRole, targetRole) { + return fmt.Errorf("permission denied: %s cannot manage %s", actorRole, targetRole) + } + if newRole == RoleAdmin || newRole == RoleRoot { + return fmt.Errorf("permission denied: %s cannot grant %s", actorRole, newRole) + } + } if err := m.Store.UpdateRole(targetUser, newRole); err != nil { return err } return m.Enforcer.AddUserRole(targetUser, newRole) } +func (m *Manager) UpdateUserRoleSimple(targetUser, newRole string) error { + return m.UpdateUserRoleAs("", targetUser, newRole) +} + +func (m *Manager) BanUser(actor, target string) error { + actorRole := m.getRole(actor) + if actorRole != RoleRoot && actorRole != RoleAdmin { + return fmt.Errorf("permission denied: ban requires admin") + } + targetRole := m.getRole(target) + if targetRole == RoleRoot { + return fmt.Errorf("cannot ban root") + } + if actorRole == RoleAdmin && targetRole == RoleAdmin && actor != target { + return fmt.Errorf("admin cannot ban another admin") + } + return m.Store.BanUser(target) +} + +func (m *Manager) UnbanUser(actor, target string) error { + actorRole := m.getRole(actor) + if actorRole != RoleRoot && actorRole != RoleAdmin { + return fmt.Errorf("permission denied: unban requires admin") + } + return m.Store.UnbanUser(target) +} + +func (m *Manager) getRole(username string) string { + if username == "" { + return RoleGuest + } + if u, ok := m.Store.Get(username); ok { + return u.Role + } + return RoleGuest +} diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 7a67282..843bd19 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -10,8 +10,15 @@ import ( func TestUserStore(t *testing.T) { store, _ := NewUserStore("") + // 正式运行不再内置种子账号,用户需显式创建。 + if _, ok := store.Get("admin"); ok { + t.Fatal("no seed admin should exist") + } + if _, err := store.Create("admin", "Admin123!", "admin"); err != nil { + t.Fatalf("create admin: %v", err) + } if _, ok := store.Get("admin"); !ok { - t.Fatal("admin should exist") + t.Fatal("admin should exist after create") } _, ok := store.Verify("admin", "Admin123!") if !ok { @@ -101,6 +108,7 @@ func TestAuthManagerLogin(t *testing.T) { defer os.Remove(tmpPol) store, _ := NewUserStore("") + _, _ = store.Create("admin", "Admin123!", "admin") enc, _ := NewEnforcer("model.conf", tmpPol, store) mgr := NewManagerWithStore(store, "test-jwt", time.Hour, enc) @@ -134,6 +142,8 @@ func TestHTTPMiddleware(t *testing.T) { defer os.Remove(tmpPol) store, _ := NewUserStore("") + _, _ = store.Create("viewer", "Viewer123!", "viewer") + _, _ = store.Create("publisher", "Publisher123!", "publisher") enc, _ := NewEnforcer("model.conf", tmpPol, store) mgr := NewManagerWithStore(store, "test-jwt", time.Hour, enc) diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go index 25d2b21..1b5b32b 100644 --- a/internal/auth/jwt.go +++ b/internal/auth/jwt.go @@ -10,13 +10,15 @@ import ( type Claims struct { Username string `json:"username"` Role string `json:"role"` + TokenType string `json:"token_type,omitempty"` // access / refresh,兼容 SyncTV 双令牌模型 jwt.RegisteredClaims } type JWTManager struct { - secret []byte - ttl time.Duration - issuer string + secret []byte + ttl time.Duration + refreshTTL time.Duration + issuer string } func NewJWTManager(secret string, ttl time.Duration) *JWTManager { @@ -26,20 +28,54 @@ func NewJWTManager(secret string, ttl time.Duration) *JWTManager { if ttl == 0 { ttl = 2 * time.Hour } - return &JWTManager{secret: []byte(secret), ttl: ttl, issuer: "live-sfu-demo"} + return &JWTManager{secret: []byte(secret), ttl: ttl, refreshTTL: 7 * 24 * time.Hour, issuer: "sync-live"} +} + +func NewJWTManagerWithRefresh(secret string, ttl, refreshTTL time.Duration) *JWTManager { + if secret == "" { + secret = "insecure-jwt-secret-change-me" + } + if ttl == 0 { + ttl = 2 * time.Hour + } + if refreshTTL == 0 { + refreshTTL = 7 * 24 * time.Hour + } + return &JWTManager{secret: []byte(secret), ttl: ttl, refreshTTL: refreshTTL, issuer: "sync-live"} } func (j *JWTManager) Sign(username, role string) (string, error) { now := time.Now() claims := Claims{ - Username: username, - Role: role, + Username: username, + Role: role, + TokenType: "access", RegisteredClaims: jwt.RegisteredClaims{ Issuer: j.issuer, Subject: username, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(j.ttl)), NotBefore: jwt.NewNumericDate(now), + ID: username + "-access-" + now.Format(time.RFC3339Nano), + }, + } + tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return tok.SignedString(j.secret) +} + +func (j *JWTManager) SignRefresh(username, role string) (string, error) { + now := time.Now() + claims := Claims{ + Username: username, + Role: role, + TokenType: "refresh", + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: j.issuer, + Subject: username, + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(j.refreshTTL)), + NotBefore: jwt.NewNumericDate(now), + ID: username + "-refresh-" + now.Format(time.RFC3339Nano), }, } tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) @@ -66,5 +102,40 @@ func (j *JWTManager) Verify(tokenStr string) (*Claims, error) { return claims, nil } -func (j *JWTManager) TTL() time.Duration { return j.ttl } +func (j *JWTManager) VerifyRefresh(tokenStr string) (*Claims, error) { + claims, err := j.Verify(tokenStr) + if err != nil { + return nil, err + } + if claims.TokenType != "" && claims.TokenType != "refresh" { + return nil, errors.New("not a refresh token") + } + return claims, nil +} +func (j *JWTManager) Refresh(refreshToken string) (string, *Claims, error) { + claims, err := j.VerifyRefresh(refreshToken) + if err != nil { + return "", nil, err + } + newAccess, err := j.Sign(claims.Username, claims.Role) + if err != nil { + return "", nil, err + } + newClaims, _ := j.Verify(newAccess) + return newAccess, newClaims, nil +} + +func (j *JWTManager) TTL() time.Duration { return j.ttl } +func (j *JWTManager) RefreshTTL() time.Duration { return j.refreshTTL } +func (j *JWTManager) IssuePair(username, role string) (access, refresh string, err error) { + access, err = j.Sign(username, role) + if err != nil { + return "", "", err + } + refresh, err = j.SignRefresh(username, role) + if err != nil { + return "", "", err + } + return access, refresh, nil +} diff --git a/internal/auth/policy.csv b/internal/auth/policy.csv index 73e766f..e6321ac 100644 --- a/internal/auth/policy.csv +++ b/internal/auth/policy.csv @@ -1,35 +1,47 @@ -# Casbin RBAC 策略 -# p, 角色, 资源, 动作 +p, root, config, read +p, root, room, list +p, root, room, publish +p, root, room, subscribe +p, root, room, stop +p, root, room, watch +p, root, room, chat +p, root, room, manage +p, root, room, delete +p, root, user, list +p, root, user, manage +p, root, srs, streams +p, root, system, manage p, admin, config, read p, admin, room, list p, admin, room, publish p, admin, room, subscribe p, admin, room, stop p, admin, room, watch +p, admin, room, chat +p, admin, room, manage p, admin, user, list p, admin, user, manage p, admin, srs, streams - p, publisher, config, read p, publisher, room, list p, publisher, room, publish p, publisher, room, subscribe p, publisher, room, stop p, publisher, room, watch +p, publisher, room, chat p, publisher, srs, streams - p, viewer, config, read p, viewer, room, list p, viewer, room, subscribe p, viewer, room, watch +p, viewer, room, chat p, viewer, srs, streams - p, guest, config, read p, guest, srs, streams - -# 默认 g 关系由代码动态维护,亦可在此预设示例用户 -p, admin, room, chat -p, publisher, room, chat -p, viewer, room, chat p, guest, room, chat -# g, alice, admin +g, admin, admin +g, root, root +g, publisher, publisher +g, viewer, viewer +g, guest, guest +g, alice, viewer \ No newline at end of file diff --git a/internal/auth/store.go b/internal/auth/store.go index 1a634c5..d2bc84a 100644 --- a/internal/auth/store.go +++ b/internal/auth/store.go @@ -11,23 +11,21 @@ import ( "golang.org/x/crypto/bcrypt" ) -// User 表示系统用户 type User struct { Username string `json:"username"` Password string `json:"-"` // bcrypt hash,不序列化到前端 Hash string `json:"hash,omitempty"` // 持久化用 Role string `json:"role"` + Status string `json:"status,omitempty"` // active / banned,对标 SyncTV UserStatus CreatedAt int64 `json:"created_at"` } -// UserStore 负责用户持久化与校验,内存为主,可选落盘到 JSON 文件 type UserStore struct { mu sync.RWMutex users map[string]*User filePath string } -// NewUserStore 创建用户存储,若 filePath 非空则尝试从文件加载;若文件不存在则预置种子用户 func NewUserStore(filePath string) (*UserStore, error) { s := &UserStore{ users: make(map[string]*User), @@ -38,38 +36,27 @@ func NewUserStore(filePath string) (*UserStore, error) { if !os.IsNotExist(err) { return nil, fmt.Errorf("load users: %w", err) } - s.seed() - _ = s.save() - } else if len(s.users) == 0 { - s.seed() - _ = s.save() } - } else { - s.seed() } return s, nil } -func (s *UserStore) seed() { - seeds := []struct { - username string - password string - role string - }{ - {"admin", "Admin123!", RoleAdmin}, - {"publisher", "Publisher123!", RolePublisher}, - {"viewer", "Viewer123!", RoleViewer}, +func (s *UserStore) BootstrapAdmin(username, password string) error { + if username == "" || password == "" { + return fmt.Errorf("bootstrap admin requires user and password") } - for _, u := range seeds { - hash, _ := bcrypt.GenerateFromPassword([]byte(u.password), bcrypt.DefaultCost) - s.users[u.username] = &User{ - Username: u.username, - Password: string(hash), - Hash: string(hash), - Role: u.role, - CreatedAt: time.Now().Unix(), - } + if len(password) < 8 { + return fmt.Errorf("bootstrap admin password too short (min 8)") } + if _, exists := s.users[username]; exists { + return nil + } + u, err := s.Create(username, password, RoleAdmin) + if err != nil { + return err + } + _ = u + return nil } func (s *UserStore) Get(username string) (*User, bool) { @@ -88,7 +75,7 @@ func (s *UserStore) List() []*User { defer s.mu.RUnlock() out := make([]*User, 0, len(s.users)) for _, u := range s.users { - out = append(out, &User{Username: u.Username, Role: u.Role, CreatedAt: u.CreatedAt}) + out = append(out, &User{Username: u.Username, Role: u.Role, Status: u.Status, CreatedAt: u.CreatedAt}) } return out } @@ -120,6 +107,7 @@ func (s *UserStore) Create(username, password, role string) (*User, error) { Password: string(hash), Hash: string(hash), Role: role, + Status: StatusActive, CreatedAt: time.Now().Unix(), } s.users[username] = u @@ -137,6 +125,9 @@ func (s *UserStore) Verify(username, password string) (*User, bool) { if !ok { return nil, false } + if u.Status == StatusBanned { + return nil, false + } if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)); err != nil { return nil, false } @@ -154,10 +145,45 @@ func (s *UserStore) UpdateRole(username, role string) error { if !ok { return fmt.Errorf("user %q not found", username) } + if u.Status == StatusBanned { + return fmt.Errorf("user %q is banned", username) + } u.Role = role return s.saveLocked() } +func (s *UserStore) BanUser(username string) error { + s.mu.Lock() + defer s.mu.Unlock() + u, ok := s.users[username] + if !ok { + return fmt.Errorf("user %q not found", username) + } + u.Status = StatusBanned + return s.saveLocked() +} + +func (s *UserStore) UnbanUser(username string) error { + s.mu.Lock() + defer s.mu.Unlock() + u, ok := s.users[username] + if !ok { + return fmt.Errorf("user %q not found", username) + } + u.Status = StatusActive + return s.saveLocked() +} + +func (s *UserStore) IsBanned(username string) bool { + s.mu.RLock() + defer s.mu.RUnlock() + u, ok := s.users[username] + if !ok { + return false + } + return u.Status == StatusBanned +} + func (s *UserStore) load() error { data, err := os.ReadFile(s.filePath) if err != nil { @@ -174,6 +200,12 @@ func (s *UserStore) load() error { if u.Hash == "" && u.Password != "" { u.Hash = u.Password } + if u.Status == "" { + u.Status = StatusActive + } + if u.Status != StatusBanned { + u.Status = StatusActive + } s.users[u.Username] = u } return nil @@ -197,6 +229,9 @@ func (s *UserStore) saveLocked() error { if u.Hash == "" { u.Hash = u.Password } + if u.Status == "" { + u.Status = StatusActive + } list = append(list, u) } data, _ := json.MarshalIndent(list, "", " ") @@ -204,19 +239,46 @@ func (s *UserStore) saveLocked() error { } const ( + RoleRoot = "root" RoleAdmin = "admin" RolePublisher = "publisher" RoleViewer = "viewer" RoleGuest = "guest" + StatusActive = "active" + StatusBanned = "banned" ) func isValidRole(r string) bool { switch r { - case RoleAdmin, RolePublisher, RoleViewer: + case RoleRoot, RoleAdmin, RolePublisher, RoleViewer: return true default: return false } } -func AllRoles() []string { return []string{RoleAdmin, RolePublisher, RoleViewer} } +func AllRoles() []string { return []string{RoleRoot, RoleAdmin, RolePublisher, RoleViewer} } + +func IsAdminOrAbove(role string) bool { return role == RoleRoot || role == RoleAdmin } + +func CanManage(actorRole, targetRole string) bool { + switch actorRole { + case RoleRoot: + return true + case RoleAdmin: + return targetRole == RoleViewer || targetRole == RolePublisher + default: + return false + } +} + +func NormalizeRole(r string) string { + switch r { + case RoleRoot, RoleAdmin, RolePublisher, RoleViewer: + return r + case "user": + return RoleViewer + default: + return RoleViewer + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 679ab73..dee34cd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,14 +1,15 @@ package config import ( + "fmt" "os" "strings" "time" ) -// Config 承载直播 SFU 分流 Demo 的运行参数。 +// Config 承载直播 SFU 分流服务的运行参数。 // 优先级:环境变量 > 默认值。Cloudflare Realtime 为主 SFU,SRS 为本地对照后端。 -// 默认 DB 使用 Turso 嵌入式 (libSQL file) 持久化房间拓扑;同时支持 JWT/Casbin 登录。 +// 默认 DB 使用 libSQL 嵌入式 (file) 持久化房间拓扑;同时支持 JWT/Casbin 登录。 type Config struct { HTTPPort string GRPCPort string @@ -25,18 +26,46 @@ type Config struct { CFBaseURL string CFStunURL string - TokenSecret string + TokenSecret string // 推流 JWT 签名密钥(SRS 入口鉴权) TokenRequired bool - JWTSecret string - JWTTTL time.Duration - AuthModel string - AuthPolicy string - AuthUserFile string + JWTSecret string + JWTTTL time.Duration + AuthModel string + AuthPolicy string + AuthUserFile string AllowRegister bool - DatabaseURL string // TURSO_DATABASE_URL 或 DATABASE_URL,默认 file:./data/live-sfu.db - AuthToken string // 兼容远程 Turso,嵌入模式可为空 + SMTPHost string + SMTPPort int + SMTPUser string + SMTPPass string + SMTPFrom string + SMTPFromName string + SMTPTLS bool + SMTPEnabled bool + + EmailVerifyRequired bool + EmailVerifyTTL time.Duration + EmailVerifyBaseURL string + + OAuthEnabled bool + OAuthCallbackBase string + OAuthGoogleID string + OAuthGoogleSecret string + OAuthGithubID string + OAuthGithubSecret string + OAuthGitlabID string + OAuthGitlabSecret string + OAuthGenericID string + OAuthGenericSecret string + OAuthGenericURL string + OAuthGenericName string + + AuthBossEnabled bool + + DatabaseURL string // 嵌入式 DB DSN(TURSO_DATABASE_URL / DATABASE_URL),默认 file:./data/sync-live.db + AuthToken string // 远程 libSQL 凭证,嵌入模式可为空 } func Load() *Config { @@ -56,7 +85,7 @@ func Load() *Config { CFBaseURL: getenv("CF_BASE_URL", "https://rtc.live.cloudflare.com/v1"), CFStunURL: getenv("CF_STUN_URL", "stun:stun.cloudflare.com:3478"), - TokenSecret: getenv("DEMO_TOKEN_SECRET", ""), + TokenSecret: getenv("SFU_TOKEN_SECRET", ""), TokenRequired: getenv("SFU_TOKEN_REQUIRED", "0") == "1", JWTSecret: getenv("JWT_SECRET", ""), @@ -66,19 +95,42 @@ func Load() *Config { AuthUserFile: getenv("AUTH_USER_FILE", "data/users.json"), AllowRegister: getenv("ALLOW_REGISTER", "1") == "1", - DatabaseURL: getenv("TURSO_DATABASE_URL", "file:./data/live-sfu.db?cache=shared&_journal_mode=WAL"), + SMTPHost: getenv("SMTP_HOST", ""), + SMTPPort: parseInt(getenv("SMTP_PORT", "587"), 587), + SMTPUser: getenv("SMTP_USER", ""), + SMTPPass: getenv("SMTP_PASS", ""), + SMTPFrom: getenv("SMTP_FROM", ""), + SMTPFromName: getenv("SMTP_FROM_NAME", "SyncLive"), + SMTPTLS: getenv("SMTP_TLS", "1") == "1", + SMTPEnabled: getenv("SMTP_ENABLED", "0") == "1", + + EmailVerifyRequired: getenv("EMAIL_VERIFY_REQUIRED", "0") == "1", + EmailVerifyTTL: parseDuration(getenv("EMAIL_VERIFY_TTL", "24h"), 24*time.Hour), + EmailVerifyBaseURL: getenv("EMAIL_VERIFY_BASE_URL", ""), + + OAuthEnabled: getenv("OAUTH_ENABLED", "0") == "1", + OAuthCallbackBase: getenv("OAUTH_CALLBACK_BASE", ""), + OAuthGoogleID: getenv("OAUTH_GOOGLE_CLIENT_ID", ""), + OAuthGoogleSecret: getenv("OAUTH_GOOGLE_CLIENT_SECRET", ""), + OAuthGithubID: getenv("OAUTH_GITHUB_CLIENT_ID", ""), + OAuthGithubSecret: getenv("OAUTH_GITHUB_CLIENT_SECRET", ""), + OAuthGitlabID: getenv("OAUTH_GITLAB_CLIENT_ID", ""), + OAuthGitlabSecret: getenv("OAUTH_GITLAB_CLIENT_SECRET", ""), + OAuthGenericID: getenv("OAUTH_GENERIC_CLIENT_ID", ""), + OAuthGenericSecret: getenv("OAUTH_GENERIC_CLIENT_SECRET", ""), + OAuthGenericURL: getenv("OAUTH_GENERIC_URL", ""), + OAuthGenericName: getenv("OAUTH_GENERIC_NAME", "generic"), + + AuthBossEnabled: getenv("AUTHBOSS_ENABLED", "0") == "1", + + DatabaseURL: getenv("TURSO_DATABASE_URL", "file:./data/sync-live.db?cache=shared&_journal_mode=WAL"), AuthToken: getenv("TURSO_AUTH_TOKEN", ""), } - if c.TokenSecret == "" { - c.TokenSecret = "insecure-demo-secret-change-me" - } + // JWT 密钥优先使用专用配置;否则回退到推流密钥(两者都应显式配置)。 if c.JWTSecret == "" { c.JWTSecret = c.TokenSecret - if c.JWTSecret == "" { - c.JWTSecret = "insecure-jwt-secret-change-me" - } } - if c.DatabaseURL == "file:./data/live-sfu.db?cache=shared&_journal_mode=WAL" { + if c.DatabaseURL == "file:./data/sync-live.db?cache=shared&_journal_mode=WAL" { if v := os.Getenv("DATABASE_URL"); v != "" { c.DatabaseURL = v } else if v := os.Getenv("LIBSQL_URL"); v != "" { @@ -92,9 +144,30 @@ func Load() *Config { c.AuthToken = v } } + if c.SMTPFrom == "" && c.SMTPUser != "" { + c.SMTPFrom = c.SMTPUser + } + if c.EmailVerifyBaseURL == "" { + c.EmailVerifyBaseURL = "http://localhost:" + c.HTTPPort + } + if c.OAuthCallbackBase == "" { + c.OAuthCallbackBase = "http://localhost:" + c.HTTPPort + } return c } +// Validate 校验正式运行所需的敏感配置,缺省会返回错误,避免误以不安全默认密钥上线。 +func (c *Config) Validate() error { + if c.JWTSecret == "" { + return fmt.Errorf("JWT_SECRET 必须显式配置,禁止依赖不安全默认密钥") + } + if c.TokenRequired && c.TokenSecret == "" { + return fmt.Errorf("已启用 SFU_TOKEN_REQUIRED,但 SFU_TOKEN_SECRET 未配置") + } + return nil +} + + func getenv(k, def string) string { if v := os.Getenv(k); v != "" { return v @@ -112,6 +185,17 @@ func parseDuration(s string, def time.Duration) time.Duration { return def } +func parseInt(s string, def int) int { + if s == "" { + return def + } + var v int + if _, err := fmt.Sscanf(s, "%d", &v); err == nil { + return v + } + return def +} + func (c *Config) ProviderList() []string { parts := strings.Split(c.ProviderOrder, ",") out := make([]string, 0, len(parts)) @@ -151,3 +235,11 @@ func (c *Config) DSN() string { func (c *Config) IsRemoteTurso() bool { return strings.HasPrefix(c.DatabaseURL, "libsql://") || strings.HasPrefix(c.DatabaseURL, "https://") } + +func (c *Config) SMTPConfigured() bool { + return c.SMTPHost != "" && c.SMTPUser != "" && c.SMTPPass != "" +} + +func (c *Config) OAuthConfigured() bool { + return c.OAuthGoogleID != "" || c.OAuthGithubID != "" || c.OAuthGitlabID != "" +} diff --git a/internal/db/db.go b/internal/db/db.go index ca745b0..c708d7c 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -11,18 +11,18 @@ import ( "time" _ "github.com/tursodatabase/go-libsql" - "gospeak-live-sfu-demo/gen" + "sync-live/gen" ) -// Open 打开 Turso 嵌入式数据库(libSQL file)。 -// 仅支持嵌入模式:file:./data/live-sfu.db / :memory: / file::memory: +// Open 打开嵌入式 libSQL 数据库(file 模式)。 +// 仅支持嵌入模式:file:./data/sync-live.db / :memory: / file::memory: // 已禁用远程 libsql://,如需远程请另行扩展。 func Open(dsn string) (*sql.DB, error) { if strings.HasPrefix(dsn, "libsql://") || strings.HasPrefix(dsn, "https://") { - return nil, fmt.Errorf("embedded mode only: remote Turso DSN not supported (%q), use file: DSN", dsn) + return nil, fmt.Errorf("embedded mode only: remote libSQL DSN not supported (%q), use file: DSN", dsn) } if dsn == "" { - dsn = "file:./data/live-sfu.db?cache=shared&_journal_mode=WAL" + dsn = "file:./data/sync-live.db?cache=shared&_journal_mode=WAL" } // 确保本地文件目录存在 if strings.HasPrefix(dsn, "file:") { @@ -55,7 +55,7 @@ func Open(dsn string) (*sql.DB, error) { if err := Migrate(db); err != nil { return nil, fmt.Errorf("migrate: %w", err) } - log.Printf("[db] turso embedded opened: %s", dsn) + log.Printf("[db] embedded libsql opened: %s", dsn) return db, nil } @@ -78,12 +78,102 @@ func Migrate(db *sql.DB) error { created_at INTEGER, updated_at INTEGER )`, + `CREATE TABLE IF NOT EXISTS rooms_v2 ( + name TEXT PRIMARY KEY, + display_name TEXT, + description TEXT, + creator TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'active', + is_public INTEGER NOT NULL DEFAULT 1, + password_hash TEXT DEFAULT '', + require_approval INTEGER NOT NULL DEFAULT 0, + max_members INTEGER NOT NULL DEFAULT 100, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + closed_at INTEGER, + settings_json TEXT, + version INTEGER NOT NULL DEFAULT 1 + )`, + `CREATE TABLE IF NOT EXISTS room_members ( + room TEXT NOT NULL, + user_id TEXT NOT NULL, + username TEXT NOT NULL, + role INTEGER NOT NULL, + status INTEGER NOT NULL DEFAULT 1, + added_permissions INTEGER NOT NULL DEFAULT 0, + removed_permissions INTEGER NOT NULL DEFAULT 0, + admin_added_permissions INTEGER NOT NULL DEFAULT 0, + admin_removed_permissions INTEGER NOT NULL DEFAULT 0, + joined_at INTEGER NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + PRIMARY KEY (room, user_id) + )`, + `CREATE INDEX IF NOT EXISTS idx_room_members_user ON room_members(user_id)`, + `CREATE INDEX IF NOT EXISTS idx_room_members_room_role ON room_members(room, role)`, + `CREATE TABLE IF NOT EXISTS room_bans ( + room TEXT PRIMARY KEY, + reason TEXT, + banned_at INTEGER, + banned_by TEXT + )`, + `CREATE TABLE IF NOT EXISTS room_join_requests ( + room TEXT NOT NULL, + user_id TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + created_at INTEGER NOT NULL, + PRIMARY KEY (room, user_id) + )`, + `CREATE TABLE IF NOT EXISTS room_categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + description TEXT DEFAULT '', + sort_order INTEGER NOT NULL DEFAULT 0, + is_enabled INTEGER NOT NULL DEFAULT 1, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS room_labels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + description TEXT DEFAULT '', + color TEXT DEFAULT '', + category_id INTEGER REFERENCES room_categories(id) ON DELETE SET NULL, + sort_order INTEGER NOT NULL DEFAULT 0, + is_enabled INTEGER NOT NULL DEFAULT 1, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS user_bans ( + username TEXT PRIMARY KEY, + reason TEXT, + banned_at INTEGER NOT NULL, + banned_by TEXT, + expires_at INTEGER + )`, + `CREATE TABLE IF NOT EXISTS user_registration_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + status TEXT NOT NULL DEFAULT 'pending', + created_at INTEGER NOT NULL, + reviewed_at INTEGER, + reviewed_by TEXT + )`, } for _, s := range stmts { if _, err := db.Exec(s); err != nil { return fmt.Errorf("exec %q: %w", s, err) } } + // 兼容旧库:尝试新增列(若已存在则忽略错误,对标 SyncTV 的增量迁移策略) + for _, alter := range []string{ + `ALTER TABLE rooms_v2 ADD COLUMN category_id INTEGER`, + `ALTER TABLE rooms_v2 ADD COLUMN cover_file_id TEXT`, + `ALTER TABLE rooms_v2 ADD COLUMN label_ids TEXT`, + } { + _, _ = db.Exec(alter) + } return nil } diff --git a/internal/room/member.go b/internal/room/member.go new file mode 100644 index 0000000..7fe81eb --- /dev/null +++ b/internal/room/member.go @@ -0,0 +1,95 @@ +package room + +import ( + "time" +) + +type RoomMember struct { + RoomName string `json:"room_name"` + RoomID string `json:"room_id"` + UserID string `json:"user_id"` + Username string `json:"username"` + Role RoomRole `json:"role"` + Status MemberStatus `json:"status"` + AddedPerms uint64 `json:"added_permissions"` + RemovedPerms uint64 `json:"removed_permissions"` + AdminAdded uint64 `json:"admin_added_permissions"` + AdminRemoved uint64 `json:"admin_removed_permissions"` + JoinedAt time.Time `json:"joined_at"` + Version int64 `json:"version"` +} + +func NewMember(room, username string, role RoomRole) *RoomMember { + return &RoomMember{ + RoomName: room, + Username: username, + UserID: username, + Role: role, + Status: MemberActive, + JoinedAt: time.Now(), + Version: 1, + } +} + +func (m *RoomMember) EffectivePermissions(settings *RoomSettings) PermissionSet { + if m.Role == RoleCreator { + return PermAll + } + base := m.basePermissions(settings) + switch m.Role { + case RoleAdmin: + res := base + res |= PermissionSet(m.AdminAdded) + res &^= PermissionSet(m.AdminRemoved) + return res + case RoleMember: + res := base + res |= PermissionSet(m.AddedPerms) + res &^= PermissionSet(m.RemovedPerms) + return res + case RoleGuest: + res := base & PermGuestAssignable + added := PermissionSet(m.AddedPerms) & PermGuestAssignable + removed := PermissionSet(m.RemovedPerms) & PermGuestAssignable + res |= added + res &^= removed + return res + default: + return base + } +} + +func (m *RoomMember) basePermissions(settings *RoomSettings) PermissionSet { + if settings == nil { + return DefaultForRole(m.Role) + } + switch m.Role { + case RoleAdmin: + return settings.AdminPermissions() + case RoleMember: + return settings.MemberPermissions() + case RoleGuest: + return settings.GuestPermissions() + case RoleCreator: + return PermAll + default: + return DefaultForRole(m.Role) + } +} + +func (m *RoomMember) HasPermission(p RoomPermission, settings *RoomSettings) bool { + return m.EffectivePermissions(settings).Has(p) +} + +func (m *RoomMember) CanGrant(target *RoomMember, perm RoomPermission, settings *RoomSettings) bool { + if !m.HasPermission(PermManagePermissions, settings) { + return false + } + if !m.Role.CanManage(target.Role) { + return false + } + if m.Role == RoleAdmin && perm == PermDeleteRoom { + return false + } + return true +} diff --git a/internal/room/permission.go b/internal/room/permission.go new file mode 100644 index 0000000..3e14a6e --- /dev/null +++ b/internal/room/permission.go @@ -0,0 +1,211 @@ +package room + +import ( + "fmt" + "sort" + "strings" +) + +type RoomPermission int + +const ( + PermSendChat RoomPermission = iota + PermViewChatHistory + PermViewMembers + PermSubscribe + PermWatchRoom + PermPublish + PermStopStream + PermBroadcast + PermManageMembers + PermAddMembers + PermRemoveMembers + PermManagePermissions + PermManageRoom + PermDeleteRoom + PermDeleteChatMessages + PermControlStream + _permCount +) + +func (p RoomPermission) Bit() uint64 { return 1 << uint(p) } +func (p RoomPermission) String() string { + switch p { + case PermSendChat: + return "send_chat" + case PermViewChatHistory: + return "view_chat_history" + case PermViewMembers: + return "view_members" + case PermSubscribe: + return "subscribe" + case PermWatchRoom: + return "watch_room" + case PermPublish: + return "publish" + case PermStopStream: + return "stop_stream" + case PermBroadcast: + return "broadcast" + case PermManageMembers: + return "manage_members" + case PermAddMembers: + return "add_members" + case PermRemoveMembers: + return "remove_members" + case PermManagePermissions: + return "manage_permissions" + case PermManageRoom: + return "manage_room" + case PermDeleteRoom: + return "delete_room" + case PermDeleteChatMessages: + return "delete_chat_messages" + case PermControlStream: + return "control_stream" + default: + return fmt.Sprintf("perm_%d", int(p)) + } +} + +func ParsePermission(s string) (RoomPermission, error) { + switch strings.ToLower(strings.TrimSpace(s)) { + case "send_chat", "sendchat", "chat", "room:chat": + return PermSendChat, nil + case "view_chat_history", "viewchathistory": + return PermViewChatHistory, nil + case "view_members", "viewmembers", "members:view": + return PermViewMembers, nil + case "subscribe", "room:subscribe": + return PermSubscribe, nil + case "watch", "watch_room", "room:watch": + return PermWatchRoom, nil + case "publish", "room:publish": + return PermPublish, nil + case "stop", "stop_stream", "room:stop": + return PermStopStream, nil + case "broadcast", "room:broadcast": + return PermBroadcast, nil + case "manage_members", "managemembers": + return PermManageMembers, nil + case "add_members", "addmembers": + return PermAddMembers, nil + case "remove_members", "removemembers", "kick": + return PermRemoveMembers, nil + case "manage_permissions", "managepermissions": + return PermManagePermissions, nil + case "manage_room", "manageroom", "room:manage": + return PermManageRoom, nil + case "delete_room", "deleteroom", "room:delete": + return PermDeleteRoom, nil + case "delete_chat_messages", "deletechat": + return PermDeleteChatMessages, nil + case "control_stream", "controlstream": + return PermControlStream, nil + default: + return 0, fmt.Errorf("unknown permission %q", s) + } +} + +type PermissionSet uint64 + +func (s PermissionSet) Has(p RoomPermission) bool { return uint64(s)&p.Bit() != 0 } +func (s PermissionSet) HasAll(o PermissionSet) bool { return uint64(s)&uint64(o) == uint64(o) } +func (s PermissionSet) HasAny(o PermissionSet) bool { return uint64(s)&uint64(o) != 0 } +func (s *PermissionSet) Grant(p RoomPermission) { *s |= PermissionSet(p.Bit()) } +func (s *PermissionSet) Revoke(p RoomPermission) { *s &^= PermissionSet(p.Bit()) } +func (s PermissionSet) Bits() uint64 { return uint64(s) } +func (s PermissionSet) List() []RoomPermission { + var out []RoomPermission + for i := 0; i < int(_permCount); i++ { + p := RoomPermission(i) + if s.Has(p) { + out = append(out, p) + } + } + return out +} +func (s PermissionSet) Names() []string { + list := s.List() + names := make([]string, len(list)) + for i, p := range list { + names[i] = p.String() + } + sort.Strings(names) + return names +} + +func (s PermissionSet) String() string { return strings.Join(s.Names(), ",") } + +func AllPermissions() PermissionSet { + var all PermissionSet + for i := 0; i < int(_permCount); i++ { + all.Grant(RoomPermission(i)) + } + return all +} + +var ( + PermAll = AllPermissions() + PermNone = PermissionSet(0) + + PermDefaultCreator = PermAll + PermDefaultAdmin = func() PermissionSet { + var s PermissionSet + s.Grant(PermSendChat) + s.Grant(PermViewChatHistory) + s.Grant(PermViewMembers) + s.Grant(PermSubscribe) + s.Grant(PermWatchRoom) + s.Grant(PermPublish) + s.Grant(PermStopStream) + s.Grant(PermBroadcast) + s.Grant(PermManageMembers) + s.Grant(PermAddMembers) + s.Grant(PermRemoveMembers) + s.Grant(PermManagePermissions) + s.Grant(PermManageRoom) + s.Grant(PermDeleteChatMessages) + s.Grant(PermControlStream) + return s + }() + PermDefaultMember = func() PermissionSet { + var s PermissionSet + s.Grant(PermSendChat) + s.Grant(PermViewChatHistory) + s.Grant(PermViewMembers) + s.Grant(PermSubscribe) + s.Grant(PermWatchRoom) + s.Grant(PermPublish) + s.Grant(PermStopStream) + return s + }() + PermDefaultGuest = func() PermissionSet { + var s PermissionSet + s.Grant(PermViewChatHistory) + s.Grant(PermViewMembers) + s.Grant(PermSubscribe) + s.Grant(PermWatchRoom) + return s + }() + PermGuestAssignable = PermDefaultGuest | func() PermissionSet { + var s PermissionSet + s.Grant(PermSendChat) + return s + }() +) + +func DefaultForRole(r RoomRole) PermissionSet { + switch r { + case RoleCreator: + return PermDefaultCreator + case RoleAdmin: + return PermDefaultAdmin + case RoleMember: + return PermDefaultMember + case RoleGuest: + return PermDefaultGuest + default: + return PermDefaultGuest + } +} diff --git a/internal/room/role.go b/internal/room/role.go new file mode 100644 index 0000000..d4dc8de --- /dev/null +++ b/internal/room/role.go @@ -0,0 +1,156 @@ +package room + +import ( + "encoding/json" + "fmt" + "strings" +) + +type RoomRole int16 + +const ( + RoleCreator RoomRole = 1 + RoleAdmin RoomRole = 2 + RoleMember RoomRole = 3 + RoleGuest RoomRole = 4 +) + +func (r RoomRole) String() string { + switch r { + case RoleCreator: + return "creator" + case RoleAdmin: + return "admin" + case RoleMember: + return "member" + case RoleGuest: + return "guest" + default: + return fmt.Sprintf("role_%d", int(r)) + } +} + +func ParseRoomRole(s string) (RoomRole, error) { + switch strings.ToLower(strings.TrimSpace(s)) { + case "creator", "owner": + return RoleCreator, nil + case "admin": + return RoleAdmin, nil + case "member", "publisher", "viewer": + return RoleMember, nil + case "guest": + return RoleGuest, nil + default: + return 0, fmt.Errorf("unknown role %q", s) + } +} + +func (r RoomRole) IsPrivileged() bool { return r == RoleCreator || r == RoleAdmin } +func (r RoomRole) Rank() int { + switch r { + case RoleCreator: + return 100 + case RoleAdmin: + return 80 + case RoleMember: + return 50 + case RoleGuest: + return 10 + default: + return 0 + } +} + +func (r RoomRole) MarshalJSON() ([]byte, error) { + return json.Marshal(r.String()) +} + +func (r *RoomRole) UnmarshalJSON(data []byte) error { + if len(data) == 0 || string(data) == "null" { + *r = 0 + return nil + } + var s string + if err := json.Unmarshal(data, &s); err == nil { + parsed, err := ParseRoomRole(s) + if err != nil { + return err + } + *r = parsed + return nil + } + var n int16 + if err := json.Unmarshal(data, &n); err == nil { + *r = RoomRole(n) + return nil + } + return fmt.Errorf("invalid RoomRole %s", string(data)) +} + +func (r RoomRole) CanManage(target RoomRole) bool { + if r == RoleCreator { + return true + } + if r == RoleAdmin && target != RoleCreator { + return true + } + return false +} + +type MemberStatus int16 + +const ( + MemberActive MemberStatus = 1 + MemberBanned MemberStatus = 2 + MemberPending MemberStatus = 3 + MemberKicked MemberStatus = 4 +) + + +func (s MemberStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +func (s *MemberStatus) UnmarshalJSON(data []byte) error { + if len(data) == 0 || string(data) == "null" { + *s = 0 + return nil + } + var str string + if err := json.Unmarshal(data, &str); err == nil { + switch strings.ToLower(strings.TrimSpace(str)) { + case "active": + *s = MemberActive + case "banned": + *s = MemberBanned + case "pending": + *s = MemberPending + case "kicked": + *s = MemberKicked + default: + return fmt.Errorf("unknown status %q", str) + } + return nil + } + var n int16 + if err := json.Unmarshal(data, &n); err == nil { + *s = MemberStatus(n) + return nil + } + return fmt.Errorf("invalid MemberStatus %s", string(data)) +} + +func (s MemberStatus) String() string { + switch s { + case MemberActive: + return "active" + case MemberBanned: + return "banned" + case MemberPending: + return "pending" + case MemberKicked: + return "kicked" + default: + return fmt.Sprintf("status_%d", int(s)) + } +} diff --git a/internal/room/room.go b/internal/room/room.go new file mode 100644 index 0000000..ade3cda --- /dev/null +++ b/internal/room/room.go @@ -0,0 +1,281 @@ +package room + +import ( + "fmt" + "strings" + "time" +) + +type RoomStatus string + +const ( + RoomActive RoomStatus = "active" + RoomClosed RoomStatus = "closed" + RoomBanned RoomStatus = "banned" +) + +func (s RoomStatus) IsActive() bool { return s == RoomActive } +func (s RoomStatus) IsClosed() bool { return s == RoomClosed || s == RoomBanned } +func (s RoomStatus) CanTransitionTo(next RoomStatus) bool { + if s == next { + return true + } + switch s { + case RoomActive: + return next == RoomClosed || next == RoomBanned + case RoomClosed: + return next == RoomActive + case RoomBanned: + return next == RoomActive + default: + return false + } +} + +func ParseRoomStatus(raw string) (RoomStatus, error) { + switch strings.ToLower(strings.TrimSpace(raw)) { + case "active": + return RoomActive, nil + case "closed": + return RoomClosed, nil + case "banned": + return RoomBanned, nil + default: + return "", fmt.Errorf("unknown room status %q", raw) + } +} + +type PlayMode string + +const ( + PlaySequential PlayMode = "sequential" + PlayRepeatOne PlayMode = "repeat_one" + PlayRepeatAll PlayMode = "repeat_all" + PlayShuffle PlayMode = "shuffle" +) + +func ParsePlayMode(s string) (PlayMode, error) { + switch strings.ToLower(strings.TrimSpace(s)) { + case "sequential", "": + return PlaySequential, nil + case "repeat_one", "repeatone": + return PlayRepeatOne, nil + case "repeat_all", "repeatall": + return PlayRepeatAll, nil + case "shuffle", "random": + return PlayShuffle, nil + default: + return "", fmt.Errorf("unknown play mode %q", s) + } +} + +type AutoPlaySettings struct { + Enabled bool `json:"enabled"` + Mode PlayMode `json:"mode"` + Delay uint32 `json:"delay"` // seconds +} + +func DefaultAutoPlay() AutoPlaySettings { + return AutoPlaySettings{Enabled: true, Mode: PlaySequential, Delay: 3} +} + +type RoomCategory struct { + ID int64 `json:"id"` + Key string `json:"key"` + Name string `json:"name"` + Description string `json:"description"` + SortOrder int32 `json:"sort_order"` + IsEnabled bool `json:"is_enabled"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type RoomLabel struct { + ID int64 `json:"id"` + Key string `json:"key"` + Name string `json:"name"` + Description string `json:"description"` + Color string `json:"color"` + CategoryID *int64 `json:"category_id,omitempty"` + SortOrder int32 `json:"sort_order"` + IsEnabled bool `json:"is_enabled"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type Room struct { + Name string `json:"name"` + DisplayName string `json:"display_name,omitempty"` + Description string `json:"description,omitempty"` + Creator string `json:"creator"` + Status RoomStatus `json:"status"` + IsPublic bool `json:"is_public"` + PasswordHash string `json:"-"` + RequireApproval bool `json:"require_approval"` + MaxMembers int `json:"max_members"` + MemberCount int `json:"member_count"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ClosedAt *time.Time `json:"closed_at,omitempty"` + Settings *RoomSettings `json:"settings,omitempty"` + Version int64 `json:"version"` + CategoryID *int64 `json:"category_id,omitempty"` + LabelIDs []int64 `json:"label_ids,omitempty"` + CoverFileID *string `json:"cover_file_id,omitempty"` + AutoPlay AutoPlaySettings `json:"auto_play"` +} + +type RoomWithCount struct { + Room `json:",inline"` + MemberCount int `json:"member_count"` +} + +type SortDirection string + +const ( + SortAsc SortDirection = "asc" + SortDesc SortDirection = "desc" +) + +type PageParams struct { + Page int `json:"page"` + PageSize int `json:"page_size"` +} + +func (p PageParams) Offset() int { + if p.Page <= 1 { + return 0 + } + return (p.Page - 1) * p.Limit() +} +func (p PageParams) Limit() int { + if p.PageSize <= 0 { + return 20 + } + if p.PageSize > 100 { + return 100 + } + return p.PageSize +} + +type RoomListSortBy string + +const ( + SortByCreatedAt RoomListSortBy = "created_at" + SortByUpdatedAt RoomListSortBy = "updated_at" + SortByName RoomListSortBy = "name" + SortByMemberCount RoomListSortBy = "member_count" +) + +func (s RoomListSortBy) AsSQL() string { + switch s { + case SortByName: + return "name" + case SortByMemberCount: + return "member_count" + case SortByUpdatedAt: + return "updated_at" + default: + return "created_at" + } +} + +type RoomListQuery struct { + Pagination PageParams `json:"pagination"` + Status *RoomStatus `json:"status,omitempty"` + Search *string `json:"search,omitempty"` + IsBanned *bool `json:"is_banned,omitempty"` + IsPublic *bool `json:"is_public,omitempty"` + Creator *string `json:"creator,omitempty"` + CategoryID *int64 `json:"category_id,omitempty"` + LabelIDs []int64 `json:"label_ids,omitempty"` + SortBy RoomListSortBy `json:"sort_by"` + SortDirection SortDirection `json:"sort_direction"` +} + +func DefaultRoomListQuery() RoomListQuery { + active := RoomActive + return RoomListQuery{ + Pagination: PageParams{Page: 1, PageSize: 20}, + Status: &active, + SortBy: SortByCreatedAt, + SortDirection: SortDesc, + } +} + +func NewRoom(name, creator string) *Room { + now := time.Now() + return &Room{ + Name: name, + DisplayName: name, + Creator: creator, + Status: RoomActive, + IsPublic: true, + MaxMembers: 100, + CreatedAt: now, + UpdatedAt: now, + Version: 1, + Settings: DefaultSettings(), + AutoPlay: DefaultAutoPlay(), + } +} + +type RoomSettings struct { + IsPublic bool `json:"is_public"` + RequireApproval bool `json:"require_approval"` + AllowGuestJoin bool `json:"allow_guest_join"` + ChatEnabled bool `json:"chat_enabled"` + MaxMembers int `json:"max_members"` + AllowAutoJoin bool `json:"allow_auto_join"` + AdminAddedPerms uint64 `json:"admin_added_permissions"` + AdminRemovedPerms uint64 `json:"admin_removed_permissions"` + MemberAddedPerms uint64 `json:"member_added_permissions"` + MemberRemovedPerms uint64 `json:"member_removed_permissions"` + GuestAddedPerms uint64 `json:"guest_added_permissions"` + GuestRemovedPerms uint64 `json:"guest_removed_permissions"` + AutoPlayEnabled bool `json:"auto_play_enabled"` + PlayMode PlayMode `json:"play_mode"` +} + +func DefaultSettings() *RoomSettings { + return &RoomSettings{ + IsPublic: true, + AllowGuestJoin: false, + ChatEnabled: true, + MaxMembers: 100, + AllowAutoJoin: true, + RequireApproval: false, + AutoPlayEnabled: true, + PlayMode: PlaySequential, + } +} + +func (s *RoomSettings) AdminPermissions() PermissionSet { + base := PermDefaultAdmin + base |= PermissionSet(s.AdminAddedPerms) + base &^= PermissionSet(s.AdminRemovedPerms) + return base +} +func (s *RoomSettings) MemberPermissions() PermissionSet { + base := PermDefaultMember + base |= PermissionSet(s.MemberAddedPerms) + base &^= PermissionSet(s.MemberRemovedPerms) + return base +} +func (s *RoomSettings) GuestPermissions() PermissionSet { + base := PermDefaultGuest + base |= PermissionSet(s.GuestAddedPerms) + base &^= PermissionSet(s.GuestRemovedPerms) + return base & PermGuestAssignable +} + +func (s *RoomSettings) IsChatEnabled() bool { return s.ChatEnabled } + +func (s *RoomSettings) EffectiveAutoPlay() AutoPlaySettings { + enabled := s.AutoPlayEnabled + mode := s.PlayMode + if mode == "" { + mode = PlaySequential + } + return AutoPlaySettings{Enabled: enabled, Mode: mode, Delay: 3} +} diff --git a/internal/room/service.go b/internal/room/service.go new file mode 100644 index 0000000..ffd38d6 --- /dev/null +++ b/internal/room/service.go @@ -0,0 +1,263 @@ +package room + +import ( + "context" + "database/sql" + "errors" +) + +type Service struct { + store *Store +} + +func NewService(store *Store) *Service { return &Service{store: store} } + +func (s *Service) Store() *Store { return s.store } + +func (s *Service) GetRoom(ctx context.Context, name string) (*Room, error) { + if s.store == nil || s.store.db == nil { + return nil, errors.New("room store not configured") + } + return s.store.GetRoom(ctx, name) +} + +func (s *Service) CreateRoom(ctx context.Context, name, creator string, settings *RoomSettings) (*Room, error) { + if name == "" { + return nil, errors.New("room name required") + } + if creator == "" { + return nil, errors.New("creator required") + } + r := NewRoom(name, creator) + if settings != nil { + r.Settings = settings + r.IsPublic = settings.IsPublic + r.RequireApproval = settings.RequireApproval + if settings.MaxMembers > 0 { + r.MaxMembers = settings.MaxMembers + } + } + if err := s.store.CreateRoom(ctx, r); err != nil { + return nil, err + } + creatorMember := NewMember(name, creator, RoleCreator) + if err := s.store.UpsertMember(ctx, creatorMember); err != nil { + return nil, err + } + return r, nil +} + +func (s *Service) JoinRoom(ctx context.Context, roomName, username string, password string) (*RoomMember, error) { + r, err := s.store.GetRoom(ctx, roomName) + if err != nil { + return nil, err + } + if r.Status == RoomBanned { + return nil, errors.New("room is banned") + } + if r.Status == RoomClosed { + return nil, errors.New("room is closed") + } + if r.PasswordHash != "" { + if !verifyPassword(password, r.PasswordHash) { + return nil, errors.New("invalid room password") + } + } + if cnt, _ := s.store.CountMembers(ctx, roomName); cnt >= r.MaxMembers && r.MaxMembers > 0 { + return nil, errors.New("room is full") + } + member, err := s.store.GetMember(ctx, roomName, username) + if err == nil && member.Status == MemberActive { + return member, nil + } + if err == nil && member.Status == MemberBanned { + return nil, errors.New("you are banned from this room") + } + newMember := NewMember(roomName, username, RoleMember) + if s.store != nil { + _ = s.store.UpsertMember(ctx, newMember) + } + return newMember, nil +} + +func (s *Service) GetMember(ctx context.Context, room, userID string) (*RoomMember, error) { + return s.store.GetMember(ctx, room, userID) +} + +func (s *Service) EnsurePermission(ctx context.Context, room, userID string, perm RoomPermission) error { + if s.store == nil || s.store.db == nil { + return nil + } + r, err := s.store.GetRoom(ctx, room) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return errors.New("room not found") + } + return err + } + if r.Status == RoomBanned { + return errors.New("room is banned") + } + if r.Status == RoomClosed && perm != PermViewMembers && perm != PermViewChatHistory { + return errors.New("room is closed") + } + m, err := s.store.GetMember(ctx, room, userID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + if r.IsPublic || r.Settings.AllowGuestJoin { + guest := NewMember(room, userID, RoleGuest) + if guest.HasPermission(perm, r.Settings) { + return nil + } + } + return errors.New("not a member of this room") + } + return err + } + if m.Status != MemberActive { + return errors.New("member not active") + } + if !m.HasPermission(perm, r.Settings) { + return errors.New("permission denied: " + perm.String()) + } + return nil +} + +func (s *Service) CheckPermission(ctx context.Context, room, userID string, perm RoomPermission) bool { + return s.EnsurePermission(ctx, room, userID, perm) == nil +} + +func (s *Service) ListMembers(ctx context.Context, room string) ([]*RoomMember, error) { + return s.store.ListMembers(ctx, room) +} + +func (s *Service) UpdateMemberRole(ctx context.Context, room, actorID, targetID string, newRole RoomRole) error { + if newRole == RoleCreator { + return errors.New("cannot assign creator role") + } + roomObj, err := s.store.GetRoom(ctx, room) + if err != nil { + return err + } + actor, err := s.store.GetMember(ctx, room, actorID) + if err != nil { + return errors.New("actor not in room") + } + if !actor.HasPermission(PermManageMembers, roomObj.Settings) { + return errors.New("permission denied: manage members") + } + if !actor.Role.CanManage(newRole) { + return errors.New("cannot manage role higher than yours") + } + target, err := s.store.GetMember(ctx, room, targetID) + if err != nil { + return errors.New("target not in room") + } + if target.Role == RoleCreator { + return errors.New("cannot change creator role") + } + if !actor.Role.CanManage(target.Role) { + return errors.New("cannot manage member with higher or equal role") + } + target.Role = newRole + return s.store.UpsertMember(ctx, target) +} + +func (s *Service) UpdateMemberPermissions(ctx context.Context, room, actorID, targetID string, added, removed uint64, isAdmin bool) error { + roomObj, err := s.store.GetRoom(ctx, room) + if err != nil { + return err + } + actor, err := s.store.GetMember(ctx, room, actorID) + if err != nil { + return errors.New("actor not in room") + } + if !actor.HasPermission(PermManagePermissions, roomObj.Settings) { + return errors.New("permission denied: manage permissions") + } + target, err := s.store.GetMember(ctx, room, targetID) + if err != nil { + return errors.New("target not in room") + } + if !actor.Role.CanManage(target.Role) { + return errors.New("cannot manage this member") + } + if isAdmin { + if target.Role != RoleAdmin && target.Role != RoleCreator { + return errors.New("admin permissions only for admin/creator") + } + target.AdminAdded = added + target.AdminRemoved = removed + } else { + target.AddedPerms = added + target.RemovedPerms = removed + } + return s.store.UpsertMember(ctx, target) +} + +func (s *Service) KickMember(ctx context.Context, room, actorID, targetID string) error { + roomObj, err := s.store.GetRoom(ctx, room) + if err != nil { + return err + } + actor, err := s.store.GetMember(ctx, room, actorID) + if err != nil { + return errors.New("actor not in room") + } + if !actor.HasPermission(PermRemoveMembers, roomObj.Settings) { + return errors.New("permission denied: remove members") + } + target, err := s.store.GetMember(ctx, room, targetID) + if err != nil { + return errors.New("target not in room") + } + if target.Role == RoleCreator { + return errors.New("cannot kick creator") + } + if !actor.Role.CanManage(target.Role) { + return errors.New("cannot kick higher role") + } + return s.store.RemoveMember(ctx, room, targetID) +} + +func (s *Service) BanMember(ctx context.Context, room, actorID, targetID string) error { + if err := s.KickMember(ctx, room, actorID, targetID); err != nil { + return err + } + banned := NewMember(room, targetID, RoleGuest) + banned.Status = MemberBanned + return s.store.UpsertMember(ctx, banned) +} + +func (s *Service) LeaveRoom(ctx context.Context, room, userID string) error { + m, err := s.store.GetMember(ctx, room, userID) + if err != nil { + return err + } + if m.Role == RoleCreator { + return errors.New("creator cannot leave, transfer ownership or delete room") + } + return s.store.RemoveMember(ctx, room, userID) +} + +func (s *Service) UpdateRoomSettings(ctx context.Context, room, actorID string, newSettings *RoomSettings) error { + r, err := s.store.GetRoom(ctx, room) + if err != nil { + return err + } + actor, err := s.store.GetMember(ctx, room, actorID) + if err != nil { + return errors.New("actor not in room") + } + if !actor.HasPermission(PermManageRoom, r.Settings) { + return errors.New("permission denied: manage room") + } + return s.store.UpdateRoomSettings(ctx, room, newSettings) +} + +func verifyPassword(input, hash string) bool { + if hash == "" { + return true + } + return input == hash +} diff --git a/internal/room/store.go b/internal/room/store.go new file mode 100644 index 0000000..b021da8 --- /dev/null +++ b/internal/room/store.go @@ -0,0 +1,415 @@ +package room + +import ( + "context" + "strings" + "database/sql" + "encoding/json" + "fmt" + "time" +) + +type Store struct { + db *sql.DB +} + +func NewStore(db *sql.DB) *Store { return &Store{db: db} } + +func (s *Store) DB() *sql.DB { return s.db } + +func InitSchema(db *sql.DB) error { + stmts := []string{ + `CREATE TABLE IF NOT EXISTS rooms_v2 ( + name TEXT PRIMARY KEY, + display_name TEXT, + description TEXT, + creator TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'active', + is_public INTEGER NOT NULL DEFAULT 1, + password_hash TEXT DEFAULT '', + require_approval INTEGER NOT NULL DEFAULT 0, + max_members INTEGER NOT NULL DEFAULT 100, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + closed_at INTEGER, + settings_json TEXT, + version INTEGER NOT NULL DEFAULT 1 + )`, + `CREATE TABLE IF NOT EXISTS room_members ( + room TEXT NOT NULL, + user_id TEXT NOT NULL, + username TEXT NOT NULL, + role INTEGER NOT NULL, + status INTEGER NOT NULL DEFAULT 1, + added_permissions INTEGER NOT NULL DEFAULT 0, + removed_permissions INTEGER NOT NULL DEFAULT 0, + admin_added_permissions INTEGER NOT NULL DEFAULT 0, + admin_removed_permissions INTEGER NOT NULL DEFAULT 0, + joined_at INTEGER NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + PRIMARY KEY (room, user_id), + FOREIGN KEY (room) REFERENCES rooms_v2(name) ON DELETE CASCADE + )`, + `CREATE INDEX IF NOT EXISTS idx_room_members_user ON room_members(user_id)`, + `CREATE INDEX IF NOT EXISTS idx_room_members_room_role ON room_members(room, role)`, + `CREATE TABLE IF NOT EXISTS room_bans ( + room TEXT PRIMARY KEY, + reason TEXT, + banned_at INTEGER, + banned_by TEXT + )`, + `CREATE TABLE IF NOT EXISTS room_join_requests ( + room TEXT NOT NULL, + user_id TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + created_at INTEGER NOT NULL, + PRIMARY KEY (room, user_id) + )`, + } + for _, stmt := range stmts { + if _, err := db.Exec(stmt); err != nil { + return fmt.Errorf("init schema %q: %w", stmt, err) + } + } + return nil +} + +func (s *Store) CreateRoom(ctx context.Context, r *Room) error { + if s.db == nil { + return fmt.Errorf("no db") + } + settingsJSON, _ := json.Marshal(r.Settings) + _, err := s.db.ExecContext(ctx, `INSERT INTO rooms_v2 (name, display_name, description, creator, status, is_public, password_hash, require_approval, max_members, created_at, updated_at, settings_json, version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + r.Name, r.DisplayName, r.Description, r.Creator, string(r.Status), boolToInt(r.IsPublic), r.PasswordHash, boolToInt(r.RequireApproval), r.MaxMembers, r.CreatedAt.Unix(), r.UpdatedAt.Unix(), string(settingsJSON), r.Version) + return err +} + +func (s *Store) GetRoom(ctx context.Context, name string) (*Room, error) { + if s.db == nil { + return nil, sql.ErrNoRows + } + row := s.db.QueryRowContext(ctx, `SELECT name, display_name, description, creator, status, is_public, password_hash, require_approval, max_members, created_at, updated_at, closed_at, settings_json, version FROM rooms_v2 WHERE name = ?`, name) + var r Room + var status string + var isPublic, requireApproval int + var createdAt, updatedAt int64 + var closedAt sql.NullInt64 + var settingsJSON sql.NullString + var displayName, description sql.NullString + var passwordHash sql.NullString + if err := row.Scan(&r.Name, &displayName, &description, &r.Creator, &status, &isPublic, &passwordHash, &requireApproval, &r.MaxMembers, &createdAt, &updatedAt, &closedAt, &settingsJSON, &r.Version); err != nil { + return nil, err + } + r.DisplayName = displayName.String + r.Description = description.String + r.PasswordHash = passwordHash.String + r.Status = RoomStatus(status) + r.IsPublic = isPublic != 0 + r.RequireApproval = requireApproval != 0 + r.CreatedAt = time.Unix(createdAt, 0) + r.UpdatedAt = time.Unix(updatedAt, 0) + if closedAt.Valid { + t := time.Unix(closedAt.Int64, 0) + r.ClosedAt = &t + } + r.Settings = DefaultSettings() + if settingsJSON.Valid && settingsJSON.String != "" { + _ = json.Unmarshal([]byte(settingsJSON.String), r.Settings) + } else { + r.Settings.IsPublic = r.IsPublic + r.Settings.RequireApproval = r.RequireApproval + r.Settings.MaxMembers = r.MaxMembers + } + return &r, nil +} + +func (s *Store) ListRooms(ctx context.Context) ([]*Room, error) { + if s.db == nil { + return nil, nil + } + rows, err := s.db.QueryContext(ctx, `SELECT name, display_name, description, creator, status, is_public, require_approval, max_members, created_at, updated_at, closed_at, settings_json, version FROM rooms_v2 WHERE status != 'banned' ORDER BY updated_at DESC`) + if err != nil { + return nil, err + } + defer rows.Close() + var out []*Room + for rows.Next() { + var r Room + var status string + var isPublic, requireApproval int + var createdAt, updatedAt int64 + var closedAt sql.NullInt64 + var settingsJSON sql.NullString + var displayName, description sql.NullString + if err := rows.Scan(&r.Name, &displayName, &description, &r.Creator, &status, &isPublic, &requireApproval, &r.MaxMembers, &createdAt, &updatedAt, &closedAt, &settingsJSON, &r.Version); err != nil { + return nil, err + } + r.DisplayName = displayName.String + r.Description = description.String + r.Status = RoomStatus(status) + r.IsPublic = isPublic != 0 + r.RequireApproval = requireApproval != 0 + r.CreatedAt = time.Unix(createdAt, 0) + r.UpdatedAt = time.Unix(updatedAt, 0) + if closedAt.Valid { + t := time.Unix(closedAt.Int64, 0) + r.ClosedAt = &t + } + r.Settings = DefaultSettings() + if settingsJSON.Valid && settingsJSON.String != "" { + _ = json.Unmarshal([]byte(settingsJSON.String), r.Settings) + } + out = append(out, &r) + } + return out, rows.Err() +} + +func (s *Store) UpdateRoomSettings(ctx context.Context, name string, settings *RoomSettings) error { + if s.db == nil { + return fmt.Errorf("no db") + } + b, _ := json.Marshal(settings) + _, err := s.db.ExecContext(ctx, `UPDATE rooms_v2 SET settings_json = ?, is_public = ?, require_approval = ?, max_members = ?, updated_at = ?, version = version + 1 WHERE name = ?`, string(b), boolToInt(settings.IsPublic), boolToInt(settings.RequireApproval), settings.MaxMembers, time.Now().Unix(), name) + return err +} + +func (s *Store) UpdateRoomStatus(ctx context.Context, name string, status RoomStatus) error { + if s.db == nil { + return fmt.Errorf("no db") + } + var closedAt *int64 + if status == RoomClosed { + v := time.Now().Unix() + closedAt = &v + } + _, err := s.db.ExecContext(ctx, `UPDATE rooms_v2 SET status = ?, closed_at = ?, updated_at = ?, version = version + 1 WHERE name = ?`, string(status), closedAt, time.Now().Unix(), name) + return err +} + +func (s *Store) DeleteRoom(ctx context.Context, name string) error { + if s.db == nil { + return fmt.Errorf("no db") + } + _, err := s.db.ExecContext(ctx, `DELETE FROM rooms_v2 WHERE name = ?`, name) + if err != nil { + return err + } + _, _ = s.db.ExecContext(ctx, `DELETE FROM room_members WHERE room = ?`, name) + return nil +} + +func (s *Store) UpsertMember(ctx context.Context, m *RoomMember) error { + if s.db == nil { + return fmt.Errorf("no db") + } + _, err := s.db.ExecContext(ctx, `INSERT INTO room_members (room, user_id, username, role, status, added_permissions, removed_permissions, admin_added_permissions, admin_removed_permissions, joined_at, version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(room, user_id) DO UPDATE SET role=excluded.role, status=excluded.status, added_permissions=excluded.added_permissions, removed_permissions=excluded.removed_permissions, admin_added_permissions=excluded.admin_added_permissions, admin_removed_permissions=excluded.admin_removed_permissions, version=version+1`, + m.RoomName, m.UserID, m.Username, int(m.Role), int(m.Status), int64(m.AddedPerms), int64(m.RemovedPerms), int64(m.AdminAdded), int64(m.AdminRemoved), m.JoinedAt.Unix(), m.Version) + return err +} + +func (s *Store) GetMember(ctx context.Context, room, userID string) (*RoomMember, error) { + if s.db == nil { + return nil, sql.ErrNoRows + } + row := s.db.QueryRowContext(ctx, `SELECT room, user_id, username, role, status, added_permissions, removed_permissions, admin_added_permissions, admin_removed_permissions, joined_at, version FROM room_members WHERE room = ? AND user_id = ?`, room, userID) + var m RoomMember + var role, status int + var joinedAt int64 + var added, removed, adminAdded, adminRemoved int64 + if err := row.Scan(&m.RoomName, &m.UserID, &m.Username, &role, &status, &added, &removed, &adminAdded, &adminRemoved, &joinedAt, &m.Version); err != nil { + return nil, err + } + m.Role = RoomRole(role) + m.Status = MemberStatus(status) + m.AddedPerms = uint64(added) + m.RemovedPerms = uint64(removed) + m.AdminAdded = uint64(adminAdded) + m.AdminRemoved = uint64(adminRemoved) + m.JoinedAt = time.Unix(joinedAt, 0) + m.RoomID = m.RoomName + return &m, nil +} + +func (s *Store) ListMembers(ctx context.Context, room string) ([]*RoomMember, error) { + if s.db == nil { + return nil, nil + } + rows, err := s.db.QueryContext(ctx, `SELECT room, user_id, username, role, status, added_permissions, removed_permissions, admin_added_permissions, admin_removed_permissions, joined_at, version FROM room_members WHERE room = ? ORDER BY role ASC, joined_at ASC`, room) + if err != nil { + return nil, err + } + defer rows.Close() + var out []*RoomMember + for rows.Next() { + var m RoomMember + var role, status int + var joinedAt int64 + var added, removed, adminAdded, adminRemoved int64 + if err := rows.Scan(&m.RoomName, &m.UserID, &m.Username, &role, &status, &added, &removed, &adminAdded, &adminRemoved, &joinedAt, &m.Version); err != nil { + return nil, err + } + m.Role = RoomRole(role) + m.Status = MemberStatus(status) + m.AddedPerms = uint64(added) + m.RemovedPerms = uint64(removed) + m.AdminAdded = uint64(adminAdded) + m.AdminRemoved = uint64(adminRemoved) + m.JoinedAt = time.Unix(joinedAt, 0) + m.RoomID = m.RoomName + out = append(out, &m) + } + return out, rows.Err() +} + +func (s *Store) RemoveMember(ctx context.Context, room, userID string) error { + if s.db == nil { + return fmt.Errorf("no db") + } + _, err := s.db.ExecContext(ctx, `DELETE FROM room_members WHERE room = ? AND user_id = ?`, room, userID) + return err +} + +func (s *Store) CountMembers(ctx context.Context, room string) (int, error) { + if s.db == nil { + return 0, nil + } + var cnt int + err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM room_members WHERE room = ? AND status = 1`, room).Scan(&cnt) + return cnt, err +} + + +func (s *Store) CountRooms(ctx context.Context, q RoomListQuery) (int, error) { + if s.db == nil { + return 0, nil + } + query := `SELECT COUNT(*) FROM rooms_v2 WHERE 1=1` + args := []interface{}{} + if q.Status != nil { + query += ` AND status = ?` + args = append(args, string(*q.Status)) + } else if q.IsBanned != nil && !*q.IsBanned { + query += ` AND status != 'banned'` + } + if q.IsPublic != nil { + query += ` AND is_public = ?` + args = append(args, boolToInt(*q.IsPublic)) + } + if q.Creator != nil && *q.Creator != "" { + query += ` AND creator = ?` + args = append(args, *q.Creator) + } + if q.Search != nil && *q.Search != "" { + query += ` AND (name LIKE ? OR display_name LIKE ? OR description LIKE ?)` + like := "%" + *q.Search + "%" + args = append(args, like, like, like) + } + var cnt int + err := s.db.QueryRowContext(ctx, query, args...).Scan(&cnt) + return cnt, err +} + +func (s *Store) QueryRooms(ctx context.Context, q RoomListQuery) ([]*Room, int, error) { + if s.db == nil { + return nil, 0, nil + } + total, err := s.CountRooms(ctx, q) + if err != nil { + return nil, 0, err + } + query := `SELECT name, display_name, description, creator, status, is_public, require_approval, max_members, created_at, updated_at, closed_at, settings_json, version FROM rooms_v2 WHERE 1=1` + args := []interface{}{} + if q.Status != nil { + query += ` AND status = ?` + args = append(args, string(*q.Status)) + } else if q.IsBanned != nil && !*q.IsBanned { + query += ` AND status != 'banned'` + } + if q.IsPublic != nil { + query += ` AND is_public = ?` + args = append(args, boolToInt(*q.IsPublic)) + } + if q.Creator != nil && *q.Creator != "" { + query += ` AND creator = ?` + args = append(args, *q.Creator) + } + if q.Search != nil && *q.Search != "" { + query += ` AND (name LIKE ? OR display_name LIKE ? OR description LIKE ?)` + like := "%" + *q.Search + "%" + args = append(args, like, like, like) + } + // Sorting + sortCol := q.SortBy.AsSQL() + dir := strings.ToUpper(string(q.SortDirection)) + if dir != "ASC" && dir != "DESC" { + dir = "DESC" + } + // whitelist sortCol + allowed := map[string]bool{"created_at": true, "updated_at": true, "name": true, "member_count": true} + if !allowed[sortCol] { + sortCol = "created_at" + } + query += fmt.Sprintf(` ORDER BY %s %s`, sortCol, dir) + // Pagination + limit := q.Pagination.Limit() + offset := q.Pagination.Offset() + query += ` LIMIT ? OFFSET ?` + args = append(args, limit, offset) + + rows, err := s.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, 0, err + } + defer rows.Close() + var out []*Room + for rows.Next() { + var r Room + var status string + var isPublic, requireApproval int + var createdAt, updatedAt int64 + var closedAt sql.NullInt64 + var settingsJSON sql.NullString + var displayName, description sql.NullString + if err := rows.Scan(&r.Name, &displayName, &description, &r.Creator, &status, &isPublic, &requireApproval, &r.MaxMembers, &createdAt, &updatedAt, &closedAt, &settingsJSON, &r.Version); err != nil { + return nil, 0, err + } + r.DisplayName = displayName.String + r.Description = description.String + r.Status = RoomStatus(status) + r.IsPublic = isPublic != 0 + r.RequireApproval = requireApproval != 0 + r.CreatedAt = time.Unix(createdAt, 0) + r.UpdatedAt = time.Unix(updatedAt, 0) + if closedAt.Valid { + t := time.Unix(closedAt.Int64, 0) + r.ClosedAt = &t + } + r.Settings = DefaultSettings() + if settingsJSON.Valid && settingsJSON.String != "" { + _ = json.Unmarshal([]byte(settingsJSON.String), r.Settings) + } + out = append(out, &r) + } + return out, total, rows.Err() +} + +func (s *Store) ListRoomsFiltered(ctx context.Context, q RoomListQuery) ([]*RoomWithCount, int, error) { + rooms, total, err := s.QueryRooms(ctx, q) + if err != nil { + return nil, 0, err + } + var out []*RoomWithCount + for _, r := range rooms { + cnt, _ := s.CountMembers(ctx, r.Name) + out = append(out, &RoomWithCount{Room: *r, MemberCount: cnt}) + } + return out, total, nil +} + +func boolToInt(b bool) int { + + if b { + return 1 + } + return 0 +} diff --git a/internal/server/auth_handler.go b/internal/server/auth_handler.go index b224403..4256643 100644 --- a/internal/server/auth_handler.go +++ b/internal/server/auth_handler.go @@ -5,7 +5,7 @@ import ( "net/http" "time" - "gospeak-live-sfu-demo/internal/auth" + "sync-live/internal/auth" ) type authRequest struct { @@ -15,10 +15,13 @@ type authRequest struct { } type authResponse struct { - Token string `json:"token"` - Username string `json:"username"` - Role string `json:"role"` - ExpiresAt int64 `json:"expires_at,omitempty"` + Token string `json:"token"` + RefreshToken string `json:"refresh_token,omitempty"` + Username string `json:"username"` + Role string `json:"role"` + Status string `json:"status,omitempty"` + ExpiresAt int64 `json:"expires_at,omitempty"` + RefreshExpiresAt int64 `json:"refresh_expires_at,omitempty"` } func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { @@ -35,25 +38,101 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { http.Error(w, "username and password required", http.StatusBadRequest) return } - token, user, err := s.auth.Login(req.Username, req.Password) + access, refresh, user, err := s.auth.LoginWithRefresh(req.Username, req.Password) if err != nil { - http.Error(w, "login failed: "+err.Error(), http.StatusUnauthorized) + // fall back to single token for backwards compat + token, u, err2 := s.auth.Login(req.Username, req.Password) + if err2 != nil { + http.Error(w, "login failed: "+err.Error(), http.StatusUnauthorized) + return + } + http.SetCookie(w, &http.Cookie{ + Name: "token", + Value: token, + Path: "/", + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + Expires: time.Now().Add(s.auth.JWT.TTL()), + }) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(authResponse{ + Token: token, + Username: u.Username, + Role: u.Role, + Status: u.Status, + ExpiresAt: time.Now().Add(s.auth.JWT.TTL()).Unix(), + }) return } http.SetCookie(w, &http.Cookie{ Name: "token", - Value: token, + Value: access, + Path: "/", + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + Expires: time.Now().Add(s.auth.JWT.TTL()), + }) + http.SetCookie(w, &http.Cookie{ + Name: "refresh_token", + Value: refresh, + Path: "/api/auth/refresh", + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + Expires: time.Now().Add(s.auth.JWT.RefreshTTL()), + }) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(authResponse{ + Token: access, + RefreshToken: refresh, + Username: user.Username, + Role: user.Role, + Status: user.Status, + ExpiresAt: time.Now().Add(s.auth.JWT.TTL()).Unix(), + RefreshExpiresAt: time.Now().Add(s.auth.JWT.RefreshTTL()).Unix(), + }) +} + +func (s *Server) handleRefresh(w http.ResponseWriter, r *http.Request) { + if s.auth == nil { + http.Error(w, "auth not configured", http.StatusInternalServerError) + return + } + var req struct { + RefreshToken string `json:"refresh_token"` + } + _ = json.NewDecoder(r.Body).Decode(&req) + token := req.RefreshToken + if token == "" { + if c, err := r.Cookie("refresh_token"); err == nil { + token = c.Value + } + } + if token == "" { + token = r.Header.Get("X-Refresh-Token") + } + if token == "" { + http.Error(w, "refresh_token required", http.StatusBadRequest) + return + } + newAccess, claims, err := s.auth.RefreshAccess(token) + if err != nil { + http.Error(w, "refresh failed: "+err.Error(), http.StatusUnauthorized) + return + } + http.SetCookie(w, &http.Cookie{ + Name: "token", + Value: newAccess, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(s.auth.JWT.TTL()), }) w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(authResponse{ - Token: token, - Username: user.Username, - Role: user.Role, - ExpiresAt: time.Now().Add(s.auth.JWT.TTL()).Unix(), + json.NewEncoder(w).Encode(map[string]interface{}{ + "token": newAccess, + "username": claims.Username, + "role": claims.Role, + "expires_at": claims.ExpiresAt.Unix(), }) } @@ -79,10 +158,15 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) { if role == "" { role = auth.RoleViewer } + role = auth.NormalizeRole(role) + if role == auth.RoleRoot { + http.Error(w, "cannot register as root", http.StatusForbidden) + return + } if role == auth.RoleAdmin || role == auth.RolePublisher { token := extractAuthToken(r) if token != "" { - if claims, _, err := s.auth.VerifyToken(token); err == nil && claims.Role == auth.RoleAdmin { + if claims, _, err := s.auth.VerifyToken(token); err == nil && auth.IsAdminOrAbove(claims.Role) { } else { role = auth.RoleViewer } @@ -108,6 +192,7 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) { Token: token, Username: user.Username, Role: user.Role, + Status: user.Status, ExpiresAt: time.Now().Add(s.auth.JWT.TTL()).Unix(), }) } @@ -120,6 +205,13 @@ func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { HttpOnly: true, MaxAge: -1, }) + http.SetCookie(w, &http.Cookie{ + Name: "refresh_token", + Value: "", + Path: "/api/auth/refresh", + HttpOnly: true, + MaxAge: -1, + }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "logged out"}) } @@ -143,6 +235,7 @@ func (s *Server) handleMe(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]interface{}{ "username": user.Username, "role": user.Role, + "status": user.Status, "expires_at": claims.ExpiresAt.Unix(), "issued_at": claims.IssuedAt.Unix(), }) @@ -155,7 +248,7 @@ func (s *Server) handleListUsers(w http.ResponseWriter, r *http.Request) { } token := extractAuthToken(r) claims, _, err := s.auth.VerifyToken(token) - if err != nil || claims.Role != auth.RoleAdmin { + if err != nil || !auth.IsAdminOrAbove(claims.Role) { http.Error(w, "forbidden: admin only", http.StatusForbidden) return } @@ -171,7 +264,7 @@ func (s *Server) handleUpdateRole(w http.ResponseWriter, r *http.Request) { } token := extractAuthToken(r) claims, _, err := s.auth.VerifyToken(token) - if err != nil || claims.Role != auth.RoleAdmin { + if err != nil || !auth.IsAdminOrAbove(claims.Role) { http.Error(w, "forbidden: admin only", http.StatusForbidden) return } @@ -187,7 +280,7 @@ func (s *Server) handleUpdateRole(w http.ResponseWriter, r *http.Request) { http.Error(w, "username and role required", http.StatusBadRequest) return } - if err := s.auth.UpdateUserRole(req.Username, req.Role); err != nil { + if err := s.auth.UpdateUserRoleAs(claims.Username, req.Username, req.Role); err != nil { http.Error(w, "update failed: "+err.Error(), http.StatusBadRequest) return } @@ -195,6 +288,58 @@ func (s *Server) handleUpdateRole(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{"status": "ok", "username": req.Username, "role": req.Role}) } +func (s *Server) handleBanUser(w http.ResponseWriter, r *http.Request) { + if s.auth == nil { + http.Error(w, "auth not configured", http.StatusInternalServerError) + return + } + token := extractAuthToken(r) + claims, _, err := s.auth.VerifyToken(token) + if err != nil || !auth.IsAdminOrAbove(claims.Role) { + http.Error(w, "forbidden: admin only", http.StatusForbidden) + return + } + var req struct { + Username string `json:"username"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Username == "" { + http.Error(w, "username required", http.StatusBadRequest) + return + } + if err := s.auth.BanUser(claims.Username, req.Username); err != nil { + http.Error(w, "ban failed: "+err.Error(), http.StatusForbidden) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "banned", "username": req.Username}) +} + +func (s *Server) handleUnbanUser(w http.ResponseWriter, r *http.Request) { + if s.auth == nil { + http.Error(w, "auth not configured", http.StatusInternalServerError) + return + } + token := extractAuthToken(r) + claims, _, err := s.auth.VerifyToken(token) + if err != nil || !auth.IsAdminOrAbove(claims.Role) { + http.Error(w, "forbidden: admin only", http.StatusForbidden) + return + } + var req struct { + Username string `json:"username"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Username == "" { + http.Error(w, "username required", http.StatusBadRequest) + return + } + if err := s.auth.UnbanUser(claims.Username, req.Username); err != nil { + http.Error(w, "unban failed: "+err.Error(), http.StatusForbidden) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "unbanned", "username": req.Username}) +} + func (s *Server) handleAuthCheck(w http.ResponseWriter, r *http.Request) { if s.auth == nil { w.Header().Set("Content-Type", "application/json") @@ -223,8 +368,11 @@ func (s *Server) handleAuthCheck(w http.ResponseWriter, r *http.Request) { {"room:watch", "room", "watch"}, {"room:stop", "room", "stop"}, {"room:chat", "room", "chat"}, + {"room:manage", "room", "manage"}, + {"room:delete", "room", "delete"}, {"user:list", "user", "list"}, {"user:manage", "user", "manage"}, + {"system:manage", "system", "manage"}, {"config:read", "config", "read"}, {"srs:streams", "srs", "streams"}, } { @@ -237,7 +385,10 @@ func (s *Server) handleAuthCheck(w http.ResponseWriter, r *http.Request) { "authenticated": true, "username": user.Username, "role": user.Role, + "status": user.Status, "permissions": perms, + "is_admin": auth.IsAdminOrAbove(user.Role), + "is_root": user.Role == auth.RoleRoot, }) } diff --git a/internal/server/gateway.go b/internal/server/gateway.go index 23d8c04..75632b3 100644 --- a/internal/server/gateway.go +++ b/internal/server/gateway.go @@ -8,7 +8,8 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" - "gospeak-live-sfu-demo/gen" + "sync-live/gen" + "sync-live/internal/room" ) func (s *Server) writeProto(w http.ResponseWriter, m proto.Message, err error) { @@ -158,3 +159,82 @@ func decodeTargets(msg []byte) []*gen.StreamTarget { } return out } + +func (s *Server) handleRoomsQuery(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + page := 1 + pageSize := 20 + if v := q.Get("page"); v != "" { + fmt.Sscanf(v, "%d", &page) + } + if v := q.Get("page_size"); v != "" { + fmt.Sscanf(v, "%d", &pageSize) + } + if v := q.Get("pageSize"); v != "" { + fmt.Sscanf(v, "%d", &pageSize) + } + search := q.Get("search") + var searchPtr *string + if search != "" { + searchPtr = &search + } + statusStr := q.Get("status") + var statusPtr *room.RoomStatus + if statusStr != "" { + if st, err := room.ParseRoomStatus(statusStr); err == nil { + statusPtr = &st + } + } + creator := q.Get("creator") + var creatorPtr *string + if creator != "" { + creatorPtr = &creator + } + var isPublicPtr *bool + if v := q.Get("is_public"); v != "" { + b := v == "true" || v == "1" + isPublicPtr = &b + } + sortBy := room.RoomListSortBy(q.Get("sort_by")) + if sortBy == "" { + sortBy = room.SortByCreatedAt + } + sortDir := room.SortDirection(q.Get("sort_direction")) + if sortDir == "" { + sortDir = room.SortDesc + } + query := room.RoomListQuery{ + Pagination: room.PageParams{Page: page, PageSize: pageSize}, + Search: searchPtr, + Status: statusPtr, + Creator: creatorPtr, + IsPublic: isPublicPtr, + SortBy: sortBy, + SortDirection: sortDir, + } + // prefer DB-backed query if available + if s.roomStore != nil && s.roomStore.DB() != nil { + rooms, total, err := s.roomStore.QueryRooms(r.Context(), query) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "rooms": rooms, + "total": total, + "page": page, + "page_size": pageSize, + }) + return + } + // fallback to memory hub + all := s.hub.list() + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "rooms": all, + "total": len(all), + "page": 1, + "page_size": len(all), + }) +} diff --git a/internal/server/grpc.go b/internal/server/grpc.go index 263c89a..2ac7802 100644 --- a/internal/server/grpc.go +++ b/internal/server/grpc.go @@ -3,11 +3,11 @@ package server import ( "context" - "gospeak-live-sfu-demo/gen" + "sync-live/gen" ) type grpcServer struct { - gen.UnimplementedLiveSFUServer + gen.UnimplementedSyncLiveServer svc *Service } @@ -35,7 +35,7 @@ func (g *grpcServer) StopStream(ctx context.Context, req *gen.StopStreamRequest) return g.svc.StopStream(ctx, req) } -func (g *grpcServer) WatchRoom(req *gen.WatchRoomRequest, stream gen.LiveSFU_WatchRoomServer) error { +func (g *grpcServer) WatchRoom(req *gen.WatchRoomRequest, stream gen.SyncLive_WatchRoomServer) error { return g.svc.WatchRoom(req, stream) } diff --git a/internal/server/grpc_test.go b/internal/server/grpc_test.go index 98fc726..9f1cdec 100644 --- a/internal/server/grpc_test.go +++ b/internal/server/grpc_test.go @@ -7,10 +7,10 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - "gospeak-live-sfu-demo/gen" - "gospeak-live-sfu-demo/internal/config" - "gospeak-live-sfu-demo/internal/sfu/cloudflare" - "gospeak-live-sfu-demo/internal/sfu/srs" + "sync-live/gen" + "sync-live/internal/config" + "sync-live/internal/sfu/cloudflare" + "sync-live/internal/sfu/srs" ) // TestGRPCGetConfig 校验 protobuf/gRPC 控制面:生成的服务端 + 生成的客户端能互通。 @@ -27,7 +27,7 @@ func TestGRPCGetConfig(t *testing.T) { svc := NewService(cfg, cf, srsP, hub) gs := grpc.NewServer() - gen.RegisterLiveSFUServer(gs, NewGRPCServer(svc)) + gen.RegisterSyncLiveServer(gs, NewGRPCServer(svc)) lis, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) @@ -41,7 +41,7 @@ func TestGRPCGetConfig(t *testing.T) { } defer conn.Close() - cli := gen.NewLiveSFUClient(conn) + cli := gen.NewSyncLiveClient(conn) resp, err := cli.GetConfig(context.Background(), &gen.GetConfigRequest{}) if err != nil { t.Fatalf("GetConfig: %v", err) diff --git a/internal/server/manage.go b/internal/server/manage.go new file mode 100644 index 0000000..1781339 --- /dev/null +++ b/internal/server/manage.go @@ -0,0 +1,485 @@ +package server + +import ( + "encoding/json" + "net/http" + "strings" + "time" + + "sync-live/internal/auth" + "sync-live/internal/room" +) + +func (s *Server) handleManageRooms(w http.ResponseWriter, r *http.Request) { + if s.roomSvc == nil || s.roomSvc.Store().DB() == nil { + http.Error(w, "room service not configured", http.StatusServiceUnavailable) + return + } + switch r.Method { + case http.MethodGet: + rooms, err := s.roomSvc.Store().ListRooms(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + enhanced := make([]map[string]interface{}, 0, len(rooms)) + for _, rm := range rooms { + targets := s.hub.targets(rm.Name) + members, _ := s.roomSvc.Store().ListMembers(r.Context(), rm.Name) + enhanced = append(enhanced, map[string]interface{}{ + "room": rm, + "targets": targets, + "member_count": len(members), + "live": len(targets) > 0, + }) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"rooms": enhanced}) + case http.MethodPost: + var body struct { + Name string `json:"name"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + IsPublic *bool `json:"is_public"` + RequireApproval *bool `json:"require_approval"` + MaxMembers *int `json:"max_members"` + Password string `json:"password"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "bad json: "+err.Error(), http.StatusBadRequest) + return + } + name := strings.TrimSpace(body.Name) + if name == "" { + http.Error(w, "room name required", http.StatusBadRequest) + return + } + u, _ := auth.FromContext(r.Context()) + creator := "" + if u != nil { + creator = u.Username + } + if creator == "" { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + settings := room.DefaultSettings() + if body.IsPublic != nil { + settings.IsPublic = *body.IsPublic + } + if body.RequireApproval != nil { + settings.RequireApproval = *body.RequireApproval + } + if body.MaxMembers != nil && *body.MaxMembers > 0 { + settings.MaxMembers = *body.MaxMembers + } + newRoom := room.NewRoom(name, creator) + newRoom.DisplayName = body.DisplayName + if newRoom.DisplayName == "" { + newRoom.DisplayName = name + } + newRoom.Description = body.Description + newRoom.Settings = settings + newRoom.IsPublic = settings.IsPublic + newRoom.RequireApproval = settings.RequireApproval + newRoom.MaxMembers = settings.MaxMembers + if body.Password != "" { + newRoom.PasswordHash = body.Password + } + created, err := s.roomSvc.CreateRoom(r.Context(), name, creator, settings) + if err != nil { + http.Error(w, "create room: "+err.Error(), http.StatusBadRequest) + return + } + created.Description = body.Description + created.DisplayName = newRoom.DisplayName + if body.Password != "" { + created.PasswordHash = body.Password + } + s.hub.createRoom(name) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "room": created}) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func (s *Server) handleManageRoomDetail(w http.ResponseWriter, r *http.Request) { + if s.roomSvc == nil { + http.Error(w, "room service not configured", http.StatusServiceUnavailable) + return + } + roomName := r.PathValue("room") + if roomName == "" { + roomName = r.URL.Query().Get("room") + } + if roomName == "" { + http.Error(w, "room required", http.StatusBadRequest) + return + } + switch r.Method { + case http.MethodGet: + rm, err := s.roomSvc.Store().GetRoom(r.Context(), roomName) + if err != nil { + http.Error(w, "room not found: "+err.Error(), http.StatusNotFound) + return + } + members, _ := s.roomSvc.Store().ListMembers(r.Context(), roomName) + targets := s.hub.targets(roomName) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "room": rm, + "members": members, + "targets": targets, + }) + case http.MethodPatch, http.MethodPut: + var body struct { + DisplayName *string `json:"display_name"` + Description *string `json:"description"` + Status *string `json:"status"` + IsPublic *bool `json:"is_public"` + RequireApproval *bool `json:"require_approval"` + MaxMembers *int `json:"max_members"` + Password *string `json:"password"` + ChatEnabled *bool `json:"chat_enabled"` + AllowGuestJoin *bool `json:"allow_guest_join"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "bad json: "+err.Error(), http.StatusBadRequest) + return + } + u, _ := auth.FromContext(r.Context()) + actor := "" + if u != nil { + actor = u.Username + } + rm, err := s.roomSvc.Store().GetRoom(r.Context(), roomName) + if err != nil { + http.Error(w, "room not found", http.StatusNotFound) + return + } + if err := s.roomSvc.EnsurePermission(r.Context(), roomName, actor, room.PermManageRoom); err != nil { + http.Error(w, "forbidden: "+err.Error(), http.StatusForbidden) + return + } + settings := rm.Settings + if settings == nil { + settings = room.DefaultSettings() + } + if body.IsPublic != nil { + settings.IsPublic = *body.IsPublic + } + if body.RequireApproval != nil { + settings.RequireApproval = *body.RequireApproval + } + if body.MaxMembers != nil { + settings.MaxMembers = *body.MaxMembers + } + if body.ChatEnabled != nil { + settings.ChatEnabled = *body.ChatEnabled + } + if body.AllowGuestJoin != nil { + settings.AllowGuestJoin = *body.AllowGuestJoin + } + if body.Status != nil { + status := room.RoomStatus(*body.Status) + _ = s.roomSvc.Store().UpdateRoomStatus(r.Context(), roomName, status) + } + if body.DisplayName != nil || body.Description != nil || body.Password != nil || body.IsPublic != nil { + rm.DisplayName = stringOr(rm.DisplayName, body.DisplayName) + rm.Description = stringOr(rm.Description, body.Description) + rm.IsPublic = settings.IsPublic + rm.RequireApproval = settings.RequireApproval + rm.MaxMembers = settings.MaxMembers + if body.Password != nil { + rm.PasswordHash = *body.Password + } + rm.Settings = settings + rm.UpdatedAt = time.Now() + _ = s.roomSvc.Store().UpdateRoomSettings(r.Context(), roomName, settings) + } else { + _ = s.roomSvc.Store().UpdateRoomSettings(r.Context(), roomName, settings) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"ok": true}) + case http.MethodDelete: + u, _ := auth.FromContext(r.Context()) + actor := "" + if u != nil { + actor = u.Username + } + if err := s.roomSvc.EnsurePermission(r.Context(), roomName, actor, room.PermDeleteRoom); err != nil { + http.Error(w, "forbidden: "+err.Error(), http.StatusForbidden) + return + } + if err := s.roomSvc.Store().DeleteRoom(r.Context(), roomName); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + s.hub.removeRoom(roomName) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"ok": true}) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func (s *Server) handleManageMembers(w http.ResponseWriter, r *http.Request) { + roomName := r.PathValue("room") + if roomName == "" { + http.Error(w, "room required", http.StatusBadRequest) + return + } + switch r.Method { + case http.MethodGet: + members, err := s.roomSvc.Store().ListMembers(r.Context(), roomName) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + rm, _ := s.roomSvc.Store().GetRoom(r.Context(), roomName) + var settings *room.RoomSettings + if rm != nil { + settings = rm.Settings + } + out := make([]map[string]interface{}, 0, len(members)) + for _, m := range members { + eff := m.EffectivePermissions(settings) + out = append(out, map[string]interface{}{ + "username": m.Username, + "role": m.Role.String(), + "status": m.Status.String(), + "joined_at": m.JoinedAt.Unix(), + "effective_permissions": eff.Names(), + "effective_bits": eff.Bits(), + "added": m.AddedPerms, + "removed": m.RemovedPerms, + "admin_added": m.AdminAdded, + "admin_removed": m.AdminRemoved, + "version": m.Version, + }) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"members": out, "room": roomName}) + case http.MethodPost: + var body struct { + Username string `json:"username"` + Role string `json:"role"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "bad json: "+err.Error(), http.StatusBadRequest) + return + } + actorID := actorName(r) + role, err := room.ParseRoomRole(body.Role) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := s.roomSvc.UpdateMemberRole(r.Context(), roomName, actorID, body.Username, role); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"ok": true}) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func (s *Server) handleManageMemberRole(w http.ResponseWriter, r *http.Request) { + roomName := r.PathValue("room") + target := r.PathValue("user") + if roomName == "" || target == "" { + http.Error(w, "room and user required", http.StatusBadRequest) + return + } + switch r.Method { + case http.MethodPut, http.MethodPost: + var body struct { + Role string `json:"role"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "bad json", http.StatusBadRequest) + return + } + role, err := room.ParseRoomRole(body.Role) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := s.roomSvc.UpdateMemberRole(r.Context(), roomName, actorName(r), target, role); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + json.NewEncoder(w).Encode(map[string]interface{}{"ok": true}) + case http.MethodDelete: + if err := s.roomSvc.KickMember(r.Context(), roomName, actorName(r), target); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + json.NewEncoder(w).Encode(map[string]interface{}{"ok": true}) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func (s *Server) handleManageMemberPerms(w http.ResponseWriter, r *http.Request) { + roomName := r.PathValue("room") + target := r.PathValue("user") + if roomName == "" || target == "" { + http.Error(w, "room and user required", http.StatusBadRequest) + return + } + var body struct { + Added uint64 `json:"added"` + Removed uint64 `json:"removed"` + IsAdmin bool `json:"is_admin"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "bad json", http.StatusBadRequest) + return + } + if err := s.roomSvc.UpdateMemberPermissions(r.Context(), roomName, actorName(r), target, body.Added, body.Removed, body.IsAdmin); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + json.NewEncoder(w).Encode(map[string]interface{}{"ok": true}) +} + +func (s *Server) handleManagePermissionsMatrix(w http.ResponseWriter, r *http.Request) { + roomName := r.PathValue("room") + if r.Method == http.MethodGet && roomName == "" { + defs := map[string]interface{}{ + "roles": map[string]interface{}{ + "creator": map[string]interface{}{"bits": room.PermDefaultCreator.Bits(), "perms": room.PermDefaultCreator.Names()}, + "admin": map[string]interface{}{"bits": room.PermDefaultAdmin.Bits(), "perms": room.PermDefaultAdmin.Names()}, + "member": map[string]interface{}{"bits": room.PermDefaultMember.Bits(), "perms": room.PermDefaultMember.Names()}, + "guest": map[string]interface{}{"bits": room.PermDefaultGuest.Bits(), "perms": room.PermDefaultGuest.Names()}, + }, + "all_permissions": room.PermAll.Names(), + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(defs) + return + } + if roomName == "" { + http.Error(w, "room required", http.StatusBadRequest) + return + } + members, err := s.roomSvc.Store().ListMembers(r.Context(), roomName) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + rm, _ := s.roomSvc.Store().GetRoom(r.Context(), roomName) + settings := room.DefaultSettings() + if rm != nil && rm.Settings != nil { + settings = rm.Settings + } + matrix := make([]map[string]interface{}, 0, len(members)) + for _, m := range members { + matrix = append(matrix, map[string]interface{}{ + "username": m.Username, + "role": m.Role.String(), + "base": room.DefaultForRole(m.Role).Names(), + "effective": m.EffectivePermissions(settings).Names(), + "added": m.AddedPerms, + "removed": m.RemovedPerms, + }) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"room": roomName, "settings": settings, "matrix": matrix}) +} + +func (s *Server) handleManageOverview(w http.ResponseWriter, r *http.Request) { + rooms, _ := s.roomSvc.Store().ListRooms(r.Context()) + totalRooms := len(rooms) + liveRooms := 0 + totalMembers := 0 + for _, rm := range rooms { + if len(s.hub.targets(rm.Name)) > 0 { + liveRooms++ + } + cnt, _ := s.roomSvc.Store().CountMembers(r.Context(), rm.Name) + totalMembers += cnt + } + users := 0 + if s.auth != nil && s.auth.Store != nil { + users = len(s.auth.Store.List()) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "rooms_total": totalRooms, + "rooms_live": liveRooms, + "members_total": totalMembers, + "users_total": users, + "backends": s.svc.Backends(), + }) +} + +func (s *Server) handleManageJoin(w http.ResponseWriter, r *http.Request) { + roomName := r.PathValue("room") + var body struct { + Password string `json:"password"` + } + _ = json.NewDecoder(r.Body).Decode(&body) + user := actorName(r) + if user == "" { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + m, err := s.roomSvc.JoinRoom(r.Context(), roomName, user, body.Password) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "member": m}) +} + +func (s *Server) handleManageLeave(w http.ResponseWriter, r *http.Request) { + roomName := r.PathValue("room") + user := actorName(r) + if err := s.roomSvc.LeaveRoom(r.Context(), roomName, user); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + json.NewEncoder(w).Encode(map[string]interface{}{"ok": true}) +} + +func (s *Server) handleManageRoomSettings(w http.ResponseWriter, r *http.Request) { + roomName := r.PathValue("room") + rm, err := s.roomSvc.Store().GetRoom(r.Context(), roomName) + if err != nil { + http.Error(w, "room not found", http.StatusNotFound) + return + } + if r.Method == http.MethodGet { + json.NewEncoder(w).Encode(rm.Settings) + return + } + var newSettings room.RoomSettings + if err := json.NewDecoder(r.Body).Decode(&newSettings); err != nil { + http.Error(w, "bad json", http.StatusBadRequest) + return + } + if err := s.roomSvc.UpdateRoomSettings(r.Context(), roomName, actorName(r), &newSettings); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + json.NewEncoder(w).Encode(map[string]interface{}{"ok": true}) +} + +func actorName(r *http.Request) string { + u, _ := auth.FromContext(r.Context()) + if u != nil { + return u.Username + } + return "" +} +func stringOr(cur string, override *string) string { + if override != nil { + return *override + } + return cur +} diff --git a/internal/server/rooms.go b/internal/server/rooms.go index b86b7c4..8871477 100644 --- a/internal/server/rooms.go +++ b/internal/server/rooms.go @@ -8,13 +8,13 @@ import ( "sync" "time" - "gospeak-live-sfu-demo/gen" - "gospeak-live-sfu-demo/internal/db" + "sync-live/gen" + "sync-live/internal/db" ) // roomHub 维护房间 -> 各后端分发目标(StreamTarget)的内存状态, // 并向订阅者广播拓扑变化(实现「分流」状态实时可见)。 -// 默认由 Turso 嵌入式 (libSQL file) 持久化。 +// 默认由嵌入式 libSQL (file) 持久化。 type roomHub struct { mu sync.RWMutex rooms map[string]*roomEntry @@ -30,7 +30,7 @@ func newRoomHub() *roomHub { return &roomHub{rooms: map[string]*roomEntry{}} } -// newRoomHubWithDB 创建带 Turso 嵌入式持久化的 hub,启动时自动从 DB 加载全量房间。 +// newRoomHubWithDB 创建带嵌入式 libSQL 持久化的 hub,启动时自动从 DB 加载全量房间。 func newRoomHubWithDB(database *sql.DB) *roomHub { h := &roomHub{rooms: map[string]*roomEntry{}, db: database} if database != nil { @@ -38,7 +38,7 @@ func newRoomHubWithDB(database *sql.DB) *roomHub { defer cancel() all, err := db.LoadAll(ctx, database) if err != nil { - log.Printf("[hub] load from turso embedded failed: %v", err) + log.Printf("[hub] load from embedded db failed: %v", err) } else { for room, backends := range all { e := &roomEntry{targets: map[string]*gen.StreamTarget{}, subs: map[chan []byte]struct{}{}} @@ -48,7 +48,7 @@ func newRoomHubWithDB(database *sql.DB) *roomHub { h.rooms[room] = e } if len(all) > 0 { - log.Printf("[hub] loaded %d rooms from turso embedded", len(all)) + log.Printf("[hub] loaded %d rooms from embedded db", len(all)) } } } @@ -86,7 +86,7 @@ func (h *roomHub) setTarget(room, backend string, t *gen.StreamTarget) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if err := db.SaveTarget(ctx, h.db, room, backend, t); err != nil { - log.Printf("[hub] save target %s/%s to turso embedded: %v", room, backend, err) + log.Printf("[hub] save target %s/%s to embedded db: %v", room, backend, err) } } } @@ -178,3 +178,9 @@ func (h *roomHub) broadcast(room string, e *roomEntry) { } } } + +func (h *roomHub) removeRoom(name string) { + h.mu.Lock() + defer h.mu.Unlock() + delete(h.rooms, name) +} diff --git a/internal/server/server.go b/internal/server/server.go index 5052334..e3deead 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -12,12 +12,13 @@ import ( "time" "google.golang.org/grpc" - "gospeak-live-sfu-demo/gen" - "gospeak-live-sfu-demo/internal/auth" - "gospeak-live-sfu-demo/internal/config" - "gospeak-live-sfu-demo/internal/db" - "gospeak-live-sfu-demo/internal/sfu/cloudflare" - "gospeak-live-sfu-demo/internal/sfu/srs" + "sync-live/gen" + "sync-live/internal/auth" + "sync-live/internal/config" + "sync-live/internal/db" + "sync-live/internal/room" + "sync-live/internal/sfu/cloudflare" + "sync-live/internal/sfu/srs" ) //go:embed static @@ -34,17 +35,24 @@ type Server struct { auth *auth.Manager chat *chatHub chatRL *chatRateLimiter + roomSvc *room.Service + roomStore *room.Store } func New(cfg *config.Config) *Server { var hub *roomHub + var rs *room.Store + var rsvc *room.Service if cfg.DatabaseURL != "" { if dbConn, err := db.Open(cfg.DatabaseURL); err != nil { - log.Printf("[warn] turso db open failed (%v), falling back to memory hub: %s", err, cfg.DatabaseURL) + log.Printf("[warn] embedded db open failed (%v), falling back to memory hub: %s", err, cfg.DatabaseURL) hub = newRoomHub() } else { hub = newRoomHubWithDB(dbConn) - log.Printf("[db] turso enabled: dsn=%s remote=%v", cfg.DSN(), cfg.IsRemoteTurso()) + log.Printf("[db] embedded db enabled: dsn=%s remote=%v", cfg.DSN(), cfg.IsRemoteTurso()) + rs = room.NewStore(dbConn) + _ = room.InitSchema(dbConn) + rsvc = room.NewService(rs) } } else { hub = newRoomHub() @@ -52,7 +60,7 @@ func New(cfg *config.Config) *Server { cf := cloudflare.NewProvider(cfg.CFAppID, cfg.CFAppSecret, cfg.CFBaseURL, cfg.CFStunURL) srsP := srs.NewProvider(cfg.SRSBaseURL, cfg.SRSApp, cfg.SRSSecret, cfg.SRSCandidate) svc := NewService(cfg, cf, srsP, hub) - s := &Server{cfg: cfg, svc: svc, hub: hub} + s := &Server{cfg: cfg, svc: svc, hub: hub, roomStore: rs, roomSvc: rsvc} s.srsProxy = s.srsProxyHandler() s.cfProxy = s.cfProxyHandler() s.srsHlsProxy = s.srsHlsProxyHandler() @@ -73,7 +81,12 @@ func NewWithHub(cfg *config.Config, hub *roomHub) *Server { cf := cloudflare.NewProvider(cfg.CFAppID, cfg.CFAppSecret, cfg.CFBaseURL, cfg.CFStunURL) srsP := srs.NewProvider(cfg.SRSBaseURL, cfg.SRSApp, cfg.SRSSecret, cfg.SRSCandidate) svc := NewService(cfg, cf, srsP, hub) - s := &Server{cfg: cfg, svc: svc, hub: hub} + rs := room.NewStore(nil) + if hub != nil && hub.DB() != nil { + rs = room.NewStore(hub.DB()) + _ = room.InitSchema(hub.DB()) + } + s := &Server{cfg: cfg, svc: svc, hub: hub, roomStore: rs, roomSvc: room.NewService(rs)} s.srsProxy = s.srsProxyHandler() s.cfProxy = s.cfProxyHandler() s.srsHlsProxy = s.srsHlsProxyHandler() @@ -102,7 +115,7 @@ func (s *Server) StartGRPC() error { ) } gs := grpc.NewServer(opts...) - gen.RegisterLiveSFUServer(gs, NewGRPCServer(s.svc)) + gen.RegisterSyncLiveServer(gs, NewGRPCServer(s.svc)) go func() { _ = gs.Serve(lis) }() return nil } @@ -131,15 +144,22 @@ func (s *Server) Handler() http.Handler { mux.Handle("GET /publish", index) mux.Handle("GET /watch", index) mux.Handle("GET /login", index) + mux.Handle("GET /manage", index) + mux.Handle("GET /manage/", index) mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) mux.HandleFunc("POST /api/auth/login", s.handleLogin) mux.HandleFunc("POST /api/auth/register", s.handleRegister) mux.HandleFunc("POST /api/auth/logout", s.handleLogout) + mux.HandleFunc("POST /api/auth/refresh", s.handleRefresh) mux.Handle("GET /api/auth/me", s.authWrap(http.HandlerFunc(s.handleMe), "user", "list", true)) mux.Handle("GET /api/auth/users", s.authWrap(http.HandlerFunc(s.handleListUsers), "user", "list", true)) mux.Handle("POST /api/auth/users/role", s.authWrap(http.HandlerFunc(s.handleUpdateRole), "user", "manage", true)) + mux.Handle("POST /api/auth/users/ban", s.authWrap(http.HandlerFunc(s.handleBanUser), "user", "manage", true)) + mux.Handle("POST /api/auth/users/unban", s.authWrap(http.HandlerFunc(s.handleUnbanUser), "user", "manage", true)) mux.Handle("GET /api/auth/check", s.authWrap(http.HandlerFunc(s.handleAuthCheck), "config", "read", false)) + // 对标 SyncTV 的分页查询:GET /api/rooms/query?page=1&page_size=20&search=&status=active + mux.Handle("GET /api/rooms/query", s.authWrap(http.HandlerFunc(s.handleRoomsQuery), "room", "list", true)) mux.Handle("GET /api/config", s.authWrap(http.HandlerFunc(s.handleConfig), "config", "read", false)) mux.Handle("GET /api/rooms", s.authWrap(http.HandlerFunc(s.handleRooms), "room", "list", true)) @@ -155,6 +175,26 @@ func (s *Server) Handler() http.Handler { // 主播独立外部推送接口:仅 publisher/admin(room:publish),供 OBS/机器人/管理工具以 Bearer token 调用,消息标记 host mux.Handle("POST /api/room/{room}/broadcast", s.authWrap(http.HandlerFunc(s.handleRoomBroadcast), "room", "publish", true)) + // ---- Manage 统一管理面 ---- + mux.Handle("GET /api/manage/overview", s.authWrap(http.HandlerFunc(s.handleManageOverview), "config", "read", true)) + mux.Handle("GET /api/manage/rooms", s.authWrap(http.HandlerFunc(s.handleManageRooms), "room", "list", true)) + mux.Handle("POST /api/manage/rooms", s.authWrap(http.HandlerFunc(s.handleManageRooms), "room", "list", true)) + mux.Handle("GET /api/manage/rooms/{room}", s.authWrap(http.HandlerFunc(s.handleManageRoomDetail), "room", "list", true)) + mux.Handle("PUT /api/manage/rooms/{room}", s.authWrap(http.HandlerFunc(s.handleManageRoomDetail), "room", "list", true)) + mux.Handle("PATCH /api/manage/rooms/{room}", s.authWrap(http.HandlerFunc(s.handleManageRoomDetail), "room", "list", true)) + mux.Handle("DELETE /api/manage/rooms/{room}", s.authWrap(http.HandlerFunc(s.handleManageRoomDetail), "room", "list", true)) + mux.Handle("GET /api/manage/rooms/{room}/members", s.authWrap(http.HandlerFunc(s.handleManageMembers), "room", "list", true)) + mux.Handle("POST /api/manage/rooms/{room}/members", s.authWrap(http.HandlerFunc(s.handleManageMembers), "room", "list", true)) + mux.Handle("PUT /api/manage/rooms/{room}/members/{user}", s.authWrap(http.HandlerFunc(s.handleManageMemberRole), "room", "list", true)) + mux.Handle("DELETE /api/manage/rooms/{room}/members/{user}", s.authWrap(http.HandlerFunc(s.handleManageMemberRole), "room", "list", true)) + mux.Handle("POST /api/manage/rooms/{room}/members/{user}/perms", s.authWrap(http.HandlerFunc(s.handleManageMemberPerms), "room", "list", true)) + mux.Handle("GET /api/manage/permissions", s.authWrap(http.HandlerFunc(s.handleManagePermissionsMatrix), "config", "read", true)) + mux.Handle("GET /api/manage/permissions/{room}", s.authWrap(http.HandlerFunc(s.handleManagePermissionsMatrix), "room", "list", true)) + mux.Handle("GET /api/manage/rooms/{room}/settings", s.authWrap(http.HandlerFunc(s.handleManageRoomSettings), "room", "list", true)) + mux.Handle("PUT /api/manage/rooms/{room}/settings", s.authWrap(http.HandlerFunc(s.handleManageRoomSettings), "room", "list", true)) + mux.Handle("POST /api/manage/rooms/{room}/join", s.authWrap(http.HandlerFunc(s.handleManageJoin), "room", "subscribe", true)) + mux.Handle("POST /api/manage/rooms/{room}/leave", s.authWrap(http.HandlerFunc(s.handleManageLeave), "room", "subscribe", true)) + mux.Handle("GET /rtc/v1/", s.authWrap(s.srsProxy, "room", "publish", false)) mux.Handle("POST /rtc/v1/", s.authWrap(s.srsProxy, "room", "publish", false)) mux.Handle("PUT /rtc/v1/", s.authWrap(s.srsProxy, "room", "publish", false)) diff --git a/internal/server/service.go b/internal/server/service.go index 94ed41e..acb52d9 100644 --- a/internal/server/service.go +++ b/internal/server/service.go @@ -5,14 +5,14 @@ import ( "fmt" "time" - "gospeak-live-sfu-demo/gen" - "gospeak-live-sfu-demo/internal/auth" - "gospeak-live-sfu-demo/internal/config" - "gospeak-live-sfu-demo/internal/sfu/cloudflare" - "gospeak-live-sfu-demo/internal/sfu/srs" + "sync-live/gen" + "sync-live/internal/auth" + "sync-live/internal/config" + "sync-live/internal/sfu/cloudflare" + "sync-live/internal/sfu/srs" ) -// Service 实现 LiveSFU 控制面逻辑:协调房间在各 SFU 后端的发布/订阅拓扑。 +// Service 实现 SyncLive 控制面逻辑:协调房间在各 SFU 后端的发布/订阅拓扑。 type Service struct { cfg *config.Config cf *cloudflare.Provider @@ -45,6 +45,12 @@ func backendName(k gen.BackendKind) string { } } +func (s *Service) Backends() []*gen.BackendInfo { + resp, _ := s.GetConfig(context.Background(), &gen.GetConfigRequest{}) + if resp != nil { return resp.Backends } + return nil +} + func (s *Service) GetConfig(ctx context.Context, req *gen.GetConfigRequest) (*gen.GetConfigResponse, error) { resp := &gen.GetConfigResponse{ Candidate: s.srsP.Candidate(), @@ -189,7 +195,7 @@ func (s *Service) StopStream(ctx context.Context, req *gen.StopStreamRequest) (* return &gen.StopStreamResponse{Ok: true}, nil } -func (s *Service) WatchRoom(req *gen.WatchRoomRequest, stream gen.LiveSFU_WatchRoomServer) error { +func (s *Service) WatchRoom(req *gen.WatchRoomRequest, stream gen.SyncLive_WatchRoomServer) error { if err := s.checkAuth(stream.Context(), "room", "watch"); err != nil { return err } @@ -313,8 +319,18 @@ func (s *Service) UpdateUserRole(ctx context.Context, req *gen.UpdateUserRoleReq if req.GetUsername() == "" || req.GetRole() == "" { return nil, fmt.Errorf("username and role required") } - if err := s.auth.UpdateUserRole(req.GetUsername(), req.GetRole()); err != nil { - return nil, err + actor := "" + if u, ok := authContextUser(ctx); ok { + actor = u.Username + } + if actor != "" { + if err := s.auth.UpdateUserRoleAs(actor, req.GetUsername(), req.GetRole()); err != nil { + return nil, err + } + } else { + if err := s.auth.UpdateUserRole(req.GetUsername(), req.GetRole()); err != nil { + return nil, err + } } u, _ := s.auth.Store.Get(req.GetUsername()) return &gen.UpdateUserRoleResponse{ @@ -323,6 +339,22 @@ func (s *Service) UpdateUserRole(ctx context.Context, req *gen.UpdateUserRoleReq }, nil } +func authContextUser(ctx context.Context) (*auth.AuthedUser, bool) { + // helper to extract actor from gRPC/HTTP context (dual auth) + if md, ok := authFromContext(ctx); ok { + return md, true + } + return nil, false +} + +func authFromContext(ctx context.Context) (*auth.AuthedUser, bool) { + // try auth package context + if u, ok := ctx.Value(auth.ContextUserKey).(*auth.AuthedUser); ok { + return u, true + } + return nil, false +} + func (s *Service) findTarget(room string, kind gen.BackendKind) *gen.StreamTarget { for _, t := range s.hub.targets(room) { if t.GetBackend() == kind { diff --git a/internal/server/static/assets/arrow-right-CYxcOjf4.js b/internal/server/static/assets/arrow-right-BaPiev--.js similarity index 64% rename from internal/server/static/assets/arrow-right-CYxcOjf4.js rename to internal/server/static/assets/arrow-right-BaPiev--.js index f6766d1..d0dd499 100644 --- a/internal/server/static/assets/arrow-right-CYxcOjf4.js +++ b/internal/server/static/assets/arrow-right-BaPiev--.js @@ -1 +1 @@ -import{G as e,L as t,m as n}from"./ui-BEZm0Fjb.js";var r=[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]],i=i=>t(n,e(i,{iconNode:r,name:`arrow-right`}));export{i as t}; \ No newline at end of file +import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]],i=i=>t(n,e(i,{iconNode:r,name:`arrow-right`}));export{i as t}; \ No newline at end of file diff --git a/internal/server/static/assets/cloud-DZf51awA.js b/internal/server/static/assets/cloud-DZf51awA.js new file mode 100644 index 0000000..ba86ea1 --- /dev/null +++ b/internal/server/static/assets/cloud-DZf51awA.js @@ -0,0 +1 @@ +import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`path`,{d:`M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z`,key:`p7xjir`}]],i=i=>t(n,e(i,{iconNode:r,name:`cloud`}));export{i as t}; \ No newline at end of file diff --git a/internal/server/static/assets/eye-DaP7VxYZ.js b/internal/server/static/assets/eye-DaP7VxYZ.js new file mode 100644 index 0000000..e5d2bd0 --- /dev/null +++ b/internal/server/static/assets/eye-DaP7VxYZ.js @@ -0,0 +1 @@ +import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`path`,{d:`M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49`,key:`ct8e1f`}],[`path`,{d:`M14.084 14.158a3 3 0 0 1-4.242-4.242`,key:`151rxh`}],[`path`,{d:`M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143`,key:`13bj9a`}],[`path`,{d:`m2 2 20 20`,key:`1ooewy`}]],i=i=>t(n,e(i,{iconNode:r,name:`eye-off`})),a=[[`path`,{d:`M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0`,key:`1nclc0`}],[`circle`,{cx:`12`,cy:`12`,r:`3`,key:`1v7zrd`}]],o=r=>t(n,e(r,{iconNode:a,name:`eye`}));export{i as n,o as t}; \ No newline at end of file diff --git a/internal/server/static/assets/hash-ORiZzY9V.js b/internal/server/static/assets/hash-ORiZzY9V.js new file mode 100644 index 0000000..3da8a82 --- /dev/null +++ b/internal/server/static/assets/hash-ORiZzY9V.js @@ -0,0 +1 @@ +import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`line`,{x1:`4`,x2:`20`,y1:`9`,y2:`9`,key:`4lhtct`}],[`line`,{x1:`4`,x2:`20`,y1:`15`,y2:`15`,key:`vyu0kd`}],[`line`,{x1:`10`,x2:`8`,y1:`3`,y2:`21`,key:`1ggp8o`}],[`line`,{x1:`16`,x2:`14`,y1:`3`,y2:`21`,key:`weycgp`}]],i=i=>t(n,e(i,{iconNode:r,name:`hash`}));export{i as t}; \ No newline at end of file diff --git a/internal/server/static/assets/index-BTRwyDO1.js b/internal/server/static/assets/index-BTRwyDO1.js new file mode 100644 index 0000000..d9e3619 --- /dev/null +++ b/internal/server/static/assets/index-BTRwyDO1.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/routes-BsH3d8_E.js","assets/ui-CzB-gjVP.js","assets/arrow-right-BaPiev--.js","assets/cloud-DZf51awA.js","assets/hash-ORiZzY9V.js","assets/monitor-play-Bo4SAxB6.js","assets/radio-tower-UMy9OSpZ.js","assets/server-eXVtBH0Y.js","assets/shield-check-PAk2q1NC.js","assets/video-C1co3G53.js","assets/publish-D-rS1-js.js","assets/webrtc-CWAdwVhe.js","assets/message-square-1ZGuav88.js","assets/watch-sfB79fT6.js","assets/eye-DaP7VxYZ.js","assets/rooms-DcveOed7.js","assets/users-D7ZZ6mzL.js","assets/users-DA_5fDuj.js","assets/user-round-BkRPX9Ri.js","assets/login-B-50707a.js","assets/log-out-BSFDyM4m.js","assets/manage-BUOThicK.js"])))=>i.map(i=>d[i]); +import{$ as e,B as t,C as n,F as r,G as i,H as a,I as o,J as s,K as c,L as l,M as u,N as d,O as f,P as p,Q as m,R as h,S as g,T as _,U as v,V as y,W as b,X as x,Y as S,Z as C,b as w,et as T,g as ee,h as te,j as ne,k as E,m as D,p as re,q as ie,v as ae,w as O,x as oe,y as se,z as ce}from"./ui-CzB-gjVP.js";(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function le(e){return e[e.length-1]}function ue(e,t){return typeof e==`function`?e(t):e}var de=Object.prototype.hasOwnProperty,fe=Object.prototype.propertyIsEnumerable;function pe(e){for(let t in e)if(de.call(e,t))return!0;return!1}var me=()=>Object.create(null),he=(e,t)=>k(e,t,me);function k(e,t,n=()=>({}),r=0){if(e===t)return e;if(r>500)return t;let i=t,a=ye(e)&&ye(i);if(!a&&!(_e(e)&&_e(i)))return i;let o=a?e:ge(e);if(!o)return i;let s=a?i:ge(i);if(!s)return i;let c=o.length,l=s.length,u=a?Array(l):n(),d=0;for(let t=0;ti||!A(e[o],t[o],n)))return!1;return i===a}return!1}var be=/[\x00-\x1f\x7f"<>`{}]/g;function xe(e){return e.replace(be,e=>`%`+e.charCodeAt(0).toString(16).toUpperCase().padStart(2,`0`))}function Se(e){let t;try{t=decodeURI(e)}catch{t=e.replaceAll(/%[0-9A-F]{2}/gi,e=>{try{return decodeURI(e)}catch{return e}})}return xe(t)}var Ce=[`http:`,`https:`,`mailto:`,`tel:`];function we(e,t){if(!e)return!1;try{let n=new URL(e);return!t.has(n.protocol)}catch{return!1}}function Te(e){if(!e||!/[%\\\x00-\x1f\x7f]/.test(e)&&!e.startsWith(`//`))return{path:e,handledProtocolRelativeURL:!1};let t=/%25|%5C/gi,n=0,r=``,i;for(;(i=t.exec(e))!==null;)r+=Se(e.slice(n,i.index))+i[0],n=t.lastIndex;r+=Se(n?e.slice(n):e);let a=!1;return r.startsWith(`//`)&&(a=!0,r=`/`+r.replace(/^\/+/,``)),{path:r,handledProtocolRelativeURL:a}}function Ee(e){return/\s|[^\u0000-\u007F]/.test(e)?e.replace(/\s|[^\u0000-\u007F]/gu,encodeURIComponent):e}function De(e,t){if(e===t)return!0;if(e.length!==t.length)return!1;for(let n=0;n{e.next&&(e.prev?(e.prev.next=e.next,e.next.prev=e.prev,e.next=void 0,r&&(r.next=e,e.prev=r)):(e.next.prev=void 0,n=e.next,e.next=void 0,r&&(e.prev=r,r.next=e)),r=e)};return{get(e){let n=t.get(e);if(n)return i(n),n.value},set(a,o){if(t.size>=e&&n){let e=n;t.delete(e.key),e.next&&(n=e.next,e.next.prev=void 0),e===r&&(r=void 0)}let s=t.get(a);if(s)s.value=o,i(s);else{let e={key:a,value:o,prev:r};r&&(r.next=e),r=e,n||=e,t.set(a,e)}},clear(){t.clear(),n=void 0,r=void 0}}}var j=4,Ae=5;function je(e,t,n=new Uint16Array(6)){let r=e.indexOf(`/`,t),i=r===-1?e.length:r,a=e.substring(t,i);if(!a||!a.includes(`$`))return n[0]=0,n[1]=t,n[2]=t,n[3]=i,n[4]=i,n[5]=i,n;if(a===`$`){let r=e.length;return n[0]=2,n[1]=t,n[2]=t,n[3]=r,n[4]=r,n[5]=r,n}if(a.charCodeAt(0)===36)return n[0]=1,n[1]=t,n[2]=t+1,n[3]=i,n[4]=i,n[5]=i,n;let o=a.indexOf(`{`),s;if(o!==-1&&o+1!e.parse&&e.caseSensitive===p&&e.prefix===m&&e.suffix===h);if(_)n=_;else{let e=Fe(f,r,p,m,h);n=e,e.parent=i,e.depth=a;let t;t=f===1?i.dynamic??=[]:f===3?i.optional??=[]:i.wildcard??=[],t.push(e),t.length===2&&o?.push(t)}break}}i=n}if(d&&n.children&&!n.isRoot&&n.id&&n.id.charCodeAt(n.id.lastIndexOf(`/`)+1)===95){let e=Pe(r);e.kind=Ae,e.parent=i,a++,e.depth=a,i.pathless??=[],i.pathless.push(e),i=e}let f=(n.path||!n.children)&&!n.isRoot;if(f&&r.endsWith(`/`)){let e=Pe(r);e.kind=j,e.parent=i,a++,e.depth=a,i.index=e,i=e}i.parse=d??null,i.priority=s?.params?.priority??0,f&&!i.route&&(i.route=n,i.fullPath=r)}if(n.children)for(let r of n.children)Me(e,t,r,c,i,a,o,s)}function Ne(e,t){if(e.parse&&!t.parse)return-1;if(!e.parse&&t.parse)return 1;if(e.parse&&t.parse&&(e.priority||t.priority))return t.priority-e.priority;if(e.prefix&&t.prefix&&e.prefix!==t.prefix){if(e.prefix.startsWith(t.prefix))return-1;if(t.prefix.startsWith(e.prefix))return 1}if(e.suffix&&t.suffix&&e.suffix!==t.suffix){if(e.suffix.endsWith(t.suffix))return-1;if(t.suffix.endsWith(e.suffix))return 1}return e.prefix&&!t.prefix?-1:!e.prefix&&t.prefix?1:e.suffix&&!t.suffix?-1:!e.suffix&&t.suffix?1:e.caseSensitive&&!t.caseSensitive?-1:!e.caseSensitive&&t.caseSensitive?1:0}function Pe(e){return{kind:0,depth:0,pathless:null,index:null,static:null,staticInsensitive:null,dynamic:null,optional:null,wildcard:null,route:null,fullPath:e,parent:null,parse:null,priority:0}}function Fe(e,t,n,r,i){return{kind:e,depth:0,pathless:null,index:null,static:null,staticInsensitive:null,dynamic:null,optional:null,wildcard:null,route:null,fullPath:t,parent:null,parse:null,priority:0,caseSensitive:n,prefix:r,suffix:i}}function Ie(e,t){let n=Pe(`/`),r=new Uint16Array(6),i=[];for(let t of e)Me(!1,r,t,1,n,0,i);for(let e of i)e.sort(Ne);t.masksTree=n,t.flatCache=ke(1e3)}function Le(e,t){e||=`/`;let n=t.flatCache.get(e);if(n!==void 0)return n;let r=He(e,t.masksTree);return t.flatCache.set(e,r),r}function Re(e,t,n,r,i){e||=`/`,r||=`/`;let a=t?`case\0${e}`:e,o=i.singleCache.get(a);return o||(o=Pe(`/`),Me(t,new Uint16Array(6),{from:e},1,o,0),i.singleCache.set(a,o)),He(r,o,n)}function ze(e,t,n=!1){let r=n?e:`nofuzz\0${e}`,i=t.matchCache.get(r);if(i!==void 0)return i;e||=`/`;let a;try{a=He(e,t.segmentTree,n)}catch(e){if(e instanceof URIError)a=null;else throw e}return a&&(a.branch=We(a.route)),t.matchCache.set(r,a),a}function Be(e){return e===`/`?e:e.replace(/\/{1,}$/,``)}function Ve(e,t=!1,n){let r=Pe(e.fullPath),i=new Uint16Array(6),a=[],o={},s={},c=0;Me(t,i,e,1,r,0,a,e=>{if(n?.(e,c),e.id in o&&Oe(),o[e.id]=e,c!==0&&e.path){let t=Be(e.fullPath);(!s[t]||e.fullPath.endsWith(`/`))&&(s[t]=e)}c++});for(let e of a)e.sort(Ne);return{processedTree:{segmentTree:r,singleCache:ke(1e3),matchCache:ke(1e3),flatCache:null,masksTree:null},routesById:o,routesByPath:s}}function He(e,t,n=!1){let r=e.split(`/`),i=Ke(e,r,t,n);if(!i)return null;let[a]=Ue(e,r,i);return{route:i.node.route,rawParams:a}}function Ue(e,t,n){let r=Ge(n.node),i=null,a=Object.create(null),o=n.extract?.part??0,s=n.extract?.node??0,c=n.extract?.path??0,l=n.extract?.segment??0;for(;s=0;e--){let n=i.wildcard[e],{prefix:r,suffix:a}=n;if(!(r&&(_||!(n.caseSensitive?v:y??=v.toLowerCase()).startsWith(r)))){if(a){if(_)continue;let e=t.slice(u).join(`/`).slice(-a.length);if((n.caseSensitive?e:e.toLowerCase())!==a)continue}s.push({node:n,index:o,skipped:d,statics:f,dynamics:p,optionals:m,extract:h,rawParams:g})}}if(i.optional){let e=d|1<=0;t--){let n=i.optional[t];s.push({node:n,index:u,skipped:e,statics:f,dynamics:p,optionals:m,extract:h,rawParams:g})}if(!_)for(let e=i.optional.length-1;e>=0;e--){let t=i.optional[e],{prefix:n,suffix:r}=t;if(n||r){let e=t.caseSensitive?v:y??=v.toLowerCase();if(n&&!e.startsWith(n)||r&&!e.endsWith(r))continue}s.push({node:t,index:u+1,skipped:d,statics:f,dynamics:p,optionals:m+qe(o,u),extract:h,rawParams:g})}}if(!_&&i.dynamic&&v)for(let e=i.dynamic.length-1;e>=0;e--){let t=i.dynamic[e],{prefix:n,suffix:r}=t;if(n||r){let e=t.caseSensitive?v:y??=v.toLowerCase();if(n&&!e.startsWith(n)||r&&!e.endsWith(r))continue}s.push({node:t,index:u+1,skipped:d,statics:f,dynamics:p+qe(o,u),optionals:m,extract:h,rawParams:g})}if(!_&&i.staticInsensitive){let e=i.staticInsensitive.get(y??=v.toLowerCase());e&&s.push({node:e,index:u+1,skipped:d,statics:f+qe(o,u),dynamics:p,optionals:m,extract:h,rawParams:g})}if(!_&&i.static){let e=i.static.get(v);e&&s.push({node:e,index:u+1,skipped:d,statics:f+qe(o,u),dynamics:p,optionals:m,extract:h,rawParams:g})}if(i.pathless)for(let e=i.pathless.length-1;e>=0;e--){let t=i.pathless[e];s.push({node:t,index:u,skipped:d,statics:f,dynamics:p,optionals:m,extract:h,rawParams:g})}}if(l)return l;if(r&&c){let n=c.index;for(let e=0;ee.statics||t.statics===e.statics&&(t.dynamics>e.dynamics||t.dynamics===e.dynamics&&(t.optionals>e.optionals||t.optionals===e.optionals&&((t.node.kind===j)>(e.node.kind===j)||t.node.kind===j==(e.node.kind===j)&&t.node.depth>e.node.depth)))}function Ze(e){return Qe(e.filter(e=>e!==void 0).join(`/`))}function Qe(e){return e.replace(/\/{2,}/g,`/`)}function $e(e){return e===`/`?e:e.replace(/^\/{1,}/,``)}function M(e){let t=e.length;return t>1&&e[t-1]===`/`?e.replace(/\/{1,}$/,``):e}function et(e){return M($e(e))}function tt(e,t){return e?.endsWith(`/`)&&e!==`/`&&e!==`${t}/`?e.slice(0,-1):e}function nt(e,t,n){return tt(e,n)===tt(t,n)}function rt({base:e,to:t,trailingSlash:n=`never`,cache:r}){if(t.includes(`//`)&&(t=Qe(t)),t.startsWith(`/`))return t.length===1||n===`preserve`?t:n===`always`?t.endsWith(`/`)?t:`${t}/`:t.endsWith(`/`)?t.slice(0,-1):t;let i=t===`.`,a;if(r){a=i?e:e+`\0`+t;let n=r.get(a);if(n)return n}let o;if(i)o=e.split(`/`);else{for(e.includes(`//`)&&(e=Qe(e)),o=e.split(`/`);o.length>1&&le(o)===``;)o.pop();let n=t.split(`/`);for(let e=0,t=n.length;e1?o.pop():o=[``]:r===`.`||o.push(r)}}o.length>1&&(le(o)===``?n===`never`&&o.pop():n===`always`&&o.push(``));let s=o.join(`/`),c=(i?Qe(s):s)||`/`;return a&&r&&r.set(a,c),c}function it(e){let t=new Map(e.map(e=>[encodeURIComponent(e),e])),n=Array.from(t.keys()).map(e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)).join(`|`),r=new RegExp(n,`g`);return e=>e.replace(r,e=>t.get(e)??e)}function at(e,t,n){let r=t[e];return typeof r==`string`?e===`_splat`?/^[a-zA-Z0-9\-._~!/]*$/.test(r)?r:r.split(`/`).map(e=>st(e,n)).join(`/`):st(r,n):r}function ot({path:e,params:t,decoder:n,...r}){let i=!1,a=Object.create(null);if(!e||e===`/`)return{interpolatedPath:`/`,usedParams:a,isMissingParams:i};if(!e.includes(`$`))return{interpolatedPath:e,usedParams:a,isMissingParams:i};let o=e.length,s=0,c,l=``;for(;se.state.__TSR_key||e.href;function _t(e){let t=e.getAttribute(ht);if(t)return`[${ht}="${t}"]`;let n=``,r=e,i;for(;i=r.parentNode;){let e=1,t=r;for(;t=t.previousElementSibling;)e++;let a=`${r.localName}:nth-child(${e})`;n=n?`${a} > ${n}`:a,r=i}return n}var vt=!1,yt=`window`;function bt(e){try{return typeof e==`function`?e():document.querySelector(e)}catch{}}function xt(e){let t=new Set;for(let n of e){if(n===yt)continue;let e=bt(n);e&&t.add(e)}return t}function St(e,t){let n=t??e.options.scrollRestoration,r=e._scroll;n&&(r.restoring=!0);let i=e.options.getScrollRestorationKey||gt,a=new Set,o=e=>{let t=mt[e]||={};for(let e of a)e===document?t[yt]={scrollX,scrollY}:e.isConnected&&(t[_t(e)]={scrollX:e.scrollLeft,scrollY:e.scrollTop})};n&&!r.restoration&&(r.restoration=!0,vt=!1,history.scrollRestoration=`manual`,document.addEventListener(`scroll`,e=>{vt||a.add(e.target)},!0),e.subscribe(`onBeforeLoad`,e=>{e.fromLocation&&o(i(e.fromLocation)),a.clear()}),addEventListener(`pagehide`,()=>{o(i(e.stores.resolvedLocation.get()??e.stores.location.get())),pt()})),!r.reset&&(r.reset=!0,e.subscribe(`onRendered`,t=>{let n=e.options.scrollRestorationBehavior,o=e.options.scrollToTopSelectors,s=r.next,c=r.hash,l;if(a.clear(),r.next=!0,r.hash=!1,typeof e.options.scrollRestoration==`function`&&!e.options.scrollRestoration({location:e.latestLocation}))return;let u=i(t.toLocation),d=t.fromLocation&&i(t.fromLocation);if(r.restoring&&d&&d!==u){let e=mt[d];if(e){let t=mt[u];for(let n in e){if(n===yt){if(s)continue}else{let e=bt(n);if(!e||s&&o&&(l??=xt(o),l.has(e)))continue}t||=mt[u]={},t[n]??=e[n]}}}vt=!0;try{let e=t.toLocation.hash,i=t.toLocation.state.__hashScrollIntoViewOptions??!0,a=!1;if(s){!e&&o&&(l??=xt(o));let t=e&&i&&c,s=r.restoring?mt[u]:void 0;if(s)for(let e in s){let{scrollX:r,scrollY:i}=s[e];if(e===yt){if(t)continue;scrollTo({top:i,left:r,behavior:n}),a=!0}else{let t=bt(e);t&&(t.scrollLeft=r,t.scrollTop=i,l?.delete(t))}}if(!e){let e={top:0,left:0,behavior:n};if(a||scrollTo(e),l)for(let t of l)t.scrollTo(e)}}!a&&e&&i&&document.getElementById(e)?.scrollIntoView(i)}finally{vt=!1}}))}function Ct(e,t=String){let n=new URLSearchParams;for(let r in e){let i=e[r];i!==void 0&&n.set(r,t(i))}return n.toString()}function wt(e){return e?e===`false`?!1:e===`true`?!0:e*0==0&&+e+``===e?+e:e:``}function Tt(e){let t=new URLSearchParams(e),n=Object.create(null);for(let[e,r]of t.entries()){let t=n[e];t==null?n[e]=wt(r):Array.isArray(t)?t.push(wt(r)):n[e]=[t,wt(r)]}return n}var Et=/^(?:\s|["[{\d-]|fa|nu|tr)/,Dt=kt(JSON.parse),Ot=At(JSON.stringify,JSON.parse);function kt(e){return t=>{t[0]===`?`&&(t=t.substring(1));let n=Tt(t);for(let t in n){let r=n[t];if(typeof r==`string`)try{n[t]=e(r)}catch{}}return n}}function At(e,t){let n=t===JSON.parse;function r(r){if(r&&typeof r==`object`)try{return e(r)}catch{}else if(t&&typeof r==`string`){if(n&&!Et.test(r))return r;try{return t(r),e(r)}catch{}}return r}return e=>{let t=Ct(e,r);return t?`?${t}`:``}}var jt=`__root__`;function Mt(e){if(e.statusCode=e.statusCode||e.code||307,!e.reloadDocument&&typeof e.href==`string`)try{new URL(e.href),e.reloadDocument=!0}catch{}let t=new Headers(e.headers);e.href&&t.get(`Location`)===null&&t.set(`Location`,e.href);let n=new Response(null,{status:e.statusCode,headers:t});if(n.options=e,e.throw)throw n;return n}function Nt(e){return e instanceof Response&&!!e.options}function Pt(e){return{input:({url:t})=>{for(let n of e)t=It(n,t);return t},output:({url:t})=>{for(let n=e.length-1;n>=0;n--)t=Lt(e[n],t);return t}}}function Ft(e){let t=et(e.basepath),n=`/${t}`,r=e.caseSensitive?n:n.toLowerCase(),i=`${r}/`;return{input:({url:t})=>{let a=e.caseSensitive?t.pathname:t.pathname.toLowerCase();return a===r?t.pathname=`/`:a.startsWith(i)&&(t.pathname=t.pathname.slice(n.length)),t},output:({url:e})=>(e.pathname=Ze([`/`,t,e.pathname]),e)}}function It(e,t){let n=e?.input?.({url:t});if(n){if(typeof n==`string`)return new URL(n);if(n instanceof URL)return n}return t}function Lt(e,t){let n=e?.output?.({url:t});if(n){if(typeof n==`string`)return new URL(n);if(n instanceof URL)return n}return t}function Rt(e,t){let{createMutableStore:n,createReadonlyStore:r,batch:i}=t,a=new Map,o=n(`idle`),s=n(e),c=n(void 0),l=n([]),u=r(()=>l.get().map(e=>a.get(e).get())),d=r(()=>({status:o.get(),isLoading:o.get()===`pending`,matches:u.get(),location:s.get(),resolvedLocation:c.get()}));function f(e){let t=a.get(e);return t||(t=n(void 0),a.set(e,t)),t}let p={status:o,location:s,resolvedLocation:c,ids:l,matches:u,byRoute:a,__store:d,getMatchStore:f,setMatches:m};function m(e){let t=l.get(),n=e.map(e=>e.routeId);i(()=>{De(t,n)||l.set(n);for(let e of t)n.includes(e)||a.get(e).set(()=>void 0);for(let t of e){let e=f(t.routeId);e.get()!==t&&e.set(t)}})}return p}var N=`__TSR_index`,zt=`popstate`,Bt=`beforeunload`;function Vt(e){let t=e.getLocation(),n=new Set,r=r=>{t=e.getLocation(),n.forEach(e=>e({location:t,action:r}))},i=n=>{e.notifyOnIndexChange??!0?r(n):t=e.getLocation()},a=async({task:n,navigateOpts:r,...i})=>{if(r?.ignoreBlocker??!1){n();return}let a=e.getBlockers?.()??[],o=i.type===`PUSH`||i.type===`REPLACE`;if(typeof document<`u`&&a.length&&o)for(let n of a){let r=Gt(i.path,i.state);if(await n.blockerFn({currentLocation:t,nextLocation:r,action:i.type})){e.onBlocked?.();return}}n()};return{get location(){return t},get length(){return e.getLength()},subscribers:n,subscribe:e=>(n.add(e),()=>{n.delete(e)}),push:(n,i,o)=>{let s=t.state[N];i=Ht(s+1,i),a({task:()=>{e.pushState(n,i),r({type:`PUSH`})},navigateOpts:o,type:`PUSH`,path:n,state:i})},replace:(n,i,o)=>{let s=t.state[N];i=Ht(s,i),a({task:()=>{e.replaceState(n,i),r({type:`REPLACE`})},navigateOpts:o,type:`REPLACE`,path:n,state:i})},go:(t,n)=>{a({task:()=>{e.go(t),i({type:`GO`,index:t})},navigateOpts:n,type:`GO`})},back:t=>{a({task:()=>{e.back(t?.ignoreBlocker??!1),i({type:`BACK`})},navigateOpts:t,type:`BACK`})},forward:t=>{a({task:()=>{e.forward(t?.ignoreBlocker??!1),i({type:`FORWARD`})},navigateOpts:t,type:`FORWARD`})},canGoBack:()=>t.state[N]!==0,createHref:t=>e.createHref(t),block:t=>{if(!e.setBlockers)return()=>{};let n=e.getBlockers?.()??[];return e.setBlockers([...n,t]),()=>{let n=e.getBlockers?.()??[];e.setBlockers?.(n.filter(e=>e!==t))}},flush:()=>e.flush?.(),destroy:()=>e.destroy?.(),notify:r}}function Ht(e,t){t||={};let n=Kt();return{...t,key:n,__TSR_key:n,[N]:e}}function Ut(e){let t=e?.window??(typeof document<`u`?window:void 0),n=t.history.pushState,r=t.history.replaceState,i=[],a=()=>i,o=e=>i=e,s=e?.createHref??(e=>e),c=e?.parseLocation??(()=>Gt(`${t.location.pathname}${t.location.search}${t.location.hash}`,t.history.state));if(!t.history.state?.__TSR_key&&!t.history.state?.key){let e=Kt();t.history.replaceState({[N]:0,key:e,__TSR_key:e},``)}let l=c(),u,d=!1,f=!1,p=!1,m=!1,h=()=>l,g,_=()=>{g&&(S._ignoreSubscribers=!0,(g[2]?t.history.pushState:t.history.replaceState)(g[1],``,g[0]),S._ignoreSubscribers=!1,g=void 0,u=void 0)},v=(e,t,n)=>{let r=s(t),i=!!g;i||(u=l),l=Gt(t,n),g=[r,n,g?.[2]||e],i||queueMicrotask(()=>_())},y=e=>{l=c(),S.notify({type:e})},b=async()=>{if(f){f=!1;return}let e=c(),n=e.state[N]-l.state[N],r=n===1,i=n===-1,o=!r&&!i||d;d=!1;let s=o?`GO`:i?`BACK`:`FORWARD`,u=o?{type:`GO`,index:n}:{type:i?`BACK`:`FORWARD`};if(p)p=!1;else{let n=a();if(typeof document<`u`&&n.length){for(let r of n)if(await r.blockerFn({currentLocation:l,nextLocation:e,action:s})){f=!0,t.history.go(1),S.notify(u);return}}}l=c(),S.notify(u)},x=e=>{if(m){m=!1;return}let t=!1,n=a();if(typeof document<`u`&&n.length)for(let e of n){let n=e.enableBeforeUnload??!0;if(n===!0){t=!0;break}if(typeof n==`function`&&n()===!0){t=!0;break}}if(t)return e.preventDefault(),e.returnValue=``},S=Vt({getLocation:h,getLength:()=>t.history.length,pushState:(e,t)=>v(!0,e,t),replaceState:(e,t)=>v(!1,e,t),back:e=>(e&&(p=!0),m=!0,t.history.back()),forward:e=>{e&&(p=!0),m=!0,t.history.forward()},go:e=>{d=!0,t.history.go(e)},createHref:e=>s(e),flush:_,destroy:()=>{t.history.pushState=n,t.history.replaceState=r,t.removeEventListener(Bt,x,{capture:!0}),t.removeEventListener(zt,b)},onBlocked:()=>{u&&l!==u&&(l=u)},getBlockers:a,setBlockers:o,notifyOnIndexChange:!1});return t.addEventListener(Bt,x,{capture:!0}),t.addEventListener(zt,b),t.history.pushState=function(...e){let r=n.apply(t.history,e);return S._ignoreSubscribers||y(`PUSH`),r},t.history.replaceState=function(...e){let n=r.apply(t.history,e);return S._ignoreSubscribers||y(`REPLACE`),n},S}function Wt(e){let t=e.replace(/[\x00-\x1f\x7f]/g,``);return t.startsWith(`//`)&&(t=`/`+t.replace(/^\/+/,``)),t}function Gt(e,t){let n=Wt(e),r=n.indexOf(`#`),i=n.indexOf(`?`),a=Kt();return{href:n,pathname:n.substring(0,r>0?i>0?Math.min(r,i):r:i>0?i:n.length),hash:r>-1?n.substring(r):``,search:i>-1?n.slice(i,r===-1?void 0:r):``,state:t||{[N]:0,key:a,__TSR_key:a}}}function Kt(){return(Math.random()+1).toString(36).substring(7)}function qt(e){return e.options.loader||e.options.beforeLoad||e.lazyFn||e.options.component?.preload||e.options.pendingComponent?.preload}function Jt(e,t){return{fromLocation:t,toLocation:e,pathChanged:t?.pathname!==e.pathname,hrefChanged:t?.href!==e.href,hashChanged:t?.hash!==e.hash}}function Yt({key:e,__TSR_key:t,__TSR_index:n,__hashScrollIntoViewOptions:r,...i}){return i}function Xt(e,t,n,r){for(let i of t){if(r&&e._tx!==r)return;n.some(e=>e.routeId===i.routeId)||e.routesById[i.routeId].options.onLeave?.(i)}for(let i of n){if(r&&e._tx!==r)return;e.routesById[i.routeId].options[t.some(e=>e.routeId===i.routeId)?`onStay`:`onEnter`]?.(i)}}var Zt=class{constructor(e,t){this.tempLocationKey=`${Math.round(Math.random()*1e7)}`,this._scroll={next:!0},this.subscribers=new Set,this._cache=new Map,this._committed=[],this.routeBranchCache=new WeakMap,this.lightweightCache=new WeakMap,this.startTransition=async e=>(e(),!1),this.update=e=>{let t=this.options,n=this.basepath??t?.basepath??`/`,r=this.basepath===void 0,i=t?.rewrite;if(this.options={...t,...e},this.isServer=this.options.isServer??!1??typeof document>`u`,this.protocolAllowlist=new Set(this.options.protocolAllowlist),this.options.pathParamsAllowedCharacters&&(this.pathParamsDecoder=it(this.options.pathParamsAllowedCharacters)),(!this.history||this.options.history&&this.options.history!==this.history)&&(this.history=this.options.history?this.options.history:Ut()),this.origin=this.options.origin,this.origin||=window?.origin&&window.origin!==`null`?window.origin:`http://localhost`,this.history&&this.updateLatestLocation(),this.options.routeTree!==this.routeTree){this.routeTree=this.options.routeTree;let e;this.resolvePathCache=ke(1e3),e=this.buildRouteTree(),this.setRoutes(e)}if(!this.stores&&this.latestLocation){let e=this.getStoreConfig(this);this.batch=e.batch,this.stores=Rt(this.latestLocation,e),St(this)}let a=this.options.basepath??`/`,o=this.options.rewrite;if(r||n!==a||i!==o){this.basepath=a;let e=[],t=et(a);t&&t!==`/`&&e.push(Ft({basepath:a})),o&&e.push(o),this.rewrite=e.length===0?void 0:e.length===1?e[0]:Pt(e),this.history&&this.updateLatestLocation(),this.stores&&this.stores.location.set(this.latestLocation)}},this.updateLatestLocation=()=>{this.latestLocation=this.parseLocation(this.history.location,this.latestLocation)},this.buildRouteTree=()=>{let e=Ve(this.routeTree,this.options.caseSensitive,(e,t)=>{e.init({originalIndex:t})});return this.options.routeMasks&&Ie(this.options.routeMasks,e.processedTree),e},this.subscribe=(e,t)=>{let n={eventType:e,fn:t};return this.subscribers.add(n),()=>{this.subscribers.delete(n)}},this.emit=e=>{for(let t of this.subscribers)if(t.eventType===e.type)try{t.fn(e)}catch(e){console.error(e)}},this.parseLocation=(e,t)=>{let n=({pathname:e,search:n,hash:r,href:i,state:a})=>{if(!this.rewrite&&!/[ \x00-\x1f\x7f\u0080-\uffff]/.test(e)){let i=this.options.parseSearch(n),o=this.options.stringifySearch(i);return{href:e+o+r,publicHref:e+o+r,pathname:Te(e).path,external:!1,searchStr:o,search:he(t?.search,i),hash:Te(r.slice(1)).path,state:k(t?.state,a)}}let o=new URL(i,this.origin),s=It(this.rewrite,o),c=this.options.parseSearch(s.search),l=this.options.stringifySearch(c);return s.search=l,{href:s.href.replace(s.origin,``),publicHref:i,pathname:Te(s.pathname).path,external:!!this.rewrite&&s.origin!==this.origin,searchStr:l,search:he(t?.search,c),hash:Te(s.hash.slice(1)).path,state:k(t?.state,a)}},r=n(e),{__tempLocation:i,__tempKey:a}=r.state;if(i&&(!a||a===this.tempLocationKey)){let e=n(i);return e.state.key=r.state.key,e.state.__TSR_key=r.state.__TSR_key,delete e.state.__tempLocation,{...e,maskedLocation:r}}return r},this.resolvePathWithBase=(e,t)=>rt({base:e,to:t,trailingSlash:this.options.trailingSlash,cache:this.resolvePathCache}),this.matchRoutes=(e,t,n)=>typeof e==`string`?this.matchRoutesInternal({pathname:e,search:t},n):this.matchRoutesInternal(e,t),this.getMatchedRoutes=e=>{let t=Object.create(null),n=ze(M(e),this.processedTree,!0);return n&&Object.assign(t,n.rawParams),[n?.branch||[this.routesById.__root__],t,n?.route]},this.buildLocation=e=>{let t=(t={})=>{if(t.href){let e=Gt(t.href,{});t={...t,to:It(this.rewrite,new URL(e.pathname,this.origin)).pathname,search:this.options.parseSearch(e.search),hash:e.hash.slice(1)}}let n=t._fromLocation||this._pendingLocation||this.latestLocation,r=this.matchRoutesLightweight(n);t.from;let i=t.unsafeRelative===`path`?n.pathname:t.from??r[1],a=r[2],o=r[3],s=this.resolvePathWithBase(i,t.to?`${t.to}`:`.`),c=rn(t.params,o),l=this.routesByPath[M(s)],u;if(l)u=this.getRouteBranch(l);else if(s.includes(`$`))u=[];else{let[e,t,n]=this.getMatchedRoutes(s);u=e,this.options.notFoundRoute&&(!n||n.path!==`/`&&t[`**`])&&(u=[...u,this.options.notFoundRoute])}if(u.length&&pe(c))for(let e of u){let t=e.options.params?.stringify??e.options.stringifyParams;if(t){c===o&&(c=Object.assign(Object.create(null),c));try{Object.assign(c,t(c))}catch{}}}let d=e.leaveParams?s:Te(ot({path:s,params:c,decoder:this.pathParamsDecoder,server:this.isServer}).interpolatedPath).path,f=a;if(e._includeValidateSearch&&this.options.search?.strict){let e={};u.forEach(t=>{if(t.options.validateSearch)try{Object.assign(e,en(t.options.validateSearch,{...e,...f}))}catch{}}),f=e}f=tn(f,t,u,e._includeValidateSearch),f=he(a,f);let p=this.options.stringifySearch(f),m=t.hash===!0?n.hash:t.hash?ue(t.hash,n.hash):void 0,h=m?`#${m}`:``,g=t.state===!0?n.state:t.state?ue(t.state,n.state):{};t.state&&(g=k(n.state,g));let _=`${d}${p}${h}`,v,y,b=!1;if(this.rewrite){let e=new URL(_,this.origin),t=Lt(this.rewrite,e);v=e.href.replace(e.origin,``),t.origin===this.origin?y=t.pathname+t.search+t.hash:(y=t.href,b=!0)}else v=Ee(_),y=v;return{publicHref:y,href:v,pathname:d,search:f,searchStr:p,state:g,hash:m??``,external:b,unmaskOnReload:t.unmaskOnReload}},n=t(e);if(e.mask)n.maskedLocation=t({from:e.from,...e.mask});else if(this.options.routeMasks){let r=Le(n.pathname,this.processedTree);if(r){let i=Object.assign(Object.create(null),r.rawParams),{from:a,params:o,...s}=r.route,c=rn(o,i);n.maskedLocation=t({from:e.from,...s,params:c})}}return n},this.commitLocation=async({viewTransition:e,ignoreBlocker:t,...n})=>{let r,i=M(this.latestLocation.href)===M(n.href)&&A(Yt(n.state),Yt(this.latestLocation.state)),a=this._commitPromise,o,s=new Promise(e=>{o=e});if(s.resolve=()=>{o(),a?.resolve()},this._commitPromise=s,i)this.load();else{let{maskedLocation:i,hashScrollIntoView:a,...o}=n;i&&(o={...i,state:{...i.state,__tempKey:void 0,__tempLocation:{...o,search:o.searchStr,state:{...o.state,__tempKey:void 0,__tempLocation:void 0,__TSR_key:void 0,key:void 0}}}},(o.unmaskOnReload??this.options.unmaskOnReload??!1)&&(o.state.__tempKey=this.tempLocationKey)),o.state.__hashScrollIntoViewOptions=a??this.options.defaultHashScrollIntoView??!0,this.shouldViewTransition=e,r=n.replace?`REPLACE`:`PUSH`,this.history[r===`REPLACE`?`replace`:`push`](o.publicHref,o.state,{ignoreBlocker:t}),this.history.subscribers.size||this.load({action:{type:r}})}return this._scroll.next=n.resetScroll??!0,this._commitPromise},this.buildAndCommitLocation=({replace:e,resetScroll:t,hashScrollIntoView:n,viewTransition:r,ignoreBlocker:i,...a}={})=>{let o=this.buildLocation({...a,_includeValidateSearch:!0});this._pendingLocation=o;let s=this.commitLocation({...o,viewTransition:r,replace:e,resetScroll:t,hashScrollIntoView:n,ignoreBlocker:i});return queueMicrotask(()=>{this._pendingLocation===o&&(this._pendingLocation=void 0)}),s},this.navigate=async({to:e,reloadDocument:t,href:n,publicHref:r,...i})=>{let a=!1;if(n)try{new URL(`${n}`),a=!0}catch{}if(a&&!t&&(t=!0),t){if(e!==void 0||!n){let t=this.buildLocation({to:e,...i});n??=t.publicHref,r??=t.publicHref}let t=!a&&r?r:n;if(we(t,this.protocolAllowlist))return;if(!i.ignoreBlocker){let e=this.history.getBlockers?.()??[];for(let t of e)if(t?.blockerFn&&await t.blockerFn({currentLocation:this.latestLocation,nextLocation:this.latestLocation,action:`PUSH`}))return}i.replace?window.location.replace(t):window.location.href=t;return}return this.buildAndCommitLocation({...i,href:n,to:e,_isNavigate:!0})},this.load=async e=>{this.updateLatestLocation(),e?.action&&(this._scroll.hash=e.action.type===`PUSH`||e.action.type===`REPLACE`),await zn(this,e)},this.startViewTransition=e=>{let t=this.shouldViewTransition??this.options.defaultViewTransition;if(this.shouldViewTransition=void 0,t&&typeof document.startViewTransition==`function`){let n;if(typeof t==`object`&&window.CSS?.supports?.(`selector(:active-view-transition-type(a))`)){let r=this.latestLocation,i=this.stores.resolvedLocation.get(),a=typeof t.types==`function`?t.types(Jt(r,i)):t.types;if(a===!1)return e();n={update:e,types:a}}else n=e;return document.startViewTransition(n).updateCallbackDone}return e()},this.invalidate=e=>{let t=this._committed,n=e?.filter,r=this._preloads,i=new Set([...t,...this._cache.values(),...[...r?.values()??[]].flat(),...this._tx?.[3]??[]].filter(e=>!n||n(e)).map(e=>e.id)),a=[];for(let[e,t]of r??[])t.some(e=>i.has(e.id))&&(r.delete(e),a.push(e));let o=t=>{if(i.has(t.id)){let n=this.routesById[t.routeId],r={...t,invalid:!0,...(e?.forcePending||t.status===`error`||t.status===`notFound`)&&qt(n)?{status:`pending`,error:void 0}:void 0};return t._flight=void 0,r}return t};this._committed=t.map(o);for(let[t,n]of this._cache)i.has(t)&&(n.invalid=!0,e?.forcePending&&(n.status=`pending`));for(let e of i)this._flights?.delete(e);for(let e of a)e.abort();return this.shouldViewTransition=!1,this.load({sync:e?.sync})},this.resolveRedirect=e=>{let t=e.headers.get(`Location`);if(!e.options.href){let t=this.buildLocation(e.options).publicHref||`/`;e.options.href=t,e.headers.set(`Location`,t)}else if(t)try{let n=new URL(t);if(this.origin&&n.origin===this.origin){let t=n.pathname+n.search+n.hash;e.options.href=t,e.headers.set(`Location`,t)}}catch{}if(e.options.href&&we(e.options.href,this.protocolAllowlist))throw Error(`Redirect blocked: unsafe protocol`);return e.headers.get(`Location`)||e.headers.set(`Location`,e.options.href),e},this.clearCache=e=>{let t=this._cache,n=this._preloads,r=e?.filter,i=[],a=[];for(let[e,n]of t)(!r||r(n))&&(a.push(e),i.push(n));let o=[];for(let[e,t]of n??[])(!r||t.some(r))&&(o.push(e),i.push(...t));for(let e of a)t.delete(e);for(let e of o)n.delete(e);for(let e of i){let t=e._flight;e._flight=void 0,t&&!--t[2]&&(this._flights?.get(e.id)===t&&this._flights.delete(e.id),o.push(t[1]))}for(let e of o)e.abort()},this.loadRouteChunk=cn,this.preloadRoute=(e,t)=>Bn(this,e,0,t),this.matchRoute=(e,t)=>{let n={...e,to:e.to?this.resolvePathWithBase(e.from||``,e.to):void 0,params:e.params||{},leaveParams:!0},r=this.buildLocation(n),i=this.stores.status.get()===`pending`;if(t?.pending&&!i)return!1;let a=t?.pending??!i?this.latestLocation:this.stores.resolvedLocation.get()||this.stores.location.get(),o=Re(r.pathname,t?.caseSensitive??!1,t?.fuzzy??!1,a.pathname,this.processedTree);return!o||e.params&&!A(o.rawParams,e.params,{partial:!0})?!1:t?.includeSearch??!0?A(a.search,r.search,{partial:!0})?o.rawParams:!1:o.rawParams},this.getStoreConfig=t,this.update({defaultPreloadDelay:50,defaultPendingMs:1e3,defaultPendingMinMs:500,context:void 0,...e,caseSensitive:e.caseSensitive??!1,notFoundMode:e.notFoundMode??`fuzzy`,stringifySearch:e.stringifySearch??Ot,parseSearch:e.parseSearch??Dt,protocolAllowlist:e.protocolAllowlist??Ce}),self.__TSR_ROUTER__=this}isShell(){return!!this.options.isShell}get state(){return this.stores.__store.get()}setRoutes({routesById:e,routesByPath:t,processedTree:n}){this.routesById=e,this.routesByPath=t,this.processedTree=n;let r=this.options.notFoundRoute;r&&(r.init({originalIndex:99999999999}),this.routesById[r.id]=r)}getRouteBranch(e){let t=this.routeBranchCache.get(e);return t||(t=We(e),this.routeBranchCache.set(e,t)),t}matchRoutesInternal(e,t){let[n,r,i]=this.getMatchedRoutes(e.pathname),a=n,o=!1;(i?i.path!==`/`&&r[`**`]:M(e.pathname))&&(this.options.notFoundRoute?a=[...a,this.options.notFoundRoute]:o=!0);let s=o?nn(this.options.notFoundMode,a):void 0,c=Array(a.length),l=this._committed,u=(e,t)=>{let n=l[t];return n?.routeId===e.id?n:e===this.options.notFoundRoute?l.find(t=>t.routeId===e.id):void 0},d;for(let n=0;n{let r=n(t.preSearchFilters?t.preSearchFilters.reduce((e,t)=>t(e),e):e);return t.postSearchFilters?t.postSearchFilters.reduce((e,t)=>t(e),r):r});let n=t.validateSearch;r&&n&&i.push(({search:e,next:t,meta:r})=>{let i=t(e);try{let e=en(n,i);if(r&&e)for(let t in e)t in i||(r.defaulted||=new Map).set(t,e[t]);return{...i,...e}}catch{}return i})}let a=(e,n,r)=>{if(e>=i.length){if(!t.search)return{};if(t.search===!0)return n;let e=ue(t.search,n);return r&&(r.explicit=e),e}return i[e]({search:n,next:(t,n)=>{if(n){let n=r||{};return{search:a(e+1,t,n),meta:n}}return a(e+1,t,r)},meta:r})};return a(0,e)}function nn(e,t){if(e!==`root`){let e;for(let n=t.length-1;n>=0;n--){let r=t[n];if(r.options.notFoundComponent)return r.id;e||=r.children&&r.id}if(e)return e}return jt}function rn(e,t){if(e===!1||e===null)return Object.create(null);if((e??!0)===!0)return t;let n=Object.assign(Object.create(null),t);return Object.assign(n,ue(e,n))}function an(e,t){let n=e.options.params?.parse??e.options.parseParams;n&&Object.assign(t,n(t))}function on(e,t){return e.options[t]?.preload?.()}function sn(e,t){let n=on(e,`component`),r=on(e,`pendingComponent`),i=t&&r?r.then(t):r;return t&&!r&&t(),n&&i?Promise.all([n,i]).then(()=>{}):n??i}function cn(e,t,n){let r=()=>t===!1?void 0:t?on(e,t):sn(e,n),i=e._lazy;if(i)return i===!0?r():i.then(r);if(!e.lazyFn)return r();let a=e.lazyFn().then(t=>{{let{id:n,...r}=t.options;Object.assign(e.options,r),e._lazy=!0}},t=>{throw e._lazy=void 0,t});return e._lazy=a,a.then(r)}function ln(e){let t=e.findIndex(e=>e.status!==`success`||e._notFound)+1;return t&&t{let i=()=>r(t);t.addEventListener(`abort`,i,{once:!0}),Promise.resolve(e).then(n,r).finally(()=>t.removeEventListener(`abort`,i))})}function z(e,t){return e.routesById[t.routeId]}function fn(e,t,n){return Nt(e)?[I,e]:ct(e)?(e.routeId||=n,[un,e]):(t&&typeof e?.then==`function`&&(e=Error(`A Promise was thrown`,{cause:e})),t?[F,e]:[P,e])}function pn(e,t){let n=fn(t,!0,e.id);if(n[0]!==F)return n;try{e.options.onError?.(n[1])}catch(t){n=fn(t,!0,e.id)}return n}function mn(e,t,n,r,i){return i[0].signal.aborted?L:Dn(e,t,n,pn(n,r),i)}async function hn(e,t,n,r,i,a){let[o,s]=t,c=n[0].signal,l=!!n[3];for(let i=n[6]??0;ie.navigate({...t,_fromLocation:o}),buildLocation:e.buildLocation,cause:l?`preload`:r.cause,abortController:n[0],preload:l,matches:s,routeId:u.id},p=d;try{let e=r._ctx;!e&&u.options.context&&(e=r._ctx=u.options.context({...f,deps:r.loaderDeps,context:d})||{}),p={...d,...e},r.context=p}catch(a){return B(e,r),[i,mn(e,t,u,a,n)]}if(c.aborted)return[i,L];let m=r.paramsError??r.searchError;if(m!==void 0)return B(e,r),[i,mn(e,t,u,m,n)];let h=u.options.beforeLoad;if(!h)continue;let g={...f,search:r.search,context:p,...e.options.additionalContext},_=r.status;i>=a&&(r.status=`pending`,n[7]?.());try{vn(e,r,`beforeLoad`,n[0]);let a=await R(h(g),c);if(c.aborted)return[i,L];let o=Dn(e,t,u,fn(a,!1,u.id),n);if(o[0]!==P)return B(e,r),[i,o];r.context={...p,...a}}catch(a){return B(e,r),[i,mn(e,t,u,a,n)]}finally{r.status===`pending`&&(r.status=_),vn(e,r,!1,n[0])}}i()}function gn(e,t,n){if(!(!n||--n[2])){if(e._flights?.get(t.id)===n){let n=e._tx;if(n&&!n[0].signal.aborted&&!n[3].includes(t)&&n[3].some(e=>e.id===t.id)&&n[3].some(e=>e.isFetching===`beforeLoad`))return;e._flights.delete(t.id)}return n[1]}}function B(e,t){let n=t._flight;t._flight=void 0,gn(e,t,n)?.abort()}function V(e,t,n,r){let i=[];for(let a of t)if(!n?.includes(a)){let t=a._flight;if(a._flight=void 0,r&&t?.[2]===1&&e._flights?.get(a.id)===t&&n?.some(e=>e.id===a.id))t[2]=0;else{let n=gn(e,a,t);n&&i.push(n)}}for(let e of i)e.abort()}function _n(e){for(let t of e){let e=t._flight;e&&e[2]++}}function vn(e,t,n,r){if(t.isFetching=n,r&&e._tx?.[0]!==r)return;let i=e.stores.byRoute.get(t.routeId),a=i?.get();a?.id===t.id&&i.set({...a,isFetching:n})}function yn(e,t,n,r,i,a,o){let s=t[0];return{params:n.params,location:s,navigate:t=>e.navigate({...t,_fromLocation:s}),cause:o?`preload`:n.cause,abortController:i,preload:o,deps:n.loaderDeps,parentMatchPromise:a,context:n.context,route:r,...e.options.additionalContext}}async function bn(e,t,n,r,i,a,o){let s=o[0],c=s.signal;if(c.aborted)return L;if(!i)return[P,void 0];let l=n._flight;vn(e,n,`loader`,s);try{if(!l){let s=new AbortController;l=[Promise.resolve().then(()=>i(yn(e,t,n,r,s,a,!!o[3]))).then(e=>fn(e,!1,r.id),e=>fn(e,!0,r.id)).then(t=>(t[0]!==P&&e._flights?.get(n.id)===l&&(e._flights.delete(n.id),l[2]||s.abort()),t[0]===F&&l[2]?pn(r,t[1]):t)),s,1],(e._flights??=new Map).set(n.id,l)}return n._flight=l,n.abortController=l[1],Dn(e,t,r,await R(l[0],c),o)}catch(t){if(t!==c||!c.aborted)throw t;return B(e,n),L}finally{vn(e,n,!1,s)}}function xn(e,t,n){t[0]===P?(e.loaderData=t[1],e.error=void 0,e.status=`success`,e.invalid=!1,e.updatedAt=Date.now(),e.preload=n):t[0]!==I&&(e.status=`success`,e.error=void 0,e.invalid=!0)}function Sn(e,t,n){let r=e._cache.get(t.id);if(r!==n||e._committed.some(e=>e.id===t.id&&e._flight===t._flight))return;let i={...t,_notFound:void 0,context:{}};i._flight&&i._flight[2]++,e._cache.set(t.id,i),r&&B(e,r)}function Cn(e,t){return t[0]===F||t[0]===un?{...e,status:t[0]===F?`error`:`notFound`,error:t[1],_flight:void 0}:e}function wn(e,t,n,r,i,a,o){let s=t[1][n],c=z(e,s),l=!!a[3],u=e._cache.get(s.id),d,f=!1,p;try{if(s.status===`success`&&(d=c.options.shouldReload,typeof d==`function`&&(d=d(yn(e,t,s,c,a[0],i,l))),a[0].signal.aborted&&(p=L)),!p){if(s.status!==`success`)f=!0;else{let t=a[3]||s.preload?c.options.preloadStaleTime??e.options.defaultPreloadStaleTime??3e4:c.options.staleTime??e.options.defaultStaleTime??0;f=!!(s.invalid||d||d===void 0&&Date.now()-s.updatedAt>=t&&(a[5]||s.cause===`enter`||a[2].some(e=>e.routeId===s.routeId&&e.id!==s.id)))}}}catch(n){s.invalid=!0,B(e,s),p=mn(e,t,c,n,a)}let m=c.options.loader,h=typeof m==`function`?m:m?.handler,g=(!l||c.options.preload!==!1)&&m?e._flights?.get(s.id):void 0;g===s._flight||p?g=void 0:g&&!f&&!l&&d===void 0?f=!0:f||(g=void 0);let _=!!(m&&f&&s.status===`success`&&!l&&!a[4]&&((typeof m==`function`?void 0:m?.staleReloadMode)??e.options.defaultStaleReloadMode)!==`blocking`),v=f&&(!l||c.options.preload!==!1),y=v&&!_&&(s.status!==`success`||!!m),b=n>=o?a[7]:void 0,x=c.lazyFn&&c._lazy!==!0?b:void 0;if(v&&!m&&(s.invalid=!1,s.updatedAt=Date.now()),g&&g[2]++,y){let t=s._flight;s._flight=g,gn(e,s,t)?.abort(),n>=o&&(s.status=`pending`),b?.()}v||(s.isFetching=!1);let S=(p?Promise.resolve(p):y?bn(e,t,s,c,h,i,a):Promise.resolve([P,s.loaderData])).then(t=>(y&&(xn(s,t,l),t[0]===P&&(m&&!a[0].signal.aborted&&Sn(e,s,u),n>=o&&(s.status=`pending`))),t)),C=R(Promise.resolve().then(()=>cn(c,void 0,x)),a[0].signal).then(()=>void 0,r=>t[1].some((e,t)=>t<=n&&(e.status===`error`||e.status===`notFound`||e._notFound))?void 0:[n,mn(e,t,c,r,a)]).then(e=>S.then(t=>(y&&!e&&t[0]===P&&s.status===`pending`&&!a[0].signal.aborted&&(s.status=`success`,b?.()),e)));if(r.push([n,S,C]),!_)return S.then(e=>Cn(s,e));let w={...s,status:`pending`,preload:!1,_flight:g};s.invalid=!1,s.isFetching=`loader`;let T=bn(e,t,w,c,h,i,a).then(e=>(s.isFetching=!1,xn(w,e,!1),e));return(t[2]??=[]).push([n,T,C,w]),T.then(e=>Cn(w,e))}async function Tn(e,t,n,r,i=0){let a=n?.[1][1],o=a?.routeId?t.findIndex(e=>e.routeId===a.routeId):n?.[0]??t.length-1;o<0&&(o=0);for(let n=o;n>=0;n--){let i=z(e,t[n]);try{let e=cn(i,!1);e&&await R(e,r)}catch(e){if(e===r&&r.aborted)throw e}if(i.options.notFoundComponent)return n}return a?.routeId?o:i}function H(e,t){t[2]&&=(V(e,t[2].map(e=>e[3])),void 0)}async function En(e,t,n,r){let i;try{await Promise.all(e.map(e=>e[1].then(async t=>{let a=e[0];if(!(r&&a>=await r)){if(t[0]>=I)throw[a,t];!i&&t[0]!==P&&(i=[a,t],await Promise.all((n??[]).map(e=>{if(!(e[0]<=a))return e[1].then(t=>{if(t[0]===I)throw[e[0],t]})})))}})))}catch(e){return e}return t??i}function Dn(e,t,n,r,i,a){for(;r[0]===I;){let o=r[1];if(o.options.reloadDocument?i[3]:i[1]>=20)return r;try{return o.options.href&&o.options.reloadDocument?(e.resolveRedirect(o),r):[I,o,e.buildLocation({...o.options,_fromLocation:t[0],_includeValidateSearch:!0})]}catch(e){r=a?[F,e]:pn(n,e),a=!0}}return r}async function On(e,t,n,r,i,a){let o=t[1],s=await i,c=!1,l=o.findIndex(e=>e._notFound),u=t=>t[1][0]===un?Tn(e,o,t,r.signal):t[0],d=l<0?o.length:l;if((s?.[1][0]??0)>=I)d=0;else if(s){d=s[2]??=await u(s);for(let e of n){if(e[0]>=d)break;let t=await e[1];if(t[0]!==P&&t[0]=d)break;let t=await e[2];if(t){s=t;break}}if((s?.[1][0]??0)>=I){let n=s[1];if(n[0]!==I||n[1].options.reloadDocument||n[2])return H(e,t),n;c=!0,s=[0,[F,Error(`Too many redirects`)]]}let f=s?s[2]??await u(s):l;if(f>=0){let i=s?.[1],l=i?.[0],u=o[f],d=i?.[1],p=()=>{i&&(u._notFound=void 0,l===F?u.status=`error`:(d.routeId=u.routeId,u.routeId===e.routeTree.id?(u.status=`success`,u._notFound=!0):u.status=`notFound`),u.error=d,u.isFetching=!1)};p(),i||a?.();let m=z(e,u);try{await R(i?Promise.resolve().then(()=>cn(m,l===F?`errorComponent`:`notFoundComponent`)):Promise.all([cn(m),cn(m,`notFoundComponent`)]),r.signal)}catch(n){if(n===r.signal&&r.signal.aborted)return H(e,t),L}i?c&&(r.abort(),await Promise.all([...n.map(e=>e[1]),...n.map(e=>e[2]),...(t[2]??[]).map(e=>e[1])]),H(e,t),V(e,o),p()):u.status=`success`}return t}async function kn(e,t,n,r=0,i=t[1].length){let a=t[1];for(let t=r;te._notFound);if(e.options.notFoundMode!==`root`&&s>=0){let t=await Tn(e,i[1],void 0,a,s);t!==s&&(n[s]._notFound=void 0,n[t]._notFound=!0),s=t}let c=s<0?n.length:s+1,l=0;for(;l{for(let t=d;t=I&&(c=0);p()}if(!a.aborted&&!r[3]){let t=[];for(let[n,r]of e._flights??[])r[2]||(e._flights.delete(n),t.push(r[1]));for(let e of t)e.abort()}let h=On(e,i,u,r[0],En(u,m,i[2]),r[7]);i[2]?.length&&(i[3]=En(i[2],void 0,void 0,h.then(e=>dn(e)?0:ln(e[1]).length,()=>0))),o=await h}catch(t){if(H(e,i),t===a&&a.aborted)return L;throw t}return dn(o)?o:kn(e,o,a,r[6]===o[1].length?r[6]:0)}function jn(e,t){if(e._tx!==t)return;let n=t[3],r=e.stores.matches.get(),i=e._pending;for(let a=0;a0){i[3]=setTimeout(()=>jn(e,t),n);return}i[2]=0}let m=n.map(e=>({...e,_flight:void 0}));m[a].status=`pending`;let h=i[4]=e.startTransition(()=>e.stores.setMatches(m),m).then(t=>(t&&e._pending===i&&i[4]===h&&!i[2]&&(i[2]=Date.now()+f),t));return}}function U(e,t){let n=e._pending;(e._tx===t||!e._tx?.[3].some(e=>e.id===n?.[1]))&&(clearTimeout(n?.[3]),e._pending=void 0)}async function Mn(e,t){let n=e._pending;if(!n)return;clearTimeout(n[3]);let r=n[2]-Date.now();if(!n[4]||r<=0||!ln(t[3]).some(e=>e.id===n[1]))return;let i;try{await R(new Promise(e=>{i=setTimeout(e,r)}),t[0].signal)}catch{}clearTimeout(i)}function Nn(e,t){e._committed=t,e.stores.setMatches(t)}function Pn(e,t,n,r){let i=e._committed,a=e._cache;for(let e of n)e.preload=!1,r&&(e._assetEnd=void 0);let o=ln(n).length,s=new Map;{let t=Date.now();for(let r of[...i,...a.values()]){if(r.status!==`success`||n.some((e,t)=>e.id===r.id&&(t=(r.preload?i.options.preloadGcTime??e.options.defaultPreloadGcTime??3e5:i.options.gcTime??e.options.defaultGcTime??3e5)||s.set(r.id,a.get(r.id)===r?r:{...r,_flight:void 0,isFetching:!1,context:{}})}}t[3]=[],e._cache=s,Nn(e,n),V(e,[...a.values(),...i],[...n,...s.values()]),Xt(e,i,n,t)}async function Fn(e,t){let n=e._tx;for(;n&&n!==t;){if(await n[5],e._tx===n)return;n=e._tx}}async function In(e,t,n){let r=n[1],i=n[2];if(!i){await e.navigate({...r.options,replace:!0,ignoreBlocker:!0});return}if(r.options.reloadDocument){await e.navigate({href:i.publicHref,reloadDocument:!0,replace:!0,ignoreBlocker:!0});return}i._redirects=t[1]+1,e._pendingLocation=i;let a=e.commitLocation({...i,viewTransition:r.options.viewTransition,replace:!0,resetScroll:r.options.resetScroll,hashScrollIntoView:r.options.hashScrollIntoView,ignoreBlocker:!0});queueMicrotask(()=>{e._pendingLocation===i&&(e._pendingLocation=void 0)}),await a}async function Ln(e,t,n,r,i){let a=n.map(e=>({...e}));_n(a);for(let t of r)B(e,a[t[0]]),a[t[0]]=t[3];let o=[t[2],a],s;try{s=await On(e,o,r,t[0],i)}catch(t){throw V(e,a),t}if(dn(s)){V(e,a),s[0]===I&&e._tx===t&&e._committed===n&&await In(e,t,s);return}let c=await kn(e,s,t[0].signal);if(e._tx!==t||e._committed!==n){V(e,c[1]);return}for(let t of c[1]){let n=e._cache.get(t.id);n?._flight&&n._flight===t._flight&&(e._cache.delete(t.id),B(e,n))}Nn(e,c[1]),V(e,n,c[1])}async function Rn(e,t,n,r,i,a){let o=[t[0],t[1],e._committed,void 0,i,n,a,r],s=await An(e,t[2],t[3],o);if(dn(s)){let n=s[0]===I&&e._tx===t;if((!n||s[1].options.reloadDocument)&&U(e,t),V(e,t[3]),t[3]=[],!n)return;if(e._tx!==t){U(e,t);return}await In(e,t,s);return}if(e._tx!==t){U(e,t),V(e,s[1]),H(e,s);return}if(await Mn(e,t),e._tx!==t){U(e,t),V(e,s[1]),H(e,s);return}let c=t[2],l=Jt(c,e.stores.resolvedLocation.get()),u=s[2];await e.startViewTransition(async()=>{if(e._tx!==t){U(e,t),V(e,s[1]),H(e,s);return}if(await Mn(e,t),e._tx!==t){U(e,t),V(e,s[1]),H(e,s);return}let n=await e.startTransition(()=>{U(e,t),Pn(e,t,s[1],a),e._tx===t&&(e.emit({type:`onLoad`,...l}),e._tx===t&&e.emit({type:`onBeforeRouteMount`,...l}))},s[1]);if(e._tx!==t){H(e,s);return}u?.length&&Ln(e,t,s[1],u,s[3]).catch(console.error),e.batch(()=>{e.stores.resolvedLocation.set(c),e.stores.status.set(`idle`),e._tx===t&&e.emit({type:`onResolved`,...l}),n&&e._tx===t&&e.emit({type:`onRendered`,...l})}),e._tx===t&&(e._commitPromise?.resolve(),e._commitPromise=void 0)})}async function zn(e,t){let n=e._tx,r=e.stores.resolvedLocation.get(),i=r??e.stores.location.get(),a=e.latestLocation,o=e._pendingLocation,s=o?.href===a.href?o._redirects??0:0,c=e._handoff,l=c?.[0](),u=new AbortController,d=e._preflight;if(e._preflight=u,l||c?.[1](),d?.abort(),u.signal.aborted){await Fn(e,n);return}let f=Jt(a,r);if(e.emit({type:`onBeforeNavigate`,...f}),u.signal.aborted||e.emit({type:`onBeforeLoad`,...f}),u.signal.aborted){await Fn(e,n);return}let p=i.href===a.href,m=u,h=e.matchRoutes(a,{_controller:u});_n(h);let g=l?c[1](h):void 0;if(g?m=l:l?.abort(),u.signal.aborted){V(e,h),await Fn(e,n);return}e._preflight=void 0;let _=[m,s,a,h,Date.now(),Promise.resolve().then(()=>Rn(e,_,p,()=>jn(e,_),t?.sync,g)).then()];if(e._tx=_,n){for(let t of e.stores.matches.get()){if(e._tx!==_)break;t.isFetching&&vn(e,t,!1)}n[0].abort(),V(e,n[3],_[3],!0)}if(e._tx!==_){V(e,_[3]),_[3]=[],await Fn(e,_);return}e.batch(()=>{e.stores.status.set(`pending`),e.stores.location.set(a)}),(g||!e._committed.length&&!h.some(e=>e._notFound))&&jn(e,_),await _[5],await Fn(e,_)}async function Bn(e,t,n=0,r){let i=r??e.buildLocation(t),a=e._committed,o=new AbortController,s;try{s=e.matchRoutes(i,{_controller:o}),_n(s)}catch(e){o.abort(),ct(e)||console.error(e);return}(e._preloads??=new Map).set(o,s);let c;try{let t;try{t=await An(e,i,s,[o,n,a,!0])}finally{c=e._preloads.delete(o),V(e,s),o.abort()}if(!dn(t))return t[1];if(c&&t[0]===I&&!t[1].options.reloadDocument)return Bn(e,t[1].options,n+1,t[2])}catch(e){ct(e)||console.error(e)}}var Vn=`Error preloading route! ☝️`,Hn=class{get to(){return this._to}get id(){return this._id}get path(){return this._path}get fullPath(){return this._fullPath}constructor(e){if(this.init=e=>{this.originalIndex=e.originalIndex;let t=this.options,n=!t?.path&&!t?.id;this.parentRoute=this.options.getParentRoute?.(),n?this._path=jt:this.parentRoute||Oe();let r=n?jt:t?.path;r&&r!==`/`&&(r=$e(r));let i=t?.id||r,a=n?jt:Ze([this.parentRoute.id===`__root__`?``:this.parentRoute.id,i]);r===`__root__`&&(r=`/`),a!==`__root__`&&(a=Ze([`/`,a]));let o=a===`__root__`?`/`:Ze([this.parentRoute.fullPath,r]);this._path=r,this._id=a,this._fullPath=o,this._to=M(o)},this.addChildren=e=>this._addFileChildren(e),this._addFileChildren=e=>(Array.isArray(e)&&(this.children=e),typeof e==`object`&&e&&(this.children=Object.values(e)),this),this._addFileTypes=()=>this,this.updateLoader=e=>(Object.assign(this.options,e),this),this.update=e=>(Object.assign(this.options,e),this),this.lazy=e=>(this.lazyFn=e,this),this.redirect=e=>Mt({from:this.fullPath,...e}),this.options=e||{},this.isRoot=!e?.getParentRoute,e?.id&&e?.path)throw Error(`Route cannot have both an 'id' and a 'path' option.`)}},Un=class extends Hn{constructor(e){super(e)}},Wn=E(`
Something went wrong!
`),Gn=E(`
`),Kn=E(``);function qn(e){return h(ne,{fallback:(n,r)=>(e.onCatch?.(n),t(ie(e.getResetKey,()=>r(),{defer:!0})),h(w,{get component(){return e.errorComponent??Jn},error:n,reset:r})),get children(){return e.children}})}function Jn({error:e}){let[t,r]=b(!1);return(()=>{var i=Wn(),a=i.firstChild,o=a.firstChild.nextSibling;return a.nextSibling,o.$$click=()=>r(e=>!e),n(o,()=>t()?`Hide Error`:`Show Error`),n(i,(()=>{var r=O(()=>!!t());return()=>r()?(()=>{var t=Gn(),r=t.firstChild;return n(r,(()=>{var t=O(()=>!!e.message);return()=>t()?(()=>{var t=Kn();return n(t,()=>e.message),t})():null})()),t})():null})(),null),i})()}g([`click`]);function Yn(e){let t=Zn();return h(p,{get when(){return t()},get fallback(){return e.fallback??null},get children(){return O(()=>e.children)}})}var Xn=!1;function Zn(){let[e,t]=b(Xn&&!x.context);return S(()=>{Xn=!0,t(!0)}),e}function Qn(e){return(...t)=>{for(let n of e)n&&n(...t)}}function $n(...e){return Qn(e)}var er=ce(null);function W(e){return T(er)}function tr(e,n,r){let i=typeof IntersectionObserver==`function`,a=null;return t(()=>{let t=e();if(r()||!t||!i){s(()=>n());return}a=new IntersectionObserver(e=>{n(e.pop())},{rootMargin:`100px`}),a.observe(t),s(()=>{a?.disconnect(),n()})}),()=>a}var nr=E(``),rr=E(``),ir=new WeakMap,ar=e=>{clearTimeout(ir.get(e)),ir.delete(e)};function or(n){let r=W(),i=!!r.options.ssr,a=Zn(),o=!1,[s,l]=C(c({activeProps:cr,inactiveProps:ur},n),[`activeProps`,`inactiveProps`,`activeOptions`,`to`,`preload`,`preloadDelay`,`preloadIntentProximity`,`hashScrollIntoView`,`replace`,`startTransition`,`resetScroll`,`viewTransition`,`target`,`disabled`,`style`,`class`,`onClick`,`onBlur`,`onFocus`,`onMouseEnter`,`onMouseLeave`,`onMouseOver`,`onMouseOut`,`onTouchStart`,`ignoreBlocker`]),[u,d]=C(l,[`params`,`search`,`hash`,`state`,`mask`,`reloadDocument`,`unsafeRelative`,`from`]),f=y(()=>r.stores.location.get(),void 0,{equals:(e,t)=>e.href===t.href}),p=y(()=>{let t={_fromLocation:f(),...n};return e(()=>r.buildLocation(t))}),m=y(()=>{if(n.disabled)return;let e=p().maskedLocation??p(),t=e.publicHref;return e.external?{href:t,external:!0}:{href:r.history.createHref(t)||`/`,external:!1}}),h=y(()=>{let e=m();if(e?.external)return we(e.href,r.protocolAllowlist)?void 0:e.href;let t=n.to;if(!hr(t)&&typeof t==`string`&&t.indexOf(`:`)!==-1)try{return new URL(t),we(t,r.protocolAllowlist)?void 0:t}catch{}}),g=y(()=>n.reloadDocument||h()||s.disabled?!1:s.preload??r.options.defaultPreload),_=()=>s.preloadDelay??r.options.defaultPreloadDelay??0,v=y(()=>{if(h())return!1;let e=s.activeOptions,t=f(),n=p();if(e?.exact){if(!nt(t.pathname,n.pathname,r.basepath))return!1}else{let e=tt(t.pathname,r.basepath),i=tt(n.pathname,r.basepath);if(!(e.startsWith(i)&&(e.length===i.length||e[i.length]===`/`)))return!1}return(e?.includeSearch??!0)&&!A(t.search,n.search,{partial:!e?.exact,ignoreUndefined:!e?.explicitUndefined})?!1:!e?.includeHash||(i&&!a()?``:t.hash)===n.hash}),x=()=>r.preloadRoute(n,p()).catch(e=>{console.warn(e),console.warn(Vn)}),[S,w]=b(null),T=e=>{if(!e){ar(S);return}if(!(e.isIntersecting??g()===`intent`)){e.isIntersecting===!1&&ar(S);return}if(!_()){x();return}ir.has(S)||ir.set(S,setTimeout(()=>{ir.delete(S),x()},_()))};if(tr(S,T,()=>g()!==`viewport`),t(()=>{o||g()===`render`&&(x(),o=!0)}),h())return c(d,{ref:$n(w,n.ref),href:h()},C(s,[`target`,`disabled`,`style`,`class`,`onClick`,`onBlur`,`onFocus`,`onMouseEnter`,`onMouseLeave`,`onMouseOut`,`onMouseOver`,`onTouchStart`])[0]);let ee=e=>{let t=e.currentTarget.getAttribute(`target`),i=s.target===void 0?t:s.target;!s.disabled&&!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&(!i||i===`_self`)&&e.button===0&&(e.preventDefault(),r.navigate({...n,replace:s.replace,resetScroll:s.resetScroll,hashScrollIntoView:s.hashScrollIntoView,startTransition:s.startTransition,viewTransition:s.viewTransition,ignoreBlocker:s.ignoreBlocker}))},te=()=>{g()===`intent`&&x()},ne=()=>{g()===`intent`&&ar(S)},E=y(()=>s.activeProps===cr&&s.inactiveProps===ur&&s.class===void 0&&s.style===void 0),D=G(()=>s.onClick,ee),re=G(()=>s.onBlur,ne),ie=G(()=>s.onFocus,T),ae=G(()=>s.onMouseEnter,T),O=G(()=>s.onMouseOver,T),oe=G(()=>s.onMouseLeave,ne),se=G(()=>s.onMouseOut,ne),ce=G(()=>s.onTouchStart,te),le=y(()=>{let e=v(),t={href:m()?.href,ref:$n(w,n.ref),onClick:D,onBlur:re,onFocus:ie,onMouseEnter:ae,onMouseOver:O,onMouseLeave:oe,onMouseOut:se,onTouchStart:ce,disabled:!!s.disabled,target:s.target,...s.disabled&&fr};if(E())return{...t,...e&&dr};let r=e?ue(s.activeProps,{})??lr:lr,i=e?lr:ue(s.inactiveProps,{}),a={...s.style,...r.style,...i.style},o=[s.class,r.class,i.class].filter(Boolean).join(` `);return{...r,...i,...t,...pe(a)?{style:a}:void 0,...o?{class:o}:void 0,...e&&pr}});return c(d,le)}var sr={class:`active`},cr=()=>sr,lr={},ur=()=>lr,dr={class:`active`,"data-status":`active`,"aria-current":`page`},fr={role:`link`,"aria-disabled":!0},pr={"data-status":`active`,"aria-current":`page`};function mr(e,t){return typeof t==`function`?t(e):t[0](t[1],e),e.defaultPrevented}function G(e,t){return n=>{let r=e();(!r||!mr(n,r))&&t(n)}}var K=e=>{let[t,r]=C(e,[`_asChild`,`children`]),[i,a]=C(or(r),[`type`]),o=y(()=>{let e=t.children;return typeof e==`function`?e({get isActive(){return a[`data-status`]===`active`}}):e});if(t._asChild===`svg`){let[e,t]=C(a,[`class`]);return(()=>{var e=nr(),r=e.firstChild;return f(r,t,!1,!0),n(r,o),e})()}return t._asChild?h(w,c({get component(){return t._asChild}},a,{get children(){return o()}})):(()=>{var e=rr();return f(e,a,!1,!0),n(e,o),e})()};function hr(e){if(typeof e!=`string`)return!1;let t=e.charCodeAt(0);return t===47?e.charCodeAt(1)!==47:t===46}var gr=ce([()=>void 0,()=>void 0]);function q(e){let n=W(),r=T(gr),i=e.from?void 0:r,a=()=>e.from?n.stores.getMatchStore(e.from).get():i?.[1]();return t(()=>{a()===void 0&&(e.shouldThrow??!0)&&Oe()}),y(t=>{let n=a();if(n===void 0)return;let r=e.select?e.select(n):n;return t===void 0?r:k(t,r)})}function _r(e){return q({from:e.from,strict:e.strict,select:t=>e.select?e.select(t.loaderData):t.loaderData})}function vr(e){return q({...e,select:t=>e.select?e.select(t.loaderDeps):t.loaderDeps})}function yr(e){return q({from:e.from,strict:e.strict,shouldThrow:e.shouldThrow,select:t=>{let n=e.strict===!1?t.params:t._strictParams;return e.select?e.select(n):n}})}function br(e){return q({from:e.from,strict:e.strict,shouldThrow:e.shouldThrow,select:t=>{let n=t.search;return e.select?e.select(n):n}})}function xr(e){let t=W();return n=>t.navigate({...n,from:n.from??e?.from})}function Sr(e){return q({...e,select:t=>e.select?e.select(t.context):t.context})}var J=class extends Hn{constructor(e){super(e),this.useMatch=e=>q({select:e?.select,from:this.id}),this.useRouteContext=e=>Sr({...e,from:this.id}),this.useSearch=e=>br({select:e?.select,from:this.id}),this.useParams=e=>yr({select:e?.select,from:this.id}),this.useLoaderDeps=e=>vr({...e,from:this.id}),this.useLoaderData=e=>_r({...e,from:this.id}),this.useNavigate=()=>xr({from:this.fullPath}),this.Link=e=>{let t=this;return h(K,c({get from(){return t.fullPath}},e))}}},Cr=class extends Un{constructor(e){super(e),this.useMatch=e=>q({select:e?.select,from:this.id}),this.useRouteContext=e=>Sr({...e,from:this.id}),this.useSearch=e=>br({select:e?.select,from:this.id}),this.useParams=e=>yr({select:e?.select,from:this.id}),this.useLoaderDeps=e=>vr({...e,from:this.id}),this.useLoaderData=e=>_r({...e,from:this.id}),this.useNavigate=()=>xr({from:this.fullPath}),this.Link=e=>{let t=this;return h(K,c({get from(){return t.fullPath}},e))}}};function wr(e){let t=e.stores.resolvedLocation.get();if(t?.href===e.latestLocation.href&&t.state.__TSR_key===e.latestLocation.state.__TSR_key)return t}function Tr(){let e=W(),t;return e.startTransition=e=>(t?.(!1),new Promise((n,r)=>{let i=e=>{t===i&&(t=void 0,n(e))},a=e=>{t===i&&(t=void 0,r(e))};t=i,m(()=>{if(t===i)try{e()}catch(e){a(e)}}).then(()=>i(!0),a)})),s(()=>{t?.(!1)}),S(()=>{s(e.history.subscribe(e.load));let t=e.buildLocation({to:e.latestLocation.pathname,search:!0,params:!0,hash:!0,state:!0,_includeValidateSearch:!0});if(M(e.latestLocation.publicHref)!==M(t.publicHref)){e.commitLocation({...t,replace:!0,ignoreBlocker:!0});return}!wr(e)&&!e._tx&&e.load().catch(console.error)}),null}function Er(){let e=W();return S(()=>{let t=wr(e);t&&e.emit({type:`onRendered`,...Jt(t,t)})}),null}function Y(e){return O(()=>e.children)}var Dr=E(`

Not Found`);function Or(e){if(ct(e))return e;if(ct(e?.cause))return e.cause}function kr(e){let t=W(),n=y(()=>t.stores.location.get().pathname),r=y(()=>t.stores.status.get());return h(qn,{getResetKey:()=>`not-found-${n()}-${r()}`,onCatch:t=>{let n=Or(t);if(n)e.onCatch?.(n);else throw t},errorComponent:({error:t})=>{let n=Or(t);if(n)return e.fallback?.(n);throw t},get children(){return e.children}})}function Ar(){return Dr()}function jr(e,t,n){return t.options.notFoundComponent?h(t.options.notFoundComponent,n):e.options.defaultNotFoundComponent?h(e.options.defaultNotFoundComponent,n):h(Ar,{})}var Mr=void 0,Nr=e=>{let t=W(),n=y(()=>t.stores.byRoute.get(e.routeId).get()),i=[()=>e.routeId,n],a=t.routesById[e.routeId],s=()=>(n(),a.options),l=()=>s().pendingComponent??t.options.defaultPendingComponent,u=()=>s().errorComponent??t.options.defaultErrorComponent,f=()=>a.isRoot?s().notFoundComponent??t.options.notFoundRoute?.options.component:s().notFoundComponent,m=()=>n().ssr===!1||n().ssr===`data-only`,g=()=>n().ssr===`data-only`,_=a.isRoot?a.options.shellComponent??Y:Y,v=()=>h(p,{get when(){return n().status!==`pending`},get fallback(){return h(w,{get component(){return l()}})},get children(){return h(Pr,{})}});return h(_,{get children(){return[h(gr.Provider,{value:i,get children(){return h(r,{get fallback(){if(!g())return h(w,{get component(){return l()}})},get children(){return h(w,{get component(){return u()?qn:Y},getResetKey:n,get errorComponent(){return u()},onCatch:e=>{let r=Or(e);if(r)throw r.routeId??=n().routeId,r;(a.options.onCatch??t.options.defaultOnCatch)?.(e)},get children(){return h(w,{get component(){return f()?kr:Y},fallback:e=>{let t=Or(e)??e;if(t.routeId??=n().routeId,t.routeId!==n().routeId)throw t;return h(w,c({get component(){return f()}},t))},get children(){return h(o,{get children(){return[h(d,{get when(){return m()},get children(){return h(Yn,{get fallback(){return h(w,{get component(){return l()}})},get children(){return h(v,{})}})}}),h(d,{get when(){return!m()},get children(){return h(v,{})}})]}})}})}})}})}}),O(()=>Mr?.(t,a))]}})},Pr=()=>{let e=W(),t=T(gr),n=t[1],r=t[0],i=e.routesById[r()],a=()=>n(),s=()=>{let t=a(),n=i.options.remountDeps??e.options.defaultRemountDeps;if(!n)return r();let o=n({routeId:r(),loaderDeps:t.loaderDeps,params:t._strictParams,search:t._strictSearch});return JSON.stringify(o)??r()},c=()=>{let t=i.options.component??e.options.defaultComponent;return h(t||Fr,{})},l=()=>h(p,{get when(){return s()},keyed:!0,children:e=>c()});return h(o,{get children(){return[h(d,{get when(){return a().status===`notFound`},children:t=>jr(e,i,a().error)}),h(d,{get when(){return a().status===`error`},children:e=>{throw a().error}}),h(d,{get when(){return a().status===`success`},get children(){return l()}})]}})},Fr=()=>{let e=W(),t=T(gr),n=t[1],i=t[0],a=e.routesById[i()],o=()=>{if(n()._notFound)return;let t=e.stores.ids.get();return t[t.indexOf(i())+1]};return h(p,{get when(){return o()},keyed:!0,get fallback(){return O(()=>!!n()._notFound)()?jr(e,a,n().error):void 0},children:t=>h(p,{get when(){return i()===jt},get fallback(){return h(Nr,{get routeId(){return o()}})},get children(){return h(r,{get fallback(){return h(w,{get component(){return e.options.defaultPendingComponent}})},get children(){return h(Nr,{get routeId(){return o()}})}})}})})};function Ir(){let e=W(),t=typeof document<`u`&&e.ssr?Y:r,n=e.routesById[jt].options.pendingComponent??e.options.defaultPendingComponent,i=e.options.InnerWrap||Y;return h(i,{get children(){return h(t,{get fallback(){return n?h(n,{}):null},get children(){return[h(Tr,{}),h(Lr,{}),h(Er,{})]}})}})}function Lr(){let e=W(),t=()=>e.stores.ids.get()[0],n=()=>t()?e.stores.byRoute.get(t())?.get():void 0,r=[t,n],i=()=>h(p,{get when(){return t()},get children(){return h(Nr,{get routeId(){return t()}})}});return h(gr.Provider,{value:r,get children(){return O(()=>!!e.options.disableGlobalCatchBoundary)()?i():h(qn,{getResetKey:n,errorComponent:Jn,get onCatch(){},get children(){return i()}})}})}function Rr(e){let[t,n]=b(e);return{get:t,set:n}}function zr(e){return{get:v(()=>y(e))}}var Br=e=>({createMutableStore:Rr,createReadonlyStore:zr,batch:l}),Vr=e=>new Hr(e),Hr=class extends Zt{constructor(e){super(e,Br)}};function Ur({router:e,children:t,...n}){e.update({...e.options,...n,context:{...e.options.context,...n.context}});let r=e.options.Wrap||Y;return h(r,{get children(){return h(er.Provider,{value:e,get children(){return t()}})}})}function Wr({router:e,...t}){return h(Ur,c({router:e},t,{children:()=>h(Ir,{})}))}function Gr(e){let t=W();if(!e?.select)return()=>t.stores.location.get();let n=e.select;return y(e=>{let r=n(t.stores.location.get());return e===void 0?r:k(e,r)})}var Kr=class{constructor(){this.listeners=new Set,this.subscribe=this.subscribe.bind(this)}subscribe(e){return this.listeners.add(e),this.onSubscribe(),()=>{this.listeners.delete(e),this.onUnsubscribe()}}hasListeners(){return this.listeners.size>0}onSubscribe(){}onUnsubscribe(){}},qr=new class extends Kr{#e;#t;#n;constructor(){super(),this.#n=e=>{if(typeof window<`u`&&window.addEventListener){let t=()=>e();return window.addEventListener(`visibilitychange`,t,!1),()=>{window.removeEventListener(`visibilitychange`,t)}}}}onSubscribe(){this.#t||this.setEventListener(this.#n)}onUnsubscribe(){this.hasListeners()||(this.#t?.(),this.#t=void 0)}setEventListener(e){this.#n=e,this.#t?.(),this.#t=e(e=>{typeof e==`boolean`?this.setFocused(e):this.onFocus()})}setFocused(e){this.#e!==e&&(this.#e=e,this.onFocus())}onFocus(){let e=this.isFocused();this.listeners.forEach(t=>{t(e)})}isFocused(){return typeof this.#e==`boolean`?this.#e:globalThis.document?.visibilityState!==`hidden`}},Jr={setTimeout:(e,t)=>setTimeout(e,t),clearTimeout:e=>clearTimeout(e),setInterval:(e,t)=>setInterval(e,t),clearInterval:e=>clearInterval(e)},Yr=new class{#e=Jr;setTimeoutProvider(e){this.#e=e}setTimeout(e,t){return this.#e.setTimeout(e,t)}clearTimeout(e){this.#e.clearTimeout(e)}setInterval(e,t){return this.#e.setInterval(e,t)}clearInterval(e){this.#e.clearInterval(e)}};function Xr(e){setTimeout(e,0)}var Zr=typeof window>`u`||`Deno`in globalThis;function X(){}function Qr(e,t){return typeof e==`function`?e(t):e}function $r(e){return typeof e==`number`&&e>=0&&e!==1/0}function ei(e,t){return Math.max(e+(t||0)-Date.now(),0)}function ti(e,t){return typeof e==`function`?e(t):e}function ni(e,t){return typeof e==`function`?e(t):e}function ri(e,t){let{type:n=`all`,exact:r,fetchStatus:i,predicate:a,queryKey:o,stale:s}=e;if(o){if(r){if(t.queryHash!==ai(o,t.options))return!1}else if(!si(t.queryKey,o))return!1}if(n!==`all`){let e=t.isActive();if(n===`active`&&!e||n===`inactive`&&e)return!1}return!(typeof s==`boolean`&&t.isStale()!==s||i&&i!==t.state.fetchStatus||a&&!a(t))}function ii(e,t){let{exact:n,status:r,predicate:i,mutationKey:a}=e;if(a){if(!t.options.mutationKey)return!1;if(n){if(oi(t.options.mutationKey)!==oi(a))return!1}else if(!si(t.options.mutationKey,a))return!1}return!(r&&t.state.status!==r||i&&!i(t))}function ai(e,t){return(t?.queryKeyHashFn||oi)(e)}function oi(e){return JSON.stringify(e,(e,t)=>di(t)?Object.keys(t).sort().reduce((e,n)=>(e[n]=t[n],e),{}):t)}function si(e,t){if(e===t)return!0;if(typeof e!=typeof t)return!1;if(e&&t&&typeof e==`object`&&typeof t==`object`){if(Array.isArray(e)&&Array.isArray(t)){for(let n=0;n500)return t;let r=ui(e)&&ui(t);if(!r&&!(di(e)&&di(t)))return t;let i=(r?e:Object.keys(e)).length,a=r?t:Object.keys(t),o=a.length,s=r?Array(o):{},c=0;for(let l=0;l{Yr.setTimeout(t,e)})}function mi(e,t,n){return typeof n.structuralSharing==`function`?n.structuralSharing(e,t):n.structuralSharing===!1?t:li(e,t)}function hi(e,t,n=0){let r=[...e,t];return n&&r.length>n?r.slice(1):r}function gi(e,t,n=0){let r=[t,...e];return n&&r.length>n?r.slice(0,-1):r}var _i=Symbol();function vi(e,t){return!e.queryFn&&t?.initialPromise?()=>t.initialPromise:!e.queryFn||e.queryFn===_i?()=>Promise.reject(Error(`Missing queryFn: '${e.queryHash}'`)):e.queryFn}function yi(e,t,n){let r=!1,i;return Object.defineProperty(e,"signal",{enumerable:!0,get:()=>(i??=t(),r?i:(r=!0,i.aborted?n():i.addEventListener(`abort`,n,{once:!0}),i))}),e}var bi=(()=>{let e=()=>Zr;return{isServer(){return e()},setIsServer(t){e=t}}})();function xi(){let e,t,n=new Promise((n,r)=>{e=n,t=r});n.status=`pending`,n.catch(()=>{});function r(e){Object.assign(n,e),delete n.resolve,delete n.reject}return n.resolve=t=>{r({status:`fulfilled`,value:t}),e(t)},n.reject=e=>{r({status:`rejected`,reason:e}),t(e)},n}var Si=Xr;function Ci(){let e=[],t=0,n=e=>{e()},r=e=>{e()},i=Si,a=r=>{t?e.push(r):i(()=>{n(r)})},o=()=>{let t=e;e=[],t.length&&i(()=>{r(()=>{t.forEach(e=>{n(e)})})})};return{batch:e=>{let n;t++;try{n=e()}finally{t--,t||o()}return n},batchCalls:e=>(...t)=>{a(()=>{e(...t)})},schedule:a,setNotifyFunction:e=>{n=e},setBatchNotifyFunction:e=>{r=e},setScheduler:e=>{i=e}}}var Z=Ci(),wi=new class extends Kr{#e=!0;#t;#n;constructor(){super(),this.#n=e=>{if(typeof window<`u`&&window.addEventListener){let t=()=>e(!0),n=()=>e(!1);return window.addEventListener(`online`,t,!1),window.addEventListener(`offline`,n,!1),()=>{window.removeEventListener(`online`,t),window.removeEventListener(`offline`,n)}}}}onSubscribe(){this.#t||this.setEventListener(this.#n)}onUnsubscribe(){this.hasListeners()||(this.#t?.(),this.#t=void 0)}setEventListener(e){this.#n=e,this.#t?.(),this.#t=e(this.setOnline.bind(this))}setOnline(e){this.#e!==e&&(this.#e=e,this.listeners.forEach(t=>{t(e)}))}isOnline(){return this.#e}};function Ti(e){return Math.min(1e3*2**e,3e4)}function Ei(e){return(e??`online`)!==`online`||wi.isOnline()}var Di=class extends Error{constructor(e){super(`CancelledError`),this.revert=e?.revert,this.silent=e?.silent}};function Oi(e){let t=!1,n=0,r,i=xi(),a=()=>i.status!==`pending`,o=t=>{if(!a()){let n=new Di(t);f(n),e.onCancel?.(n)}},s=()=>{t=!0},c=()=>{t=!1},l=()=>qr.isFocused()&&(e.networkMode===`always`||wi.isOnline())&&e.canRun(),u=()=>Ei(e.networkMode)&&e.canRun(),d=e=>{a()||(r?.(),i.resolve(e))},f=e=>{a()||(r?.(),i.reject(e))},p=()=>new Promise(t=>{r=e=>{(a()||l())&&t(e)},e.onPause?.()}).then(()=>{r=void 0,a()||e.onContinue?.()}),m=()=>{if(a())return;let r,i=n===0?e.initialPromise:void 0;try{r=i??e.fn()}catch(e){r=Promise.reject(e)}Promise.resolve(r).then(d).catch(r=>{if(a())return;let i=e.retry??(bi.isServer()?0:3),o=e.retryDelay??Ti,s=typeof o==`function`?o(n,r):o,c=i===!0||typeof i==`number`&&nl()?void 0:p()).then(()=>{t?f(r):m()})})};return{promise:i,status:()=>i.status,cancel:o,continue:()=>(r?.(),i),cancelRetry:s,continueRetry:c,canStart:u,start:()=>(u()?m():p().then(m),i)}}var ki=class{#e;destroy(){this.clearGcTimeout()}scheduleGc(){this.clearGcTimeout(),$r(this.gcTime)&&(this.#e=Yr.setTimeout(()=>{this.optionalRemove()},this.gcTime))}updateGcTime(e){this.gcTime=Math.max(this.gcTime||0,e??(bi.isServer()?1/0:3e5))}clearGcTimeout(){this.#e!==void 0&&(Yr.clearTimeout(this.#e),this.#e=void 0)}};function Ai(e){return{onFetch:(t,n)=>{let r=t.options,i=t.fetchOptions?.meta?.fetchMore?.direction,a=t.state.data?.pages||[],o=t.state.data?.pageParams||[],s={pages:[],pageParams:[]},c=0,l=async()=>{let n=!1,l=e=>{yi(e,()=>t.signal,()=>n=!0)},u=vi(t.options,t.fetchOptions),d=async(e,r,i)=>{if(n)return Promise.reject(t.signal.reason);if(r==null&&e.pages.length)return Promise.resolve(e);let a=(()=>{let e={client:t.client,queryKey:t.queryKey,pageParam:r,direction:i?`backward`:`forward`,meta:t.options.meta};return l(e),e})(),o=await u(a),{maxPages:s}=t.options,c=i?gi:hi;return{pages:c(e.pages,o,s),pageParams:c(e.pageParams,r,s)}};if(i&&a.length){let e=i===`backward`,t=e?Mi:ji,n={pages:a,pageParams:o};s=await d(n,t(r,n),e)}else{let t=e??a.length;do{let e=c===0?o[0]??r.initialPageParam:ji(r,s);if(c>0&&e==null)break;s=await d(s,e),c++}while(ct.options.persister?.(l,{client:t.client,queryKey:t.queryKey,meta:t.options.meta,signal:t.signal},n):l}}}function ji(e,{pages:t,pageParams:n}){let r=t.length-1;return t.length>0?e.getNextPageParam(t[r],t,n[r],n):void 0}function Mi(e,{pages:t,pageParams:n}){return t.length>0?e.getPreviousPageParam?.(t[0],t,n[0],n):void 0}var Ni=class extends ki{#e;#t;#n;#r;#i;#a;#o;#s;constructor(e){super(),this.#s=!1,this.#o=e.defaultOptions,this.setOptions(e.options),this.observers=[],this.#i=e.client,this.#r=this.#i.getQueryCache(),this.queryKey=e.queryKey,this.queryHash=e.queryHash,this.#t=Ii(this.options),this.state=e.state??this.#t,this.scheduleGc()}get meta(){return this.options.meta}get queryType(){return this.#e}get promise(){return this.#a?.promise}setOptions(e){if(this.options={...this.#o,...e},e?._type&&(this.#e=e._type),this.updateGcTime(this.options.gcTime),this.state&&this.state.data===void 0){let e=Ii(this.options);e.data!==void 0&&(this.setState(Fi(e.data,e.dataUpdatedAt)),this.#t=e)}}optionalRemove(){!this.observers.length&&this.state.fetchStatus===`idle`&&this.#r.remove(this)}setData(e,t){let n=mi(this.state.data,e,this.options);return this.#l({data:n,type:`success`,dataUpdatedAt:t?.updatedAt,manual:t?.manual}),n}setState(e){this.#l({type:`setState`,state:e})}cancel(e){let t=this.#a?.promise;return this.#a?.cancel(e),t?t.then(X).catch(X):Promise.resolve()}destroy(){super.destroy(),this.cancel({silent:!0})}get resetState(){return this.#t}reset(){this.destroy(),this.setState(this.resetState)}isActive(){return this.observers.some(e=>ni(e.options.enabled,this)!==!1)}isDisabled(){return this.getObserversCount()>0?!this.isActive():this.options.queryFn===_i||!this.isFetched()}isFetched(){return this.state.dataUpdateCount+this.state.errorUpdateCount>0}isStatic(){return this.getObserversCount()>0&&this.observers.some(e=>ti(e.options.staleTime,this)===`static`)}isStale(){return this.getObserversCount()>0?this.observers.some(e=>e.getCurrentResult().isStale):this.state.data===void 0||this.state.isInvalidated}isStaleByTime(e=0){return this.state.data===void 0?!0:e===`static`?!1:this.state.isInvalidated?!0:!ei(this.state.dataUpdatedAt,e)}onFocus(){this.observers.find(e=>e.shouldFetchOnWindowFocus())?.refetch({cancelRefetch:!1}),this.#a?.continue()}onOnline(){this.observers.find(e=>e.shouldFetchOnReconnect())?.refetch({cancelRefetch:!1}),this.#a?.continue()}addObserver(e){this.observers.includes(e)||(this.observers.push(e),this.clearGcTimeout(),this.#r.notify({type:`observerAdded`,query:this,observer:e}))}removeObserver(e){this.observers.includes(e)&&(this.observers=this.observers.filter(t=>t!==e),this.observers.length||(this.#a&&(this.#s||this.#c()?this.#a.cancel({revert:!0}):this.#a.cancelRetry()),this.scheduleGc()),this.#r.notify({type:`observerRemoved`,query:this,observer:e}))}getObserversCount(){return this.observers.length}#c(){return this.state.fetchStatus===`paused`&&this.state.status===`pending`}invalidate(){this.state.isInvalidated||this.#l({type:`invalidate`})}async fetch(e,t){if(this.state.fetchStatus!==`idle`&&this.#a?.status()!==`rejected`){if(this.state.data!==void 0&&t?.cancelRefetch)this.cancel({silent:!0});else if(this.#a)return this.#a.continueRetry(),this.#a.promise}if(e&&this.setOptions(e),!this.options.queryFn){let e=this.observers.find(e=>e.options.queryFn);e&&this.setOptions(e.options)}let n=new AbortController,r=e=>{Object.defineProperty(e,"signal",{enumerable:!0,get:()=>(this.#s=!0,n.signal)})},i=()=>{let e=vi(this.options,t),n=(()=>{let e={client:this.#i,queryKey:this.queryKey,meta:this.meta};return r(e),e})();return this.#s=!1,this.options.persister?this.options.persister(e,n,this):e(n)},a=(()=>{let e={fetchOptions:t,options:this.options,queryKey:this.queryKey,client:this.#i,state:this.state,fetchFn:i};return r(e),e})();(this.#e===`infinite`?Ai(this.options.pages):this.options.behavior)?.onFetch(a,this),this.#n=this.state,(this.state.fetchStatus===`idle`||this.state.fetchMeta!==a.fetchOptions?.meta)&&this.#l({type:`fetch`,meta:a.fetchOptions?.meta}),this.#a=Oi({initialPromise:t?.initialPromise,fn:a.fetchFn,onCancel:e=>{e instanceof Di&&e.revert&&this.setState({...this.#n,fetchStatus:`idle`}),n.abort()},onFail:(e,t)=>{this.#l({type:`failed`,failureCount:e,error:t})},onPause:()=>{this.#l({type:`pause`})},onContinue:()=>{this.#l({type:`continue`})},retry:a.options.retry,retryDelay:a.options.retryDelay,networkMode:a.options.networkMode,canRun:()=>!0});try{let e=await this.#a.start();if(e===void 0)throw Error(`${this.queryHash} data is undefined`);return this.setData(e),this.#r.config.onSuccess?.(e,this),this.#r.config.onSettled?.(e,this.state.error,this),e}catch(e){if(e instanceof Di){if(e.silent)return this.#a.promise;if(e.revert){if(this.state.data===void 0)throw e;return this.state.data}}throw this.#l({type:`error`,error:e}),this.#r.config.onError?.(e,this),this.#r.config.onSettled?.(this.state.data,e,this),e}finally{this.scheduleGc()}}#l(e){let t=t=>{switch(e.type){case`failed`:return{...t,fetchFailureCount:e.failureCount,fetchFailureReason:e.error};case`pause`:return{...t,fetchStatus:`paused`};case`continue`:return{...t,fetchStatus:`fetching`};case`fetch`:return{...t,...Pi(t.data,this.options),fetchMeta:e.meta??null};case`success`:let n={...t,...Fi(e.data,e.dataUpdatedAt),dataUpdateCount:t.dataUpdateCount+1,...!e.manual&&{fetchStatus:`idle`,fetchFailureCount:0,fetchFailureReason:null}};return this.#n=e.manual?n:void 0,n;case`error`:let r=e.error;return{...t,error:r,errorUpdateCount:t.errorUpdateCount+1,errorUpdatedAt:Date.now(),fetchFailureCount:t.fetchFailureCount+1,fetchFailureReason:r,fetchStatus:`idle`,status:`error`,isInvalidated:!0};case`invalidate`:return{...t,isInvalidated:!0};case`setState`:return{...t,...e.state}}};this.state=t(this.state),Z.batch(()=>{this.observers.forEach(e=>{e.onQueryUpdate()}),this.#r.notify({query:this,type:`updated`,action:e})})}};function Pi(e,t){return{fetchFailureCount:0,fetchFailureReason:null,fetchStatus:Ei(t.networkMode)?`fetching`:`paused`,...e===void 0&&{error:null,status:`pending`}}}function Fi(e,t){return{data:e,dataUpdatedAt:t??Date.now(),error:null,isInvalidated:!1,status:`success`}}function Ii(e){let t=typeof e.initialData==`function`?e.initialData():e.initialData,n=t!==void 0,r=n?typeof e.initialDataUpdatedAt==`function`?e.initialDataUpdatedAt():e.initialDataUpdatedAt:0;return{data:t,dataUpdateCount:0,dataUpdatedAt:n?r??Date.now():0,error:null,errorUpdateCount:0,errorUpdatedAt:0,fetchFailureCount:0,fetchFailureReason:null,fetchMeta:null,isInvalidated:!1,status:n?`success`:`pending`,fetchStatus:`idle`}}var Li=class extends ki{#e;#t;#n;#r;constructor(e){super(),this.#e=e.client,this.mutationId=e.mutationId,this.#n=e.mutationCache,this.#t=[],this.state=e.state||Ri(),this.setOptions(e.options),this.scheduleGc()}setOptions(e){this.options=e,this.updateGcTime(this.options.gcTime)}get meta(){return this.options.meta}addObserver(e){this.#t.includes(e)||(this.#t.push(e),this.clearGcTimeout(),this.#n.notify({type:`observerAdded`,mutation:this,observer:e}))}removeObserver(e){this.#t=this.#t.filter(t=>t!==e),this.scheduleGc(),this.#n.notify({type:`observerRemoved`,mutation:this,observer:e})}optionalRemove(){this.#t.length||(this.state.status===`pending`?this.scheduleGc():this.#n.remove(this))}continue(){return this.#r?.continue()??this.execute(this.state.variables)}async execute(e){let t=()=>{this.#i({type:`continue`})},n={client:this.#e,meta:this.options.meta,mutationKey:this.options.mutationKey};this.#r=Oi({fn:()=>this.options.mutationFn?this.options.mutationFn(e,n):Promise.reject(Error(`No mutationFn found`)),onFail:(e,t)=>{this.#i({type:`failed`,failureCount:e,error:t})},onPause:()=>{this.#i({type:`pause`})},onContinue:t,retry:this.options.retry??0,retryDelay:this.options.retryDelay,networkMode:this.options.networkMode,canRun:()=>this.#n.canRun(this)});let r=this.state.status===`pending`,i=!this.#r.canStart();try{if(r)t();else{this.#i({type:`pending`,variables:e,isPaused:i}),this.#n.config.onMutate&&await this.#n.config.onMutate(e,this,n);let t=await this.options.onMutate?.(e,n);t!==this.state.context&&this.#i({type:`pending`,context:t,variables:e,isPaused:i})}let a=await this.#r.start();return await this.#n.config.onSuccess?.(a,e,this.state.context,this,n),await this.options.onSuccess?.(a,e,this.state.context,n),await this.#n.config.onSettled?.(a,null,this.state.variables,this.state.context,this,n),await this.options.onSettled?.(a,null,e,this.state.context,n),this.#i({type:`success`,data:a}),a}catch(t){try{await this.#n.config.onError?.(t,e,this.state.context,this,n)}catch(e){Promise.reject(e)}try{await this.options.onError?.(t,e,this.state.context,n)}catch(e){Promise.reject(e)}try{await this.#n.config.onSettled?.(void 0,t,this.state.variables,this.state.context,this,n)}catch(e){Promise.reject(e)}try{await this.options.onSettled?.(void 0,t,e,this.state.context,n)}catch(e){Promise.reject(e)}throw this.#i({type:`error`,error:t}),t}finally{this.#n.runNext(this)}}#i(e){let t=t=>{switch(e.type){case`failed`:return{...t,failureCount:e.failureCount,failureReason:e.error};case`pause`:return{...t,isPaused:!0};case`continue`:return{...t,isPaused:!1};case`pending`:return{...t,context:e.context,data:void 0,failureCount:0,failureReason:null,error:null,isPaused:e.isPaused,status:`pending`,variables:e.variables,submittedAt:Date.now()};case`success`:return{...t,data:e.data,failureCount:0,failureReason:null,error:null,status:`success`,isPaused:!1};case`error`:return{...t,data:void 0,error:e.error,failureCount:t.failureCount+1,failureReason:e.error,isPaused:!1,status:`error`}}};this.state=t(this.state),Z.batch(()=>{this.#t.forEach(t=>{t.onMutationUpdate(e)}),this.#n.notify({mutation:this,type:`updated`,action:e})})}};function Ri(){return{context:void 0,data:void 0,error:null,failureCount:0,failureReason:null,isPaused:!1,status:`idle`,variables:void 0,submittedAt:0}}var zi=class extends Kr{constructor(e={}){super(),this.config=e,this.#e=new Set,this.#t=new Map,this.#n=0}#e;#t;#n;build(e,t,n){let r=new Li({client:e,mutationCache:this,mutationId:++this.#n,options:e.defaultMutationOptions(t),state:n});return this.add(r),r}add(e){this.#e.add(e);let t=Bi(e);if(typeof t==`string`){let n=this.#t.get(t);n?n.push(e):this.#t.set(t,[e])}this.notify({type:`added`,mutation:e})}remove(e){if(this.#e.delete(e)){let t=Bi(e);if(typeof t==`string`){let n=this.#t.get(t);if(n){if(n.length>1){let t=n.indexOf(e);t!==-1&&n.splice(t,1)}else n[0]===e&&this.#t.delete(t)}}}this.notify({type:`removed`,mutation:e})}canRun(e){let t=Bi(e);if(typeof t==`string`){let n=this.#t.get(t)?.find(e=>e.state.status===`pending`);return!n||n===e}return!0}runNext(e){let t=Bi(e);return typeof t==`string`?(this.#t.get(t)?.find(t=>t!==e&&t.state.isPaused))?.continue()??Promise.resolve():Promise.resolve()}clear(){Z.batch(()=>{this.#e.forEach(e=>{this.notify({type:`removed`,mutation:e})}),this.#e.clear(),this.#t.clear()})}getAll(){return Array.from(this.#e)}find(e){let t={exact:!0,...e};return this.getAll().find(e=>ii(t,e))}findAll(e={}){return this.getAll().filter(t=>ii(e,t))}notify(e){Z.batch(()=>{this.listeners.forEach(t=>{t(e)})})}resumePausedMutations(){let e=this.getAll().filter(e=>e.state.isPaused);return Z.batch(()=>Promise.all(e.map(e=>e.continue().catch(X))))}};function Bi(e){return e.options.scope?.id}var Vi=class extends Kr{constructor(e={}){super(),this.config=e,this.#e=new Map}#e;build(e,t,n){let r=t.queryKey,i=t.queryHash??ai(r,t),a=this.get(i);return a||(a=new Ni({client:e,queryKey:r,queryHash:i,options:e.defaultQueryOptions(t),state:n,defaultOptions:e.getQueryDefaults(r)}),this.add(a)),a}add(e){this.#e.has(e.queryHash)||(this.#e.set(e.queryHash,e),this.notify({type:`added`,query:e}))}remove(e){let t=this.#e.get(e.queryHash);t&&(e.destroy(),t===e&&this.#e.delete(e.queryHash),this.notify({type:`removed`,query:e}))}clear(){Z.batch(()=>{this.getAll().forEach(e=>{this.remove(e)})})}get(e){return this.#e.get(e)}getAll(){return[...this.#e.values()]}find(e){let t={exact:!0,...e};return this.getAll().find(e=>ri(t,e))}findAll(e={}){let t=this.getAll();return Object.keys(e).length>0?t.filter(t=>ri(e,t)):t}notify(e){Z.batch(()=>{this.listeners.forEach(t=>{t(e)})})}onFocus(){Z.batch(()=>{this.getAll().forEach(e=>{e.onFocus()})})}onOnline(){Z.batch(()=>{this.getAll().forEach(e=>{e.onOnline()})})}},Hi=class{#e;#t;#n;#r;#i;#a;#o;#s;constructor(e={}){this.#e=e.queryCache||new Vi,this.#t=e.mutationCache||new zi,this.#n=e.defaultOptions||{},this.#r=new Map,this.#i=new Map,this.#a=0}mount(){this.#a++,this.#a===1&&(this.#o=qr.subscribe(async e=>{e&&(await this.resumePausedMutations(),this.#e.onFocus())}),this.#s=wi.subscribe(async e=>{e&&(await this.resumePausedMutations(),this.#e.onOnline())}))}unmount(){this.#a--,this.#a===0&&(this.#o?.(),this.#o=void 0,this.#s?.(),this.#s=void 0)}isFetching(e){return this.#e.findAll({...e,fetchStatus:`fetching`}).length}isMutating(e){return this.#t.findAll({...e,status:`pending`}).length}getQueryData(e){let t=this.defaultQueryOptions({queryKey:e});return this.#e.get(t.queryHash)?.state.data}ensureQueryData(e){let t=this.defaultQueryOptions(e),n=this.#e.build(this,t),r=n.state.data;return r===void 0?this.fetchQuery(e):(e.revalidateIfStale&&n.isStaleByTime(ti(t.staleTime,n))&&this.prefetchQuery(t),Promise.resolve(r))}getQueriesData(e){return this.#e.findAll(e).map(({queryKey:e,state:t})=>[e,t.data])}setQueryData(e,t,n){let r=this.defaultQueryOptions({queryKey:e}),i=this.#e.get(r.queryHash)?.state.data,a=Qr(t,i);if(a!==void 0)return this.#e.build(this,r).setData(a,{...n,manual:!0})}setQueriesData(e,t,n){return Z.batch(()=>this.#e.findAll(e).map(({queryKey:e})=>[e,this.setQueryData(e,t,n)]))}getQueryState(e){let t=this.defaultQueryOptions({queryKey:e});return this.#e.get(t.queryHash)?.state}removeQueries(e){let t=this.#e;Z.batch(()=>{t.findAll(e).forEach(e=>{t.remove(e)})})}resetQueries(e,t){let n=this.#e;return Z.batch(()=>(n.findAll(e).forEach(e=>{e.reset()}),this.refetchQueries({type:`active`,...e},t)))}cancelQueries(e,t={}){let n={revert:!0,...t},r=Z.batch(()=>this.#e.findAll(e).map(e=>e.cancel(n)));return Promise.all(r).then(X).catch(X)}invalidateQueries(e,t={}){return Z.batch(()=>(this.#e.findAll(e).forEach(e=>{e.invalidate()}),e?.refetchType===`none`?Promise.resolve():this.refetchQueries({...e,type:e?.refetchType??e?.type??`active`},t)))}refetchQueries(e,t={}){let n={...t,cancelRefetch:t.cancelRefetch??!0},r=Z.batch(()=>this.#e.findAll(e).filter(e=>!e.isDisabled()&&!e.isStatic()).map(e=>{let t=e.fetch(void 0,n);return n.throwOnError||(t=t.catch(X)),e.state.fetchStatus===`paused`?Promise.resolve():t}));return Promise.all(r).then(X)}fetchQuery(e){let t=this.defaultQueryOptions(e);t.retry===void 0&&(t.retry=!1);let n=this.#e.build(this,t);return n.isStaleByTime(ti(t.staleTime,n))?n.fetch(t):Promise.resolve(n.state.data)}prefetchQuery(e){return this.fetchQuery(e).then(X).catch(X)}fetchInfiniteQuery(e){return e._type=`infinite`,this.fetchQuery(e)}prefetchInfiniteQuery(e){return this.fetchInfiniteQuery(e).then(X).catch(X)}ensureInfiniteQueryData(e){return e._type=`infinite`,this.ensureQueryData(e)}resumePausedMutations(){return wi.isOnline()?this.#t.resumePausedMutations():Promise.resolve()}getQueryCache(){return this.#e}getMutationCache(){return this.#t}getDefaultOptions(){return this.#n}setDefaultOptions(e){this.#n=e}setQueryDefaults(e,t){this.#r.set(oi(e),{queryKey:e,defaultOptions:t})}getQueryDefaults(e){let t=[...this.#r.values()],n={};return t.forEach(t=>{si(e,t.queryKey)&&Object.assign(n,t.defaultOptions)}),n}setMutationDefaults(e,t){this.#i.set(oi(e),{mutationKey:e,defaultOptions:t})}getMutationDefaults(e){let t=[...this.#i.values()],n={};return t.forEach(t=>{si(e,t.mutationKey)&&Object.assign(n,t.defaultOptions)}),n}defaultQueryOptions(e){if(e._defaulted)return e;let t={...this.#n.queries,...this.getQueryDefaults(e.queryKey),...e,_defaulted:!0};return t.queryHash||=ai(t.queryKey,t),t.refetchOnReconnect===void 0&&(t.refetchOnReconnect=t.networkMode!==`always`),t.throwOnError===void 0&&(t.throwOnError=!!t.suspense),!t.networkMode&&t.persister&&(t.networkMode=`offlineFirst`),t.queryFn===_i&&(t.enabled=!1),t}defaultMutationOptions(e){return e?._defaulted?e:{...this.#n.mutations,...e?.mutationKey&&this.getMutationDefaults(e.mutationKey),...e,_defaulted:!0}}clear(){this.#e.clear(),this.#t.clear()}},Ui=ce(void 0),Wi=e=>(a(t=>(t?.(),e.client.mount(),e.client.unmount.bind(e.client))),s(()=>e.client.unmount()),h(Ui.Provider,{value:()=>e.client,get children(){return e.children}}));ce(()=>!1).Provider;var Gi=class extends Hi{constructor(e={}){super(e)}},Ki=[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M4.929 4.929 19.07 19.071`,key:`196cmz`}]],qi=e=>h(ae,c(e,{iconNode:Ki,name:`ban`})),Ji=[[`path`,{d:`M11.562 3.266a.5.5 0 0 1 .876 0L15.39 8.87a1 1 0 0 0 1.516.294L21.183 5.5a.5.5 0 0 1 .798.519l-2.834 10.246a1 1 0 0 1-.956.734H5.81a1 1 0 0 1-.957-.734L2.02 6.02a.5.5 0 0 1 .798-.519l4.276 3.664a1 1 0 0 0 1.516-.294z`,key:`1vdc57`}],[`path`,{d:`M5 21h14`,key:`11awu3`}]],Yi=e=>h(ae,c(e,{iconNode:Ji,name:`crown`})),Xi=E(``),Zi=E(`

账号已被封禁(banned),部分功能受限,请联系超管解封`),Qi=E(``),$i=E(`SYNC·LIVE`),ea=E(`