mirror of
https://github.com/taigrr/wtf
synced 2025-01-18 04:03:14 -08:00
Merge branch 'add_pihole' of github.com:jonhadfield/wtf into jonhadfield-add_pihole
This commit is contained in:
commit
daf1e61335
@ -46,6 +46,7 @@ import (
|
|||||||
"github.com/wtfutil/wtf/modules/newrelic"
|
"github.com/wtfutil/wtf/modules/newrelic"
|
||||||
"github.com/wtfutil/wtf/modules/opsgenie"
|
"github.com/wtfutil/wtf/modules/opsgenie"
|
||||||
"github.com/wtfutil/wtf/modules/pagerduty"
|
"github.com/wtfutil/wtf/modules/pagerduty"
|
||||||
|
"github.com/wtfutil/wtf/modules/pihole"
|
||||||
"github.com/wtfutil/wtf/modules/pocket"
|
"github.com/wtfutil/wtf/modules/pocket"
|
||||||
"github.com/wtfutil/wtf/modules/power"
|
"github.com/wtfutil/wtf/modules/power"
|
||||||
"github.com/wtfutil/wtf/modules/resourceusage"
|
"github.com/wtfutil/wtf/modules/resourceusage"
|
||||||
@ -226,6 +227,9 @@ func MakeWidget(
|
|||||||
case "pagerduty":
|
case "pagerduty":
|
||||||
settings := pagerduty.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
settings := pagerduty.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
||||||
widget = pagerduty.NewWidget(app, settings)
|
widget = pagerduty.NewWidget(app, settings)
|
||||||
|
case "pihole":
|
||||||
|
settings := pihole.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
||||||
|
widget = pihole.NewWidget(app, pages, settings)
|
||||||
case "power":
|
case "power":
|
||||||
settings := power.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
settings := power.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
||||||
widget = power.NewWidget(app, settings)
|
widget = power.NewWidget(app, settings)
|
||||||
|
363
modules/pihole/client.go
Normal file
363
modules/pihole/client.go
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
package pihole
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
url2 "net/url"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Status struct {
|
||||||
|
DomainsBeingBlocked string `json:"domains_being_blocked"`
|
||||||
|
DNSQueriesToday string `json:"dns_queries_today"`
|
||||||
|
AdsBlockedToday string `json:"ads_blocked_today"`
|
||||||
|
AdsPercentageToday string `json:"ads_percentage_today"`
|
||||||
|
UniqueDomains string `json:"unique_domains"`
|
||||||
|
QueriesForwarded string `json:"queries_forwarded"`
|
||||||
|
QueriesCached string `json:"queries_cached"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
GravityLastUpdated struct {
|
||||||
|
Relative struct {
|
||||||
|
Days string `json:"days"`
|
||||||
|
Hours string `json:"hours"`
|
||||||
|
Minutes string `json:"minutes"`
|
||||||
|
}
|
||||||
|
} `json:"gravity_last_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStatus(c http.Client, apiURL string) (status Status, err error) {
|
||||||
|
var req *http.Request
|
||||||
|
|
||||||
|
var url *url2.URL
|
||||||
|
|
||||||
|
if url, err = url2.Parse(apiURL); err != nil {
|
||||||
|
return status, fmt.Errorf(" failed to parse API URL\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var query url2.Values
|
||||||
|
|
||||||
|
if query, err = url2.ParseQuery(url.RawQuery); err != nil {
|
||||||
|
return status, fmt.Errorf(" failed to parse query\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Add("summary", "")
|
||||||
|
|
||||||
|
url.RawQuery = query.Encode()
|
||||||
|
if req, err = http.NewRequest("GET", url.String(), nil); err != nil {
|
||||||
|
return status, fmt.Errorf(" failed to create request\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp *http.Response
|
||||||
|
|
||||||
|
if resp, err = c.Do(req); err != nil || resp == nil {
|
||||||
|
return status, fmt.Errorf(" failed to connect to Pi-hole server\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode >= http.StatusBadRequest {
|
||||||
|
return status, fmt.Errorf(" failed to retrieve version from Pi-hole server\n http status code: %d",
|
||||||
|
resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rBody []byte
|
||||||
|
|
||||||
|
if rBody, err = ioutil.ReadAll(resp.Body); err != nil {
|
||||||
|
return status, fmt.Errorf(" failed to read status response\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(rBody, &status); err != nil {
|
||||||
|
return status, fmt.Errorf(" failed to retrieve top items: check provided api URL and token\n %s\n\n",
|
||||||
|
parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return status, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type TopItems struct {
|
||||||
|
TopQueries map[string]int `json:"top_queries"`
|
||||||
|
TopAds map[string]int `json:"top_ads"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTopItems(c http.Client, settings *Settings) (ti TopItems, err error) {
|
||||||
|
var req *http.Request
|
||||||
|
|
||||||
|
var url *url2.URL
|
||||||
|
|
||||||
|
if url, err = url2.Parse(settings.apiUrl); err != nil {
|
||||||
|
return ti, fmt.Errorf(" failed to parse API URL\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var query url2.Values
|
||||||
|
|
||||||
|
if query, err = url2.ParseQuery(url.RawQuery); err != nil {
|
||||||
|
return ti, fmt.Errorf(" failed to parse query\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Add("auth", settings.token)
|
||||||
|
query.Add("topItems", strconv.Itoa(settings.showTopItems))
|
||||||
|
|
||||||
|
url.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
req, err = http.NewRequest("GET", url.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return ti, fmt.Errorf(" failed to create request\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp *http.Response
|
||||||
|
|
||||||
|
if resp, err = c.Do(req); err != nil || resp == nil {
|
||||||
|
return ti, fmt.Errorf(" failed to connect to Pi-hole server\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode >= http.StatusBadRequest {
|
||||||
|
return ti, fmt.Errorf(" failed to retrieve version from Pi-hole server\n http status code: %d",
|
||||||
|
resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rBody []byte
|
||||||
|
|
||||||
|
rBody, err = ioutil.ReadAll(resp.Body)
|
||||||
|
if err = json.Unmarshal(rBody, &ti); err != nil {
|
||||||
|
return ti, fmt.Errorf(" failed to retrieve top items: check provided api URL and token\n %s\n\n",
|
||||||
|
parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ti, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type TopClients struct {
|
||||||
|
TopSources map[string]int `json:"top_sources"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseError removes any token from output and ensures a non-nil response
|
||||||
|
func parseError(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return "unknown error"
|
||||||
|
}
|
||||||
|
|
||||||
|
var re = regexp.MustCompile(`auth=[a-zA-Z0-9]*`)
|
||||||
|
|
||||||
|
return re.ReplaceAllString(err.Error(), "auth=<token>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTopClients(c http.Client, settings *Settings) (tc TopClients, err error) {
|
||||||
|
var req *http.Request
|
||||||
|
|
||||||
|
var url *url2.URL
|
||||||
|
|
||||||
|
if url, err = url2.Parse(settings.apiUrl); err != nil {
|
||||||
|
return tc, fmt.Errorf(" failed to parse API URL\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var query url2.Values
|
||||||
|
|
||||||
|
if query, err = url2.ParseQuery(url.RawQuery); err != nil {
|
||||||
|
return tc, fmt.Errorf(" failed to parse query\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Add("topClients", strconv.Itoa(settings.showTopClients))
|
||||||
|
query.Add("auth", settings.token)
|
||||||
|
url.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
if req, err = http.NewRequest("GET", url.String(), nil); err != nil {
|
||||||
|
return tc, fmt.Errorf(" failed to create request\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp *http.Response
|
||||||
|
|
||||||
|
if resp, err = c.Do(req); err != nil || resp == nil {
|
||||||
|
return tc, fmt.Errorf(" failed to connect to Pi-hole server\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode >= http.StatusBadRequest {
|
||||||
|
return tc, fmt.Errorf(" failed to retrieve version from Pi-hole server\n http status code: %d",
|
||||||
|
resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rBody []byte
|
||||||
|
|
||||||
|
if rBody, err = ioutil.ReadAll(resp.Body); err != nil {
|
||||||
|
return tc, fmt.Errorf(" failed to read top clients response\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(rBody, &tc); err != nil {
|
||||||
|
return tc, fmt.Errorf(" failed to retrieve top clients: check provided api URL and token\n %s\n\n",
|
||||||
|
parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryTypes struct {
|
||||||
|
QueryTypes map[string]float32 `json:"querytypes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getQueryTypes(c http.Client, settings *Settings) (qt QueryTypes, err error) {
|
||||||
|
var req *http.Request
|
||||||
|
|
||||||
|
var url *url2.URL
|
||||||
|
|
||||||
|
if url, err = url2.Parse(settings.apiUrl); err != nil {
|
||||||
|
return qt, fmt.Errorf(" failed to parse API URL\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var query url2.Values
|
||||||
|
|
||||||
|
if query, err = url2.ParseQuery(url.RawQuery); err != nil {
|
||||||
|
return qt, fmt.Errorf(" failed to parse query\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Add("getQueryTypes", strconv.Itoa(settings.showTopClients))
|
||||||
|
query.Add("auth", settings.token)
|
||||||
|
|
||||||
|
url.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
if req, err = http.NewRequest("GET", url.String(), nil); err != nil {
|
||||||
|
return qt, fmt.Errorf(" failed to create request\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp *http.Response
|
||||||
|
|
||||||
|
if resp, err = c.Do(req); err != nil || resp == nil {
|
||||||
|
return qt, fmt.Errorf(" failed to connect to Pi-hole server\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode >= http.StatusBadRequest {
|
||||||
|
return qt, fmt.Errorf(" failed to retrieve version from Pi-hole server\n http status code: %d",
|
||||||
|
resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rBody []byte
|
||||||
|
|
||||||
|
if rBody, err = ioutil.ReadAll(resp.Body); err != nil {
|
||||||
|
return qt, fmt.Errorf(" failed to read top clients response\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(rBody, &qt); err != nil {
|
||||||
|
return qt, fmt.Errorf(" failed to parse query types response\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return qt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkServer(c http.Client, apiURL string) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var req *http.Request
|
||||||
|
|
||||||
|
var url *url2.URL
|
||||||
|
|
||||||
|
if url, err = url2.Parse(apiURL); err != nil {
|
||||||
|
return fmt.Errorf(" failed to parse API URL\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if url.Host == "" {
|
||||||
|
return fmt.Errorf(" please specify 'apiUrl' in Pi-hole settings, e.g.\n apiUrl: http://<server>:<port>/admin/api.php")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req, err = http.NewRequest("GET", fmt.Sprintf("%s?version",
|
||||||
|
url.String()), nil); err != nil {
|
||||||
|
return fmt.Errorf("invalid request: %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp *http.Response
|
||||||
|
|
||||||
|
if resp, err = c.Do(req); err != nil {
|
||||||
|
return fmt.Errorf(" failed to connect to Pi-hole server\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode >= http.StatusBadRequest {
|
||||||
|
return fmt.Errorf(" failed to retrieve version from Pi-hole server\n http status code: %d",
|
||||||
|
resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var vResp struct {
|
||||||
|
Version int `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var rBody []byte
|
||||||
|
|
||||||
|
if rBody, err = ioutil.ReadAll(resp.Body); err != nil {
|
||||||
|
return fmt.Errorf(" Pi-hole server failed to respond\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(rBody, &vResp); err != nil {
|
||||||
|
return fmt.Errorf(" invalid response returned from Pi-hole Server\n %s\n", parseError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if vResp.Version != 3 {
|
||||||
|
return fmt.Errorf(" only Pi-hole API version 3 is supported\n version %d was detected", vResp.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *Widget) adblockSwitch(action string) {
|
||||||
|
var req *http.Request
|
||||||
|
|
||||||
|
var url *url2.URL
|
||||||
|
url, _ = url2.Parse(widget.settings.apiUrl)
|
||||||
|
|
||||||
|
var query url2.Values
|
||||||
|
query, _ = url2.ParseQuery(url.RawQuery)
|
||||||
|
|
||||||
|
query.Add(strings.ToLower(action), "")
|
||||||
|
query.Add("auth", widget.settings.token)
|
||||||
|
|
||||||
|
url.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
req, _ = http.NewRequest("GET", url.String(), nil)
|
||||||
|
|
||||||
|
c := getClient()
|
||||||
|
resp, _ := c.Do(req)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
widget.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClient() http.Client {
|
||||||
|
return http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSHandshakeTimeout: 5 * time.Second,
|
||||||
|
DisableKeepAlives: false,
|
||||||
|
DisableCompression: false,
|
||||||
|
ResponseHeaderTimeout: 20 * time.Second,
|
||||||
|
},
|
||||||
|
Timeout: 21 * time.Second,
|
||||||
|
}
|
||||||
|
}
|
8
modules/pihole/keyboard.go
Normal file
8
modules/pihole/keyboard.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package pihole
|
||||||
|
|
||||||
|
func (widget *Widget) initializeKeyboardControls() {
|
||||||
|
widget.InitializeCommonControls(widget.Refresh)
|
||||||
|
|
||||||
|
widget.SetKeyboardChar("d", widget.disable, "disable Pi-hole")
|
||||||
|
widget.SetKeyboardChar("e", widget.enable, "enable Pi-hole")
|
||||||
|
}
|
38
modules/pihole/settings.go
Normal file
38
modules/pihole/settings.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package pihole
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/olebedev/config"
|
||||||
|
"github.com/wtfutil/wtf/cfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultFocusable = true
|
||||||
|
defaultTitle = "Pi-hole"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
common *cfg.Common
|
||||||
|
wrapText bool
|
||||||
|
apiUrl string
|
||||||
|
token string
|
||||||
|
showTopItems int
|
||||||
|
showTopClients int
|
||||||
|
maxClientWidth int
|
||||||
|
maxDomainWidth int
|
||||||
|
showSummary bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {
|
||||||
|
settings := Settings{
|
||||||
|
common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),
|
||||||
|
apiUrl: ymlConfig.UString("apiUrl"),
|
||||||
|
token: ymlConfig.UString("token"),
|
||||||
|
showSummary: ymlConfig.UBool("showSummary", true),
|
||||||
|
showTopItems: ymlConfig.UInt("showTopItems", 5),
|
||||||
|
showTopClients: ymlConfig.UInt("showTopClients", 5),
|
||||||
|
maxClientWidth: ymlConfig.UInt("maxClientWidth", 20),
|
||||||
|
maxDomainWidth: ymlConfig.UInt("maxDomainWidth", 20),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &settings
|
||||||
|
}
|
230
modules/pihole/view.go
Normal file
230
modules/pihole/view.go
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
package pihole
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/olekukonko/tablewriter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getSummaryView(c http.Client, settings *Settings) string {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var s Status
|
||||||
|
|
||||||
|
s, err = getStatus(c, settings.apiUrl)
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
switch strings.ToLower(s.Status) {
|
||||||
|
case "disabled":
|
||||||
|
sb.WriteString(" [white]Status [red]DISABLED\n")
|
||||||
|
case "enabled":
|
||||||
|
sb.WriteString(" [white]Status [green]ENABLED\n")
|
||||||
|
default:
|
||||||
|
sb.WriteString(" [white]Status [yellow]UNKNOWN\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryTable := createTable([]string{}, buf)
|
||||||
|
summaryTable.Append([]string{"Domain blocklist", s.DomainsBeingBlocked, "Queries today", s.DNSQueriesToday})
|
||||||
|
summaryTable.Append([]string{"Ads blocked today", fmt.Sprintf("%s (%s%%)", s.AdsBlockedToday, s.AdsPercentageToday), "Cached queries", s.QueriesCached})
|
||||||
|
summaryTable.Append([]string{"Blocklist Age", fmt.Sprintf("%sd %sh %sm", s.GravityLastUpdated.Relative.Days,
|
||||||
|
s.GravityLastUpdated.Relative.Hours, s.GravityLastUpdated.Relative.Minutes), "Forwarded queries", s.QueriesForwarded})
|
||||||
|
summaryTable.Render()
|
||||||
|
|
||||||
|
sb.WriteString(buf.String())
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTopItemsView(c http.Client, settings *Settings) string {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var ti TopItems
|
||||||
|
|
||||||
|
ti, err = getTopItems(c, settings)
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
tiTable := createTable([]string{"Top Queries", "", "Top Ads", ""}, buf)
|
||||||
|
|
||||||
|
largest := len(ti.TopAds)
|
||||||
|
if len(ti.TopQueries) > largest {
|
||||||
|
largest = len(ti.TopQueries)
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedTiQueries := sortMapByIntVal(ti.TopQueries)
|
||||||
|
|
||||||
|
sortedTiAds := sortMapByIntVal(ti.TopAds)
|
||||||
|
|
||||||
|
for x := 0; x < largest; x++ {
|
||||||
|
tiQVal := []string{"", ""}
|
||||||
|
if len(sortedTiQueries) > x {
|
||||||
|
tiQVal = []string{shorten(sortedTiQueries[x][0], settings.maxDomainWidth), sortedTiQueries[x][1]}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiAVal := []string{"", ""}
|
||||||
|
|
||||||
|
if len(sortedTiAds) > x {
|
||||||
|
tiAVal = []string{shorten(sortedTiAds[x][0], settings.maxDomainWidth), sortedTiAds[x][1]}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiTable.Append([]string{tiQVal[0], tiQVal[1], tiAVal[0], tiAVal[1]})
|
||||||
|
}
|
||||||
|
|
||||||
|
tiTable.Render()
|
||||||
|
sb.WriteString(buf.String())
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTopClientsView(c http.Client, settings *Settings) string {
|
||||||
|
tc, err := getTopClients(c, settings)
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
var tq QueryTypes
|
||||||
|
|
||||||
|
tq, err = getQueryTypes(c, settings)
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
tcTable := createTable([]string{"Top Clients", "", "Top Query Types", ""}, buf)
|
||||||
|
|
||||||
|
sortedTcQueries := sortMapByIntVal(tc.TopSources)
|
||||||
|
|
||||||
|
sortedTopQT := sortMapByFloatVal(tq.QueryTypes)
|
||||||
|
|
||||||
|
largest := len(tc.TopSources)
|
||||||
|
|
||||||
|
if len(tq.QueryTypes) > largest {
|
||||||
|
largest = len(tq.QueryTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.showTopClients < largest {
|
||||||
|
largest = settings.showTopClients
|
||||||
|
}
|
||||||
|
|
||||||
|
for x := 0; x < largest; x++ {
|
||||||
|
tcVal := []string{"", ""}
|
||||||
|
|
||||||
|
if len(sortedTcQueries) > x {
|
||||||
|
tcVal = []string{sortedTcQueries[x][0], sortedTcQueries[x][1]}
|
||||||
|
}
|
||||||
|
|
||||||
|
tqtVal := []string{"", ""}
|
||||||
|
|
||||||
|
if len(sortedTopQT) > x && sortedTopQT[x][0] != "" {
|
||||||
|
tqtVal = []string{sortedTopQT[x][0], sortedTopQT[x][1] + "%"}
|
||||||
|
}
|
||||||
|
|
||||||
|
tcTable.Append([]string{tcVal[0], tcVal[1], tqtVal[0], tqtVal[1]})
|
||||||
|
}
|
||||||
|
|
||||||
|
tcTable.Render()
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
sb.WriteString(buf.String())
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func shorten(s string, limit int) string {
|
||||||
|
if len(s) > limit {
|
||||||
|
return s[:limit] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTable(header []string, buf io.Writer) *tablewriter.Table {
|
||||||
|
table := tablewriter.NewWriter(buf)
|
||||||
|
|
||||||
|
if len(header) != 0 {
|
||||||
|
table.SetHeader(header)
|
||||||
|
table.SetHeaderLine(false)
|
||||||
|
table.SetHeaderAlignment(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.SetAutoWrapText(false)
|
||||||
|
table.SetAutoFormatHeaders(true)
|
||||||
|
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||||
|
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||||
|
table.SetBorder(true)
|
||||||
|
table.SetCenterSeparator("")
|
||||||
|
table.SetColumnSeparator("")
|
||||||
|
table.SetRowSeparator("")
|
||||||
|
table.SetTablePadding(" ")
|
||||||
|
table.SetNoWhiteSpace(false)
|
||||||
|
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortMapByIntVal(m map[string]int) (sorted [][]string) {
|
||||||
|
type kv struct {
|
||||||
|
Key string
|
||||||
|
Value int
|
||||||
|
}
|
||||||
|
|
||||||
|
ss := make([]kv, len(m))
|
||||||
|
for k, v := range m {
|
||||||
|
ss = append(ss, kv{k, v})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(ss, func(i, j int) bool {
|
||||||
|
return ss[i].Value > ss[j].Value
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, kv := range ss {
|
||||||
|
sorted = append(sorted, []string{kv.Key, strconv.Itoa(kv.Value)})
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortMapByFloatVal(m map[string]float32) (sorted [][]string) {
|
||||||
|
type kv struct {
|
||||||
|
Key string
|
||||||
|
Value float32
|
||||||
|
}
|
||||||
|
|
||||||
|
ss := make([]kv, len(m))
|
||||||
|
|
||||||
|
for k, v := range m {
|
||||||
|
if k == "" || v == 0.00 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ss = append(ss, kv{k, v})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(ss, func(i, j int) bool {
|
||||||
|
return ss[i].Value > ss[j].Value
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, kv := range ss {
|
||||||
|
sorted = append(sorted, []string{kv.Key, fmt.Sprintf("%.2f", kv.Value)})
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
89
modules/pihole/widget.go
Normal file
89
modules/pihole/widget.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package pihole
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rivo/tview"
|
||||||
|
"github.com/wtfutil/wtf/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Widget struct {
|
||||||
|
view.KeyboardWidget
|
||||||
|
view.MultiSourceWidget
|
||||||
|
view.TextWidget
|
||||||
|
|
||||||
|
settings *Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWidget creates a new instance of a widget
|
||||||
|
//func NewWidget(app *tview.Application, settings *Settings) *Widget {
|
||||||
|
func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *Widget {
|
||||||
|
widget := Widget{
|
||||||
|
KeyboardWidget: view.NewKeyboardWidget(app, pages, settings.common),
|
||||||
|
TextWidget: view.NewTextWidget(app, settings.common),
|
||||||
|
settings: settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.settings.common.RefreshInterval = 30
|
||||||
|
widget.View.SetInputCapture(widget.InputCapture)
|
||||||
|
widget.initializeKeyboardControls()
|
||||||
|
widget.SetDisplayFunction(widget.Refresh)
|
||||||
|
widget.View.SetWordWrap(true)
|
||||||
|
widget.View.SetWrap(settings.wrapText)
|
||||||
|
|
||||||
|
widget.KeyboardWidget.SetView(widget.View)
|
||||||
|
|
||||||
|
return &widget
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------- Exported Functions -------------------- */
|
||||||
|
|
||||||
|
func (widget *Widget) Refresh() {
|
||||||
|
if widget.Disabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.Redraw(widget.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *Widget) HelpText() string {
|
||||||
|
return widget.KeyboardWidget.HelpText()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------- Unexported Functions -------------------- */
|
||||||
|
|
||||||
|
func (widget *Widget) content() (string, string, bool) {
|
||||||
|
title := widget.CommonSettings().Title
|
||||||
|
|
||||||
|
c := getClient()
|
||||||
|
|
||||||
|
if err := checkServer(c, widget.settings.apiUrl); err != nil {
|
||||||
|
return title, err.Error(), widget.settings.wrapText
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
if widget.settings.showSummary {
|
||||||
|
sb.WriteString(getSummaryView(c, widget.settings))
|
||||||
|
}
|
||||||
|
|
||||||
|
if widget.settings.showTopItems > 0 {
|
||||||
|
sb.WriteString(getTopItemsView(c, widget.settings))
|
||||||
|
}
|
||||||
|
|
||||||
|
if widget.settings.showTopClients > 0 {
|
||||||
|
sb.WriteString(getTopClientsView(c, widget.settings))
|
||||||
|
}
|
||||||
|
|
||||||
|
output := sb.String()
|
||||||
|
|
||||||
|
return title, output, widget.settings.wrapText
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *Widget) disable() {
|
||||||
|
widget.adblockSwitch("disable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *Widget) enable() {
|
||||||
|
widget.adblockSwitch("enable")
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user