feat: init
This commit is contained in:
0
modules/.keep
Normal file
0
modules/.keep
Normal file
72
modules/proxy/logic.go
Normal file
72
modules/proxy/logic.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
|
||||
"dyproxy/.gen/model"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (p *Proxy) processFollowers(body []byte) {
|
||||
var follower Follower
|
||||
if err := json.Unmarshal(body, &follower); err != nil {
|
||||
err = errors.Wrap(err, "unmarshal followers")
|
||||
logrus.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
followers := []model.Follower{}
|
||||
for _, f := range follower.Followers {
|
||||
m := model.Follower{
|
||||
Avatar: f.AvatarThumb.URLList[0],
|
||||
Nickname: f.Nickname,
|
||||
SecUID: f.SecUID,
|
||||
ShortID: f.ShortID,
|
||||
UID: f.UID,
|
||||
UniqueID: f.UniqueID,
|
||||
ExpertUID: follower.MyselfUserID,
|
||||
}
|
||||
|
||||
logrus.Warnf("follower: %+v", m)
|
||||
followers = append([]model.Follower{m}, followers...)
|
||||
}
|
||||
|
||||
// post followers
|
||||
if _, err := p.client.R().SetBody(followers).Post("/api/followers"); err != nil {
|
||||
logrus.Error("post /api/followers, ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy) processUserInfo(body []byte) {
|
||||
pattern := `self.__pace_f.push\(.*?"uid\\":\\"(.*?)\\",.*?\\"secUid\\":\\"(.*?)\\",.*?\\"shortId\\":\\"(.*?)\\",.*\\"realName\\":\\"(.*?)\\",.*?"nickname\\":\\"(.*?)\\",.*?`
|
||||
reg := regexp.MustCompile(pattern)
|
||||
|
||||
matches := reg.FindSubmatch(body)
|
||||
if len(matches) == 0 {
|
||||
logrus.Error("no match users")
|
||||
return
|
||||
}
|
||||
|
||||
if len(matches) != 6 {
|
||||
logrus.Error("invalid match")
|
||||
return
|
||||
}
|
||||
|
||||
expert := model.Expert{
|
||||
UID: string(matches[1]),
|
||||
SecUID: string(matches[2]),
|
||||
ShortID: string(matches[3]),
|
||||
RealName: string(matches[4]),
|
||||
NickName: string(matches[5]),
|
||||
}
|
||||
|
||||
logrus.Warnf("expert: %+v", expert)
|
||||
|
||||
// post user info
|
||||
if _, err := p.client.R().SetBody(expert).Post("/api/experts"); err != nil {
|
||||
logrus.Error("post /api/experts, ", err)
|
||||
}
|
||||
}
|
||||
70
modules/proxy/opt_follower.go
Normal file
70
modules/proxy/opt_follower.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/elazarl/goproxy.v1"
|
||||
)
|
||||
|
||||
type Follower struct {
|
||||
Extra struct {
|
||||
FatalItemIds []any `json:"fatal_item_ids"`
|
||||
Logid string `json:"logid"`
|
||||
Now int64 `json:"now"`
|
||||
} `json:"extra"`
|
||||
Followers []struct {
|
||||
AvatarThumb struct {
|
||||
Height int `json:"height"`
|
||||
URI string `json:"uri"`
|
||||
URLList []string `json:"url_list"`
|
||||
Width int `json:"width"`
|
||||
} `json:"avatar_thumb"`
|
||||
Nickname string `json:"nickname"`
|
||||
SecUID string `json:"sec_uid"`
|
||||
ShortID string `json:"short_id"`
|
||||
UID string `json:"uid"`
|
||||
UniqueID string `json:"unique_id"`
|
||||
UniqueIDModifyTime int `json:"unique_id_modify_time"`
|
||||
} `json:"followers"`
|
||||
HasMore bool `json:"has_more"`
|
||||
MyselfUserID string `json:"myself_user_id"`
|
||||
Offset int `json:"offset"`
|
||||
RecHasMore bool `json:"rec_has_more"`
|
||||
StatusCode int `json:"status_code"`
|
||||
StorePage string `json:"store_page"`
|
||||
Total int `json:"total"`
|
||||
VcdCount int `json:"vcd_count"`
|
||||
}
|
||||
|
||||
func WithFollower() Option {
|
||||
return func(p *Proxy) {
|
||||
p.proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
|
||||
if resp.StatusCode != 200 {
|
||||
return resp
|
||||
}
|
||||
|
||||
if resp.Request.Host != "www.douyin.com" {
|
||||
return resp
|
||||
}
|
||||
|
||||
if resp.Request.URL.Path != "/aweme/v1/web/user/follower/list/" {
|
||||
return resp
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
return resp
|
||||
}
|
||||
resp.Body.Close()
|
||||
resp.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
go p.processFollowers(body)
|
||||
|
||||
return resp
|
||||
})
|
||||
}
|
||||
}
|
||||
75
modules/proxy/opt_user_info.go
Normal file
75
modules/proxy/opt_user_info.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/elazarl/goproxy.v1"
|
||||
)
|
||||
|
||||
type UserInfo struct {
|
||||
UID string
|
||||
SecUID string
|
||||
ShortID string
|
||||
RealName string
|
||||
Nickname string
|
||||
}
|
||||
|
||||
func WithUserInfo(duration int) Option {
|
||||
return func(p *Proxy) {
|
||||
p.proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
|
||||
if resp.StatusCode != 200 {
|
||||
return resp
|
||||
}
|
||||
|
||||
if resp.Request.Host != "www.douyin.com" {
|
||||
return resp
|
||||
}
|
||||
|
||||
if resp.Request.URL.Path != "/user/self" {
|
||||
return resp
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
return resp
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// 添加定时刷新
|
||||
codes := `
|
||||
<!------------------->
|
||||
<script nonce>
|
||||
var hookFans = function (){
|
||||
document.querySelector('div[data-e2e="user-info-fans"]').click()
|
||||
setTimeout( () => document.querySelector('div[data-e2e="user-fans-container"]').parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.querySelector('svg').parentElement.click(), 2*1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script nonce>
|
||||
if (location.href.startsWith("https://www.douyin.com/user/self")) {
|
||||
console.log(">>>>>>>>>>>>>>>>>>>>>>>>>> start hook fans")
|
||||
var interval = setInterval(hookFans, (%d+Math.random()*100 %% %d)*1000)
|
||||
// setTimeout(() => location.reload, 5*60*1000)
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<!------------------->
|
||||
`
|
||||
codes = fmt.Sprintf(codes, duration, duration)
|
||||
body = bytes.Replace(body, []byte("</head>"), []byte(codes), 1)
|
||||
resp.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
// remove Content-Security-Policy
|
||||
resp.Header.Del("Content-Security-Policy")
|
||||
|
||||
go p.processUserInfo(body)
|
||||
|
||||
return resp
|
||||
})
|
||||
}
|
||||
}
|
||||
133
modules/proxy/serve.go
Normal file
133
modules/proxy/serve.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/elazarl/goproxy.v1"
|
||||
)
|
||||
|
||||
func ServeE(cmd *cobra.Command, args []string) error {
|
||||
duration, err := cmd.Flags().GetInt("duration")
|
||||
if err != nil {
|
||||
duration = 10
|
||||
}
|
||||
|
||||
host, err := cmd.Flags().GetString("host")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" {
|
||||
logrus.Fatal("host is empty")
|
||||
}
|
||||
|
||||
logrus.SetLevel(logrus.WarnLevel)
|
||||
|
||||
debug, err := cmd.Flags().GetBool("debug")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return NewProxy(host, debug, duration).Serve(29999)
|
||||
}
|
||||
|
||||
func NewProxy(host string, debug bool, duration int) *Proxy {
|
||||
return New(
|
||||
WithHost(host),
|
||||
WithLogger(log.New(io.Discard, "", log.LstdFlags)),
|
||||
WithHttps(),
|
||||
WithDebug(debug),
|
||||
WithVerbose(),
|
||||
WithFollower(),
|
||||
WithUserInfo(duration),
|
||||
)
|
||||
}
|
||||
|
||||
type Option func(*Proxy)
|
||||
|
||||
func WithLogger(logger *log.Logger) Option {
|
||||
return func(p *Proxy) {
|
||||
p.proxy.Logger = logger
|
||||
// p.proxy.Logger = log.New(io.Discard, "", log.LstdFlags)
|
||||
}
|
||||
}
|
||||
|
||||
func WithVerbose() Option {
|
||||
return func(p *Proxy) {
|
||||
p.proxy.Verbose = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithHttps() Option {
|
||||
return func(p *Proxy) {
|
||||
p.proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)
|
||||
}
|
||||
}
|
||||
|
||||
func WithDebug(debug bool) Option {
|
||||
return func(p *Proxy) {
|
||||
if debug {
|
||||
p.client = p.client.DevMode()
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithHost(h string) Option {
|
||||
return func(p *Proxy) {
|
||||
logrus.Infof("post data to host: %s", h)
|
||||
p.client = req.C().
|
||||
SetBaseURL(h).
|
||||
EnableInsecureSkipVerify().
|
||||
SetTimeout(10*time.Second).
|
||||
SetCommonBasicAuth("rogeecn", "xixi@0202")
|
||||
}
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
proxy *goproxy.ProxyHttpServer
|
||||
client *req.Client
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
func New(opts ...Option) *Proxy {
|
||||
proxy := &Proxy{
|
||||
proxy: goproxy.NewProxyHttpServer(),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(proxy)
|
||||
}
|
||||
|
||||
return proxy
|
||||
}
|
||||
|
||||
// run
|
||||
func (p *Proxy) Serve(port uint) error {
|
||||
logrus.Infof("douyin proxy start serve at: :%d", port)
|
||||
p.server = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", port),
|
||||
Handler: p.proxy,
|
||||
}
|
||||
|
||||
return p.server.ListenAndServe()
|
||||
}
|
||||
|
||||
func (p *Proxy) Shutdown() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
return p.server.Shutdown(ctx)
|
||||
}
|
||||
34
modules/web/ca.crt
Normal file
34
modules/web/ca.crt
Normal 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
262
modules/web/route_device.go
Normal 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
200
modules/web/route_expert.go
Normal 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
|
||||
}
|
||||
109
modules/web/route_follower.go
Normal file
109
modules/web/route_follower.go
Normal 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
|
||||
}
|
||||
18
modules/web/route_index.go
Normal file
18
modules/web/route_index.go
Normal 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 👋!")
|
||||
}
|
||||
71
modules/web/route_version.go
Normal file
71
modules/web/route_version.go
Normal 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
66
modules/web/routes.go
Normal 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
267
modules/web/routes_test.go
Normal 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
156
modules/web/serve.go
Normal 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()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user