Clean project layout and fix release workflow
Some checks failed
build-windows-exe / build (push) Failing after 1m13s
Some checks failed
build-windows-exe / build (push) Failing after 1m13s
This commit is contained in:
214
internal/app/join_leave_events.go
Normal file
214
internal/app/join_leave_events.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"vrc_osc_go/internal/common"
|
||||
)
|
||||
|
||||
const joinLeaveEventsFileName = "join_leave_events.json"
|
||||
|
||||
var joinLeaveEventLinePattern = regexp.MustCompile(`^\[(join|leave)\]\s+(.+?)\s+\((\d+)\)$`)
|
||||
|
||||
type JoinLeaveEvent struct {
|
||||
At time.Time `json:"at"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Count int `json:"count,omitempty"`
|
||||
}
|
||||
|
||||
type joinLeaveEventsSnapshot struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Events []JoinLeaveEvent `json:"events"`
|
||||
}
|
||||
|
||||
type JoinLeaveEventTracker struct {
|
||||
mu sync.RWMutex
|
||||
events []JoinLeaveEvent
|
||||
}
|
||||
|
||||
var joinLeaveEventTracker *JoinLeaveEventTracker
|
||||
|
||||
func InitJoinLeaveEventTracker(t *JoinLeaveEventTracker) {
|
||||
joinLeaveEventTracker = t
|
||||
}
|
||||
|
||||
func GetJoinLeaveEventTracker() *JoinLeaveEventTracker {
|
||||
return joinLeaveEventTracker
|
||||
}
|
||||
|
||||
func NewJoinLeaveEventTracker() *JoinLeaveEventTracker {
|
||||
t := &JoinLeaveEventTracker{}
|
||||
_ = t.load()
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *JoinLeaveEventTracker) load() error {
|
||||
dir := filepath.Join(common.RootDir(), "runtime")
|
||||
path := filepath.Join(dir, joinLeaveEventsFileName)
|
||||
b, err := os.ReadFile(path)
|
||||
if err == nil && len(b) > 0 {
|
||||
if events, ok := decodeJoinLeaveEventsSnapshot(b); ok {
|
||||
t.events = dedupeJoinLeaveEvents(events)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
events, err := loadJoinLeaveEventsFromTextLog(filepath.Join(dir, "join_leave.log"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.events = dedupeJoinLeaveEvents(events)
|
||||
if len(t.events) > 0 {
|
||||
_ = t.persistLocked()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeJoinLeaveEventsSnapshot(b []byte) ([]JoinLeaveEvent, bool) {
|
||||
var snap joinLeaveEventsSnapshot
|
||||
if err := json.Unmarshal(b, &snap); err == nil && len(snap.Events) > 0 {
|
||||
return snap.Events, true
|
||||
}
|
||||
var events []JoinLeaveEvent
|
||||
if err := json.Unmarshal(b, &events); err == nil {
|
||||
return events, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func loadJoinLeaveEventsFromTextLog(path string) ([]JoinLeaveEvent, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil || len(b) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
events := make([]JoinLeaveEvent, 0, 256)
|
||||
var currentAt time.Time
|
||||
for _, raw := range strings.Split(string(b), "\n") {
|
||||
line := strings.TrimSpace(strings.TrimRight(raw, "\r"))
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(line, "VRC JOIN/LEAVE") {
|
||||
if len(line) >= 21 {
|
||||
if at, err := time.Parse("2006-01-02 15:04:05", line[1:20]); err == nil {
|
||||
currentAt = at
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
m := joinLeaveEventLinePattern.FindStringSubmatch(line)
|
||||
if len(m) != 4 {
|
||||
continue
|
||||
}
|
||||
count, _ := strconv.Atoi(m[3])
|
||||
events = append(events, JoinLeaveEvent{
|
||||
At: currentAt,
|
||||
Kind: m[1],
|
||||
Name: strings.TrimSpace(m[2]),
|
||||
Count: count,
|
||||
})
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (t *JoinLeaveEventTracker) Record(kind, name string, count int, at time.Time) {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
kind = strings.TrimSpace(kind)
|
||||
name = strings.TrimSpace(name)
|
||||
if kind != "join" && kind != "leave" {
|
||||
return
|
||||
}
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
if at.IsZero() {
|
||||
at = time.Now()
|
||||
}
|
||||
ev := JoinLeaveEvent{At: at, Kind: kind, Name: name, Count: count}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if containsJoinLeaveEvent(t.events, ev) {
|
||||
return
|
||||
}
|
||||
t.events = append(t.events, ev)
|
||||
_ = t.persistLocked()
|
||||
}
|
||||
|
||||
func (t *JoinLeaveEventTracker) Snapshot() []JoinLeaveEvent {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
out := append([]JoinLeaveEvent(nil), t.events...)
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
if out[i].At.Equal(out[j].At) {
|
||||
if out[i].Kind == out[j].Kind {
|
||||
return out[i].Name < out[j].Name
|
||||
}
|
||||
return out[i].Kind < out[j].Kind
|
||||
}
|
||||
return out[i].At.Before(out[j].At)
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func (t *JoinLeaveEventTracker) persistLocked() error {
|
||||
dir := filepath.Join(common.RootDir(), "runtime")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Create(filepath.Join(dir, joinLeaveEventsFileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
enc := json.NewEncoder(f)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(joinLeaveEventsSnapshot{
|
||||
UpdatedAt: time.Now(),
|
||||
Events: append([]JoinLeaveEvent(nil), t.events...),
|
||||
})
|
||||
}
|
||||
|
||||
func dedupeJoinLeaveEvents(events []JoinLeaveEvent) []JoinLeaveEvent {
|
||||
if len(events) <= 1 {
|
||||
return events
|
||||
}
|
||||
seen := make(map[string]struct{}, len(events))
|
||||
out := make([]JoinLeaveEvent, 0, len(events))
|
||||
for _, ev := range events {
|
||||
key := joinLeaveEventKey(ev)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, ev)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func containsJoinLeaveEvent(events []JoinLeaveEvent, candidate JoinLeaveEvent) bool {
|
||||
key := joinLeaveEventKey(candidate)
|
||||
for _, ev := range events {
|
||||
if joinLeaveEventKey(ev) == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func joinLeaveEventKey(ev JoinLeaveEvent) string {
|
||||
return ev.At.UTC().Format(time.RFC3339Nano) + "|" + ev.Kind + "|" + ev.Name
|
||||
}
|
||||
Reference in New Issue
Block a user