diff --git a/.gitignore b/.gitignore index c2658d7..7a42440 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,16 @@ -node_modules/ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.bat + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Custom Blacklist +config.json \ No newline at end of file diff --git a/README.md b/README.md index 41a401f..7f05e39 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,71 @@ -# tsviewer +# go-tsviewer **[WIP]** +## **WARNING** This API is not usable ATM +A REST API made for TS3 Viewer. + +# Features +## Config +The config file is generated automatically on first startup. +```json +{ + "ip": "127.0.0.1", // Server IP + "port": 10011, // Dataquery Port + "user": { + "name": "serveradmin", // Username + "password": "" // Password + }, + "server": { + "port": 9987 // Port of the target server + } +} +``` +## URL-Parameter +| Name | Type | Description | +| ---------- | -------- | ------------------------ | +| `pretty` | `bool` | pretty-prints JSON | +| `envelope` | `bool` | wraps JSON in data array | +## Channels +- **`GET`** `/v1/channels/:id` +- **`GET`** `/v1/channels` +```json +{ + "databaseId": 1, + "channelId": 1, + "nickname": "serveradmin from 127.0.0.1:58359", + "type": 1, + "away": false, + "awayMessage": "" +} +``` +## Clients +- **`GET`** `/v1/clients/:id` +- **`GET`** `/v1/clients/` +```json +{ + "id": 1, + "subchannels": [ + ... (contains all subchannel) + ], + "name": "main1", + "totalClients": 0, + "neededSubscribePower": 0 +} +``` +## Server +- **`GET`** `/v1/server/info` +```json +{ + "name": "TeamSpeak ]I[ Server", + "status": "online", + "version": "3.5.1 [Build: 1545076855]", + "welcomeMessage": "Welcome to TeamSpeak, check [URL]www.teamspeak.com[/URL] for latest information", + "maxClients": 32, + "clientsOnline": 2, + "reservedSlots": 0, + "uptime": 5976, + "totalPing": 0, + "minAndroidVersion": 1502275280, + "minClientVersion": 1513163251, + "miniOSVersion": 1502275280 +} +``` diff --git a/app.js b/app.js deleted file mode 100644 index 73b5026..0000000 --- a/app.js +++ /dev/null @@ -1,43 +0,0 @@ -var express = require('express'); -var exphbs = require('express-handlebars'); - -var app = express(); - -app.engine('handlebars', exphbs({defaultLayout: 'main'})); -app.set('view engine', 'handlebars'); - -app.get('/', function (req, res) { - res.render('home', {foo: 'bar'}); -}); - -app.listen(3000); - -const TeamSpeak3 = require("ts3-nodejs-library") - -var ts3conn = new TeamSpeak3({ - host: "localhost", - queryport: 10011, - serverport: 9987, - username: "serveradmin", - password: "R0cHL6tb", - nickname: "NodeJS Query Framework", -}) - - ts3conn.on("ready", () => { - ts3conn.clientList({client_type:0}).then(clients => { - clients.forEach(client => { - console.log("Online: ", client.getCache().client_nickname) - }) - }).catch(e => console.log("CATCHED", e.message)) - - ts3conn.channelList({}).then(channel => { - channel.forEach(channel => { - console.log("Channel: ", channel.getCache().channel_name) - }) - }).catch(e => console.log("CATCHED", e.message)) - }) - - - - ts3conn.on("error", e => console.log("Error", e.message)) - ts3conn.on("close", e => console.log("Connection has been closed!", e)) diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..ea0acb5 --- /dev/null +++ b/config/config.go @@ -0,0 +1,69 @@ +package config + +import ( + "encoding/json" + "io/ioutil" + "log" + "os" +) + +type Config struct { + IP string `json:"ip"` + Port uint16 `json:"port"` + User User `json:"user"` + Server Server `json:"server"` +} + +type User struct { + Name string `json:"name"` + Password string `json:"password"` +} + +type Server struct { + Port uint16 `json:"port"` +} + +const FileName = "config.json" + +func New() (*Config, error) { + config := defaults() + + configFile, err := os.Open(FileName) + if err != nil { + if err := config.createFile(); err != nil { + return nil, err + } + + log.Println(config) + return &config, nil + } + defer configFile.Close() + + jsonParser := json.NewDecoder(configFile) + jsonParser.Decode(&config) + + return &config, nil +} + +func (config Config) createFile() error { + configJSON, err := json.MarshalIndent(config, "", " ") + if err != nil { + return err + } + + return ioutil.WriteFile(FileName, configJSON, 0644) +} + +func defaults() Config { + return Config{ + IP: "127.0.0.1", + Port: 10011, + User: User{ + Name: "serveradmin", + Password: "", + }, + Server: Server{ + Port: 9987, + }, + } +} diff --git a/features/channel/channel.go b/features/channel/channel.go new file mode 100644 index 0000000..cad648b --- /dev/null +++ b/features/channel/channel.go @@ -0,0 +1,14 @@ +package channel + +type Service interface { + Channel(id int) (*Channel, error) + Channels() ([]*Channel, error) +} + +type Channel struct { + ID int `json:"id"` + Subchannels []Channel `json:"subchannels,omitempty"` + Name string `json:"name"` + TotalClients int `json:"totalClients"` + NeededSubscribePower int `json:"neededSubscribePower"` +} diff --git a/features/channel/handler.go b/features/channel/handler.go new file mode 100644 index 0000000..d0e3be8 --- /dev/null +++ b/features/channel/handler.go @@ -0,0 +1,40 @@ +package channel + +import ( + "net/http" + "strconv" + + "git.cliffbreak.de/haveachin/go-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, 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, 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) + })) + }) +} diff --git a/features/channel/routes.go b/features/channel/routes.go new file mode 100644 index 0000000..02532d8 --- /dev/null +++ b/features/channel/routes.go @@ -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 +} diff --git a/features/client/client.go b/features/client/client.go new file mode 100644 index 0000000..e6ef949 --- /dev/null +++ b/features/client/client.go @@ -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"` +} diff --git a/features/client/handler.go b/features/client/handler.go new file mode 100644 index 0000000..56b036a --- /dev/null +++ b/features/client/handler.go @@ -0,0 +1,40 @@ +package client + +import ( + "net/http" + "strconv" + + "git.cliffbreak.de/haveachin/go-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, 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, 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) + })) + }) +} diff --git a/features/client/routes.go b/features/client/routes.go new file mode 100644 index 0000000..9e20d55 --- /dev/null +++ b/features/client/routes.go @@ -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 +} diff --git a/features/server/handler.go b/features/server/handler.go new file mode 100644 index 0000000..6ed9303 --- /dev/null +++ b/features/server/handler.go @@ -0,0 +1,20 @@ +package server + +import ( + "net/http" + + "git.cliffbreak.de/haveachin/go-tsviewer/response" +) + +func InfoAPIHandler(s Service) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response.Handler(w, response.HandlerFunc(func() (int, error) { + s, err := s.Info() + if err != nil { + return http.StatusNotFound, err + } + + return response.New(s, r).Send(w, http.StatusOK) + })) + }) +} diff --git a/features/server/routes.go b/features/server/routes.go new file mode 100644 index 0000000..22342f3 --- /dev/null +++ b/features/server/routes.go @@ -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 +} diff --git a/features/server/server.go b/features/server/server.go new file mode 100644 index 0000000..401614f --- /dev/null +++ b/features/server/server.go @@ -0,0 +1,22 @@ +package server + +import "time" + +type Service interface { + Info() (*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"` +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..c8e02de --- /dev/null +++ b/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "log" + "net/http" + "time" + + "git.cliffbreak.de/haveachin/go-tsviewer/config" + "git.cliffbreak.de/haveachin/go-tsviewer/features/channel" + "git.cliffbreak.de/haveachin/go-tsviewer/features/client" + "git.cliffbreak.de/haveachin/go-tsviewer/features/server" + "git.cliffbreak.de/haveachin/go-tsviewer/service" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func Routes(s service.Service) *chi.Mux { + router := chi.NewRouter() + router.Use( + middleware.Logger, + middleware.Timeout(5*time.Second), + middleware.DefaultCompress, + middleware.RedirectSlashes, + middleware.Recoverer, + ) + + 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)) + }) + + return router +} + +func main() { + config, err := config.New() + if err != nil { + log.Fatal(err) + } + + service, err := service.New(*config) + if err != nil { + log.Fatal(err) + } + defer service.TSClient.Close() + + router := Routes(*service) + + log.Fatal("Handler: ", http.ListenAndServe(":8080", router)) +} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 781ac2c..0000000 --- a/package-lock.json +++ /dev/null @@ -1,552 +0,0 @@ -{ - "name": "TSViewer", - "version": "0.0.1", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", - "requires": { - "mime-types": "2.1.21", - "negotiator": "0.6.1" - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "requires": { - "lodash": "4.17.11" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "1.0.4", - "debug": "2.6.9", - "depd": "1.1.2", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", - "on-finished": "2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "1.6.16" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "requires": { - "object-keys": "1.0.12" - } - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "express": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", - "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", - "requires": { - "accepts": "1.3.5", - "array-flatten": "1.1.1", - "body-parser": "1.18.3", - "content-disposition": "0.5.2", - "content-type": "1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "1.1.2", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "etag": "1.8.1", - "finalhandler": "1.1.1", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "1.1.2", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "2.0.4", - "qs": "6.5.2", - "range-parser": "1.2.0", - "safe-buffer": "5.1.2", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "1.4.0", - "type-is": "1.6.16", - "utils-merge": "1.0.1", - "vary": "1.1.2" - } - }, - "express-handlebars": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-3.0.0.tgz", - "integrity": "sha1-gKBwu4GbCeSvLKbQeA91zgXnXC8=", - "requires": { - "glob": "6.0.4", - "graceful-fs": "4.1.15", - "handlebars": "4.0.12", - "object.assign": "4.1.0", - "promise": "7.3.1" - } - }, - "finalhandler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "statuses": "1.4.0", - "unpipe": "1.0.0" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" - }, - "handlebars": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz", - "integrity": "sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==", - "requires": { - "async": "2.6.1", - "optimist": "0.6.1", - "source-map": "0.6.1", - "uglify-js": "3.4.9" - } - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "requires": { - "depd": "1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": "1.4.0" - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "requires": { - "safer-buffer": "2.1.2" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ipaddr.js": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", - "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "mime-db": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" - }, - "mime-types": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", - "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", - "requires": { - "mime-db": "1.37.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==" - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "requires": { - "define-properties": "1.1.3", - "function-bind": "1.1.1", - "has-symbols": "1.0.0", - "object-keys": "1.0.12" - } - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "requires": { - "minimist": "0.0.10", - "wordwrap": "0.0.3" - } - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "requires": { - "asap": "2.0.6" - } - }, - "proxy-addr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", - "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", - "requires": { - "forwarded": "0.1.2", - "ipaddr.js": "1.8.0" - } - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", - "unpipe": "1.0.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "requires": { - "debug": "2.6.9", - "depd": "1.1.2", - "destroy": "1.0.4", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "etag": "1.8.1", - "fresh": "0.5.2", - "http-errors": "1.6.3", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "2.3.0", - "range-parser": "1.2.0", - "statuses": "1.4.0" - } - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "requires": { - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "parseurl": "1.3.2", - "send": "0.16.2" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - }, - "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "2.1.21" - } - }, - "uglify-js": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", - "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", - "optional": true, - "requires": { - "commander": "2.17.1", - "source-map": "0.6.1" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index fa42f7e..0000000 --- a/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "TSViewer", - "version": "0.0.0", - "description": "", - "private": true, - "main": "app.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node app.js" - }, - "author": "Marc Kemper", - "license": "", - "dependencies": { - "express": "^4.16.4", - "express-handlebars": "^3.0.0" - } -} diff --git a/request/meta/meta.go b/request/meta/meta.go new file mode 100644 index 0000000..9b7544a --- /dev/null +++ b/request/meta/meta.go @@ -0,0 +1,36 @@ +package meta + +import ( + "net/http" + "strconv" +) + +type Meta struct { + Pretty bool `json:"pretty"` + Envelope bool `json:"envelope"` +} + +func NewFromRequest(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 +} diff --git a/request/routes.go b/request/routes.go new file mode 100644 index 0000000..2923553 --- /dev/null +++ b/request/routes.go @@ -0,0 +1,10 @@ +package request + +const ( + GET = "GET" + PUT = "PUT" + POST = "POST" + DELETE = "DELETE" + LINK = "LINK" + UNLINK = "UNLINK" +) diff --git a/response/error.go b/response/error.go new file mode 100644 index 0000000..ce1c42d --- /dev/null +++ b/response/error.go @@ -0,0 +1,21 @@ +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) *Response { + return &Response{ + Content: Error{ + Status: status, + Error: err.Error(), + Timestamp: time.Now().Format("2006-01-02T15:04:05Z"), //.Format("02 Jan 2006, 15:04:05 MST"), + }, + } +} diff --git a/response/response.go b/response/response.go new file mode 100644 index 0000000..82f65e4 --- /dev/null +++ b/response/response.go @@ -0,0 +1,87 @@ +package response + +import ( + "encoding/json" + "log" + "net/http" + + "git.cliffbreak.de/haveachin/go-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, hf HandlerFunc) { + status, err := hf() + if err == nil { + return + } + + log.Printf("HTTP %d: %q", status, err) + if status, err = NewError(status, err).Send(w, status); err != nil { + http.Error(w, http.StatusText(status), status) + } +} diff --git a/service/channel.go b/service/channel.go new file mode 100644 index 0000000..51cf321 --- /dev/null +++ b/service/channel.go @@ -0,0 +1,64 @@ +package service + +import ( + "errors" + + "git.cliffbreak.de/haveachin/go-tsviewer/features/channel" + "github.com/multiplay/go-ts3" +) + +func (s Service) Channel(id int) (*channel.Channel, error) { + channels, err := s.TSClient.Server.ChannelList() + if err != nil { + return nil, err + } + + var c *channel.Channel + + for _, channel := range channels { + if channel.ID == id { + c = convertChannel(channel) + break + } + } + + if c == nil { + return nil, errors.New("channel does not exist") + } + + return c, nil +} + +func (s Service) Channels() ([]*channel.Channel, error) { + channels, err := s.TSClient.Server.ChannelList() + if err != nil { + return nil, err + } + + var cc []*channel.Channel + + for _, channel := range channels { + if channel.ParentID == 0 { + cc = append(cc, convertChannel(channel)) + continue + } + + for _, c := range cc { + if c.ID == channel.ParentID { + c.Subchannels = append(c.Subchannels, *convertChannel(channel)) + } + } + } + + return cc, nil +} + +func convertChannel(c *ts3.Channel) *channel.Channel { + return &channel.Channel{ + ID: c.ID, + Subchannels: []channel.Channel{}, + Name: c.ChannelName, + TotalClients: c.TotalClients, + NeededSubscribePower: c.NeededSubscribePower, + } +} diff --git a/service/client.go b/service/client.go new file mode 100644 index 0000000..f806807 --- /dev/null +++ b/service/client.go @@ -0,0 +1,60 @@ +package service + +import ( + "errors" + + "git.cliffbreak.de/haveachin/go-tsviewer/features/client" + "github.com/multiplay/go-ts3" +) + +func (s Service) Client(id int) (*client.Client, error) { + clients, err := s.TSClient.Server.ClientList() + if err != nil { + return nil, err + } + + var c *client.Client + + for _, client := range clients { + if client.DatabaseID == id { + c = convertClient(client) + break + } + } + + if c == nil { + return nil, errors.New("client does not exist") + } + + /* if _, err := s.TSClient.Server.ExecCmd(ts3.NewCmd(fmt.Sprintf("clientinfo clid=%d", c.ID)).WithResponse(&c)); err != nil { + return nil, err + } */ + + return c, nil +} + +func (s Service) Clients() ([]*client.Client, error) { + clients, err := s.TSClient.Server.ClientList() + if err != nil { + return nil, err + } + + var cc []*client.Client + + for _, client := range clients { + cc = append(cc, convertClient(client)) + } + + return cc, nil +} + +func convertClient(c *ts3.OnlineClient) *client.Client { + return &client.Client{ + DatabaseID: c.DatabaseID, + ChannelID: c.ID, + Nickname: c.Nickname, + Type: c.Type, + Away: c.Away, + AwayMessage: c.AwayMessage, + } +} diff --git a/service/server.go b/service/server.go new file mode 100644 index 0000000..d5f2d4a --- /dev/null +++ b/service/server.go @@ -0,0 +1,29 @@ +package service + +import ( + "time" + + "git.cliffbreak.de/haveachin/go-tsviewer/features/server" +) + +func (s Service) Info() (*server.Server, error) { + serverInfo, err := s.TSClient.Server.Info() + if err != nil { + return nil, err + } + + return &server.Server{ + Name: serverInfo.Name, + Status: serverInfo.Status, + Version: serverInfo.Version, + WelcomeMessage: serverInfo.WelcomeMessage, + MaxClients: serverInfo.MaxClients, + ClientsOnline: serverInfo.ClientsOnline, + ReservedSlots: serverInfo.ReservedSlots, + Uptime: time.Duration(serverInfo.Uptime) * time.Nanosecond, + TotalPing: serverInfo.TotalPing, + MinAndroidVersion: serverInfo.MinAndroidVersion, + MinClientVersion: serverInfo.MinClientVersion, + MiniOSVersion: serverInfo.MiniOSVersion, + }, nil +} diff --git a/service/service.go b/service/service.go new file mode 100644 index 0000000..86d4187 --- /dev/null +++ b/service/service.go @@ -0,0 +1,37 @@ +package service + +import ( + "strconv" + + "git.cliffbreak.de/haveachin/go-tsviewer/config" + "git.cliffbreak.de/haveachin/go-tsviewer/stringer" + ts3 "github.com/multiplay/go-ts3" +) + +type Service struct { + TSClient *ts3.Client +} + +func New(config config.Config) (*Service, error) { + addr, err := stringer.Build(config.IP, ":", strconv.Itoa(int(config.Port))) + if err != nil { + return nil, err + } + + client, err := ts3.NewClient(addr) + if err != nil { + return nil, err + } + + if err := client.Login(config.User.Name, config.User.Password); err != nil { + return nil, err + } + + if err := client.UsePort(int(config.Server.Port)); err != nil { + return nil, err + } + + return &Service{ + TSClient: client, + }, nil +} diff --git a/stringer/stringer.go b/stringer/stringer.go new file mode 100644 index 0000000..c77ccf6 --- /dev/null +++ b/stringer/stringer.go @@ -0,0 +1,49 @@ +package stringer + +import ( + "fmt" + "strings" + "unicode" +) + +func Build(ss ...string) (string, error) { + var builder strings.Builder + + for _, s := range ss { + if _, err := builder.WriteString(s); err != nil { + return "", err + } + } + + return builder.String(), nil +} + +func ToCamel(str string) string { + if len(str) < 1 { + return str + } + + for i, c := range str[1:] { + if c >= 'A' && c <= 'Z' { + str = fmt.Sprintf("%s_%c%s", str[:i+1], unicode.ToLower(c), str[i+2:]) + return ToCamel(str) + } + } + + return str +} + +func ToSnake(str string) string { + if len(str) < 1 { + return str + } + + for i, c := range str[1:] { + if c == '_' { + str = fmt.Sprintf("%s%c%s", str[:i+1], unicode.ToUpper(rune(str[i+2])), str[i+3:]) + return ToSnake(str) + } + } + + return str +} diff --git a/views/home.handlebars b/views/home.handlebars deleted file mode 100644 index 8ba16d1..0000000 --- a/views/home.handlebars +++ /dev/null @@ -1 +0,0 @@ -