feat: init

This commit is contained in:
Rogee
2024-09-30 11:02:26 +08:00
parent 679759846b
commit 694dfd2a4f
90 changed files with 6046 additions and 15 deletions

34
modules/web/ca.crt Normal file
View File

@@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIF9DCCA9ygAwIBAgIJAODqYUwoVjJkMA0GCSqGSIb3DQEBCwUAMIGOMQswCQYD
VQQGEwJJTDEPMA0GA1UECAwGQ2VudGVyMQwwCgYDVQQHDANMb2QxEDAOBgNVBAoM
B0dvUHJveHkxEDAOBgNVBAsMB0dvUHJveHkxGjAYBgNVBAMMEWdvcHJveHkuZ2l0
aHViLmlvMSAwHgYJKoZIhvcNAQkBFhFlbGF6YXJsQGdtYWlsLmNvbTAeFw0xNzA0
MDUyMDAwMTBaFw0zNzAzMzEyMDAwMTBaMIGOMQswCQYDVQQGEwJJTDEPMA0GA1UE
CAwGQ2VudGVyMQwwCgYDVQQHDANMb2QxEDAOBgNVBAoMB0dvUHJveHkxEDAOBgNV
BAsMB0dvUHJveHkxGjAYBgNVBAMMEWdvcHJveHkuZ2l0aHViLmlvMSAwHgYJKoZI
hvcNAQkBFhFlbGF6YXJsQGdtYWlsLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAJ4Qy+H6hhoY1s0QRcvIhxrjSHaO/RbaFj3rwqcnpOgFq07gRdI9
3c0TFKQJHpgv6feLRhEvX/YllFYu4J35lM9ZcYY4qlKFuStcX8Jm8fqpgtmAMBzP
sqtqDi8M9RQGKENzU9IFOnCV7SAeh45scMuI3wz8wrjBcH7zquHkvqUSYZz035t9
V6WTrHyTEvT4w+lFOVN2bA/6DAIxrjBiF6DhoJqnha0SZtDfv77XpwGG3EhA/qoh
hiYrDruYK7zJdESQL44LwzMPupVigqalfv+YHfQjbhT951IVurW2NJgRyBE62dLr
lHYdtT9tCTCrd+KJNMJ+jp9hAjdIu1Br/kifU4F4+4ZLMR9Ueji0GkkPKsYdyMnq
j0p0PogyvP1l4qmboPImMYtaoFuYmMYlebgC9LN10bL91K4+jLt0I1YntEzrqgJo
WsJztYDw543NzSy5W+/cq4XRYgtq1b0RWwuUiswezmMoeyHZ8BQJe2xMjAOllASD
fqa8OK3WABHJpy4zUrnUBiMuPITzD/FuDx4C5IwwlC68gHAZblNqpBZCX0nFCtKj
YOcI2So5HbQ2OC8QF+zGVuduHUSok4hSy2BBfZ1pfvziqBeetWJwFvapGB44nIHh
WKNKvqOxLNIy7e+TGRiWOomrAWM18VSR9LZbBxpJK7PLSzWqYJYTRCZHAgMBAAGj
UzBRMB0GA1UdDgQWBBR4uDD9Y6x7iUoHO+32ioOcw1ICZTAfBgNVHSMEGDAWgBR4
uDD9Y6x7iUoHO+32ioOcw1ICZTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB
CwUAA4ICAQAaCEupzGGqcdh+L7BzhX7zyd7yzAKUoLxFrxaZY34Xyj3lcx1XoK6F
AqsH2JM25GixgadzhNt92JP7vzoWeHZtLfstrPS638Y1zZi6toy4E49viYjFk5J0
C6ZcFC04VYWWx6z0HwJuAS08tZ37JuFXpJGfXJOjZCQyxse0Lg0tuKLMeXDCk2Y3
Ba0noeuNyHRoWXXPyiUoeApkVCU5gIsyiJSWOjhJ5hpJG06rQNfNYexgKrrraEin
o0jmEMtJMx5TtD83hSnLCnFGBBq5lkE7jgXME1KsbIE3lJZzRX1mQwUK8CJDYxye
i6M/dzSvy0SsPvz8fTAlprXRtWWtJQmxgWENp3Dv+0Pmux/l+ilk7KA4sMXGhsfr
bvTOeWl1/uoFTPYiWR/ww7QEPLq23yDFY04Q7Un0qjIk8ExvaY8lCkXMgc8i7sGY
VfvOYb0zm67EfAQl3TW8Ky5fl5CcxpVCD360Bzi6hwjYixa3qEeBggOixFQBFWft
8wrkKTHpOQXjn4sDPtet8imm9UYEtzWrFX6T9MFYkBR0/yye0FIh9+YPiTA6WB86
NCNwK5Yl6HuvF97CIH5CdgO+5C7KifUtqTOL8pQKbNwy0S3sNYvB+njGvRpR7pKV
BUnFpB/Atptqr4CUlTXrc5IPLAqAfmwk5IKcwy3EXUbruf9Dwz69YA==
-----END CERTIFICATE-----

