Compare commits

..

5 Commits

Author SHA1 Message Date
dd00e06a27 add option to only parse last raid or full log 2025-04-26 20:35:56 -07:00
fd18343b84 bugfix 2025-04-13 13:39:56 -07:00
b3014466ac build 2025-04-07 14:04:30 -07:00
f197492cf4 LICENSE 2025-03-12 12:14:41 -07:00
715b0277e0 README 2025-03-12 12:14:20 -07:00
9 changed files with 237 additions and 36 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
WoWChatLog.txt
.idea
config.toml
testloot.csv
*.csv
gamboupload

13
LICENSE Normal file
View File

@ -0,0 +1,13 @@
Copyright 2021 Evan Burkey <evan@burkey.co>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

10
Makefile Normal file
View File

@ -0,0 +1,10 @@
all: build
build:
go build
install: build
cp gamboupload /home/evan/.local/bin/gamboupload
clean:
rm gamboupload

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# gamboupload
Tool to upload gambos and RCLootCouncil exports.
## Setup
You need an API key to upload. Talk to Evan for one

View File

@ -8,6 +8,7 @@ import (
"net/http"
"os"
"strings"
"time"
)
func gambo() {
@ -21,20 +22,63 @@ func gambo() {
lines := strings.Split(string(b), "\n")
games, err := parseGames(lines)
fmt.Println("Parse entire log or just the last raid's entries?")
fmt.Println("1) Last raid")
fmt.Println("2) Full log")
var (
start int
choice int
)
_, err = fmt.Scanf("%d", &choice)
if err != nil {
log.Fatal(err)
}
switch choice {
case 1:
start = findStart(lines)
case 2:
start = 0
default:
log.Fatalf("%d is not a valid choice, retard\n", choice)
}
games, err := parseGames(lines, start)
if err != nil {
log.Fatal(err)
}
for _, game := range games {
//err = uploadGambo(game, "https://forcek.in/game")
err = uploadGamboTest(game)
err = uploadGambo(game, "https://forcek.in/game")
//err = uploadGamboTest(game)
if err != nil {
log.Fatal(err)
}
}
}
func findStart(lines []string) int {
now := time.Now()
t := time.Date(now.Year(), now.Month(), now.Day(), 15, 45, 0, 0, time.UTC)
for t.Weekday() != time.Sunday {
t = t.AddDate(0, 0, -1)
}
// skip ending newline
var i = len(lines) - 2
for ; i >= 0; i-- {
ts, err := timestamp(lines[i])
if err != nil {
log.Fatalf("Failed to rewind log: %v\n", err)
}
if ts.Before(t) {
break
}
}
return i + 1
}
func uploadGambo(game Game, endpoint string) error {
marshalled, err := json.Marshal(game)
if err != nil {

116
loot.go
View File

@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"github.com/sqweek/dialog"
"io"
"log"
"net/http"
"os"
@ -15,6 +16,11 @@ import (
"time"
)
type LootUpload struct {
Loot []LootRecord `json:"loot"`
Players []string `json:"players"`
}
type LootRecord struct {
Name string `json:"player"`
ItemID int `json:"item_id"`
@ -24,25 +30,62 @@ type LootRecord struct {
}
func loot() {
loots, err := parseLoot()
if err != nil {
log.Fatal(err)
}
fmt.Print("Enter the matching warcraft logs report id:")
var reportID string
_, err = fmt.Scanln(&reportID)
if err != nil {
log.Fatal(err)
}
players, err := getAttendance(reportID)
if err != nil {
log.Fatal(err)
}
upload := LootUpload{
Loot: loots,
Players: players,
}
err = uploadLoot(upload, "https://forcek.in/loot")
//err = uploadLootTest(upload)
if err != nil {
log.Fatal(err)
}
}
func parseLoot() ([]LootRecord, error) {
csvPath, err := dialog.File().Title("Select the csv file").Load()
if err != nil {
if errors.Is(err, dialog.ErrCancelled) {
log.Fatalf("Cancelled dialog box, exiting")
return nil, fmt.Errorf("cancelled dialog box, exiting")
} else {
log.Fatal(err)
return nil, err
}
}
csvFile, err := os.Open(csvPath)
if err != nil {
log.Fatal(err)
return nil, err
}
defer csvFile.Close()
csvReader := csv.NewReader(csvFile)
data, err := io.ReadAll(csvFile)
if err != nil {
return nil, err
}
data = bytes.ReplaceAll(data, []byte("[\"Bullet-Proof\" Vestplate]"), []byte("[Bullet-Proof Vestplate]"))
csvReader := csv.NewReader(bytes.NewReader(data))
records, err := csvReader.ReadAll()
if err != nil {
log.Fatalf("Failed to parse csv: %v\n", err)
return nil, fmt.Errorf("failed to parse csv: %v\n", err)
}
loots := make([]LootRecord, 0)
@ -80,16 +123,61 @@ func loot() {
loots = append(loots, l)
}
for _, l := range loots {
err = uploadLoot(l, "https://forcek.in/loot")
//err = uploadLootTest(l)
if err != nil {
log.Fatal(err)
}
}
return loots, nil
}
func uploadLoot(l LootRecord, endpoint string) error {
type WarcraftLog struct {
Lang string `json:"lang"`
Fights any `json:"fights"`
CompleteRaids any `json:"complete_raids"`
Friendlies any `json:"friendlies"`
Enemies any `json:"enemies"`
FriendlyPets any `json:"friendly_pets"`
EnemyPets any `json:"enemyPets"`
LogVersion int `json:"logVersion"`
GameVersion int `json:"gameVersion"`
Phases any `json:"phases"`
Title string `json:"title"`
Owner string `json:"owner"`
Start int64 `json:"start"`
End int64 `json:"end"`
Zone int `json:"zone"`
ExportedCharacters []struct {
ID int `json:"id"`
Name string `json:"name"`
Server string `json:"server"`
Region string `json:"region"`
} `json:"exportedCharacters"`
}
func getAttendance(reportID string) ([]string, error) {
logUrl := fmt.Sprintf("https://www.warcraftlogs.com:443/v1/report/fights/%s?api_key=%s", reportID, config.WarcraftLogsApiKey)
resp, err := http.Get(logUrl)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var wlog WarcraftLog
err = json.Unmarshal(body, &wlog)
if err != nil {
return nil, err
}
players := make([]string, 0)
for _, p := range wlog.ExportedCharacters {
players = append(players, p.Name)
}
return players, nil
}
func uploadLoot(l LootUpload, endpoint string) error {
marshalled, err := json.Marshal(l)
if err != nil {
return err
@ -113,6 +201,6 @@ func uploadLoot(l LootRecord, endpoint string) error {
return nil
}
func uploadLootTest(l LootRecord) error {
func uploadLootTest(l LootUpload) error {
return uploadLoot(l, "http://localhost:3000/loot")
}

14
loot_test.go Normal file
View File

@ -0,0 +1,14 @@
package main
import "testing"
func TestWarcraftLogs(t *testing.T) {
setupConfig()
players, err := getAttendance("VFy1BaAJ2pjKG8N6")
if err != nil {
t.Fatal(err)
}
if len(players) != 17 {
t.Fatalf("Wrong number of players: %d", len(players))
}
}

30
main.go
View File

@ -7,32 +7,50 @@ import (
"github.com/sqweek/dialog"
"log"
"os"
"path/filepath"
"runtime"
)
type Config struct {
Log string
Apikey string
WarcraftLogsApiKey string
}
var config Config
func getConfigPath() string {
if runtime.GOOS == "windows" {
return "config.toml"
}
xdg := os.Getenv("XDG_CONFIG_HOME")
if xdg == "" {
log.Fatal("$XDG_CONFIG_HOME not set")
}
os.MkdirAll(filepath.Join(xdg, "gambosite"), 0755)
return filepath.Join(xdg, "gambosite", "config.toml")
}
func setupConfig() {
if _, statErr := os.Stat("config.toml"); os.IsNotExist(statErr) {
path, err := dialog.File().Title("Select your WoWChatLog.txt").Load()
cfgPath := getConfigPath()
if _, statErr := os.Stat(cfgPath); os.IsNotExist(statErr) {
chatlogPath, err := dialog.File().Title("Select your WoWChatLog.txt").Load()
if err != nil {
if errors.Is(err, dialog.ErrCancelled) {
log.Fatalf("Cancelled dialog box, exiting")
}
} else {
log.Fatal(err)
}
newConfig := &Config{
Log: path,
Log: chatlogPath,
Apikey: "12345",
}
file, err := os.Create("config.toml")
file, err := os.Create(cfgPath)
if err != nil {
log.Fatal(err)
}
@ -44,7 +62,7 @@ func setupConfig() {
}
}
_, err := toml.DecodeFile("config.toml", &config)
_, err := toml.DecodeFile(cfgPath, &config)
if err != nil {
log.Fatal(err)
}

View File

@ -26,7 +26,7 @@ type Game struct {
}
var (
reTimeStamp = regexp.MustCompile(`(\d/\d \d+:\d+:\d+.\d+)`)
reTimeStamp = regexp.MustCompile(`(\d+/\d+ \d+:\d+:\d+\.\d+)`)
reGameStart = regexp.MustCompile(`WoWGoldGambler: A new game has been started`)
reWager = regexp.MustCompile(`Game Mode - ([A-Z]+) - Wager - ([\d,]+)g$`)
reSignup = regexp.MustCompile(`([\p{L}']+)-[\p{L}'0-9]+: (1|-1)`)
@ -34,11 +34,11 @@ var (
reEnd = regexp.MustCompile(`([\p{L}']+) owes ([\p{L}']+) ([\d,]+) gold!`)
)
func parseGames(lines []string) ([]Game, error) {
func parseGames(lines []string, start int) ([]Game, error) {
games := make([]Game, 0)
var err error
i := 0
i := start
for i < len(lines) {
if reGameStart.MatchString(lines[i]) {
var game Game
@ -54,19 +54,25 @@ func parseGames(lines []string) ([]Game, error) {
return games, nil
}
func timestamp(line string) (time.Time, error) {
ts := reTimeStamp.FindString(line)
if ts == "" {
return time.Now(), fmt.Errorf("failed to extract timestamp from %s", line)
}
stamp, err := time.Parse("1/2 15:04:05.000", ts)
if err != nil {
return time.Now(), fmt.Errorf("failed to extract timestamp from %s", line)
}
return time.Date(time.Now().Year(), stamp.Month(), stamp.Day(), stamp.Hour(), stamp.Minute(), stamp.Second(), 0, time.UTC), nil
}
func parse(lines []string, i int) (Game, int, error) {
var (
g Game
err error
)
// Timestamp
ts := reTimeStamp.FindString(lines[i])
if ts == "" {
return g, i, fmt.Errorf("failed to extract timestamp from %s", lines[i])
}
g.Timestamp, err = time.Parse("1/2 15:04:05.000", ts)
g.Timestamp = time.Date(time.Now().Year(), g.Timestamp.Month(), g.Timestamp.Day(), g.Timestamp.Hour(), g.Timestamp.Minute(), g.Timestamp.Second(), 0, time.UTC)
g.Timestamp, err = timestamp(lines[i])
if err != nil {
return g, i, err
}