Skip to content

Commit

Permalink
Merge branch 'main' into dn2_bindata
Browse files Browse the repository at this point in the history
  • Loading branch information
davidnewhall authored Jul 21, 2024
2 parents bd968d5 + 15642ba commit 27adcdf
Show file tree
Hide file tree
Showing 43 changed files with 1,133 additions and 909 deletions.
6 changes: 5 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
issues:
max-issues-per-linter: 0
max-same-issues: 0
exclude-rules:
# Exclude funlen for testing files.
- linters:
Expand Down Expand Up @@ -26,4 +28,6 @@ linters:
- depguard
- tagalign
run:
timeout: 5m
timeout: 5m
output:
sort-results: true
18 changes: 10 additions & 8 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/Notifiarr/notifiarr/pkg/ui"
)

// @title Notifiarr Client API Docs
// @title Notifiarr Client API Documentation
// @version 1.0
// @description Notifiarr Client monitors local services and sends notifications.
// @termsOfService https://notifiarr.com
Expand All @@ -27,14 +27,16 @@ func main() {
log.SetFlags(log.LstdFlags)
log.SetPrefix("[ERROR] ")

defer func() {
if r := recover(); r != nil {
log.Printf("Go Panic! %s\n%v\n%s", mnd.BugIssue, r, string(debug.Stack()))
}
}()
defer logPanic()

if err := client.Start(); err != nil {
_, _ = ui.Error(mnd.Title, err.Error())
log.Fatal(err) //nolint:gocritic // defer does not need to run if we have an error.
_, _ = ui.Error(err.Error())
defer log.Fatal(err)
}
}

func logPanic() {
if r := recover(); r != nil {
log.Printf("Go Panic! %s\n%v\n%s", mnd.BugIssue, r, string(debug.Stack()))
}
}
2 changes: 1 addition & 1 deletion pkg/bindata/templates/includes/footer.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
</div> <!-- index :: content-wrapper -->
</div> <!-- index :: ts-main-content -->
</div> <!-- index :: page-body -->
<div class="page-footer {{if not .Flags.ConfigFile}}bk-danger{{end}}">
<div class="page-footer{{if not .Flags.ConfigFile}} bk-danger{{end}}">
{{- if not .Flags.ConfigFile}}
<div style="display:none;" class="dialogText">
The application is currently running without a config file. Saving changes is disabled.
Expand Down
120 changes: 120 additions & 0 deletions pkg/checkapp/checkapp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Package checkapp provides a suite of small procedures to check integration URLs and commands.
// This is used by all the double-green check marks on the client UI.
package checkapp

import (
"context"
"errors"
"html"
"net/http"
"strconv"
"strings"

"github.com/Notifiarr/notifiarr/pkg/configfile"
"github.com/Notifiarr/notifiarr/pkg/mnd"
"github.com/Notifiarr/notifiarr/pkg/snapshot"
"github.com/gorilla/mux"
)

const (
success = "Connection Successful! Version: "
connecting = "Connecting: "
validation = "Validation: "
)

type Input struct {
Real *configfile.Config
Post *configfile.Config
Type string
Index int
}

var ErrBadIndex = errors.New("index provided has no configuration data")

func Test(orig *configfile.Config, writer http.ResponseWriter, req *http.Request) {
posted := configfile.Config{}

if err := mnd.ConfigPostDecoder.Decode(&posted, req.PostForm); err != nil {
http.Error(writer, "Decoding POST data into Go data structure failed: "+err.Error(), http.StatusBadRequest)
return
}

index, _ := strconv.Atoi(mux.Vars(req)["index"])
reply, code := testInstance(req.Context(), &Input{
Real: orig,
Post: &posted,
Type: mux.Vars(req)["type"],
Index: index,
})
http.Error(writer, html.EscapeString(reply), code)
}