262
modules/web/route_device.go Normal file
View File

@@ -0,0 +1,262 @@
package web
import (
"errors"
"fmt"
"strconv"
"time"
"dyproxy/.gen/model"
"dyproxy/.gen/table"
"github.com/go-jet/jet/v2/qrm"
. "github.com/go-jet/jet/v2/sqlite"
"github.com/gofiber/fiber/v3"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
type ExpertConfig struct {
Voice string
Hello string
Wechat string
Region []string
NameKeyword []string
Produce bool
DefaultName bool
DefaultAvatar bool
}
func (s *WebServer) routeGetDevices(c fiber.Ctx) error {
devices := []model.Device{}
stmt := table.Device.SELECT(table.Device.AllColumns).ORDER_BY(table.Device.ID)
if err := stmt.QueryContext(c.UserContext(), s.db, &devices); err != nil {
return err
}
expertsIds := lo.FilterMap(devices, func(device model.Device, _ int) (Expression, bool) {
if device.Expert == "" {
return nil, false
}
return String(device.Expert), true
})
if len(expertsIds) == 0 {
return c.JSON(devices)
}
type listDevice struct {
model.Device `json:",inline"`
ExpertName string
}
// find experts by ids
experts := []model.Expert{}
stmt = table.Expert.SELECT(table.Expert.AllColumns).WHERE(table.Expert.UID.IN(expertsIds...))
if err := stmt.QueryContext(c.UserContext(), s.db, &experts); err != nil {
return err
}
expertsMap := make(map[string]string)
for _, expert := range experts {
expertsMap[expert.UID] = expert.RealName
}
list := make([]listDevice, 0, len(devices))
for _, device := range devices {
if expertName, ok := expertsMap[device.Expert]; ok {
list = append(list, listDevice{
Device: device,
ExpertName: expertName,
})
} else {
list = append(list, listDevice{
Device: device,
ExpertName: "未设置",
})
}
}
return c.JSON(list)
}
func (s *WebServer) routeGetDevice(c fiber.Ctx) error {
deviceID := c.Params("uuid")
var device model.Device
err := table.Device.SELECT(table.Device.AllColumns).WHERE(table.Device.UUID.EQ(String(deviceID))).QueryContext(c.UserContext(), s.db, &device)
if err != nil {
if errors.Is(err, qrm.ErrNoRows) {
// create new device
device.UUID = deviceID
_, err = table.Device.INSERT(table.Device.AllColumns.Except(table.Device.ID)).MODEL(device).ExecContext(c.UserContext(), s.db)
if err != nil {
return err
}
return c.JSON(nil)
}
return err
}
return c.JSON(device)
}
func (s *WebServer) routeGetDeviceFollower(c fiber.Ctx) error {
if s.pendingItems == nil {
s.pendingItems = make(map[int32]time.Time)
}
// get device
deviceID := c.Params("uuid")
var device model.Device
err := table.Device.SELECT(table.Device.AllColumns).WHERE(table.Device.UUID.EQ(String(deviceID))).QueryContext(c.UserContext(), s.db, &device)
if err != nil {
if errors.Is(err, qrm.ErrNoRows) {
// create new device
device.UUID = deviceID
_, err = table.Device.INSERT(table.Device.AllColumns.Except(table.Device.ID)).MODEL(device).ExecContext(c.UserContext(), s.db)
if err != nil {
return err
}
return c.JSON(nil)
}
return err
}
if device.Expert == "" {
return c.JSON(nil)
}
if device.State == StateStop {
return c.JSON(nil)
}
tbl := table.Follower
lastID := c.Query("last", "")
if lastID != "" {
id, err := strconv.Atoi(lastID)
if err != nil {
return err
}
// remove from pending
s.listLock.Lock()
delete(s.pendingItems, int32(id))
s.listLock.Unlock()
tbl.
UPDATE(table.Follower.Followed).
SET(Int32(1)).
WHERE(table.Follower.ID.EQ(Int32(int32(id)))).
ExecContext(c.UserContext(), s.db)
}
pendingIDs := []Expression{}
for i := range s.pendingItems {
pendingIDs = append(pendingIDs, Int32(i))
}
pendingIDs = []Expression{}
// get device expert
var expert model.Expert
err = table.Expert.SELECT(table.Expert.AllColumns).WHERE(table.Expert.UID.EQ(String(device.Expert))).QueryContext(c.UserContext(), s.db, &expert)
if err != nil {
return err
}
c.Response().Header.Set("x-expert-uid", expert.UID)
c.Response().Header.Set("x-config-at", fmt.Sprintf("%d", expert.ConfigAt))
if expert.State == StateStop {
return c.JSON(nil)
}
condition := tbl.Followed.EQ(Int32(0)).
AND(tbl.ExpertUID.EQ(String(device.Expert))).
AND(tbl.ID.NOT_IN(pendingIDs...)).
AND(tbl.CreatedAt.GT(Int32(expert.Since)))
stmt := tbl.
SELECT(tbl.AllColumns).
WHERE(condition).
ORDER_BY(table.Follower.ID.DESC()).
LIMIT(1)
logrus.Debug(stmt.DebugSql())
var follower model.Follower
if err := stmt.QueryContext(c.UserContext(), s.db, &follower); err != nil {
if errors.Is(err, qrm.ErrNoRows) {
return c.JSON(nil)
}
return err
}
s.listLock.Lock()
s.pendingItems[int32(follower.ID)] = time.Now()
s.listLock.Unlock()
return c.JSON(follower)
}
// routeSetDeviceExpert
func (s *WebServer) routeSetDeviceExpert(c fiber.Ctx) error {
deviceID := c.Params("uuid")
uid := c.Params("uid")
type body struct {
Name string `json:"name"`
}
b := &body{}
if err := c.Bind().JSON(b); err != nil {
return err
}
var device model.Device
err := table.Device.SELECT(table.Device.AllColumns).WHERE(table.Device.UUID.EQ(String(deviceID))).QueryContext(c.UserContext(), s.db, &device)
if err != nil {
return err
}
var expert model.Expert
err = table.Expert.SELECT(table.Expert.UID).WHERE(table.Expert.UID.EQ(String(uid))).QueryContext(c.UserContext(), s.db, &expert)
if err != nil {
return err
}
device.Expert = expert.UID
_, err = table.Device.UPDATE().SET(
table.Device.Expert.SET(String(device.Expert)),
table.Device.Name.SET(String(b.Name)),
).WHERE(table.Device.UUID.EQ(String(deviceID))).ExecContext(c.UserContext(), s.db)
if err != nil {
return err
}
return c.JSON(nil)
}
func (s *WebServer) routePatchDeviceState(c fiber.Ctx) error {
var state struct {
State string `json:"state"`
Note string `json:"note"`
}
if err := c.Bind().JSON(&state); err != nil {
return err
}
uuid := c.Params("uuid")
tbl := table.Device
_, err := tbl.
UPDATE().
SET(
tbl.State.SET(String(state.State)),
tbl.Note.SET(String(state.Note)),
).
WHERE(tbl.UUID.EQ(String(uuid))).
ExecContext(c.UserContext(), s.db)
return err
}

200
modules/web/route_expert.go Normal file
View File

@@ -0,0 +1,200 @@
package web
import (
"encoding/json"
"fmt"
"time"
"dyproxy/.gen/model"
"dyproxy/.gen/table"
. "github.com/go-jet/jet/v2/sqlite"
"github.com/gofiber/fiber/v3"
"github.com/sirupsen/logrus"
)
func (s *WebServer) routeGetExpertConfig(c fiber.Ctx) error {
uid := c.Params("uid", "")
var user model.Expert
err := table.Expert.SELECT(table.Expert.AllColumns).WHERE(table.Expert.UID.EQ(String(uid))).QueryContext(c.UserContext(), s.db, &user)
if err != nil {
return err
}
if user.Config == "" {
user.Config = "{}"
}
var config ExpertConfig
if err := json.Unmarshal([]byte(user.Config), &config); err != nil {
return err
}
c.Response().Header.Set("x-config-at", fmt.Sprintf("%d", user.ConfigAt))
return c.JSON(config)
}
func (s *WebServer) routeGetExpert(c fiber.Ctx) error {
uid := c.Params("uid", "")
var user model.Expert
err := table.Expert.SELECT(table.Expert.AllColumns).WHERE(table.Expert.UID.EQ(String(uid))).QueryContext(c.UserContext(), s.db, &user)
if err != nil {
return err
}
return c.JSON(user)
}
func (s *WebServer) routeGetExperts(c fiber.Ctx) error {
stmt := table.Expert.SELECT(table.Expert.AllColumns).ORDER_BY(table.Expert.ID)
var rows []model.Expert
if err := stmt.Query(s.db, &rows); err != nil {
return err
}
sql := `SELECT
expert_uid,
COUNT(*) AS total,
SUM(case when followed = 1 then 1 else 0 end) AS followed
FROM
follower
GROUP BY
expert_uid;`
r, err := s.db.QueryContext(c.UserContext(), sql)
if err != nil {
return err
}
type statistics struct {
UserUID string
Total int32
Followed int32
}
var statisticsItems []statistics
for r.Next() {
item := statistics{}
if err := r.Scan(&item.UserUID, &item.Total, &item.Followed); err != nil {
return err
}
statisticsItems = append(statisticsItems, item)
}
type resp struct {
Focus int32
Total int32
Conf ExpertConfig
model.Expert `json:",inline"`
}
var users []resp
for _, row := range rows {
stat := statistics{}
// get item from statisticsItems where row.UID == item.UserUID
for _, item := range statisticsItems {
if row.UID == item.UserUID {
stat = item
break
}
}
var conf ExpertConfig
if row.Config == "" {
row.Config = "{}"
}
if err := json.Unmarshal([]byte(row.Config), &conf); err != nil {
logrus.Error(err)
continue
}
row.Config = ""
users = append(users, resp{
Focus: stat.Followed,
Total: stat.Total,
Conf: conf,
Expert: row,
})
}
return c.JSON(users)
}
func (s *WebServer) routePostExperts(c fiber.Ctx) error {
expert := &model.Expert{}
if err := c.Bind().JSON(expert); err != nil {
return err
}
tbl := table.Expert
_, err := tbl.
INSERT(tbl.AllColumns.Except(tbl.ID)).
MODEL(expert).
ON_CONFLICT(tbl.UID).
DO_NOTHING().
ExecContext(c.UserContext(), s.db)
return err
}
func (s *WebServer) routePatchExpertConfig(c fiber.Ctx) error {
var data struct {
Since int32 `json:"since"`
ExpertConfig `json:",inline"`
}
if err := c.Bind().JSON(&data); err != nil {
return err
}
uid := c.Params("uid")
tbl := table.Expert
// get expert by uid
var expert model.Expert
if err := tbl.SELECT(tbl.AllColumns).WHERE(tbl.UID.EQ(String(uid))).QueryContext(c.UserContext(), s.db, &expert); err != nil {
return err
}
if expert.Config == "" {
expert.Config = "{}"
}
newExpertConfig, err := json.Marshal(data.ExpertConfig)
if err != nil {
return err
}
_, err = tbl.
UPDATE().
SET(
tbl.Config.SET(String(string(newExpertConfig))),
tbl.ConfigAt.SET(Int(time.Now().Unix())),
).
WHERE(tbl.UID.EQ(String(uid))).
ExecContext(c.UserContext(), s.db)
return err
}
const (
StateNormal = ""
StateStop = "stop"
)
func (s *WebServer) routePatchExpertState(c fiber.Ctx) error {
var state struct {
State string `json:"state"`
}
if err := c.Bind().JSON(&state); err != nil {
return err
}
uid := c.Params("uid")
tbl := table.Expert
_, err := tbl.
UPDATE().
SET(tbl.State.SET(String(state.State))).
WHERE(tbl.UID.EQ(String(uid))).
ExecContext(c.UserContext(), s.db)
return err
}

View File

@@ -0,0 +1,109 @@
package web
import (
"errors"
"strconv"
"time"
"dyproxy/.gen/model"
"dyproxy/.gen/table"
"github.com/go-jet/jet/v2/qrm"
. "github.com/go-jet/jet/v2/sqlite"
"github.com/gofiber/fiber/v3"
"github.com/sirupsen/logrus"
)
func (s *WebServer) routeGetFollower(c fiber.Ctx) error {
if s.pendingItems == nil {
s.pendingItems = make(map[int32]time.Time)
}
tbl := table.Follower
lastID := c.Query("last", "")
if lastID != "" {
id, err := strconv.Atoi(lastID)
if err != nil {
return err
}
// remove from pending
s.listLock.Lock()
delete(s.pendingItems, int32(id))
s.listLock.Unlock()
tbl.
UPDATE(table.Follower.Followed).
SET(Int32(1)).
WHERE(table.Follower.ID.EQ(Int32(int32(id)))).
ExecContext(c.UserContext(), s.db)
}
uid := c.Params("uid")
pendingIDs := []Expression{}
for i := range s.pendingItems {
pendingIDs = append(pendingIDs, Int32(i))
}
var expert model.Expert
err := table.Expert.SELECT(table.Expert.State, table.Expert.Since).WHERE(table.Expert.UID.EQ(String(uid))).QueryContext(c.UserContext(), s.db, &expert)
if err != nil {
return err
}
if expert.State == StateStop {
return c.JSON(nil)
}
condition := tbl.Followed.EQ(Int32(0)).
AND(tbl.ExpertUID.EQ(String(uid))).
AND(tbl.ID.NOT_IN(pendingIDs...)).
AND(tbl.CreatedAt.GT(Int32(expert.Since)))
stmt := tbl.
SELECT(tbl.AllColumns).
WHERE(condition).
ORDER_BY(table.Follower.ID.DESC()).
LIMIT(1)
logrus.Debug(stmt.DebugSql())
var follower model.Follower
if err := stmt.QueryContext(c.UserContext(), s.db, &follower); err != nil {
if errors.Is(err, qrm.ErrNoRows) {
return c.JSON(nil)
}
return err
}
s.listLock.Lock()
s.pendingItems[int32(follower.ID)] = time.Now()
s.listLock.Unlock()
return c.JSON(follower)
}
func (s *WebServer) routePostFollower(c fiber.Ctx) error {
followers := []model.Follower{}
if err := c.Bind().JSON(&followers); err != nil {
return err
}
tbl := table.Follower
for _, f := range followers {
f.CreatedAt = int32(time.Now().In(s.local).Unix())
_, err := tbl.
INSERT(tbl.AllColumns.Except(tbl.ID)).
MODEL(f).
ON_CONFLICT(tbl.UID).
DO_NOTHING().
ExecContext(c.UserContext(), s.db)
if err != nil {
logrus.Error(err)
}
}
return nil
}

View File

@@ -0,0 +1,18 @@
package web
import (
"os"
"github.com/gofiber/fiber/v3"
)
func (s *WebServer) routeCss(c fiber.Ctx) error {
b, _ := os.ReadFile("./modules/web/dst/style.css")
c.Set("Content-Type", "text/css")
return c.Send(b)
}
// routeIndex
func (s *WebServer) routeIndex(c fiber.Ctx) error {
return c.SendString("Hello 👋!")
}

View File

@@ -0,0 +1,71 @@
package web
import (
"io/fs"
"path/filepath"
"strings"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/utils/v2"
)
func (s *WebServer) routeGetVersion(c fiber.Ctx) error {
files := []string{}
error := filepath.WalkDir(config.Path, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if strings.HasPrefix(path, filepath.Join(config.Path, ".git/")) {
return nil
}
if strings.HasPrefix(path, filepath.Join(config.Path, "boot/")) {
return nil
}
if strings.ToLower(path) == strings.ToLower(filepath.Join(config.Path, "README.md")) {
return nil
}
if strings.ToLower(path) == strings.ToLower(filepath.Join(config.Path, "main.js")) {
return nil
}
if strings.ToLower(path) == strings.ToLower(filepath.Join(config.Path, "project.json")) {
return nil
}
if path == config.Path {
return nil
}
files = append(files, strings.Replace(path, config.Path+"/", "", -1))
return nil
})
if error != nil {
return error
}
files = append(files, "version.txt")
return c.JSON(map[string]interface{}{
"version": config.Version,
"files": files,
})
}
func (s *WebServer) routeGetVersionFile(c fiber.Ctx) error {
file := c.Params("+")
c.Response().Header.SetCanonical(
utils.UnsafeBytes(fiber.HeaderContentDisposition),
utils.UnsafeBytes(`attachment;`),
)
if file == "version.txt" {
return c.SendString(config.Version)
}
return c.SendFile(filepath.Join(config.Path, file), false)
}

