Skip to content

Commit

Permalink
web: init
Browse files Browse the repository at this point in the history
  • Loading branch information
tulir committed Oct 6, 2024
1 parent 4767def commit 1a359f9
Show file tree
Hide file tree
Showing 40 changed files with 5,091 additions and 0 deletions.
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
target/
.tmp/
gomuks
start
run
*.exe
*.deb
coverage.out
coverage.html
deb/usr
*.prof
*.db*
*.log
28 changes: 28 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
62 changes: 62 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
272 changes: 272 additions & 0 deletions gomuks.go
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

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)
}
}
Loading

0 comments on commit 1a359f9

Please sign in to comment.