Merged postgres into master

This commit is contained in:
Sascha L. Teichmann 2022-03-07 10:39:29 +01:00
commit 4829b413ce
16 changed files with 831 additions and 248 deletions

View File

@ -1,12 +1,18 @@
# MTSatellite # MTSatellite
**Attention** MTSatellite is currently getting some improvements
namely PostgreSQL backend support and and a new Vue-JS based web client.
The new features are in but the documentation is not updated, yet.
Hopefully this will change soon.
MTSatellite is a "realtime" web mapping system for [Minetest](http://minetest.net) worlds. MTSatellite is a "realtime" web mapping system for [Minetest](http://minetest.net) worlds.
With this system you can play your world and you instantly have an online map of it which With this system you can play your world and you instantly have an online map of it which
can be shared on the web. can be shared on the web.
To get a glimpse what it does watch [Realtime Webmapping for Minetest worlds](http://youtu.be/iYEROGPj7RI) To get a glimpse what it does watch [Realtime Webmapping for Minetest worlds](http://youtu.be/iYEROGPj7RI)
on YouTube. on YouTube.
A live map of an online world can be viewed [here](http://maps.mt.sha-bang.de/). A live map of an online world can be viewed [here](https://maps.mt.intevation.de/).
See [COMPILE](https://bitbucket.org/s_l_teichmann/mtsatellite/src/default/COMPILE.md) how to compile See [COMPILE](https://bitbucket.org/s_l_teichmann/mtsatellite/src/default/COMPILE.md) how to compile
MTSatellite. Essentially you need Go 1.4 (or higher) and a GNU/Linux system. MTSatellite. Essentially you need Go 1.4 (or higher) and a GNU/Linux system.

View File

@ -10,7 +10,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"sync" "sync"
"bitbucket.org/s_l_teichmann/mtsatellite/common" "bitbucket.org/s_l_teichmann/mtsatellite/common"
@ -57,7 +56,7 @@ func createTiles(
} }
func createBaseLevel( func createBaseLevel(
address string, dbcf common.DBClientFactory,
xMin, yMin, zMin, xMax, yMax, zMax int, xMin, yMin, zMin, xMax, yMax, zMax int,
transparent bool, transparentDim float32, transparent bool, transparentDim float32,
colorsFile string, bg color.RGBA, outDir string, colorsFile string, bg color.RGBA, outDir string,
@ -79,17 +78,10 @@ func createBaseLevel(
jobs := make(chan blockPos) jobs := make(chan blockPos)
var done sync.WaitGroup var done sync.WaitGroup
var proto string
if strings.ContainsRune(address, '/') {
proto = "unix"
} else {
proto = "tcp"
}
for i := 0; i < numWorkers; i++ { for i := 0; i < numWorkers; i++ {
var client *common.RedisClient var client common.DBClient
if client, err = common.NewRedisClient(proto, address); err != nil { if client, err = dbcf.Create(); err != nil {
return return
} }
done.Add(1) done.Add(1)

View File

@ -6,9 +6,7 @@ package main
import ( import (
"flag" "flag"
"fmt"
"log" "log"
"strings"
"bitbucket.org/s_l_teichmann/mtsatellite/common" "bitbucket.org/s_l_teichmann/mtsatellite/common"
) )
@ -70,16 +68,16 @@ func main() {
bg := common.ParseColorDefault(bgColor, common.BackgroundColor) bg := common.ParseColorDefault(bgColor, common.BackgroundColor)
dbcf, err := common.CreateDBClientFactory(host, port)
if err != nil {
log.Fatalf("error: %s\n", err)
}
defer dbcf.Close()
if !skipBaseLevel { if !skipBaseLevel {
td := common.Clamp32f(float32(transparentDim/100.0), 0.0, 1.0) td := common.Clamp32f(float32(transparentDim/100.0), 0.0, 1.0)
var address string
if strings.ContainsRune(host, '/') {
address = host
} else {
address = fmt.Sprintf("%s:%d", host, port)
}
if err := createBaseLevel( if err := createBaseLevel(
address, dbcf,
xMin, yMin, zMin, xMax, yMax, zMax, xMin, yMin, zMin, xMax, yMax, zMax,
transparent, td, transparent, td,
colorsFile, bg, colorsFile, bg,

View File

@ -6,12 +6,10 @@ package main
import ( import (
"flag" "flag"
"fmt"
"image" "image"
"log" "log"
"os" "os"
"runtime/pprof" "runtime/pprof"
"strings"
"bitbucket.org/s_l_teichmann/mtsatellite/common" "bitbucket.org/s_l_teichmann/mtsatellite/common"
) )
@ -85,17 +83,14 @@ func main() {
colors.TransparentDim = common.Clamp32f( colors.TransparentDim = common.Clamp32f(
float32(transparentDim/100.0), 0.0, 100.0) float32(transparentDim/100.0), 0.0, 100.0)
var proto, address string cf, err := common.CreateDBClientFactory(host, port)
if strings.ContainsRune(host, '/') { if err != nil {
proto, address = "unix", host log.Fatalf("error: %v\n", err)
} else {
proto, address = "tcp", fmt.Sprintf("%s:%d", host, port)
} }
var client *common.RedisClient client, err := cf.Create()
if err != nil {
if client, err = common.NewRedisClient(proto, address); err != nil { log.Fatalf("Cannot connect to '%s:%d': %s", host, port, err)
log.Fatalf("Cannot connect to '%s': %s", address, err)
} }
defer client.Close() defer client.Close()

91
cmd/mtwebmapper/config.go Normal file
View File

@ -0,0 +1,91 @@
// Copyright 2014, 2015, 2022 by Sascha L. Teichmann
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
package main
import (
"flag"
"time"
"github.com/BurntSushi/toml"
"bitbucket.org/s_l_teichmann/mtsatellite/common"
)
type duration struct {
time.Duration
}
type config struct {
WebPort int `toml:"web_port"`
WebHost string `toml:"web_host"`
WebDir string `toml:"web"`
MapDir string `toml:"map"`
RedisPort int `toml:"redis_port"`
RedisHost string `toml:"redis_host"`
ColorsFile string `toml:"colors"`
BGColor string `toml:"background"`
Workers int `toml:"workers"`
Transparent bool `toml:"transparent"`
TransparentDim float64 `toml:"transparent_dim"`
UpdateHosts string `toml:"update_hosts"`
Websockets bool `toml:"websockets"`
PlayersFIFO string `toml:"players"`
YMin int `toml:"ymin"`
YMax int `toml:"ymax"`
ChangeDuration duration `toml:"change_duration"`
}
func (d *duration) UnmarshalText(text []byte) error {
var err error
d.Duration, err = time.ParseDuration(string(text))
return err
}
func (cfg *config) bindFlags() {
defaultBgColor := common.ColorToHex(common.BackgroundColor)
flag.IntVar(&cfg.WebPort, "web-port", 8808, "port of the web server")
flag.IntVar(&cfg.WebPort, "p", 8808, "port of the web server (shorthand)")
flag.StringVar(&cfg.WebHost, "web-host", "localhost", "address to bind web server")
flag.StringVar(&cfg.WebHost, "h", "localhost", "address to bind web server(shorthand)")
flag.StringVar(&cfg.WebDir, "web", "web", "static served web files.")
flag.StringVar(&cfg.WebDir, "w", "web", "static served web files (shorthand)")
flag.StringVar(&cfg.MapDir, "map", "map", "directory of prerendered tiles")
flag.StringVar(&cfg.MapDir, "m", "map", "directory of prerendered tiles (shorthand)")
flag.StringVar(&cfg.UpdateHosts, "update-hosts", "localhost",
"';' separated list of hosts which are allowed to send map update requests")
flag.StringVar(&cfg.UpdateHosts, "u", "localhost",
"';' separated list of hosts which are allowed to send map update requests (shorthand)")
flag.StringVar(&cfg.RedisHost, "redis-host", "", "address of the backend Redis server")
flag.StringVar(&cfg.RedisHost, "rh", "", "address of the backend Redis server (shorthand)")
flag.IntVar(&cfg.RedisPort, "redis-port", 6379, "port of the backend Redis server")
flag.IntVar(&cfg.RedisPort, "rp", 6379, "port of the backend Redis server (shorthand)")
flag.IntVar(&cfg.Workers, "workers", 1, "number of workers to render tiles")
flag.StringVar(&cfg.ColorsFile, "colors", "colors.txt", "colors used to render map tiles.")
flag.StringVar(&cfg.ColorsFile, "c", "colors.txt", "colors used to render map tiles (shorthand).")
flag.StringVar(&cfg.BGColor, "background", defaultBgColor, "background color")
flag.StringVar(&cfg.BGColor, "bg", defaultBgColor, "background color (shorthand)")
flag.BoolVar(&cfg.Transparent, "transparent", false, "Render transparent blocks.")
flag.BoolVar(&cfg.Transparent, "t", false, "Render transparent blocks (shorthand).")
flag.Float64Var(&cfg.TransparentDim,
"transparent-dim", common.DefaultTransparentDim*100.0,
"Extra dimming of transparent nodes each depth meter in percent.")
flag.Float64Var(&cfg.TransparentDim,
"td", common.DefaultTransparentDim*100.0,
"Extra dimming of transparent nodes each depth meter in percent. (shorthand)")
flag.BoolVar(&cfg.Websockets, "websockets", false, "Forward tile changes to clients via websockets.")
flag.BoolVar(&cfg.Websockets, "ws", false, "Forward tile changes to clients via websockets (shorthand).")
flag.StringVar(&cfg.PlayersFIFO, "players", "", "Path to FIFO file to read active players from.")
flag.StringVar(&cfg.PlayersFIFO, "ps", "", "Path to FIFO file to read active players from (shorthand).")
flag.IntVar(&cfg.YMin, "ymin", common.MinHeight, "Minimum y in blocks.")
flag.IntVar(&cfg.YMax, "ymax", common.MaxHeight, "Maximum y in blocks.")
flag.DurationVar(&cfg.ChangeDuration.Duration,
"change-duration", time.Second, "Duration to aggregate changes. (PG only)")
}
func (cfg *config) load(fname string) error {
_, err := toml.DecodeFile(fname, &cfg)
return err
}

View File

@ -5,7 +5,6 @@
package main package main
import ( import (
"bytes"
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
@ -15,10 +14,9 @@ import (
type websocketForwarder struct { type websocketForwarder struct {
upgrader *websocket.Upgrader upgrader *websocket.Upgrader
register chan *connection connections map[*connection]struct{}
unregister chan *connection funcs chan func(*websocketForwarder)
broadcast chan msg init func(*websocketForwarder, *connection)
connections map[*connection]bool
} }
type connection struct { type connection struct {
@ -26,55 +24,70 @@ type connection struct {
send chan []byte send chan []byte
} }
type msg struct { type (
tiles []xz tilesMsg struct {
pls []*player Tiles []xz `json:"tiles"`
} }
plsMsg struct {
Pls []*player `json:"players"`
}
)
func newWebsocketForwarder() *websocketForwarder { func newWebsocketForwarder() *websocketForwarder {
upgrader := &websocket.Upgrader{ReadBufferSize: 512, WriteBufferSize: 2048} upgrader := &websocket.Upgrader{
ReadBufferSize: 512,
WriteBufferSize: 2048,
//CheckOrigin: func(*http.Request) bool { return true },
}
return &websocketForwarder{ return &websocketForwarder{
upgrader: upgrader, upgrader: upgrader,
register: make(chan *connection), connections: make(map[*connection]struct{}),
unregister: make(chan *connection), funcs: make(chan func(*websocketForwarder)),
broadcast: make(chan msg), }
connections: make(map[*connection]bool)}
} }
func (wsf *websocketForwarder) run() { func (wsf *websocketForwarder) run() {
for { for fn := range wsf.funcs {
select { fn(wsf)
case c := <-wsf.register: }
wsf.connections[c] = true }
case c := <-wsf.unregister:
func (wsf *websocketForwarder) register(c *connection) {
wsf.funcs <- func(wsf *websocketForwarder) {
wsf.connections[c] = struct{}{}
}
}
func (wsf *websocketForwarder) unregister(c *connection) {
wsf.funcs <- func(wsf *websocketForwarder) {
if _, ok := wsf.connections[c]; ok { if _, ok := wsf.connections[c]; ok {
delete(wsf.connections, c) delete(wsf.connections, c)
close(c.send) close(c.send)
} }
case message := <-wsf.broadcast: }
}
func (wsf *websocketForwarder) setInit(init func(*websocketForwarder, *connection)) {
wsf.funcs <- func(wsf *websocketForwarder) {
wsf.init = init
}
}
func (wsf *websocketForwarder) send(m interface{}) {
wsf.funcs <- func(wsf *websocketForwarder) {
if len(wsf.connections) == 0 { if len(wsf.connections) == 0 {
continue return
}
encMsg := map[string]interface{}{}
if message.tiles != nil {
encMsg["tiles"] = message.tiles
} }
if message.pls != nil { data, err := json.Marshal(m)
encMsg["players"] = message.pls if err != nil {
log.Printf("encoding failed. %v\n", err)
return
} }
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
if err := encoder.Encode(encMsg); err != nil {
log.Printf("encoding changes failed: %s\n", err)
continue
}
m := buf.Bytes()
for c := range wsf.connections { for c := range wsf.connections {
select { select {
case c.send <- m: case c.send <- data:
default: default:
delete(wsf.connections, c) delete(wsf.connections, c)
close(c.send) close(c.send)
@ -82,6 +95,33 @@ func (wsf *websocketForwarder) run() {
} }
} }
} }
func (wsf *websocketForwarder) singleSend(c *connection, m interface{}) {
wsf.funcs <- func(wsf *websocketForwarder) {
_, ok := wsf.connections[c]
if !ok {
return
}
data, err := json.Marshal(m)
if err != nil {
log.Printf("encoding failed. %v\n", err)
return
}
select {
case c.send <- data:
default:
delete(wsf.connections, c)
close(c.send)
}
}
}
func (wsf *websocketForwarder) BaseTilesUpdated(changes []xz) {
wsf.send(&tilesMsg{Tiles: changes})
}
func (wsf *websocketForwarder) BroadcastPlayers(pls []*player) {
wsf.send(&plsMsg{Pls: pls})
} }
func (wsf *websocketForwarder) ServeHTTP(rw http.ResponseWriter, r *http.Request) { func (wsf *websocketForwarder) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
@ -91,20 +131,15 @@ func (wsf *websocketForwarder) ServeHTTP(rw http.ResponseWriter, r *http.Request
return return
} }
c := &connection{ws: ws, send: make(chan []byte, 8)} c := &connection{ws: ws, send: make(chan []byte, 8)}
wsf.register <- c wsf.register(c)
defer func() { wsf.unregister <- c }() defer wsf.unregister(c)
go c.writer() go c.writer()
if wsf.init != nil {
wsf.init(wsf, c)
}
c.reader() c.reader()
} }
func (wsf *websocketForwarder) BaseTilesUpdated(changes []xz) {
wsf.broadcast <- msg{tiles: changes}
}
func (wsf *websocketForwarder) BroadcastPlayers(pls []*player) {
wsf.broadcast <- msg{pls: pls}
}
func (c *connection) writer() { func (c *connection) writer() {
defer c.ws.Close() defer c.ws.Close()
for msg := range c.send { for msg := range c.send {

View File

@ -10,7 +10,6 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
"strings"
"bitbucket.org/s_l_teichmann/mtsatellite/common" "bitbucket.org/s_l_teichmann/mtsatellite/common"
@ -18,64 +17,16 @@ import (
) )
func main() { func main() {
var ( var (
webPort int cfg config
webHost string cfgFile string
webDir string
mapDir string
redisPort int
redisHost string
colorsFile string
bgColor string
workers int
transparent bool
transparentDim float64
updateHosts string
websockets bool
playersFIFO string
version bool version bool
yMin int
yMax int
) )
defaultBgColor := common.ColorToHex(common.BackgroundColor) flag.StringVar(&cfgFile, "config", "", "configuration file")
flag.IntVar(&webPort, "web-port", 8808, "port of the web server")
flag.IntVar(&webPort, "p", 8808, "port of the web server (shorthand)")
flag.StringVar(&webHost, "web-host", "localhost", "address to bind web server")
flag.StringVar(&webHost, "h", "localhost", "address to bind web server(shorthand)")
flag.StringVar(&webDir, "web", "web", "static served web files.")
flag.StringVar(&webDir, "w", "web", "static served web files (shorthand)")
flag.StringVar(&mapDir, "map", "map", "directory of prerendered tiles")
flag.StringVar(&mapDir, "m", "map", "directory of prerendered tiles (shorthand)")
flag.StringVar(&updateHosts, "update-hosts", "localhost",
"';' separated list of hosts which are allowed to send map update requests")
flag.StringVar(&updateHosts, "u", "localhost",
"';' separated list of hosts which are allowed to send map update requests (shorthand)")
flag.StringVar(&redisHost, "redis-host", "", "address of the backend Redis server")
flag.StringVar(&redisHost, "rh", "", "address of the backend Redis server (shorthand)")
flag.IntVar(&redisPort, "redis-port", 6379, "port of the backend Redis server")
flag.IntVar(&redisPort, "rp", 6379, "port of the backend Redis server (shorthand)")
flag.IntVar(&workers, "workers", 1, "number of workers to render tiles")
flag.StringVar(&colorsFile, "colors", "colors.txt", "colors used to render map tiles.")
flag.StringVar(&colorsFile, "c", "colors.txt", "colors used to render map tiles (shorthand).")
flag.StringVar(&bgColor, "background", defaultBgColor, "background color")
flag.StringVar(&bgColor, "bg", defaultBgColor, "background color (shorthand)")
flag.BoolVar(&transparent, "transparent", false, "Render transparent blocks.")
flag.BoolVar(&transparent, "t", false, "Render transparent blocks (shorthand).")
flag.Float64Var(&transparentDim,
"transparent-dim", common.DefaultTransparentDim*100.0,
"Extra dimming of transparent nodes each depth meter in percent.")
flag.Float64Var(&transparentDim,
"td", common.DefaultTransparentDim*100.0,
"Extra fimming of transparent nodes each depth meter in percent. (shorthand)")
flag.BoolVar(&websockets, "websockets", false, "Forward tile changes to clients via websockets.")
flag.BoolVar(&websockets, "ws", false, "Forward tile changes to clients via websockets (shorthand).")
flag.StringVar(&playersFIFO, "players", "", "Path to FIFO file to read active players from.")
flag.StringVar(&playersFIFO, "ps", "", "Path to FIFO file to read active players from (shorthand).")
flag.IntVar(&yMin, "ymin", common.MinHeight, "Minimum y in blocks.")
flag.IntVar(&yMax, "ymax", common.MaxHeight, "Maximum y in blocks.")
flag.BoolVar(&version, "version", false, "Print version and exit.") flag.BoolVar(&version, "version", false, "Print version and exit.")
cfg.bindFlags()
flag.Parse() flag.Parse()
@ -83,66 +34,80 @@ func main() {
common.PrintVersionAndExit() common.PrintVersionAndExit()
} }
bg := common.ParseColorDefault(bgColor, common.BackgroundColor) if cfgFile != "" {
if err := cfg.load(cfgFile); err != nil {
log.Fatalf("error: %v\n", err)
}
}
bg := common.ParseColorDefault(cfg.BGColor, common.BackgroundColor)
router := mux.NewRouter() router := mux.NewRouter()
subBaseLine := newSubBaseLine(mapDir, bg) subBaseLine := newSubBaseLine(cfg.MapDir, bg)
router.Path("/map/{z:[0-9]+}/{x:[0-9]+}/{y:[0-9]+}.png").Handler(subBaseLine) router.Path("/map/{z:[0-9]+}/{x:[0-9]+}/{y:[0-9]+}.png").Handler(subBaseLine)
var btu baseTilesUpdates var btu baseTilesUpdates
var wsf *websocketForwarder var wsf *websocketForwarder
if websockets { if cfg.Websockets {
wsf = newWebsocketForwarder() wsf = newWebsocketForwarder()
go wsf.run() go wsf.run()
router.Path("/ws").Methods("GET").Handler(wsf) router.Path("/ws").Methods("GET").Handler(wsf)
btu = wsf btu = wsf
} }
if playersFIFO != "" { if cfg.PlayersFIFO != "" {
plys := newPlayers(playersFIFO, wsf) plys := newPlayers(cfg.PlayersFIFO, wsf)
wsf.setInit(plys.initConnection)
go plys.run() go plys.run()
router.Path("/players").Methods("GET").Handler(plys) router.Path("/players").Methods("GET").Handler(plys)
} }
if redisHost != "" { if cfg.RedisHost != "" {
var colors *common.Colors var colors *common.Colors
var err error var err error
if colors, err = common.ParseColors(colorsFile); err != nil { if colors, err = common.ParseColors(cfg.ColorsFile); err != nil {
log.Fatalf("ERROR: problem loading colors: %s", err) log.Fatalf("ERROR: problem loading colors: %s", err)
} }
colors.TransparentDim = common.Clamp32f( colors.TransparentDim = common.Clamp32f(
float32(transparentDim/100.0), 0.0, 100.0) float32(cfg.TransparentDim/100.0), 0.0, 100.0)
var redisAddress string
if strings.ContainsRune(redisHost, '/') { dbcf, err := common.CreateDBClientFactory(cfg.RedisHost, cfg.RedisPort)
redisAddress = redisHost if err != nil {
} else { log.Fatalf("error: %v\n", err)
redisAddress = fmt.Sprintf("%s:%d", redisHost, redisPort)
} }
defer dbcf.Close()
var allowedUpdateIps []net.IP var allowedUpdateIps []net.IP
if allowedUpdateIps, err = ipsFromHosts(updateHosts); err != nil { if allowedUpdateIps, err = ipsFromHosts(cfg.UpdateHosts); err != nil {
log.Fatalf("ERROR: name resolving problem: %s", err) log.Fatalf("ERROR: name resolving problem: %s", err)
} }
tu := newTileUpdater( tu := newTileUpdater(
mapDir, cfg.MapDir,
redisAddress, dbcf,
allowedUpdateIps, allowedUpdateIps,
colors, bg, colors, bg,
yMin, yMax, cfg.YMin, cfg.YMax,
transparent, cfg.Transparent,
workers, cfg.Workers,
btu) btu)
go tu.doUpdates() go tu.doUpdates()
if pgHost, ok := common.IsPostgreSQL(cfg.RedisHost); btu != nil && ok {
go tu.listen(pgHost, cfg.ChangeDuration.Duration)
} else {
router.Path("/update").Methods("POST").Handler(tu) router.Path("/update").Methods("POST").Handler(tu)
} }
}
router.PathPrefix("/").Handler(http.FileServer(http.Dir(webDir))) router.PathPrefix("/").Handler(http.FileServer(http.Dir(cfg.WebDir)))
http.Handle("/", router) http.Handle("/", router)
addr := fmt.Sprintf("%s:%d", webHost, webPort) addr := fmt.Sprintf("%s:%d", cfg.WebHost, cfg.WebPort)
if err := http.ListenAndServe(addr, nil); err != nil { if err := http.ListenAndServe(addr, nil); err != nil {
log.Fatalf("Starting server failed: %s\n", err) log.Fatalf("Starting server failed: %s\n", err)
} }

View File

@ -7,6 +7,8 @@ package main
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"context"
"database/sql"
"encoding/json" "encoding/json"
"html/template" "html/template"
"log" "log"
@ -16,9 +18,14 @@ import (
"sort" "sort"
"sync" "sync"
"time" "time"
"bitbucket.org/s_l_teichmann/mtsatellite/common"
) )
const sleepInterval = time.Second * 5 const (
sleepInterval = time.Second * 5
sleepPG = time.Second
)
var geoJSONTmpl = template.Must(template.New("geojson").Parse( var geoJSONTmpl = template.Must(template.New("geojson").Parse(
`{ "type": "Feature", `{ "type": "Feature",
@ -64,21 +71,39 @@ func (p *player) same(o *player) bool {
math.Abs(p.Z-o.Z) < 0.000001 math.Abs(p.Z-o.Z) < 0.000001
} }
type sortPlayersByName []*player const selectPlayersSQL = `
SELECT posx/10.0, posy/10.0, posz/10.0, name
FROM player
WHERE modification_date > now() - '2m'::interval`
func (pls sortPlayersByName) Len() int { func playersFromPostgreSQL(connS string) ([]*player, error) {
return len(pls) time.Sleep(sleepPG)
db, err := sql.Open("pgx", connS)
if err != nil {
return nil, err
} }
defer db.Close()
func (pls sortPlayersByName) Less(i, j int) bool { rows, err := db.QueryContext(context.Background(), selectPlayersSQL)
return pls[i].Name < pls[j].Name if err != nil {
return nil, err
} }
defer rows.Close()
func (pls sortPlayersByName) Swap(i, j int) { var pls []*player
pls[i], pls[j] = pls[j], pls[i] for rows.Next() {
var p player
if err := rows.Scan(&p.X, &p.Y, &p.Z, &p.Name); err != nil {
return nil, err
}
pls = append(pls, &p)
}
return pls, rows.Err()
} }
func (ps *players) readFromFIFO() ([]*player, error) { func (ps *players) readFromFIFO() ([]*player, error) {
if host, ok := common.IsPostgreSQL(ps.fifo); ok {
return playersFromPostgreSQL(host)
}
file, err := os.Open(ps.fifo) file, err := os.Open(ps.fifo)
if err != nil { if err != nil {
return nil, err return nil, err
@ -108,18 +133,22 @@ func samePlayers(a, b []*player) bool {
func (ps *players) run() { func (ps *players) run() {
for { for {
empty := len(ps.current()) == 0
pls, err := ps.readFromFIFO() pls, err := ps.readFromFIFO()
if err != nil { if err != nil {
//log.Printf("err: %s\n", err) //log.Printf("err: %s\n", err)
time.Sleep(sleepInterval) time.Sleep(sleepInterval)
continue continue
} }
if pls == nil { if empty && pls == nil {
//log.Println("no players") //log.Println("no players")
continue continue
} }
//log.Printf("%+q\n", pls) //log.Printf("%+q\n", pls)
sort.Sort(sortPlayersByName(pls)) sort.Slice(pls, func(i, j int) bool {
return pls[i].Name < pls[j].Name
})
var change bool var change bool
ps.mu.Lock() ps.mu.Lock()
//log.Printf("%+q\n", pls) //log.Printf("%+q\n", pls)
@ -129,18 +158,28 @@ func (ps *players) run() {
} }
ps.mu.Unlock() ps.mu.Unlock()
if change && ps.wsf != nil { if change && ps.wsf != nil {
if pls == nil {
pls = []*player{}
}
// TODO: Throttle this! // TODO: Throttle this!
ps.wsf.BroadcastPlayers(pls) ps.wsf.BroadcastPlayers(pls)
} }
} }
} }
func (ps *players) current() []*player {
ps.mu.RLock()
defer ps.mu.RUnlock()
return ps.pls
}
func (ps *players) initConnection(wsf *websocketForwarder, c *connection) {
wsf.singleSend(c, &plsMsg{Pls: ps.current()})
}
func (ps *players) ServeHTTP(rw http.ResponseWriter, r *http.Request) { func (ps *players) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json") rw.Header().Set("Content-Type", "application/json")
var pls []*player pls := ps.current()
ps.mu.RLock()
pls = ps.pls
ps.mu.RUnlock()
encoder := json.NewEncoder(rw) encoder := json.NewEncoder(rw)
if err := encoder.Encode(pls); err != nil { if err := encoder.Encode(pls); err != nil {
log.Printf("error: sending JSON failed: %s\n", err) log.Printf("error: sending JSON failed: %s\n", err)

View File

@ -5,6 +5,7 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"image" "image"
"image/color" "image/color"
@ -16,8 +17,10 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"github.com/bamiaux/rez" "github.com/bamiaux/rez"
"github.com/jackc/pgx/v4"
"bytes" "bytes"
@ -35,7 +38,7 @@ type tileUpdater struct {
changes map[xz]struct{} changes map[xz]struct{}
btu baseTilesUpdates btu baseTilesUpdates
mapDir string mapDir string
redisAddress string dbcf common.DBClientFactory
ips []net.IP ips []net.IP
colors *common.Colors colors *common.Colors
bg color.RGBA bg color.RGBA
@ -78,7 +81,8 @@ func (c xz) parent() xzm {
} }
func newTileUpdater( func newTileUpdater(
mapDir, redisAddress string, mapDir string,
dbcf common.DBClientFactory,
ips []net.IP, ips []net.IP,
colors *common.Colors, colors *common.Colors,
bg color.RGBA, bg color.RGBA,
@ -90,7 +94,7 @@ func newTileUpdater(
tu := tileUpdater{ tu := tileUpdater{
btu: btu, btu: btu,
mapDir: mapDir, mapDir: mapDir,
redisAddress: redisAddress, dbcf: dbcf,
ips: ips, ips: ips,
changes: map[xz]struct{}{}, changes: map[xz]struct{}{},
colors: colors, colors: colors,
@ -130,6 +134,60 @@ func (tu *tileUpdater) checkIP(r *http.Request) bool {
return false return false
} }
func (tu *tileUpdater) listen(host string, changeDuration time.Duration) {
xzCh := make(chan xz)
go func() {
ctx := context.Background()
conn, err := pgx.Connect(ctx, host)
if err != nil {
log.Printf("error: %v\n", err)
return
}
defer conn.Close(ctx)
if _, err := conn.Exec(ctx, "listen blockchanges"); err != nil {
log.Printf("error: %v\n", err)
return
}
for {
n, err := conn.WaitForNotification(ctx)
if err != nil {
log.Printf("error: %v\n", err)
continue
}
if n.Payload == "" {
continue
}
var c xz
dec := json.NewDecoder(strings.NewReader(n.Payload))
if err := dec.Decode(&c); err != nil {
log.Printf("error: %v\n", err)
continue
}
xzCh <- c
}
}()
ticker := time.NewTicker(changeDuration)
defer ticker.Stop()
var changes []xz
for {
select {
case c := <-xzCh:
changes = append(changes, c)
case <-ticker.C:
tu.sendChanges(changes)
changes = changes[:0]
}
}
}
func (tu *tileUpdater) ServeHTTP(rw http.ResponseWriter, r *http.Request) { func (tu *tileUpdater) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
if !tu.checkIP(r) { if !tu.checkIP(r) {
log.Printf("WARN: Unauthorized update request from '%s'\n", r.RemoteAddr) log.Printf("WARN: Unauthorized update request from '%s'\n", r.RemoteAddr)
@ -146,18 +204,23 @@ func (tu *tileUpdater) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
return return
} }
if len(newChanges) > 0 { tu.sendChanges(newChanges)
rw.WriteHeader(http.StatusOK)
}
func (tu *tileUpdater) sendChanges(changes []xz) {
if len(changes) == 0 {
return
}
tu.cond.L.Lock() tu.cond.L.Lock()
for _, c := range newChanges { for _, c := range changes {
tu.changes[c.quantize()] = struct{}{} tu.changes[c.quantize()] = struct{}{}
} }
tu.cond.L.Unlock() tu.cond.L.Unlock()
tu.cond.Signal() tu.cond.Signal()
} }
rw.WriteHeader(http.StatusOK)
}
func extractChanges(changes map[xz]struct{}) []xzc { func extractChanges(changes map[xz]struct{}) []xzc {
chs := make([]xzc, len(changes)) chs := make([]xzc, len(changes))
var i int var i int
@ -196,20 +259,14 @@ func (tu *tileUpdater) doUpdates() {
jobs := make(chan *xzc) jobs := make(chan *xzc)
var done sync.WaitGroup var done sync.WaitGroup
var proto string
if strings.ContainsRune(tu.redisAddress, '/') {
proto = "unix"
} else {
proto = "tcp"
}
for i, n := 0, common.Min(tu.workers, len(changes)); i < n; i++ { for i, n := 0, common.Min(tu.workers, len(changes)); i < n; i++ {
var client *common.RedisClient var client common.DBClient
var err error var err error
if client, err = common.NewRedisClient(proto, tu.redisAddress); err != nil { if client, err = tu.dbcf.Create(); err != nil {
log.Printf("WARN: Cannot connect to redis server: %s\n", err) log.Printf("WARN: Cannot connect to redis server: %s\n", err)
continue continue
} }
btc := common.NewBaseTileCreator( btc := common.NewBaseTileCreator(
client, tu.colors, tu.bg, client, tu.colors, tu.bg,
tu.yMin, tu.yMax, tu.yMin, tu.yMax,
@ -343,26 +400,30 @@ func (tu *tileUpdater) updateBaseTiles(
update common.BaseTileUpdateFunc) { update common.BaseTileUpdateFunc) {
type jobWriter struct { type jobWriter struct {
job *xzc canceled *bool
wFn func() (bool, error) wFn func() (bool, error)
} }
jWs := make(chan jobWriter) jWs := make(chan jobWriter)
asyncWrite := make(chan struct{})
go func() { go func() {
defer close(asyncWrite)
for jw := range jWs { for jw := range jWs {
updated, err := jw.wFn() updated, err := jw.wFn()
if err != nil { if err != nil {
*jw.canceled = true
log.Printf("WARN: writing tile failed: %v.\n", err) log.Printf("WARN: writing tile failed: %v.\n", err)
} }
if !updated { if !updated {
jw.job.canceled = true *jw.canceled = true
} }
} }
}() }()
defer func() { defer func() {
close(jWs)
btc.Close() btc.Close()
done.Done() done.Done()
}() }()
@ -374,6 +435,13 @@ func (tu *tileUpdater) updateBaseTiles(
job.canceled = true job.canceled = true
continue continue
} }
jWs <- jobWriter{job, btc.WriteFunc(int(job.X), int(job.Z), update)} jWs <- jobWriter{
&job.canceled,
btc.WriteFunc(int(job.X), int(job.Z), update),
} }
} }
close(jWs)
// Wait until all tiles are written.
<-asyncWrite
}

View File

@ -52,7 +52,7 @@ var BackgroundColor = color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
type BaseTileUpdateFunc func(x, y int, hash []byte) bool type BaseTileUpdateFunc func(x, y int, hash []byte) bool
type BaseTileCreator struct { type BaseTileCreator struct {
client *RedisClient client DBClient
colors *Colors colors *Colors
renderer *Renderer renderer *Renderer
yOrder *YOrder yOrder *YOrder
@ -64,7 +64,7 @@ type BaseTileCreator struct {
} }
func NewBaseTileCreator( func NewBaseTileCreator(
client *RedisClient, client DBClient,
colors *Colors, colors *Colors,
bg color.RGBA, bg color.RGBA,
yMin, yMax int16, yMin, yMax int16,
@ -200,7 +200,7 @@ func (btc *BaseTileCreator) WriteFunc(i, j int, update BaseTileUpdateFunc) func(
return true, SaveAsPNGAtomic(path, image) return true, SaveAsPNGAtomic(path, image)
} }
log.Printf("(%d, %d) is unchanged.\n", x, z) //log.Printf("(%d, %d) is unchanged.\n", x, z)
return false, nil return false, nil
} }
} }

35
common/clientfactory.go Normal file
View File

@ -0,0 +1,35 @@
// Copyright 2022 by Sascha L. Teichmann
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
package common
import (
"strings"
)
type DBClient interface {
QueryCuboid(cuboid Cuboid, fn func(*Block) *Block) (count int, err error)
Close() error
}
type DBClientFactory interface {
Create() (DBClient, error)
Close() error
}
func IsPostgreSQL(host string) (string, bool) {
if strings.HasPrefix(host, "postgres:") {
return host[len("postgres:"):], true
}
return "", false
}
func CreateDBClientFactory(host string, port int) (DBClientFactory, error) {
if connS, ok := IsPostgreSQL(host); ok {
return NewPGClientFactory(connS)
}
return NewRedisClientFactory(host, port)
}

117
common/pgclient.go Normal file
View File

@ -0,0 +1,117 @@
// Copyright 2022 by Sascha L. Teichmann
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
package common
import (
"context"
"database/sql"
_ "github.com/jackc/pgx/v4/stdlib"
)
const queryCuboidSQL = `
SELECT posx, posy, posz, data FROM blocks
WHERE
posx BETWEEN $1 AND $2 AND
posy BETWEEN $3 AND $4 AND
posz BETWEEN $5 AND $6`
type PGClient struct {
conn *sql.Conn
queryCuboidStmt *sql.Stmt
}
type PGClientFactory struct {
db *sql.DB
}
func NewPGClientFactory(connS string) (*PGClientFactory, error) {
db, err := sql.Open("pgx", connS)
if err != nil {
return nil, err
}
return &PGClientFactory{db: db}, nil
}
func (pgcf *PGClientFactory) Close() error {
return pgcf.db.Close()
}
func (pgcf *PGClientFactory) Create() (DBClient, error) {
ctx := context.Background()
conn, err := pgcf.db.Conn(ctx)
if err != nil {
return nil, err
}
stmt, err := conn.PrepareContext(ctx, queryCuboidSQL)
if err != nil {
conn.Close()
return nil, err
}
return &PGClient{
conn: conn,
queryCuboidStmt: stmt,
}, nil
}
func (pgc *PGClient) QueryCuboid(
cuboid Cuboid,
fn func(*Block) *Block,
) (int, error) {
rows, err := pgc.queryCuboidStmt.QueryContext(
context.Background(),
cuboid.P1.X, cuboid.P2.X,
cuboid.P1.Y, cuboid.P2.Y,
cuboid.P1.Z, cuboid.P2.Z)
if err != nil {
return 0, err
}
defer rows.Close()
var (
posX, posY, posZ int
data []byte
count int
block *Block
)
for ; rows.Next(); count++ {
if err := rows.Scan(
&posX, &posY, &posZ,
&data,
); err != nil {
return count, err
}
c := Coord{
X: int16(posX),
Y: int16(posY),
Z: int16(posZ),
}
if block == nil {
block = &Block{Coord: c, Data: data}
} else {
*block = Block{Coord: c, Data: data}
}
if block = fn(block); block != nil {
data = block.Data[:0]
} else {
data = nil
}
}
return count, rows.Err()
}
func (pgc *PGClient) Close() error {
if pgc.queryCuboidStmt != nil {
pgc.queryCuboidStmt.Close()
}
return pgc.conn.Close()
}

View File

@ -11,9 +11,43 @@ import (
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
"strings"
"unicode" "unicode"
) )
type RedisClientFactory struct {
proto string
address string
}
func NewRedisClientFactory(host string, port int) (*RedisClientFactory, error) {
var address string
if strings.ContainsRune(host, '/') {
address = host
} else {
address = fmt.Sprintf("%s:%d", host, port)
}
var proto string
if strings.ContainsRune(address, '/') {
proto = "unix"
} else {
proto = "tcp"
}
return &RedisClientFactory{
proto: proto,
address: address,
}, nil
}
func (rcf *RedisClientFactory) Close() error {
return nil
}
func (rcf *RedisClientFactory) Create() (DBClient, error) {
return NewRedisClient(rcf.proto, rcf.address)
}
type RedisClient struct { type RedisClient struct {
conn net.Conn conn net.Conn
reader *bufio.Reader reader *bufio.Reader

12
go.mod
View File

@ -3,10 +3,14 @@ module bitbucket.org/s_l_teichmann/mtsatellite
go 1.13 go 1.13
require ( require (
github.com/BurntSushi/toml v1.0.0 // indirect
github.com/bamiaux/rez v0.0.0-20170731184118-29f4463c688b github.com/bamiaux/rez v0.0.0-20170731184118-29f4463c688b
github.com/gorilla/mux v1.7.3 github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.4.1 github.com/gorilla/websocket v1.5.0
github.com/jackc/pgx/v4 v4.15.0
github.com/jmhodges/levigo v1.0.0 github.com/jmhodges/levigo v1.0.0
github.com/mattn/go-sqlite3 v1.12.0 github.com/mattn/go-sqlite3 v1.14.11
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 golang.org/x/crypto v0.0.0-20220214200702-86341886e292
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 // indirect
golang.org/x/text v0.3.7 // indirect
) )

199
go.sum
View File

@ -1,18 +1,199 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/bamiaux/rez v0.0.0-20170731184118-29f4463c688b h1:5Ci5wpOL75rYF6RQGRoqhEAU6xLJ6n/D4SckXX1yB74= github.com/bamiaux/rez v0.0.0-20170731184118-29f4463c688b h1:5Ci5wpOL75rYF6RQGRoqhEAU6xLJ6n/D4SckXX1yB74=
github.com/bamiaux/rez v0.0.0-20170731184118-29f4463c688b/go.mod h1:obBQGGIFbbv9KWg92Qu9UHeD94JXmHD1jovY/z6I3O8= github.com/bamiaux/rez v0.0.0-20170731184118-29f4463c688b/go.mod h1:obBQGGIFbbv9KWg92Qu9UHeD94JXmHD1jovY/z6I3O8=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.11.0 h1:HiHArx4yFbwl91X3qqIHtUFoiIfLNJXCQRsnzkiwwaQ=
github.com/jackc/pgconn v1.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns=
github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38=
github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w=
github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U=
github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ=
github.com/mattn/go-sqlite3 v1.12.0 h1:u/x3mp++qUxvYfulZ4HKOvVO0JWhk7HtE8lWhbGz/Do= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.11 h1:gt+cp9c0XGqe9S/wAHTL3n/7MqY+siPWgWJgqdsFrzQ=
github.com/mattn/go-sqlite3 v1.14.11/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 h1:BXxu8t6QN0G1uff4bzZzSkpsax8+ALqTGUtz08QrV00=
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=

23
pg/send_block_changes.sql Normal file
View File

@ -0,0 +1,23 @@
BEGIN;
CREATE OR REPLACE FUNCTION send_block_changes()
RETURNS TRIGGER AS
$$
BEGIN
PERFORM pg_notify('blockchanges',
json_build_object(
'X', NEW.posx,
'Z', NEW.posz
)::text
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER blocks_changed
AFTER INSERT OR UPDATE
ON blocks
FOR EACH ROW
EXECUTE PROCEDURE send_block_changes();
END;