66
modules/web/routes.go Normal file
View File

@@ -0,0 +1,66 @@
package web
import (
"dyproxy/frontend"
_ "embed"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/basicauth"
"github.com/gofiber/fiber/v3/middleware/filesystem"
"github.com/gofiber/fiber/v3/middleware/redirect"
)
//go:embed ca.crt
var ca []byte
func WithRoutes() Option {
return func(s *WebServer) {
apiGroup := s.engine.Group("/api", basicauth.New(basicauth.Config{
Users: users,
}))
apiGroup.Get("/version", s.routeGetVersion)
apiGroup.Get("/version/file/+", s.routeGetVersionFile)
apiGroup.Get("/experts", s.routeGetExperts)
apiGroup.Get("/experts/:uid", s.routeGetExpert)
apiGroup.Post("/experts", s.routePostExperts)
apiGroup.Get("/experts/:uid/config", s.routeGetExpertConfig)
apiGroup.Patch("/experts/:uid/config", s.routePatchExpertConfig)
apiGroup.Patch("/experts/:uid/state", s.routePatchExpertState)
apiGroup.Get("/experts/:uid/follower", s.routeGetFollower)
apiGroup.Post("/followers", s.routePostFollower)
apiGroup.Get("/devices", s.routeGetDevices)
apiGroup.Get("/devices/:uuid", s.routeGetDevice)
apiGroup.Get("/devices/:uuid/follower", s.routeGetDeviceFollower)
apiGroup.Patch("/devices/:uuid/experts/:uid", s.routeSetDeviceExpert)
apiGroup.Patch("/devices/:uuid/state", s.routePatchDeviceState)
apiGroup.Post("/devices/:uuid/block", s.routePatchDeviceState)
s.engine.Get("/ca", func(c fiber.Ctx) error {
// send attach ment ca.crt from embeded file
c.Set(fiber.HeaderContentType, "application/x-x509-ca-cert")
c.Set(fiber.HeaderContentDisposition, "attachment; filename=ca.crt")
return c.Send(ca)
})
s.engine.Use(redirect.New(redirect.Config{
Rules: map[string]string{"/": "/index.html"},
StatusCode: 301,
}))
s.engine.Static("/static", config.Static, fiber.Static{
Compress: true,
ByteRange: true,
Download: true,
})
s.engine.Use(filesystem.New(filesystem.Config{
Root: frontend.Static,
PathPrefix: "dist",
Index: "/dist/index.html",
}))
}
}

