206 lines
		
	
	
		
			3.9 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			206 lines
		
	
	
		
			3.9 KiB
		
	
	
	
		
			Go
		
	
	
	
| package minecraft
 | |
| 
 | |
| import (
 | |
| 	"regexp"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/influxdata/telegraf/plugins/inputs/minecraft/internal/rcon"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	scoreboardRegexLegacy = regexp.MustCompile(`(?U):\s(?P<value>\d+)\s\((?P<name>.*)\)`)
 | |
| 	scoreboardRegex       = regexp.MustCompile(`\[(?P<name>[^\]]+)\]: (?P<value>\d+)`)
 | |
| )
 | |
| 
 | |
| // Connection is an established connection to the Minecraft server.
 | |
| type Connection interface {
 | |
| 	// Execute runs a command.
 | |
| 	Execute(command string) (string, error)
 | |
| }
 | |
| 
 | |
| // Connector is used to create connections to the Minecraft server.
 | |
| type Connector interface {
 | |
| 	// Connect establishes a connection to the server.
 | |
| 	Connect() (Connection, error)
 | |
| }
 | |
| 
 | |
| func NewConnector(hostname, port, password string) (*connector, error) {
 | |
| 	return &connector{
 | |
| 		hostname: hostname,
 | |
| 		port:     port,
 | |
| 		password: password,
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| type connector struct {
 | |
| 	hostname string
 | |
| 	port     string
 | |
| 	password string
 | |
| }
 | |
| 
 | |
| func (c *connector) Connect() (Connection, error) {
 | |
| 	p, err := strconv.Atoi(c.port)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	rcon, err := rcon.NewClient(c.hostname, p)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	_, err = rcon.Authorize(c.password)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return &connection{rcon: rcon}, nil
 | |
| }
 | |
| 
 | |
| func NewClient(connector Connector) (*client, error) {
 | |
| 	return &client{connector: connector}, nil
 | |
| }
 | |
| 
 | |
| type client struct {
 | |
| 	connector Connector
 | |
| 	conn      Connection
 | |
| }
 | |
| 
 | |
| func (c *client) Connect() error {
 | |
| 	conn, err := c.connector.Connect()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	c.conn = conn
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (c *client) Players() ([]string, error) {
 | |
| 	if c.conn == nil {
 | |
| 		err := c.Connect()
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	resp, err := c.conn.Execute("/scoreboard players list")
 | |
| 	if err != nil {
 | |
| 		c.conn = nil
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	players, err := parsePlayers(resp)
 | |
| 	if err != nil {
 | |
| 		c.conn = nil
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return players, nil
 | |
| }
 | |
| 
 | |
| func (c *client) Scores(player string) ([]Score, error) {
 | |
| 	if c.conn == nil {
 | |
| 		err := c.Connect()
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	resp, err := c.conn.Execute("/scoreboard players list " + player)
 | |
| 	if err != nil {
 | |
| 		c.conn = nil
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	scores, err := parseScores(resp)
 | |
| 	if err != nil {
 | |
| 		c.conn = nil
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return scores, nil
 | |
| }
 | |
| 
 | |
| type connection struct {
 | |
| 	rcon *rcon.Client
 | |
| }
 | |
| 
 | |
| func (c *connection) Execute(command string) (string, error) {
 | |
| 	packet, err := c.rcon.Execute(command)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	return packet.Body, nil
 | |
| }
 | |
| 
 | |
| func parsePlayers(input string) ([]string, error) {
 | |
| 	parts := strings.SplitAfterN(input, ":", 2)
 | |
| 	if len(parts) != 2 {
 | |
| 		return []string{}, nil
 | |
| 	}
 | |
| 
 | |
| 	names := strings.Split(parts[1], ",")
 | |
| 
 | |
| 	// Detect Minecraft <= 1.12
 | |
| 	if strings.Contains(parts[0], "players on the scoreboard") && len(names) > 0 {
 | |
| 		// Split the last two player names: ex: "notch and dinnerbone"
 | |
| 		head := names[:len(names)-1]
 | |
| 		tail := names[len(names)-1]
 | |
| 		names = append(head, strings.SplitN(tail, " and ", 2)...)
 | |
| 	}
 | |
| 
 | |
| 	var players []string
 | |
| 	for _, name := range names {
 | |
| 		name := strings.TrimSpace(name)
 | |
| 		if name == "" {
 | |
| 			continue
 | |
| 		}
 | |
| 		players = append(players, name)
 | |
| 
 | |
| 	}
 | |
| 	return players, nil
 | |
| }
 | |
| 
 | |
| // Score is an individual tracked scoreboard stat.
 | |
| type Score struct {
 | |
| 	Name  string
 | |
| 	Value int64
 | |
| }
 | |
| 
 | |
| func parseScores(input string) ([]Score, error) {
 | |
| 	if strings.Contains(input, "has no scores") {
 | |
| 		return []Score{}, nil
 | |
| 	}
 | |
| 
 | |
| 	// Detect Minecraft <= 1.12
 | |
| 	var re *regexp.Regexp
 | |
| 	if strings.Contains(input, "tracked objective") {
 | |
| 		re = scoreboardRegexLegacy
 | |
| 	} else {
 | |
| 		re = scoreboardRegex
 | |
| 	}
 | |
| 
 | |
| 	var scores []Score
 | |
| 	matches := re.FindAllStringSubmatch(input, -1)
 | |
| 	for _, match := range matches {
 | |
| 		score := Score{}
 | |
| 		for i, subexp := range re.SubexpNames() {
 | |
| 			switch subexp {
 | |
| 			case "name":
 | |
| 				score.Name = match[i]
 | |
| 			case "value":
 | |
| 				value, err := strconv.ParseInt(match[i], 10, 64)
 | |
| 				if err != nil {
 | |
| 					continue
 | |
| 				}
 | |
| 				score.Value = value
 | |
| 			default:
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 		scores = append(scores, score)
 | |
| 	}
 | |
| 	return scores, nil
 | |
| }
 |