added backbone
This commit is contained in:
parent
5783a922b0
commit
fe74c466fb
24 changed files with 790 additions and 0 deletions
40
config/config.go
Normal file
40
config/config.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Port uint16
|
||||||
|
DataService DataService
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataService struct {
|
||||||
|
IP string
|
||||||
|
Port uint16
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Database string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() (*Config, error) {
|
||||||
|
viper.SetConfigType("yaml")
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
viper.SetConfigName("config")
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaults()
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
err := viper.Unmarshal(&config)
|
||||||
|
|
||||||
|
return &config, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDefaults() {
|
||||||
|
viper.SetDefault("Port", "8080")
|
||||||
|
viper.SetDefault("DataService.IP", "127.0.0.1")
|
||||||
|
viper.SetDefault("DataService.Port", "27017")
|
||||||
|
}
|
14
features/api/channel/channel.go
Executable file
14
features/api/channel/channel.go
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Channel(id int) (*Channel, error)
|
||||||
|
Channels() ([]*Channel, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Channel struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Subchannels []Channel `json:"subchannels,omitempty"`
|
||||||
|
TotalClients int `json:"totalClients"`
|
||||||
|
NeededSubscribePower int `json:"neededSubscribePower"`
|
||||||
|
}
|
40
features/api/channel/handler.go
Executable file
40
features/api/channel/handler.go
Executable file
|
@ -0,0 +1,40 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/Cliffbreak/tsviewer/response"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ChannelAPIHandler(s Service) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response.Handler(w, r, response.HandlerFunc(func() (int, error) {
|
||||||
|
id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := s.Channel(int(id))
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusNotFound, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.New(c, r).Send(w, http.StatusOK)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChannelsAPIHandler(s Service) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response.Handler(w, r, response.HandlerFunc(func() (int, error) {
|
||||||
|
cc, err := s.Channels()
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.New(cc, r).Send(w, http.StatusOK)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
14
features/api/channel/routes.go
Executable file
14
features/api/channel/routes.go
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func APIRoutes(s Service) *chi.Mux {
|
||||||
|
router := chi.NewRouter()
|
||||||
|
|
||||||
|
router.Get("/{id}", ChannelAPIHandler(s))
|
||||||
|
router.Get("/", ChannelsAPIHandler(s))
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
15
features/api/client/client.go
Executable file
15
features/api/client/client.go
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Client(id int) (*Client, error)
|
||||||
|
Clients() ([]*Client, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
DatabaseID int `json:"databaseId"`
|
||||||
|
ChannelID int `json:"channelId"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Type int `json:"type"`
|
||||||
|
Away bool `json:"away"`
|
||||||
|
AwayMessage string `json:"awayMessage"`
|
||||||
|
}
|
40
features/api/client/handler.go
Executable file
40
features/api/client/handler.go
Executable file
|
@ -0,0 +1,40 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/Cliffbreak/tsviewer/response"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ClientAPIHandler(s Service) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response.Handler(w, r, response.HandlerFunc(func() (int, error) {
|
||||||
|
id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := s.Client(int(id))
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusNotFound, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.New(c, r).Send(w, http.StatusOK)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClientsAPIHandler(s Service) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response.Handler(w, r, response.HandlerFunc(func() (int, error) {
|
||||||
|
cc, err := s.Clients()
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.New(cc, r).Send(w, http.StatusOK)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
14
features/api/client/routes.go
Executable file
14
features/api/client/routes.go
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func APIRoutes(s Service) *chi.Mux {
|
||||||
|
router := chi.NewRouter()
|
||||||
|
|
||||||
|
router.Get("/{id}", ClientAPIHandler(s))
|
||||||
|
router.Get("/", ClientsAPIHandler(s))
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
20
features/api/server/handler.go
Executable file
20
features/api/server/handler.go
Executable file
|
@ -0,0 +1,20 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/Cliffbreak/tsviewer/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InfoAPIHandler(s Service) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response.Handler(w, r, response.HandlerFunc(func() (int, error) {
|
||||||
|
s, err := s.ServerInfo()
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusNotFound, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.New(s, r).Send(w, http.StatusOK)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
13
features/api/server/routes.go
Executable file
13
features/api/server/routes.go
Executable file
|
@ -0,0 +1,13 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func APIRoutes(s Service) *chi.Mux {
|
||||||
|
router := chi.NewRouter()
|
||||||
|
|
||||||
|
router.Get("/info", InfoAPIHandler(s))
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
22
features/api/server/server.go
Executable file
22
features/api/server/server.go
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
ServerInfo() (*Server, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
WelcomeMessage string `json:"welcomeMessage"`
|
||||||
|
MaxClients int `json:"maxClients"`
|
||||||
|
ClientsOnline int `json:"clientsOnline"`
|
||||||
|
ReservedSlots int `json:"reservedSlots"`
|
||||||
|
Uptime time.Duration `json:"uptime"`
|
||||||
|
TotalPing float32 `json:"totalPing"`
|
||||||
|
MinAndroidVersion int `json:"minAndroidVersion"`
|
||||||
|
MinClientVersion int `json:"minClientVersion"`
|
||||||
|
MiniOSVersion int `json:"miniOSVersion"`
|
||||||
|
}
|
32
features/web/index/handler.go
Executable file
32
features/web/index/handler.go
Executable file
|
@ -0,0 +1,32 @@
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/Cliffbreak/tsviewer/features/web/weberror"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IndexGUIHandler(s Service, t template.Template) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
server, err := s.ServerInfo()
|
||||||
|
if err != nil {
|
||||||
|
weberror.NewPage(err, http.StatusNotFound).Send(w, t)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channels, err := s.ChannelsRaw()
|
||||||
|
if err != nil {
|
||||||
|
weberror.NewPage(err, http.StatusNotFound).Send(w, t)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clients, err := s.Clients()
|
||||||
|
if err != nil {
|
||||||
|
weberror.NewPage(err, http.StatusNotFound).Send(w, t)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
NewPage(*server, channels, clients).Send(w, t)
|
||||||
|
})
|
||||||
|
}
|
35
features/web/index/page.go
Executable file
35
features/web/index/page.go
Executable file
|
@ -0,0 +1,35 @@
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/Cliffbreak/tsviewer/features/api/channel"
|
||||||
|
"git.cliffbreak.de/Cliffbreak/tsviewer/features/api/client"
|
||||||
|
"git.cliffbreak.de/Cliffbreak/tsviewer/features/api/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
ServerInfo() (*server.Server, error)
|
||||||
|
Clients() ([]*client.Client, error)
|
||||||
|
Channels() ([]*channel.Channel, error)
|
||||||
|
ChannelsRaw() ([]*channel.Channel, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexPage struct {
|
||||||
|
Server server.Server
|
||||||
|
Channels []*channel.Channel
|
||||||
|
Clients []*client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPage(server server.Server, channels []*channel.Channel, clients []*client.Client) *IndexPage {
|
||||||
|
return &IndexPage{
|
||||||
|
Server: server,
|
||||||
|
Channels: channels,
|
||||||
|
Clients: clients,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (page IndexPage) Send(w io.Writer, t template.Template) {
|
||||||
|
t.Lookup("index.html").Execute(w, page)
|
||||||
|
}
|
15
features/web/index/routes.go
Executable file
15
features/web/index/routes.go
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GUIRoutes(s Service, t template.Template) *chi.Mux {
|
||||||
|
router := chi.NewRouter()
|
||||||
|
|
||||||
|
router.Get("/", IndexGUIHandler(s, t))
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
22
features/web/weberror/page.go
Executable file
22
features/web/weberror/page.go
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
package weberror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrorPage struct {
|
||||||
|
Error error
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPage(err error, statusCode int) *ErrorPage {
|
||||||
|
return &ErrorPage{
|
||||||
|
Error: err,
|
||||||
|
StatusCode: statusCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (page ErrorPage) Send(w io.Writer, t template.Template) {
|
||||||
|
t.Lookup("error.html").Execute(w, page)
|
||||||
|
}
|
35
gui/template.go
Executable file
35
gui/template.go
Executable file
|
@ -0,0 +1,35 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadTemplates() (*template.Template, error) {
|
||||||
|
return loadTemplates("templates/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTemplates(path string) (*template.Template, error) {
|
||||||
|
dir, err := ioutil.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ff []string
|
||||||
|
|
||||||
|
for _, file := range dir {
|
||||||
|
filename := file.Name()
|
||||||
|
if strings.HasSuffix(filename, ".html") {
|
||||||
|
ff = append(ff, fmt.Sprintf("%s%s", path, filename))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templates, err := template.ParseFiles(ff...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates, nil
|
||||||
|
}
|
81
main.go
Normal file
81
main.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/Cliffbreak/tsviewer/config"
|
||||||
|
"git.cliffbreak.de/Cliffbreak/tsviewer/features/api/channel"
|
||||||
|
"git.cliffbreak.de/Cliffbreak/tsviewer/features/api/client"
|
||||||
|
"git.cliffbreak.de/Cliffbreak/tsviewer/features/api/server"
|
||||||
|
"git.cliffbreak.de/Cliffbreak/tsviewer/features/web/index"
|
||||||
|
"git.cliffbreak.de/Cliffbreak/tsviewer/gui"
|
||||||
|
"git.cliffbreak.de/Cliffbreak/tsviewer/service"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
"github.com/go-chi/chi/middleware"
|
||||||
|
"github.com/go-chi/cors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Routes(s service.Service, t template.Template) *chi.Mux {
|
||||||
|
router := chi.NewRouter()
|
||||||
|
cors := cors.New(cors.Options{
|
||||||
|
AllowedOrigins: []string{"*"},
|
||||||
|
AllowedMethods: []string{"Get"},
|
||||||
|
})
|
||||||
|
router.Use(
|
||||||
|
middleware.Logger,
|
||||||
|
middleware.Timeout(5*time.Second),
|
||||||
|
middleware.DefaultCompress,
|
||||||
|
middleware.RedirectSlashes,
|
||||||
|
middleware.Recoverer,
|
||||||
|
|
||||||
|
cors.Handler,
|
||||||
|
)
|
||||||
|
|
||||||
|
router.Route("/v1", func(r chi.Router) {
|
||||||
|
r.Mount("/channels", channel.APIRoutes(s))
|
||||||
|
r.Mount("/clients", client.APIRoutes(s))
|
||||||
|
r.Mount("/server", server.APIRoutes(s))
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Route("/", func(r chi.Router) {
|
||||||
|
r.Mount("/", index.GUIRoutes(s, t))
|
||||||
|
})
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Println("Loading configurations...")
|
||||||
|
config, err := config.New()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("Configurations loaded!")
|
||||||
|
|
||||||
|
log.Println("Starting query service...")
|
||||||
|
service, err := service.New(*config)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer service.TSClient.Close()
|
||||||
|
log.Println("Query service connected to", fmt.Sprintf("%s:%d!", config.ServerTS.IP, config.ServerTS.PortQuery))
|
||||||
|
|
||||||
|
log.Println("Loading templates...")
|
||||||
|
templates, err := gui.LoadTemplates()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("All templates loaded!")
|
||||||
|
|
||||||
|
router := Routes(*service, *templates)
|
||||||
|
router.Get("/static/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.StripPrefix("/static", http.FileServer(http.Dir("./static"))).ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Println("Starting the web server locally on port", config.ServerWeb.Port)
|
||||||
|
log.Fatal("Handler: ", http.ListenAndServe(fmt.Sprintf(":%d", config.ServerWeb.Port), router))
|
||||||
|
}
|
36
request/meta.go
Executable file
36
request/meta.go
Executable file
|
@ -0,0 +1,36 @@
|
||||||
|
package meta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Meta struct {
|
||||||
|
Pretty bool `json:"pretty"`
|
||||||
|
Envelope bool `json:"envelope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseFromRequest(r *http.Request) *Meta {
|
||||||
|
meta := Meta{
|
||||||
|
Pretty: false,
|
||||||
|
Envelope: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
pretty, ok := r.URL.Query()["pretty"]
|
||||||
|
if ok && len(pretty) > 0 {
|
||||||
|
prettyBool, err := strconv.ParseBool(pretty[0])
|
||||||
|
if err == nil {
|
||||||
|
meta.Pretty = prettyBool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
envelope, ok := r.URL.Query()["envelope"]
|
||||||
|
if ok && len(envelope) > 0 {
|
||||||
|
envelopeBool, err := strconv.ParseBool(envelope[0])
|
||||||
|
if err == nil {
|
||||||
|
meta.Envelope = envelopeBool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &meta
|
||||||
|
}
|
19
response/error.go
Executable file
19
response/error.go
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
package response
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Error data type
|
||||||
|
type Error struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewError creates a new Response fill with an error
|
||||||
|
func NewError(status int, err error) *Error {
|
||||||
|
return &Error{
|
||||||
|
Status: status,
|
||||||
|
Error: err.Error(),
|
||||||
|
Timestamp: time.Now().Format("2006-01-02T15:04:05Z"), //.Format("02 Jan 2006, 15:04:05 MST"),
|
||||||
|
}
|
||||||
|
}
|
87
response/response.go
Executable file
87
response/response.go
Executable file
|
@ -0,0 +1,87 @@
|
||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/Cliffbreak/tsviewer/request/meta"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HandlerFunc func() (int, error)
|
||||||
|
|
||||||
|
// Responder is a service for responses
|
||||||
|
type Responder interface {
|
||||||
|
Send(http.ResponseWriter, int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response data type
|
||||||
|
type Response struct {
|
||||||
|
Content interface{}
|
||||||
|
Pretty bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Envelope struct {
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Response
|
||||||
|
func New(content interface{}, r *http.Request) *Response {
|
||||||
|
var resp Response
|
||||||
|
|
||||||
|
meta := meta.NewFromRequest(r)
|
||||||
|
|
||||||
|
if meta.Envelope {
|
||||||
|
resp = Response{
|
||||||
|
Content: Envelope{
|
||||||
|
Data: content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resp = Response{
|
||||||
|
Content: content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Pretty = meta.Pretty
|
||||||
|
|
||||||
|
return &resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send sends the response
|
||||||
|
func (resp Response) Send(w http.ResponseWriter, status int) (int, error) {
|
||||||
|
var (
|
||||||
|
rawJSON []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if resp.Pretty {
|
||||||
|
rawJSON, err = json.MarshalIndent(resp.Content, "", " ")
|
||||||
|
} else {
|
||||||
|
rawJSON, err = json.Marshal(resp.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
} else if string(rawJSON) == "null" {
|
||||||
|
rawJSON = make([]byte, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
w.Write(rawJSON)
|
||||||
|
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Handler(w http.ResponseWriter, r *http.Request, hf HandlerFunc) {
|
||||||
|
status, err := hf()
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("HTTP %d: %q", status, err)
|
||||||
|
if status, err = New(NewError(status, err), r).Send(w, status); err != nil {
|
||||||
|
http.Error(w, http.StatusText(status), status)
|
||||||
|
}
|
||||||
|
}
|
69
service/crud.go
Normal file
69
service/crud.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/haveachin/sawtexapi/service/mongodb/filter"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s DataService) Create(v interface{}) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result, err := s.collection(reflect.TypeOf(v)).InsertOne(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflect.ValueOf(v).Kind() == reflect.Ptr {
|
||||||
|
reflect.ValueOf(v).Elem().FieldByName("ID").Set(reflect.ValueOf(result.InsertedID))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s DataService) Query(v interface{}, filter *filter.Filter, multiQuery bool) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if !multiQuery {
|
||||||
|
if err := s.collection(reflect.TypeOf(v)).FindOne(ctx, filter).Decode(v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor, err := s.collection(reflect.TypeOf(v)).Find(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursor.All(ctx, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s DataService) Update(v interface{}, filter *filter.Filter) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := s.collection(reflect.TypeOf(v)).UpdateOne(ctx, filter, bson.M{"$set": v})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s DataService) Delete(v interface{}, filter *filter.Filter) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if _, err := s.collection(reflect.TypeOf(v)).DeleteOne(ctx, filter); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
61
service/service.go
Normal file
61
service/service.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/config"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataService struct {
|
||||||
|
client *mongo.Client
|
||||||
|
db *mongo.Database
|
||||||
|
collections map[reflect.Type]*mongo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ds config.DataService) (*DataService, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
connectionString := fmt.Sprintf("mongodb://%s:%s@%s:%d/?authSource=admin", url.QueryEscape(ds.Username), url.QueryEscape(ds.Password), ds.IP, ds.Port)
|
||||||
|
client, err := mongo.Connect(ctx, options.Client().ApplyURI(connectionString))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Ping(ctx, readpref.Primary()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DataService{
|
||||||
|
client: client,
|
||||||
|
db: client.Database(ds.Database),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s DataService) collection(t reflect.Type) *mongo.Collection {
|
||||||
|
for t.Kind() != reflect.Struct {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
collection, ok := s.collections[t]
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("No collection registered for %T", t))
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s DataService) RegisterCollection(t reflect.Type, collection string) {
|
||||||
|
for t.Kind() != reflect.Struct {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.collections[t] = s.db.Collection(collection)
|
||||||
|
}
|
22
static/styles.css
Executable file
22
static/styles.css
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
*{
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2{
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
small{
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.client{
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
11
templates/error.html
Executable file
11
templates/error.html
Executable file
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Error</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Oops, an error occurred!</h1>
|
||||||
|
<p1>Error {{.StatusCode}}</p1>
|
||||||
|
<p1>{{.Error}}</p1>
|
||||||
|
</body>
|
||||||
|
</html>
|
33
templates/index.html
Executable file
33
templates/index.html
Executable file
|
@ -0,0 +1,33 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>TS3 Viewer</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="static/styles.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="content">
|
||||||
|
<h2>Online Members</h2>
|
||||||
|
<ul>
|
||||||
|
{{range .Clients}}
|
||||||
|
<li>{{.Nickname}}</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
<h2>{{.Server.Name}}</h2>
|
||||||
|
<!-- <small>{{.Server.Version}}</small> -->
|
||||||
|
{{$clients := .Clients}}
|
||||||
|
{{range .Channels}}
|
||||||
|
<p class="channel">{{.Name}}</p>
|
||||||
|
{{$channelId := .ID}}
|
||||||
|
{{range $clients}}
|
||||||
|
{{if eq $channelId .ChannelID}}
|
||||||
|
<p class="client">{{.Nickname}}</p>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Reference in a new issue