32 changed files with 1788 additions and 292 deletions
@ -0,0 +1,16 @@
|
||||
kind: pipeline |
||||
type: docker |
||||
name: default |
||||
|
||||
steps: |
||||
- name: docker |
||||
image: plugins/docker |
||||
settings: |
||||
username: |
||||
from_secret: docker_username |
||||
password: |
||||
from_secret: docker_password |
||||
repo: registry.meow.tf/tyler/residentsleeper |
||||
registry: registry.meow.tf |
||||
tags: |
||||
- latest |
@ -0,0 +1,9 @@
|
||||
FROM golang:alpine AS builder |
||||
|
||||
RUN go build -o sleeper |
||||
|
||||
FROM alpine |
||||
|
||||
COPY --from=builder sleeper /sleeper |
||||
|
||||
CMD ["/sleeper"] |
@ -1,76 +0,0 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"strconv" |
||||
"strings" |
||||
) |
||||
|
||||
func sendMessage(user, message string) { |
||||
_, err := client.SendCommand(fmt.Sprintf("msg %s %s", user, message)) |
||||
|
||||
if err != nil { |
||||
sendMessage(user, message) |
||||
} |
||||
} |
||||
|
||||
func serverMessage(message string) { |
||||
_, err := client.SendCommand("say " + message) |
||||
|
||||
if err != nil { |
||||
serverMessage(message) |
||||
} |
||||
} |
||||
|
||||
func onlineUsers() (int, int, []string, error) { |
||||
res, err := client.SendCommand("list") |
||||
|
||||
if err != nil { |
||||
return -1, -1, nil, err |
||||
} |
||||
|
||||
m := userRegexp.FindStringSubmatch(res) |
||||
|
||||
if m == nil { |
||||
return -1, -1, nil, errors.New("unexpected response") |
||||
} |
||||
|
||||
online, _ := strconv.Atoi(m[1]) |
||||
max, _ := strconv.Atoi(m[2]) |
||||
names := strings.Split(m[3], ", ") |
||||
|
||||
return online, max, names, nil |
||||
} |
||||
|
||||
func queryTime() (int, error) { |
||||
res, err := client.SendCommand("time query daytime") |
||||
|
||||
if err != nil { |
||||
return -1, err |
||||
} |
||||
|
||||
m := timeRegexp.FindStringSubmatch(res) |
||||
|
||||
if m == nil { |
||||
return -1, errors.New("no time found") |
||||
} |
||||
|
||||
return strconv.Atoi(m[1]) |
||||
} |
||||
|
||||
func addTime(ticks int) (int, error) { |
||||
res, err := client.SendCommand(fmt.Sprintf("time add %d", ticks)) |
||||
|
||||
if err != nil { |
||||
return -1, err |
||||
} |
||||
|
||||
m := timeRegexp.FindStringSubmatch(res) |
||||
|
||||
if m == nil { |
||||
return -1, err |
||||
} |
||||
|
||||
return strconv.Atoi(m[1]) |
||||
} |
@ -0,0 +1,88 @@
|
||||
package commands |
||||
|
||||
import "strings" |
||||
|
||||
const ( |
||||
ArgumentTypeBasic = iota |
||||
ArgumentTypeUserMention |
||||
ArgumentTypeChannelMention |
||||
) |
||||
|
||||
func parseCommandUsage(prefix string) *CommandHolder { |
||||
holder := &CommandHolder{} |
||||
|
||||
holder.Name = prefix |
||||
holder.Usage = prefix |
||||
|
||||
if idx := strings.Index(prefix, " "); idx != -1 { |
||||
holder.Name = prefix[0:idx] |
||||
|
||||
prefix = prefix[idx+1:] |
||||
|
||||
// Parse out command arguments, example:
|
||||
// test <arg1> [optional arg2]
|
||||
// Walk through string, match < and >, [ and ]
|
||||
holder.Arguments = make(map[string]*CommandArgument) |
||||
|
||||
str := prefix |
||||
|
||||
var name string |
||||
|
||||
var index int |
||||
|
||||
for { |
||||
if len(str) == 0 { |
||||
break |
||||
} |
||||
|
||||
ch := str[0] |
||||
|
||||
if ch == '<' || ch == '[' { |
||||
// Scan until closing arrow or end of string
|
||||
for i := 1; i < len(str); i++ { |
||||
if (str[i] == '>' || str[i] == ']') && str[i-1] != '\\' { |
||||
name = str[1:i] |
||||
if i+2 < len(str) { |
||||
str = str[i+2:] |
||||
} else { |
||||
str = "" |
||||
} |
||||
|
||||
required := false |
||||
|
||||
if ch == '<' { |
||||
required = true |
||||
|
||||
holder.RequiredArgumentCount++ |
||||
} |
||||
|
||||
t := ArgumentTypeBasic |
||||
|
||||
if name[0] == '@' { |
||||
t = ArgumentTypeUserMention |
||||
name = name[1:] |
||||
} else if name[0] == '#' { |
||||
t = ArgumentTypeChannelMention |
||||
name = name[1:] |
||||
} |
||||
|
||||
holder.Arguments[name] = &CommandArgument{ |
||||
Index: index, |
||||
Name: name, |
||||
Required: required, |
||||
Type: t, |
||||
} |
||||
|
||||
index++ |
||||
|
||||
break |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
holder.ArgumentCount = len(holder.Arguments) |
||||
} |
||||
|
||||
return holder |
||||
} |
@ -0,0 +1,15 @@
|
||||
package commands |
||||
|
||||
type CommandContext struct { |
||||
holder *CommandHolder |
||||
|
||||
User string |
||||
|
||||
Prefix string |
||||
Command string |
||||
ArgumentString string |
||||
Arguments []string |
||||
ArgumentCount int |
||||
|
||||
Reply func(text string) |
||||
} |
@ -0,0 +1,69 @@
|
||||
package commands |
||||
|
||||
import "errors" |
||||
|
||||
type CommandHolder struct { |
||||
Name string |
||||
Usage string |
||||
Handler CommandHandler |
||||
Permission int |
||||
Arguments map[string]*CommandArgument |
||||
ArgumentCount int |
||||
RequiredArgumentCount int |
||||
} |
||||
|
||||
type CommandArgument struct { |
||||
Index int |
||||
Name string |
||||
Required bool |
||||
Type int |
||||
} |
||||
|
||||
func (c *CommandHolder) Call(ctx *CommandContext) { |
||||
ctx.holder = c |
||||
|
||||
if c.ArgumentCount > 0 { |
||||
// Arguments are cached, construct usage
|
||||
if err := c.Validate(ctx); err != nil { |
||||
if err == UsageError { |
||||
ctx.Reply("Usage: " + ctx.Prefix + c.Usage) |
||||
} else { |
||||
ctx.Reply(err.Error()) |
||||
} |
||||
return |
||||
} |
||||
} |
||||
|
||||
// Use a goroutine to avoid any blocking operations
|
||||
c.Handler(ctx) |
||||
} |
||||
|
||||
var ( |
||||
UsageError = errors.New("usage") |
||||
) |
||||
|
||||
func (c *CommandHolder) Validate(ctx *CommandContext) error { |
||||
if ctx.ArgumentCount < c.RequiredArgumentCount { |
||||
return UsageError |
||||
} |
||||
|
||||
var argValue string |
||||
|
||||
for _, arg := range c.Arguments { |
||||
if ctx.ArgumentCount < arg.Index+1 { |
||||
break |
||||
} |
||||
|
||||
if !arg.Required { |
||||
continue |
||||
} |
||||
|
||||
argValue = ctx.Arguments[arg.Index] |
||||
|
||||
if argValue == "" { |
||||
return errors.New("The " + arg.Name + " argument is required.") |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,35 @@
|
||||
package commands |
||||
|
||||
import "strings" |
||||
|
||||
type CommandHandler func(ctx *CommandContext) |
||||
|
||||
var ( |
||||
commandMap = make(map[string]*CommandHolder) |
||||
) |
||||
|
||||
func Register(prefix string, f CommandHandler) { |
||||
h := parseCommandUsage(prefix) |
||||
h.Handler = f |
||||
commandMap[h.Name] = h |
||||
} |
||||
|
||||
func Find(prefix, name, full string) *CommandHolder { |
||||
if strings.Index(name, prefix) == 0 { |
||||
name = name[len(prefix):] |
||||
|
||||
if holder := Handler(name); holder != nil { |
||||
return holder |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func Handler(name string) *CommandHolder { |
||||
if holder, ok := commandMap[name]; ok { |
||||
return holder |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,71 @@
|
||||
package commands |
||||
|
||||
type SpaceTokenizer struct { |
||||
Input string |
||||
} |
||||
|
||||
func (t *SpaceTokenizer) NextToken() string { |
||||
if len(t.Input) == 0 { |
||||
return "" |
||||
} |
||||
|
||||
ch := t.Input[0] |
||||
|
||||
if ch == '"' { |
||||
// Scan until closing quote or end of string
|
||||
for i := 1; i < len(t.Input); i++ { |
||||
if t.Input[i] == '"' && t.Input[i-1] != '\\' { |
||||
ret := t.Input[1:i] |
||||
if i+2 < len(t.Input) { |
||||
t.Input = t.Input[i+2:] |
||||
} else { |
||||
t.Input = "" |
||||
} |
||||
return ret |
||||
} |
||||
} |
||||
} else { |
||||
for i := 0; i < len(t.Input); i++ { |
||||
if t.Input[i] == ' ' { |
||||
ret := t.Input[0:i] |
||||
if i+1 < len(t.Input) { |
||||
t.Input = t.Input[i+1:] |
||||
} else { |
||||
t.Input = "" |
||||
} |
||||
return ret |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
ret := t.Input |
||||
|
||||
t.Input = "" |
||||
|
||||
return ret |
||||
} |
||||
|
||||
func (t *SpaceTokenizer) Empty() bool { |
||||
return t.Input == "" |
||||
} |
||||
|
||||
func NewSpaceTokenizer(input string) *SpaceTokenizer { |
||||
return &SpaceTokenizer{Input: input} |
||||
} |
||||
|
||||
func ParseCommandArguments(command string) []string { |
||||
tokenizer := NewSpaceTokenizer(command) |
||||
|
||||
arguments := make([]string, 0) |
||||
|
||||
for { |
||||
if tokenizer.Empty() { |
||||
break |
||||
} |
||||
|
||||
arguments = append(arguments, tokenizer.NextToken()) |
||||
} |
||||
|
||||
return arguments |
||||
} |
@ -0,0 +1,69 @@
|
||||
package commands |
||||
|
||||
import ( |
||||
"regexp" |
||||
"testing" |
||||
) |
||||
|
||||
func TestParseCommandArguments(t *testing.T) { |
||||
args := ParseCommandArguments("normal \"testing quoted\" and normal \"end closed\"") |
||||
expected := []string{"normal", "testing quoted", "and", "normal", "end closed"} |
||||
|
||||
for i := 0; i < len(expected); i++ { |
||||
if args[i] != expected[i] { |
||||
t.Errorf("Expected %s, got %s at index %d", expected[i], args[i], i) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestDispatching(t *testing.T) { |
||||
Register("testing", func(ctx *CommandContext) { |
||||
// Something
|
||||
}) |
||||
|
||||
if handler := Find("!", "!testing", "testing"); handler == nil { |
||||
t.Error("Expected handler for testing") |
||||
} |
||||
} |
||||
|
||||
func TestRegexpDispatching(t *testing.T) { |
||||
RegisterMatch(regexp.MustCompile("^compliment"), func(ctx *CommandContext) { |
||||
// Something
|
||||
}) |
||||
|
||||
if handler := Find("", "compliment", "compliment me"); handler == nil { |
||||
t.Error("Expected handler for compliment") |
||||
} |
||||
} |
||||
|
||||
func TestArgumentParsing(t *testing.T) { |
||||
holder := parseCommandUsage("test <arg1> <arg2> [arg3]") |
||||
|
||||
if holder.Name != "test" { |
||||
t.Error("Unexpected command name") |
||||
} |
||||
|
||||
if len(holder.Arguments) != 3 { |
||||
t.Error("Invalid number of command arguments:", len(holder.Arguments)) |
||||
} |
||||
|
||||
if holder.RequiredArgumentCount != 2 { |
||||
t.Error("Invalid required argument count") |
||||
} |
||||
} |
||||
|
||||
func TestArgumentDispatching(t *testing.T) { |
||||
holder := parseCommandUsage("test <arg1> <arg2> [arg3]") |
||||
|
||||
if holder.Name != "test" { |
||||
t.Error("Unexpected command name") |
||||
} |
||||
|
||||
if len(holder.Arguments) != 3 { |
||||
t.Error("Invalid number of command arguments:", len(holder.Arguments)) |
||||
} |
||||
|
||||
if holder.RequiredArgumentCount != 2 { |
||||
t.Error("Invalid required argument count") |
||||
} |
||||
} |
@ -0,0 +1,54 @@
|
||||
package events |
||||
|
||||
import ( |
||||
"sync" |
||||
) |
||||
|
||||
type eventHandler func(args ...interface{}) |
||||
|
||||
var ( |
||||
eventMap = make(map[string][]eventHandler) |
||||
eventMapLock sync.RWMutex |
||||
|
||||
allListeners = make([]eventHandler, 0) |
||||
|
||||
initialized = false |
||||
) |
||||
|
||||
func On(name string, f eventHandler) { |
||||
eventMapLock.Lock() |
||||
defer eventMapLock.Unlock() |
||||
|
||||
handlers, exists := eventMap[name] |
||||
|
||||
if !exists { |
||||
handlers = make([]eventHandler, 0) |
||||
} |
||||
|
||||
handlers = append(handlers, f) |
||||
|
||||
eventMap[name] = handlers |
||||
} |
||||
|
||||
func OnALl(f eventHandler) { |
||||
allListeners = append(allListeners, f) |
||||
} |
||||
|
||||
func Call(name string, args ...interface{}) { |
||||
if !initialized && name != Init { |
||||
Call(Init) |
||||
initialized = true |
||||
} |
||||
|
||||
eventMapLock.RLock() |
||||
handlers, exists := eventMap[name] |
||||
eventMapLock.RUnlock() |
||||
|
||||
if !exists { |
||||
return |
||||
} |
||||
|
||||
for _, handler := range handlers { |
||||
handler(args...) |
||||
} |
||||
} |
@ -0,0 +1,29 @@
|
||||
package events |
||||
|
||||
const ( |
||||
Init = "init" |
||||
|
||||
ServerStarted = "server_started" |
||||
ServerClosing = "server_closing" |
||||
|
||||
// LoggedIn, Args: User, Address, X, Y, Z
|
||||
LoggedIn = "logged_in" |
||||
|
||||
// Authenticated, Args: User, UUID
|
||||
Authenticated = "authenticated" |
||||
|
||||
// Join, Args: User
|
||||
Join = "join" |
||||
|
||||
// Leave, Args: User
|
||||
Leave = "leave" |
||||
|
||||
// Op, Args: User, Source?
|
||||
Op = "op" |
||||
|
||||
// Deop, Args: User, Source?
|
||||
Deop = "deop" |
||||
|
||||
// Message, Args: User, Message
|
||||
Message = "message" |
||||
) |
@ -1,12 +1,17 @@
|
||||
module sleepvote |
||||
module meow.tf/residentsleeper |
||||
|
||||
go 1.12 |
||||
|
||||
require ( |
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d |
||||
github.com/fsnotify/fsnotify v1.4.7 // indirect |
||||
github.com/hpcloud/tail v1.0.0 |
||||
github.com/tystuyfzand/mcgorcon v0.0.0-20190416171454-d0d528ef5548 |
||||
github.com/ppacher/nbt v0.0.0-20181201174858-0cad976cf07c |
||||
github.com/tystuyfzand/mcgorcon v0.0.0-20190418232414-4bb1402707f4 |
||||
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583 |
||||
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd // indirect |
||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect |
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect |
||||
layeh.com/gopher-json v0.0.0-20190114024228-97fed8db8427 |
||||
layeh.com/gopher-luar v1.0.5 |
||||
) |
||||
|
@ -1,12 +1,26 @@
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= |
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= |
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= |
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= |
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= |
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= |
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= |
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= |
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= |
||||
github.com/tystuyfzand/mcgorcon v0.0.0-20190416171454-d0d528ef5548 h1:ho5QHzfPALI0+v7sPzJFtSHDH/5amyuG88+3ycp70Rk= |
||||
github.com/tystuyfzand/mcgorcon v0.0.0-20190416171454-d0d528ef5548/go.mod h1:MpDGxcw1VVpnQrSbEjy5ZRc+RVgO3j68N8RuI8snkF4= |
||||
github.com/ppacher/nbt v0.0.0-20181201174858-0cad976cf07c h1:d9Pm+C0vGwMRxn04D6MjHbqkEvG+qlpHjlARadAAmpQ= |
||||
github.com/ppacher/nbt v0.0.0-20181201174858-0cad976cf07c/go.mod h1:vvnpyLNjExupwOjP8Dvqprqwtt3BxXKoDSvWkHvCODs= |
||||
github.com/tystuyfzand/mcgorcon v0.0.0-20190418232414-4bb1402707f4 h1:E2wndDHuZBEqCUijjvLhfFu8wInU9ciBZuXXkn/lGHk= |
||||
github.com/tystuyfzand/mcgorcon v0.0.0-20190418232414-4bb1402707f4/go.mod h1:MpDGxcw1VVpnQrSbEjy5ZRc+RVgO3j68N8RuI8snkF4= |
||||
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583 h1:SZPG5w7Qxq7bMcMVl6e3Ht2X7f+AAGQdzjkbyOnNNZ8= |
||||
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= |
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd h1:MNN7PRW7zYXd8upVO5qfKeOnQG74ivRNv7sz4k4cQMs= |
||||
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= |
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= |
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= |
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= |
||||
layeh.com/gopher-json v0.0.0-20190114024228-97fed8db8427 h1:RZkKxMR3jbQxdCEcglq3j7wY3PRJIopAwBlx1RE71X0= |
||||
layeh.com/gopher-json v0.0.0-20190114024228-97fed8db8427/go.mod h1:ivKkcY8Zxw5ba0jldhZCYYQfGdb2K6u9tbYK1AwMIBc= |
||||
layeh.com/gopher-luar v1.0.5 h1:fBuMh/xVN7bZxOsFzY6mxL2I+0ePJIWfyGc0dBdpqs4= |
||||
layeh.com/gopher-luar v1.0.5/go.mod h1:N3rev/ttQd8yVluXaYsa0M/eknzRYWe+pxZ35ZFmaaI= |
||||
|
@ -0,0 +1,110 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"github.com/acarl005/stripansi" |
||||
"github.com/hpcloud/tail" |
||||
"log" |
||||
"meow.tf/residentsleeper/events" |
||||
"meow.tf/residentsleeper/rcon" |
||||
"os" |
||||
"regexp" |
||||
"strings" |
||||
) |
||||
|
||||
const ( |
||||
timeThreadRegexp = "^\\[.*?\\]\\s\\[.*?\\/INFO\\]:\\s" |
||||
) |
||||
|
||||
var ( |
||||
messageRegexp = regexp.MustCompile(timeThreadRegexp + "<(.*?)>\\s(.*)") |
||||
joinedRegexp = regexp.MustCompile(timeThreadRegexp + "(.*?) joined the game$") |
||||
leftRegexp = regexp.MustCompile(timeThreadRegexp + "(.*?) left the game$") |
||||
stoppingRegexp = regexp.MustCompile(timeThreadRegexp + "Stopping server") |
||||
rconRegexp = regexp.MustCompile(timeThreadRegexp + "RCON running on") |
||||
loggedInRegexp = regexp.MustCompile(timeThreadRegexp + "(.*?)\\[(.*?)\\] logged in with entity id \\d+ at \\((.*?)\\)") |
||||
startedRegexp = regexp.MustCompile(timeThreadRegexp + "Done \\(.*?\\)! For help, type \"help\"") |
||||
authenticatedRegexp = regexp.MustCompile(timeThreadRegexp + "UUID of player (.*?) is (.*?)$") |
||||
opRegexp = regexp.MustCompile("Made (.*?) a server operator") |
||||
deopRegexp = regexp.MustCompile("Made (.*?) no longer a server operator") |
||||
sourceRegexp = regexp.MustCompile(timeThreadRegexp + "\\[(.*?): (.*?)\\]") |
||||
|
||||
sleepRegexp = regexp.MustCompile("^z{3,}$") |
||||
timeRegexp = regexp.MustCompile("(\\d+)$") |
||||
) |
||||
|
||||
func logParser(logPath string) { |
||||
log.Println("Watching log path", logPath) |
||||
|
||||
stat, err := os.Stat(logPath) |
||||
|
||||
if err != nil { |
||||
log.Fatalln("Unable to open log file:", err) |
||||
} |
||||
|
||||
seek := &tail.SeekInfo{ |
||||
Offset: stat.Size(), |
||||
} |
||||
|
||||
// Start parsing file
|
||||
t, err := tail.TailFile(logPath, tail.Config{Location: seek, Follow: true, ReOpen: true}) |
||||
|
||||
if err != nil { |
||||
log.Fatalln("Unable to open file:", err) |
||||
} |
||||
|
||||
var m []string |
||||
|
||||
for line := range t.Lines { |
||||
line.Text = stripansi.Strip(strings.TrimSpace(line.Text)) |
||||
|
||||
if debug { |
||||
log.Println("Parsing line", line.Text) |
||||
log.Println("Bytes:", []byte(line.Text)) |
||||
} |
||||
|
||||
if m = messageRegexp.FindStringSubmatch(line.Text); m != nil { |
||||
events.Call(events.Message, m[1], m[2]) |
||||
} else if m = loggedInRegexp.FindStringSubmatch(line.Text); m != nil { |
||||
position, err := rcon.SliceToFloats(strings.Split(m[3], ", ")) |
||||
|
||||
if err != nil { |
||||
position = []float64{0, 0, 0} |
||||
} |
||||
|
||||
events.Call(events.LoggedIn, m[1], m[2], position[0], position[1], position[2]) |
||||
} else if m = authenticatedRegexp.FindStringSubmatch(line.Text); m != nil { |
||||
events.Call(events.Authenticated, m[1], m[2]) |
||||
} else if m = joinedRegexp.FindStringSubmatch(line.Text); m != nil { |
||||
events.Call(events.Join, m[1]) |
||||
} else if m = leftRegexp.FindStringSubmatch(line.Text); m != nil { |
||||
events.Call(events.Leave, m[1]) |
||||
} else if m = opRegexp.FindStringSubmatch(line.Text); m != nil { |
||||
source := "Server" |
||||
|
||||
subM := sourceRegexp.FindStringSubmatch(line.Text) |
||||
|
||||
if subM != nil { |
||||
source = subM[1] |
||||
} |
||||
|
||||
events.Call(events.Op, m[1], source) |
||||
} else if m = deopRegexp.FindStringSubmatch(line.Text); m != nil { |
||||
source := "Server" |
||||
|
||||
subM := sourceRegexp.FindStringSubmatch(line.Text) |
||||
|
||||
if subM != nil { |
||||
source = subM[1] |
||||
} |
||||
|
||||
events.Call(events.Deop, m[1], source) |
||||
} else if stoppingRegexp.MatchString(line.Text) { |
||||
events.Call(events.ServerClosing) |
||||
client.Disconnect() |
||||
} else if startedRegexp.MatchString(line.Text) { |
||||
events.Call(events.ServerStarted) |
||||
} else if rconRegexp.MatchString(line.Text) { |
||||
events.Call(events.Init) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,48 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"bufio" |
||||
"os" |
||||
"regexp" |
||||
"testing" |
||||
) |
||||
|
||||
type regexpTest struct { |
||||
input string |
||||
re *regexp.Regexp |
||||
} |
||||
|
||||
func Test_Regexp(t *testing.T) { |
||||
inputs := map[string]regexpTest{ |
||||
"join": {"[19:22:13] [Server thread/INFO]: notch joined the game", joinedRegexp}, |
||||
"leave": {"[19:16:13] [Server thread/INFO]: notch left the game", leftRegexp}, |
||||
"message": {"[19:37:13] [Server thread/INFO]: <notch> hello", messageRegexp}, |
||||
"spigotMessage": {"[23:32:42] [Async Chat Thread - #2/INFO]: <ccatss> zzz", messageRegexp}, |
||||
"rcon": {"[16:46:42] [RCON Listener #1/INFO]: RCON running on 0.0.0.0:25575", rconRegexp}, |
||||
} |
||||
|
||||
for key, re := range inputs { |
||||
if !re.re.MatchString(re.input) { |
||||
t.Fatal("Regexp for", key, "did not match") |
||||
} |
||||
} |
||||
} |
||||
|
||||
func Test_EndCharacters(t *testing.T) { |
||||
f, err := os.Open("latest.log") |
||||
|
||||
if err != nil { |
||||
t.Fatal("Log file not found") |
||||
} |
||||
|
||||
defer f.Close() |
||||
|
||||
scanner := bufio.NewScanner(f) |
||||
|
||||
if !scanner.Scan() { |
||||
t.Fatal("Log empty") |
||||
} |
||||
|
||||
t.Log(scanner.Text()) |
||||
t.Log(scanner.Bytes()) |
||||
} |
@ -0,0 +1,81 @@
|
||||
package rcon |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"regexp" |
||||
"strconv" |
||||
"strings" |
||||
) |
||||
|
||||
var ( |
||||
userRegexp = regexp.MustCompile("There are (\\d+) of a max of (\\d+) players online: (.*?)$") |
||||
|
||||
teleportedRegexp = regexp.MustCompile("^Teleported (.*?) to (.*?)$") |
||||
) |
||||
|
||||
func (r *Session) OnlinePlayers() (int, int, []string, error) { |
||||
res, err := r.connection().SendCommand("list") |
||||
|
||||
if err != nil { |
||||
return -1, -1, nil, err |
||||
} |
||||
|
||||
m := userRegexp.FindStringSubmatch(res) |
||||
|
||||
if m == nil { |
||||
return -1, -1, nil, errors.New("unexpected response") |
||||
} |
||||
|
||||
online, _ := strconv.Atoi(m[1]) |
||||
max, _ := strconv.Atoi(m[2]) |
||||
names := strings.Split(m[3], ", ") |
||||
|
||||
return online, max, names, nil |
||||
} |
||||
|
||||
func (r *Session) GetLocation(user string) []float32 { |
||||
res, err := r.connection().SendCommand(fmt.Sprintf("execute at %s run tp %s ~ ~ ~", user, user)) |
||||
|
||||
if err != nil { |
||||
return nil |
||||
} |
||||
|
||||
m := teleportedRegexp.FindStringSubmatch(res) |
||||
|
||||
pos, err := SliceToFloats(strings.Split(m[2], ",")) |
||||
|
||||
if err != nil { |
||||
return nil |
||||
} |
||||
|
||||
ret := make([]float32, 3) |
||||
|
||||
for i, v := range pos { |
||||
ret[i] = float32(v) |
||||
} |
||||
|
||||
return ret |
||||
} |
||||
|
||||
func (r *Session) Teleport(user, destination string) (string, error) { |
||||
return r.connection().SendCommand(fmt.Sprintf("tp %s %s", user, destination)) |
||||
} |
||||
|
||||
func SliceToFloats(str []string) ([]float64, error) { |
||||
ret := make([]float64, len(str)) |
||||
|
||||
var err error |
||||
|
||||
for i, s := range str { |
||||
s = strings.TrimSpace(s) |
||||
|
||||
ret[i], err = strconv.ParseFloat(s, 64) |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
return ret, err |
||||
} |
@ -0,0 +1,115 @@
|
||||
package rcon |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"github.com/tystuyfzand/mcgorcon" |
||||
"log" |
||||
"time" |
||||
) |
||||
|
||||
const ( |
||||
pingDuration = 30 * time.Second |
||||
) |
||||
|
||||
type Session struct { |
||||
host string |
||||
port int |
||||
password string |
||||
|
||||
Debug bool |
||||
|
||||
client *mcgorcon.Client |
||||
lastUse time.Time |
||||
} |
||||
|
||||
func NewSession(host string, port int, password string) *Session { |
||||
return &Session{host: host, port: port, password: password} |
||||
} |
||||
|
||||
func (r *Session) Disconnect() { |
||||
if r.client != nil { |
||||
r.client.Close() |
||||
} |
||||
} |
||||
|
||||
func (r *Session) connection() *mcgorcon.Client { |
||||
if r.client != nil { |
||||
if time.Since(r.lastUse) > pingDuration { |
||||
_, err := r.client.SendCommand("seed") |
||||
|
||||
if err == nil { |
||||
r.lastUse = time.Now() |
||||
return r.client |
||||
} |
||||
} else { |
||||
return r.client |
||||
} |
||||
} |
||||
|
||||
// Open new connection
|
||||
r.client = r.openConnection() |
||||
r.lastUse = time.Now() |
||||
|
||||
return r.client |
||||
} |
||||
|
||||
func (r *Session) openConnection() *mcgorcon.Client { |
||||
var c *mcgorcon.Client |
||||
var err error |
||||
|
||||
for c == nil || err != nil { |
||||
c, err = mcgorcon.Dial(r.host, r.port, r.password) |
||||
|
||||
if err == nil { |
||||
break |
||||
} |
||||
|
||||
<-time.After(10 * time.Second) |
||||
} |
||||
|
||||
log.Println("Connection opened") |
||||
|
||||
return c |
||||
} |
||||
|
||||
func (r *Session) SendCommand(command string) (string, error) { |
||||
if r.Debug { |
||||
log.Println("Send command: " + command) |
||||
} |
||||
|
||||
return r.connection().SendCommand(command) |
||||
} |
||||
|
||||
func (r *Session) ServerMessage(message string) error { |
||||
_, err := r.SendCommand(fmt.Sprintf("say %s", message)) |
||||
|
||||
return err |
||||
} |
||||
|
||||
func (r *Session) SendMessage(user, message string) error { |
||||
_, err := r.SendCommand(fmt.Sprintf("msg %s %s", user, message)) |
||||
|
||||
return err |
||||
} |
||||
|
||||
type colorMessage struct { |
||||
Text string `json:"text"` |
||||
Color string `json:"color"` |
||||
} |
||||
|
||||
func (r *Session) SendColorfulMessage(target, color, message string) error { |
||||
messages := []colorMessage{ |
||||
{Text: message, Color: color}, |
||||
} |
||||
|
||||
b, err := json.Marshal(messages) |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = r.SendCommand(fmt.Sprintf("tellraw %s %s", target, string(b))) |
||||
|
||||
return err |
||||
} |
@ -0,0 +1,47 @@
|
||||
package rcon |
||||
|
||||
import ( |
||||
"fmt" |
||||
"regexp" |
||||
"strconv" |
||||
) |
||||
|
||||
var ( |
||||
timeRegexp = regexp.MustCompile("(\\d+)$") |
||||
) |
||||
|
||||
func (r *Session) AddTime(delta int) int { |
||||
res, err := r.connection().SendCommand(fmt.Sprintf("time add %d", delta)) |
||||
|
||||
if err != nil { |
||||
return -1 |
||||
} |
||||
|
||||
m := timeRegexp.FindStringSubmatch(res) |
||||
|
||||
if m == nil { |
||||
return -1 |
||||
} |
||||
|
||||
ret, _ := strconv.Atoi(m[1]) |
||||
|
||||
return ret |
||||
} |
||||
|
||||
func (r *Session) GetTime() int { |
||||
res, err := r.connection().SendCommand("time query daytime") |
||||
|
||||
if err != nil { |
||||
return -1 |
||||
} |
||||
|
||||
m := timeRegexp.FindStringSubmatch(res) |
||||
|
||||
if m == nil { |
||||
return -1 |
||||
} |
||||
|
||||
v, _ := strconv.Atoi(m[1]) |
||||
|
||||
return v |
||||
} |
@ -0,0 +1,55 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"github.com/yuin/gopher-lua" |
||||
"io/ioutil" |
||||
luajson "layeh.com/gopher-json" |
||||
"layeh.com/gopher-luar" |
||||
"log" |
||||
"meow.tf/residentsleeper/scripting/commands" |
||||
"meow.tf/residentsleeper/scripting/config" |
||||
"meow.tf/residentsleeper/scripting/event" |
||||
"meow.tf/residentsleeper/scripting/minecraft" |
||||
"meow.tf/residentsleeper/scripting/regexp" |
||||
"path" |
||||
) |
||||
|
||||
func LoadScripts(scriptPath string) error { |
||||
files, err := ioutil.ReadDir(scriptPath) |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
for _, file := range files { |
||||
if file.IsDir() { |
||||
continue |
||||
} |
||||
|
||||
log.Println("Loading script", file.Name()) |
||||
|
||||
err = loadScript(path.Join(scriptPath, file.Name())) |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func loadScript(scriptFile string) error { |
||||
L := lua.NewState() |
||||
|
||||
luajson.Preload(L) |
||||
|
||||
L.PreloadModule("config", config.Loader) |
||||
L.PreloadModule("event", event.Loader) |
||||
L.PreloadModule("regexp", regexp.Loader) |
||||
L.PreloadModule("minecraft", minecraft.Loader) |
||||
L.PreloadModule("commands", commands.Loader) |
||||
|
||||
L.SetGlobal("rcon", luar.New(L, client)) |
||||
|
||||
return L.DoFile(scriptFile) |
||||
} |
@ -0,0 +1,37 @@
|
||||
package commands |
||||
|
||||
import ( |
||||
lua "github.com/yuin/gopher-lua" |
||||
"meow.tf/residentsleeper/commands" |
||||
) |
||||
|
||||
func Loader(L *lua.LState) int { |
||||
mod := L.SetFuncs(L.NewTable(), exports) |
||||
L.Push(mod) |
||||
return 1 |
||||
} |
||||
|
||||
var exports = map[string]lua.LGFunction{ |
||||
"register": commandRegister, |
||||
} |
||||
|
||||
func commandRegister(L *lua.LState) int { |
||||
name := L.CheckString(1) |
||||
|
||||
handler := L.CheckFunction(2) |
||||
|
||||
cb := func(ctx *commands.CommandContext) { |
||||
L.Push(handler) |
||||
L.Push(lua.LString(ctx.User)) |
||||
|
||||
for _, v := range ctx.Arguments { |
||||
L.Push(lua.LString(v)) |
||||
} |
||||
|
||||
L.PCall(1+ctx.ArgumentCount, 0, nil) |
||||
} |
||||
|
||||
commands.Register(name, cb) |
||||
|
||||
return 0 |
||||
} |