2023-03-14 22:24:26 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2023-03-15 21:29:26 +00:00
|
|
|
"fmt"
|
2024-01-06 11:24:28 +00:00
|
|
|
"io"
|
2023-03-14 22:24:26 +00:00
|
|
|
"net/http"
|
2023-03-15 21:29:26 +00:00
|
|
|
"os"
|
2023-03-19 10:39:57 +00:00
|
|
|
"strconv"
|
2023-05-13 11:43:11 +01:00
|
|
|
"strings"
|
2023-03-14 22:24:26 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/gocolly/colly/v2"
|
|
|
|
)
|
2023-03-15 16:16:40 +00:00
|
|
|
|
2023-03-14 22:24:26 +00:00
|
|
|
/* Store the calls locally */
|
|
|
|
var (
|
2023-07-14 21:23:13 +01:00
|
|
|
monitor Monitor
|
2023-03-14 22:24:26 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type Call struct {
|
|
|
|
Num string `json:"num"`
|
|
|
|
Date string `json:"date"`
|
2023-03-19 10:39:57 +00:00
|
|
|
Name string `json:"name"`
|
2023-03-14 22:24:26 +00:00
|
|
|
Call string `json:"call"`
|
2023-03-15 21:34:05 +00:00
|
|
|
Id string `json:"id"`
|
|
|
|
Sec string `json:"sec"`
|
2023-03-14 22:24:26 +00:00
|
|
|
Slot string `json:"slot"`
|
|
|
|
Talkgroup string `json:"talkgroup"`
|
|
|
|
}
|
|
|
|
|
2023-07-14 21:23:13 +01:00
|
|
|
/* Monitor data */
|
|
|
|
type Monitor struct {
|
|
|
|
Calls []Call
|
|
|
|
Config Config
|
|
|
|
Cache_stale bool
|
|
|
|
Last_access time.Time
|
|
|
|
Mu sync.Mutex
|
|
|
|
Stats Stats
|
|
|
|
Uptime time.Time
|
|
|
|
Users Users
|
|
|
|
User_name map[string]string
|
|
|
|
User_update time.Time
|
|
|
|
}
|
|
|
|
|
2023-03-20 20:44:31 +00:00
|
|
|
/* Store API Stats */
|
|
|
|
type Stats struct {
|
2023-07-14 21:23:13 +01:00
|
|
|
Cache bool `json:"stale_cache"`
|
2023-03-20 20:44:31 +00:00
|
|
|
Hits uint64 `json:"hits"`
|
|
|
|
Refresh uint64 `json:"refresh"`
|
|
|
|
Uptime uint64 `json:"uptime"`
|
|
|
|
}
|
|
|
|
|
2023-03-19 10:39:57 +00:00
|
|
|
/*
|
|
|
|
User data from radioid.net database
|
|
|
|
*/
|
|
|
|
type Users struct {
|
|
|
|
Users []struct {
|
|
|
|
First_name string `json:"fname"`
|
|
|
|
Name string `json:"name"`
|
|
|
|
Country string `json:"country"`
|
|
|
|
Callsign string `json:"callsign"`
|
|
|
|
City string `json:"city"`
|
|
|
|
Surname string `json:"surname"`
|
|
|
|
Radio_id uint32 `json:"radio_id"`
|
|
|
|
Id int `json:"id"`
|
|
|
|
State string `json:"state"`
|
|
|
|
} `json:"users"`
|
|
|
|
}
|
|
|
|
|
2023-03-15 21:29:26 +00:00
|
|
|
type Config struct {
|
2023-03-22 10:57:59 +00:00
|
|
|
Last_access time.Duration `json:"last_access"`
|
|
|
|
Page string `json:"page"`
|
2023-03-22 11:04:31 +00:00
|
|
|
Reload time.Duration `json:"reload"`
|
2023-03-22 10:57:59 +00:00
|
|
|
Users string `json:"users"`
|
|
|
|
Users_reload int64 `json:"users_reload"`
|
2023-03-19 10:39:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Pull data from radioid.net user dump */
|
|
|
|
func nameLookup(config *Config) {
|
|
|
|
/* Only update cache if timer has expired or we have no data
|
2024-01-06 11:31:03 +00:00
|
|
|
if we fail just silently return */
|
2023-07-14 21:23:13 +01:00
|
|
|
if time.Since(monitor.User_update) >= time.Second*time.Duration(config.Users_reload) || len(monitor.Users.Users) == 0 {
|
|
|
|
monitor.User_update = time.Now()
|
2023-03-19 10:39:57 +00:00
|
|
|
client := &http.Client{}
|
|
|
|
reqs, err := http.NewRequest("GET", config.Users, nil)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
reqs.Header.Add("Content-Type", "application/json")
|
|
|
|
resp, err := client.Do(reqs)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
if resp.StatusCode != 200 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-06 11:24:28 +00:00
|
|
|
body, err := io.ReadAll(resp.Body)
|
2023-03-19 10:39:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
2023-08-10 08:00:10 +01:00
|
|
|
if err := json.Unmarshal(body, &monitor.Users); err != nil {
|
2023-03-19 10:39:57 +00:00
|
|
|
fmt.Println(err)
|
|
|
|
}
|
|
|
|
|
2023-07-14 21:23:13 +01:00
|
|
|
for _, u := range monitor.Users.Users {
|
|
|
|
monitor.User_name[strconv.Itoa(u.Id)] = u.Name
|
2023-03-19 10:39:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2023-03-15 21:29:26 +00:00
|
|
|
}
|
|
|
|
|
2023-03-15 16:16:40 +00:00
|
|
|
func req(w http.ResponseWriter, r *http.Request) {
|
2023-03-14 22:24:26 +00:00
|
|
|
w.Header().Add("Content-Type", "application/json")
|
2023-03-15 16:16:40 +00:00
|
|
|
|
2023-07-14 21:23:13 +01:00
|
|
|
monitor.Mu.Lock()
|
2023-07-14 21:31:32 +01:00
|
|
|
monitor.Last_access = time.Now()
|
2023-07-14 21:23:13 +01:00
|
|
|
monitor.Stats.Hits++
|
|
|
|
|
|
|
|
/* Wait for new data */
|
|
|
|
if monitor.Cache_stale {
|
|
|
|
for {
|
|
|
|
monitor.Mu.Unlock()
|
|
|
|
time.Sleep(time.Millisecond * 10)
|
|
|
|
monitor.Mu.Lock()
|
|
|
|
if !monitor.Cache_stale {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
json.NewEncoder(w).Encode(monitor.Calls)
|
|
|
|
monitor.Mu.Unlock()
|
2023-03-14 22:24:26 +00:00
|
|
|
}
|
|
|
|
|
2023-03-20 20:44:31 +00:00
|
|
|
func getStats(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.Header().Add("Content-Type", "application/json")
|
|
|
|
|
2023-07-14 21:23:13 +01:00
|
|
|
monitor.Mu.Lock()
|
|
|
|
monitor.Stats.Uptime = uint64(time.Since(monitor.Uptime)) / 100_000_000_0
|
|
|
|
json.NewEncoder(w).Encode(monitor.Stats)
|
|
|
|
monitor.Mu.Unlock()
|
2023-03-20 20:44:31 +00:00
|
|
|
}
|
|
|
|
|
2023-03-15 16:16:40 +00:00
|
|
|
func serv() {
|
2023-03-14 22:24:26 +00:00
|
|
|
srv := &http.Server{
|
2023-03-15 16:16:40 +00:00
|
|
|
Addr: ":8181",
|
2023-03-14 22:24:26 +00:00
|
|
|
}
|
|
|
|
http.HandleFunc("/monitor", req)
|
2023-03-20 20:44:31 +00:00
|
|
|
http.HandleFunc("/monitor/stats", getStats)
|
2023-03-14 22:24:26 +00:00
|
|
|
srv.ListenAndServe()
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Scrape the dashboard */
|
2024-01-06 11:31:03 +00:00
|
|
|
func scrape(config *Config, monitor *Monitor) {
|
2023-03-14 22:24:26 +00:00
|
|
|
var new_calls []Call
|
|
|
|
c := colly.NewCollector()
|
2023-03-19 10:39:57 +00:00
|
|
|
nameLookup(config)
|
2023-03-14 22:24:26 +00:00
|
|
|
c.OnHTML("table > tbody", func(h *colly.HTMLElement) {
|
|
|
|
h.ForEach("tr", func(_ int, el *colly.HTMLElement) {
|
2023-03-15 16:16:40 +00:00
|
|
|
if el.ChildText("td:nth-child(1)") != "" {
|
2023-03-15 21:34:05 +00:00
|
|
|
this_call := Call{Num: el.ChildText("td:nth-child(1)"), Date: el.ChildText("td:nth-child(3)"), Call: el.ChildText("td:nth-child(8)"), Id: el.ChildText("td:nth-child(7)"), Sec: el.ChildText("td:nth-child(4)"), Slot: el.ChildText("td:nth-child(10)"), Talkgroup: el.ChildText("td:nth-child(11)")}
|
2023-07-14 21:23:13 +01:00
|
|
|
this_call.Name = monitor.User_name[this_call.Id]
|
2023-03-14 22:24:26 +00:00
|
|
|
new_calls = append(new_calls, this_call)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2023-03-15 21:29:26 +00:00
|
|
|
c.Visit(config.Page)
|
2024-01-06 11:31:03 +00:00
|
|
|
monitor.Mu.Lock()
|
|
|
|
monitor.Calls = new_calls
|
|
|
|
monitor.Stats.Refresh++
|
|
|
|
monitor.Mu.Unlock()
|
2023-03-14 22:24:26 +00:00
|
|
|
}
|
|
|
|
|
2023-07-14 21:23:13 +01:00
|
|
|
func (config *Monitor) updateCheck() bool {
|
|
|
|
monitor.Mu.Lock()
|
|
|
|
monitor.Cache_stale = time.Since(monitor.Last_access) >= time.Minute*monitor.Config.Last_access
|
|
|
|
monitor.Stats.Cache = monitor.Cache_stale
|
|
|
|
monitor.Mu.Unlock()
|
|
|
|
return monitor.Cache_stale
|
|
|
|
}
|
|
|
|
|
2023-03-14 22:24:26 +00:00
|
|
|
func main() {
|
2023-05-13 11:43:11 +01:00
|
|
|
cFileL := "./dvsmon.conf"
|
|
|
|
|
|
|
|
if len(os.Args) > 2 {
|
|
|
|
if !strings.ContainsAny(os.Args[1], "\\!@#$%^&*():;'\"|<>?") {
|
|
|
|
cFileL = os.Args[1]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
cFile, err := os.ReadFile(cFileL)
|
2023-03-15 21:29:26 +00:00
|
|
|
if err != nil {
|
|
|
|
fmt.Println("Can't open config file! Expecting .dvsmon.conf: ", err)
|
|
|
|
os.Exit(-1)
|
|
|
|
}
|
|
|
|
|
2023-07-14 21:23:13 +01:00
|
|
|
if err := json.Unmarshal(cFile, &monitor.Config); err != nil {
|
2023-03-15 21:29:26 +00:00
|
|
|
fmt.Println("Trouble parsing config file: ", err)
|
|
|
|
}
|
|
|
|
|
2023-07-14 21:23:13 +01:00
|
|
|
monitor.Last_access = time.Now()
|
2023-03-14 22:24:26 +00:00
|
|
|
last_update := time.Now()
|
2023-07-14 21:23:13 +01:00
|
|
|
monitor.Uptime = time.Now()
|
|
|
|
monitor.User_update = time.Now()
|
|
|
|
monitor.User_name = make(map[string]string)
|
2023-03-15 21:29:26 +00:00
|
|
|
/* Serve the API service */
|
2023-03-14 22:24:26 +00:00
|
|
|
go serv()
|
|
|
|
|
|
|
|
for {
|
2023-03-22 10:57:59 +00:00
|
|
|
time.Sleep(time.Millisecond * 256)
|
|
|
|
|
|
|
|
/* If we're idle don't scrape */
|
2023-07-14 21:23:13 +01:00
|
|
|
if monitor.updateCheck() {
|
2023-03-22 10:57:59 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-07-14 21:23:13 +01:00
|
|
|
if time.Since(last_update) >= time.Second*monitor.Config.Reload {
|
2023-03-14 22:24:26 +00:00
|
|
|
last_update = time.Now()
|
2024-01-06 11:31:03 +00:00
|
|
|
go scrape(&monitor.Config, &monitor)
|
2023-03-14 22:24:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|