feat: init
This commit is contained in:
6
.devdbrc
Normal file
6
.devdbrc
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"type": "sqlite",
|
||||
"path": "/projects/douyin-proxy/data.db"
|
||||
}
|
||||
]
|
||||
17
.gen/model/device.go
Normal file
17
.gen/model/device.go
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
type Device struct {
|
||||
ID int32 `sql:"primary_key"`
|
||||
UUID string
|
||||
Name string
|
||||
Expert string
|
||||
State string
|
||||
Note string
|
||||
}
|
||||
21
.gen/model/expert.go
Normal file
21
.gen/model/expert.go
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
type Expert struct {
|
||||
ID int32 `sql:"primary_key"`
|
||||
UID string
|
||||
SecUID string
|
||||
ShortID string
|
||||
RealName string
|
||||
NickName string
|
||||
State string
|
||||
Since int32
|
||||
Config string
|
||||
ConfigAt int32
|
||||
}
|
||||
21
.gen/model/follower.go
Normal file
21
.gen/model/follower.go
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
type Follower struct {
|
||||
ID int32 `sql:"primary_key"`
|
||||
Avatar string
|
||||
Nickname string
|
||||
SecUID string
|
||||
ShortID string
|
||||
UID string
|
||||
UniqueID string
|
||||
ExpertUID string
|
||||
Followed int32
|
||||
CreatedAt int32
|
||||
}
|
||||
90
.gen/table/device.go
Normal file
90
.gen/table/device.go
Normal file
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/sqlite"
|
||||
)
|
||||
|
||||
var Device = newDeviceTable("", "device", "")
|
||||
|
||||
type deviceTable struct {
|
||||
sqlite.Table
|
||||
|
||||
// Columns
|
||||
ID sqlite.ColumnInteger
|
||||
UUID sqlite.ColumnString
|
||||
Name sqlite.ColumnString
|
||||
Expert sqlite.ColumnString
|
||||
State sqlite.ColumnString
|
||||
Note sqlite.ColumnString
|
||||
|
||||
AllColumns sqlite.ColumnList
|
||||
MutableColumns sqlite.ColumnList
|
||||
}
|
||||
|
||||
type DeviceTable struct {
|
||||
deviceTable
|
||||
|
||||
EXCLUDED deviceTable
|
||||
}
|
||||
|
||||
// AS creates new DeviceTable with assigned alias
|
||||
func (a DeviceTable) AS(alias string) *DeviceTable {
|
||||
return newDeviceTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new DeviceTable with assigned schema name
|
||||
func (a DeviceTable) FromSchema(schemaName string) *DeviceTable {
|
||||
return newDeviceTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new DeviceTable with assigned table prefix
|
||||
func (a DeviceTable) WithPrefix(prefix string) *DeviceTable {
|
||||
return newDeviceTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new DeviceTable with assigned table suffix
|
||||
func (a DeviceTable) WithSuffix(suffix string) *DeviceTable {
|
||||
return newDeviceTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newDeviceTable(schemaName, tableName, alias string) *DeviceTable {
|
||||
return &DeviceTable{
|
||||
deviceTable: newDeviceTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newDeviceTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newDeviceTableImpl(schemaName, tableName, alias string) deviceTable {
|
||||
var (
|
||||
IDColumn = sqlite.IntegerColumn("id")
|
||||
UUIDColumn = sqlite.StringColumn("uuid")
|
||||
NameColumn = sqlite.StringColumn("name")
|
||||
ExpertColumn = sqlite.StringColumn("expert")
|
||||
StateColumn = sqlite.StringColumn("state")
|
||||
NoteColumn = sqlite.StringColumn("note")
|
||||
allColumns = sqlite.ColumnList{IDColumn, UUIDColumn, NameColumn, ExpertColumn, StateColumn, NoteColumn}
|
||||
mutableColumns = sqlite.ColumnList{UUIDColumn, NameColumn, ExpertColumn, StateColumn, NoteColumn}
|
||||
)
|
||||
|
||||
return deviceTable{
|
||||
Table: sqlite.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
UUID: UUIDColumn,
|
||||
Name: NameColumn,
|
||||
Expert: ExpertColumn,
|
||||
State: StateColumn,
|
||||
Note: NoteColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
||||
102
.gen/table/expert.go
Normal file
102
.gen/table/expert.go
Normal file
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/sqlite"
|
||||
)
|
||||
|
||||
var Expert = newExpertTable("", "expert", "")
|
||||
|
||||
type expertTable struct {
|
||||
sqlite.Table
|
||||
|
||||
// Columns
|
||||
ID sqlite.ColumnInteger
|
||||
UID sqlite.ColumnString
|
||||
SecUID sqlite.ColumnString
|
||||
ShortID sqlite.ColumnString
|
||||
RealName sqlite.ColumnString
|
||||
NickName sqlite.ColumnString
|
||||
State sqlite.ColumnString
|
||||
Since sqlite.ColumnInteger
|
||||
Config sqlite.ColumnString
|
||||
ConfigAt sqlite.ColumnInteger
|
||||
|
||||
AllColumns sqlite.ColumnList
|
||||
MutableColumns sqlite.ColumnList
|
||||
}
|
||||
|
||||
type ExpertTable struct {
|
||||
expertTable
|
||||
|
||||
EXCLUDED expertTable
|
||||
}
|
||||
|
||||
// AS creates new ExpertTable with assigned alias
|
||||
func (a ExpertTable) AS(alias string) *ExpertTable {
|
||||
return newExpertTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ExpertTable with assigned schema name
|
||||
func (a ExpertTable) FromSchema(schemaName string) *ExpertTable {
|
||||
return newExpertTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ExpertTable with assigned table prefix
|
||||
func (a ExpertTable) WithPrefix(prefix string) *ExpertTable {
|
||||
return newExpertTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ExpertTable with assigned table suffix
|
||||
func (a ExpertTable) WithSuffix(suffix string) *ExpertTable {
|
||||
return newExpertTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newExpertTable(schemaName, tableName, alias string) *ExpertTable {
|
||||
return &ExpertTable{
|
||||
expertTable: newExpertTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newExpertTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newExpertTableImpl(schemaName, tableName, alias string) expertTable {
|
||||
var (
|
||||
IDColumn = sqlite.IntegerColumn("id")
|
||||
UIDColumn = sqlite.StringColumn("uid")
|
||||
SecUIDColumn = sqlite.StringColumn("sec_uid")
|
||||
ShortIDColumn = sqlite.StringColumn("short_id")
|
||||
RealNameColumn = sqlite.StringColumn("real_name")
|
||||
NickNameColumn = sqlite.StringColumn("nick_name")
|
||||
StateColumn = sqlite.StringColumn("state")
|
||||
SinceColumn = sqlite.IntegerColumn("since")
|
||||
ConfigColumn = sqlite.StringColumn("config")
|
||||
ConfigAtColumn = sqlite.IntegerColumn("config_at")
|
||||
allColumns = sqlite.ColumnList{IDColumn, UIDColumn, SecUIDColumn, ShortIDColumn, RealNameColumn, NickNameColumn, StateColumn, SinceColumn, ConfigColumn, ConfigAtColumn}
|
||||
mutableColumns = sqlite.ColumnList{UIDColumn, SecUIDColumn, ShortIDColumn, RealNameColumn, NickNameColumn, StateColumn, SinceColumn, ConfigColumn, ConfigAtColumn}
|
||||
)
|
||||
|
||||
return expertTable{
|
||||
Table: sqlite.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
UID: UIDColumn,
|
||||
SecUID: SecUIDColumn,
|
||||
ShortID: ShortIDColumn,
|
||||
RealName: RealNameColumn,
|
||||
NickName: NickNameColumn,
|
||||
State: StateColumn,
|
||||
Since: SinceColumn,
|
||||
Config: ConfigColumn,
|
||||
ConfigAt: ConfigAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
||||
102
.gen/table/follower.go
Normal file
102
.gen/table/follower.go
Normal file
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/sqlite"
|
||||
)
|
||||
|
||||
var Follower = newFollowerTable("", "follower", "")
|
||||
|
||||
type followerTable struct {
|
||||
sqlite.Table
|
||||
|
||||
// Columns
|
||||
ID sqlite.ColumnInteger
|
||||
Avatar sqlite.ColumnString
|
||||
Nickname sqlite.ColumnString
|
||||
SecUID sqlite.ColumnString
|
||||
ShortID sqlite.ColumnString
|
||||
UID sqlite.ColumnString
|
||||
UniqueID sqlite.ColumnString
|
||||
ExpertUID sqlite.ColumnString
|
||||
Followed sqlite.ColumnInteger
|
||||
CreatedAt sqlite.ColumnInteger
|
||||
|
||||
AllColumns sqlite.ColumnList
|
||||
MutableColumns sqlite.ColumnList
|
||||
}
|
||||
|
||||
type FollowerTable struct {
|
||||
followerTable
|
||||
|
||||
EXCLUDED followerTable
|
||||
}
|
||||
|
||||
// AS creates new FollowerTable with assigned alias
|
||||
func (a FollowerTable) AS(alias string) *FollowerTable {
|
||||
return newFollowerTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new FollowerTable with assigned schema name
|
||||
func (a FollowerTable) FromSchema(schemaName string) *FollowerTable {
|
||||
return newFollowerTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new FollowerTable with assigned table prefix
|
||||
func (a FollowerTable) WithPrefix(prefix string) *FollowerTable {
|
||||
return newFollowerTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new FollowerTable with assigned table suffix
|
||||
func (a FollowerTable) WithSuffix(suffix string) *FollowerTable {
|
||||
return newFollowerTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newFollowerTable(schemaName, tableName, alias string) *FollowerTable {
|
||||
return &FollowerTable{
|
||||
followerTable: newFollowerTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newFollowerTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newFollowerTableImpl(schemaName, tableName, alias string) followerTable {
|
||||
var (
|
||||
IDColumn = sqlite.IntegerColumn("id")
|
||||
AvatarColumn = sqlite.StringColumn("avatar")
|
||||
NicknameColumn = sqlite.StringColumn("nickname")
|
||||
SecUIDColumn = sqlite.StringColumn("sec_uid")
|
||||
ShortIDColumn = sqlite.StringColumn("short_id")
|
||||
UIDColumn = sqlite.StringColumn("uid")
|
||||
UniqueIDColumn = sqlite.StringColumn("unique_id")
|
||||
ExpertUIDColumn = sqlite.StringColumn("expert_uid")
|
||||
FollowedColumn = sqlite.IntegerColumn("followed")
|
||||
CreatedAtColumn = sqlite.IntegerColumn("created_at")
|
||||
allColumns = sqlite.ColumnList{IDColumn, AvatarColumn, NicknameColumn, SecUIDColumn, ShortIDColumn, UIDColumn, UniqueIDColumn, ExpertUIDColumn, FollowedColumn, CreatedAtColumn}
|
||||
mutableColumns = sqlite.ColumnList{AvatarColumn, NicknameColumn, SecUIDColumn, ShortIDColumn, UIDColumn, UniqueIDColumn, ExpertUIDColumn, FollowedColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return followerTable{
|
||||
Table: sqlite.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Avatar: AvatarColumn,
|
||||
Nickname: NicknameColumn,
|
||||
SecUID: SecUIDColumn,
|
||||
ShortID: ShortIDColumn,
|
||||
UID: UIDColumn,
|
||||
UniqueID: UniqueIDColumn,
|
||||
ExpertUID: ExpertUIDColumn,
|
||||
Followed: FollowedColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
||||
16
.gen/table/table_use_schema.go
Normal file
16
.gen/table/table_use_schema.go
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
// UseSchema sets a new schema name for all generated table SQL builder types. It is recommended to invoke
|
||||
// this method only once at the beginning of the program.
|
||||
func UseSchema(schema string) {
|
||||
Device = Device.FromSchema(schema)
|
||||
Expert = Expert.FromSchema(schema)
|
||||
Follower = Follower.FromSchema(schema)
|
||||
}
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,13 +1,3 @@
|
||||
# ---> AL
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
*.app
|
||||
.snapshots/*
|
||||
|
||||
data.db
|
||||
node_modules
|
||||
__debug_*
|
||||
20
.vscode/launch.json
vendored
Normal file
20
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "web",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/cmd/web/web.go",
|
||||
"workspaceFolder": "/projects/douyin-proxy",
|
||||
"args": [
|
||||
"--config", "/projects/douyin-proxy/config.yml"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
39
Makefile
Normal file
39
Makefile
Normal file
@@ -0,0 +1,39 @@
|
||||
model:
|
||||
jet -source=sqlite -dsn="./data.db" -schema=follower -path=./.gen
|
||||
|
||||
fmt:
|
||||
gofumpt -w -l -extra .
|
||||
|
||||
db:
|
||||
rm -rf data.db; sqlite3 data.db < init.sql
|
||||
sync:
|
||||
scp -r dst root@10.1.1.105:/opt/netboot/www/
|
||||
|
||||
proxy:
|
||||
rm -rf ./dst/*
|
||||
CGO_ENABLE=1 go build -o dst/proxy ./cmd/proxy
|
||||
cp -ap modules/web/dst dst/
|
||||
cp certs/ca.crt dst/
|
||||
rm -rf ./dst/data.db; sqlite3 ./dst/data.db < init.sql
|
||||
|
||||
# rm -rf proxy.gz
|
||||
# tar zcvf proxy.gz ./dst
|
||||
# scp proxy.gz root@10.1.1.105:/opt/netboot/www/
|
||||
web:
|
||||
CGO_ENABLE=1 go build -o ./dst/web ./cmd/web
|
||||
|
||||
all:
|
||||
rm -rf ./dst/*
|
||||
cd frontend && npm run build && cd ..
|
||||
CGO_ENABLE=0 go build -o ./dst/web ./cmd/web
|
||||
CGO_ENABLE=0 go build -o ./dst/proxy ./cmd/proxy
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o dst/proxy.exe ./cmd/proxy
|
||||
|
||||
echo 'start /d "D:\proxy" proxy.exe' > ./dst/proxy.bat
|
||||
|
||||
win:
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o proxy.exe ./cmd/proxy
|
||||
|
||||
publish: all
|
||||
# scp -r dst/ root@39.105.111.158:/projects/douyin-proxy/
|
||||
rsync -aH --progress dst/ root@39.105.111.158:/projects/douyin-proxy/
|
||||
34
certs/ca.crt
Normal file
34
certs/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-----
|
||||
34
certs/ca.pem
Normal file
34
certs/ca.pem
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-----
|
||||
37
cmd/init.go
Normal file
37
cmd/init.go
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"dyproxy/providers/db"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// initCmd represents the init command
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "init db",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
db, err := db.Connect("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
initSql, _ := os.ReadFile("init.sql")
|
||||
_, err = db.Exec(string(initSql))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(initCmd)
|
||||
}
|
||||
BIN
cmd/proxy/apps.png
Normal file
BIN
cmd/proxy/apps.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
34
cmd/proxy/ca.crt
Normal file
34
cmd/proxy/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-----
|
||||
BIN
cmd/proxy/icon.ico
Normal file
BIN
cmd/proxy/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
15
cmd/proxy/proxy.manifest
Normal file
15
cmd/proxy/proxy.manifest
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="SomeFunkyNameHere" type="win32"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True</dpiAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
24
cmd/proxy/proxy_linux.go
Normal file
24
cmd/proxy/proxy_linux.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"dyproxy/modules/proxy"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd := &cobra.Command{
|
||||
Use: "proxy",
|
||||
Short: "serve proxy",
|
||||
RunE: proxy.ServeE,
|
||||
}
|
||||
cmd.Flags().StringP("host", "H", "https://f.jdwan.com", "default post data to host")
|
||||
cmd.Flags().BoolP("debug", "D", false, "debug mode")
|
||||
cmd.Flags().IntP("duration", "d", 10, "fans fetch duration")
|
||||
|
||||
if err := cmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
114
cmd/proxy/proxy_windows.go
Normal file
114
cmd/proxy/proxy_windows.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"dyproxy/modules/proxy"
|
||||
|
||||
"github.com/lxn/walk"
|
||||
. "github.com/lxn/walk/declarative"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
outTE *walk.TextEdit
|
||||
wnd *walk.MainWindow
|
||||
btnService *walk.PushButton
|
||||
err error
|
||||
pr *proxy.Proxy
|
||||
)
|
||||
|
||||
//go:embed ca.crt
|
||||
var ca []byte
|
||||
|
||||
func init() {
|
||||
// if go os is windows
|
||||
if strings.ToLower(os.Getenv("GOOS")) == "windows" {
|
||||
// get tmp path
|
||||
tmpPath := os.TempDir()
|
||||
// write ca.crt to tmp path
|
||||
err := os.WriteFile(tmpPath+"/ca.crt", ca, 0o644)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Info("import root cert")
|
||||
// run certutil.exe -addstore root tmpPath+"/ca.crt"
|
||||
cmd := exec.Command("certutil.exe", "-addstore", "root", tmpPath+"/ca.crt")
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
MainWindow{
|
||||
AssignTo: &wnd,
|
||||
Title: "粉丝代理服务",
|
||||
Size: Size{300, 300},
|
||||
Layout: VBox{},
|
||||
Children: []Widget{
|
||||
PushButton{
|
||||
Text: "开启服务",
|
||||
MinSize: Size{300, 50},
|
||||
AssignTo: &btnService,
|
||||
OnClicked: func() {
|
||||
errChan := make(chan error)
|
||||
if btnService.Text() == "开启服务" {
|
||||
btnService.SetEnabled(false)
|
||||
outTE.AppendText("服务启动中...\r\n")
|
||||
if pr == nil {
|
||||
pr = proxy.NewProxy("https://f.jdwan.com", false, 10)
|
||||
|
||||
go func() {
|
||||
errChan <- pr.Serve(29999)
|
||||
}()
|
||||
}
|
||||
select {
|
||||
case err := <-errChan:
|
||||
outTE.AppendText(err.Error() + "\r\n")
|
||||
case <-time.After(2 * time.Second):
|
||||
outTE.AppendText("服务启动成功\r\n")
|
||||
btnService.SetText("停止服务")
|
||||
go func() {
|
||||
err = <-errChan
|
||||
outTE.AppendText("[ERR] " + err.Error() + "\r\n")
|
||||
btnService.SetText("开启服务")
|
||||
btnService.SetEnabled(true)
|
||||
}()
|
||||
}
|
||||
btnService.SetEnabled(true)
|
||||
} else {
|
||||
btnService.SetEnabled(false)
|
||||
outTE.AppendText("服务停止中...\r\n")
|
||||
pr.Shutdown()
|
||||
outTE.AppendText("服务停止成功\r\n")
|
||||
btnService.SetText("开启服务")
|
||||
btnService.SetEnabled(true)
|
||||
}
|
||||
},
|
||||
},
|
||||
TextEdit{AssignTo: &outTE, ReadOnly: true},
|
||||
},
|
||||
}.Create()
|
||||
|
||||
wnd.Closing().Attach(func(canceled *bool, reason walk.CloseReason) {
|
||||
fmt.Println("Application closing.")
|
||||
if pr != nil {
|
||||
outTE.AppendText("服务停止中...\r\n")
|
||||
pr.Shutdown()
|
||||
outTE.AppendText("服务停止成功\r\n")
|
||||
}
|
||||
})
|
||||
|
||||
wnd.Disposing().Attach(func() {
|
||||
fmt.Println("Application has exited.")
|
||||
})
|
||||
|
||||
wnd.Run()
|
||||
}
|
||||
BIN
cmd/proxy/rsrc.syso
Normal file
BIN
cmd/proxy/rsrc.syso
Normal file
Binary file not shown.
46
cmd/root.go
Normal file
46
cmd/root.go
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "dyproxy",
|
||||
Short: "A brief description of your application",
|
||||
Long: `A longer description that spans multiple lines and likely contains
|
||||
examples and usage of using your application. For example:
|
||||
|
||||
Cobra is a CLI library for Go that empowers applications.
|
||||
This application is a tool to generate the needed files
|
||||
to quickly create a Cobra application.`,
|
||||
// Uncomment the following line if your bare application
|
||||
// has an action associated with it:
|
||||
// Run: func(cmd *cobra.Command, args []string) { },
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Here you will define your flags and configuration settings.
|
||||
// Cobra supports persistent flags, which, if defined here,
|
||||
// will be global for your application.
|
||||
|
||||
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dyproxy.yaml)")
|
||||
|
||||
// Cobra also supports local flags, which will only run
|
||||
// when this action is called directly.
|
||||
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
||||
88
cmd/test.go
Normal file
88
cmd/test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"dyproxy/.gen/model"
|
||||
"dyproxy/.gen/table"
|
||||
"dyproxy/providers/db"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/go-jet/jet/v2/qrm"
|
||||
. "github.com/go-jet/jet/v2/sqlite"
|
||||
)
|
||||
|
||||
// testCmd represents the test command
|
||||
var testCmd = &cobra.Command{
|
||||
Use: "test",
|
||||
Short: "A brief description of your command",
|
||||
Run: func(cmd *cobra.Command, argvs []string) {
|
||||
db, err := db.Connect("")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
expert := model.Expert{
|
||||
UID: "123",
|
||||
SecUID: "123",
|
||||
ShortID: "123",
|
||||
RealName: "123",
|
||||
NickName: "123",
|
||||
}
|
||||
|
||||
logrus.Warnf("expert: %+v", expert)
|
||||
stmt := table.Expert.SELECT(table.Expert.AllColumns).WHERE(table.Expert.UID.EQ(String(expert.UID)))
|
||||
sql, args := stmt.Sql()
|
||||
logrus.Debugf("sql: %s args: %+v", sql, args)
|
||||
|
||||
var ui model.Expert
|
||||
if err := stmt.Query(db, &ui); err != nil {
|
||||
if errors.Is(err, qrm.ErrNoRows) {
|
||||
_, err = table.Expert.INSERT(
|
||||
table.Expert.UID,
|
||||
table.Expert.SecUID,
|
||||
table.Expert.ShortID,
|
||||
table.Expert.RealName,
|
||||
table.Expert.NickName,
|
||||
).VALUES(
|
||||
String(expert.UID),
|
||||
String(expert.SecUID),
|
||||
String(expert.ShortID),
|
||||
String(expert.RealName),
|
||||
String(expert.NickName),
|
||||
).Exec(db)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("no rows")
|
||||
return
|
||||
}
|
||||
log.Fatal("123", err)
|
||||
}
|
||||
log.Println("ID", ui)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(testCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// testCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// testCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
||||
23
cmd/web/web.go
Normal file
23
cmd/web/web.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"dyproxy/modules/web"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// webCmd represents the web command
|
||||
func main() {
|
||||
cmd := &cobra.Command{
|
||||
Use: "web",
|
||||
Short: "A brief description of your command",
|
||||
RunE: web.ServeE,
|
||||
}
|
||||
|
||||
cmd.Flags().String("config", "config.yml", "config file")
|
||||
if err := cmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
3
config.yml
Normal file
3
config.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
version: 1.0.1
|
||||
path: /projects/douyin-robot
|
||||
static: /projects/douyin-proxy-static
|
||||
1
contracts/runner.go
Normal file
1
contracts/runner.go
Normal file
@@ -0,0 +1 @@
|
||||
package contracts
|
||||
2
dst/.gitignore
vendored
Normal file
2
dst/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
frontend/.vscode/extensions.json
vendored
Normal file
3
frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
9
frontend/README.md
Normal file
9
frontend/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (previously Volar) and disable Vetur
|
||||
|
||||
- Use [vue-tsc](https://github.com/vuejs/language-tools/tree/master/packages/tsc) for performing the same type checking from the command line, or for generating d.ts files for SFCs.
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>专家粉丝统计</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
27
frontend/package.json
Normal file
27
frontend/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"moment": "^2.30.1",
|
||||
"vue": "^3.4.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"daisyui": "^4.11.1",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.2.0",
|
||||
"vue-tsc": "^2.0.6"
|
||||
}
|
||||
}
|
||||
1807
frontend/pnpm-lock.yaml
generated
Normal file
1807
frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
41
frontend/src/App.vue
Normal file
41
frontend/src/App.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import Devices from "./components/Devices.vue";
|
||||
import Experts from "./components/Experts.vue";
|
||||
|
||||
const active = ref<Number>(1);
|
||||
|
||||
const activeTab = (index: number) => {
|
||||
active.value = index;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mx-auto py-10">
|
||||
<div role="tablist" class="tabs tabs-boxed">
|
||||
<a
|
||||
role="tab"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': active == 0 }"
|
||||
@click="activeTab(0)"
|
||||
>专家管理</a
|
||||
>
|
||||
<a
|
||||
role="tab"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': active == 1 }"
|
||||
@click="activeTab(1)"
|
||||
>设备管理</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="tab-data mt-10">
|
||||
<div v-show="active == 0">
|
||||
<Experts />
|
||||
</div>
|
||||
<div v-show="active == 1">
|
||||
<Devices />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
1
frontend/src/assets/vue.svg
Normal file
1
frontend/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
192
frontend/src/components/Devices.vue
Normal file
192
frontend/src/components/Devices.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<script setup lang="ts">
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
interface Device {
|
||||
ID: number
|
||||
UUID: string
|
||||
Name: string
|
||||
Expert: string
|
||||
ExpertName: string
|
||||
State: string
|
||||
Note: string
|
||||
}
|
||||
|
||||
interface Expert {
|
||||
ID: number
|
||||
UID: string
|
||||
SecUID: string
|
||||
ShortID: string
|
||||
RealName: string
|
||||
NickName: string
|
||||
State: string
|
||||
Since: number
|
||||
Focus: number
|
||||
Total: number
|
||||
Voice: string
|
||||
Hello: string
|
||||
}
|
||||
|
||||
|
||||
const devices = ref<Device[]>([]);
|
||||
|
||||
const experts = ref<Expert[]>([]);
|
||||
|
||||
// use axios get /experts onMount
|
||||
const loadExperts = function () {
|
||||
axios.get<Expert[]>('/api/experts').then((resp: AxiosResponse) => {
|
||||
experts.value = resp.data;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// use axios get /experts onMount
|
||||
const loadDevices = function () {
|
||||
axios.get<Device[]>('/api/devices').then((resp: AxiosResponse) => {
|
||||
devices.value = resp.data;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDevices()
|
||||
});
|
||||
|
||||
setInterval(loadDevices, 30 * 1000);
|
||||
|
||||
const currentDevice = ref<Device | null>(null);
|
||||
const showDeviceUpdateModal = (id: number) => {
|
||||
loadExperts();
|
||||
|
||||
currentDevice.value = devices.value.find((device) => device.ID === id) || null;
|
||||
|
||||
selectExpert.value = currentDevice.value?.Expert
|
||||
deviceName.value = currentDevice.value?.Name
|
||||
|
||||
const dialog = document.getElementById("show_device_update_modal") as HTMLDialogElement;
|
||||
dialog.showModal();
|
||||
};
|
||||
|
||||
const resetStateNormal = (id: number) => {
|
||||
currentDevice.value = devices.value.find((device) => device.ID === id) || null;
|
||||
if (!currentDevice.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// alert to confirm
|
||||
if (!confirm(`确定要恢复 ${currentDevice.value.UUID} 的自动关注吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { state: "" };
|
||||
axios.patch(`/api/devices/${currentDevice.value?.UUID}/state`, data).then((resp: AxiosResponse) => {
|
||||
console.log(resp.data)
|
||||
loadDevices();
|
||||
});
|
||||
};
|
||||
|
||||
const setStateStop = (id: number) => {
|
||||
currentDevice.value = devices.value.find((device) => device.ID === id) || null;
|
||||
if (!currentDevice.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// alert to confirm
|
||||
if (!confirm(`确定要停止 ${currentDevice.value.UUID} 的自动关注吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { state: "stop" };
|
||||
axios.patch(`/api/devices/${currentDevice.value?.UUID}/state`, data).then((resp: AxiosResponse) => {
|
||||
console.log(resp.data)
|
||||
loadDevices();
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = function () {
|
||||
const dialog = document.getElementById("show_device_update_modal") as HTMLDialogElement;
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
const deviceName = ref<String | null>();
|
||||
const selectExpert = ref<String | null>();
|
||||
const saveDate = () => {
|
||||
const data = {
|
||||
name: deviceName.value,
|
||||
};
|
||||
console.log(data)
|
||||
axios.patch(`/api/devices/${currentDevice.value?.UUID}/experts/${selectExpert.value}`, data).then((resp: AxiosResponse) => {
|
||||
console.log(resp.data)
|
||||
loadDevices();
|
||||
setTimeout(closeModal, 500)
|
||||
});
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dialog id="show_device_update_modal" class="modal">
|
||||
<div class="modal-box">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
|
||||
<h3 class="font-bold text-lg">{{ currentDevice?.UUID }}</h3>
|
||||
<div class="py-5">
|
||||
<label class="form-control w-full max-w-xs">
|
||||
<div class="label">
|
||||
<span class="label-text">设备名称</span>
|
||||
</div>
|
||||
<input type="text" placeholder="设备名称" class="input input-bordered w-full max-w-xs" v-model="deviceName"/>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">选择专家</span>
|
||||
</div>
|
||||
<select class="select select-bordered w-full max-w-xs" v-model="selectExpert">
|
||||
<option v-for="expert in experts" :key="expert.ID" :value="expert.UID">{{ expert.RealName }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button class="btn btn-wide btn-primary" @click="saveDate">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div v-if="devices.length == 0">
|
||||
<h1 class="text-lg text-center">还没有设备</h1>
|
||||
</div>
|
||||
|
||||
<table className="table" v-else>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>设备号</th>
|
||||
<th>名称</th>
|
||||
<th>专家</th>
|
||||
<th>停止关注</th>
|
||||
<th>备注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tr :class='idx % 2 == 1 ? "bg-slate-50" : ""' v-for="(item, idx) in devices" :key="item.ID">
|
||||
<td class="flex flex-col">
|
||||
<div class="text-lg font-semibold">{{ item.UUID }}</div>
|
||||
</td>
|
||||
<td>{{ item.Name }}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm" @click="showDeviceUpdateModal(item.ID)">{{ item.ExpertName }}</button>
|
||||
</td>
|
||||
<td>
|
||||
<button v-if="item.State == 'stop'" class="btn btn-warning btn-sm"
|
||||
@click="resetStateNormal(item.ID)">恢复</button>
|
||||
<button v-else class="btn btn-error btn-sm text-white" @click="setStateStop(item.ID)">停止</button>
|
||||
</td>
|
||||
<td>{{ item.Note }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
288
frontend/src/components/Experts.vue
Normal file
288
frontend/src/components/Experts.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<script setup lang="ts">
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import moment from 'moment';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
interface Expert {
|
||||
ID: number
|
||||
UID: string
|
||||
SecUID: string
|
||||
ShortID: string
|
||||
RealName: string
|
||||
NickName: string
|
||||
State: string
|
||||
Since: number
|
||||
Focus: number
|
||||
Total: number
|
||||
Conf: Conf
|
||||
}
|
||||
|
||||
interface Conf {
|
||||
Voice: string
|
||||
Hello: string
|
||||
Wechat: string
|
||||
Region: string[]
|
||||
NameKeyword: string[]
|
||||
Produce: boolean
|
||||
DefaultName: boolean
|
||||
DefaultAvatar: boolean
|
||||
}
|
||||
|
||||
|
||||
const experts = ref<Expert[]>([]);
|
||||
|
||||
// use axios get /experts onMount
|
||||
const loadExperts = function () {
|
||||
axios.get<Expert[]>('/api/experts').then((resp: AxiosResponse) => {
|
||||
experts.value = resp.data;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(loadExperts);
|
||||
setInterval(loadExperts, 30 * 1000);
|
||||
|
||||
|
||||
const parseTime = function (timestamp: number) {
|
||||
if (timestamp == 0) {
|
||||
return '不限制'
|
||||
}
|
||||
|
||||
return moment.unix(timestamp).format('YY/MM/DD HH:mm:ss')
|
||||
}
|
||||
const currentExpert = ref<Expert | null>(null);
|
||||
|
||||
const showExpertDateModal = (id: number) => {
|
||||
currentExpert.value = experts.value.find((expert) => expert.ID === id) || null;
|
||||
|
||||
date.value = moment.unix(currentExpert.value?.Since || 0).format('YYYY-MM-DDTHH:mm');
|
||||
|
||||
voice.value = currentExpert.value?.Conf.Voice || '';
|
||||
hello.value = currentExpert.value?.Conf.Hello || '';
|
||||
wechat.value = currentExpert.value?.Conf.Wechat || '';
|
||||
region.value = (currentExpert.value?.Conf.Region || []).join(',');
|
||||
produce.value = currentExpert.value?.Conf.Produce || false;
|
||||
default_name.value = currentExpert.value?.Conf.DefaultName || false;
|
||||
default_avatar.value = currentExpert.value?.Conf.DefaultAvatar || false;
|
||||
name_keyword.value = (currentExpert.value?.Conf.NameKeyword || []).join(',');
|
||||
|
||||
|
||||
const dialog = document.getElementById("set_date_modal") as HTMLDialogElement;
|
||||
dialog.showModal();
|
||||
};
|
||||
|
||||
const resetStateNormal = (id: number) => {
|
||||
currentExpert.value = experts.value.find((expert) => expert.ID === id) || null;
|
||||
if (!currentExpert.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// alert to confirm
|
||||
if (!confirm(`确定要恢复 ${currentExpert.value.RealName} 的自动关注吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { state: "" };
|
||||
axios.patch(`/api/experts/${currentExpert.value?.UID}/state`, data).then((resp: AxiosResponse) => {
|
||||
console.log(resp.data)
|
||||
loadExperts();
|
||||
});
|
||||
};
|
||||
|
||||
const setStateStop = (id: number) => {
|
||||
currentExpert.value = experts.value.find((expert) => expert.ID === id) || null;
|
||||
if (!currentExpert.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// alert to confirm
|
||||
if (!confirm(`确定要停止 ${currentExpert.value.RealName} 的自动关注吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { state: "stop" };
|
||||
axios.patch(`/api/experts/${currentExpert.value?.UID}/state`, data).then((resp: AxiosResponse) => {
|
||||
console.log(resp.data)
|
||||
loadExperts();
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = function () {
|
||||
const dialog = document.getElementById("set_date_modal") as HTMLDialogElement;
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
const resetDate = () => {
|
||||
const data = { since: 0 };
|
||||
axios.patch(`/api/experts/${currentExpert.value?.UID}/config`, data).then((resp: AxiosResponse) => {
|
||||
console.log(resp.data)
|
||||
loadExperts();
|
||||
setTimeout(closeModal, 500)
|
||||
});
|
||||
};
|
||||
|
||||
const date = ref('2022-02-01T01:10');
|
||||
const voice = ref('');
|
||||
const hello = ref('');
|
||||
const wechat = ref('');
|
||||
const region = ref('');
|
||||
const produce = ref(false);
|
||||
const default_name = ref(false);
|
||||
const default_avatar = ref(false);
|
||||
const name_keyword = ref('');
|
||||
|
||||
const saveDate = () => {
|
||||
let regions = region.value.replace(/\s/g, '').replace(",", "").split(',');
|
||||
let name_keywords = name_keyword.value.replace(/\s/g, '').replace(",", "").split(',');
|
||||
|
||||
const data = {
|
||||
since: Date.parse(date.value) / 1000,
|
||||
voice: voice.value,
|
||||
hello: hello.value,
|
||||
wechat: wechat.value,
|
||||
region: regions,
|
||||
produce: produce.value,
|
||||
DefaultName: default_name.value,
|
||||
DefaultAvatar: default_avatar.value,
|
||||
NameKeyword: name_keywords,
|
||||
};
|
||||
console.log(data)
|
||||
axios.patch(`/api/experts/${currentExpert.value?.UID}/config`, data).then((resp: AxiosResponse) => {
|
||||
console.log(resp.data)
|
||||
loadExperts();
|
||||
setTimeout(closeModal, 500)
|
||||
});
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dialog id="set_date_modal" class="modal">
|
||||
<div class="modal-box">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
|
||||
<h3 class="font-bold text-lg">{{ currentExpert?.RealName }}</h3>
|
||||
<div class="py-5">
|
||||
|
||||
<label class="form-control w-full mb-5">
|
||||
<div class="label">
|
||||
<span class="label-text">微信号</span>
|
||||
</div>
|
||||
<input type="text" placeholder="微信号" class="input input-bordered w-full " v-model="wechat" />
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full mb-5">
|
||||
<div class="label">
|
||||
<span class="label-text">语音ID</span>
|
||||
</div>
|
||||
<input type="text" placeholder="语音ID" class="input input-bordered w-full " v-model="voice" />
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full " mb-5>
|
||||
<div class="label">
|
||||
<span class="label-text">打招呼模板</span>
|
||||
</div>
|
||||
<textarea v-model="hello" class="textarea textarea-bordered w-full " placeholder="打招呼模板"></textarea>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full mb-5">
|
||||
<div class="label">
|
||||
<span class="label-text">屏蔽IP区域用户</span>
|
||||
</div>
|
||||
<input type="text" placeholder="屏蔽IP区域用户" class="input input-bordered w-full" v-model="region" />
|
||||
<div class="label">
|
||||
<span class="label-text-alt">多个区域用 , 号分割</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full mb-5">
|
||||
<div class="label">
|
||||
<span class="label-text">屏蔽用户名关键字</span>
|
||||
</div>
|
||||
<input type="text" placeholder="屏蔽用户名关键字" class="input input-bordered w-full" v-model="name_keyword" />
|
||||
<div class="label">
|
||||
<span class="label-text-alt">多个用,号分割</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full ">
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-1">
|
||||
<input type="checkbox" v-model="produce" class="checkbox checkbox-success" />
|
||||
<span class="label-text">屏蔽0作品用户</span>
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full ">
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-1">
|
||||
<input type="checkbox" v-model="default_name" class="checkbox checkbox-success" />
|
||||
<span class="label-text">屏蔽默认用户名用户</span>
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
|
||||
<label class="form-control w-full ">
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-1">
|
||||
<input type="checkbox" v-model="default_avatar" class="checkbox checkbox-success" />
|
||||
<span class="label-text">屏蔽默认头像名用户</span>
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">选择时间</span>
|
||||
</div>
|
||||
<input type="datetime-local" v-model="date" placeholder="请选择时间" class="input input-bordered w-full" />
|
||||
<div class="label">
|
||||
<span class="label-text-alt">获取晚于这个时间关注的粉丝数据</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button class="btn btn-default" @click="resetDate">恢复不限制</button>
|
||||
<button class="btn btn-wide btn-primary" @click="saveDate">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div v-if="experts.length == 0">
|
||||
<h1 class="text-lg text-center">还没有专家</h1>
|
||||
</div>
|
||||
|
||||
<table className="table" v-else>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>专家</th>
|
||||
<th>已关注/所有</th>
|
||||
<th>配置</th>
|
||||
<th>停止关注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tr :class='idx % 2 == 1 ? "bg-slate-50" : ""' v-for="(item, idx) in experts" :key="item.ID">
|
||||
<td class="flex flex-col">
|
||||
<div class="text-lg font-semibold">{{ item.RealName }}</div>
|
||||
<div>{{ item.UID }}</div>
|
||||
</td>
|
||||
<td>{{ item.Focus }} / {{ item.Total }}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm" @click="showExpertDateModal(item.ID)">{{ parseTime(item.Since) }}</button>
|
||||
</td>
|
||||
<td>
|
||||
<button v-if="item.State == 'stop'" class="btn btn-warning btn-sm"
|
||||
@click="resetStateNormal(item.ID)">恢复</button>
|
||||
<button v-else class="btn btn-error btn-sm text-white" @click="setStateStop(item.ID)">停止</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
5
frontend/src/main.ts
Normal file
5
frontend/src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
3
frontend/src/style.css
Normal file
3
frontend/src/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
6
frontend/static.go
Normal file
6
frontend/static.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package frontend
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed dist
|
||||
var Static embed.FS
|
||||
18
frontend/tailwind.config.js
Normal file
18
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
daisyui: {
|
||||
themes: ["light", "dark"],
|
||||
},
|
||||
plugins: [
|
||||
require("@tailwindcss/typography"),
|
||||
require('daisyui'),
|
||||
],
|
||||
}
|
||||
|
||||
25
frontend/tsconfig.json
Normal file
25
frontend/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
11
frontend/tsconfig.node.json
Normal file
11
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
15
frontend/vite.config.ts
Normal file
15
frontend/vite.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:9090',
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
76
go.mod
Normal file
76
go.mod
Normal file
@@ -0,0 +1,76 @@
|
||||
module dyproxy
|
||||
|
||||
go 1.22.1
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/go-faker/faker/v4 v4.4.1
|
||||
github.com/go-jet/jet/v2 v2.11.1
|
||||
github.com/gofiber/fiber/v3 v3.0.0-beta.2
|
||||
github.com/gofiber/utils/v2 v2.0.0-beta.4
|
||||
github.com/imroc/req/v3 v3.43.5
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rogeecn/fabfile v1.4.0
|
||||
github.com/samber/lo v1.39.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/smartystreets/goconvey v1.8.1
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
gopkg.in/elazarl/goproxy.v1 v1.0.0-20180725130230-947c36da3153
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 // indirect
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20231117061959-7cc037d33fb5 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jtolds/gls v4.20.0+incompatible // indirect
|
||||
github.com/klauspost/compress v1.17.7 // indirect
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.16.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/quic-go v0.41.0 // indirect
|
||||
github.com/refraction-networking/utls v1.6.3 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/smarty/assertions v1.15.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.52.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
178
go.sum
Normal file
178
go.sum
Normal file
@@ -0,0 +1,178 @@
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 h1:m62nsMU279qRD9PQSWD1l66kmkXzuYcnVJqL4XLeV2M=
|
||||
github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20231117061959-7cc037d33fb5 h1:iGoePcl8bIDJxxRAL2Q4E4Rt35z5m917RJb8lAvdrQw=
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20231117061959-7cc037d33fb5/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-faker/faker/v4 v4.4.1 h1:LY1jDgjVkBZWIhATCt+gkl0x9i/7wC61gZx73GTFb+Q=
|
||||
github.com/go-faker/faker/v4 v4.4.1/go.mod h1:HRLrjis+tYsbFtIHufEPTAIzcZiRu0rS9EYl2Ccwme4=
|
||||
github.com/go-jet/jet/v2 v2.11.1 h1:SEbh2lRUIiQweJpV0boWsQ4bV13x9p4h+RfajnL6vgM=
|
||||
github.com/go-jet/jet/v2 v2.11.1/go.mod h1:+DTofDkGp1c0vpooXWEZyNhyi0k0mL7N2W9tdP4YqfA=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/gofiber/fiber/v3 v3.0.0-beta.2 h1:mVVgt8PTaHGup3NGl/+7U7nEoZaXJ5OComV4E+HpAao=
|
||||
github.com/gofiber/fiber/v3 v3.0.0-beta.2/go.mod h1:w7sdfTY0okjZ1oVH6rSOGvuACUIt0By1iK0HKUb3uqM=
|
||||
github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co=
|
||||
github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/imroc/req/v3 v3.43.5 h1:fL7dOEfld+iEv1rwnIxseJz2/Y7JZ/HgbAURLZkat80=
|
||||
github.com/imroc/req/v3 v3.43.5/go.mod h1:SQIz5iYop16MJxbo8ib+4LnostGCok8NQf8ToyQc2xA=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/onsi/ginkgo/v2 v2.16.0 h1:7q1w9frJDzninhXxjZd+Y/x54XNjG/UlRLIYPZafsPM=
|
||||
github.com/onsi/ginkgo/v2 v2.16.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
|
||||
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
|
||||
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/quic-go v0.41.0 h1:aD8MmHfgqTURWNJy48IYFg2OnxwHT3JL7ahGs73lb4k=
|
||||
github.com/quic-go/quic-go v0.41.0/go.mod h1:qCkNjqczPEvgsOnxZ0eCD14lv+B2LHlFAB++CNOh9hA=
|
||||
github.com/refraction-networking/utls v1.6.3 h1:MFOfRN35sSx6K5AZNIoESsBuBxS2LCgRilRIdHb6fDc=
|
||||
github.com/refraction-networking/utls v1.6.3/go.mod h1:yil9+7qSl+gBwJqztoQseO6Pr3h62pQoY1lXiNR/FPs=
|
||||
github.com/rogeecn/fabfile v1.4.0 h1:Rw7/7OH8cV4aRPw79Oa4hHHFKaC/ol+sNmGcB/usHaQ=
|
||||
github.com/rogeecn/fabfile v1.4.0/go.mod h1:EPwX7TtVcIWSLJkJAqxSzYjM/aV1Q0wymcaXqnMgzas=
|
||||
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
|
||||
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
|
||||
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
|
||||
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
|
||||
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
|
||||
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/elazarl/goproxy.v1 v1.0.0-20180725130230-947c36da3153 h1:i2sumy6EgvN2dbX7HPhoDc7hLyoym3OYdU5HlvUUrpE=
|
||||
gopkg.in/elazarl/goproxy.v1 v1.0.0-20180725130230-947c36da3153/go.mod h1:xzjpkyedLMz3EXUTBbkRuuGPsxfsBX3Sy7J6kC9Gvoc=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
49
init.sql
Normal file
49
init.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
CREATE TABLE "main"."expert" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"uid" text NOT NULL DEFAULT '',
|
||||
"sec_uid" text NOT NULL DEFAULT '',
|
||||
"short_id" text NOT NULL DEFAULT '',
|
||||
"real_name" text NOT NULL DEFAULT '',
|
||||
"nick_name" text NOT NULL DEFAULT '',
|
||||
"state" text NOT NULL DEFAULT 'enable',
|
||||
"since" INTEGER NOT NULL DEFAULT 0,
|
||||
"config" text NOT NULL DEFAULT '',
|
||||
"config_at" INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "main"."idx_expert_uid" ON "expert" (
|
||||
"uid"
|
||||
);
|
||||
|
||||
CREATE TABLE "main"."follower" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"avatar" TEXT NOT NULL DEFAULT '',
|
||||
"nickname" TEXT NOT NULL DEFAULT '',
|
||||
"sec_uid" text NOT NULL DEFAULT '',
|
||||
"short_id" text NOT NULL DEFAULT '',
|
||||
"uid" text NOT NULL DEFAULT '',
|
||||
"unique_id" text NOT NULL DEFAULT '',
|
||||
"expert_uid" text NOT NULL DEFAULT '',
|
||||
"followed" INTEGER NOT NULL DEFAULT 0,
|
||||
"created_at" INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "main"."idx_follower_uid" ON "follower" (
|
||||
"uid"
|
||||
);
|
||||
|
||||
CREATE TABLE "main"."device" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"uuid" text NOT NULL DEFAULT '',
|
||||
"name" text NOT NULL DEFAULT '',
|
||||
"expert" text NOT NULL DEFAULT '',
|
||||
"state" text NOT NULL DEFAULT 'enable',
|
||||
"note" text NOT NULL DEFAULT '',
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "main"."idx_device_uuid" ON "device" (
|
||||
"uuid"
|
||||
);
|
||||
10
main.go
Normal file
10
main.go
Normal file
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package main
|
||||
|
||||
import "dyproxy/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
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()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
6
package.json
Normal file
6
package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"moment": "^2.30.1"
|
||||
}
|
||||
}
|
||||
10
pkg/utils/ptr.go
Normal file
10
pkg/utils/ptr.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package utils
|
||||
|
||||
func BytesToStringPtr(b []byte) *string {
|
||||
s := string(b)
|
||||
return &s
|
||||
}
|
||||
|
||||
func StrPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
95
pnpm-lock.yaml
generated
Normal file
95
pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,95 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
axios:
|
||||
specifier: ^1.7.2
|
||||
version: 1.7.2
|
||||
moment:
|
||||
specifier: ^2.30.1
|
||||
version: 2.30.1
|
||||
|
||||
packages:
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==, tarball: https://npm.hub.118848.xyz/repository/npm/asynckit/-/asynckit-0.4.0.tgz}
|
||||
|
||||
axios@1.7.2:
|
||||
resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==, tarball: https://npm.hub.118848.xyz/repository/npm/axios/-/axios-1.7.2.tgz}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==, tarball: https://npm.hub.118848.xyz/repository/npm/combined-stream/-/combined-stream-1.0.8.tgz}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==, tarball: https://npm.hub.118848.xyz/repository/npm/delayed-stream/-/delayed-stream-1.0.0.tgz}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
follow-redirects@1.15.6:
|
||||
resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==, tarball: https://npm.hub.118848.xyz/repository/npm/follow-redirects/-/follow-redirects-1.15.6.tgz}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
form-data@4.0.0:
|
||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==, tarball: https://npm.hub.118848.xyz/repository/npm/form-data/-/form-data-4.0.0.tgz}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, tarball: https://npm.hub.118848.xyz/repository/npm/mime-db/-/mime-db-1.52.0.tgz}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==, tarball: https://npm.hub.118848.xyz/repository/npm/mime-types/-/mime-types-2.1.35.tgz}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
moment@2.30.1:
|
||||
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==, tarball: https://npm.hub.118848.xyz/repository/npm/moment/-/moment-2.30.1.tgz}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==, tarball: https://npm.hub.118848.xyz/repository/npm/proxy-from-env/-/proxy-from-env-1.1.0.tgz}
|
||||
|
||||
snapshots:
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
axios@1.7.2:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.6
|
||||
form-data: 4.0.0
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
follow-redirects@1.15.6: {}
|
||||
|
||||
form-data@4.0.0:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
moment@2.30.1: {}
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
24
providers/db/sqlite.go
Normal file
24
providers/db/sqlite.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func Truncate(db *sql.DB, table string) {
|
||||
db.Exec("DELETE FROM " + table + ";")
|
||||
db.Exec("UPDATE SQLITE_SEQUENCE SET seq = 0 WHERE name = '<table>" + table + "';")
|
||||
}
|
||||
|
||||
func Connect(file string) (*sql.DB, error) {
|
||||
if file == "" {
|
||||
file = "data.db"
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite3", file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
BIN
robot/audio/caixueying.m4a
Normal file
BIN
robot/audio/caixueying.m4a
Normal file
Binary file not shown.
BIN
robot/audio/chenhong.m4a
Normal file
BIN
robot/audio/chenhong.m4a
Normal file
Binary file not shown.
BIN
robot/audio/dubaojun.m4a
Normal file
BIN
robot/audio/dubaojun.m4a
Normal file
Binary file not shown.
BIN
robot/audio/genghonghai.m4a
Normal file
BIN
robot/audio/genghonghai.m4a
Normal file
Binary file not shown.
BIN
robot/audio/liliuji.m4a
Normal file
BIN
robot/audio/liliuji.m4a
Normal file
Binary file not shown.
BIN
robot/audio/liudong.m4a
Normal file
BIN
robot/audio/liudong.m4a
Normal file
Binary file not shown.
BIN
robot/audio/liulianqi.m4a
Normal file
BIN
robot/audio/liulianqi.m4a
Normal file
Binary file not shown.
BIN
robot/audio/wangguozhong.m4a
Normal file
BIN
robot/audio/wangguozhong.m4a
Normal file
Binary file not shown.
BIN
robot/audio/xueyongdong.m4a
Normal file
BIN
robot/audio/xueyongdong.m4a
Normal file
Binary file not shown.
BIN
robot/audio/yaobaosen.m4a
Normal file
BIN
robot/audio/yaobaosen.m4a
Normal file
Binary file not shown.
BIN
robot/audio/zhangguangsheng.m4a
Normal file
BIN
robot/audio/zhangguangsheng.m4a
Normal file
Binary file not shown.
BIN
robot/audio/zhangzhaofa.m4a
Normal file
BIN
robot/audio/zhangzhaofa.m4a
Normal file
Binary file not shown.
545
robot/lib.js
Normal file
545
robot/lib.js
Normal file
@@ -0,0 +1,545 @@
|
||||
let _global_duration = 1000;
|
||||
|
||||
let global = {
|
||||
setDuration: function (duration) {
|
||||
_global_duration = duration
|
||||
},
|
||||
}
|
||||
|
||||
let store = storages.create("_##_@_DY_Proxy");
|
||||
|
||||
const project = "autojs"
|
||||
|
||||
let pwdFile = function (path) {
|
||||
let pwd = engines.myEngine().cwd()
|
||||
|
||||
if (!path) {
|
||||
return pwd
|
||||
}
|
||||
|
||||
if (path) {
|
||||
path = path.replace(/^\//, "")
|
||||
}
|
||||
|
||||
return `${pwd}/${project}/${path}`.replace(/\/\//g, "/").replace(/\/$/, "")
|
||||
}
|
||||
|
||||
let pageUser = {
|
||||
activity: "com.ss.android.ugc.aweme.profile.ui.UserProfileActivity",
|
||||
wait: function () {
|
||||
waitForActivity(this.activity)
|
||||
},
|
||||
open: function () {
|
||||
let activity = "snssdk1128://user/profile"
|
||||
app.startActivity({
|
||||
packageName: "com.ss.android.ugc.aweme",
|
||||
action: "android.intent.action.VIEW",
|
||||
data: activity,
|
||||
});
|
||||
let appId = id("com.miui.securitycore:id/app1").findOne(1000)
|
||||
if (appId) {
|
||||
appId.click()
|
||||
}
|
||||
|
||||
waitForActivity("com.ss.android.ugc.aweme.main.MainActivity")
|
||||
|
||||
let pt = className("android.widget.TextView").find().findOne(text("我")).bounds()
|
||||
click(pt.centerX(), pt.centerY())
|
||||
},
|
||||
openUser: function (uid) {
|
||||
let activity = "snssdk1128://user/profile/{uid}".replace("{uid}", uid)
|
||||
app.startActivity({
|
||||
packageName: "com.ss.android.ugc.aweme",
|
||||
action: "android.intent.action.VIEW",
|
||||
data: activity,
|
||||
});
|
||||
let appId = id("com.miui.securitycore:id/app1").findOne(1000)
|
||||
if (appId) {
|
||||
appId.click()
|
||||
}
|
||||
waitForActivity("com.ss.android.ugc.aweme.profile.ui.UserProfileActivity")
|
||||
},
|
||||
hasPublishItems: function () {
|
||||
let txt = "还没有作品"
|
||||
let id = "com.ss.android.ugc.aweme:id/title"
|
||||
|
||||
let elem = text(txt).findOne(2 * 1000)
|
||||
if (!elem) {
|
||||
return true
|
||||
}
|
||||
|
||||
return !(elem.id() == id)
|
||||
},
|
||||
|
||||
getBtnGuanZhu: function () {
|
||||
let txt = "关注"
|
||||
let _id = "r2s"
|
||||
|
||||
let elems = text(txt).find()
|
||||
if (elems.empty()) {
|
||||
log("找不到关注按钮")
|
||||
return false
|
||||
}
|
||||
|
||||
let elem = elems.findOne(id(_id))
|
||||
if (!elem) {
|
||||
log("找不到关注按钮 id")
|
||||
return false
|
||||
}
|
||||
|
||||
return elem
|
||||
},
|
||||
|
||||
getBtnSiXin: function () {
|
||||
let txt = "私信"
|
||||
let desc = "私信"
|
||||
|
||||
let elem = text(txt).findOne(2 * this.duration)
|
||||
if (!elem) {
|
||||
log("找不到私信按钮")
|
||||
return false
|
||||
}
|
||||
|
||||
if (elem.desc() != desc) {
|
||||
log("私信按钮 desc 不匹配")
|
||||
return false
|
||||
}
|
||||
return elem
|
||||
},
|
||||
|
||||
getIPLocation: function () {
|
||||
let txt = "IP:"
|
||||
let _pid = "com.ss.android.ugc.aweme:id/tpw"
|
||||
|
||||
let elems = textStartsWith(txt).find()
|
||||
if (elems.empty()) {
|
||||
log("找不到 IP 标签")
|
||||
return false
|
||||
}
|
||||
let elem = null
|
||||
elems.forEach(function (e) {
|
||||
if (elem) return
|
||||
if (e.parent().id() == _pid) {
|
||||
elem = e
|
||||
}
|
||||
})
|
||||
if (!elem) {
|
||||
log("找不到 IP 标签 id")
|
||||
return false
|
||||
}
|
||||
|
||||
return elem.text().replace(txt, "").trim()
|
||||
},
|
||||
|
||||
clickBtnSiXin: function () {
|
||||
let elem = this.getBtnSiXin()
|
||||
if (!elem) {
|
||||
return false
|
||||
}
|
||||
|
||||
elem.click()
|
||||
sleep(2 * this.duration)
|
||||
return true
|
||||
},
|
||||
|
||||
clickBtnGuanZhu: function () {
|
||||
let elem = this.getBtnGuanZhu()
|
||||
if (!elem) {
|
||||
return false
|
||||
}
|
||||
|
||||
elem.click()
|
||||
sleep(2 * this.duration)
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
let pageChat = {
|
||||
getBtnVoice: function () {
|
||||
let _desc = "语音"
|
||||
let _class = "android.widget.Button"
|
||||
|
||||
let elem = className(_class).find().findOne(desc(_desc))
|
||||
if (!elem) {
|
||||
log("找不到 语音 按钮")
|
||||
return false
|
||||
}
|
||||
return elem
|
||||
},
|
||||
getBtnMyLoveSend: function () {
|
||||
let _txt = "发送"
|
||||
let _id = "com.ss.android.ugc.aweme:id/send"
|
||||
|
||||
let elem = id(_id).findOne(2 * _global_duration)
|
||||
if (!elem) {
|
||||
log("找不到发送按钮")
|
||||
return false
|
||||
}
|
||||
|
||||
return elem
|
||||
},
|
||||
getMyLoveFirstProduct: function () {
|
||||
let _class = "com.bytedance.ies.dmt.ui.widget.DmtTextView";
|
||||
let loves = className(_class).find()
|
||||
if (loves.empty()) {
|
||||
log("找不到我的喜欢")
|
||||
return false
|
||||
}
|
||||
|
||||
return loves.shift()
|
||||
},
|
||||
getMyLoveButton: function () {
|
||||
let _txt = "我的喜欢"
|
||||
let _class = "android.widget.TextView"
|
||||
|
||||
let _pclass = "android.view.ViewGroup"
|
||||
|
||||
let elem = className(_pclass).find().findOne(text(_txt)).parent()
|
||||
if (!elem) {
|
||||
log("找不到 我的喜欢 按钮")
|
||||
return false
|
||||
}
|
||||
|
||||
return elem
|
||||
},
|
||||
getPanelMore: function () {
|
||||
let _class = "androidx.viewpager.widget.ViewPager"
|
||||
|
||||
let elem = className(_class).findOne(2 * _global_duration)
|
||||
if (!elem) {
|
||||
log("找不到 更多面板")
|
||||
return false
|
||||
}
|
||||
|
||||
return elem
|
||||
},
|
||||
getBtnCloseMore: function () {
|
||||
let _desc = "关闭面板"
|
||||
let _class = "android.widget.Button"
|
||||
|
||||
let elem = className(_class).find().findOne(desc(_desc))
|
||||
if (!elem) {
|
||||
log("找不到关闭面板按钮")
|
||||
return false
|
||||
}
|
||||
|
||||
return elem
|
||||
},
|
||||
getBtnMore: function () {
|
||||
let _desc = "更多面板"
|
||||
let _class = "android.widget.Button"
|
||||
|
||||
let elem = className(_class).find().findOne(desc(_desc))
|
||||
if (!elem) {
|
||||
log("找不到更多面板按钮")
|
||||
return false
|
||||
}
|
||||
log(elem.id())
|
||||
|
||||
return elem
|
||||
},
|
||||
clickBtnCloseMore: function () {
|
||||
let elem = this.getBtnCloseMore()
|
||||
if (!elem) {
|
||||
return false
|
||||
}
|
||||
|
||||
elem.click()
|
||||
sleep(_global_duration)
|
||||
return true
|
||||
},
|
||||
clickBtnMore: function () {
|
||||
let elem = this.getBtnMore()
|
||||
if (!elem) {
|
||||
return false
|
||||
}
|
||||
|
||||
elem.click()
|
||||
sleep(_global_duration)
|
||||
return true
|
||||
},
|
||||
swipeMorePanelRight: function () {
|
||||
let elem = this.getPanelMore()
|
||||
if (!elem) {
|
||||
return false
|
||||
}
|
||||
|
||||
// elem.scrollLeft()
|
||||
let x = elem.bounds().centerX() + elem.bounds().width() / 4
|
||||
let y = elem.bounds().centerY()
|
||||
let x1 = elem.bounds().left
|
||||
let y1 = elem.bounds().centerY()
|
||||
log(x, y, x1, y1)
|
||||
swipe(x, y, x1, y1, 200)
|
||||
sleep(_global_duration)
|
||||
return true
|
||||
},
|
||||
swipeMorePanelLeft: function () {
|
||||
let elem = this.getPanelMore()
|
||||
if (!elem) {
|
||||
return false
|
||||
}
|
||||
|
||||
let x = elem.bounds().centerX() - elem.bounds().width() / 4
|
||||
let y = elem.bounds().centerY()
|
||||
let x1 = elem.bounds().right
|
||||
let y1 = elem.bounds().centerY()
|
||||
log(x, y, x1, y1)
|
||||
swipe(x, y, x1, y1, 200)
|
||||
sleep(_global_duration)
|
||||
return true
|
||||
},
|
||||
|
||||
clickBtnMyLove: function () {
|
||||
let elem = this.getMyLoveButton()
|
||||
if (!elem) {
|
||||
return false
|
||||
}
|
||||
|
||||
elem.click()
|
||||
// click(elem.bounds().centerX(), elem.bounds().centerY())
|
||||
sleep(_global_duration)
|
||||
return true
|
||||
},
|
||||
clickBtnMyLoveFirstProduct: function () {
|
||||
let elem = this.getMyLoveFirstProduct()
|
||||
if (!elem) {
|
||||
return false
|
||||
}
|
||||
|
||||
log(elem.className())
|
||||
click(elem.bounds().centerX(), elem.bounds().centerY())
|
||||
sleep(_global_duration)
|
||||
return true
|
||||
},
|
||||
clickBtnMyLoveSend: function () {
|
||||
let elem = this.getBtnMyLoveSend()
|
||||
if (!elem) {
|
||||
return false
|
||||
}
|
||||
|
||||
elem.click()
|
||||
sleep(_global_duration)
|
||||
return true
|
||||
},
|
||||
pressBtnVoice: function (duration) {
|
||||
let elem = this.getBtnVoice()
|
||||
if (!elem) {
|
||||
return false
|
||||
}
|
||||
|
||||
press(elem.bounds().centerX(), elem.bounds().centerY(), duration)
|
||||
sleep(_global_duration)
|
||||
return true
|
||||
},
|
||||
sendAudioVoice: function (voice) {
|
||||
let elem = this.getBtnVoice()
|
||||
if (!elem) {
|
||||
log("找不到 语音 按钮")
|
||||
return false
|
||||
}
|
||||
|
||||
var musicDuration = threads.disposable();
|
||||
threads.start(function () {
|
||||
sleep(500)
|
||||
//播放音乐
|
||||
media.playMusic(pwdFile(`audio/${voice}.m4a`));
|
||||
media.pauseMusic()
|
||||
|
||||
//让音乐播放完
|
||||
let duration = media.getMusicDuration()
|
||||
musicDuration.setAndNotify(duration)
|
||||
sleep(500)
|
||||
media.resumeMusic()
|
||||
|
||||
sleep(duration);
|
||||
});
|
||||
|
||||
let duration = musicDuration.blockedGet()
|
||||
|
||||
press(elem.bounds().centerX(), elem.bounds().centerY(), 1000 + duration)
|
||||
sleep(_global_duration)
|
||||
return true
|
||||
|
||||
},
|
||||
isSendVoiceMode: function () {
|
||||
let _txt = "按住 说话"
|
||||
return text(_txt).findOne(2 * _global_duration)
|
||||
},
|
||||
getBtnVoiceToTxt: function () {
|
||||
return desc("文本").findOne(2 * _global_duration)
|
||||
},
|
||||
getInputSendTxt: function () {
|
||||
return text("发送消息").findOne(2 * _global_duration)
|
||||
},
|
||||
getBtnSendTxt: function () {
|
||||
return desc('发送').findOne(2 * _global_duration)
|
||||
},
|
||||
sendTxt: function (txt) {
|
||||
if (this.isSendVoiceMode()) {
|
||||
let elem = this.getBtnVoiceToTxt()
|
||||
if (!elem) {
|
||||
return false
|
||||
}
|
||||
elem.click()
|
||||
sleep(_global_duration)
|
||||
}
|
||||
|
||||
elem = this.getInputSendTxt()
|
||||
if (!elem) {
|
||||
log("找不到发送消息输入框")
|
||||
return false
|
||||
}
|
||||
|
||||
elem.setText(txt)
|
||||
sleep(2 * _global_duration)
|
||||
|
||||
if (!this.getBtnSendTxt().click()) {
|
||||
log("找不到发送按钮")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
let pageFriend = {
|
||||
activity: "com.ss.android.ugc.aweme.following.ui.FollowRelationTabActivity",
|
||||
wait: function () {
|
||||
lib.waitForActivity(_activity)
|
||||
},
|
||||
open: function () {
|
||||
// app.startActivity(this.activity)
|
||||
},
|
||||
listFriends: function () {
|
||||
let _id = "com.ss.android.ugc.aweme:id/content"
|
||||
let elems = id(_id).find()
|
||||
if (elems.empty()) {
|
||||
log("找不到 好友列表")
|
||||
return []
|
||||
}
|
||||
|
||||
let friends = [];
|
||||
elems.forEach((elem, i) => {
|
||||
let [name, online] = elem.desc().split(",")
|
||||
|
||||
let btn = elem.findOne(text("发私信"))
|
||||
log(name, online, btn)
|
||||
friends.push({
|
||||
name: name,
|
||||
online: online,
|
||||
btn: btn,
|
||||
})
|
||||
})
|
||||
|
||||
return friends
|
||||
},
|
||||
}
|
||||
|
||||
let messages = [
|
||||
[
|
||||
"你好,是要男性健康方面的问题吗",
|
||||
"{voice}",
|
||||
"十我详谈吧,这里敏感词太多",
|
||||
"这里敏感词多",
|
||||
"这里有时候不常在,消息回复不及时",
|
||||
],
|
||||
[
|
||||
"{wechat}"
|
||||
],
|
||||
[
|
||||
"↑微訫号",
|
||||
"微訫号",
|
||||
"↑微訫号",
|
||||
"↑徽沁",
|
||||
"↑微沁",
|
||||
],
|
||||
[
|
||||
"十了说一下给你通过",
|
||||
"加上一对一给你解答",
|
||||
"",
|
||||
"加上详谈",
|
||||
"加上详细沟通一下你的情况",
|
||||
]
|
||||
];
|
||||
|
||||
let st = {
|
||||
times: function () {
|
||||
return store.get('__times', 0)
|
||||
},
|
||||
addTimes: function () {
|
||||
return store.put('__times', this.times() + 1)
|
||||
},
|
||||
host: function () {
|
||||
let host = store.get("__host", "")
|
||||
log("HOST=", host)
|
||||
if (host.length == 0) {
|
||||
host = "http://10.1.1.108:8080"
|
||||
store.put("__host", host)
|
||||
}
|
||||
return host
|
||||
},
|
||||
lastID: function (id) {
|
||||
if (id) {
|
||||
return store.put('__lastID', id)
|
||||
}
|
||||
return store.get('__lastID', 0)
|
||||
}
|
||||
}
|
||||
let fetch = function () {
|
||||
//指定确定按钮点击时要执行的动作
|
||||
const deviceID = device.getAndroidId()
|
||||
|
||||
let lastID = st.lastID()
|
||||
let path = "{host}/api/devices/{deviceID}/follower"
|
||||
let url = path.replace("{host}", st.host()).replace("{deviceID}", deviceID)
|
||||
if (lastID != 0) {
|
||||
url = url + "?lastID=" + lastID
|
||||
}
|
||||
// log("url =", url)
|
||||
|
||||
let resp = http.get(url)
|
||||
let body = resp.body.json()
|
||||
if (body == null) {
|
||||
toastLog("没有新的数据")
|
||||
return
|
||||
}
|
||||
|
||||
st.lastID(body.ID)
|
||||
// log("body =", body)
|
||||
|
||||
return body.UID
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
duration: 1000,
|
||||
global: global,
|
||||
page: {
|
||||
user: pageUser,
|
||||
chat: pageChat,
|
||||
friend: pageFriend,
|
||||
},
|
||||
messages: messages,
|
||||
fetch: fetch,
|
||||
store: st,
|
||||
kill: function () {
|
||||
let name = "com.ss.android.ugc.aweme"
|
||||
app.openAppSetting(name);//通过包名打开应用的详情页(设置页)
|
||||
text(app.getAppName(name)).waitFor();//通过包名获取已安装的应用名称,判断是否已经跳转至该app的应用设置界面
|
||||
sleep(500);//稍微休息一下,不然看不到运行过程,自己用时可以删除这行
|
||||
let is_sure = textMatches(/(.*强.*|.*停.*|.*结.*)/).findOne();//在app的应用设置界面找寻包含“强”,“停”,“结”,“行”的控件
|
||||
//特别注意,应用设置界面可能存在并非关闭该app的控件,但是包含上述字样的控件,如果某个控件包含名称“行”字
|
||||
//textMatches(/(.*强.*|.*停.*|.*结.*|.*行.*)/)改为textMatches(/(.*强.*|.*停.*|.*结.*)/)
|
||||
//或者结束应用的控件名为“结束运行”直接将textMatches(/(.*强.*|.*停.*|.*结.*|.*行.*)/)改为text("结束运行")
|
||||
|
||||
if (is_sure.enabled()) {//判断控件是否已启用(想要关闭的app是否运行)
|
||||
is_sure.parent().click();//结束应用的控件如果无法点击,需要在布局中找寻它的父控件,如果还无法点击,再上一级控件,本案例就是控件无法点击
|
||||
textMatches(/(.*确.*|.*定.*)/).findOne().click();//需找包含“确”,“定”的控件
|
||||
log(app.getAppName(name) + "应用已被关闭");
|
||||
sleep(1000);
|
||||
back();
|
||||
} else {
|
||||
log(app.getAppName(name) + "应用不能被正常关闭或不在后台运行");
|
||||
back();
|
||||
}
|
||||
},
|
||||
}
|
||||
29
robot/main.js
Normal file
29
robot/main.js
Normal file
@@ -0,0 +1,29 @@
|
||||
let lib = require("./autojs/lib.js")
|
||||
log(currentActivity())
|
||||
// exit()
|
||||
// 先回主页面
|
||||
home()
|
||||
sleep(1000)
|
||||
|
||||
|
||||
// lib.kill()
|
||||
// sleep(2000)
|
||||
|
||||
lib.page.user.open()
|
||||
// let uid = lib.fetch()
|
||||
// log("UID =",uid)
|
||||
// if (uid) {
|
||||
// lib.page.user.open(uid)
|
||||
// // lib.store.addTimes()
|
||||
// sleep(1000)
|
||||
// // home()
|
||||
// } else {
|
||||
// toastLog("没有找到用户ID")
|
||||
// }
|
||||
|
||||
|
||||
// className("android.widget.TextView").find().forEach(function (tv) {
|
||||
// if (tv.text() != "") {
|
||||
// log(tv.text());
|
||||
// }
|
||||
// })
|
||||
81
robot/main.js.bak
Normal file
81
robot/main.js.bak
Normal file
@@ -0,0 +1,81 @@
|
||||
"ui";
|
||||
|
||||
// auto.waitFor();
|
||||
|
||||
var storage = storages.create("_##_@_DY_Proxy");
|
||||
|
||||
ui.layout(
|
||||
<vertical padding="16" gravity="center">
|
||||
<text id="_id" textSize="28sp" textColor="gray" text="设备号" gravity="center" />
|
||||
<text id="id" textSize="28sp" textColor="red" text="" gravity="center" textStyle="bold" bg="#0f0f0f" />
|
||||
<text textSize="16sp" textColor="gray" text="点击序列号复制到剪贴板" gravity="center" />
|
||||
</vertical>
|
||||
);
|
||||
|
||||
let times = storage.get('__times', 0)
|
||||
let host = storage.get("__host", "")
|
||||
log("HOST=", host)
|
||||
if (host.length == 0) {
|
||||
host = "http://10.1.1.108:8080"
|
||||
storage.put("__host", host)
|
||||
}
|
||||
|
||||
//指定确定按钮点击时要执行的动作
|
||||
const deviceID = device.getAndroidId()
|
||||
// split device per 4 char with -
|
||||
let __deviceID = deviceID.slice(0, 4) + "-" + deviceID.slice(4, 8) + "-" + deviceID.slice(8, 12) + "-" + deviceID.slice(12, 16)
|
||||
log(__deviceID)
|
||||
ui.id.setText(__deviceID)
|
||||
ui.id.click(() => {
|
||||
setClip(__deviceID)
|
||||
toast("已复制到剪贴板")
|
||||
})
|
||||
|
||||
//启用按键监听
|
||||
events.setKeyInterceptionEnabled("volume_up", true);
|
||||
events.setKeyInterceptionEnabled("volume_down", true);
|
||||
|
||||
events.observeKey();
|
||||
events.observeNotification();
|
||||
|
||||
let eventVolumeUp = function (event) {
|
||||
toast("音量上键被按下了");
|
||||
let lastID = storage.get('__lastID', 0)
|
||||
|
||||
let path = "{host}/api/devices/{deviceID}/follower"
|
||||
let url = path.replace("{host}", host).replace("{deviceID}", deviceID)
|
||||
if (lastID != 0) {
|
||||
url = url + "?lastID=" + lastID
|
||||
}
|
||||
log("url =", url)
|
||||
|
||||
// let resp = http.get(url)
|
||||
// let body = resp.body.json()
|
||||
// if (body == null) {
|
||||
// toast("没有新的数据")
|
||||
// return
|
||||
// }
|
||||
// storage.put('__lastID', body.ID)
|
||||
// log("lastID =", storage.get('__lastID', 0))
|
||||
// log("body =", body)
|
||||
}
|
||||
|
||||
let eventHandler = {
|
||||
volumeUp: eventVolumeUp,
|
||||
volumeDown: function (event) {
|
||||
toast("音量下键被按下了");
|
||||
exit();
|
||||
},
|
||||
notification: function (n) {
|
||||
log("通知时间为:" + new Date(n.when));
|
||||
log("应用包名为:" + n.getPackageName());
|
||||
log("标题:" + n.getTitle());
|
||||
log("内容:" + n.getText());
|
||||
}
|
||||
};
|
||||
|
||||
events.onKeyUp("volume_up", eventHandler.volumeUp);
|
||||
events.onKeyDown("volume_down", eventHandler.volumeDown);
|
||||
events.on("notification", eventHandler.notification);
|
||||
|
||||
// setInterval(() => { log("WORKING...") }, 1000);
|
||||
10
robot/project.json
Normal file
10
robot/project.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "X",
|
||||
"main": "main.js",
|
||||
"ignore": [
|
||||
"build"
|
||||
],
|
||||
"packageName": "com.example",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": 1
|
||||
}
|
||||
7
robot/readme.md
Normal file
7
robot/readme.md
Normal file
@@ -0,0 +1,7 @@
|
||||
git config --global user.email "rogeecn@qq.com"
|
||||
git config --global user.name "Rogee"
|
||||
|
||||
activity:
|
||||
|
||||
com.ss.android.ugc.aweme.profile.ui.UserProfileActivity
|
||||
|
||||
6
robot/test.js
Normal file
6
robot/test.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// random char/num 16 length
|
||||
const id = "1234567890abcdef"
|
||||
let deviceID = id.toUpperCase()
|
||||
// cut deviceID per 4 char with -, no regexp
|
||||
deviceID = deviceID.slice(0, 4) + "-" + deviceID.slice(4, 8) + "-" + deviceID.slice(8, 12) + "-" + deviceID.slice(12, 16)
|
||||
console.log(deviceID)
|
||||
Reference in New Issue
Block a user