From 1a359f97932b4b127244ba41ebd874de9e52b35d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 6 Oct 2024 21:45:46 +0300 Subject: [PATCH] web: init --- .editorconfig | 1 + .gitignore | 4 + go.mod | 28 + go.sum | 62 + gomuks.go | 272 +++ main.go | 127 ++ media.go | 182 ++ web/.gitignore | 9 + web/eslint.config.js | 66 + web/frontend.go | 26 + web/index.html | 13 + web/package-lock.json | 2585 ++++++++++++++++++++++++++ web/package.json | 35 + web/public/gomuks.png | Bin 0 -> 85635 bytes web/src/App.css | 0 web/src/App.tsx | 68 + web/src/MainScreen.css | 7 + web/src/MainScreen.tsx | 36 + web/src/RoomList.css | 50 + web/src/RoomList.tsx | 101 + web/src/RoomView.css | 3 + web/src/RoomView.tsx | 38 + web/src/TimelineEvent.css | 0 web/src/TimelineEvent.tsx | 39 + web/src/client.ts | 81 + web/src/eventdispatcher.ts | 79 + web/src/hievents.ts | 96 + web/src/hitypes.ts | 125 ++ web/src/index.css | 32 + web/src/login/LoginScreen.css | 66 + web/src/login/LoginScreen.tsx | 87 + web/src/login/VerificationScreen.tsx | 52 + web/src/login/index.ts | 2 + web/src/main.tsx | 25 + web/src/rpc.ts | 39 + web/src/statestore.ts | 196 ++ web/src/wsclient.ts | 147 ++ web/tsconfig.json | 28 + web/vite.config.ts | 17 + websocket.go | 267 +++ 40 files changed, 5091 insertions(+) create mode 100644 gomuks.go create mode 100644 main.go create mode 100644 media.go create mode 100644 web/.gitignore create mode 100644 web/eslint.config.js create mode 100644 web/frontend.go create mode 100644 web/index.html create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/public/gomuks.png create mode 100644 web/src/App.css create mode 100644 web/src/App.tsx create mode 100644 web/src/MainScreen.css create mode 100644 web/src/MainScreen.tsx create mode 100644 web/src/RoomList.css create mode 100644 web/src/RoomList.tsx create mode 100644 web/src/RoomView.css create mode 100644 web/src/RoomView.tsx create mode 100644 web/src/TimelineEvent.css create mode 100644 web/src/TimelineEvent.tsx create mode 100644 web/src/client.ts create mode 100644 web/src/eventdispatcher.ts create mode 100644 web/src/hievents.ts create mode 100644 web/src/hitypes.ts create mode 100644 web/src/index.css create mode 100644 web/src/login/LoginScreen.css create mode 100644 web/src/login/LoginScreen.tsx create mode 100644 web/src/login/VerificationScreen.tsx create mode 100644 web/src/login/index.ts create mode 100644 web/src/main.tsx create mode 100644 web/src/rpc.ts create mode 100644 web/src/statestore.ts create mode 100644 web/src/wsclient.ts create mode 100644 web/tsconfig.json create mode 100644 web/vite.config.ts create mode 100644 websocket.go diff --git a/.editorconfig b/.editorconfig index 9fa8d76..1d1f4ed 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,7 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +max_line_length = 120 [.gitlab-ci.yml] indent_size = 2 diff --git a/.gitignore b/.gitignore index 8bf7688..123bc5c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,13 @@ target/ .tmp/ gomuks +start +run *.exe *.deb coverage.out coverage.html deb/usr *.prof +*.db* +*.log diff --git a/go.mod b/go.mod index 3d0c041..fb82b31 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,31 @@ module go.mau.fi/gomuks go 1.23.0 toolchain go1.23.2 + +require ( + github.com/coder/websocket v1.8.12 + github.com/mattn/go-sqlite3 v1.14.23 + github.com/rs/zerolog v1.33.0 + go.mau.fi/util v0.8.1-0.20241003092848-3b49d3e0b9ee + go.mau.fi/zeroconfig v0.1.3 + maunium.net/go/mauflag v1.0.0 + maunium.net/go/mautrix v0.21.1-0.20241006181705-64692eb06e11 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect +) diff --git a/go.sum b/go.sum index e69de29..2b1713b 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,62 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= +github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +go.mau.fi/util v0.8.1-0.20241003092848-3b49d3e0b9ee h1:/BGpUK7fzVyFgy5KBiyP7ktEDn20vzz/5FTngrXtIEE= +go.mau.fi/util v0.8.1-0.20241003092848-3b49d3e0b9ee/go.mod h1:L9qnqEkhe4KpuYmILrdttKTXL79MwGLyJ4EOskWxO3I= +go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM= +go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= +maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= +maunium.net/go/mautrix v0.21.1-0.20241006181705-64692eb06e11 h1:XhBqRfWg75OCsXxmb4uFJtHs6feq4MG9xaBWZKcMhFg= +maunium.net/go/mautrix v0.21.1-0.20241006181705-64692eb06e11/go.mod h1:+fF5qsmXRCEXQZgW5ececC0PI3c7gISHTLcyftP4Bh0= diff --git a/gomuks.go b/gomuks.go new file mode 100644 index 0000000..ccdcd07 --- /dev/null +++ b/gomuks.go @@ -0,0 +1,272 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import ( + "context" + "errors" + "fmt" + "io/fs" + "maps" + "net/http" + "os" + "os/signal" + "path/filepath" + "runtime" + "slices" + "sync" + "syscall" + + "github.com/coder/websocket" + "github.com/rs/zerolog" + "github.com/rs/zerolog/hlog" + "go.mau.fi/util/dbutil" + "go.mau.fi/util/exerrors" + "go.mau.fi/util/exzerolog" + "go.mau.fi/util/ptr" + "go.mau.fi/util/requestlog" + "go.mau.fi/zeroconfig" + "maunium.net/go/mautrix/hicli" + + "go.mau.fi/gomuks/web" +) + +type Gomuks struct { + Log *zerolog.Logger + Server *http.Server + Client *hicli.HiClient + + ConfigDir string + DataDir string + CacheDir string + LogDir string + + stopOnce sync.Once + stopChan chan struct{} + + websocketClosers map[uint64]WebsocketCloseFunc + eventListeners map[uint64]func(*hicli.JSONCommand) + nextListenerID uint64 + eventListenersLock sync.RWMutex +} + +func NewGomuks() *Gomuks { + return &Gomuks{ + stopChan: make(chan struct{}), + eventListeners: make(map[uint64]func(*hicli.JSONCommand)), + websocketClosers: make(map[uint64]WebsocketCloseFunc), + } +} + +func (gmx *Gomuks) LoadConfig() { + // We need 4 directories: config, data, cache, logs + // + // 1. If GOMUKS_ROOT is set, all directories are created under that. + // 2. If GOMUKS_*_HOME is set, that value is used as the directory. + // 3. Use system-specific defaults as below + // + // *nix: + // - Config: $XDG_CONFIG_HOME/gomuks or $HOME/.config/gomuks + // - Data: $XDG_DATA_HOME/gomuks or $HOME/.local/share/gomuks + // - Cache: $XDG_CACHE_HOME/gomuks or $HOME/.cache/gomuks + // - Logs: $XDG_STATE_HOME/gomuks or $HOME/.local/state/gomuks + // + // Windows: + // - Config and Data: %AppData%\gomuks + // - Cache: %LocalAppData%\gomuks + // - Logs: %LocalAppData%\gomuks\logs + // + // macOS: + // - Config and Data: $HOME/Library/Application Support/gomuks + // - Cache: $HOME/Library/Caches/gomuks + // - Logs: $HOME/Library/Logs/gomuks + if gomuksRoot := os.Getenv("GOMUKS_ROOT"); gomuksRoot != "" { + exerrors.PanicIfNotNil(os.MkdirAll(gomuksRoot, 0700)) + gmx.CacheDir = filepath.Join(gomuksRoot, "cache") + gmx.ConfigDir = filepath.Join(gomuksRoot, "config") + gmx.DataDir = filepath.Join(gomuksRoot, "data") + gmx.LogDir = filepath.Join(gomuksRoot, "logs") + } else { + homeDir := exerrors.Must(os.UserHomeDir()) + if cacheDir := os.Getenv("GOMUKS_CACHE_HOME"); cacheDir != "" { + gmx.CacheDir = cacheDir + } else { + gmx.CacheDir = filepath.Join(exerrors.Must(os.UserCacheDir()), "gomuks") + } + if configDir := os.Getenv("GOMUKS_CONFIG_HOME"); configDir != "" { + gmx.ConfigDir = configDir + } else { + gmx.ConfigDir = filepath.Join(exerrors.Must(os.UserConfigDir()), "gomuks") + } + if dataDir := os.Getenv("GOMUKS_DATA_HOME"); dataDir != "" { + gmx.DataDir = dataDir + } else if dataDir = os.Getenv("XDG_DATA_HOME"); dataDir != "" { + gmx.DataDir = filepath.Join(dataDir, "gomuks") + } else if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { + gmx.DataDir = gmx.ConfigDir + } else { + gmx.DataDir = filepath.Join(homeDir, ".local", "share", "gomuks") + } + if logDir := os.Getenv("GOMUKS_LOGS_HOME"); logDir != "" { + gmx.LogDir = logDir + } else if logDir = os.Getenv("XDG_STATE_HOME"); logDir != "" { + gmx.LogDir = filepath.Join(logDir, "gomuks") + } else if runtime.GOOS == "darwin" { + gmx.DataDir = filepath.Join(homeDir, "Library", "Logs", "gomuks") + } else if runtime.GOOS == "windows" { + gmx.DataDir = filepath.Join(gmx.CacheDir, "logs") + } else { + gmx.LogDir = filepath.Join(homeDir, ".local", "state", "gomuks") + } + } + exerrors.PanicIfNotNil(os.MkdirAll(gmx.ConfigDir, 0700)) + exerrors.PanicIfNotNil(os.MkdirAll(gmx.CacheDir, 0700)) + exerrors.PanicIfNotNil(os.MkdirAll(gmx.DataDir, 0700)) + exerrors.PanicIfNotNil(os.MkdirAll(gmx.LogDir, 0700)) +} + +func (gmx *Gomuks) SetupLog() { + gmx.Log = exerrors.Must((&zeroconfig.Config{ + MinLevel: ptr.Ptr(zerolog.TraceLevel), + Writers: []zeroconfig.WriterConfig{{ + Type: zeroconfig.WriterTypeStdout, + Format: zeroconfig.LogFormatPrettyColored, + }}, + }).Compile()) + exzerolog.SetupDefaults(gmx.Log) +} + +func (gmx *Gomuks) StartServer(addr string) { + api := http.NewServeMux() + api.HandleFunc("GET /websocket", gmx.HandleWebsocket) + api.HandleFunc("GET /media/{server}/{media_id}", gmx.DownloadMedia) + middlewares := []func(http.Handler) http.Handler{ + hlog.NewHandler(*gmx.Log), + hlog.RequestIDHandler("request_id", "Request-ID"), + requestlog.AccessLogger(false), + } + apiHandler := http.StripPrefix("/_gomuks", api) + for _, middleware := range slices.Backward(middlewares) { + apiHandler = middleware(apiHandler) + } + router := http.NewServeMux() + router.Handle("/_gomuks/", apiHandler) + if frontend, err := fs.Sub(web.Frontend, "dist"); err != nil { + gmx.Log.Warn().Msg("Frontend not found") + } else { + router.Handle("/", http.FileServerFS(frontend)) + } + gmx.Server = &http.Server{ + Addr: addr, + Handler: router, + } + go func() { + err := gmx.Server.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + panic(err) + } + }() + gmx.Log.Info().Str("address", addr).Msg("Server started") +} + +func (gmx *Gomuks) StartClient() { + rawDB, err := dbutil.NewFromConfig("gomuks", dbutil.Config{ + PoolConfig: dbutil.PoolConfig{ + Type: "sqlite3-fk-wal", + URI: fmt.Sprintf("file:%s/gomuks.db?_txlock=immediate", gmx.DataDir), + MaxOpenConns: 5, + MaxIdleConns: 1, + }, + }, dbutil.ZeroLogger(gmx.Log.With().Str("component", "hicli").Str("db_section", "main").Logger())) + if err != nil { + gmx.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to open database") + os.Exit(10) + } + ctx := gmx.Log.WithContext(context.Background()) + gmx.Client = hicli.New( + rawDB, + nil, + gmx.Log.With().Str("component", "hicli").Logger(), + []byte("meow"), + hicli.JSONEventHandler(gmx.OnEvent).HandleEvent, + ) + userID, err := gmx.Client.DB.Account.GetFirstUserID(ctx) + if err != nil { + gmx.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to get first user ID") + os.Exit(11) + } + err = gmx.Client.Start(ctx, userID, nil) + if err != nil { + gmx.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to start client") + os.Exit(12) + } + gmx.Log.Info().Stringer("user_id", userID).Msg("Client started") +} + +func (gmx *Gomuks) Stop() { + gmx.stopOnce.Do(func() { + close(gmx.stopChan) + }) +} + +func (gmx *Gomuks) WaitForInterrupt() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + select { + case <-c: + case <-gmx.stopChan: + } +} + +func (gmx *Gomuks) directStop() { + gmx.eventListenersLock.Lock() + closers := slices.Collect(maps.Values(gmx.websocketClosers)) + gmx.eventListenersLock.Unlock() + for _, closer := range closers { + closer(websocket.StatusServiceRestart, "Server shutting down") + } + gmx.Client.Stop() + err := gmx.Server.Close() + if err != nil { + gmx.Log.Error().Err(err).Msg("Failed to close server") + } +} + +func (gmx *Gomuks) OnEvent(evt *hicli.JSONCommand) { + gmx.eventListenersLock.RLock() + defer gmx.eventListenersLock.RUnlock() + for _, listener := range gmx.eventListeners { + listener(evt) + } +} + +type WebsocketCloseFunc func(websocket.StatusCode, string) + +func (gmx *Gomuks) SubscribeEvents(closeForRestart WebsocketCloseFunc, cb func(command *hicli.JSONCommand)) func() { + gmx.eventListenersLock.Lock() + defer gmx.eventListenersLock.Unlock() + gmx.nextListenerID++ + id := gmx.nextListenerID + gmx.eventListeners[id] = cb + gmx.websocketClosers[id] = closeForRestart + return func() { + gmx.eventListenersLock.Lock() + defer gmx.eventListenersLock.Unlock() + delete(gmx.eventListeners, id) + delete(gmx.websocketClosers, id) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..3f88180 --- /dev/null +++ b/main.go @@ -0,0 +1,127 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import ( + "fmt" + "os" + "runtime" + "strings" + "time" + + _ "github.com/mattn/go-sqlite3" + _ "go.mau.fi/util/dbutil/litestream" + flag "maunium.net/go/mauflag" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/hicli" +) + +var ( + Tag = "unknown" + Commit = "unknown" + BuildTime = "unknown" +) + +const StaticVersion = "0.4.0" +const URL = "https://github.com/tulir/gomuks" + +var ( + Version string + VersionDesc string + LinkifiedVersion string + ParsedBuildTime time.Time +) + +var wantHelp, _ = flag.MakeHelpFlag() +var version = flag.MakeFull("v", "version", "View gomuks version and quit.", "false").Bool() +var listenAddress = flag.MakeFull("l", "listen", "Address to listen on.", "localhost:29325").String() + +func main() { + hicli.InitialDeviceDisplayName = "gomuks web" + initVersion(Tag, Commit, BuildTime) + flag.SetHelpTitles( + "gomuks - A Matrix client written in Go.", + "gomuks [-hv] [-l address]", + ) + err := flag.Parse() + + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, err) + flag.PrintHelp() + os.Exit(1) + } else if *wantHelp { + flag.PrintHelp() + os.Exit(0) + } else if *version { + fmt.Println(VersionDesc) + os.Exit(0) + } + + gmx := NewGomuks() + gmx.LoadConfig() + gmx.SetupLog() + gmx.Log.Info(). + Str("version", Version). + Str("go_version", runtime.Version()). + Time("built_at", ParsedBuildTime). + Msg("Initializing gomuks") + gmx.StartServer(*listenAddress) + gmx.StartClient() + gmx.Log.Info().Msg("Initialization complete") + gmx.WaitForInterrupt() + gmx.Log.Info().Msg("Shutting down...") + gmx.directStop() + gmx.Log.Info().Msg("Shutdown complete") + os.Exit(0) +} + +func initVersion(tag, commit, rawBuildTime string) { + if len(tag) > 0 && tag[0] == 'v' { + tag = tag[1:] + } + if tag != StaticVersion { + suffix := "+dev" + if len(commit) > 8 { + Version = fmt.Sprintf("%s%s.%s", StaticVersion, suffix, commit[:8]) + } else { + Version = fmt.Sprintf("%s%s.unknown", StaticVersion, suffix) + } + } else { + Version = StaticVersion + } + + LinkifiedVersion = fmt.Sprintf("v%s", Version) + if tag == Version { + LinkifiedVersion = fmt.Sprintf("[v%s](%s/releases/v%s)", Version, URL, tag) + } else if len(commit) > 8 { + LinkifiedVersion = strings.Replace(LinkifiedVersion, commit[:8], fmt.Sprintf("[%s](%s/commit/%s)", commit[:8], URL, commit), 1) + } + if rawBuildTime != "unknown" { + ParsedBuildTime, _ = time.Parse(time.RFC3339, rawBuildTime) + } + var builtWith string + if ParsedBuildTime.IsZero() { + rawBuildTime = "unknown" + builtWith = runtime.Version() + } else { + rawBuildTime = ParsedBuildTime.Format(time.RFC1123) + builtWith = fmt.Sprintf("built at %s with %s", rawBuildTime, runtime.Version()) + } + mautrix.DefaultUserAgent = fmt.Sprintf("gomuks/%s %s", Version, mautrix.DefaultUserAgent) + VersionDesc = fmt.Sprintf("gomuks %s (%s)", Version, builtWith) + BuildTime = rawBuildTime +} diff --git a/media.go b/media.go new file mode 100644 index 0000000..4924eb2 --- /dev/null +++ b/media.go @@ -0,0 +1,182 @@ +package main + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "mime" + "net/http" + "os" + "path/filepath" + "strconv" + + "github.com/rs/zerolog" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/hicli/database" + "maunium.net/go/mautrix/id" +) + +var ErrBadGateway = mautrix.RespError{ + ErrCode: "FI.MAU.GOMUKS.BAD_GATEWAY", + StatusCode: http.StatusBadGateway, +} + +func (gmx *Gomuks) downloadMediaFromCache(ctx context.Context, w http.ResponseWriter, entry *database.CachedMedia, force bool) bool { + if entry == nil || entry.Hash == nil { + if force { + mautrix.MNotFound.WithMessage("Media not found in cache").Write(w) + return true + } + return false + } + log := zerolog.Ctx(ctx) + cacheFile, err := os.Open(gmx.cacheEntryToPath(entry)) + if err != nil { + if errors.Is(err, os.ErrNotExist) && !force { + return false + } + log.Err(err).Msg("Failed to open cache file") + mautrix.MUnknown.WithMessage(fmt.Sprintf("Failed to open cache file: %v", err)).Write(w) + return true + } + defer func() { + _ = cacheFile.Close() + }() + cacheEntryToHeaders(w, entry) + w.WriteHeader(http.StatusOK) + _, err = io.Copy(w, cacheFile) + if err != nil { + log.Err(err).Msg("Failed to copy cache file to response") + } + return true +} + +func (gmx *Gomuks) cacheEntryToPath(entry *database.CachedMedia) string { + hashPath := hex.EncodeToString(entry.Hash[:]) + return filepath.Join(gmx.CacheDir, "media", hashPath[0:2], hashPath[2:4], hashPath[4:]) +} + +func cacheEntryToHeaders(w http.ResponseWriter, entry *database.CachedMedia) { + w.Header().Set("Content-Type", entry.MimeType) + w.Header().Set("Content-Length", strconv.FormatInt(entry.Size, 10)) + w.Header().Set("Content-Disposition", mime.FormatMediaType(entry.ContentDisposition(), map[string]string{"filename": entry.FileName})) + w.Header().Set("Content-Security-Policy", "sandbox; default-src 'none'; script-src 'none';") +} + +func (gmx *Gomuks) DownloadMedia(w http.ResponseWriter, r *http.Request) { + mxc := id.ContentURI{ + Homeserver: r.PathValue("server"), + FileID: r.PathValue("media_id"), + } + if !mxc.IsValid() { + mautrix.MInvalidParam.WithMessage("Invalid mxc URI").Write(w) + return + } + query := r.URL.Query() + encrypted, _ := strconv.ParseBool(query.Get("encrypted")) + + logVal := zerolog.Ctx(r.Context()).With(). + Stringer("mxc_uri", mxc). + Bool("encrypted", encrypted). + Logger() + log := &logVal + ctx := log.WithContext(r.Context()) + cacheEntry, err := gmx.Client.DB.CachedMedia.Get(ctx, mxc) + if err != nil { + log.Err(err).Msg("Failed to get cached media entry") + mautrix.MUnknown.WithMessage(fmt.Sprintf("Failed to get cached media entry: %v", err)).Write(w) + return + } else if (cacheEntry == nil || cacheEntry.EncFile == nil) && encrypted { + mautrix.MNotFound.WithMessage("Media encryption keys not found in cache").Write(w) + return + } + + if gmx.downloadMediaFromCache(ctx, w, cacheEntry, false) { + return + } + + tempFile, err := os.CreateTemp("", "gomuks-download-*") + if err != nil { + log.Err(err).Msg("Failed to create temporary file") + mautrix.MUnknown.WithMessage(fmt.Sprintf("Failed to create temp file: %v", err)).Write(w) + return + } + defer func() { + _ = tempFile.Close() + _ = os.Remove(tempFile.Name()) + }() + + resp, err := gmx.Client.Client.Download(ctx, mxc) + if err != nil { + log.Err(err).Msg("Failed to download media") + ErrBadGateway.WithMessage(err.Error()).Write(w) + return + } + defer func() { + _ = resp.Body.Close() + }() + if cacheEntry == nil { + cacheEntry = &database.CachedMedia{ + MXC: mxc, + MimeType: resp.Header.Get("Content-Type"), + Size: resp.ContentLength, + } + } + + reader := resp.Body + if cacheEntry.EncFile != nil { + err = cacheEntry.EncFile.PrepareForDecryption() + if err != nil { + log.Err(err).Msg("Failed to prepare media for decryption") + mautrix.MUnknown.WithMessage(fmt.Sprintf("Failed to prepare media for decryption: %v", err)).Write(w) + return + } + reader = cacheEntry.EncFile.DecryptStream(reader) + } + fileHasher := sha256.New() + hashReader := io.TeeReader(reader, fileHasher) + cacheEntry.Size, err = io.Copy(tempFile, hashReader) + if err != nil { + log.Err(err).Msg("Failed to copy media to temporary file") + mautrix.MUnknown.WithMessage(fmt.Sprintf("Failed to copy media to temp file: %v", err)).Write(w) + return + } + err = reader.Close() + if err != nil { + log.Err(err).Msg("Failed to close media reader") + mautrix.MUnknown.WithMessage(fmt.Sprintf("Failed to finish reading media: %v", err)).Write(w) + return + } + _ = tempFile.Close() + if cacheEntry.FileName == "" { + _, params, _ := mime.ParseMediaType(resp.Header.Get("Content-Disposition")) + cacheEntry.FileName = params["filename"] + } + if cacheEntry.MimeType == "" { + cacheEntry.MimeType = resp.Header.Get("Content-Type") + } + cacheEntry.Hash = (*[32]byte)(fileHasher.Sum(nil)) + err = gmx.Client.DB.CachedMedia.Put(ctx, cacheEntry) + if err != nil { + log.Err(err).Msg("Failed to save cache entry") + mautrix.MUnknown.WithMessage(fmt.Sprintf("Failed to save cache entry: %v", err)).Write(w) + return + } + cachePath := gmx.cacheEntryToPath(cacheEntry) + err = os.MkdirAll(filepath.Dir(cachePath), 0700) + if err != nil { + log.Err(err).Msg("Failed to create cache directory") + mautrix.MUnknown.WithMessage(fmt.Sprintf("Failed to create cache directory: %v", err)).Write(w) + return + } + err = os.Rename(tempFile.Name(), cachePath) + if err != nil { + log.Err(err).Msg("Failed to rename temporary file") + mautrix.MUnknown.WithMessage(fmt.Sprintf("Failed to rename temp file: %v", err)).Write(w) + return + } + gmx.downloadMediaFromCache(ctx, w, cacheEntry, true) +} diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..65e4336 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,9 @@ +logs +*.log +npm-debug.log* +tsconfig.tsbuildinfo + +node_modules +dist +dist-ssr +*.local diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 0000000..4bedcd0 --- /dev/null +++ b/web/eslint.config.js @@ -0,0 +1,66 @@ +import js from "@eslint/js" +import globals from "globals" +import reactHooks from "eslint-plugin-react-hooks" +import reactRefresh from "eslint-plugin-react-refresh" +import tseslint from "typescript-eslint" + +export default tseslint.config( + {ignores: ["dist"]}, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2023, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + {allowConstantExport: true}, + ], + "indent": ["error", "tab", { + "FunctionDeclaration": {"parameters": "first"}, + "FunctionExpression": {"parameters": "first"}, + "CallExpression": {"arguments": "first"}, + "ArrayExpression": "first", + "ObjectExpression": "first", + "ImportDeclaration": "first", + }], + "object-curly-newline": ["error", { + "consistent": true, + }], + "object-curly-spacing": ["error", "always", { + "arraysInObjects": false, + "objectsInObjects": false, + }], + "array-bracket-spacing": ["error", "never"], + "one-var-declaration-per-line": ["error", "initializations"], + "quotes": ["error", "double"], + "semi": ["error", "never"], + "comma-dangle": ["error", "always-multiline"], + "max-len": ["warn", 120], + "space-before-function-paren": ["error", { + "anonymous": "never", + "named": "never", + "asyncArrow": "always", + }], + "func-style": ["warn", "declaration", {"allowArrowFunctions": true}], + "id-length": ["warn", {"max": 40, "exceptions": ["i", "j", "x", "y", "_"]}], + "new-cap": ["warn", { + "newIsCap": true, + "capIsNew": true, + }], + "no-empty": ["error", { + "allowEmptyCatch": true, + }], + "eol-last": ["error", "always"], + "no-console": "off", + "@typescript-eslint/no-non-null-assertion": "off", + }, + }, +) diff --git a/web/frontend.go b/web/frontend.go new file mode 100644 index 0000000..3ef52d8 --- /dev/null +++ b/web/frontend.go @@ -0,0 +1,26 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package web + +import ( + "embed" +) + +//go:generate npm install --legacy-peer-deps +//go:generate npm run build +//go:embed dist +var Frontend embed.FS diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..0127288 --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + gomuks web + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..eef6cfc --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,2585 @@ +{ + "name": "gomuks-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gomuks-web", + "version": "0.1.0", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@types/react": "npm:types-react@rc", + "@types/react-dom": "npm:types-react-dom@rc", + "react": "^19.0.0-rc-0751fac7-20241002", + "react-dom": "^19.0.0-rc-0751fac7-20241002", + "react-spinners": "^0.14.1" + }, + "devDependencies": { + "@eslint/js": "^9.11.1", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^9.11.1", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.12", + "globals": "^15.9.0", + "typescript": "^5.5.3", + "typescript-eslint": "^8.7.0", + "vite": "^5.4.8" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", + "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz", + "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", + "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "dev": true, + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.26.tgz", + "integrity": "sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.12" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.7.26", + "@swc/core-darwin-x64": "1.7.26", + "@swc/core-linux-arm-gnueabihf": "1.7.26", + "@swc/core-linux-arm64-gnu": "1.7.26", + "@swc/core-linux-arm64-musl": "1.7.26", + "@swc/core-linux-x64-gnu": "1.7.26", + "@swc/core-linux-x64-musl": "1.7.26", + "@swc/core-win32-arm64-msvc": "1.7.26", + "@swc/core-win32-ia32-msvc": "1.7.26", + "@swc/core-win32-x64-msvc": "1.7.26" + }, + "peerDependencies": { + "@swc/helpers": "*" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.26.tgz", + "integrity": "sha512-FF3CRYTg6a7ZVW4yT9mesxoVVZTrcSWtmZhxKCYJX9brH4CS/7PRPjAKNk6kzWgWuRoglP7hkjQcd6EpMcZEAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.26.tgz", + "integrity": "sha512-az3cibZdsay2HNKmc4bjf62QVukuiMRh5sfM5kHR/JMTrLyS6vSw7Ihs3UTkZjUxkLTT8ro54LI6sV6sUQUbLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.26.tgz", + "integrity": "sha512-VYPFVJDO5zT5U3RpCdHE5v1gz4mmR8BfHecUZTmD2v1JeFY6fv9KArJUpjrHEEsjK/ucXkQFmJ0jaiWXmpOV9Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.26.tgz", + "integrity": "sha512-YKevOV7abpjcAzXrhsl+W48Z9mZvgoVs2eP5nY+uoMAdP2b3GxC0Df1Co0I90o2lkzO4jYBpTMcZlmUXLdXn+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.26.tgz", + "integrity": "sha512-3w8iZICMkQQON0uIcvz7+Q1MPOW6hJ4O5ETjA0LSP/tuKqx30hIniCGOgPDnv3UTMruLUnQbtBwVCZTBKR3Rkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.26.tgz", + "integrity": "sha512-c+pp9Zkk2lqb06bNGkR2Looxrs7FtGDMA4/aHjZcCqATgp348hOKH5WPvNLBl+yPrISuWjbKDVn3NgAvfvpH4w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.26.tgz", + "integrity": "sha512-PgtyfHBF6xG87dUSSdTJHwZ3/8vWZfNIXQV2GlwEpslrOkGqy+WaiiyE7Of7z9AvDILfBBBcJvJ/r8u980wAfQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.26.tgz", + "integrity": "sha512-9TNXPIJqFynlAOrRD6tUQjMq7KApSklK3R/tXgIxc7Qx+lWu8hlDQ/kVPLpU7PWvMMwC/3hKBW+p5f+Tms1hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.26.tgz", + "integrity": "sha512-9YngxNcG3177GYdsTum4V98Re+TlCeJEP4kEwEg9EagT5s3YejYdKwVAkAsJszzkXuyRDdnHUpYbTrPG6FiXrQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.26.tgz", + "integrity": "sha512-VR+hzg9XqucgLjXxA13MtV5O3C0bK0ywtLIBw/+a+O+Oc6mxFWHtdUeXDbIi5AiPbn0fjgVJMqYnyjGyyX8u0w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true + }, + "node_modules/@swc/types": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", + "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", + "dev": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/react": { + "name": "types-react", + "version": "19.0.0-rc.1", + "resolved": "https://registry.npmjs.org/types-react/-/types-react-19.0.0-rc.1.tgz", + "integrity": "sha512-RshndUfqTW6K3STLPis8BtAYCGOkMbtvYsi90gmVNDZBXUyUc5juf2PE9LfS/JmOlUIRO8cWTS/1MTnmhjDqyQ==", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "name": "types-react-dom", + "version": "19.0.0-rc.1", + "resolved": "https://registry.npmjs.org/types-react-dom/-/types-react-dom-19.0.0-rc.1.tgz", + "integrity": "sha512-VSLZJl8VXCD0fAWp7DUTFUDCcZ8DVXOQmjhJMD03odgeFmu14ZQJHCXeETm3BEAhJqfgJaFkLnGkQv88sRx0fQ==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz", + "integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.8.0", + "@typescript-eslint/type-utils": "8.8.0", + "@typescript-eslint/utils": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz", + "integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.8.0", + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/typescript-estree": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz", + "integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz", + "integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.8.0", + "@typescript-eslint/utils": "8.8.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz", + "integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz", + "integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz", + "integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.8.0", + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/typescript-estree": "8.8.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz", + "integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.1.tgz", + "integrity": "sha512-vgWOY0i1EROUK0Ctg1hwhtC3SdcDjZcdit4Ups4aPkDcB1jYhmo+RMYWY87cmXMhvtD5uf8lV89j2w16vkdSVg==", + "dev": true, + "dependencies": { + "@swc/core": "^1.7.26" + }, + "peerDependencies": { + "vite": "^4 || ^5" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz", + "integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.6.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.11.1", + "@eslint/plugin-kit": "^0.2.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", + "@nodelib/fs.walk": "^1.2.8", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.1.0-rc-fb9a90fa48-20240614", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0-rc-fb9a90fa48-20240614.tgz", + "integrity": "sha512-xsiRwaDNF5wWNC4ZHLut+x/YcAxksUd9Rizt7LaEn3bV8VyYRpXnRJQlLOfYaVy9esk4DFP4zPPnoNVjq5Gc0w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.12.tgz", + "integrity": "sha512-9neVjoGv20FwYtCP6CB1dzR1vr57ZDNOXst21wd2xJ/cTlM2xLq0GWVlSNTdMn/4BtP6cHYBMCSp1wFBJ9jBsg==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-scope": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.10.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.10.0.tgz", + "integrity": "sha512-tqFIbz83w4Y5TCbtgjZjApohbuh7K9BxGYFm7ifwDR240tvdb7P9x+/9VvUKlmkPoiknoJtanI8UOrqxS3a7lQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "19.0.0-rc-0751fac7-20241002", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0-rc-0751fac7-20241002.tgz", + "integrity": "sha512-qbwgll2JbsH16OHaNhbUtS55eyjgf9f1i682crOjcHLns9pyFvPjeiJIztBnhAk9GVS26ZiI00O2oA8AV/FGDA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.0.0-rc-0751fac7-20241002", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0-rc-0751fac7-20241002.tgz", + "integrity": "sha512-RpPpmpjWsKI4ThsphySlyFMfXx5Fof8Q8k9oPKqxEAR5Yq20DbQiY/fj+QYQipI0p3zptRSnovaRAKFnmidHKA==", + "dependencies": { + "scheduler": "0.25.0-rc-0751fac7-20241002" + }, + "peerDependencies": { + "react": "19.0.0-rc-0751fac7-20241002" + } + }, + "node_modules/react-spinners": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.14.1.tgz", + "integrity": "sha512-2Izq+qgQ08HTofCVEdcAQCXFEYfqTDdfeDQJeo/HHQiQJD4imOicNLhkfN2eh1NYEWVOX4D9ok2lhuDB0z3Aag==", + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.25.0-rc-0751fac7-20241002", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-0751fac7-20241002.tgz", + "integrity": "sha512-1aCByRIndzfrl7Qh2Uh/hhOyXK20pCre8XwR1RKOKK9FvWAt8iukv3Xtpf04yJajXW6zRUfSv6dRZFL9E0ZMuw==" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.8.0.tgz", + "integrity": "sha512-BjIT/VwJ8+0rVO01ZQ2ZVnjE1svFBiRczcpr1t1Yxt7sT25VSbPfrJtDsQ8uQTy2pilX5nI9gwxhUyLULNentw==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.8.0", + "@typescript-eslint/parser": "8.8.0", + "@typescript-eslint/utils": "8.8.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..edf1d93 --- /dev/null +++ b/web/package.json @@ -0,0 +1,35 @@ +{ + "name": "gomuks-web", + "private": true, + "license": "AGPL-3.0-or-later", + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@types/react": "npm:types-react@rc", + "@types/react-dom": "npm:types-react-dom@rc", + "react": "^19.0.0-rc-0751fac7-20241002", + "react-dom": "^19.0.0-rc-0751fac7-20241002", + "react-spinners": "^0.14.1" + }, + "devDependencies": { + "@eslint/js": "^9.11.1", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^9.11.1", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.12", + "globals": "^15.9.0", + "typescript": "^5.5.3", + "typescript-eslint": "^8.7.0", + "vite": "^5.4.8" + }, + "overrides": { + "@types/react": "npm:types-react@rc", + "@types/react-dom": "npm:types-react-dom@rc" + } +} diff --git a/web/public/gomuks.png b/web/public/gomuks.png new file mode 100644 index 0000000000000000000000000000000000000000..645df4f910e59cd590968e6b6f1f90568c540e36 GIT binary patch literal 85635 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 literal 0 HcmV?d00001 diff --git a/web/src/App.css b/web/src/App.css new file mode 100644 index 0000000..e69de29 diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..96360ad --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,68 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { useEffect, useMemo, useState } from "react" +import "./App.css" +import Client from "./client.ts" +import WSClient from "./wsclient.ts" +import { ClientState } from "./hievents.ts" +import { ConnectionEvent } from "./rpc.ts" +import { LoginScreen, VerificationScreen } from "./login" +import { ScaleLoader } from "react-spinners" +import MainScreen from "./MainScreen.tsx" + +function App() { + const [connState, setConnState] = useState() + const [clientState, setClientState] = useState() + const client = useMemo(() => new Client(new WSClient("/_gomuks/websocket")), []) + useEffect(() => { + ((window as unknown) as { client: Client }).client = client + + // TODO remove this debug log + const unlistenDebug = client.rpc.event.listen(ev => { + console.debug("Received event:", ev) + }) + const unlistenConnect = client.rpc.connect.listen(setConnState) + const unlistenState = client.state.listen(setClientState) + client.rpc.start() + return () => { + unlistenConnect() + unlistenState() + unlistenDebug() + client.rpc.stop() + } + }, [client]) + + if (connState?.error) { + return

+ } else if (!connState?.connected || !clientState) { + const msg = connState?.connected ? + "Waiting for client state..." : "Connecting to backend..." + return
+ + {msg} +
+ } else if (!clientState.is_logged_in) { + return + } else if (!clientState.is_verified) { + return + } else { + return + } +} + +export default App diff --git a/web/src/MainScreen.css b/web/src/MainScreen.css new file mode 100644 index 0000000..3fe4e5b --- /dev/null +++ b/web/src/MainScreen.css @@ -0,0 +1,7 @@ +main.matrix-main { + position: fixed; + inset: 0; + + display: grid; + grid-template: "roomlist roomview" 1fr / 300px 1fr; +} diff --git a/web/src/MainScreen.tsx b/web/src/MainScreen.tsx new file mode 100644 index 0000000..f8d3643 --- /dev/null +++ b/web/src/MainScreen.tsx @@ -0,0 +1,36 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { useState } from "react" +import type Client from "./client.ts" +import type { RoomID } from "./hitypes.ts" +import RoomList from "./RoomList.tsx" +import RoomView from "./RoomView.tsx" +import "./MainScreen.css" + +export interface MainScreenProps { + client: Client +} + +const MainScreen = ({ client }: MainScreenProps) => { + const [activeRoomID, setActiveRoomID] = useState(null) + const activeRoom = activeRoomID && client.store.rooms.get(activeRoomID) + return
+ + {activeRoom && } +
+} + +export default MainScreen diff --git a/web/src/RoomList.css b/web/src/RoomList.css new file mode 100644 index 0000000..19fb4e9 --- /dev/null +++ b/web/src/RoomList.css @@ -0,0 +1,50 @@ +div.room-list { + grid-area: roomlist; + overflow-y: auto; + + div.room-entry { + width: 100%; + display: flex; + gap: 4px; + border-radius: 4px; + user-select: none; + cursor: pointer; + + &:hover { + background-color: #EEE; + } + + > div.room-entry-left { + height: 48px; + width: 48px; + + > img.room-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + padding: 4px; + } + } + + > div.room-entry-right { + width: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + justify-content: space-around; + + > div.room-name { + font-weight: bold; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + > div.message-preview { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + } + } +} diff --git a/web/src/RoomList.tsx b/web/src/RoomList.tsx new file mode 100644 index 0000000..b15addb --- /dev/null +++ b/web/src/RoomList.tsx @@ -0,0 +1,101 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { useMemo } from "react" +import Client from "./client.ts" +import { DBEvent, RoomID } from "./hitypes.ts" +import { useNonNullEventAsState } from "./eventdispatcher.ts" +import { RoomListEntry } from "./statestore.ts" +import "./RoomList.css" + +export interface RoomListProps { + client: Client + setActiveRoom: (room_id: RoomID) => void +} + +const RoomList = ({ client, setActiveRoom }: RoomListProps) => { + const roomList = useNonNullEventAsState(client.store.roomList) + const clickRoom = useMemo(() => (evt: React.MouseEvent) => { + const roomID = evt.currentTarget.getAttribute("data-room-id") + if (roomID) { + setActiveRoom(roomID) + } else { + console.warn("No room ID :(", evt.currentTarget) + } + }, [setActiveRoom]) + + return
+ {reverseMap(roomList, room => + , + )} +
+} + +function reverseMap(arg: T[], fn: (a: T) => O) { + return arg.map((_, i, arr) => fn(arr[arr.length - i - 1])) +} + +export interface RoomListEntryProps { + client: Client + room: RoomListEntry + setActiveRoom: (evt: React.MouseEvent) => void +} + +function makePreviewText(evt?: DBEvent): string { + if (!evt) { + return "" + } + if (evt.type === "m.room.message") { + // @ts-expect-error TODO add content types + return evt.content.body + } else if (evt.decrypted_type === "m.room.message") { + // @ts-expect-error TODO add content types + return evt.decrypted.body + } + return "" +} + +const avatarRegex = /^mxc:\/\/([a-zA-Z0-9.:-]+)\/([a-zA-Z0-9_-]+)$/ + +const getAvatarURL = (avatar?: string): string | undefined => { + if (!avatar) { + return undefined + } + const match = avatar.match(avatarRegex) + if (!match) { + return undefined + } + return `_gomuks/media/${match[1]}/${match[2]}` +} + +const RoomEntry = ({ room, setActiveRoom }: RoomListEntryProps) => { + const previewText = makePreviewText(room.preview_event) + return
+
+ +
+
+
{room.name}
+ {previewText &&
{previewText}
} +
+
+} + +export default RoomList diff --git a/web/src/RoomView.css b/web/src/RoomView.css new file mode 100644 index 0000000..445b64a --- /dev/null +++ b/web/src/RoomView.css @@ -0,0 +1,3 @@ +div.room-view { + overflow-y: scroll; +} diff --git a/web/src/RoomView.tsx b/web/src/RoomView.tsx new file mode 100644 index 0000000..fe8ff44 --- /dev/null +++ b/web/src/RoomView.tsx @@ -0,0 +1,38 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import Client from "./client.ts" +import { RoomStateStore } from "./statestore.ts" +import { useNonNullEventAsState } from "./eventdispatcher.ts" +import "./RoomView.css" +import TimelineEvent from "./TimelineEvent.tsx" + +export interface RoomViewProps { + client: Client + room: RoomStateStore +} + +const RoomView = ({ client, room }: RoomViewProps) => { + const roomMeta = useNonNullEventAsState(room.meta) + const timeline = useNonNullEventAsState(room.timeline) + return
+ {roomMeta.room_id} + {timeline.map(entry => )} +
+} + +export default RoomView diff --git a/web/src/TimelineEvent.css b/web/src/TimelineEvent.css new file mode 100644 index 0000000..e69de29 diff --git a/web/src/TimelineEvent.tsx b/web/src/TimelineEvent.tsx new file mode 100644 index 0000000..91adca4 --- /dev/null +++ b/web/src/TimelineEvent.tsx @@ -0,0 +1,39 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { RoomViewProps } from "./RoomView.tsx" +import "./TimelineEvent.css" + +export interface TimelineEventProps extends RoomViewProps { + eventRowID: number +} + +const TimelineEvent = ({ room, eventRowID }: TimelineEventProps) => { + const evt = room.eventsByRowID.get(eventRowID) + if (!evt) { + return null + } + // @ts-expect-error TODO add content types + const body = (evt.decrypted ?? evt.content).body + return
+ {evt.decrypted_type ?? evt.type} +   + {evt.sender} +   + {body ?? {JSON.stringify(evt.decrypted ?? evt.content, null, " ")}} +
+} + +export default TimelineEvent diff --git a/web/src/client.ts b/web/src/client.ts new file mode 100644 index 0000000..0f1525e --- /dev/null +++ b/web/src/client.ts @@ -0,0 +1,81 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import type { + ClientWellKnown, DBEvent, EventID, EventRowID, EventType, RoomID, TimelineRowID, UserID, +} from "./hitypes.ts" +import { ClientState, RPCEvent } from "./hievents.ts" +import { RPCClient } from "./rpc.ts" +import { CachedEventDispatcher } from "./eventdispatcher.ts" +import { StateStore } from "./statestore.ts" + +export default class Client { + readonly state = new CachedEventDispatcher() + readonly store = new StateStore() + + constructor(readonly rpc: RPCClient) { + this.rpc.event.listen(this.#handleEvent) + } + + #handleEvent = (ev: RPCEvent) => { + if (ev.command === "client_state") { + this.state.emit(ev.data) + } else if (ev.command === "sync_complete") { + this.store.applySync(ev.data) + } else if (ev.command === "events_decrypted") { + this.store.applyDecrypted(ev.data) + } + } + + request(command: string, data: Req): Promise { + return this.rpc.request(command, data) + } + + sendMessage(room_id: RoomID, event_type: EventType, content: Record): Promise { + return this.request("send_message", { room_id, event_type, content }) + } + + ensureGroupSessionShared(room_id: RoomID): Promise { + return this.request("ensure_group_session_shared", { room_id }) + } + + getEvent(room_id: RoomID, event_id: EventID): Promise { + return this.request("get_event", { room_id, event_id }) + } + + getEventsByRowIDs(row_ids: EventRowID[]): Promise { + return this.request("get_events_by_row_ids", { row_ids }) + } + + paginate(room_id: RoomID, max_timeline_id: TimelineRowID, limit: number): Promise { + return this.request("paginate", { room_id, max_timeline_id, limit }) + } + + paginateServer(room_id: RoomID, limit: number): Promise { + return this.request("paginate_server", { room_id, limit }) + } + + discoverHomeserver(user_id: UserID): Promise { + return this.request("discover_homeserver", { user_id }) + } + + login(homeserver_url: string, username: string, password: string): Promise { + return this.request("login", { homeserver_url, username, password }) + } + + verify(recovery_key: string): Promise { + return this.request("verify", { recovery_key }) + } +} diff --git a/web/src/eventdispatcher.ts b/web/src/eventdispatcher.ts new file mode 100644 index 0000000..9daae59 --- /dev/null +++ b/web/src/eventdispatcher.ts @@ -0,0 +1,79 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { useEffect, useState } from "react" + +export function useEventAsState(dispatcher?: EventDispatcher): T | null { + const [state, setState] = useState(null) + useEffect(() => dispatcher && dispatcher.listen(setState), [dispatcher]) + return state +} + +export function useNonNullEventAsState(dispatcher: NonNullCachedEventDispatcher): T { + const [state, setState] = useState(dispatcher.current) + useEffect(() => dispatcher.listen(setState), [dispatcher]) + return state +} + +export class EventDispatcher { + #listeners: ((data: T) => void)[] = [] + + listen(listener: (data: T) => void): () => void { + this.#listeners.push(listener) + return () => { + const idx = this.#listeners.indexOf(listener) + if (idx >= 0) { + this.#listeners.splice(idx, 1) + } + } + } + + emit(data: T) { + for (const listener of this.#listeners) { + listener(data) + } + } +} + +export class CachedEventDispatcher extends EventDispatcher { + current: T | null + + constructor(cache?: T | null) { + super() + this.current = cache ?? null + } + + emit(data: T) { + this.current = data + super.emit(data) + } + + listen(listener: (data: T) => void): () => void { + const unlisten = super.listen(listener) + if (this.current !== null) { + listener(this.current) + } + return unlisten + } +} + +export class NonNullCachedEventDispatcher extends CachedEventDispatcher { + current: T + + constructor(cache: T) { + super(cache) + this.current = cache + } +} diff --git a/web/src/hievents.ts b/web/src/hievents.ts new file mode 100644 index 0000000..cbd6ea5 --- /dev/null +++ b/web/src/hievents.ts @@ -0,0 +1,96 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { + DBEvent, + DBRoom, + DeviceID, + EventRowID, + RoomID, + TimelineRowTuple, + UserID, +} from "./hitypes.ts" + +export interface RPCCommand { + command: string + request_id: number + data: T +} + +export interface TypingEventData { + room_id: RoomID + user_ids: UserID[] +} + +export interface TypingEvent extends RPCCommand { + command: "typing" +} + +export interface SendCompleteData { + event: DBEvent + error: string | null +} + +export interface SendCompleteEvent extends RPCCommand { + command: "send_complete" +} + +export interface EventsDecryptedData { + room_id: RoomID + preview_event_rowid?: EventRowID + events: DBEvent[] +} + +export interface EventsDecryptedEvent extends RPCCommand { + command: "events_decrypted" +} + +export interface SyncRoom { + meta: DBRoom + timeline: TimelineRowTuple[] + events: DBEvent[] + reset: boolean +} + +export interface SyncCompleteData { + rooms: Record +} + +export interface SyncCompleteEvent extends RPCCommand { + command: "sync_complete" +} + + +export type ClientState = { + is_logged_in: false + is_verified: false +} | { + is_logged_in: true + is_verified: boolean + user_id: UserID + device_id: DeviceID + homeserver_url: string +} + +export interface ClientStateEvent extends RPCCommand { + command: "client_state" +} + +export type RPCEvent = + ClientStateEvent | + TypingEvent | + SendCompleteEvent | + EventsDecryptedEvent | + SyncCompleteEvent diff --git a/web/src/hitypes.ts b/web/src/hitypes.ts new file mode 100644 index 0000000..190aebe --- /dev/null +++ b/web/src/hitypes.ts @@ -0,0 +1,125 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +export type EventRowID = number +export type TimelineRowID = number +export type RoomID = string +export type EventID = string +export type UserID = string +export type DeviceID = string +export type EventType = string +export type ContentURI = string +export type RoomAlias = string +export type RoomVersion = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" +export type RoomType = "" | "m.space" +export type RelationType = "m.annotation" | "m.reference" | "m.replace" | "m.thread" + +export interface TimelineRowTuple { + timeline_rowid: TimelineRowID + event_rowid: EventRowID +} + +export enum RoomNameQuality { + Nil = 0, + Participants, + CanonicalAlias, + Explicit, +} + +export interface RoomPredecessor { + room_id: RoomID + event_id: EventID +} + +export interface CreateEventContent { + type: RoomType + "m.federate": boolean + room_version: RoomVersion + predecessor: RoomPredecessor +} + +export interface LazyLoadSummary { + heroes?: UserID[] + "m.joined_member_count"?: number + "m.invited_member_count"?: number +} + +export interface EncryptionEventContent { + algorithm: string + rotation_period_ms?: number + rotation_period_msgs?: number +} + +export interface DBRoom { + room_id: RoomID + creation_content: CreateEventContent + + name?: string + name_quality: RoomNameQuality + avatar?: ContentURI + topic?: string + canonical_alias?: RoomAlias + lazy_load_summary?: LazyLoadSummary + + encryption_event?: EncryptionEventContent + has_member_list: boolean + + preview_event_rowid: EventRowID + sorting_timestamp: number + + prev_batch: string +} + +export interface DBEvent { + rowid: EventRowID + timeline_rowid: TimelineRowID + + room_id: RoomID + event_id: EventID + sender: UserID + type: EventType + state_key?: string + timestamp: number + + content: unknown + decrypted?: unknown + decrypted_type?: EventType + unsigned: EventUnsigned + + transaction_id?: string + + redacted_by?: EventID + relates_to?: EventID + relation_type?: RelationType + + decryption_error?: string + + reactions?: Record + last_edit_rowid?: EventRowID +} + +export interface EventUnsigned { + prev_content?: unknown + prev_sender?: UserID +} + +export interface ClientWellKnown { + "m.homeserver": { + base_url: string + }, + "m.identity_server": { + base_url: string + } +} diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..8d46438 --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,32 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #EEE; +} + +#root { + display: flex; + justify-content: center; +} + +main { + background-color: white; +} + +pre, code { + font-family: "Fira Code", monospace; +} + +button { + cursor: pointer; + font-size: 1em; +} + +:root { + --primary-color: #00c853; + --primary-color-light: #92ffc0; + --primary-color-dark: #00b24a; + --error-color: red; + --error-color-light: #ff6666; +} diff --git a/web/src/login/LoginScreen.css b/web/src/login/LoginScreen.css new file mode 100644 index 0000000..7f67ffd --- /dev/null +++ b/web/src/login/LoginScreen.css @@ -0,0 +1,66 @@ +main.matrix-login { + max-width: 30rem; + width: 100%; + padding: 3rem 6rem; + + box-shadow: 0 0 1rem rgba(0, 0, 0, 0.25); + margin: 2rem; + + @media (width < 800px) { + padding: 2rem 4rem; + } + + @media (width < 500px) { + padding: 1rem; + box-shadow: none; + margin: 0 !important; + } + + h1 { + margin: 0 0 2rem; + text-align: center; + } + + button, input { + margin-top: .5rem; + padding: 1rem; + font-size: 1rem; + width: 100%; + display: block; + border-radius: .25rem; + box-sizing: border-box; + } + + input { + border: 1px solid var(--primary-color); + + &:hover { + outline: 1px solid var(--primary-color); + } + + &:focus { + outline: 3px solid var(--primary-color); + } + } + + form { + margin: 2rem 0; + } + + button { + background-color: var(--primary-color); + color: white; + font-weight: bold; + border: none; + + &:hover, &:focus { + background-color: var(--primary-color-dark); + } + } + + div.error { + border: 2px solid var(--error-color); + border-radius: .25rem; + padding: 1rem; + } +} diff --git a/web/src/login/LoginScreen.tsx b/web/src/login/LoginScreen.tsx new file mode 100644 index 0000000..a85ad24 --- /dev/null +++ b/web/src/login/LoginScreen.tsx @@ -0,0 +1,87 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { useCallback, useEffect, useState } from "react" +import type Client from "../client.ts" +import "./LoginScreen.css" +import { ClientState } from "../hievents.ts" + +export interface LoginScreenProps { + client: Client + clientState: ClientState +} + +export const LoginScreen = ({ client }: LoginScreenProps) => { + const [username, setUsername] = useState("") + const [password, setPassword] = useState("") + const [homeserverURL, setHomeserverURL] = useState("") + const [error, setError] = useState("") + + const login = useCallback((evt: React.FormEvent) => { + evt.preventDefault() + client.login(homeserverURL, username, password).then( + () => {}, + err => setError(err.toString()), + ) + }, [homeserverURL, username, password, client]) + + const resolveHomeserver = useCallback(() => { + client.discoverHomeserver(username).then( + resp => setHomeserverURL(resp["m.homeserver"].base_url), + err => setError(`Failed to resolve homeserver: ${err}`), + ) + }, [client, username]) + + useEffect(() => { + if (!username.startsWith("@") || !username.includes(":") || !username.includes(".")) { + return + } + const timeout = setTimeout(resolveHomeserver, 500) + return () => { + clearTimeout(timeout) + } + }, [username, resolveHomeserver]) + + return
+

gomuks web

+
+ setUsername(evt.target.value)} + /> + setPassword(evt.target.value)} + /> + setHomeserverURL(evt.target.value)} + /> + +
+ {error &&
+ {error} +
} +
+} diff --git a/web/src/login/VerificationScreen.tsx b/web/src/login/VerificationScreen.tsx new file mode 100644 index 0000000..2333e0f --- /dev/null +++ b/web/src/login/VerificationScreen.tsx @@ -0,0 +1,52 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { useCallback, useState } from "react" +import "./LoginScreen.css" +import { LoginScreenProps } from "./LoginScreen.tsx" + +export const VerificationScreen = ({ client, clientState }: LoginScreenProps) => { + if (!clientState.is_logged_in) { + throw new Error("Invalid state") + } + const [recoveryKey, setRecoveryKey] = useState("") + const [error, setError] = useState("") + + const verify = useCallback((evt: React.FormEvent) => { + evt.preventDefault() + client.verify(recoveryKey).then( + () => {}, + err => setError(err.toString()), + ) + }, [recoveryKey, client]) + + return
+

gomuks web

+
+

Successfully logged in as {clientState.user_id}

+ setRecoveryKey(evt.target.value)} + /> + +
+ {error &&
+ {error} +
} +
+} diff --git a/web/src/login/index.ts b/web/src/login/index.ts new file mode 100644 index 0000000..18e4e84 --- /dev/null +++ b/web/src/login/index.ts @@ -0,0 +1,2 @@ +export { LoginScreen } from "./LoginScreen.tsx" +export { VerificationScreen } from "./VerificationScreen.tsx" diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..173a9d9 --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,25 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { StrictMode } from "react" +import { createRoot } from "react-dom/client" +import App from "./App.tsx" +import "./index.css" + +createRoot(document.getElementById("root")!).render( + + + , +) diff --git a/web/src/rpc.ts b/web/src/rpc.ts new file mode 100644 index 0000000..95ec284 --- /dev/null +++ b/web/src/rpc.ts @@ -0,0 +1,39 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { RPCEvent } from "./hievents.ts" +import { EventDispatcher } from "./eventdispatcher.ts" + +export class CancellablePromise extends Promise { + constructor( + executor: (resolve: (value: T) => void, reject: (reason?: Error) => void) => void, + readonly cancel: (reason: string) => void, + ) { + super(executor) + } +} + +export interface RPCClient { + connect: EventDispatcher + event: EventDispatcher + start(): void + stop(): void + request(command: string, data: Req): CancellablePromise +} + +export interface ConnectionEvent { + connected: boolean + error: Error | null +} diff --git a/web/src/statestore.ts b/web/src/statestore.ts new file mode 100644 index 0000000..62553e5 --- /dev/null +++ b/web/src/statestore.ts @@ -0,0 +1,196 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import type { + ContentURI, + DBEvent, + DBRoom, + EventID, + EventRowID, + LazyLoadSummary, + RoomID, + TimelineRowTuple, +} from "./hitypes.ts" +import type { EventsDecryptedData, SyncCompleteData, SyncRoom } from "./hievents.ts" +import { NonNullCachedEventDispatcher } from "./eventdispatcher.ts" + +function arraysAreEqual(arr1?: T[], arr2?: T[]): boolean { + if (!arr1 || !arr2) { + return !arr1 && !arr2 + } + if (arr1.length !== arr2.length) { + return false + } + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] !== arr2[i]) { + return false + } + } + return true +} + +function llSummaryIsEqual(ll1?: LazyLoadSummary, ll2?: LazyLoadSummary): boolean { + return ll1?.["m.joined_member_count"] === ll2?.["m.joined_member_count"] && + ll1?.["m.invited_member_count"] === ll2?.["m.invited_member_count"] && + arraysAreEqual(ll1?.heroes, ll2?.heroes) +} + +function visibleMetaIsEqual(meta1: DBRoom, meta2: DBRoom): boolean { + return meta1.name === meta2.name && + meta1.avatar === meta2.avatar && + meta1.topic === meta2.topic && + meta1.canonical_alias === meta2.canonical_alias && + llSummaryIsEqual(meta1.lazy_load_summary, meta2.lazy_load_summary) && + meta1.encryption_event?.algorithm === meta2.encryption_event?.algorithm && + meta1.has_member_list === meta2.has_member_list +} + +export class RoomStateStore { + readonly meta: NonNullCachedEventDispatcher + readonly timeline = new NonNullCachedEventDispatcher([]) + readonly eventsByRowID: Map = new Map() + readonly eventsByID: Map = new Map() + + constructor(meta: DBRoom) { + this.meta = new NonNullCachedEventDispatcher(meta) + } + + applySync(sync: SyncRoom) { + if (visibleMetaIsEqual(this.meta.current, sync.meta)) { + this.meta.current = sync.meta + } else { + this.meta.emit(sync.meta) + } + for (const evt of sync.events) { + this.eventsByRowID.set(evt.rowid, evt) + this.eventsByID.set(evt.event_id, evt) + } + if (sync.reset) { + this.timeline.emit(sync.timeline) + } else { + this.timeline.emit([...this.timeline.current, ...sync.timeline]) + } + } + + applyDecrypted(decrypted: EventsDecryptedData) { + let timelineChanged = false + for (const evt of decrypted.events) { + timelineChanged = timelineChanged || !!this.timeline.current.find(rt => rt.event_rowid === evt.rowid) + this.eventsByRowID.set(evt.rowid, evt) + this.eventsByID.set(evt.event_id, evt) + } + if (timelineChanged) { + this.timeline.emit([...this.timeline.current]) + } + if (decrypted.preview_event_rowid) { + this.meta.current.preview_event_rowid = decrypted.preview_event_rowid + } + } +} + +export interface RoomListEntry { + room_id: RoomID + sorting_timestamp: number + preview_event?: DBEvent + name: string + avatar?: ContentURI +} + +export class StateStore { + readonly rooms: Map = new Map() + readonly roomList = new NonNullCachedEventDispatcher([]) + + #roomListEntryChanged(entry: SyncRoom, oldEntry: RoomStateStore): boolean { + return entry.meta.sorting_timestamp !== oldEntry.meta.current.sorting_timestamp || + entry.meta.preview_event_rowid !== oldEntry.meta.current.preview_event_rowid || + entry.events.findIndex(evt => evt.rowid === entry.meta.preview_event_rowid) !== -1 + } + + #makeRoomListEntry(entry: SyncRoom, room?: RoomStateStore): RoomListEntry { + if (!room) { + room = this.rooms.get(entry.meta.room_id) + } + return { + room_id: entry.meta.room_id, + sorting_timestamp: entry.meta.sorting_timestamp, + preview_event: room?.eventsByRowID.get(entry.meta.preview_event_rowid), + name: entry.meta.name ?? "Unnamed room", + avatar: entry.meta.avatar, + } + } + + applySync(sync: SyncCompleteData) { + const resyncRoomList = this.roomList.current.length === 0 + const changedRoomListEntries = new Map() + for (const [roomID, data] of Object.entries(sync.rooms)) { + let isNewRoom = false + let room = this.rooms.get(roomID) + if (!room) { + room = new RoomStateStore(data.meta) + this.rooms.set(roomID, room) + isNewRoom = true + } + const roomListEntryChanged = !resyncRoomList && (isNewRoom || this.#roomListEntryChanged(data, room)) + room.applySync(data) + if (roomListEntryChanged) { + changedRoomListEntries.set(roomID, this.#makeRoomListEntry(data, room)) + } + } + + let updatedRoomList: RoomListEntry[] | undefined + if (resyncRoomList) { + updatedRoomList = Object.values(sync.rooms).map(entry => this.#makeRoomListEntry(entry)) + updatedRoomList.sort((r1, r2) => r1.sorting_timestamp - r2.sorting_timestamp) + } else if (changedRoomListEntries.size > 0) { + updatedRoomList = this.roomList.current.filter(entry => !changedRoomListEntries.has(entry.room_id)) + for (const entry of changedRoomListEntries.values()) { + if (updatedRoomList.length === 0 || entry.sorting_timestamp >= + updatedRoomList[updatedRoomList.length - 1].sorting_timestamp) { + updatedRoomList.push(entry) + } else if (entry.sorting_timestamp <= 0 || + entry.sorting_timestamp < updatedRoomList[0]?.sorting_timestamp) { + updatedRoomList.unshift(entry) + } else { + const indexToPushAt = updatedRoomList.findLastIndex(val => + val.sorting_timestamp <= entry.sorting_timestamp) + updatedRoomList.splice(indexToPushAt + 1, 0, entry) + } + } + } + if (updatedRoomList) { + this.roomList.emit(updatedRoomList) + } + } + + applyDecrypted(decrypted: EventsDecryptedData) { + const room = this.rooms.get(decrypted.room_id) + if (!room) { + // TODO log or something? + return + } + room.applyDecrypted(decrypted) + if (decrypted.preview_event_rowid) { + const idx = this.roomList.current.findIndex(entry => entry.room_id === decrypted.room_id) + if (idx !== -1) { + const updatedRoomList = [...this.roomList.current] + updatedRoomList[idx] = { + ...updatedRoomList[idx], + preview_event: room.eventsByRowID.get(decrypted.preview_event_rowid), + } + this.roomList.emit(updatedRoomList) + } + } + } +} diff --git a/web/src/wsclient.ts b/web/src/wsclient.ts new file mode 100644 index 0000000..279560d --- /dev/null +++ b/web/src/wsclient.ts @@ -0,0 +1,147 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { RPCCommand, RPCEvent } from "./hievents.ts" +import { CachedEventDispatcher, EventDispatcher } from "./eventdispatcher.ts" +import { CancellablePromise, ConnectionEvent, RPCClient } from "./rpc.ts" + +export class ErrorResponse extends Error { + constructor(public data: unknown) { + super(`${data}`) + } +} + +export default class WSClient implements RPCClient { + #conn: WebSocket | null = null + readonly connect: CachedEventDispatcher = new CachedEventDispatcher() + readonly event: EventDispatcher = new EventDispatcher() + readonly #pendingRequests: Map void, + reject: (err: Error) => void + }> = new Map() + #nextRequestID: number = 1 + + constructor(readonly addr: string) { + + } + + start() { + try { + console.info("Connecting to websocket", this.addr) + this.#conn = new WebSocket(this.addr) + this.#conn.onmessage = this.#onMessage + this.#conn.onopen = this.#onOpen + this.#conn.onerror = this.#onError + this.#conn.onclose = this.#onClose + } catch (err) { + this.#dispatchConnectionStatus(false, err as Error) + } + } + + stop() { + this.#conn?.close(1000, "Client closed") + } + + #cancelRequest(request_id: number, reason: string) { + if (!this.#pendingRequests.has(request_id)) { + console.debug("Tried to cancel unknown request", request_id) + return + } + this.request("cancel", { request_id, reason }).then( + () => console.debug("Cancelled request", request_id, "for", reason), + err => console.debug("Failed to cancel request", request_id, "for", reason, err), + ) + } + + request(command: string, data: Req): CancellablePromise { + if (!this.#conn) { + return new CancellablePromise((_resolve, reject) => { + reject(new Error("Websocket not connected")) + }, () => { + }) + } + const request_id = this.#nextRequestID++ + return new CancellablePromise((resolve, reject) => { + if (!this.#conn) { + reject(new Error("Websocket not connected")) + return + } + this.#pendingRequests.set(request_id, { resolve: resolve as ((value: unknown) => void), reject }) + this.#conn.send(JSON.stringify({ + command, + request_id, + data, + })) + }, this.#cancelRequest.bind(this, request_id)) + } + + #onMessage = (ev: MessageEvent) => { + let parsed: RPCCommand + try { + parsed = JSON.parse(ev.data) + if (!parsed.command) { + throw new Error("Missing 'command' field in JSON message") + } + } catch (err) { + console.error("Malformed JSON in websocket:", err) + console.error("Message:", ev.data) + this.#conn?.close(1003, "Malformed JSON") + return + } + if (parsed.command === "response" || parsed.command === "error") { + const target = this.#pendingRequests.get(parsed.request_id) + if (!target) { + console.error("Received response for unknown request:", parsed) + return + } + this.#pendingRequests.delete(parsed.request_id) + if (parsed.command === "response") { + target.resolve(parsed.data) + } else { + target.reject(new ErrorResponse(parsed.data)) + } + } else { + this.event.emit(parsed as RPCEvent) + } + } + + #dispatchConnectionStatus(connected: boolean, error: Error | null) { + this.connect.emit({ connected, error }) + } + + #onOpen = () => { + console.info("Websocket opened") + this.#dispatchConnectionStatus(true, null) + } + + #clearPending = () => { + for (const { reject } of this.#pendingRequests.values()) { + reject(new Error("Websocket closed")) + } + this.#pendingRequests.clear() + } + + #onError = (ev: Event) => { + console.error("Websocket error:", ev) + this.#dispatchConnectionStatus(false, new Error("Websocket error")) + this.#clearPending() + } + + #onClose = (ev: CloseEvent) => { + console.warn("Websocket closed:", ev) + this.#dispatchConnectionStatus(false, new Error(`Websocket closed: ${ev.code} ${ev.reason}`)) + this.#clearPending() + } +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..e618c22 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2023", + "useDefineForClassFields": true, + "lib": [ + "ES2023", + "DOM", + "DOM.Iterable" + ], + "module": "ESNext", + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "src", "vite.config.ts" + ] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..941d643 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,17 @@ +import {defineConfig} from "vite" +import react from "@vitejs/plugin-react-swc" + +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + "/_gomuks/websocket": { + target: "http://localhost:29325", + ws: true, + }, + "/_gomuks": { + target: "http://localhost:29325", + } + }, + }, +}) diff --git a/websocket.go b/websocket.go new file mode 100644 index 0000000..38e7e1e --- /dev/null +++ b/websocket.go @@ -0,0 +1,267 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "runtime/debug" + "sync" + "time" + + "github.com/coder/websocket" + "github.com/rs/zerolog" + + "maunium.net/go/mautrix/hicli" + "maunium.net/go/mautrix/hicli/database" + "maunium.net/go/mautrix/id" +) + +func writeCmd(ctx context.Context, conn *websocket.Conn, cmd *hicli.JSONCommand) error { + writer, err := conn.Writer(ctx, websocket.MessageText) + if err != nil { + return err + } + err = json.NewEncoder(writer).Encode(&cmd) + if err != nil { + return err + } + return writer.Close() +} + +const StatusEventsStuck = 4001 + +func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) { + var conn *websocket.Conn + log := zerolog.Ctx(r.Context()) + recoverPanic := func(context string) bool { + err := recover() + if err != nil { + logEvt := log.Error(). + Bytes(zerolog.ErrorStackFieldName, debug.Stack()). + Str("goroutine", context) + if realErr, ok := err.(error); ok { + logEvt = logEvt.Err(realErr) + } else { + logEvt = logEvt.Any(zerolog.ErrorFieldName, err) + } + logEvt.Msg("Panic in websocket handler") + return true + } + return false + } + defer recoverPanic("read loop") + + conn, acceptErr := websocket.Accept(w, r, &websocket.AcceptOptions{ + OriginPatterns: []string{"localhost:*"}, + }) + if acceptErr != nil { + log.Warn().Err(acceptErr).Msg("Failed to accept websocket connection") + return + } + log.Info().Msg("Accepted new websocket connection") + conn.SetReadLimit(128 * 1024) + ctx, cancel := context.WithCancel(context.Background()) + ctx = log.WithContext(ctx) + unsubscribe := func() {} + evts := make(chan *hicli.JSONCommand, 32) + forceClose := func() { + cancel() + unsubscribe() + _ = conn.CloseNow() + close(evts) + } + var closeOnce sync.Once + defer closeOnce.Do(forceClose) + closeManually := func(statusCode websocket.StatusCode, reason string) { + log.Debug().Stringer("status_code", statusCode).Str("reason", reason).Msg("Closing connection manually") + _ = conn.Close(statusCode, reason) + closeOnce.Do(forceClose) + } + unsubscribe = gmx.SubscribeEvents(closeManually, func(evt *hicli.JSONCommand) { + if ctx.Err() != nil { + return + } + select { + case evts <- evt: + default: + log.Warn().Msg("Event queue full, closing connection") + cancel() + go func() { + defer recoverPanic("closing connection after error in event handler") + _ = conn.Close(StatusEventsStuck, "Event queue full") + closeOnce.Do(forceClose) + }() + } + }) + + go func() { + defer recoverPanic("event loop") + defer closeOnce.Do(forceClose) + ctxDone := ctx.Done() + for { + select { + case cmd := <-evts: + err := writeCmd(ctx, conn, cmd) + if err != nil { + log.Err(err).Int64("req_id", cmd.RequestID).Msg("Failed to write outgoing event") + return + } else { + log.Trace().Int64("req_id", cmd.RequestID).Msg("Sent outgoing event") + } + case <-ctxDone: + return + } + } + }() + submitCmd := func(cmd *hicli.JSONCommand) { + defer func() { + if recoverPanic("command handler") { + _ = conn.Close(websocket.StatusInternalError, "Command handler panicked") + closeOnce.Do(forceClose) + } + }() + log.Trace(). + Int64("req_id", cmd.RequestID). + Str("command", cmd.Command). + RawJSON("data", cmd.Data). + Msg("Received command") + resp := gmx.Client.SubmitJSONCommand(ctx, cmd) + if ctx.Err() != nil { + return + } + err := writeCmd(ctx, conn, resp) + if err != nil && ctx.Err() == nil { + log.Err(err).Int64("req_id", cmd.RequestID).Msg("Failed to write response") + closeOnce.Do(forceClose) + } else { + log.Trace().Int64("req_id", cmd.RequestID).Msg("Sent response to command") + } + } + initData, initErr := json.Marshal(gmx.Client.State()) + if initErr != nil { + log.Err(initErr).Msg("Failed to marshal init message") + return + } + initErr = writeCmd(ctx, conn, &hicli.JSONCommand{ + Command: "client_state", + Data: initData, + }) + if initErr != nil { + log.Err(initErr).Msg("Failed to write init message") + return + } + go gmx.sendInitialData(ctx, conn) + log.Debug().Msg("Connection initialization complete") + var closeErr websocket.CloseError + for { + msgType, reader, err := conn.Reader(ctx) + if err != nil { + if errors.As(err, &closeErr) { + log.Debug(). + Stringer("status_code", closeErr.Code). + Str("reason", closeErr.Reason). + Msg("Connection closed") + } else { + log.Err(err).Msg("Failed to read message") + } + return + } else if msgType != websocket.MessageText { + log.Error().Stringer("message_type", msgType).Msg("Unexpected message type") + _ = conn.Close(websocket.StatusUnsupportedData, "Non-text message") + return + } + var cmd hicli.JSONCommand + err = json.NewDecoder(reader).Decode(&cmd) + if err != nil { + log.Err(err).Msg("Failed to parse message") + _ = conn.Close(websocket.StatusUnsupportedData, "Invalid JSON") + return + } + go submitCmd(&cmd) + } +} + +func (gmx *Gomuks) sendInitialData(ctx context.Context, conn *websocket.Conn) { + maxTS := time.Now().Add(1 * time.Hour) + log := zerolog.Ctx(ctx) + var roomCount int + const BatchSize = 100 + for { + rooms, err := gmx.Client.DB.Room.GetBySortTS(ctx, maxTS, BatchSize) + if err != nil { + if ctx.Err() == nil { + log.Err(err).Msg("Failed to get initial rooms to send to client") + } + return + } + roomCount += len(rooms) + payload := hicli.SyncComplete{ + Rooms: make(map[id.RoomID]*hicli.SyncRoom, len(rooms)-1), + } + for _, room := range rooms { + if room.SortingTimestamp == rooms[len(rooms)-1].SortingTimestamp { + break + } + maxTS = room.SortingTimestamp.Time + syncRoom := &hicli.SyncRoom{ + Meta: room, + Events: make([]*database.Event, 0, 2), + Timeline: make([]database.TimelineRowTuple, 0), + } + payload.Rooms[room.ID] = syncRoom + if room.PreviewEventRowID != 0 { + previewEvent, err := gmx.Client.DB.Event.GetByRowID(ctx, room.PreviewEventRowID) + if err != nil { + log.Err(err).Msg("Failed to get preview event for room") + return + } else if previewEvent != nil { + syncRoom.Events = append(syncRoom.Events, previewEvent) + } + if previewEvent != nil && previewEvent.LastEditRowID != nil { + lastEdit, err := gmx.Client.DB.Event.GetByRowID(ctx, *previewEvent.LastEditRowID) + if err != nil { + log.Err(err).Msg("Failed to get last edit for preview event") + return + } else if lastEdit != nil { + syncRoom.Events = append(syncRoom.Events, lastEdit) + } + } + } + } + marshaledPayload, err := json.Marshal(&payload) + if err != nil { + log.Err(err).Msg("Failed to marshal initial rooms to send to client") + return + } + err = writeCmd(ctx, conn, &hicli.JSONCommand{ + Command: "sync_complete", + RequestID: 0, + Data: marshaledPayload, + }) + if err != nil { + log.Err(err).Msg("Failed to send initial rooms to client") + return + } + if len(rooms) < BatchSize { + break + } + } + log.Info().Int("room_count", roomCount).Msg("Sent initial rooms to client") +}