feat: init

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

6
.devdbrc Normal file
View File

@@ -0,0 +1,6 @@
[
{
"type": "sqlite",
"path": "/projects/douyin-proxy/data.db"
}
]

17
.gen/model/device.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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,
}
}

View 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
View File

@@ -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
View 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"
]
}
]
}

BIN
Icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

0
LICENSE Normal file
View File

39
Makefile Normal file
View 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/

View File

@@ -1,2 +0,0 @@
# douyin-autojs-follower

34
certs/ca.crt Normal file
View File

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

34
certs/ca.pem Normal file
View File

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

37
cmd/init.go Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

34
cmd/proxy/ca.crt Normal file
View File

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

BIN
cmd/proxy/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

15
cmd/proxy/proxy.manifest Normal file
View 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
View 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
View 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

Binary file not shown.

46
cmd/root.go Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,3 @@
version: 1.0.1
path: /projects/douyin-robot
static: /projects/douyin-proxy-static

1
contracts/runner.go Normal file
View File

@@ -0,0 +1 @@
package contracts

2
dst/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

24
frontend/.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

9
frontend/README.md Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

41
frontend/src/App.vue Normal file
View 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>

View 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

View 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>

View 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
View 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
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

6
frontend/static.go Normal file
View File

@@ -0,0 +1,6 @@
package frontend
import "embed"
//go:embed dist
var Static embed.FS

View 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
View 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" }]
}

View 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
View 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
View 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
View 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
View 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
View 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
View File

72
modules/proxy/logic.go Normal file
View 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)
}
}

View 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
})
}
}

View 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
View 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
View File

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

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

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

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

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

6
package.json Normal file
View File

@@ -0,0 +1,6 @@
{
"dependencies": {
"axios": "^1.7.2",
"moment": "^2.30.1"
}
}

10
pkg/utils/ptr.go Normal file
View 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
View 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
View 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

Binary file not shown.

BIN
robot/audio/chenhong.m4a Normal file

Binary file not shown.

BIN
robot/audio/dubaojun.m4a Normal file

Binary file not shown.

BIN
robot/audio/genghonghai.m4a Normal file

Binary file not shown.

BIN
robot/audio/liliuji.m4a Normal file

Binary file not shown.

BIN
robot/audio/liudong.m4a Normal file

Binary file not shown.

BIN
robot/audio/liulianqi.m4a Normal file

Binary file not shown.

Binary file not shown.

BIN
robot/audio/xueyongdong.m4a Normal file

Binary file not shown.

BIN
robot/audio/yaobaosen.m4a Normal file

Binary file not shown.

Binary file not shown.

BIN
robot/audio/zhangzhaofa.m4a Normal file

Binary file not shown.

545
robot/lib.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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)

0
run.bat Normal file
View File