1
0
mirror of https://github.com/taigrr/wtf synced 2025-01-18 04:03:14 -08:00

add pihole module.

This commit is contained in:
Jon Hadfield 2020-04-13 17:52:59 +01:00
parent cb0b2f19cf
commit 72ae0ccd8b
No known key found for this signature in database
GPG Key ID: 2E2498DC47D1874C
8 changed files with 733 additions and 17 deletions

View File

@ -46,6 +46,7 @@ import (
"github.com/wtfutil/wtf/modules/newrelic"
"github.com/wtfutil/wtf/modules/opsgenie"
"github.com/wtfutil/wtf/modules/pagerduty"
"github.com/wtfutil/wtf/modules/pihole"
"github.com/wtfutil/wtf/modules/pocket"
"github.com/wtfutil/wtf/modules/power"
"github.com/wtfutil/wtf/modules/resourceusage"
@ -226,6 +227,9 @@ func MakeWidget(
case "pagerduty":
settings := pagerduty.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = pagerduty.NewWidget(app, settings)
case "pihole":
settings := pihole.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = pihole.NewWidget(app, pages, settings)
case "power":
settings := power.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = power.NewWidget(app, settings)

1
go.mod
View File

@ -30,7 +30,6 @@ require (
github.com/godbus/dbus v4.1.0+incompatible // indirect
github.com/google/go-github/v26 v26.1.3
github.com/gophercloud/gophercloud v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
github.com/hekmon/cunits v2.0.1+incompatible // indirect
github.com/hekmon/transmissionrpc v0.0.0-20190525133028-1d589625bacd
github.com/imdario/mergo v0.3.8 // indirect

17
go.sum
View File

@ -54,8 +54,6 @@ github.com/adlio/trello v1.7.0 h1:syLRJ27wCM8URf7zOBWGr981cG+dpmLSyMqjEoQc+4g=
github.com/adlio/trello v1.7.0/go.mod h1:l2068AhUuUuQ9Vsb95ECMueHThYyAj4e85lWPmr2/LE=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.7.1 h1:G1i02OhUbRi2nJxcNkwJaY/J1gHXj9tt72qN6ZouLFQ=
github.com/alecthomas/chroma v0.7.1/go.mod h1:gHw09mkX1Qp80JlYbmN9L3+4R5o6DJJ3GRShh+AICNc=
github.com/alecthomas/chroma v0.7.2 h1:B76NU/zbQYIUhUowbi4fmvREmDUJLsUzKWTZmQd3ABY=
github.com/alecthomas/chroma v0.7.2/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
@ -132,10 +130,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/digitalocean/godo v1.32.0 h1:ljfhYi/IqDYiZcBYV7nUPzw1Q7NlmPSFDtI69UeRThk=
github.com/digitalocean/godo v1.32.0/go.mod h1:iJnN9rVu6K5LioLxLimlq0uRI+y/eAQjROUmeU/r0hY=
github.com/digitalocean/godo v1.33.1 h1:W5e7EgW8EVOM+ycZ6z+LU/WTZrxohIkgxbVwLOU/Q6s=
github.com/digitalocean/godo v1.33.1/go.mod h1:gfLm3JSupWD9V/ibQygXWW3IVz7hranzckH5UimhZsI=
github.com/digitalocean/godo v1.34.0 h1:OXJhLLJS2VTB5SziTyCq8valKVZ0uBHCFQsDW3/HF78=
github.com/digitalocean/godo v1.34.0/go.mod h1:gfLm3JSupWD9V/ibQygXWW3IVz7hranzckH5UimhZsI=
github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg=
@ -301,6 +295,7 @@ github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FK
github.com/hashicorp/go-cleanhttp v0.0.0-20160407174126-ad28ea4487f0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
@ -473,8 +468,6 @@ github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/ncw/swift v0.0.0-20171019114456-c95c6e5c2d1a h1:SAjW6pL/9NssyKM1Qvyy5/V4kR3z76qlTbaqJLixhP4=
github.com/ncw/swift v0.0.0-20171019114456-c95c6e5c2d1a/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
github.com/nicklaw5/helix v0.5.7 h1:DvNyoKkuLYrqZv5/yugL18Ud99UeQoXzzAsg4OwU8uY=
github.com/nicklaw5/helix v0.5.7/go.mod h1:nRcok4VLg8ONQYW/iXBZ24wcfiJjTlDbhgk0ZatOrUY=
github.com/nicklaw5/helix v0.5.8 h1:RG1vV/XDI6Kc0V/KvoUzRb3Q/7rmAQvVuisfxxYg1ZY=
github.com/nicklaw5/helix v0.5.8/go.mod h1:nRcok4VLg8ONQYW/iXBZ24wcfiJjTlDbhgk0ZatOrUY=
github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
@ -591,8 +584,6 @@ github.com/sethgrid/pester v0.0.0-20171127025028-760f8913c048/go.mod h1:Ad7IjTpv
github.com/sguiheux/go-coverage v0.0.0-20190710153556-287b082a7197 h1:qu90yDtRE5WEfRT5mn9v0Xz9RaopLguhbPwZKx4dHq8=
github.com/sguiheux/go-coverage v0.0.0-20190710153556-287b082a7197/go.mod h1:0hhKrsUsoT7yvxwNGKa+TSYNA26DNWMqReeZEQq/9FI=
github.com/shirou/gopsutil v0.0.0-20170406131756-e49a95f3d5f8/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil v2.20.2+incompatible h1:ucK79BhBpgqQxPASyS2cu9HX8cfDVljBN1WWFvbNvgY=
github.com/shirou/gopsutil v2.20.2+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil v2.20.3+incompatible h1:0JVooMPsT7A7HqEYdydp/OfjSOYSjhXV7w1hkKj/NPQ=
github.com/shirou/gopsutil v2.20.3+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
@ -653,10 +644,6 @@ github.com/wtfutil/spotigopher v0.0.0-20191127141047-7d8168fe103a/go.mod h1:AlO4
github.com/wtfutil/todoist v0.0.2-0.20191216004217-0ec29ceda61a h1:nD8ALd4TSo+zPHK5MqQWFj01G8fMMHFfC3rWvoq/9JA=
github.com/wtfutil/todoist v0.0.2-0.20191216004217-0ec29ceda61a/go.mod h1:YuuGLJSsTK6DGBD5Zaf3J8LSMfpEC2WtzYPey3XVOdI=
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
github.com/xanzy/go-gitlab v0.28.0 h1:nsyjDVvBrP4KRXEN4b1m1ewiqmTNL4BOWW041nKGV7k=
github.com/xanzy/go-gitlab v0.28.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og=
github.com/xanzy/go-gitlab v0.29.0 h1:9tMvAkG746eIlzcdpnRgpcKPA1woUDmldMIjR/E5OWM=
github.com/xanzy/go-gitlab v0.29.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug=
github.com/xanzy/go-gitlab v0.31.0 h1:+nHztQuCXGSMluKe5Q9IRaPdz6tO8O0gMkQ0vqGpiBk=
github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
@ -801,8 +788,6 @@ google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMt
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.21.0 h1:zS+Q/CJJnVlXpXQVIz+lH0ZT2lBuT2ac7XD8Y/3w6hY=
google.golang.org/api v0.21.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=

363
modules/pihole/client.go Normal file
View 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,
}
}

View 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")
}

View 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
View 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
View 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")
}