1
0
Fork 0
forked from Mirrors/gomuks
nyxmuks/gomuks.go
2024-10-12 00:29:40 +03:00

232 lines
7 KiB
Go

// 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"
"fmt"
"maps"
"net/http"
"os"
"os/signal"
"path/filepath"
"runtime"
"slices"
"sync"
"syscall"
"github.com/coder/websocket"
"github.com/rs/zerolog"
"go.mau.fi/util/dbutil"
"go.mau.fi/util/exerrors"
"go.mau.fi/util/exzerolog"
"maunium.net/go/mautrix/hicli"
)
type Gomuks struct {
Log *zerolog.Logger
Server *http.Server
Client *hicli.HiClient
ConfigDir string
DataDir string
CacheDir string
TempDir string
LogDir string
Config Config
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) InitDirectories() {
// 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")
}
}
if gmx.TempDir = os.Getenv("GOMUKS_TMPDIR"); gmx.TempDir == "" {
gmx.TempDir = filepath.Join(gmx.CacheDir, "tmp")
}
exerrors.PanicIfNotNil(os.MkdirAll(gmx.ConfigDir, 0700))
exerrors.PanicIfNotNil(os.MkdirAll(gmx.CacheDir, 0700))
exerrors.PanicIfNotNil(os.MkdirAll(gmx.TempDir, 0700))
exerrors.PanicIfNotNil(os.MkdirAll(gmx.DataDir, 0700))
exerrors.PanicIfNotNil(os.MkdirAll(gmx.LogDir, 0700))
}
func (gmx *Gomuks) SetupLog() {
gmx.Log = exerrors.Must(gmx.Config.Logging.Compile())
exzerolog.SetupDefaults(gmx.Log)
}
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)
}
}