267
modules/web/routes_test.go Normal file
View File

@@ -0,0 +1,267 @@
package web
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http/httptest"
"testing"
"time"
"dyproxy/.gen/model"
"dyproxy/.gen/table"
"dyproxy/providers/db"
"github.com/go-faker/faker/v4"
. "github.com/go-jet/jet/v2/sqlite"
"github.com/rogeecn/fabfile"
"github.com/sirupsen/logrus"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/suite"
)
type WebTestSuite struct {
suite.Suite
web *WebServer
}
func TestWebServer(t *testing.T) {
db, err := db.Connect(fabfile.MustFind("data.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
app := New(
WithDB(db),
WithLogger(),
WithRecover(),
WithRoutes(),
WithPendingCleaner(),
)
suite.Run(t, &WebTestSuite{web: app})
}
func (s *WebTestSuite) Test_index() {
Convey("Test_index", s.T(), func() {
req := httptest.NewRequest("GET", "http://localhost/", nil)
resp, err := s.web.engine.Test(req)
So(err, ShouldBeNil)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
So(err, ShouldBeNil)
s.T().Logf("BODY: %s", body)
})
}
func (s *WebTestSuite) Test_css() {
Convey("Test_css", s.T(), func() {
req := httptest.NewRequest("GET", "http://localhost/style.css", nil)
resp, err := s.web.engine.Test(req)
So(err, ShouldBeNil)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
So(err, ShouldBeNil)
s.T().Logf("BODY: %s", body)
})
}
func (s *WebTestSuite) Test_Expert() {
Convey("Expert", s.T(), func() {
Convey("GET", func() {
t := table.Expert
db.Truncate(s.web.db, t.TableName())
var m model.Expert
So(faker.FakeData(&m), ShouldBeNil)
m.Since = 0
m.UID = "110"
_, err := t.INSERT().MODEL(m).Exec(s.web.db)
So(err, ShouldBeNil)
req := httptest.NewRequest("GET", "http://localhost/api/experts/110", nil)
resp, err := s.web.engine.Test(req)
So(err, ShouldBeNil)
body, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
So(err, ShouldBeNil)
s.T().Logf("BODY: %s", body)
})
})
}
func (s *WebTestSuite) Test_Experts() {
Convey("Experts", s.T(), func() {
Convey("GET", func() {
t := table.Expert
db.Truncate(s.web.db, t.TableName())
var m model.Expert
So(faker.FakeData(&m), ShouldBeNil)
m.Since = 0
_, err := t.INSERT().MODEL(m).Exec(s.web.db)
So(err, ShouldBeNil)
req := httptest.NewRequest("GET", "http://localhost/api/experts", nil)
resp, err := s.web.engine.Test(req)
So(err, ShouldBeNil)
body, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
So(err, ShouldBeNil)
s.T().Logf("BODY: %s", body)
})
Convey("PATCH", func() {
t := table.Expert
db.Truncate(s.web.db, t.TableName())
var m model.Expert
So(faker.FakeData(&m), ShouldBeNil)
m.ID = 1
m.UID = "110"
m.Since = 0
_, err := t.INSERT().MODEL(m).Exec(s.web.db)
So(err, ShouldBeNil)
req := httptest.NewRequest("PATCH", "http://localhost/api/experts/110/date", bytes.NewReader([]byte(`{"since": 1}`)))
req.Header.Set("Content-Type", "application/json")
_, err = s.web.engine.Test(req)
So(err, ShouldBeNil)
// body, err := io.ReadAll(resp.Body)
// defer resp.Body.Close()
// So(err, ShouldBeNil)
// s.T().Logf("BODY: %s", body)
var u model.Expert
err = t.SELECT(t.AllColumns).WHERE(t.ID.EQ(Int32(1))).Query(s.web.db, &u)
So(err, ShouldBeNil)
So(u.Since, ShouldEqual, 1)
})
Convey("POST", func() {
t := table.Expert
db.Truncate(s.web.db, t.TableName())
var m model.Expert
So(faker.FakeData(&m), ShouldBeNil)
m.UID = "110"
b, _ := json.Marshal(m)
req := httptest.NewRequest("POST", "http://localhost/api/experts", bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json")
_, err := s.web.engine.Test(req)
So(err, ShouldBeNil)
var u model.Expert
err = t.SELECT(t.AllColumns).ORDER_BY(t.ID.DESC()).LIMIT(1).Query(s.web.db, &u)
So(err, ShouldBeNil)
So(u.UID, ShouldEqual, "110")
})
})
}
func (s *WebTestSuite) Test_Follower() {
logrus.SetLevel(logrus.DebugLevel)
FocusConvey("Follower", s.T(), func() {
Convey("GET", func() {
tu := table.Expert
t := table.Follower
db.Truncate(s.web.db, tu.TableName())
db.Truncate(s.web.db, t.TableName())
s.web.pendingItems = make(map[int32]time.Time)
var m model.Expert
So(faker.FakeData(&m), ShouldBeNil)
m.UID = "110"
m.Since = 0
_, err := tu.INSERT().MODEL(m).Exec(s.web.db)
So(err, ShouldBeNil)
var f model.Follower
So(faker.FakeData(&f), ShouldBeNil)
f.ExpertUID = "110"
f.UID = "10"
f.Followed = 0
_, err = t.INSERT().MODEL(f).Exec(s.web.db)
So(err, ShouldBeNil)
req := httptest.NewRequest("GET", "http://localhost/api/experts/110/follower", nil)
resp, err := s.web.engine.Test(req)
So(err, ShouldBeNil)
body, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
So(err, ShouldBeNil)
s.T().Logf("BODY: %s", body)
})
FocusConvey("POST", func() {
tu := table.Expert
t := table.Follower
db.Truncate(s.web.db, tu.TableName())
db.Truncate(s.web.db, t.TableName())
s.web.pendingItems = make(map[int32]time.Time)
var m model.Expert
So(faker.FakeData(&m), ShouldBeNil)
m.UID = "110"
m.Since = 0
_, err := tu.INSERT().MODEL(m).Exec(s.web.db)
So(err, ShouldBeNil)
fs := []model.Follower{}
var f model.Follower
for i := 0; i < 5; i++ {
So(faker.FakeData(&f), ShouldBeNil)
f.ExpertUID = "110"
f.UID = fmt.Sprintf("%d", 10+i)
f.Followed = 0
fs = append(fs, f)
fs = append(fs, f)
}
b, _ := json.Marshal(fs)
req := httptest.NewRequest("POST", "http://localhost/api/followers", bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json")
_, err = s.web.engine.Test(req)
So(err, ShouldBeNil)
var result struct {
Count int32
}
stmt := t.SELECT(COUNT(t.ID).AS("Count"))
s.T().Log(stmt.DebugSql())
err = stmt.Query(s.web.db, &result)
s.T().Logf("%+v", result)
So(err, ShouldBeNil)
So(result.Count, ShouldEqual, 5)
})
})
}

156
modules/web/serve.go Normal file
View File

@@ -0,0 +1,156 @@
package web
import (
"database/sql"
"fmt"
"log"
"sync"
"time"
"dyproxy/providers/db"
"github.com/fsnotify/fsnotify"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/logger"
"github.com/gofiber/fiber/v3/middleware/recover"
"github.com/rogeecn/fabfile"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var users map[string]string = map[string]string{
"rogeecn": "xixi@0202",
}
type Config struct {
Version string
Path string
Static string
}
var config *Config
func load(f string) error {
viper.SetConfigFile(f)
viper.SetConfigType("yaml")
if err := viper.ReadInConfig(); err != nil {
return err
}
if err := viper.Unmarshal(&config); err != nil {
return err
}
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
if e.Op != fsnotify.Write {
return
}
if err := viper.Unmarshal(&config); err != nil {
log.Println(err)
}
log.Printf("config changed: %+v", config)
})
return nil
}
func ServeE(cmd *cobra.Command, args []string) error {
logrus.SetLevel(logrus.WarnLevel)
conf, err := cmd.Flags().GetString("config")
if err != nil {
return err
}
if err := load(conf); err != nil {
return err
}
db, err := db.Connect(fabfile.MustFind("data.db"))
if err != nil {
return err
}
defer db.Close()
return New(
WithDB(db),
WithLogger(),
WithRecover(),
WithRoutes(),
WithPendingCleaner(),
).Serve(9090)
}
type Option func(*WebServer)
func WithDB(db *sql.DB) Option {
return func(p *WebServer) {
p.db = db
}
}
func WithLogger() Option {
return func(s *WebServer) {
s.engine.Use(logger.New())
}
}
// WithRecover
func WithRecover() Option {
return func(s *WebServer) {
s.engine.Use(recover.New())
}
}
type WebServer struct {
db *sql.DB
engine *fiber.App
local *time.Location
pendingItems map[int32]time.Time
listLock sync.RWMutex
}
func New(opts ...Option) *WebServer {
cstSh, _ := time.LoadLocation("Asia/Shanghai")
s := &WebServer{
engine: fiber.New(),
local: cstSh,
}
for _, opt := range opts {
opt(s)
}
return s
}
// run
func (p *WebServer) Serve(port uint) error {
log.Printf("server start serve at: :%d", port)
return p.engine.Listen(fmt.Sprintf(":%d", port))
}
func WithPendingCleaner() Option {
return func(s *WebServer) {
if s.pendingItems == nil {
s.pendingItems = make(map[int32]time.Time)
}
go func() {
for range time.NewTicker(time.Minute * 1).C {
s.listLock.Lock()
for k, v := range s.pendingItems {
if time.Since(v) > time.Minute*2 {
delete(s.pendingItems, k)
}
}
s.listLock.Unlock()
}
}()
}
}