//nolint:funlen,cyclop // It's really not that bad.
func testInstance(ctx context.Context, input *Input) (string, int) {
switch strings.ToLower(input.Type) {
// commands.go
case "commands":
return testCommand(ctx, input)
// downloaders.go
case "nzbget":
return checkAndRun(ctx, testNZBGet, input, input.Post.Apps, input.Post.Apps.NZBGet)
case "deluge":
return checkAndRun(ctx, testDeluge, input, input.Post.Apps, input.Post.Apps.Deluge)
case "qbit":
return checkAndRun(ctx, testQbit, input, input.Post.Apps, input.Post.Apps.Qbit)
case "rtorrent":
return checkAndRun(ctx, testRtorrent, input, input.Post.Apps, input.Post.Apps.Rtorrent)
case "transmission":
return checkAndRun(ctx, testTransmission, input, input.Post.Apps, input.Post.Apps.Transmission)
case "sabnzb":
return checkAndRun(ctx, testSabNZB, input, input.Post.Apps, input.Post.Apps.SabNZB)
// starr.go
case "lidarr":
return checkAndRun(ctx, testLidarr, input, input.Post.Apps, input.Post.Apps.Lidarr)
case "prowlarr":
return checkAndRun(ctx, testProwlarr, input, input.Post.Apps, input.Post.Apps.Prowlarr)
case "radarr":
return checkAndRun(ctx, testRadarr, input, input.Post.Apps, input.Post.Apps.Radarr)
case "readarr":
return checkAndRun(ctx, testReadarr, input, input.Post.Apps, input.Post.Apps.Readarr)
case "sonarr":
return checkAndRun(ctx, testSonarr, input, input.Post.Apps, input.Post.Apps.Sonarr)
// snapshots.go
case "mysql":
return checkAndRun(ctx, testMySQL, input, input.Post.Snapshot, input.Post.Snapshot.Plugins.MySQL)
case "nvidia":
return checkAndRun(ctx, testNvidia, input, input.Post.Snapshot,
[]*snapshot.NvidiaConfig{input.Post.Snapshot.Plugins.Nvidia}) // ad-hoc slice, index is already 0.
// services.go
case "tcp":
return checkAndRun(ctx, testTCP, input, input.Post.Service, input.Post.Service)
case "http":
return checkAndRun(ctx, testHTTP, input, input.Post.Service, input.Post.Service)
case "process":
return checkAndRun(ctx, testProcess, input, input.Post.Service, input.Post.Service)
case "ping", "icmp":
return checkAndRun(ctx, testPing, input, input.Post.Service, input.Post.Service)
// media.go
case "plex":
return testPlex(ctx, input.Post.Plex)
case "tautulli":
return testTautulli(ctx, input.Post.Apps.Tautulli)
default:
return "Unknown Check Type Requested! (" + input.Type + ")", http.StatusNotImplemented
}
}

// checkAndRun makes sure the slice length is at least as long as the index, and checks parents for nil.
func checkAndRun[D any](
ctx context.Context,
checker func(ctx context.Context, input D) (string, int),
input *Input,
parent any,
slice []D,
) (string, int) {
if parent == nil || len(slice) <= input.Index {
return ErrBadIndex.Error(), http.StatusBadRequest
}

return checker(ctx, slice[input.Index])
}
40 changes: 40 additions & 0 deletions pkg/checkapp/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package checkapp

import (
"context"
"fmt"
"net/http"

"github.com/Notifiarr/notifiarr/pkg/triggers/commands"
"github.com/Notifiarr/notifiarr/pkg/triggers/common"
"github.com/Notifiarr/notifiarr/pkg/website"
)

func testCommand(ctx context.Context, input *Input) (string, int) {
if len(input.Real.Commands) > input.Index {
input.Real.Commands[input.Index].Run(&common.ActionInput{Type: website.EventGUI})
return "Command Triggered: " + input.Real.Commands[input.Index].Name, http.StatusOK
} else if len(input.Post.Commands) > input.Index { // check POST input for "new" command.
input.Post.Commands[input.Index].Setup(input.Real.Logger, input.Real.Services.Website)

if err := input.Post.Commands[input.Index].SetupRegexpArgs(); err != nil {
return err.Error(), http.StatusInternalServerError
}

return testCustomCommand(ctx, input.Post.Commands[input.Index])
}

return ErrBadIndex.Error(), http.StatusBadRequest
}

func testCustomCommand(ctx context.Context, cmd *commands.Command) (string, int) {
ctx, cancel := context.WithTimeout(ctx, cmd.Timeout.Duration)
defer cancel()

output, err := cmd.RunNow(ctx, &common.ActionInput{Type: website.EventGUI})
if err != nil {
return fmt.Sprintf("Command Failed! Error: %v", err), http.StatusInternalServerError
}

return "Command Successful! Output: " + output, http.StatusOK
}
98 changes: 98 additions & 0 deletions pkg/checkapp/downloaders.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package checkapp

