update fork to be up-to-date with matrixmc
This commit is contained in:
parent
e5c710c3b6
commit
c00b33eb1e
4 changed files with 503 additions and 386 deletions
18
go.mod
18
go.mod
|
|
@ -7,4 +7,20 @@ require (
|
||||||
github.com/Kelwing/mc-rcon v0.0.0-20221109043427-a481cb41818e
|
github.com/Kelwing/mc-rcon v0.0.0-20221109043427-a481cb41818e
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/net v0.59.0 // indirect
|
require (
|
||||||
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/rs/zerolog v1.35.1 // indirect
|
||||||
|
github.com/tidwall/gjson v1.20.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
|
go.mau.fi/util v0.11.0 // indirect
|
||||||
|
golang.org/x/crypto v0.58.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e // indirect
|
||||||
|
golang.org/x/net v0.61.0 // indirect
|
||||||
|
golang.org/x/sys v0.49.0 // indirect
|
||||||
|
golang.org/x/text v0.43.0 // indirect
|
||||||
|
maunium.net/go/mautrix v0.32.0 // indirect
|
||||||
|
)
|
||||||
|
|
|
||||||
33
go.sum
33
go.sum
|
|
@ -1,6 +1,39 @@
|
||||||
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
git.anarchists.space/meer/pixitrix v0.0.0-20261001184040-0baca98fc381 h1:S+2YsfopfcX4j4sw5YbGezYmUGyxDSeiemIDwZKbCsc=
|
git.anarchists.space/meer/pixitrix v0.0.0-20261001184040-0baca98fc381 h1:S+2YsfopfcX4j4sw5YbGezYmUGyxDSeiemIDwZKbCsc=
|
||||||
git.anarchists.space/meer/pixitrix v0.0.0-20261001184040-0baca98fc381/go.mod h1:bifrDiGNzaP6BJnlo8ZnYIPRw6zY36v0FKgaai0BGrE=
|
git.anarchists.space/meer/pixitrix v0.0.0-20261001184040-0baca98fc381/go.mod h1:bifrDiGNzaP6BJnlo8ZnYIPRw6zY36v0FKgaai0BGrE=
|
||||||
github.com/Kelwing/mc-rcon v0.0.0-20221109043427-a481cb41818e h1:btzl16nXWBeHhlqnAttNBLxs+hSkyRN1xTbRy4kUbSg=
|
github.com/Kelwing/mc-rcon v0.0.0-20221109043427-a481cb41818e h1:btzl16nXWBeHhlqnAttNBLxs+hSkyRN1xTbRy4kUbSg=
|
||||||
github.com/Kelwing/mc-rcon v0.0.0-20221109043427-a481cb41818e/go.mod h1:wlXY/EO9CBjMViEh95JOQhp//RMnHs4ZUBhYhx43LdM=
|
github.com/Kelwing/mc-rcon v0.0.0-20221109043427-a481cb41818e/go.mod h1:wlXY/EO9CBjMViEh95JOQhp//RMnHs4ZUBhYhx43LdM=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||||
|
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto=
|
||||||
|
github.com/tidwall/gjson v1.20.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
||||||
|
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/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/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.11.0 h1:JNEpkVRxfTdJUC0DIGKiRlLKzl4G/qLX/eVzoP7QuwY=
|
||||||
|
go.mau.fi/util v0.11.0/go.mod h1:04AEFVsc4zNdIkDC1gkdqbg/nZFolHY50pyXh2DSy7s=
|
||||||
|
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
||||||
|
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
||||||
|
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e h1:HgI6W1OolKK6tcV74is+rl9SMT0njsHlp0TbQiGXI/A=
|
||||||
|
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e/go.mod h1:DBoxwWhjaW8DJJw/YeM4zpeOKMC1FMXSblsYy+1mFSw=
|
||||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||||
|
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
|
||||||
|
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||||
|
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
|
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||||
|
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||||
|
maunium.net/go/mautrix v0.32.0 h1:xuAZzzyWce0pFkBhV/MCLFiJdVs2rFkY3Eep7hNng4I=
|
||||||
|
maunium.net/go/mautrix v0.32.0/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU=
|
||||||
|
|
|
||||||
503
main.go
503
main.go
|
|
@ -1,21 +1,21 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
matrix "git.anarchists.space/meer/pixitrix"
|
|
||||||
mcrcon "github.com/Kelwing/mc-rcon"
|
mcrcon "github.com/Kelwing/mc-rcon"
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EventType int
|
type EventType int
|
||||||
|
|
@ -44,11 +44,13 @@ type Minecraft struct {
|
||||||
}
|
}
|
||||||
type Bridge struct {
|
type Bridge struct {
|
||||||
MC *Minecraft
|
MC *Minecraft
|
||||||
Matrix *matrix.Client
|
Matrix *mautrix.Client
|
||||||
TrustedUsers []string
|
ctx context.Context
|
||||||
RoomID string
|
TrustedUsers []id.UserID
|
||||||
|
RoomID id.RoomID
|
||||||
Events <-chan *MCEvent
|
Events <-chan *MCEvent
|
||||||
Players *PlayerSet
|
Players *PlayerSet
|
||||||
|
Usepmp bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|
@ -63,11 +65,12 @@ type MinecraftConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type MatrixConfig struct {
|
type MatrixConfig struct {
|
||||||
Homeserver string `json:"homeserver"`
|
Homeserver string `json:"homeserver"`
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
Room string `json:"room"`
|
Room id.RoomID `json:"room"`
|
||||||
TrustedUsers []string `json:"trusted_users,omitempty"`
|
TrustedUsers []id.UserID `json:"trusted_users,omitempty"`
|
||||||
Proxy string `json:"proxy,omitempty"`
|
Usepmp bool `json:"use_pmp"`
|
||||||
|
Proxy string `json:"proxy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -85,7 +88,7 @@ var (
|
||||||
|
|
||||||
advancementRE = regexp.MustCompile(
|
advancementRE = regexp.MustCompile(
|
||||||
`^\[[0-9:]+\] \[[^]]+/INFO\]: (?:System chat: )?([A-Za-z0-9_]+) has (?:made the advancement|reached the goal|completed the challenge) \[(.+)\]$`,
|
`^\[[0-9:]+\] \[[^]]+/INFO\]: (?:System chat: )?([A-Za-z0-9_]+) has (?:made the advancement|reached the goal|completed the challenge) \[(.+)\]$`,
|
||||||
)
|
)
|
||||||
deathRE = regexp.MustCompile(
|
deathRE = regexp.MustCompile(
|
||||||
`^\[[0-9:]+\] \[[^]]+/INFO\]: (.+)$`,
|
`^\[[0-9:]+\] \[[^]]+/INFO\]: (.+)$`,
|
||||||
)
|
)
|
||||||
|
|
@ -137,15 +140,12 @@ func main() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mx, err := matrix.New(
|
cli, err := mautrix.NewClient(cfg.Matrix.Homeserver, "", cfg.Matrix.AccessToken)
|
||||||
cfg.Matrix.Homeserver,
|
|
||||||
cfg.Matrix.AccessToken,
|
|
||||||
cfg.Matrix.Proxy,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
roomID, err := mx.JoinRoom(cfg.Matrix.Room)
|
ctx := context.Background()
|
||||||
|
joinresp, err := cli.JoinRoom(ctx, cfg.Matrix.Room.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -153,12 +153,14 @@ func main() {
|
||||||
events := make(chan *MCEvent, 100)
|
events := make(chan *MCEvent, 100)
|
||||||
|
|
||||||
bridge := &Bridge{
|
bridge := &Bridge{
|
||||||
|
ctx: ctx,
|
||||||
MC: mc,
|
MC: mc,
|
||||||
Matrix: mx,
|
Matrix: cli,
|
||||||
RoomID: roomID,
|
RoomID: joinresp.RoomID,
|
||||||
Events: events,
|
Events: events,
|
||||||
TrustedUsers: cfg.Matrix.TrustedUsers,
|
TrustedUsers: cfg.Matrix.TrustedUsers,
|
||||||
Players: NewPlayerSet(),
|
Players: NewPlayerSet(),
|
||||||
|
Usepmp: cfg.Matrix.Usepmp,
|
||||||
}
|
}
|
||||||
if resp, err := mc.SendCommand("list"); err == nil {
|
if resp, err := mc.SendCommand("list"); err == nil {
|
||||||
bridge.Players.Seed(resp)
|
bridge.Players.Seed(resp)
|
||||||
|
|
@ -170,275 +172,10 @@ func main() {
|
||||||
|
|
||||||
go bridge.RunMCEvents()
|
go bridge.RunMCEvents()
|
||||||
|
|
||||||
go RunMatrixBridge(
|
RunMatrixBridge(ctx,
|
||||||
mx,
|
cli,
|
||||||
bridge,
|
bridge,
|
||||||
)
|
)
|
||||||
|
|
||||||
select {}
|
|
||||||
}
|
|
||||||
func dialRCON(addr, password string) (*mcrcon.MCConn, error) {
|
|
||||||
conn := new(mcrcon.MCConn)
|
|
||||||
|
|
||||||
if err := conn.Open(addr, password); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := conn.Authenticate(); err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMinecraft(addr, password string) (*Minecraft, error) {
|
|
||||||
conn, err := dialRCON(addr, password)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Minecraft{RCON: conn, addr: addr, password: password}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxNickLen = 32
|
|
||||||
maxMsgLen = 256
|
|
||||||
)
|
|
||||||
|
|
||||||
func sanitize(s string, max int) string {
|
|
||||||
s = strings.ToValidUTF8(s, "")
|
|
||||||
var b strings.Builder
|
|
||||||
n := 0
|
|
||||||
for _, r := range s {
|
|
||||||
if r == '\n' || r == '\r' || r == '\t' {
|
|
||||||
r = ' '
|
|
||||||
}
|
|
||||||
if r == '\u00a7' || unicode.IsControl(r) || unicode.Is(unicode.Cf, r) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if n >= max {
|
|
||||||
b.WriteString("…")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
b.WriteRune(r)
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *Minecraft) SendMessage(nick, msg string) error {
|
|
||||||
components := []map[string]any{
|
|
||||||
{"text": "", "color": "dark_gray"},
|
|
||||||
{"text": sanitize(nick, maxNickLen), "color": "aqua", "bold": true},
|
|
||||||
{"text": ": ", "color": "gray"},
|
|
||||||
{"text": sanitize(msg, maxMsgLen), "color": "white"},
|
|
||||||
}
|
|
||||||
j, err := json.Marshal(components)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = mc.SendCommand("tellraw @a " + string(j))
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var playerNameRE = regexp.MustCompile(`^[A-Za-z0-9_]{1,16}$`)
|
|
||||||
|
|
||||||
var onlineRE = regexp.MustCompile(`players online: ?(.*)$`)
|
|
||||||
|
|
||||||
type PlayerSet struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
m map[string]struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPlayerSet() *PlayerSet { return &PlayerSet{m: map[string]struct{}{}} }
|
|
||||||
|
|
||||||
func (p *PlayerSet) Add(n string) { p.mu.Lock(); p.m[n] = struct{}{}; p.mu.Unlock() }
|
|
||||||
func (p *PlayerSet) Remove(n string) { p.mu.Lock(); delete(p.m, n); p.mu.Unlock() }
|
|
||||||
func (p *PlayerSet) Has(n string) bool {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
_, ok := p.m[n]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PlayerSet) Seed(listOutput string) {
|
|
||||||
m := onlineRE.FindStringSubmatch(listOutput)
|
|
||||||
if m == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, n := range strings.Split(m[1], ",") {
|
|
||||||
if n = strings.TrimSpace(n); playerNameRE.MatchString(n) {
|
|
||||||
p.Add(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseLogLine(line string) (*MCEvent, bool) {
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
if m := chatRE.FindStringSubmatch(line); m != nil {
|
|
||||||
return &MCEvent{
|
|
||||||
Type: ChatEvent,
|
|
||||||
Player: m[1],
|
|
||||||
Message: m[2],
|
|
||||||
RawLine: line,
|
|
||||||
Time: now,
|
|
||||||
}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if m := joinRE.FindStringSubmatch(line); m != nil {
|
|
||||||
return &MCEvent{
|
|
||||||
Type: JoinEvent,
|
|
||||||
Player: m[1],
|
|
||||||
RawLine: line,
|
|
||||||
Time: now,
|
|
||||||
}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if m := leaveRE.FindStringSubmatch(line); m != nil {
|
|
||||||
return &MCEvent{
|
|
||||||
Type: LeaveEvent,
|
|
||||||
Player: m[1],
|
|
||||||
RawLine: line,
|
|
||||||
Time: now,
|
|
||||||
}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if m := advancementRE.FindStringSubmatch(line); m != nil {
|
|
||||||
return &MCEvent{
|
|
||||||
Type: AdvancementEvent,
|
|
||||||
Player: m[1],
|
|
||||||
Message: m[2],
|
|
||||||
RawLine: line,
|
|
||||||
Time: now,
|
|
||||||
}, true
|
|
||||||
}
|
|
||||||
if m := pauseRE.FindStringSubmatch(line); m != nil {
|
|
||||||
return &MCEvent{
|
|
||||||
Type: PauseEvent,
|
|
||||||
Message: m[1],
|
|
||||||
RawLine: line,
|
|
||||||
Time: now,
|
|
||||||
}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if m := deathRE.FindStringSubmatch(line); m != nil {
|
|
||||||
msg := m[1]
|
|
||||||
|
|
||||||
deaths := []string{
|
|
||||||
" was slain by ",
|
|
||||||
" was shot by ",
|
|
||||||
" was blown up",
|
|
||||||
" was doomed to ",
|
|
||||||
" drowned",
|
|
||||||
" died",
|
|
||||||
" blew up",
|
|
||||||
" burned to death",
|
|
||||||
" fell from",
|
|
||||||
" hit the ground too hard",
|
|
||||||
" was killed by ",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, d := range deaths {
|
|
||||||
if strings.Contains(msg, d) {
|
|
||||||
|
|
||||||
player := strings.Fields(msg)[0]
|
|
||||||
if !playerNameRE.MatchString(player) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return &MCEvent{
|
|
||||||
Type: DeathEvent,
|
|
||||||
Player: player,
|
|
||||||
Message: msg,
|
|
||||||
RawLine: line,
|
|
||||||
Time: now,
|
|
||||||
}, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxLogLine = 64 << 10
|
|
||||||
|
|
||||||
func WatchLog(path string, events chan<- *MCEvent) error {
|
|
||||||
f, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() { f.Close() }()
|
|
||||||
|
|
||||||
info, err := f.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = f.Seek(0, io.SeekEnd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
reader = bufio.NewReaderSize(f, 32<<10)
|
|
||||||
pending []byte
|
|
||||||
overflow bool
|
|
||||||
offset = info.Size()
|
|
||||||
)
|
|
||||||
|
|
||||||
for {
|
|
||||||
chunk, err := reader.ReadSlice('\n')
|
|
||||||
offset += int64(len(chunk))
|
|
||||||
|
|
||||||
if !overflow {
|
|
||||||
pending = append(pending, chunk...)
|
|
||||||
if len(pending) > maxLogLine {
|
|
||||||
pending, overflow = nil, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == bufio.ErrBufferFull {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
if !overflow {
|
|
||||||
line := strings.TrimSpace(string(pending))
|
|
||||||
if event, ok := ParseLogLine(line); ok {
|
|
||||||
events <- event
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pending, overflow = pending[:0], false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != io.EOF {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if cur, statErr := os.Stat(path); statErr == nil {
|
|
||||||
if !os.SameFile(info, cur) || cur.Size() < offset {
|
|
||||||
log.Printf("%s rotated or truncated, reopening", path)
|
|
||||||
|
|
||||||
nf, openErr := os.Open(path)
|
|
||||||
if openErr != nil {
|
|
||||||
return openErr
|
|
||||||
}
|
|
||||||
f.Close()
|
|
||||||
f = nf
|
|
||||||
info, err = f.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
reader.Reset(f)
|
|
||||||
pending, overflow, offset = nil, false, 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(250 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
func (mc *Minecraft) SendCommand(command string) (string, error) {
|
func (mc *Minecraft) SendCommand(command string) (string, error) {
|
||||||
if strings.ContainsAny(command, "\r\n\x00") {
|
if strings.ContainsAny(command, "\r\n\x00") {
|
||||||
|
|
@ -470,44 +207,41 @@ const (
|
||||||
maxReconnectDelay = 2 * time.Minute
|
maxReconnectDelay = 2 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
func RunMatrixBridge(
|
type failFastSyncer struct {
|
||||||
client *matrix.Client,
|
*mautrix.DefaultSyncer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *failFastSyncer) OnFailedSync(res *mautrix.RespSync, err error) (time.Duration, error) {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
func RunMatrixBridge(ctx context.Context,
|
||||||
|
client *mautrix.Client,
|
||||||
bridge *Bridge,
|
bridge *Bridge,
|
||||||
) {
|
) {
|
||||||
me, err := client.WhoAmI()
|
me, err := client.Whoami(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("whoami failed: %v", err)
|
log.Printf("whoami failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
client.UserID = me.UserID
|
||||||
log.Printf("logged in as %s", me.UserID)
|
log.Printf("logged in as %s", me.UserID)
|
||||||
|
syncer := &failFastSyncer{mautrix.NewDefaultSyncer()}
|
||||||
for msg := range client.Messages() {
|
client.Syncer = syncer
|
||||||
|
client.SyncPresence = event.PresenceOffline
|
||||||
if msg.Sender == me.UserID {
|
//syncer.FilterJSON = createSyncFilter()
|
||||||
continue
|
client.Syncer.(mautrix.ExtensibleSyncer).OnSync(client.DontProcessOldEvents)
|
||||||
|
syncer.OnEventType(event.EventMessage, func(_ context.Context, ev *event.Event) {
|
||||||
|
if ev.RoomID != bridge.RoomID || ev.Sender == client.UserID {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
msg := ev.Content.AsMessage()
|
||||||
if msg.RoomID != bridge.RoomID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg.MsgType != "m.text" || msg.Body == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
//log.Printf("[matrix] %s: (%d bytes)", msg.Sender, len(msg.Body))
|
|
||||||
|
|
||||||
if strings.HasPrefix(msg.Body, "!") {
|
if strings.HasPrefix(msg.Body, "!") {
|
||||||
handleMatrixCommand(
|
handleMatrixCommand(bridge, ev.RoomID, ev.Sender, *msg)
|
||||||
bridge,
|
return
|
||||||
msg,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
name := msg.Sender
|
name := ev.Sender.String()
|
||||||
profile, err := bridge.Matrix.GetProfile(msg.Sender)
|
profile, err := bridge.Matrix.GetDisplayName(bridge.ctx, ev.Sender)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("profile lookup failed: %v", err)
|
log.Printf("profile lookup failed: %v", err)
|
||||||
} else if profile.DisplayName != "" {
|
} else if profile.DisplayName != "" {
|
||||||
|
|
@ -517,19 +251,54 @@ func RunMatrixBridge(
|
||||||
if err := bridge.MC.SendMessage(name, msg.Body); err != nil {
|
if err := bridge.MC.SendMessage(name, msg.Body); err != nil {
|
||||||
log.Printf("minecraft send failed: %v", err)
|
log.Printf("minecraft send failed: %v", err)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
backoff := initialReconnectDelay
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Printf("Matrix bridge context cancelled. Stopping sync.")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
if err := client.SyncWithContext(ctx); err != nil {
|
||||||
|
log.Printf("Matrix sync loop halted with error: %v", err)
|
||||||
|
|
||||||
|
log.Printf("Reconnecting to homeserver in %v...", backoff)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(backoff):
|
||||||
|
}
|
||||||
|
backoff *= 2
|
||||||
|
if backoff > maxReconnectDelay {
|
||||||
|
backoff = maxReconnectDelay
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
backoff = initialReconnectDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (b *Bridge) SendMatrix(
|
func (b *Bridge) SendMatrix(sender,
|
||||||
format string,
|
format string,
|
||||||
args ...any,
|
args ...any,
|
||||||
) {
|
) {
|
||||||
text := fmt.Sprintf(format, args...)
|
text := fmt.Sprintf(format, args...)
|
||||||
|
// b.SendMessageWithpmp(b.RoomID, sender, text)
|
||||||
_, err := b.Matrix.SendMessage(
|
_, err := b.Matrix.SendText(b.ctx, id.RoomID(b.RoomID), text)
|
||||||
b.RoomID,
|
if err != nil {
|
||||||
text,
|
log.Printf(
|
||||||
)
|
"matrix send failed: %v",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (b *Bridge) _SendMatrix(
|
||||||
|
format string,
|
||||||
|
args ...any,
|
||||||
|
) {
|
||||||
|
text := fmt.Sprintf(format, args...)
|
||||||
|
// b.SendMessageWithpmp(b.RoomID, sender, text)
|
||||||
|
_, err := b.Matrix.SendText(b.ctx, id.RoomID(b.RoomID), text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf(
|
log.Printf(
|
||||||
"matrix send failed: %v",
|
"matrix send failed: %v",
|
||||||
|
|
@ -545,10 +314,7 @@ func dualMsg(
|
||||||
) {
|
) {
|
||||||
msg := fmt.Sprintf(format, args...)
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
|
||||||
_, err := bridge.Matrix.SendMessage(
|
_, err := bridge.Matrix.SendText(bridge.ctx, id.RoomID(roomID), msg)
|
||||||
roomID,
|
|
||||||
msg,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to send matrix notice: %v", err)
|
log.Printf("failed to send matrix notice: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -559,8 +325,8 @@ func dualMsg(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func handleMatrixCommand(
|
func handleMatrixCommand(
|
||||||
bridge *Bridge,
|
bridge *Bridge, id id.RoomID, sender id.UserID,
|
||||||
msg matrix.Message,
|
msg event.MessageEventContent,
|
||||||
) {
|
) {
|
||||||
parts := strings.Fields(strings.TrimPrefix(msg.Body, "!"))
|
parts := strings.Fields(strings.TrimPrefix(msg.Body, "!"))
|
||||||
|
|
||||||
|
|
@ -573,84 +339,51 @@ func handleMatrixCommand(
|
||||||
case "mclist":
|
case "mclist":
|
||||||
resp, err := bridge.MC.SendCommand("list")
|
resp, err := bridge.MC.SendCommand("list")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = bridge.Matrix.SendMessage(msg.RoomID, fmt.Sprintf("list failed: %v", err))
|
bridge.SendMessage(id, fmt.Sprintf("list failed: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _ = bridge.Matrix.SendMessage(
|
bridge.SendMessage(
|
||||||
msg.RoomID,
|
id,
|
||||||
resp,
|
resp,
|
||||||
)
|
)
|
||||||
case "rcon":
|
case "rcon":
|
||||||
if !bridge.IsTrustedUser(msg.Sender) {
|
if !bridge.IsTrustedUser(sender) {
|
||||||
bridge.Matrix.SendMessage(msg.RoomID, "You are not a whitelisted user to run an RCON command >:E")
|
bridge.SendMessage(id, "You are not a whitelisted user to run an RCON command >:E")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
torun := strings.Join(parts[1:], " ")
|
torun := strings.Join(parts[1:], " ")
|
||||||
if torun == "" {
|
if torun == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("[audit] rcon by %s: %q", msg.Sender, torun)
|
log.Printf("[audit] rcon by %s: %q", sender, torun)
|
||||||
resp, err := bridge.MC.SendCommand(torun)
|
resp, err := bridge.MC.SendCommand(torun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bridge.Matrix.SendMessage(msg.RoomID, fmt.Sprintf("error running rcon command: %s", err.Error()))
|
bridge.SendMessage(id, fmt.Sprintf("error running rcon command: %s", err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
bridge.Matrix.SendMessage(msg.RoomID, fmt.Sprintf("command response: %s", resp))
|
bridge.SendMessage(id, fmt.Sprintf("command response: %s", resp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (b *Bridge) RunMCEvents() {
|
func (b *Bridge) SendMessage(roomID id.RoomID, text string) {
|
||||||
for ev := range b.Events {
|
b.Matrix.SendText(b.ctx, b.RoomID, text)
|
||||||
|
}
|
||||||
switch ev.Type {
|
func (b *Bridge) SendMessageWithpmp(roomID id.RoomID, displayname, text string) {
|
||||||
|
if b.Usepmp {
|
||||||
case ChatEvent:
|
json := map[string]any{
|
||||||
b.Players.Add(ev.Player)
|
"msgtype": "m.text",
|
||||||
b.SendMatrix(
|
"body": text,
|
||||||
"%s: %s",
|
"com.beeper.per_message_profile": map[string]string{
|
||||||
sanitize(ev.Player, 64),
|
"displayname": displayname,
|
||||||
sanitize(ev.Message, 1024),
|
"id": displayname,
|
||||||
)
|
},
|
||||||
|
|
||||||
case JoinEvent:
|
|
||||||
b.Players.Add(ev.Player)
|
|
||||||
b.SendMatrix(
|
|
||||||
"%s joined the game :3",
|
|
||||||
ev.Player,
|
|
||||||
)
|
|
||||||
|
|
||||||
case LeaveEvent:
|
|
||||||
b.Players.Remove(ev.Player)
|
|
||||||
b.SendMatrix(
|
|
||||||
"%s left the game :<",
|
|
||||||
ev.Player,
|
|
||||||
)
|
|
||||||
|
|
||||||
case DeathEvent:
|
|
||||||
if !b.Players.Has(ev.Player) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
b.SendMatrix(
|
|
||||||
"%s",
|
|
||||||
sanitize(ev.Message, 512),
|
|
||||||
)
|
|
||||||
|
|
||||||
case AdvancementEvent:
|
|
||||||
b.SendMatrix(
|
|
||||||
"%s has made the advancement [%s] :3",
|
|
||||||
ev.Player,
|
|
||||||
sanitize(ev.Message, 128),
|
|
||||||
)
|
|
||||||
|
|
||||||
case PauseEvent:
|
|
||||||
b.SendMatrix(
|
|
||||||
"Server paused after %s seconds of inactivity :3",
|
|
||||||
ev.Message,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
b.Matrix.SendMessageEvent(b.ctx, b.RoomID, event.EventMessage, json)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
b.Matrix.SendText(b.ctx, b.RoomID, text)
|
||||||
}
|
}
|
||||||
func (b *Bridge) IsTrustedUser(mxid string) bool {
|
func (b *Bridge) IsTrustedUser(mxid id.UserID) bool {
|
||||||
if b.TrustedUsers == nil {
|
if b.TrustedUsers == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
335
minecraft.go
Normal file
335
minecraft.go
Normal file
|
|
@ -0,0 +1,335 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
mcrcon "github.com/Kelwing/mc-rcon"
|
||||||
|
)
|
||||||
|
|
||||||
|
func dialRCON(addr, password string) (*mcrcon.MCConn, error) {
|
||||||
|
conn := new(mcrcon.MCConn)
|
||||||
|
|
||||||
|
if err := conn.Open(addr, password); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := conn.Authenticate(); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMinecraft(addr, password string) (*Minecraft, error) {
|
||||||
|
conn, err := dialRCON(addr, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Minecraft{RCON: conn, addr: addr, password: password}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxNickLen = 32
|
||||||
|
maxMsgLen = 256
|
||||||
|
)
|
||||||
|
|
||||||
|
func sanitize(s string, max int) string {
|
||||||
|
s = strings.ToValidUTF8(s, "")
|
||||||
|
var b strings.Builder
|
||||||
|
n := 0
|
||||||
|
for _, r := range s {
|
||||||
|
if r == '\n' || r == '\r' || r == '\t' {
|
||||||
|
r = ' '
|
||||||
|
}
|
||||||
|
if r == '\u00a7' || unicode.IsControl(r) || unicode.Is(unicode.Cf, r) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if n >= max {
|
||||||
|
b.WriteString("…")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
b.WriteRune(r)
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *Minecraft) SendMessage(nick, msg string) error {
|
||||||
|
components := []map[string]any{
|
||||||
|
{"text": "", "color": "dark_gray"},
|
||||||
|
{"text": sanitize(nick, maxNickLen), "color": "aqua", "bold": true},
|
||||||
|
{"text": ": ", "color": "gray"},
|
||||||
|
{"text": sanitize(msg, maxMsgLen), "color": "white"},
|
||||||
|
}
|
||||||
|
j, err := json.Marshal(components)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = mc.SendCommand("tellraw @a " + string(j))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var playerNameRE = regexp.MustCompile(`^[A-Za-z0-9_]{1,16}$`)
|
||||||
|
|
||||||
|
var onlineRE = regexp.MustCompile(`players online: ?(.*)$`)
|
||||||
|
|
||||||
|
type PlayerSet struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
m map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlayerSet() *PlayerSet { return &PlayerSet{m: map[string]struct{}{}} }
|
||||||
|
|
||||||
|
func (p *PlayerSet) Add(n string) { p.mu.Lock(); p.m[n] = struct{}{}; p.mu.Unlock() }
|
||||||
|
func (p *PlayerSet) Remove(n string) { p.mu.Lock(); delete(p.m, n); p.mu.Unlock() }
|
||||||
|
func (p *PlayerSet) Has(n string) bool {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
_, ok := p.m[n]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlayerSet) Seed(listOutput string) {
|
||||||
|
m := onlineRE.FindStringSubmatch(listOutput)
|
||||||
|
if m == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, n := range strings.Split(m[1], ",") {
|
||||||
|
if n = strings.TrimSpace(n); playerNameRE.MatchString(n) {
|
||||||
|
p.Add(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseLogLine(line string) (*MCEvent, bool) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
if m := chatRE.FindStringSubmatch(line); m != nil {
|
||||||
|
return &MCEvent{
|
||||||
|
Type: ChatEvent,
|
||||||
|
Player: m[1],
|
||||||
|
Message: m[2],
|
||||||
|
RawLine: line,
|
||||||
|
Time: now,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if m := joinRE.FindStringSubmatch(line); m != nil {
|
||||||
|
return &MCEvent{
|
||||||
|
Type: JoinEvent,
|
||||||
|
Player: m[1],
|
||||||
|
RawLine: line,
|
||||||
|
Time: now,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if m := leaveRE.FindStringSubmatch(line); m != nil {
|
||||||
|
return &MCEvent{
|
||||||
|
Type: LeaveEvent,
|
||||||
|
Player: m[1],
|
||||||
|
RawLine: line,
|
||||||
|
Time: now,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if m := advancementRE.FindStringSubmatch(line); m != nil {
|
||||||
|
return &MCEvent{
|
||||||
|
Type: AdvancementEvent,
|
||||||
|
Player: m[1],
|
||||||
|
Message: m[2],
|
||||||
|
RawLine: line,
|
||||||
|
Time: now,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
if m := pauseRE.FindStringSubmatch(line); m != nil {
|
||||||
|
return &MCEvent{
|
||||||
|
Type: PauseEvent,
|
||||||
|
Message: m[1],
|
||||||
|
RawLine: line,
|
||||||
|
Time: now,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if m := deathRE.FindStringSubmatch(line); m != nil {
|
||||||
|
msg := m[1]
|
||||||
|
|
||||||
|
deaths := []string{
|
||||||
|
" was slain by ",
|
||||||
|
" was shot by ",
|
||||||
|
" was blown up",
|
||||||
|
" was doomed to ",
|
||||||
|
" drowned",
|
||||||
|
" died",
|
||||||
|
" blew up",
|
||||||
|
" burned to death",
|
||||||
|
" fell from",
|
||||||
|
" hit the ground too hard",
|
||||||
|
" was killed by ",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range deaths {
|
||||||
|
if strings.Contains(msg, d) {
|
||||||
|
|
||||||
|
player := strings.Fields(msg)[0]
|
||||||
|
if !playerNameRE.MatchString(player) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MCEvent{
|
||||||
|
Type: DeathEvent,
|
||||||
|
Player: player,
|
||||||
|
Message: msg,
|
||||||
|
RawLine: line,
|
||||||
|
Time: now,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxLogLine = 64 << 10
|
||||||
|
|
||||||
|
func WatchLog(path string, events chan<- *MCEvent) error {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { f.Close() }()
|
||||||
|
|
||||||
|
info, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = f.Seek(0, io.SeekEnd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
reader = bufio.NewReaderSize(f, 32<<10)
|
||||||
|
pending []byte
|
||||||
|
overflow bool
|
||||||
|
offset = info.Size()
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
chunk, err := reader.ReadSlice('\n')
|
||||||
|
offset += int64(len(chunk))
|
||||||
|
|
||||||
|
if !overflow {
|
||||||
|
pending = append(pending, chunk...)
|
||||||
|
if len(pending) > maxLogLine {
|
||||||
|
pending, overflow = nil, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == bufio.ErrBufferFull {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
if !overflow {
|
||||||
|
line := strings.TrimSpace(string(pending))
|
||||||
|
if event, ok := ParseLogLine(line); ok {
|
||||||
|
events <- event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pending, overflow = pending[:0], false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != io.EOF {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cur, statErr := os.Stat(path); statErr == nil {
|
||||||
|
if !os.SameFile(info, cur) || cur.Size() < offset {
|
||||||
|
log.Printf("%s rotated or truncated, reopening", path)
|
||||||
|
|
||||||
|
nf, openErr := os.Open(path)
|
||||||
|
if openErr != nil {
|
||||||
|
return openErr
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
f = nf
|
||||||
|
info, err = f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reader.Reset(f)
|
||||||
|
pending, overflow, offset = nil, false, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (b *Bridge) RunMCEvents() {
|
||||||
|
for ev := range b.Events {
|
||||||
|
|
||||||
|
switch ev.Type {
|
||||||
|
|
||||||
|
case ChatEvent:
|
||||||
|
b.Players.Add(ev.Player)
|
||||||
|
if b.Usepmp {
|
||||||
|
b.SendMessageWithpmp(b.RoomID, ev.Player, sanitize(ev.Message, 1024))
|
||||||
|
} else {
|
||||||
|
b._SendMatrix(
|
||||||
|
"%s: %s",
|
||||||
|
sanitize(ev.Player, 64),
|
||||||
|
sanitize(ev.Message, 1024),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
//b.SendMatrix(sanitize(ev.Player, 64), "%s", sanitize(ev.Message, 1024))
|
||||||
|
case JoinEvent:
|
||||||
|
b.Players.Add(ev.Player)
|
||||||
|
b._SendMatrix(
|
||||||
|
"%s joined the game :3",
|
||||||
|
ev.Player,
|
||||||
|
)
|
||||||
|
|
||||||
|
case LeaveEvent:
|
||||||
|
b.Players.Remove(ev.Player)
|
||||||
|
b._SendMatrix(
|
||||||
|
"%s left the game :<",
|
||||||
|
ev.Player,
|
||||||
|
)
|
||||||
|
|
||||||
|
case DeathEvent:
|
||||||
|
if !b.Players.Has(ev.Player) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b._SendMatrix(
|
||||||
|
"%s",
|
||||||
|
sanitize(ev.Message, 512),
|
||||||
|
)
|
||||||
|
|
||||||
|
case AdvancementEvent:
|
||||||
|
b._SendMatrix(
|
||||||
|
"%s has made the advancement [%s] :3",
|
||||||
|
ev.Player,
|
||||||
|
sanitize(ev.Message, 128),
|
||||||
|
)
|
||||||
|
|
||||||
|
case PauseEvent:
|
||||||
|
b._SendMatrix(
|
||||||
|
"Server paused after %s seconds of inactivity :3",
|
||||||
|
ev.Message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue