feat: v2(Incomplete,very early version)

This commit is contained in:
mei
2026-04-05 10:36:22 +08:00
parent 2ab9e060eb
commit 6190a8853a
41 changed files with 1131 additions and 766 deletions
+62
View File
@@ -0,0 +1,62 @@
package account
import (
"github.com/gin-gonic/gin"
"github.com/ssdomei232/goodBaby/api/user"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/model"
)
// 添加账号
func HandleAddAccount(c *gin.Context) {
gormDB, err := db.GetGormDB()
if err != nil {
c.JSON(500, gin.H{"code": 500, "data": "服务器内部错误"})
return
}
userInfo, err := user.GetUserInfoByGinCtx(c)
if err != nil {
c.JSON(401, gin.H{"code": 401, "data": "获取用户信息失败"})
return
}
var newAccount model.Account
if err := c.BindJSON(&newAccount); err != nil {
c.JSON(400, gin.H{"code": 400, "data": "输入参数错误"})
return
}
newAccount.UID = userInfo.ID
result := gormDB.Create(&newAccount)
if result.Error != nil {
c.JSON(500, gin.H{"code": 500, "data": "添加账号失败"})
return
}
c.JSON(200, gin.H{"code": 200, "data": "添加账号成功"})
}
// 获取用户的所有账号
func HandleGetAllAccounts(c *gin.Context) {
gormDB, err := db.GetGormDB()
if err != nil {
c.JSON(500, gin.H{"code": 500, "data": "服务器内部错误"})
return
}
userInfo, err := user.GetUserInfoByGinCtx(c)
if err != nil {
c.JSON(401, gin.H{"code": 401, "data": "获取用户信息失败"})
return
}
var accounts []model.Account
result := gormDB.Where("uid = ?", userInfo.ID).Find(&accounts)
if result.Error != nil {
c.JSON(500, gin.H{"code": 500, "data": "获取账号失败"})
return
}
c.JSON(200, gin.H{"code": 200, "data": accounts})
}
+62
View File
@@ -0,0 +1,62 @@
package rule
import (
"github.com/gin-gonic/gin"
"github.com/ssdomei232/goodBaby/api/user"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/model"
)
// 获取用户的所有规则
func HandleGetAllRules(c *gin.Context) {
gormDB, err := db.GetGormDB()
if err != nil {
c.JSON(500, gin.H{"code": 500, "data": "获取规则失败"})
return
}
userInfo, err := user.GetUserInfoByGinCtx(c)
if err != nil {
c.JSON(401, gin.H{"code": 401, "data": "获取用户信息失败"})
return
}
var rules []model.Rule
result := gormDB.Where("uid = ?", userInfo.ID).Find(&rules)
if result.Error != nil {
c.JSON(500, gin.H{"code": 500, "data": "获取规则失败"})
return
}
c.JSON(200, gin.H{"code": 200, "data": rules})
}
// 创建新规则
func HandleCreateRule(c *gin.Context) {
gormDB, err := db.GetGormDB()
if err != nil {
c.JSON(500, gin.H{"code": 500, "data": "服务器内部错误"})
return
}
userInfo, err := user.GetUserInfoByGinCtx(c)
if err != nil {
c.JSON(401, gin.H{"code": 401, "data": "获取用户信息失败"})
return
}
var newRule model.Rule
if err := c.BindJSON(&newRule); err != nil {
c.JSON(400, gin.H{"code": 400, "data": "输入参数错误"})
return
}
newRule.UID = userInfo.ID
result := gormDB.Create(&newRule)
if result.Error != nil {
c.JSON(500, gin.H{"code": 500, "data": "创建规则失败"})
return
}
c.JSON(200, gin.H{"code": 200, "data": "规则创建成功"})
}
+81
View File
@@ -0,0 +1,81 @@
package user
import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/model"
"golang.org/x/crypto/bcrypt"
)
// 加密密码
func encryptPassword(password string) (string, error) {
hashedID, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hashedID), nil
}
// 验证密码
func verifyPassword(hashedPassword, inputPassword string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(inputPassword))
}
func createUser(u *model.User) error {
gormDB, err := db.GetGormDB()
if err != nil {
return err
}
hashedPassword, err := encryptPassword(u.Password)
if err != nil {
return err
}
u.Password = hashedPassword
result := gormDB.Create(u)
return result.Error
}
// 校验密码
func verifyUser(u *model.User) error {
gormDB, err := db.GetGormDB()
if err != nil {
return err
}
var hashedPassword string
result := gormDB.Where("username = ?", u.Username).First(&model.User{}).Select("password").Scan(&hashedPassword)
if result.Error != nil {
return result.Error
}
return verifyPassword(hashedPassword, u.Password)
}
// 通过用户名获取用户信息
func GetUserInfo(username string) (*model.User, error) {
gormDB, err := db.GetGormDB()
if err != nil {
return nil, err
}
var user model.User
result := gormDB.Where("username = ?", username).First(&user)
if result.Error != nil {
return nil, result.Error
}
return &user, nil
}
// 通过 gin.Context 获取用户信息
func GetUserInfoByGinCtx(c *gin.Context) (userInfo *model.User, err error) {
session := sessions.Default(c)
username := session.Get("username")
userInfo, err = GetUserInfo(username.(string))
if err != nil {
return nil, err
}
return userInfo, nil
}
+103
View File
@@ -0,0 +1,103 @@
package user
import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/ssdomei232/goodBaby/configs"
"github.com/ssdomei232/goodBaby/model"
)
// 处理注册请求,配置文件中可设置禁用注册
func HandleRegistry(c *gin.Context) {
var user model.User
var err error
config, err := configs.GetConfig()
if err != nil {
c.JSON(500, gin.H{"code": 500, "data": "获取配置文件失败"})
return
}
if !config.EnableRegistry {
c.JSON(403, gin.H{"code": 403, "data": "注册功能已关闭"})
return
}
err = c.BindJSON(&user)
if err != nil {
c.JSON(400, gin.H{"code": 400, "data": "输入错误"})
return
}
if err = user.IsValid(); err != nil {
c.JSON(400, gin.H{"code": 400, "data": err.Error()})
return
}
if isExist := user.IsExist(); isExist {
c.JSON(400, gin.H{"code": 400, "data": "用户名已存在"})
return
}
if err = createUser(&user); err != nil {
c.JSON(500, gin.H{"code": 500, "data": "注册失败"})
return
}
session := sessions.Default(c)
session.Set("username", user.Username)
session.Save()
c.JSON(200, gin.H{"code": 200, "data": "注册成功"})
}
// 处理登录请求
func HandleLogin(c *gin.Context) {
var user model.User
var err error
err = c.BindJSON(&user)
if err != nil {
c.JSON(400, gin.H{"code": 400, "data": "输入错误"})
return
}
if err = user.IsValid(); err != nil {
c.JSON(400, gin.H{"code": 400, "data": err.Error()})
}
if err = verifyUser(&user); err != nil {
c.JSON(400, gin.H{"code": 400, "data": "用户名或密码错误"})
return
}
session := sessions.Default(c)
session.Set("username", user.Username)
session.Save()
c.JSON(200, gin.H{"code": 200, "data": "登录成功"})
}
// 处理获取用户信息请求
func HandleGetUserInfo(c *gin.Context) {
userInfo, err := GetUserInfoByGinCtx(c)
if err != nil {
c.JSON(500, gin.H{"code": 500, "data": "获取用户信息失败"})
return
}
c.JSON(200, gin.H{"code": 200, "data": userInfo})
}
// 认证中间件
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
session := sessions.Default(c)
username := session.Get("username")
if username == nil {
c.JSON(401, gin.H{"code": 401, "data": "未登录"})
c.Abort()
return
}
c.Next()
}
}
+7 -45
View File
@@ -5,52 +5,14 @@ import (
"os"
)
// type DingtalkBot struct {
// AccessToken string `json:"access_token"`
// Secret string `json:"secret"`
// }
type Config struct {
SignalSecret string `json:"signal_secret"`
Debug bool `json:"debug"`
DisconnectDuration int `json:"disconnect_duration"` // Hours
BiliMsg string `json:"bili_msg"`
Basic Basic `json:"basic"`
MailConfig MailConfig `json:"mail_config"`
GithubConfig GithubConfig `json:"github_config"`
DingtalkBot DingtalkBot `json:"dingtalk_bot"`
OneBotConfig OneBotConfig `json:"onebot_config"`
}
type MailConfig struct {
Host string `json:"host"`
Port int `json:"port"`
User string `json:"user"`
Password string `json:"pass"`
MailList []string `json:"mail_list"`
MailTitle string `json:"mail_title"`
MailContent string `json:"mail_content"`
}
type GithubConfig struct {
Owner string `json:"owner"`
Repos []string `json:"repos"`
Token string `json:"token"`
}
type Basic struct {
Name string `json:"name"`
Age int `json:"age"`
NickName string `json:"nickname"`
QQNumber string `json:"qq_number"`
CauseStop string `json:"cause_stop"`
}
type DingtalkBot struct {
AccessToken string `json:"access_token"`
Secret string `json:"secret"`
}
type OneBotConfig struct {
URL string `json:"url"` // 例如 "http://localhost:5700"
Token string `json:"token"`
SendGroups []int `json:"send_groups"`
Msg string `json:"msg"`
EnableRegistry bool `json:"enable_registry"`
TimeoutDurationHours int `json:"timeout_duration_hours"`
}
// Get config from json file
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -1,6 +1,6 @@
services:
goodbaby:
image: mei232/goodbaby:v0.2.0
image: mei232/goodbaby:v2.0.1
ports:
- "8088:8088"
environment:
@@ -11,7 +11,7 @@ services:
- app-network
volumes:
- ./config.json:/app/config.json:ro
- ./tmp:/app/tmp
- ./data.db:/app/data.db:ro
networks:
app-network:
+17
View File
@@ -0,0 +1,17 @@
# Bilibili
## Dynamic Config
```json
{
"msg": "something"
}
```
## Account Config
```json
{
"cookies_string": "xxxx"
}
```
+3
View File
@@ -0,0 +1,3 @@
# Email Config
## Email Account Config
+30
View File
@@ -0,0 +1,30 @@
package bilibili
import (
"log"
"os"
"github.com/CuteReimu/bilibili/v2"
)
// Qrcode login
func HandleLoginWithQRCode(biliClient *bilibili.Client) {
qrCode, err := biliClient.GetQRCode()
if err != nil {
log.Printf("获取二维码失败: %v", err)
return
}
buf, _ := qrCode.Encode()
os.WriteFile("tmp/qrcode.png", buf, 0644)
qrCode.Print()
result, err := biliClient.LoginWithQRCode(bilibili.LoginWithQRCodeParam{
QrcodeKey: qrCode.QrcodeKey,
})
if err != nil || result.Code != 0 {
log.Printf("登录失败: %v", err)
return
}
log.Println("登录成功")
}
+44
View File
@@ -0,0 +1,44 @@
package bilibili
import (
"context"
"log"
"time"
"github.com/CuteReimu/bilibili/v2"
"github.com/cenkalti/backoff/v5"
"github.com/ssdomei232/goodBaby/configs"
)
// 在Bilibili动态发送消息
//
// 暂时没有处理 429 和 403 的区别对待
func SendBiliDynamicMsg(biliClient *bilibili.Client, msg string) {
config, err := configs.GetConfig()
if err != nil {
log.Printf("获取配置失败: %v", err)
return
}
var timeout time.Duration = time.Duration(config.TimeoutDurationHours) * time.Hour
var dynamicParams bilibili.CreateDynamicParam
dynamicParams = bilibili.CreateDynamicParam{
DynamicId: 0,
Type: 4,
Rid: 0,
Content: msg,
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
operation := func() (string, error) {
_, err := biliClient.CreateDynamic(dynamicParams)
return "", err
}
_, err = backoff.Retry(ctx, operation, backoff.WithBackOff(backoff.NewExponentialBackOff()))
if err != nil {
log.Printf("发送B站动态失败: %v", err)
}
}
+9
View File
@@ -0,0 +1,9 @@
package bilibili
type BiliAccount struct {
CookiesString string `json:"cookies_string"`
}
type BiliDynamicRule struct {
Msg string `json:"msg"`
}
+134
View File
@@ -0,0 +1,134 @@
package bilibili
import (
"encoding/json"
"log"
"net/http"
"strings"
"time"
"github.com/CuteReimu/bilibili/v2"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/model"
)
// 检查cookie是否过期
func isCookiesExpired(cookiesString string) bool {
// 从cookie字符串中提取过期时间
lines := strings.SplitSeq(cookiesString, "\n")
for line := range lines {
if strings.Contains(line, "Expires=") {
// 提取过期时间字符串
expireStr := ""
parts := strings.SplitSeq(line, ";")
for part := range parts {
part = strings.TrimSpace(part)
if after, ok := strings.CutPrefix(part, "Expires="); ok {
expireStr = after
break
}
}
if expireStr != "" {
// 解析过期时间
expireTime, err := http.ParseTime(expireStr)
if err != nil {
log.Printf("解析过期时间失败: %v", err)
return true // 如果解析失败,cookie已过期
}
if time.Now().After(expireTime) {
return true
}
}
}
}
return false
}
// 检查cookie是否即将过期(提前1天)
func isCookiesExpiringSoon(cookiesString string) bool {
lines := strings.SplitSeq(cookiesString, "\n")
for line := range lines {
if strings.Contains(line, "Expires=") {
// 提取过期时间字符串
expireStr := ""
parts := strings.SplitSeq(line, ";")
for part := range parts {
part = strings.TrimSpace(part)
if after, ok := strings.CutPrefix(part, "Expires="); ok {
expireStr = after
break
}
}
if expireStr != "" {
// 解析过期时间
expireTime, err := http.ParseTime(expireStr)
if err != nil {
log.Printf("解析过期时间失败: %v", err)
return true // 如果解析失败,认为需要重新登录
}
// 检查是否即将过期(提前1天)
if time.Now().Add(24 * time.Hour).After(expireTime) {
return true // 即将过期
}
}
}
}
return false
}
// 触发登录请求
func triggerLoginRequest(biliClient *bilibili.Client) {
// 生成新的二维码
qrCode, err := biliClient.GetQRCode()
if err != nil {
log.Printf("获取二维码失败: %v", err)
return
}
// 在命令行打印二维码
log.Println("需要重新登录哔哩哔哩")
qrCode.Print()
// 通过Dingtalk发送登录请求
// title := "B站登录请求 - Cookie即将过期"
// msg := "您的B站cookie即将过期,请尽快重启摇篮系统进行登陆,请尽快完成登录操作。"
// 发送Dingtalk通知
// if config.DingtalkBot.AccessToken != "" && config.DingtalkBot.Secret != "" {
// SendDingTalkMsg(title, msg, config.DingtalkBot.AccessToken, config.DingtalkBot.Secret)
// }
}
// 获取 Bilibili Client
func GetBiliClient(rule *model.Rule) (*bilibili.Client, error) {
client := bilibili.New()
gormDB, err := db.GetGormDB()
if err != nil {
return nil, err
}
// 1. 通过 account_id 获取到对应的 account 配置
var biliAccount model.Account
result := gormDB.Where("id = ?", rule.AccountID).First(&biliAccount)
if result.Error != nil {
return nil, result.Error
}
// 2. 通过 account 配置中的 config 字段获取到 CookiesString
var biliAccountConfig BiliAccount
err = json.Unmarshal([]byte(biliAccount.Config), &biliAccountConfig)
if err != nil {
return nil, err
}
client.SetCookiesString(biliAccountConfig.CookiesString)
return client, nil
}
+5
View File
@@ -0,0 +1,5 @@
package dingtalk
import "github.com/ssdomei232/goodBaby/model"
func SendDingTalkMsg(user *model.User, title string, msg string) {}
+6
View File
@@ -0,0 +1,6 @@
package dingtalk
type DingTalkConfig struct {
AccessToken string `json:"access_token"`
Secret string `json:"secret"`
}
+20
View File
@@ -0,0 +1,20 @@
package dingtalk
import (
"encoding/json"
"github.com/ssdomei232/goodBaby/model"
)
func getDingTalkConfigFromUser(user *model.User) *DingTalkConfig {
if user.DingTalkConfig == nil {
return nil
}
var config DingTalkConfig
err := json.Unmarshal([]byte(*user.DingTalkConfig), &config)
if err != nil {
return nil
}
return &config
}
+20
View File
@@ -0,0 +1,20 @@
package email
import (
"log"
"github.com/ssdomei232/goodBaby/model"
)
// 向邮件列表发送消息
func SendMail(rule *model.Rule) {
emailRule, err := GetEmailRuleFromRule(rule)
if err != nil {
log.Printf("获取邮件规则配置失败: %v", err)
return
}
for _, destinations := range emailRule.Destinations {
go sendMailMsgWithRetry(rule, destinations) // 使用 goroutine 发送邮件,避免指数退避阻塞其他地址
}
}
+15
View File
@@ -0,0 +1,15 @@
package email
type EmailAccountConfig struct {
SMTPServer string `json:"smtp_server"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
TestDestination string `json:"test_destination"` // 用于测试邮件发送的地址
}
type EmailRule struct {
Msg string `json:"msg"`
Title string `json:"title"`
Destinations []string `json:"destinations"` // 邮件地址列表
}
+116
View File
@@ -0,0 +1,116 @@
package email
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"github.com/cenkalti/backoff/v5"
"github.com/ssdomei232/goodBaby/configs"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/model"
"github.com/wneessen/go-mail"
)
// 从 Rule 中获取 EmailAccount 配置
func GetEmailAccountFromRule(rule *model.Rule) (*EmailAccountConfig, error) {
gormDB, err := db.GetGormDB()
if err != nil {
return nil, err
}
// 1. 通过 account_id 获取到对应的 account 配置
var emailAccount model.Account
result := gormDB.Where("id = ?", rule.AccountID).First(&emailAccount)
if result.Error != nil {
return nil, result.Error
}
// 2. 通过 account 配置中的 config 字段获取到 EmailAccount
var emailAccountConfig EmailAccountConfig
err = json.Unmarshal([]byte(emailAccount.Config), &emailAccountConfig)
if err != nil {
return nil, err
}
return &emailAccountConfig, nil
}
// 从 Rule 中获取 EmailRule 配置
func GetEmailRuleFromRule(rule *model.Rule) (*EmailRule, error) {
var emailRule EmailRule
err := json.Unmarshal([]byte(rule.ConfigJson), &emailRule)
if err != nil {
return nil, err
}
return &emailRule, nil
}
func sendMailMsgWithRetry(rule *model.Rule, address string) {
config, err := configs.GetConfig()
if err != nil {
log.Printf("获取配置失败: %v", err)
return
}
var timeout time.Duration = time.Duration(config.TimeoutDurationHours) * time.Hour
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
operation := func() (string, error) {
err := sendMailMsg(address, rule)
return "", err
}
_, err = backoff.Retry(ctx, operation, backoff.WithBackOff(backoff.NewExponentialBackOff()))
if err != nil {
log.Printf("发送邮件失败: %v", err)
}
}
func sendMailMsg(address string, rule *model.Rule) error {
emailAccountConfig, err := GetEmailAccountFromRule(rule)
if err != nil {
log.Printf("获取邮件账户配置失败: %v", err)
return fmt.Errorf("获取邮件账户配置失败: %v", err)
}
emailRule, err := GetEmailRuleFromRule(rule)
if err != nil {
log.Printf("获取邮件规则配置失败: %v", err)
return fmt.Errorf("获取邮件规则配置失败: %v", err)
}
client, err := mail.NewClient(
emailAccountConfig.SMTPServer,
mail.WithPort(emailAccountConfig.Port),
mail.WithSSL(),
mail.WithUsername(emailAccountConfig.Username),
mail.WithPassword(emailAccountConfig.Password),
mail.WithSMTPAuth(mail.SMTPAuthPlain),
)
if err != nil {
return fmt.Errorf("创建邮件客户端失败: %v", err)
}
// 创建邮件
message := mail.NewMsg()
if err := message.From(emailAccountConfig.Username); err != nil {
return fmt.Errorf("设置发件人失败: %v", err)
}
if err := message.To(address); err != nil {
return fmt.Errorf("设置收件人失败: %v", err)
}
message.Subject(emailRule.Title)
message.SetBodyString(mail.TypeTextPlain, emailRule.Msg)
// 发送邮件
if err := client.DialAndSend(message); err != nil {
return fmt.Errorf("发送邮件失败: %v", err)
}
return nil
}
+42
View File
@@ -0,0 +1,42 @@
package github
import (
"context"
"log"
"time"
"github.com/cenkalti/backoff/v5"
"github.com/ssdomei232/goodBaby/configs"
"github.com/ssdomei232/goodBaby/model"
)
// 将repos设置为public
func MakeRepositoryPublic(rule *model.Rule) {
reposConfig, account := GetGithubReposAndAccountFromRule(rule)
if reposConfig == nil || account == nil {
log.Printf("无法获取 GitHub 配置或账户信息")
return
}
for _, repo := range reposConfig.Repos {
config, err := configs.GetConfig()
if err != nil {
log.Printf("获取配置失败: %v", err)
return
}
var timeout time.Duration = time.Duration(config.TimeoutDurationHours) * time.Hour
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
operation := func() (string, error) {
err := SetRepositoryPublic(account.Token, account.Owner, repo)
return "", err
}
_, err = backoff.Retry(ctx, operation, backoff.WithBackOff(backoff.NewExponentialBackOff()))
if err != nil {
log.Printf("将仓库 %s 设置为 public 失败: %v", repo, err)
}
}
}
+10
View File
@@ -0,0 +1,10 @@
package github
type GithubReposConfig struct {
Repos []string `yaml:"repos"`
}
type GithubAccount struct {
Token string
Owner string
}
+57
View File
@@ -0,0 +1,57 @@
package github
import (
"context"
"encoding/json"
"log"
"github.com/google/go-github/v84/github"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/model"
)
// 从 rule 中获取 GithubReposConfig 和 GithubAccount
func GetGithubReposAndAccountFromRule(rule *model.Rule) (*GithubReposConfig, *GithubAccount) {
var githubReposConfig GithubReposConfig
var githubAccount GithubAccount
// 获取 GithubReposConfig
err := json.Unmarshal([]byte(rule.ConfigJson), &githubReposConfig)
if err != nil {
log.Printf("获取 GitHub 配置失败: %v", err)
return nil, nil
}
// 获取 GithubAccount
gormDB, err := db.GetGormDB()
if err != nil {
log.Printf("获取 Gorm DB 失败: %v", err)
return nil, nil
}
err = gormDB.Where("id = ?", rule.AccountID).First(&githubAccount).Error
if err != nil {
log.Printf("获取 GitHub 账户失败: %v", err)
return nil, nil
}
return &githubReposConfig, &githubAccount
}
// 将仓库设置为 public
func SetRepositoryPublic(token, owner, repo string) error {
ctx := context.Background()
// 1. 初始化客户端
client := github.NewClient(nil).WithAuthToken(token)
// 2. 准备修改的参数
opts := &github.Repository{
Visibility: github.Ptr("public"),
}
// 3. 执行更新操作
_, _, err := client.Repositories.Edit(ctx, owner, repo, opts)
return err
}
+11
View File
@@ -0,0 +1,11 @@
package onebot
type OneBotConfig struct {
SendGroups []int `json:"send_groups"`
Msg string `json:"msg"`
}
type OneBotAccount struct {
URL string `json:"url"` // 例如 "http://localhost:5700"
Token string `json:"token"` // 例如 "your_token_here"
}
+42
View File
@@ -0,0 +1,42 @@
package onebot
import (
"context"
"log"
"time"
"github.com/cenkalti/backoff/v5"
"github.com/ssdomei232/goodBaby/configs"
"github.com/ssdomei232/goodBaby/model"
)
func SendOneBotMsg(rule *model.Rule) {
oneBotConfig := getOneBotConfigFromRule(rule)
oneBotAccount, err := getOneBotAccountFromRule(rule)
if err != nil {
log.Printf("获取OneBot账户失败: %v", err)
return
}
for _, groupID := range oneBotConfig.SendGroups {
config, err := configs.GetConfig()
if err != nil {
log.Printf("获取配置失败: %v", err)
return
}
var timeout time.Duration = time.Duration(config.TimeoutDurationHours) * time.Hour
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
operation := func() (string, error) {
err := sendOneBotMsg(oneBotAccount, groupID, oneBotConfig.Msg)
return "", err
}
_, err = backoff.Retry(ctx, operation, backoff.WithBackOff(backoff.NewExponentialBackOff()))
if err != nil {
log.Printf("发送OneBot消息失败: %v", err)
}
}
}
+47
View File
@@ -0,0 +1,47 @@
package onebot
import (
"encoding/json"
"strconv"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/model"
napcat "github.com/ssdomei232/napcat-http-go-sdk"
)
// 从 Rule 中获取 OneBotConfig
func getOneBotConfigFromRule(rule *model.Rule) *OneBotConfig {
var oneBotConfig OneBotConfig
err := json.Unmarshal([]byte(rule.ConfigJson), &oneBotConfig)
if err != nil {
return nil
}
return &oneBotConfig
}
// 从 Rule 中获取 OneBotAccount
func getOneBotAccountFromRule(rule *model.Rule) (*OneBotAccount, error) {
gormDB, err := db.GetGormDB()
if err != nil {
return nil, err
}
var oneBotAccountEntry model.Account
gormDB.Where("id = ?", rule.AccountID).First(&oneBotAccountEntry)
var oneBotAccount OneBotAccount
err = json.Unmarshal([]byte(oneBotAccountEntry.Config), &oneBotAccount)
if err != nil {
return nil, err
}
return &oneBotAccount, nil
}
// 发送一条消息到OneBot
func sendOneBotMsg(account *OneBotAccount, groupID int, msg string) error {
client := napcat.NewClient(account.Token, account.URL)
err := client.SendGroupMsg(strconv.Itoa(groupID), msg)
return err
}
+22 -11
View File
@@ -1,14 +1,19 @@
module github.com/ssdomei232/goodBaby
go 1.24.10
go 1.25.0
require (
github.com/CatchZeng/dingtalk v1.5.0
github.com/CuteReimu/bilibili/v2 v2.3.5
github.com/cenkalti/backoff/v5 v5.0.3
github.com/gin-contrib/sessions v1.0.4
github.com/gin-gonic/gin v1.11.0
github.com/robfig/cron/v3 v3.0.1
github.com/google/go-github/v84 v84.0.0
github.com/ssdomei232/napcat-http-go-sdk v1.0.0
github.com/wneessen/go-mail v0.7.2
golang.org/x/crypto v0.48.0
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12
)
require (
@@ -22,14 +27,21 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/go-resty/resty/v2 v2.17.2 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
@@ -41,12 +53,11 @@ require (
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)
+45 -19
View File
@@ -10,6 +10,8 @@ github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQ
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -19,6 +21,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
@@ -33,15 +37,32 @@ github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHO
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v84 v84.0.0 h1:I/0Xn5IuChMe8TdmI2bbim5nyhaRFJ7DEdzmD2w+yVA=
github.com/google/go-github/v84 v84.0.0/go.mod h1:WwYL1z1ajRdlaPszjVu/47x1L0PXukJBn73xsiYrRRQ=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
@@ -56,8 +77,11 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
@@ -70,8 +94,6 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
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/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
@@ -99,26 +121,30 @@ go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
+16
View File
@@ -0,0 +1,16 @@
package db
import (
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func GetGormDB() (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open("file:data.db"), &gorm.Config{})
if err != nil {
log.Fatal("打开数据库失败:", err)
}
return db, nil
}
-29
View File
@@ -1,29 +0,0 @@
package base
import (
"fmt"
"log"
"time"
"github.com/ssdomei232/goodBaby/configs"
)
func GetBasicInfo() string {
config, err := configs.GetConfig()
if err != nil {
log.Printf("获取配置文件失败: %v", err)
return "获取配置文件失败"
}
now := time.Now()
lastSignalTime := now.Add(-time.Duration(config.DisconnectDuration) * time.Hour).Format("2006-01-02 15:04:05")
if config.Debug {
return fmt.Sprintf("此消息为测试消息\n\n%s(QQ号:%s)已于%s至%s(UTC+8)之间去世,享年%d岁,死因可能为%s(此死因为推测最可能的死因,请勿完全相信,请等待后续消息)",
config.Basic.Name, config.Basic.QQNumber, lastSignalTime, time.Now().Format("2006-01-02 15:04:05"), config.Basic.Age, config.Basic.CauseStop)
} else {
return fmt.Sprintf("%s(QQ号:%s,网名:%s)已于%s至%s(UTC+8)之间去世,享年%d岁,死因可能为%s(此死因为推测最可能的死因,请勿完全相信,请等待后续消息)",
config.Basic.Name, config.Basic.NickName, config.Basic.QQNumber, lastSignalTime, time.Now().Format("2006-01-02 15:04:05"), config.Basic.Age, config.Basic.CauseStop)
}
}
-19
View File
@@ -1,19 +0,0 @@
package frontend
import (
"html/template"
"io/fs"
"net/http"
"github.com/gin-gonic/gin"
)
// loadTemplates loads templates from the embedded file system
func LoadTemplates(filesystem fs.FS) *template.Template {
templ := template.Must(template.New("").ParseFS(filesystem, "*.html"))
return templ
}
func IndexPage(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
}
-23
View File
@@ -1,23 +0,0 @@
package reminder
import (
"fmt"
"log"
"time"
"github.com/ssdomei232/goodBaby/configs"
"github.com/ssdomei232/goodBaby/internal/sender"
)
func Reminder() {
config, err := configs.GetConfig()
if err != nil {
log.Printf("获取配置文件失败: %v", err)
}
remainingTime := GlobalTimerManager.GetRemainingTime()
if remainingTime <= 48*time.Hour {
sendMsg := fmt.Sprintf("距离摇篮系统触发还有%d小时,请尽快发送Signal", int(remainingTime.Hours()))
sender.SendDingTalkMsg("摇篮系统触发提醒", sendMsg, config.DingtalkBot.AccessToken, config.DingtalkBot.Secret)
}
}
-52
View File
@@ -1,52 +0,0 @@
package reminder
import (
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
)
type TimerManager struct {
duration time.Duration
resetTime time.Time
mutex sync.RWMutex
}
var GlobalTimerManager *TimerManager
func InitTimerManager(duration time.Duration) {
GlobalTimerManager = &TimerManager{
duration: duration,
resetTime: time.Now(),
}
}
func (tm *TimerManager) Reset() {
tm.mutex.Lock()
defer tm.mutex.Unlock()
tm.resetTime = time.Now()
}
func (tm *TimerManager) GetRemainingTime() time.Duration {
tm.mutex.RLock()
defer tm.mutex.RUnlock()
elapsed := time.Since(tm.resetTime)
remaining := tm.duration - elapsed
if remaining < 0 {
return 0
}
return remaining
}
func HandleTimerStatus(c *gin.Context) {
remainingTime := GlobalTimerManager.GetRemainingTime()
c.JSON(http.StatusOK, gin.H{
"code": 200,
"remaining_time": remainingTime.Seconds(), // 返回剩余秒数
"message": "ok",
})
}
-283
View File
@@ -1,283 +0,0 @@
package sender
import (
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/CuteReimu/bilibili/v2"
"github.com/ssdomei232/goodBaby/configs"
"github.com/ssdomei232/goodBaby/internal/base"
)
// check tmp dir exist
func EnsureTmpDirectory() error {
_, err := os.Stat("tmp")
if os.IsNotExist(err) {
err = os.Mkdir("tmp", 0755)
if err != nil {
return err
}
log.Println("已创建tmp目录")
} else if err != nil {
return err
} else {
log.Println("tmp目录已存在")
}
return nil
}
func isCookiesExpired(cookiesString string) bool {
// parase expires date from cookie string
lines := strings.SplitSeq(cookiesString, "\n")
for line := range lines {
if strings.Contains(line, "Expires=") {
// 提取过期时间字符串
expireStr := ""
parts := strings.SplitSeq(line, ";")
for part := range parts {
part = strings.TrimSpace(part)
if after, ok := strings.CutPrefix(part, "Expires="); ok {
expireStr = after
break
}
}
if expireStr != "" {
// phrase expirres date
expireTime, err := http.ParseTime(expireStr)
if err != nil {
log.Printf("解析过期时间失败: %v", err)
return true // if phrase failed, cookie expired
}
// compare expires date and now
if time.Now().After(expireTime) {
return true // expired
}
}
}
}
return false // not expired
}
// Qrcode login
func LoginWithQRCode(biliClient *bilibili.Client) {
qrCode, err := biliClient.GetQRCode()
if err != nil {
log.Printf("获取二维码失败: %v", err)
return
}
log.Println("请使用哔哩哔哩APP扫码登录或进入文件目录寻找qrcode.png查看二维码")
buf, _ := qrCode.Encode()
os.WriteFile("tmp/qrcode.png", buf, 0644)
qrCode.Print()
result, err := biliClient.LoginWithQRCode(bilibili.LoginWithQRCodeParam{
QrcodeKey: qrCode.QrcodeKey,
})
if err != nil || result.Code != 0 {
log.Printf("登录失败: %v", err)
return
}
log.Println("登录成功")
// save new cookie
saveCookies(biliClient)
}
// save cookie to file
func saveCookies(client *bilibili.Client) {
cookiesString := client.GetCookiesString()
err := os.WriteFile("tmp/cookies.txt", []byte(cookiesString), 0644)
if err != nil {
log.Printf("保存cookie失败: %v", err)
}
}
// 从文件加载cookie并验证有效性
func LoadCookies(client *bilibili.Client) bool {
// 检查cookie文件是否存在
if _, err := os.Stat("tmp/cookies.txt"); os.IsNotExist(err) {
return false
}
// 读取cookie
cookiesBytes, err := os.ReadFile("tmp/cookies.txt")
if err != nil {
log.Printf("读取cookie文件失败: %v", err)
return false
}
cookiesString := string(cookiesBytes)
client.SetCookiesString(cookiesString)
// 验证cookie是否过期
if isCookiesExpired(cookiesString) {
log.Println("cookie已过期")
return false
}
return true
}
// 在Bilibili动态发送死亡通告
func SendBili(biliClient *bilibili.Client) {
config, err := configs.GetConfig()
if err != nil {
log.Printf("获取配置文件失败: %v", err)
return
}
stopMsg := fmt.Sprintf("%s\n\n%s\n\n本消息由自动程序发送", base.GetBasicInfo(), config.BiliMsg)
var dynamicParams bilibili.CreateDynamicParam
dynamicParams = bilibili.CreateDynamicParam{
DynamicId: 0,
Type: 4,
Rid: 0,
Content: stopMsg,
}
// 重试机制
retryCount := 10 // 重试次数
delay := time.Second * 2 // 初始重试延迟
for i := 0; i <= retryCount; i++ {
_, err := biliClient.CreateDynamic(dynamicParams)
if err == nil {
log.Println("动态发送成功")
return
}
log.Printf("动态发送失败 (尝试 %d/%d): %v", i+1, retryCount+1, err)
// 如果不是最后一次尝试,则等待后重试
if i < retryCount {
log.Printf("等待 %v 后进行第 %d 次重试", delay, i+2)
time.Sleep(delay)
delay *= 2 // 指数退避
}
}
log.Println("动态发送最终失败,已达到最大重试次数")
}
// 检查cookie有效性
func CheckCookieValidity(biliClient *bilibili.Client) {
// 读取存储的cookie文件
if _, err := os.Stat("tmp/cookies.txt"); os.IsNotExist(err) {
log.Println("cookie文件不存在,需要重新登录")
triggerLoginRequest(biliClient)
return
}
cookiesBytes, err := os.ReadFile("tmp/cookies.txt")
if err != nil {
log.Printf("读取cookie文件失败: %v", err)
triggerLoginRequest(biliClient)
return
}
cookiesString := string(cookiesBytes)
// 检查cookie是否即将过期(提前1天提醒)
if isCookiesExpiringSoon(cookiesString) {
log.Println("cookie即将过期,需要重新登录")
triggerLoginRequest(biliClient)
}
}
// 检查cookie是否即将过期(提前1天)
func isCookiesExpiringSoon(cookiesString string) bool {
lines := strings.SplitSeq(cookiesString, "\n")
for line := range lines {
if strings.Contains(line, "Expires=") {
// 提取过期时间字符串
expireStr := ""
parts := strings.SplitSeq(line, ";")
for part := range parts {
part = strings.TrimSpace(part)
if after, ok := strings.CutPrefix(part, "Expires="); ok {
expireStr = after
break
}
}
if expireStr != "" {
// 解析过期时间
expireTime, err := http.ParseTime(expireStr)
if err != nil {
log.Printf("解析过期时间失败: %v", err)
return true // 如果解析失败,认为需要重新登录
}
// 检查是否即将过期(提前1天)
if time.Now().Add(24 * time.Hour).After(expireTime) {
return true // 即将过期
}
}
}
}
return false
}
// 触发登录请求
func triggerLoginRequest(biliClient *bilibili.Client) {
// 获取配置
config, err := configs.GetConfig()
if err != nil {
log.Printf("获取配置文件失败: %v", err)
return
}
// 生成新的二维码
qrCode, err := biliClient.GetQRCode()
if err != nil {
log.Printf("获取二维码失败: %v", err)
return
}
// 在命令行打印二维码
log.Println("需要重新登录哔哩哔哩")
qrCode.Print()
// 通过Dingtalk发送登录请求
title := "B站登录请求 - Cookie即将过期"
msg := "您的B站cookie即将过期,请尽快重启摇篮系统进行登陆,请尽快完成登录操作。"
// 发送Dingtalk通知
if config.DingtalkBot.AccessToken != "" && config.DingtalkBot.Secret != "" {
SendDingTalkMsg(title, msg, config.DingtalkBot.AccessToken, config.DingtalkBot.Secret)
}
}
// Enable periodic cookie checks
func StartCookieChecker(cookieCheckTimer *time.Ticker, biliClient *bilibili.Client) {
// check cookie per hour
cookieCheckTimer = time.NewTicker(1 * time.Hour)
go func() {
for {
<-cookieCheckTimer.C
CheckCookieValidity(biliClient)
}
}()
}
func InitBilibili(biliClient *bilibili.Client) {
// try cached cookie
if !LoadCookies(biliClient) {
// if cookie check failed, request qrcode login
LoginWithQRCode(biliClient)
} else {
log.Println("使用已存储的有效cookie登录")
}
}
-99
View File
@@ -1,99 +0,0 @@
package sender
import (
"fmt"
"log"
"time"
"github.com/ssdomei232/goodBaby/configs"
"github.com/ssdomei232/goodBaby/internal/base"
"github.com/wneessen/go-mail"
)
const (
maxRetries = 10
retryDelay = 10 * time.Second
)
// 向邮件列表发送死亡通告
func SendMail() {
config, err := configs.GetConfig()
if err != nil {
log.Printf("获取配置文件失败: %v", err)
return
}
stopMsg := fmt.Sprintf("%s\n\n%s\n\n本消息由自动程序发送", base.GetBasicInfo(), config.MailConfig.MailContent)
for _, address := range config.MailConfig.MailList {
sendMailMsgWithRetry(address, stopMsg, config.MailConfig.MailTitle)
}
}
func sendMailMsgWithRetry(address string, msg string, title string) {
var lastErr error
// 重试循环
for i := 0; i <= maxRetries; i++ {
if i > 0 {
log.Printf("第 %d 次重试发送邮件给 %s", i, address)
}
err := sendMailMsg(address, msg, title)
if err == nil {
// 发送成功
fmt.Printf("邮件成功发送给 %s\n", address)
return
}
lastErr = err
log.Printf("发送邮件给 %s 失败: %v", address, err)
// 如果不是最后一次重试,则等待后重试
if i < maxRetries {
time.Sleep(retryDelay * time.Duration(i+1)) // 递增延迟
}
}
// 所有重试都失败
log.Printf("发送邮件给 %s 经过 %d 次重试后仍然失败: %v", address, maxRetries, lastErr)
}
func sendMailMsg(address string, msg string, title string) error {
config, err := configs.GetConfig()
if err != nil {
return fmt.Errorf("获取配置文件失败: %v", err)
}
client, err := mail.NewClient(
config.MailConfig.Host,
mail.WithPort(config.MailConfig.Port),
mail.WithSSL(),
mail.WithUsername(config.MailConfig.User),
mail.WithPassword(config.MailConfig.Password),
mail.WithSMTPAuth(mail.SMTPAuthPlain),
)
if err != nil {
return fmt.Errorf("创建邮件客户端失败: %v", err)
}
// 创建邮件
message := mail.NewMsg()
if err := message.From(config.MailConfig.User); err != nil {
return fmt.Errorf("设置发件人失败: %v", err)
}
if err := message.To(address); err != nil {
return fmt.Errorf("设置收件人失败: %v", err)
}
message.Subject(title)
message.SetBodyString(mail.TypeTextPlain, msg)
// 发送邮件
if err := client.DialAndSend(message); err != nil {
return fmt.Errorf("发送邮件失败: %v", err)
}
return nil
}
-36
View File
@@ -1,36 +0,0 @@
package sender
import (
"log"
"time"
"github.com/ssdomei232/goodBaby/configs"
"github.com/ssdomei232/goodBaby/pkg/github"
)
// 将repos设置为public
func Github() {
config, err := configs.GetConfig()
if err != nil {
log.Fatalln(err)
}
for _, repo := range config.GithubConfig.Repos {
maxRetries := 10
for attempt := range maxRetries {
err := github.MakeRepositoryPublic(config.GithubConfig.Owner, repo, config.GithubConfig.Token)
if err == nil {
break
}
log.Printf("尝试设置仓库 %s 为公开失败 (尝试 %d/%d): %v", repo, attempt+1, maxRetries, err)
// 如果不是最后一次尝试,则等待一段时间后重试
if attempt < maxRetries-1 {
time.Sleep(time.Duration(attempt+1) * time.Second) // 逐步增加等待时间
} else {
log.Printf("设置仓库 %s 为公开最终失败", repo)
}
}
}
}
-43
View File
@@ -1,43 +0,0 @@
package sender
import (
"fmt"
"log"
"strconv"
"github.com/ssdomei232/goodBaby/configs"
"github.com/ssdomei232/goodBaby/internal/base"
napcat "github.com/ssdomei232/napcat-http-go-sdk"
)
func SendOneBot() {
config, err := configs.GetConfig()
if err != nil {
log.Printf("获取配置文件失败: %v", err)
return
}
stopMsg := fmt.Sprintf("%s\n\n%s\n\n本消息由自动程序发送", base.GetBasicInfo(), config.OneBotConfig.Msg)
for _, groupId := range config.OneBotConfig.SendGroups {
sendOneBotMsg(groupId, stopMsg)
}
}
// 发送一条消息到OneBot
func sendOneBotMsg(groupID int, msg string) {
config, err := configs.GetConfig()
if err != nil {
log.Printf("获取配置文件失败: %v", err)
return
}
client := napcat.NewClient(config.OneBotConfig.Token, config.OneBotConfig.URL)
err = client.SendGroupMsg(strconv.Itoa(groupID), msg)
if err != nil {
log.Printf("发送消息到OneBot失败: %v", err)
} else {
log.Printf("成功发送消息到OneBot群 %d", groupID)
}
}
+31 -105
View File
@@ -1,121 +1,47 @@
package main
import (
"embed"
"io/fs"
"log"
"net/http"
"time"
"crypto/rand"
"fmt"
"github.com/CuteReimu/bilibili/v2"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"github.com/robfig/cron/v3"
"github.com/ssdomei232/goodBaby/configs"
"github.com/ssdomei232/goodBaby/internal/frontend"
"github.com/ssdomei232/goodBaby/internal/reminder"
"github.com/ssdomei232/goodBaby/internal/sender"
"github.com/ssdomei232/goodBaby/api/account"
"github.com/ssdomei232/goodBaby/api/rule"
"github.com/ssdomei232/goodBaby/api/user"
)
//go:embed static/*
var staticFiles embed.FS
//go:embed templates/*
var templateFiles embed.FS
var timer *time.Timer
var duration time.Duration
var biliClient *bilibili.Client
var cookieCheckTimer *time.Ticker
func init() {
// 1. 加载配置文件
config, err := configs.GetConfig()
if err != nil {
log.Printf("获取配置文件失败: %v", err)
}
if config.Debug {
duration = time.Duration(config.DisconnectDuration) * time.Second
} else {
duration = time.Duration(config.DisconnectDuration) * time.Hour
}
// 2. 检查并创建tmp目录
if err := sender.EnsureTmpDirectory(); err != nil {
log.Printf("创建tmp目录失败: %v", err)
}
// 3. 初始化哔哩哔哩和定时器
biliClient = bilibili.New()
sender.InitBilibili(biliClient)
sender.StartCookieChecker(cookieCheckTimer, biliClient) // Check Bilibili cookie
reminder.InitTimerManager(duration)
timer = time.NewTimer(duration)
go func() {
<-timer.C
trigger(config)
}()
}
// 触发器
func trigger(config configs.Config) {
go sender.SendOneBot()
go sender.SendMail()
go sender.Github()
if !config.Debug {
go sender.SendBili(biliClient)
}
}
func main() {
config, err := configs.GetConfig()
if err != nil {
log.Printf("获取配置文件失败: %v", err)
}
// 定时任务
reminder.Reminder()
c := cron.New()
if config.Debug {
c.AddFunc("@every 1s", reminder.Reminder)
} else {
c.AddFunc("@every 1h", reminder.Reminder)
}
c.Start()
// 前端/后端
r := gin.Default()
templFS, _ := fs.Sub(templateFiles, "templates")
r.SetHTMLTemplate(frontend.LoadTemplates(templFS))
staticFS, _ := fs.Sub(staticFiles, "static")
r.StaticFS("/static", http.FS(staticFS))
store := cookie.NewStore(generateRandomKey(32))
r.Use(sessions.Sessions("goodbaby-session", store))
v1 := r.Group("/api/v1")
{
v1.POST("/user/registry", user.HandleRegistry)
v1.POST("/user/login", user.HandleLogin)
}
// 需要认证的路由组
authorized := v1.Group("/")
authorized.Use(user.AuthMiddleware())
{
authorized.GET("/user/info", user.HandleGetUserInfo)
authorized.GET("/rules", rule.HandleGetAllRules)
authorized.POST("/rules", rule.HandleCreateRule)
authorized.GET("/accounts", account.HandleGetAllAccounts)
authorized.POST("/accounts", account.HandleAddAccount)
}
r.GET("/", frontend.IndexPage)
r.GET("/signal", handleSignal)
r.GET("/timer/status", reminder.HandleTimerStatus)
r.Run(":8088")
}
// Handle Signal
func handleSignal(c *gin.Context) {
config, err := configs.GetConfig()
func generateRandomKey(length int) []byte {
key := make([]byte, length)
_, err := rand.Read(key)
if err != nil {
log.Printf("获取配置文件失败: %v", err)
panic(fmt.Sprintf("无法生成随机密钥: %v", err))
}
secret := c.Query("secret")
if secret != config.SignalSecret {
c.JSON(403, gin.H{
"code": 403,
"message": "secret error",
})
return
}
timer.Reset(duration)
reminder.GlobalTimerManager.Reset()
c.JSON(200, gin.H{
"code": 200,
"message": "ok",
})
log.Println("触发信号")
return key
}
+9
View File
@@ -0,0 +1,9 @@
package model
type Account struct {
ID uint `gorm:"primaryKey" json:"id"`
UID uint `json:"uid"`
Name string `json:"name"` // 账号名称
Type string `json:"type"` // 账号类型,如 "bilibili"
Config string `json:"config"` // 存储账号相关配置,如cookie等
}
+11
View File
@@ -0,0 +1,11 @@
package model
type Rule struct {
ID uint `json:"id" gorm:"primaryKey"`
UID uint `json:"uid"`
TimerID uint `json:"timer_id"`
Name string `json:"name"`
AccountID uint `json:"account_id"`
Type string `json:"type"`
ConfigJson string `json:"config_json"`
}
+9
View File
@@ -0,0 +1,9 @@
package model
type Timer struct {
ID uint `gorm:"primaryKey" json:"id"`
UID uint `json:"uid"`
Name string `json:"name"`
SignDerationSeconds int64 `json:"sign_deration_seconds"`
LastSign int64 `json:"last_sign"`
}
+43
View File
@@ -0,0 +1,43 @@
package model
import (
"fmt"
"github.com/ssdomei232/goodBaby/handler/db"
)
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
CreateAt int64 `json:"create_at"`
Username string `json:"username"`
Password string `json:"-"`
DingTalkConfig *string `json:"dingtalk_config"`
}
func (u *User) IsValid() error {
if u.Username == "" || u.Password == "" {
return fmt.Errorf("用户名或密码不能为空")
}
if len(u.Username) > 32 || len(u.Username) < 2 {
return fmt.Errorf("用户名过长或过短")
}
if len(u.Password) > 64 || len(u.Password) < 6 {
return fmt.Errorf("密码过长或过短")
}
return nil
}
func (u *User) IsExist() bool {
db, err := db.GetGormDB()
if err != nil {
return false
}
var count int64
result := db.Where("username = ?", u.Username).Count(&count)
if result.Error != nil {
return false
}
return count > 0
}