import (
"context"
"fmt"
"net/http"
"net/url"

"github.com/Notifiarr/notifiarr/pkg/apps"
"github.com/Notifiarr/notifiarr/pkg/mnd"
"github.com/hekmon/transmissionrpc/v3"
"golift.io/deluge"
"golift.io/nzbget"
"golift.io/qbit"
"golift.io/version"
)

func testQbit(ctx context.Context, config *apps.QbitConfig) (string, int) {
qbit, err := qbit.New(ctx, config.Config)
if err != nil {
return connecting + err.Error(), http.StatusBadGateway
}

xfers, err := qbit.GetXfersContext(ctx)
if err != nil {
return "Getting Transfers: " + err.Error(), http.StatusBadGateway
}

return fmt.Sprintf("Connection Successful! %d Transfers", len(xfers)), http.StatusOK
}

func testRtorrent(_ context.Context, config *apps.RtorrentConfig) (string, int) {
config.Setup(0, nil)

result, err := config.Client.Call("system.hostname")
if err != nil {
return "Getting Server Name: " + err.Error(), http.StatusBadGateway
}

if names, ok := result.([]any); ok {
result = names[0]
}

if name, ok := result.(string); ok {
return fmt.Sprintf("Connection Successful! Server name: %s", name), http.StatusOK
}

return "Getting Server Name: result was not a string?", http.StatusBadGateway
}

func testSabNZB(ctx context.Context, app *apps.SabNZBConfig) (string, int) {
app.Setup(0, nil)

sab, err := app.GetQueue(ctx)
if err != nil {
return "Getting Queue: " + err.Error(), http.StatusBadGateway
}

return success + sab.Version, http.StatusOK
}

func testNZBGet(ctx context.Context, config *apps.NZBGetConfig) (string, int) {
ver, err := nzbget.New(config.Config).VersionContext(ctx)
if err != nil {
return "Getting Version: " + err.Error(), http.StatusBadGateway
}

return fmt.Sprintf("%s%s", success, ver), http.StatusOK
}

func testDeluge(ctx context.Context, config *apps.DelugeConfig) (string, int) {
deluge, err := deluge.New(ctx, config.Config)
if err != nil {
return connecting + err.Error(), http.StatusBadGateway
}

return fmt.Sprintf("%s%s", success, deluge.Version), http.StatusOK
}

func testTransmission(ctx context.Context, config *apps.XmissionConfig) (string, int) {
endpoint, err := url.Parse(config.URL)
if err != nil {
return "parsing url: " + err.Error(), http.StatusBadGateway
} else if config.User != "" {
endpoint.User = url.UserPassword(config.User, config.Pass)
}

client, _ := transmissionrpc.New(endpoint, &transmissionrpc.Config{
UserAgent: fmt.Sprintf("%s v%s-%s %s", mnd.Title, version.Version, version.Revision, version.Branch),
})

args, err := client.SessionArgumentsGetAll(ctx)
if err != nil {
return "Getting Server Version: " + err.Error(), http.StatusBadGateway
}

return fmt.Sprintln("Transmission Server version:", *args.Version), http.StatusOK
}
31 changes: 31 additions & 0 deletions pkg/checkapp/media.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package checkapp

import (
"context"
"fmt"
"net/http"

"github.com/Notifiarr/notifiarr/pkg/apps"
)

func testPlex(ctx context.Context, app *apps.PlexConfig) (string, int) {
app.Setup(0, nil)

info, err := app.GetInfo(ctx)
if err != nil {
return "Getting Info: " + err.Error(), http.StatusBadGateway
}

return "Plex OK! Version: " + info.Version, http.StatusOK
}

func testTautulli(ctx context.Context, app *apps.TautulliConfig) (string, int) {
app.Setup(0, nil)

users, err := app.GetUsers(ctx)
if err != nil {
return "Getting Users: " + err.Error(), http.StatusBadGateway
}

return fmt.Sprintf("Tautulli OK! Users: %d", len(users.Response.Data)), http.StatusOK
}
Loading

0 comments on commit 27adcdf

Please sign in to comment.