first commit
This commit is contained in:
+500
@@ -0,0 +1,500 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cdle/sillyplus/core/common"
|
||||
"github.com/cdle/sillyplus/core/logs"
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
type Message map[string]string
|
||||
|
||||
type Details struct {
|
||||
Content string
|
||||
UserID string
|
||||
ChatID string
|
||||
Username string
|
||||
Chatname string
|
||||
MessageID string
|
||||
}
|
||||
|
||||
type CustomSender struct {
|
||||
BaseSender
|
||||
details Details
|
||||
f *Factory
|
||||
}
|
||||
|
||||
type MsgChan struct {
|
||||
Chan chan string
|
||||
Msg map[string]string
|
||||
}
|
||||
|
||||
type Factory struct {
|
||||
botid string
|
||||
botplt string
|
||||
uuid string
|
||||
msgChan chan MsgChan
|
||||
demo *CustomSender
|
||||
reply func(map[string]string) string
|
||||
lm chan bool
|
||||
nm int64
|
||||
recallMessage func(interface{}) bool
|
||||
groupKick func(uid string, gid string, reject_add_request bool) bool
|
||||
groupBan func(uid string, gid string, duration int) bool
|
||||
groupUnban func(uid string, gid string) bool
|
||||
isAdmin func(string) bool
|
||||
vm *goja.Runtime
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
destroid bool
|
||||
}
|
||||
|
||||
type Bot [2]string //botplt botid
|
||||
|
||||
var Bots = map[Bot]*Factory{}
|
||||
var BotsLocker sync.RWMutex
|
||||
|
||||
var ErrNotFind = errors.New("adapter not find")
|
||||
|
||||
func DestroyAdapterByUUID(uuid string) {
|
||||
BotsLocker.RLock()
|
||||
defer BotsLocker.RUnlock()
|
||||
for i := range Bots {
|
||||
if Bots[i].uuid == uuid {
|
||||
go Bots[i].Destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetAdapter(botids ...string) (*Factory, error) {
|
||||
BotsLocker.RLock()
|
||||
defer BotsLocker.RUnlock()
|
||||
botplt := botids[0]
|
||||
bots_id := botids[1:]
|
||||
var bots = []*Factory{}
|
||||
var select_bots = []*Factory{}
|
||||
for i := range Bots {
|
||||
plt, id := i[0], i[1]
|
||||
for j := range bots_id {
|
||||
if plt == botplt && bots_id[j] == id {
|
||||
select_bots = append(select_bots, Bots[i])
|
||||
} else if plt == botplt {
|
||||
bots = append(bots, Bots[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(bots) == 0 {
|
||||
return nil, ErrNotFind
|
||||
}
|
||||
if len(select_bots) != 0 {
|
||||
i := rand.Intn(len(select_bots))
|
||||
return select_bots[i], nil
|
||||
}
|
||||
i := rand.Intn(len(bots))
|
||||
return bots[i], ErrNotFind
|
||||
}
|
||||
func GetAdapterBotsID(botplt string) []string {
|
||||
BotsLocker.RLock()
|
||||
defer BotsLocker.RUnlock()
|
||||
var bots_id = []string{}
|
||||
for i := range Bots {
|
||||
// fmt.Println("==", botplt == i[0], botplt, i[0])
|
||||
if botplt == i[0] {
|
||||
bots_id = append(bots_id, i[1])
|
||||
}
|
||||
}
|
||||
return bots_id
|
||||
}
|
||||
func GetAdapterBotPlts() []string {
|
||||
BotsLocker.RLock()
|
||||
defer BotsLocker.RUnlock()
|
||||
var bot_plts = []string{}
|
||||
for i := range Bots {
|
||||
// fmt.Println("==", botplt == i[0], botplt, i[0])
|
||||
has := false
|
||||
for _, bot_plt := range bot_plts {
|
||||
if bot_plt == i[0] {
|
||||
has = true
|
||||
}
|
||||
}
|
||||
if !has {
|
||||
bot_plts = append(bot_plts, i[0])
|
||||
}
|
||||
}
|
||||
return bot_plts
|
||||
}
|
||||
|
||||
func (f *Factory) Init(botplt, botid string) {
|
||||
f.ctx, f.cancel = context.WithCancel(context.Background())
|
||||
BotsLocker.Lock()
|
||||
defer BotsLocker.Unlock()
|
||||
f.botplt = botplt
|
||||
f.botid = botid
|
||||
f.msgChan = make(chan MsgChan, 100000)
|
||||
f.demo = &CustomSender{
|
||||
f: f,
|
||||
}
|
||||
if _, ok := Bots[[2]string{botplt, botid}]; ok {
|
||||
console.Warn("%s机器人%s因冲突销毁!", botplt, botid)
|
||||
}
|
||||
Bots[[2]string{botplt, botid}] = f
|
||||
f.lm = make(chan bool, 10)
|
||||
f.nm = 0
|
||||
if botid != "" {
|
||||
botid = fmt.Sprintf("(%s)", botid)
|
||||
}
|
||||
console.Log("%s机器人%s已初始化。", botplt, botid)
|
||||
}
|
||||
|
||||
func (f *Factory) IsAdapter(botid string) bool {
|
||||
BotsLocker.RLock()
|
||||
defer BotsLocker.RUnlock()
|
||||
for i := range Bots {
|
||||
id := i[0]
|
||||
if botid == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *Factory) Destroy() {
|
||||
BotsLocker.Lock()
|
||||
defer BotsLocker.Unlock()
|
||||
f.destroid = true
|
||||
f.cancel()
|
||||
close(f.msgChan)
|
||||
delete(Bots, [2]string{f.botplt, f.botid})
|
||||
botid := ""
|
||||
if f.botid != "" {
|
||||
botid = fmt.Sprintf("(%s)", f.botid)
|
||||
} else {
|
||||
botid = f.botid
|
||||
}
|
||||
console.Log("%s机器人%s已销毁。", strings.ToUpper(f.botplt), botid)
|
||||
}
|
||||
|
||||
func (f *Factory) Push(msg map[string]string) (string, error) {
|
||||
var demo = *f.demo
|
||||
var sender = &demo
|
||||
fsps := &common.FakerSenderParams{
|
||||
UserID: msg[USER_ID],
|
||||
}
|
||||
fsps.ChatID = msg[CHAT_ID]
|
||||
sender.SetFsps(fsps)
|
||||
return sender.Reply(msg[CONETNT], PUSH(""))
|
||||
}
|
||||
|
||||
func (f *Factory) SetReplyHandler(function func(map[string]string) string) {
|
||||
f.reply = func(m map[string]string) string {
|
||||
if f.uuid != "" {
|
||||
mutex := GetMutex(f.uuid)
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
}
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
console.Error("Sender(\""+f.botplt+"\").SetReply error:", err)
|
||||
}
|
||||
}()
|
||||
return function(m)
|
||||
}
|
||||
}
|
||||
|
||||
// func (f *Factory) GetReplies() {
|
||||
|
||||
// }
|
||||
|
||||
func (f *Factory) GetReplyMessage() *goja.Promise {
|
||||
promise, resolve, reject := f.vm.NewPromise()
|
||||
go func() {
|
||||
select {
|
||||
case <-f.ctx.Done():
|
||||
logs.Debug("%s adapter %s destroied", f.botplt, f.botid)
|
||||
reject("adapter destroied")
|
||||
case mc := <-f.msgChan:
|
||||
obj := f.vm.NewObject()
|
||||
for k, v := range mc.Msg {
|
||||
obj.Set(k, v)
|
||||
}
|
||||
resolve(f.vm.NewProxy(obj, &goja.ProxyTrapConfig{
|
||||
Set: func(target *goja.Object, property string, value, receiver goja.Value) (success bool) {
|
||||
if property == "message_id" {
|
||||
select {
|
||||
case <-mc.Chan:
|
||||
return false
|
||||
case <-time.After(time.Millisecond):
|
||||
}
|
||||
mc.Chan <- fmt.Sprint(value.Export())
|
||||
}
|
||||
return true
|
||||
},
|
||||
}))
|
||||
}
|
||||
}()
|
||||
return promise
|
||||
}
|
||||
|
||||
func (f *Factory) Send(function func(map[string]string) string) {
|
||||
f.SetReplyHandler(function)
|
||||
}
|
||||
|
||||
func (f *Factory) SetRecallMessage(function func(interface{}) bool) {
|
||||
f.recallMessage = func(i interface{}) bool {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
console.Error("Sender(\""+f.botplt+"\").recall error:", err)
|
||||
}
|
||||
}()
|
||||
return function(i)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Factory) SetGroupKick(function func(uid string, gid string, reject_add_request bool) bool) {
|
||||
f.groupKick = func(uid string, gid string, reject_add_request bool) bool {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
console.Error("Sender(\""+f.botplt+"\").GroupKick error:", err)
|
||||
}
|
||||
}()
|
||||
return function(uid, gid, reject_add_request)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Factory) SetGroupBan(function func(uid string, gid string, duration int) bool) {
|
||||
f.groupBan = func(uid string, gid string, duration int) bool {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
console.Error("Sender(\""+f.botplt+"\").SetGroupBan error:", err)
|
||||
}
|
||||
}()
|
||||
return function(uid, gid, duration)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Factory) SetGroupUnban(function func(uid string, gid string) bool) {
|
||||
f.groupUnban = func(uid string, gid string) bool {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
console.Error("Sender(\""+f.botplt+"\").SetgroupUnban error:", err)
|
||||
}
|
||||
}()
|
||||
return function(uid, gid)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Factory) SetIsAdmin(function func(string) bool) {
|
||||
f.isAdmin = func(uid string) bool {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
console.Error("Sender(\""+f.botplt+"\").SetAdmin error:", err)
|
||||
}
|
||||
}()
|
||||
return function(uid)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Factory) Receive(wt interface{}) *CustomSender {
|
||||
var demo = *f.demo
|
||||
sender := &demo
|
||||
props := wt.(map[string]interface{})
|
||||
for i := range props {
|
||||
switch strings.ToLower(i) {
|
||||
case "content":
|
||||
sender.details.Content = fmt.Sprint(props[i])
|
||||
case "message_id", "messageId":
|
||||
sender.details.MessageID = fmt.Sprint(props[i])
|
||||
case "user_id", "userId":
|
||||
sender.details.UserID = fmt.Sprint(props[i])
|
||||
case "chat_id", "chatId", "group_id", "groupId", "group_code", "groupCode":
|
||||
sender.details.ChatID = fmt.Sprint(props[i])
|
||||
case "user_name", "userName":
|
||||
sender.details.Username = fmt.Sprint(props[i])
|
||||
case "chat_name", "chatName", "groupName", "group_name":
|
||||
sender.details.Chatname = fmt.Sprint(props[i])
|
||||
}
|
||||
}
|
||||
if sender.details.Content != "" {
|
||||
Messages <- sender
|
||||
}
|
||||
return sender
|
||||
}
|
||||
|
||||
func (sender *CustomSender) GetContent() string {
|
||||
if sender.Fsps.Content != "" {
|
||||
return sender.Fsps.Content
|
||||
}
|
||||
return sender.details.Content
|
||||
}
|
||||
func (sender *CustomSender) GetUserID() string {
|
||||
if sender.Fsps.UserID != "" {
|
||||
return sender.Fsps.UserID
|
||||
}
|
||||
return sender.details.UserID
|
||||
}
|
||||
func (sender *CustomSender) GetChatID() string {
|
||||
if !utils.IsZeroOrEmpty(sender.Fsps.ChatID) {
|
||||
return fmt.Sprint(sender.Fsps.ChatID)
|
||||
}
|
||||
return sender.details.ChatID
|
||||
}
|
||||
func (sender *CustomSender) GetImType() string {
|
||||
return sender.f.botplt
|
||||
}
|
||||
func (sender *CustomSender) GetUserName() string {
|
||||
return sender.details.Username
|
||||
}
|
||||
func (sender *CustomSender) GetChatName() string {
|
||||
return sender.details.Chatname
|
||||
}
|
||||
func (sender *CustomSender) GetMessageID() string {
|
||||
return sender.details.MessageID
|
||||
}
|
||||
func (sender *CustomSender) GetReplySenderUserID() int {
|
||||
if !sender.IsReply() {
|
||||
return 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (sender *CustomSender) GetBotID() string {
|
||||
return sender.f.botid
|
||||
}
|
||||
|
||||
type PUSH string
|
||||
|
||||
func (sender *CustomSender) Reply(msgs ...interface{}) (string, error) {
|
||||
var push = false
|
||||
var content = ""
|
||||
for _, item := range msgs {
|
||||
switch item := item.(type) {
|
||||
case PUSH:
|
||||
push = true
|
||||
case string:
|
||||
content = item
|
||||
}
|
||||
}
|
||||
if !push {
|
||||
if IsNoReplyGroup(sender) {
|
||||
return "", errors.New("is no reply group")
|
||||
}
|
||||
}
|
||||
content = strings.ReplaceAll(content, "\n\r", "\n")
|
||||
content = strings.ReplaceAll(content, "\r\n", "\n")
|
||||
content = strings.ReplaceAll(content, "\r", "\n")
|
||||
content = regexp.MustCompile("[\n]{3,}").ReplaceAllString(content, "\n\n")
|
||||
if content != "" {
|
||||
msg := map[string]string{
|
||||
"message_id": sender.GetMessageID(),
|
||||
"content": content,
|
||||
"user_id": sender.GetUserID(),
|
||||
"chat_id": sender.GetChatID(),
|
||||
// "bot_id": sender.GetBotID(),
|
||||
|
||||
// "uuid": utils.GenUUID(),
|
||||
}
|
||||
if sender.f.reply == nil {
|
||||
c := MsgChan{
|
||||
Msg: msg,
|
||||
Chan: make(chan string),
|
||||
}
|
||||
if sender.f.destroid {
|
||||
return "", errors.New("adapter destroid")
|
||||
}
|
||||
sender.f.msgChan <- c
|
||||
select {
|
||||
case id := <-c.Chan:
|
||||
return id, nil
|
||||
case <-time.After(time.Second * 5):
|
||||
close(c.Chan)
|
||||
return "", errors.New("get message_id timeout")
|
||||
}
|
||||
} else {
|
||||
//todo 阻塞延迟异常
|
||||
v := sender.f.reply(msg)
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
const (
|
||||
MESSAGE_ID = "message_id"
|
||||
CONETNT = "content"
|
||||
USER_ID = "user_id"
|
||||
CHAT_ID = "chat_id"
|
||||
IS_ADMIN = "is_admin"
|
||||
// UUID = "message_id"
|
||||
)
|
||||
|
||||
func (sender *CustomSender) Copy() common.Sender {
|
||||
new := reflect.Indirect(reflect.ValueOf(interface{}(sender))).Interface().(CustomSender)
|
||||
return &new
|
||||
}
|
||||
|
||||
func (sender *CustomSender) RecallMessage(ps ...interface{}) error {
|
||||
if sender.f.recallMessage == nil {
|
||||
return nil
|
||||
}
|
||||
for _, p := range ps {
|
||||
switch p := p.(type) {
|
||||
case string:
|
||||
sender.f.recallMessage(p)
|
||||
case []string:
|
||||
for _, v := range p {
|
||||
sender.f.recallMessage(v)
|
||||
}
|
||||
case [][]string:
|
||||
for _, v := range p {
|
||||
for _, v2 := range v {
|
||||
sender.f.recallMessage(v2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sender *CustomSender) GroupKick(uid string, reject_add_request bool) {
|
||||
if sender.f.groupKick == nil {
|
||||
return
|
||||
}
|
||||
sender.f.groupKick(uid, sender.GetChatID(), reject_add_request)
|
||||
}
|
||||
|
||||
func (sender *CustomSender) GroupBan(uid string, duration int) {
|
||||
if sender.f.groupBan == nil {
|
||||
return
|
||||
}
|
||||
sender.f.groupBan(uid, sender.GetChatID(), duration)
|
||||
}
|
||||
|
||||
func (sender *CustomSender) GroupUnban(uid string) {
|
||||
if sender.f.groupUnban == nil {
|
||||
return
|
||||
}
|
||||
sender.f.groupUnban(uid, sender.GetChatID())
|
||||
}
|
||||
|
||||
func (sender *CustomSender) IsAdmin() bool {
|
||||
if sender.f.isAdmin == nil {
|
||||
return Contains(strings.Split(MakeBucket(sender.f.botplt).GetString("masters"), "&"), sender.GetUserID())
|
||||
}
|
||||
return sender.f.isAdmin(sender.GetUserID())
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cdle/sillyplus/core/storage"
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var authBucket = MakeBucket("auths")
|
||||
var auths = []*Auth{}
|
||||
|
||||
func init() {
|
||||
storage.Watch(sillyGirl, "name", func(old, new, key string) *storage.Final {
|
||||
if old == new {
|
||||
return &storage.Final{
|
||||
Error: errors.New("unchanged"),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
authBucket.Foreach(func(b1, b2 []byte) error {
|
||||
auth := &Auth{}
|
||||
if json.Unmarshal(b2, auth) == nil {
|
||||
if math.Abs(float64(int(time.Now().Unix())-auth.CreatedAt)) < 86400 {
|
||||
auths = append(auths, auth)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
var password = sillyGirl.GetString("password")
|
||||
var name = sillyGirl.GetString("name", "傻妞")
|
||||
if password == "" {
|
||||
password = utils.GenUUID()
|
||||
console.Info("可视化面板临时账号密码:%s %s", name, password)
|
||||
}
|
||||
storage.Watch(sillyGirl, "password", func(old, new, key string) *storage.Final {
|
||||
password, _ = EncryptByAes([]byte(new))
|
||||
return &storage.Final{
|
||||
Now: password,
|
||||
}
|
||||
})
|
||||
storage.Watch(sillyGirl, "name", func(old, new, key string) *storage.Final {
|
||||
name = new
|
||||
return nil
|
||||
})
|
||||
///可视化部分
|
||||
GinApi(POST, "/api/login/account", func(ctx *gin.Context) {
|
||||
var auth = struct {
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
}{}
|
||||
json.NewDecoder(ctx.Request.Body).Decode(&auth)
|
||||
epassword, _ := EncryptByAes([]byte(auth.Password))
|
||||
if (auth.Password == password || epassword == password) && auth.Username == name {
|
||||
token := utils.GenUUID()
|
||||
auth := &Auth{
|
||||
IP: ctx.ClientIP(),
|
||||
UserAgent: ctx.Request.UserAgent(),
|
||||
Token: token,
|
||||
CreatedAt: int(time.Now().Unix()),
|
||||
}
|
||||
authBucket.Create(auth)
|
||||
auths = append(auths, auth)
|
||||
console.Log("登录成功,当前有效令牌数%d,总数%d", len(ValidAuths()), len(auths))
|
||||
ctx.SetCookie("token", token, 86400, "/", "", false, true)
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"status": "ok",
|
||||
"type": "account",
|
||||
"currentAuthority": "admin",
|
||||
})
|
||||
} else {
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"status": "error",
|
||||
"type": "account",
|
||||
"currentAuthority": "guest",
|
||||
})
|
||||
}
|
||||
})
|
||||
GinApi(POST, "/api/login/outLogin", DestroyAuth, func(ctx *gin.Context) {
|
||||
sillyGirl.Set("web_token", "")
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
})
|
||||
pluginNextUuid := sillyGirl.GetString("pluginNextUuid")
|
||||
if pluginNextUuid == "" {
|
||||
pluginNextUuid = utils.GenUUID()
|
||||
sillyGirl.Set("pluginNextUuid", pluginNextUuid)
|
||||
}
|
||||
GinApi(GET, "/api/currentUser", RequireAuth, func(ctx *gin.Context) {
|
||||
rs := []Route{}
|
||||
for _, f := range Functions {
|
||||
if f.UUID == pluginNextUuid {
|
||||
pluginNextUuid = utils.GenUUID()
|
||||
sillyGirl.Set("pluginNextUuid", pluginNextUuid)
|
||||
}
|
||||
if f.UUID != "" {
|
||||
name := f.Title
|
||||
if name == "" {
|
||||
name = "无名脚本"
|
||||
}
|
||||
if f.Module {
|
||||
name = name + " 🔧"
|
||||
}
|
||||
if f.OnStart {
|
||||
name = name + " 💫"
|
||||
}
|
||||
if f.Encrypt {
|
||||
name = name + " 🔒"
|
||||
}
|
||||
if f.Public {
|
||||
name = name + " 👑"
|
||||
}
|
||||
rs = append(rs, Route{
|
||||
Path: fmt.Sprintf(`/script/%s`, f.UUID),
|
||||
Name: name,
|
||||
Component: "./Script",
|
||||
CreateAt: f.CreateAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
rrs := rs
|
||||
n := len(rrs)
|
||||
flag := true
|
||||
for i := 0; i < n && flag; i++ {
|
||||
flag = false
|
||||
for j := 0; j < n-i-1; j++ {
|
||||
if rrs[j].CreateAt < rrs[j+1].CreateAt {
|
||||
rrs[j], rrs[j+1] = rrs[j+1], rrs[j]
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
}
|
||||
rrs = append(rrs, Route{
|
||||
Path: fmt.Sprintf(`/script/%s`, pluginNextUuid),
|
||||
Name: "+新增脚本",
|
||||
Component: "./Script",
|
||||
})
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
"data": map[string]interface{}{
|
||||
"name": sillyGirl.GetString("name"),
|
||||
"avatar": "https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png",
|
||||
"plugins": rrs,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func DestroyAuth(c *gin.Context) {
|
||||
token, _ := c.Cookie("token")
|
||||
auth, _ := CheckAuth(token)
|
||||
if auth != nil {
|
||||
auth.ExpiredAt = int(time.Now().Unix())
|
||||
authBucket.Create(auth)
|
||||
}
|
||||
}
|
||||
|
||||
var tempAuth sync.Map
|
||||
|
||||
func getTempAuth() string {
|
||||
uuid := utils.GenUUID()
|
||||
tempAuth.Store(uuid, time.Now().Unix())
|
||||
return uuid
|
||||
}
|
||||
|
||||
func checkTempAuth(uuid string) bool {
|
||||
unix, ok := tempAuth.LoadAndDelete(uuid)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if time.Now().Unix()-unix.(int64) > 1 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func RequireAuth(c *gin.Context) {
|
||||
token, _ := c.Cookie("token")
|
||||
_, err := CheckAuth(token)
|
||||
if err != nil && !checkTempAuth(token) {
|
||||
c.JSON(401, map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"isLogin": false,
|
||||
},
|
||||
"errorCode": "401",
|
||||
"errorMessage": err.Error(),
|
||||
"success": true,
|
||||
"showType": 9,
|
||||
})
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckAuth(token string) (*Auth, error) {
|
||||
var errorMessage = "请先登录!"
|
||||
if token != "" {
|
||||
auths := auths
|
||||
for i := range auths {
|
||||
if auths[i].Token == token && auths[i].ExpiredAt == 0 {
|
||||
if math.Abs(float64(int(time.Now().Unix())-auths[i].CreatedAt)) > 86400 {
|
||||
auths[i].ExpiredAt = int(time.Now().Unix())
|
||||
authBucket.Create(auths[i])
|
||||
errorMessage = "授权已过期!"
|
||||
} else {
|
||||
return auths[i], nil
|
||||
}
|
||||
} else {
|
||||
errorMessage = "非法访问!"
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errors.New(errorMessage)
|
||||
}
|
||||
|
||||
func ValidAuths() []*Auth {
|
||||
tmp := []*Auth{}
|
||||
for _, auth := range auths {
|
||||
if auth.ExpiredAt == 0 {
|
||||
tmp = append(tmp, auth)
|
||||
}
|
||||
|
||||
}
|
||||
return tmp
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
func init() {
|
||||
var sillyGirl = MakeBucket("sillyGirl")
|
||||
GinApi(GET, "/api/storage/list", RequireAuth, func(ctx *gin.Context) {
|
||||
page, _ := strconv.Atoi(ctx.DefaultQuery("current", "1"))
|
||||
perPage, _ := strconv.Atoi(ctx.DefaultQuery("pageSize", "20"))
|
||||
keys := ctx.Query("keys")
|
||||
data := []map[string]string{}
|
||||
arr := strings.Split(keys, ",")
|
||||
if keys == "" {
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
"data": data,
|
||||
"page": page,
|
||||
"total": len(data),
|
||||
})
|
||||
return
|
||||
}
|
||||
for _, bk := range arr {
|
||||
ar := strings.Split(bk, ".")
|
||||
if len(ar) == 2 {
|
||||
if ar[0] == "plugins" && false { //todo
|
||||
// data[bk] = halfDeEct(MakeBucket(ar[0]).GetString(ar[1]))
|
||||
} else {
|
||||
// data[bk] = MakeBucket(ar[0]).GetString(ar[1])
|
||||
data = append(data, map[string]string{
|
||||
"bucket": ar[0],
|
||||
"key": ar[1],
|
||||
"value": MakeBucket(ar[0]).GetString(ar[1]),
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(ar) == 1 {
|
||||
MakeBucket(ar[0]).Foreach(func(b1, b2 []byte) error {
|
||||
data = append(data, map[string]string{
|
||||
"bucket": bk,
|
||||
"key": string(b1),
|
||||
"value": string(b2),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
start := (page - 1) * perPage
|
||||
end := start + perPage
|
||||
if end > len(data) {
|
||||
end = len(data)
|
||||
}
|
||||
res := data[start:end]
|
||||
index := start + 1
|
||||
for i := range res {
|
||||
res[i]["index"] = fmt.Sprint(index)
|
||||
index++
|
||||
}
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
"data": res,
|
||||
"page": page,
|
||||
"total": len(data),
|
||||
})
|
||||
})
|
||||
GinApi(GET, "/api/storage", RequireAuth, func(ctx *gin.Context) {
|
||||
keys := ctx.Query("keys")
|
||||
if keys == "" {
|
||||
buckets := sillyGirl.Buckets()
|
||||
search := ctx.Query("search")
|
||||
res := []map[string]interface{}{}
|
||||
if search == "" {
|
||||
for _, bucket := range buckets {
|
||||
if bucket == "plugins" {
|
||||
continue
|
||||
}
|
||||
res = append(res, map[string]interface{}{
|
||||
"value": bucket,
|
||||
"text": "[桶] " + bucket,
|
||||
})
|
||||
}
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
"data": res,
|
||||
})
|
||||
return
|
||||
}
|
||||
for _, bucket := range buckets {
|
||||
if bucket == "plugins" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(bucket, search) {
|
||||
res = append(res, map[string]interface{}{
|
||||
"value": bucket,
|
||||
"text": "[桶] " + bucket,
|
||||
})
|
||||
}
|
||||
b := MakeBucket(bucket)
|
||||
b.Foreach(func(b1, b2 []byte) error {
|
||||
key := string(b1)
|
||||
value := string(b2)
|
||||
if strings.Contains(key, search) {
|
||||
res = append(res, map[string]interface{}{
|
||||
"value": bucket + "." + key,
|
||||
"text": "[键] " + key,
|
||||
})
|
||||
}
|
||||
if strings.Contains(value, search) {
|
||||
res = append(res, map[string]interface{}{
|
||||
"value": bucket + "." + key,
|
||||
"text": "[值] " + value,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
"data": res,
|
||||
})
|
||||
return
|
||||
}
|
||||
data := map[string]interface{}{}
|
||||
arr := strings.Split(keys, ",")
|
||||
for _, bk := range arr {
|
||||
ar := strings.Split(bk, ".")
|
||||
if len(ar) == 2 {
|
||||
if ar[0] == "plugins" && false { //todo
|
||||
data[bk] = halfDeEct(MakeBucket(ar[0]).GetString(ar[1]))
|
||||
} else {
|
||||
data[bk] = MakeBucket(ar[0]).GetString(ar[1])
|
||||
}
|
||||
}
|
||||
if len(ar) == 1 {
|
||||
MakeBucket(ar[0]).Foreach(func(b1, b2 []byte) error {
|
||||
data[bk+"."+string(b1)] = string(b2)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
"data": data,
|
||||
})
|
||||
})
|
||||
GinApi(PUT, "/api/storage", RequireAuth, func(ctx *gin.Context) {
|
||||
data, err := ioutil.ReadAll(ctx.Request.Body)
|
||||
if err != nil {
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": false,
|
||||
"errorMessage": err.Error(),
|
||||
"showType": 2,
|
||||
})
|
||||
return
|
||||
}
|
||||
updates := map[string]interface{}{}
|
||||
err = json.Unmarshal(data, &updates)
|
||||
if err != nil {
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": false,
|
||||
"errorMessage": err.Error(),
|
||||
"showType": 2,
|
||||
})
|
||||
return
|
||||
}
|
||||
messages := map[string]interface{}{}
|
||||
errors := map[string]interface{}{}
|
||||
for bk, v := range updates {
|
||||
ar := strings.Split(bk, ".")
|
||||
if len(ar) == 2 {
|
||||
msg, err := MakeBucket(ar[0]).Set(ar[1], v)
|
||||
if msg != "" {
|
||||
messages[bk] = msg
|
||||
}
|
||||
if err != nil {
|
||||
errors[bk] = err.Error()
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
"messages": messages,
|
||||
"errors": errors,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/cdle/sillyplus/core/common"
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
)
|
||||
|
||||
var RegistedSenders = map[string]func() common.Sender{}
|
||||
|
||||
type Faker struct {
|
||||
Message string
|
||||
Type string
|
||||
UserID string
|
||||
ChatID string
|
||||
Carry chan string
|
||||
BaseSender
|
||||
Admin bool
|
||||
}
|
||||
|
||||
func (sender *Faker) Listen() chan string {
|
||||
return sender.Carry
|
||||
}
|
||||
|
||||
func (sender *Faker) GetContent() string {
|
||||
if sender.Fsps.Content != "" {
|
||||
return sender.Fsps.Content
|
||||
}
|
||||
return sender.Message
|
||||
}
|
||||
|
||||
func (sender *Faker) GetUserID() string {
|
||||
return sender.UserID
|
||||
}
|
||||
|
||||
func (sender *Faker) GetBotID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sender *Faker) GetChatID() string {
|
||||
return sender.ChatID
|
||||
}
|
||||
|
||||
func (sender *Faker) GetImType() string {
|
||||
if sender.Type == "" {
|
||||
return "fake"
|
||||
}
|
||||
return sender.Type
|
||||
}
|
||||
|
||||
func (sender *Faker) GetMessageID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sender *Faker) GetUserName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sender *Faker) GetChatName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sender *Faker) IsReply() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (sender *Faker) GetReplyUserID() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (sender *Faker) GetRawMessage() interface{} {
|
||||
return sender.Message
|
||||
}
|
||||
|
||||
func (sender *Faker) IsAdmin() bool {
|
||||
return sender.Admin
|
||||
}
|
||||
|
||||
func (sender *Faker) IsMedia() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (sender *Faker) Reply(msgs ...interface{}) (string, error) {
|
||||
rt := ""
|
||||
for _, msg := range msgs {
|
||||
switch msg := msg.(type) {
|
||||
case []byte:
|
||||
rt = (string(msg))
|
||||
case string:
|
||||
rt = msg
|
||||
}
|
||||
}
|
||||
{
|
||||
|
||||
for _, v := range regexp.MustCompile(`\[CQ:image,file=([^\[\]]+)\]`).FindAllStringSubmatch(rt, -1) {
|
||||
// qr := qrcode2console.NewQRCode2ConsoleWithUrl(v[1], true)
|
||||
// defer qr.Output()
|
||||
rt = strings.Replace(rt, fmt.Sprintf(`[CQ:image,file=%s]`, v[1]), "", -1)
|
||||
}
|
||||
}
|
||||
|
||||
// if rt != "" && n != nil {
|
||||
// NotifyMasters(rt)
|
||||
// }
|
||||
|
||||
// if rt != "" && sender.Carry != nil {
|
||||
// sender.Carry <- rt
|
||||
// }
|
||||
|
||||
if rt != "" && sender.Type == "terminal" {
|
||||
fmt.Printf("\x1b[%dm%s \x1b[0m\n", 31, rt)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (sender *Faker) Delete() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sender *Faker) Disappear(lifetime ...time.Duration) {
|
||||
|
||||
}
|
||||
|
||||
func (sender *Faker) Finish() {
|
||||
if sender.Carry != nil {
|
||||
close(sender.Carry)
|
||||
}
|
||||
}
|
||||
|
||||
func (sender *Faker) Copy() common.Sender {
|
||||
new := reflect.Indirect(reflect.ValueOf(interface{}(sender))).Interface().(Faker)
|
||||
return &new
|
||||
}
|
||||
|
||||
func (sender *Faker) GroupKick(uid string, reject_add_request bool) {
|
||||
|
||||
}
|
||||
|
||||
func (sender *Faker) GroupBan(uid string, duration int) {
|
||||
|
||||
}
|
||||
|
||||
type BaseSender struct {
|
||||
matches [][]string
|
||||
goon bool
|
||||
Fsps common.FakerSenderParams
|
||||
Atlast bool
|
||||
ToSendMessages []string
|
||||
IsFinished bool
|
||||
Duration *time.Duration
|
||||
mark interface{}
|
||||
params []string
|
||||
level int
|
||||
}
|
||||
|
||||
func (sender *BaseSender) SetLevel(l int) {
|
||||
sender.level = l
|
||||
}
|
||||
|
||||
func (sender *BaseSender) GetLevel() int {
|
||||
return sender.level
|
||||
}
|
||||
|
||||
func (sender *BaseSender) SetMark(mark interface{}) {
|
||||
sender.mark = mark
|
||||
}
|
||||
|
||||
func (sender *BaseSender) GetMark() interface{} {
|
||||
return sender.mark
|
||||
}
|
||||
|
||||
func (sender *BaseSender) SetMatch(ss []string) {
|
||||
sender.matches = [][]string{ss}
|
||||
}
|
||||
func (sender *BaseSender) SetParams(ss []string) {
|
||||
sender.params = ss
|
||||
}
|
||||
func (sender *BaseSender) SetAllMatch(ss [][]string) {
|
||||
sender.matches = ss
|
||||
}
|
||||
|
||||
func (sender *BaseSender) SetContent(content string) {
|
||||
sender.Fsps.Content = content
|
||||
}
|
||||
|
||||
func (sender *BaseSender) SetFsps(fsps *common.FakerSenderParams) {
|
||||
sender.Fsps = *fsps
|
||||
}
|
||||
|
||||
func (sender *BaseSender) GetMatch() []string {
|
||||
return sender.matches[0]
|
||||
}
|
||||
|
||||
func (sender *BaseSender) GetAllMatch() [][]string {
|
||||
return sender.matches
|
||||
}
|
||||
|
||||
func (sender *BaseSender) Continue() {
|
||||
sender.goon = true
|
||||
}
|
||||
|
||||
func (sender *BaseSender) IsContinue() bool {
|
||||
return sender.goon
|
||||
}
|
||||
|
||||
func (sender *BaseSender) ClearContinue() {
|
||||
sender.goon = false
|
||||
}
|
||||
|
||||
func (sender *BaseSender) Get(i interface{}) string {
|
||||
switch i := i.(type) {
|
||||
case int:
|
||||
if len(sender.matches) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(sender.matches[0]) < i+1 {
|
||||
return ""
|
||||
}
|
||||
return sender.matches[0][i]
|
||||
case string:
|
||||
for j := range sender.params {
|
||||
if sender.params[j] == i {
|
||||
return sender.Get(j)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sender *BaseSender) Delete() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sender *BaseSender) Disappear(lifetime ...time.Duration) {
|
||||
|
||||
}
|
||||
|
||||
func (sender *BaseSender) Finish() {
|
||||
sender.IsFinished = true
|
||||
}
|
||||
|
||||
func (sender *BaseSender) IsMedia() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (sender *BaseSender) GetRawMessage() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sender *BaseSender) IsReply() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (sender *BaseSender) GetMessageID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sender *BaseSender) RecallMessage(...interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sender *BaseSender) GetUserID() string {
|
||||
return ""
|
||||
}
|
||||
func (sender *BaseSender) GetChatID() string {
|
||||
return ""
|
||||
}
|
||||
func (sender *BaseSender) Push(msg map[string]string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (sender *BaseSender) GetImType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sender *BaseSender) GroupKick(uid string, reject_add_request bool) {
|
||||
|
||||
}
|
||||
|
||||
func (sender *BaseSender) GroupUnkick(uid string) {
|
||||
|
||||
}
|
||||
|
||||
func (sender *BaseSender) GroupBan(uid string, duration int) {
|
||||
|
||||
}
|
||||
|
||||
func (sender *BaseSender) GroupUnban(uid string) {
|
||||
|
||||
}
|
||||
|
||||
func (sender *BaseSender) GetUserName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sender *BaseSender) IsAdmin() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (sender *BaseSender) GetChatName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sender *BaseSender) GetReplyUserID() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (sender *BaseSender) GetReplyMessageID() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (sender *BaseSender) AtLast() {
|
||||
sender.Atlast = true
|
||||
}
|
||||
|
||||
func (sender *BaseSender) UAtLast() {
|
||||
sender.Atlast = false
|
||||
}
|
||||
|
||||
func (sender *BaseSender) Stop() {
|
||||
panic("stop")
|
||||
}
|
||||
|
||||
func (sender *BaseSender) IsAtLast() bool {
|
||||
return sender.Atlast
|
||||
}
|
||||
|
||||
func (sender *BaseSender) MessagesToSend() string {
|
||||
return strings.Join(sender.ToSendMessages, "\n")
|
||||
}
|
||||
|
||||
var ErrorTimeOut = errors.New("指令超时")
|
||||
var ErrorInterrupt = errors.New("被其他指令中断")
|
||||
|
||||
type Carrys struct {
|
||||
list map[int64]*Carry
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (cs *Carrys) Add(key int64, c *Carry) {
|
||||
// logs.Info("add", c.Function.Rules)
|
||||
cs.Lock()
|
||||
defer cs.Unlock()
|
||||
cs.list[key] = c
|
||||
}
|
||||
|
||||
func (cs *Carrys) Remove(Key1 int64) {
|
||||
cs.Lock()
|
||||
defer cs.Unlock()
|
||||
for key := range cs.list {
|
||||
if key == Key1 {
|
||||
// logs.Info("rem", cs.list[key].Function.Rules)
|
||||
delete(cs.list, Key1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *Carrys) RemoveByUUID(uuid string) {
|
||||
cs.Foreach(func(key int64, c *Carry) bool {
|
||||
if c.UUID == uuid {
|
||||
cs.Remove(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (cs *Carrys) Foreach(f func(key int64, c *Carry) bool) {
|
||||
cs.RLock()
|
||||
defer cs.RUnlock()
|
||||
for key, c := range cs.list {
|
||||
f(key, c)
|
||||
}
|
||||
}
|
||||
|
||||
var waits = map[int]*Carrys{
|
||||
1: {
|
||||
list: map[int64]*Carry{},
|
||||
},
|
||||
2: {
|
||||
list: map[int64]*Carry{},
|
||||
},
|
||||
3: {
|
||||
list: map[int64]*Carry{},
|
||||
},
|
||||
4: {
|
||||
list: map[int64]*Carry{},
|
||||
},
|
||||
5: {
|
||||
list: map[int64]*Carry{},
|
||||
},
|
||||
}
|
||||
|
||||
type Carry struct {
|
||||
// Rules []string
|
||||
Chan chan interface{}
|
||||
Result chan interface{}
|
||||
Message common.Sender
|
||||
Function common.Function
|
||||
RequireAdmin bool
|
||||
AllowPlatforms []string
|
||||
ProhibitPlatforms []string
|
||||
AllowGroups []string
|
||||
ProhibitGroups []string
|
||||
AllowUsers []string
|
||||
ProhibitUsers []string
|
||||
ListenPrivate bool
|
||||
ListenGroup bool
|
||||
UserID string
|
||||
ChatID string
|
||||
UUID string
|
||||
}
|
||||
|
||||
type again string
|
||||
|
||||
var Again again = ""
|
||||
|
||||
var GoAgain = func(str string) again {
|
||||
return again(str)
|
||||
}
|
||||
|
||||
type YesOrNo string
|
||||
|
||||
var YesNo YesOrNo = "yeson"
|
||||
var Yes YesOrNo = "yes"
|
||||
var No YesOrNo = "no"
|
||||
|
||||
type Range []int
|
||||
|
||||
type Switch []string
|
||||
|
||||
var listenCounter int64
|
||||
|
||||
func (s *BaseSender) Await(message common.Sender, callback func(common.Sender) interface{}, params ...interface{}) interface{} {
|
||||
timeout := time.Hour * 999999
|
||||
var handleErr func(error)
|
||||
var persistent = false
|
||||
var c *Carry
|
||||
for _, param := range params {
|
||||
switch param := param.(type) {
|
||||
case string:
|
||||
if param == "persistent" {
|
||||
persistent = true
|
||||
} else {
|
||||
c.Function.Rules = append(c.Function.Rules, param)
|
||||
}
|
||||
case []string:
|
||||
c.Function.Rules = append(c.Function.Rules, param...)
|
||||
case time.Duration:
|
||||
du := param
|
||||
if du != 0 {
|
||||
timeout = du
|
||||
}
|
||||
case func(error):
|
||||
handleErr = param
|
||||
case *Carry:
|
||||
c = param
|
||||
}
|
||||
}
|
||||
c.Message = message
|
||||
if len(c.Function.Rules) == 0 {
|
||||
c.Function.Rules = []string{`raw [\s\S]+`}
|
||||
}
|
||||
fmtRule(&c.Function)
|
||||
c.Chan = make(chan interface{}, 1)
|
||||
c.Result = make(chan interface{}, 1)
|
||||
key := atomic.AddInt64(&listenCounter, 1)
|
||||
// key := fmt.Sprintf("u=%v&c=%v&i=%v&t=%v&p=%v", message.GetUserID(), message.GetChatID(), message.GetImType(), atomic.LoadInt64(&listenCounter))
|
||||
// if fg != nil {
|
||||
// if *fg == "me" {
|
||||
// key += "&f=me"
|
||||
// } else {
|
||||
// key += "&f=true"
|
||||
// }
|
||||
// }
|
||||
waits[4-s.level].Add(key, c)
|
||||
defer func() {
|
||||
waits[4-s.level].Remove(key)
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case result := <-c.Chan:
|
||||
switch s := result.(type) {
|
||||
case common.Sender:
|
||||
if callback == nil {
|
||||
return s.GetContent()
|
||||
}
|
||||
if persistent {
|
||||
go func() {
|
||||
c.Result <- callback(s)
|
||||
}()
|
||||
continue
|
||||
}
|
||||
result := callback(s)
|
||||
if v, ok := result.(again); ok { //阻塞
|
||||
if v == "" {
|
||||
c.Result <- nil
|
||||
} else {
|
||||
c.Result <- string(v)
|
||||
}
|
||||
} else if _, ok := result.(YesOrNo); ok {
|
||||
o := strings.ToLower(regexp.MustCompile("[yYnN]").FindString(s.GetContent()))
|
||||
if o == "y" {
|
||||
return Yes
|
||||
}
|
||||
if o == "n" {
|
||||
return No
|
||||
}
|
||||
c.Result <- "Y or n ?"
|
||||
} else if vv, ok := result.(Switch); ok {
|
||||
ct := s.GetContent()
|
||||
for _, v := range vv {
|
||||
if ct == v {
|
||||
return v
|
||||
}
|
||||
}
|
||||
c.Result <- fmt.Sprintf("请从%s中选择一个。", strings.Join(vv, "、"))
|
||||
} else if vv, ok := result.(Range); ok {
|
||||
ct := s.GetContent()
|
||||
n := utils.Int(ct)
|
||||
if fmt.Sprint(n) == ct {
|
||||
if (n >= vv[0]) && (n <= vv[1]) {
|
||||
|
||||
return n
|
||||
}
|
||||
}
|
||||
c.Result <- fmt.Sprintf("请从%d~%d中选择一个整数。", vv[0], vv[1])
|
||||
} else {
|
||||
c.Result <- result
|
||||
return s.GetContent()
|
||||
}
|
||||
|
||||
case error:
|
||||
if handleErr != nil {
|
||||
handleErr(s)
|
||||
}
|
||||
c.Result <- nil
|
||||
return nil
|
||||
}
|
||||
case <-time.After(timeout):
|
||||
if handleErr != nil {
|
||||
handleErr(ErrorTimeOut)
|
||||
}
|
||||
c.Result <- nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cdle/sillyplus/core/logs"
|
||||
"github.com/cdle/sillyplus/core/storage"
|
||||
"github.com/cdle/sillyplus/core/storage/boltdb"
|
||||
"github.com/cdle/sillyplus/core/storage/redis"
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
var bkt storage.Bucket
|
||||
var HttpPort string
|
||||
var sillyGirl = MakeBucket("sillyGirl")
|
||||
|
||||
var Get = func(key string) string {
|
||||
return ""
|
||||
}
|
||||
var Set = func(key, value string, expiration time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var MakeBucketlocker sync.Mutex
|
||||
|
||||
func MakeBucket(name string) storage.Bucket {
|
||||
MakeBucketlocker.Lock()
|
||||
defer MakeBucketlocker.Unlock()
|
||||
if bkt == nil {
|
||||
utils.ReadYaml(utils.ExecPath+"/conf/", &Config, "https://raw.githubusercontent.com/cdle/sillyplus/main/conf/demo_config.yaml")
|
||||
utils.SlaveMode = Config.SlaveMode
|
||||
HttpPort = Config.HttpPort
|
||||
if !Config.EnableRedis {
|
||||
bkt = boltdb.Initsillyplus()
|
||||
Get = boltdb.Get
|
||||
Set = boltdb.Set
|
||||
logs.Info("默认使用boltdb进行数据存储。")
|
||||
} else {
|
||||
bkt = redis.Initsillyplus(Config.RedisAddr, Config.RedisPassword)
|
||||
Get = redis.Get
|
||||
Set = redis.Set
|
||||
logs.Info("已使用redis进行数据存储。")
|
||||
}
|
||||
for _, name := range bkt.Buckets() {
|
||||
b := bkt.Copy(name)
|
||||
keys, err := b.Keys()
|
||||
if len(keys) == 0 && err == nil {
|
||||
b.Delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
if name == "" {
|
||||
name = "sillyGirl"
|
||||
}
|
||||
if name == "silly" || name == "app" {
|
||||
name = "sillyGirl"
|
||||
}
|
||||
return bkt.Copy(name)
|
||||
}
|
||||
|
||||
func TransformBucketKeyValue(v string) interface{} {
|
||||
var result interface{}
|
||||
if strings.HasPrefix(v, "f:") {
|
||||
result, _ = strconv.ParseFloat(strings.Replace(v, "f:", "", 1), 64)
|
||||
return result
|
||||
}
|
||||
if strings.HasPrefix(v, "d:") {
|
||||
result = utils.Int(strings.Replace(v, "d:", "", 1))
|
||||
return result
|
||||
}
|
||||
if strings.HasPrefix(v, "b:") {
|
||||
result = strings.Replace(v, "b:", "", 1) == "true"
|
||||
return result
|
||||
}
|
||||
if strings.HasPrefix(v, "o:") {
|
||||
json.Unmarshal([]byte(strings.Replace(v, "o:", "", 1)), &result)
|
||||
return result
|
||||
}
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func GetBucketKeyValue(bucket storage.Bucket, ps ...interface{}) interface{} {
|
||||
var key interface{}
|
||||
var value interface{}
|
||||
if len(ps) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(ps) > 0 {
|
||||
key = ps[0]
|
||||
}
|
||||
if len(ps) > 1 {
|
||||
value = ps[1]
|
||||
}
|
||||
v := bucket.GetString(key)
|
||||
var result = TransformBucketKeyValue(v)
|
||||
if result == nil {
|
||||
return value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func SetBucketKeyValue(bucket storage.Bucket, key interface{}, value interface{}) (string, error) {
|
||||
new := ""
|
||||
switch value := value.(type) {
|
||||
case int, int64, int32, uint:
|
||||
new = fmt.Sprintf("d:%d", value)
|
||||
case float32, float64:
|
||||
new = fmt.Sprintf("f:%f", value)
|
||||
case string, []byte:
|
||||
new = fmt.Sprintf("%s", value)
|
||||
case bool:
|
||||
new = fmt.Sprintf("b:%t", value)
|
||||
case nil:
|
||||
new = ""
|
||||
default:
|
||||
new = fmt.Sprintf("o:%s", utils.JsonMarshal(value))
|
||||
}
|
||||
return bucket.Set(key, new)
|
||||
}
|
||||
+578
@@ -0,0 +1,578 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cdle/sillyplus/core/common"
|
||||
"github.com/cdle/sillyplus/core/storage"
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
var cgs []CarryGroup
|
||||
|
||||
type CarryGroupsResult struct {
|
||||
Success bool `json:"success"`
|
||||
Data []CarryGroup `json:"data"`
|
||||
Page int `json:"page"`
|
||||
Total int `json:"total"`
|
||||
Time time.Time `json:"time"`
|
||||
}
|
||||
|
||||
var CarryGroups = MakeBucket("CarryGroups")
|
||||
|
||||
// LOGIC
|
||||
func init() {
|
||||
AddCommand([]*common.Function{
|
||||
{
|
||||
Rules: []string{`raw [\s\S]+`},
|
||||
Hidden: true,
|
||||
Handle: func(s common.Sender) interface{} {
|
||||
var bot_id = s.GetBotID()
|
||||
var platform = s.GetImType()
|
||||
var chat_id = s.GetChatID()
|
||||
var user_id = s.GetUserID()
|
||||
var content = s.GetContent()
|
||||
var from *CarryGroup //判断当前消息来自采集源
|
||||
var cgs = cgs
|
||||
var uuid = utils.GenUUID()
|
||||
for i := range cgs {
|
||||
if chat_id == cgs[i].ID && cgs[i].In {
|
||||
from = &cgs[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if nil == from { //非采集群
|
||||
s.Continue()
|
||||
return nil
|
||||
}
|
||||
bots_id := GetAdapterBotsID(platform)
|
||||
if len(from.BotsID) == 0 && len(bots_id) != 0 {
|
||||
from.BotsID = bots_id
|
||||
}
|
||||
//判断是否来自指定采集机器人
|
||||
var from_right_bot bool
|
||||
if len(from.BotsID) != 0 && from.BotsID[0] == bot_id {
|
||||
from_right_bot = true
|
||||
}
|
||||
//检测指定机器人是否离线,离线则使用其他第一个机器人,否则忽略消息
|
||||
if !from_right_bot {
|
||||
if len(from.BotsID) != 0 {
|
||||
if Contains(bots_id, bot_id) {
|
||||
console.Debug("%s 忽略机器人(%s)消息非采集指定机器人(%s)消息", uuid, bot_id, from.BotsID[0])
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if len(bots_id) != 0 && bots_id[0] != bot_id { //不是第一个机器人
|
||||
console.Debug("%s 忽略机器人(%s)消息非其他第一个机器人(%s)的消息", uuid, bot_id, bots_id[0])
|
||||
return nil
|
||||
}
|
||||
}
|
||||
console.Debug("%s 当前采集群 %s", uuid, chat_id)
|
||||
//预测采集白名单、黑名单
|
||||
if len(from.Allowed) != 0 { //白名单
|
||||
if !Contains(from.Allowed, user_id) {
|
||||
console.Debug("%s 用户(%s)不在采集群白名单 %v", uuid, chat_id)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if Contains(from.Prohibited, user_id) {
|
||||
console.Debug("%s 用户(%s)在采集群黑名单 %v", uuid, chat_id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
//预测采集包含、排除词
|
||||
if len(from.Include) != 0 { //包含
|
||||
if word := Include(content, from.Include); word == "" {
|
||||
console.Debug("%s 消息中无采集包含词", uuid)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if len(from.Exclude) != 0 { //排除
|
||||
if word := Include(content, from.Exclude); word != "" {
|
||||
console.Debug("%s 消息中有采集排除词 %s", uuid, word)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var outs []CarryGroup //预测转发群
|
||||
for i := range cgs {
|
||||
if cgs[i].Out && cgs[i].ID != chat_id {
|
||||
for j := range cgs[i].From {
|
||||
if cgs[i].From[j] == chat_id {
|
||||
if len(cgs[i].Allowed) != 0 { //白名单
|
||||
if !Contains(cgs[i].Allowed, user_id) {
|
||||
console.Debug("%s 用户(%s)不在转发群(%s)白名单 %v", uuid, user_id, cgs[i].ID)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if Contains(cgs[i].Prohibited, user_id) {
|
||||
console.Debug("%s 用户(%s)在转发群(%s)黑名单 %v", uuid, user_id, cgs[i].ID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(cgs[i].Include) != 0 { //包含
|
||||
if word := Include(content, cgs[i].Include); word == "" {
|
||||
console.Debug("%s 消息中无转发(s)包含词", uuid, cgs[i].ID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(cgs[i].Exclude) != 0 { //排除
|
||||
if word := Include(content, cgs[i].Exclude); word != "" {
|
||||
console.Debug("%s 消息中有转发(s)排除词 %s", uuid, cgs[i].ID, word)
|
||||
continue
|
||||
}
|
||||
}
|
||||
outs = append(outs, cgs[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.Debug("%s 预测转发群数目 %v", uuid, len(outs))
|
||||
if len(outs) == 0 {
|
||||
return nil
|
||||
}
|
||||
var scripts = []string{}
|
||||
//执行采集脚本
|
||||
fs := Functions
|
||||
for i := range fs {
|
||||
for j := range from.Scripts {
|
||||
if fs[i].UUID == from.Scripts[j] && !Contains(scripts, fs[i].UUID) {
|
||||
fs[i].Handle(s)
|
||||
content = s.GetContent()
|
||||
if content == "" {
|
||||
return nil
|
||||
}
|
||||
scripts = append(scripts, fs[i].UUID)
|
||||
}
|
||||
}
|
||||
}
|
||||
//执行转发脚本
|
||||
for i := range outs {
|
||||
var scripts = scripts
|
||||
var content = content
|
||||
for j := range outs[i].Scripts {
|
||||
for k := range fs {
|
||||
if fs[k].UUID == outs[i].Scripts[j] && !Contains(scripts, fs[k].UUID) { //
|
||||
fs[k].Handle(s)
|
||||
content = s.GetContent()
|
||||
fmt.Println(content)
|
||||
if content == "" {
|
||||
goto HELL
|
||||
}
|
||||
scripts = append(scripts, fs[k].UUID)
|
||||
}
|
||||
}
|
||||
}
|
||||
HELL:
|
||||
if content != "" { //选择机器人
|
||||
adapter, err := GetAdapter(append([]string{platform}, outs[i].BotsID...)...)
|
||||
if adapter == nil {
|
||||
console.Warn("%s 转发群(%s)相关机器人%v都不在线", uuid, outs[i].ID, outs[i].BotsID)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
console.Debug("%s 指定机器人都不在线,转发群(%s)已选择其他机器人(%s)推送", uuid, outs[i].ID, adapter.botid)
|
||||
}
|
||||
if adapter != nil {
|
||||
adapter.Push(map[string]string{
|
||||
CONETNT: content,
|
||||
CHAT_ID: chat_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// VAR Watch
|
||||
func init() {
|
||||
setCgs()
|
||||
storage.Watch(CarryGroups, nil, func(old, new, key string) *storage.Final {
|
||||
console.Log("已更新搬运数据。")
|
||||
ocg := CarryGroup{}
|
||||
ncg := CarryGroup{}
|
||||
json.Unmarshal([]byte(old), &ocg)
|
||||
json.Unmarshal([]byte(new), &ncg)
|
||||
tmp := cgs
|
||||
if old != "" {
|
||||
if new == "" { // 删除
|
||||
if ocg.ID != "" {
|
||||
for i, cg := range tmp {
|
||||
if cg.ID == ocg.ID {
|
||||
tmp = append(tmp[:i], tmp[i+1:]...)
|
||||
RemListenOnGroup(cg.ID, fmt.Sprintf("已为采集群(%s)关闭监听模式。", cg.ID))
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else { // 修改
|
||||
if ocg.ID != "" {
|
||||
for i, cg := range tmp {
|
||||
if cg.ID == ocg.ID {
|
||||
tmp[i] = ncg
|
||||
if ncg.In {
|
||||
if ncg.Enable {
|
||||
AddListenOnGroup(ncg.ID, fmt.Sprintf("已为采集群(%s)开启监听模式。", ncg.ID))
|
||||
} else {
|
||||
RemListenOnGroup(ncg.ID, fmt.Sprintf("已为采集群(%s)关闭监听模式。", ncg.ID))
|
||||
}
|
||||
} else {
|
||||
RemListenOnGroup(ncg.ID, fmt.Sprintf("已为采集群(%s)关闭监听模式。", ncg.ID))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else { //创建
|
||||
if ncg.ID != "" {
|
||||
tmp = append(tmp, ncg)
|
||||
if ncg.In && ncg.Enable {
|
||||
AddListenOnGroup(ncg.ID, fmt.Sprintf("已为采集群(%s)开启监听模式。", ncg.ID))
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
sort.Sort(byCreatedAt(tmp))
|
||||
for i := range tmp {
|
||||
tmp[i].Index = i + 1
|
||||
}
|
||||
cgs = tmp
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func setCgs() {
|
||||
CarryGroups.Foreach(func(b1, b2 []byte) error {
|
||||
cg := CarryGroup{}
|
||||
err := json.Unmarshal(b2, &cg)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if cg.In {
|
||||
AddListenOnGroup(cg.ID, fmt.Sprintf("已为采集群(%s)开启监听模式。", cg.ID))
|
||||
}
|
||||
cgs = append(cgs, cg)
|
||||
return nil
|
||||
})
|
||||
sort.Sort(byCreatedAt(cgs))
|
||||
for i := range cgs {
|
||||
cgs[i].Index = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
type CarryGroup struct {
|
||||
Index int `json:"id"` //编号 顺序编号
|
||||
In bool `json:"in"` //搬进来 勾选按钮
|
||||
Out bool `json:"out"` //运出去 勾选按钮
|
||||
From []string `json:"from"` //采集源
|
||||
Allowed []string `json:"allowed"` //白名单模式
|
||||
Prohibited []string `json:"prohibited"` //黑名单模式 Select选择器多选
|
||||
ID string `json:"chat_id"` //群组ID 文字表单
|
||||
ChatName string `json:"chat_name"` //群昵称 文字表单
|
||||
Remark string `json:"remark"` //备注
|
||||
Platform string `json:"platform"` //平台 Select选择器单选
|
||||
Enable bool `json:"enable"` //启用状态 开关
|
||||
Include []string `json:"include"` //包含关键词 多个关键词用逗号隔开 用户复制粘贴过去后自动转换成多彩标签
|
||||
Exclude []string `json:"exclude"` //排除关键词 包含关键词
|
||||
CreatedAt int `json:"created_at"` //创建时间戳(秒)转换成日期
|
||||
BotsID []string `json:"bots_id"` //工作机器人 多选
|
||||
Scripts []string `json:"scripts"` //处理脚本
|
||||
}
|
||||
|
||||
// CARRY API
|
||||
func init() {
|
||||
GinApi(GET, "/api/carry/groups", RequireAuth, func(ctx *gin.Context) {
|
||||
current := utils.Int(ctx.Query("current"))
|
||||
pageSize := utils.Int(ctx.Query("pageSize"))
|
||||
rr := CarryGroupsResult{
|
||||
Success: true,
|
||||
}
|
||||
cgs := cgs
|
||||
rr.Total = len(cgs)
|
||||
if current == 0 {
|
||||
current = 1
|
||||
}
|
||||
if pageSize == 0 {
|
||||
pageSize = 20
|
||||
}
|
||||
begin := (current - 1) * pageSize
|
||||
end := (current) * pageSize
|
||||
if end > rr.Total {
|
||||
end = rr.Total
|
||||
}
|
||||
if begin > end {
|
||||
begin = end
|
||||
}
|
||||
rr.Data = cgs[begin:end]
|
||||
for i := range rr.Data {
|
||||
gn := &Nickname{
|
||||
ID: rr.Data[i].ID,
|
||||
}
|
||||
nickname.First(gn)
|
||||
if gn.Value != "" {
|
||||
rr.Data[i].ChatName = gn.Value
|
||||
}
|
||||
}
|
||||
ctx.JSON(200, rr)
|
||||
})
|
||||
GinApi(GET, "/api/carry/group_names", RequireAuth, func(ctx *gin.Context) {
|
||||
cgs := cgs
|
||||
var names = map[string]string{}
|
||||
for _, cg := range cgs {
|
||||
names[cg.ID] = cg.ChatName
|
||||
}
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
"data": names,
|
||||
})
|
||||
})
|
||||
GinApi(GET, "/api/carry/group_selects", RequireAuth, func(ctx *gin.Context) {
|
||||
chat_id := ctx.Query("chat_id")
|
||||
platform := ctx.Query("platform")
|
||||
cgs := cgs
|
||||
var names = map[string]string{}
|
||||
var bots_id = []string{}
|
||||
var users = []string{}
|
||||
for _, cg := range cgs {
|
||||
names[cg.ID] = cg.ChatName
|
||||
if cg.ID == chat_id {
|
||||
users = append(users, cg.Allowed...)
|
||||
users = append(users, cg.Prohibited...)
|
||||
if platform == "" {
|
||||
platform = cg.Platform
|
||||
}
|
||||
}
|
||||
}
|
||||
bots_id = GetAdapterBotsID(platform)
|
||||
var scripts = map[string]string{}
|
||||
functions := Functions
|
||||
for _, function := range functions {
|
||||
if function.UUID != "" && len(function.Rules) == 0 && !function.OnStart && !function.Module {
|
||||
scripts[function.UUID] = function.Title + ".js"
|
||||
}
|
||||
}
|
||||
var user_names = []NicklabeL{}
|
||||
nickname.Foreach(func(b1, b2 []byte) error {
|
||||
v := &Nickname{}
|
||||
code := string(b1)
|
||||
err := json.Unmarshal(b2, v)
|
||||
if err == nil {
|
||||
platforms = append(platforms, v.Platform)
|
||||
if Contains(users, code) {
|
||||
user_names = append(user_names, NicklabeL{
|
||||
Label: fmt.Sprintf("%s(%s)", v.Value, code),
|
||||
Value: code,
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
"data": map[string]interface{}{
|
||||
"user_names": user_names,
|
||||
"group_names": names,
|
||||
"bots_id": bots_id,
|
||||
"platforms": getPltsArray(),
|
||||
"scripts": scripts,
|
||||
},
|
||||
})
|
||||
|
||||
})
|
||||
GinApi(POST, "/api/carry/group", RequireAuth, func(ctx *gin.Context) {
|
||||
// 将请求的 JSON 数据解析为一个 map[string]interface{} 类型的变量
|
||||
var updateData map[string]interface{}
|
||||
err := ctx.BindJSON(&updateData)
|
||||
if err != nil {
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": false,
|
||||
"errorMessage": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
v, ok := updateData["chat_id"]
|
||||
if !ok {
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": false,
|
||||
"errorMessage": "群号不能为空",
|
||||
})
|
||||
return
|
||||
}
|
||||
chat_id := v.(string)
|
||||
var cg = CarryGroup{
|
||||
ID: chat_id,
|
||||
}
|
||||
err = CarryGroups.First(&cg)
|
||||
if err != nil {
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": false,
|
||||
"errorMessage": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
for key, value := range updateData {
|
||||
switch key {
|
||||
case "in":
|
||||
if in, ok := value.(bool); ok {
|
||||
cg.In = in
|
||||
}
|
||||
case "out":
|
||||
if out, ok := value.(bool); ok {
|
||||
cg.Out = out
|
||||
}
|
||||
case "from":
|
||||
if from, ok := value.([]interface{}); ok {
|
||||
cg.From = toStringSlice(from)
|
||||
}
|
||||
case "allowed":
|
||||
if allowed, ok := value.([]interface{}); ok {
|
||||
cg.Allowed = toStringSlice(allowed)
|
||||
}
|
||||
case "prohibited":
|
||||
if prohibited, ok := value.([]interface{}); ok {
|
||||
cg.Prohibited = toStringSlice(prohibited)
|
||||
}
|
||||
case "chat_name":
|
||||
if chatName, ok := value.(string); ok {
|
||||
cg.ChatName = chatName
|
||||
}
|
||||
case "remark":
|
||||
if remark, ok := value.(string); ok {
|
||||
cg.Remark = remark
|
||||
}
|
||||
case "platform":
|
||||
if platform, ok := value.(string); ok {
|
||||
cg.Platform = platform
|
||||
}
|
||||
case "enable":
|
||||
if disable, ok := value.(bool); ok {
|
||||
cg.Enable = disable
|
||||
}
|
||||
case "include":
|
||||
if include, ok := value.([]interface{}); ok {
|
||||
cg.Include = toStringSlice(include)
|
||||
}
|
||||
case "exclude":
|
||||
if exclude, ok := value.([]interface{}); ok {
|
||||
cg.Exclude = toStringSlice(exclude)
|
||||
}
|
||||
case "bots_id":
|
||||
if botsID, ok := value.([]interface{}); ok {
|
||||
cg.BotsID = toStringSlice(botsID)
|
||||
}
|
||||
case "scripts":
|
||||
if scripts, ok := value.([]interface{}); ok {
|
||||
cg.Scripts = toStringSlice(scripts)
|
||||
}
|
||||
}
|
||||
}
|
||||
if cg.CreatedAt == 0 {
|
||||
cg.CreatedAt = int(time.Now().Unix())
|
||||
}
|
||||
CarryGroups.Set(chat_id, utils.JsonMarshal(cg))
|
||||
if err != nil {
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": false,
|
||||
"errorMessage": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
})
|
||||
GinApi(DELETE, "/api/carry/group", RequireAuth, func(ctx *gin.Context) {
|
||||
cg := &CarryGroup{}
|
||||
err := ctx.BindJSON(cg)
|
||||
if err != nil {
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": false,
|
||||
"errorMessage": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if cg.ID == "" {
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": false,
|
||||
"errorMessage": "群号不为空",
|
||||
})
|
||||
return
|
||||
}
|
||||
CarryGroups.Set(cg.ID, "")
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type byCreatedAt []CarryGroup
|
||||
|
||||
func (s byCreatedAt) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s byCreatedAt) Less(i, j int) bool {
|
||||
return s[i].CreatedAt > s[j].CreatedAt
|
||||
}
|
||||
|
||||
func (s byCreatedAt) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// 将 []interface{} 转为 []string 的工具函数
|
||||
func toStringSlice(intfSlice []interface{}) []string {
|
||||
stringSlice := make([]string, len(intfSlice))
|
||||
for i, intf := range intfSlice {
|
||||
if str, ok := intf.(string); ok {
|
||||
stringSlice[i] = str
|
||||
}
|
||||
}
|
||||
return stringSlice
|
||||
}
|
||||
|
||||
func Contains(strs []string, str ...string) bool {
|
||||
for _, s := range str {
|
||||
for _, str := range strs {
|
||||
if s == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Include(content string, includes []string) string {
|
||||
for _, include := range includes {
|
||||
if len(include) > 2 && include[0] == '/' && include[len(include)-1] == '/' {
|
||||
pattern := include[1 : len(include)-1]
|
||||
_, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
console.Error("包含词/排除词正则表达式 %s 错误 %s", include, err.Error())
|
||||
continue
|
||||
}
|
||||
match, err := regexp.MatchString(pattern, content)
|
||||
if err == nil && match {
|
||||
return include
|
||||
}
|
||||
} else {
|
||||
if strings.Contains(content, include) {
|
||||
return include
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cdle/sillyplus/core/common"
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
)
|
||||
|
||||
var IsCdle = false
|
||||
|
||||
func init() {
|
||||
if sillyGirl.GetString("is_cdle") == "silly8023" {
|
||||
IsCdle = true
|
||||
}
|
||||
if !IsCdle {
|
||||
return
|
||||
}
|
||||
AddCommand([]*common.Function{ //认证订阅
|
||||
{
|
||||
Admin: true,
|
||||
Rules: []string{"identify sublink [地址] [组织]"},
|
||||
Handle: func(s common.Sender) interface{} {
|
||||
address := s.Get(0)
|
||||
organization := s.Get(1)
|
||||
// machine_id := s.Get(1)
|
||||
if err := CheckPluginAddress(address); err != nil {
|
||||
return err
|
||||
}
|
||||
str, err := EncryptByAes(utils.JsonMarshal(common.PluginPublisher{
|
||||
Address: address,
|
||||
Organization: organization,
|
||||
Identified: true,
|
||||
// MachineID: machine_id,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sublink := fmt.Sprintf("link://%s", str)
|
||||
return sublink
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// 原创记录
|
||||
// var pcr = MakeBucket("pcr")
|
||||
// GinApi(POST, "/api/plugins/record", func(c *gin.Context) {
|
||||
// data, _ := ioutil.ReadAll(c.Request.Body)
|
||||
// str, _ := EncryptByAes(data)
|
||||
// v := PluginCreateRecord{}
|
||||
// if json.Unmarshal([]byte(str), &v) == nil {
|
||||
// o := v
|
||||
// pcr.First(o)
|
||||
// if o.MachineID != "" {
|
||||
// pcr.Create(v)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
// type PluginCreateRecord struct {
|
||||
// ID string `json:"id"`
|
||||
// Unix int64 `json:"unix"`
|
||||
// MachineID string `json:"machine_id"`
|
||||
// IP string `json:"ip"`
|
||||
// Title string `json:"title"`
|
||||
// }
|
||||
@@ -0,0 +1,44 @@
|
||||
package common
|
||||
|
||||
type Function struct {
|
||||
Rules []string `json:"-"`
|
||||
Params [][]string `json:"-"`
|
||||
ImType *Filter `json:"-"`
|
||||
UserId *Filter `json:"-"`
|
||||
GroupId *Filter `json:"-"`
|
||||
FindAll bool `json:"-"`
|
||||
Admin bool `json:"-"`
|
||||
Handle func(s Sender) interface{} `json:"-"`
|
||||
Cron string `json:"cron"`
|
||||
Priority int `json:"-"`
|
||||
Disable bool `json:"-"`
|
||||
Hidden bool `json:"-"`
|
||||
CronId int `json:"-"`
|
||||
Origin string `json:"-"`
|
||||
UUID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Public bool `json:"public"`
|
||||
Icon string `json:"icon"`
|
||||
Version string `json:"version"`
|
||||
Author string `json:"author"`
|
||||
Status int `json:"status"` //0未安装 1可更新 2已安装
|
||||
Address string `json:"-"`
|
||||
CreateAt string `json:"create_at"`
|
||||
Module bool `json:"module"`
|
||||
// Web bool `json:"web"`
|
||||
Encrypt bool `json:"encrypt"`
|
||||
OnStart bool `json:"on_start"`
|
||||
PluginPublisher
|
||||
Running bool `json:"-"`
|
||||
}
|
||||
type Filter struct {
|
||||
BlackMode bool
|
||||
Items []string
|
||||
}
|
||||
|
||||
type PluginPublisher struct {
|
||||
Address string `json:"address"`
|
||||
Organization string `json:"organization"`
|
||||
Identified bool `json:"identified"`
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package common
|
||||
|
||||
type Sender interface {
|
||||
GetUserID() string
|
||||
GetChatID() string
|
||||
GetBotID() string
|
||||
GetImType() string
|
||||
GetMessageID() string
|
||||
RecallMessage(...interface{}) error
|
||||
GetUserName() string
|
||||
GetChatName() string
|
||||
IsReply() bool
|
||||
GetReplyUserID() int
|
||||
GetReplyMessageID() int
|
||||
GetRawMessage() interface{}
|
||||
SetMatch([]string)
|
||||
SetParams([]string)
|
||||
SetAllMatch([][]string)
|
||||
GetMatch() []string
|
||||
GetAllMatch() [][]string
|
||||
Get(interface{}) string
|
||||
GetContent() string
|
||||
SetContent(string)
|
||||
SetFsps(fsps *FakerSenderParams)
|
||||
IsAdmin() bool
|
||||
IsMedia() bool
|
||||
Reply(...interface{}) (string, error)
|
||||
Push(map[string]string) (string, error)
|
||||
Delete() error
|
||||
Finish()
|
||||
Continue()
|
||||
IsContinue() bool
|
||||
ClearContinue()
|
||||
Await(Sender, func(Sender) interface{}, ...interface{}) interface{}
|
||||
Copy() Sender
|
||||
GroupKick(uid string, reject_add_request bool)
|
||||
GroupUnkick(uid string)
|
||||
GroupBan(uid string, duration int)
|
||||
GroupUnban(uid string)
|
||||
AtLast()
|
||||
UAtLast()
|
||||
IsAtLast() bool
|
||||
MessagesToSend() string
|
||||
Stop()
|
||||
SetMark(interface{})
|
||||
GetMark() interface{}
|
||||
SetLevel(int)
|
||||
GetLevel() int
|
||||
}
|
||||
|
||||
type FakerSenderParams struct {
|
||||
Content string
|
||||
UserID string
|
||||
ChatID string
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package core
|
||||
|
||||
var compiled_at = ""
|
||||
@@ -0,0 +1,11 @@
|
||||
package core
|
||||
|
||||
type Yaml struct {
|
||||
EnableRedis bool `yaml:"enable_redis"`
|
||||
RedisAddr string `yaml:"redis_addr"`
|
||||
RedisPassword string `yaml:"redis_password"`
|
||||
SlaveMode bool `yaml:"slave_mode"`
|
||||
HttpPort string `yaml:"http_port"`
|
||||
}
|
||||
|
||||
var Config Yaml
|
||||
@@ -0,0 +1,10 @@
|
||||
package core
|
||||
|
||||
import cron "github.com/robfig/cron/v3"
|
||||
|
||||
var C *cron.Cron
|
||||
|
||||
func init() {
|
||||
C = cron.New(cron.WithSeconds())
|
||||
C.Start()
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var PwdKey = []byte("xoxoslslgrgriiiixoxoslslgrgriiii")
|
||||
|
||||
func pkcs7Padding(data []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(data)%blockSize
|
||||
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(data, padText...)
|
||||
}
|
||||
|
||||
func pkcs7UnPadding(data []byte) ([]byte, error) {
|
||||
length := len(data)
|
||||
if length == 0 {
|
||||
return nil, errors.New("加密字符串错误!")
|
||||
}
|
||||
unPadding := int(data[length-1])
|
||||
return data[:(length - unPadding)], nil
|
||||
}
|
||||
|
||||
func AesEncrypt(data []byte, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockSize := block.BlockSize()
|
||||
encryptBytes := pkcs7Padding(data, blockSize)
|
||||
crypted := make([]byte, len(encryptBytes))
|
||||
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
|
||||
blockMode.CryptBlocks(crypted, encryptBytes)
|
||||
return crypted, nil
|
||||
}
|
||||
|
||||
func AesDecrypt(data []byte, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockSize := block.BlockSize()
|
||||
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
|
||||
crypted := make([]byte, len(data))
|
||||
blockMode.CryptBlocks(crypted, data)
|
||||
crypted, err = pkcs7UnPadding(crypted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crypted, nil
|
||||
}
|
||||
|
||||
func EncryptByAes(data []byte) (string, error) {
|
||||
res, err := AesEncrypt(data, PwdKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(res), nil
|
||||
}
|
||||
|
||||
func DecryptByAes(data string) ([]byte, error) {
|
||||
dataByte, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return AesDecrypt(dataByte, PwdKey)
|
||||
}
|
||||
|
||||
func halfEct(str string) string {
|
||||
ss := regexp.MustCompile(`/\*hidden\*/([\s\S]+?)/\*neddih\*/`).FindAllString(str, -1)
|
||||
for _, v := range ss {
|
||||
fmt.Println(v)
|
||||
// panic("")
|
||||
c_, _ := EncryptByAes([]byte(v))
|
||||
str = strings.Replace(str, v, fmt.Sprintf(`/** Here is hidden scripts %s */`, c_), 1)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func halfDeEct(str string) string {
|
||||
ss := regexp.MustCompile(`/\*\* Here is hidden scripts ([\s\S]+?) \*/`).FindAllStringSubmatch(str, -1)
|
||||
for _, v := range ss {
|
||||
f := v[0]
|
||||
c := v[1]
|
||||
c_, _ := DecryptByAes(c)
|
||||
if c_ != nil {
|
||||
str = strings.Replace(str, f, string(c_), 1)
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
@@ -0,0 +1,678 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/cdle/sillyplus/core/common"
|
||||
"github.com/cdle/sillyplus/core/logs"
|
||||
"github.com/cdle/sillyplus/core/storage"
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
// var replyRe = regexp.MustCompile(`\$\{\s*([^\s{}]+)\s*\}`)
|
||||
var total uint64 = 0
|
||||
var finished uint64 = 0
|
||||
var contents sync.Map
|
||||
|
||||
var Functions = []*common.Function{}
|
||||
|
||||
var Messages chan common.Sender
|
||||
|
||||
var ListenOnGroups sync.Map
|
||||
var NoListenUsers sync.Map
|
||||
var NoReplyGroups sync.Map
|
||||
var StaticListenOnGroups sync.Map
|
||||
var StaticNoReplyGroups sync.Map
|
||||
var noListenUsers = MakeBucket("noListenUsers")
|
||||
var listenOnGroups = MakeBucket("listenOnGroups")
|
||||
var noReplyGroups = MakeBucket("noReplyGroups")
|
||||
|
||||
type GroupInfo struct {
|
||||
Platform string `json:"platform"`
|
||||
Desc string `json:"desc"`
|
||||
Enable bool `json:"enable"`
|
||||
}
|
||||
|
||||
var AddNoReplyGroups = func(code string, desc string) {
|
||||
_, loaded := NoReplyGroups.LoadOrStore(code, true)
|
||||
if !loaded {
|
||||
logs.Info(desc)
|
||||
}
|
||||
}
|
||||
|
||||
var AddListenOnGroup = func(code string, desc string) {
|
||||
_, loaded := ListenOnGroups.LoadOrStore(code, true)
|
||||
if !loaded {
|
||||
logs.Info(desc)
|
||||
}
|
||||
}
|
||||
|
||||
var RemNoReplyGroups = func(code string, desc string) {
|
||||
_, loaded := NoReplyGroups.Load(code)
|
||||
if loaded {
|
||||
NoReplyGroups.Delete(code)
|
||||
logs.Info(desc)
|
||||
}
|
||||
}
|
||||
|
||||
var RemListenOnGroup = func(code string, desc string) {
|
||||
_, loaded := ListenOnGroups.Load(code)
|
||||
if loaded {
|
||||
ListenOnGroups.Delete(code)
|
||||
logs.Info(desc)
|
||||
}
|
||||
}
|
||||
|
||||
var IsNoReplyGroup = func(s common.Sender) bool {
|
||||
cid := s.GetChatID()
|
||||
if utils.IsZeroOrEmpty(cid) {
|
||||
return false
|
||||
}
|
||||
_, ok1 := NoReplyGroups.Load(cid)
|
||||
_, ok2 := StaticNoReplyGroups.Load(cid)
|
||||
res := ok1 || ok2
|
||||
if res {
|
||||
logs.Info("禁言的群组 %v/%v@%v", s.GetImType(), s.GetUserID(), cid)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func initListenReply() {
|
||||
listenOnGroups.Foreach(func(b1, data []byte) error {
|
||||
groupCode := string(b1)
|
||||
info := &GroupInfo{}
|
||||
err := json.Unmarshal(data, info)
|
||||
if err != nil {
|
||||
listenOnGroups.Set(groupCode, "")
|
||||
} else {
|
||||
if info.Enable {
|
||||
StaticListenOnGroups.Store(string(b1), info.Platform)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
storage.Watch(listenOnGroups, nil, func(old, new, key string) (fin *storage.Final) {
|
||||
if new == "" {
|
||||
logs.Info("已删除监听群组%s", key)
|
||||
StaticListenOnGroups.Delete(key)
|
||||
return
|
||||
}
|
||||
info := &GroupInfo{}
|
||||
json.Unmarshal([]byte(new), info)
|
||||
if info.Enable {
|
||||
StaticListenOnGroups.Store(key, info.Platform)
|
||||
logs.Info("已设置监听群组%s/%s", info.Platform, key)
|
||||
} else {
|
||||
StaticListenOnGroups.Delete(key)
|
||||
logs.Info("已取消监听群组%s/%s", info.Platform, key)
|
||||
}
|
||||
return
|
||||
})
|
||||
noReplyGroups.Foreach(func(b1, data []byte) error {
|
||||
groupCode := string(b1)
|
||||
info := &GroupInfo{}
|
||||
err := json.Unmarshal(data, info)
|
||||
if err != nil {
|
||||
noReplyGroups.Set(groupCode, "")
|
||||
} else {
|
||||
info := &GroupInfo{}
|
||||
json.Unmarshal(data, info)
|
||||
if info.Enable {
|
||||
StaticNoReplyGroups.Store(string(b1), info.Platform)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
storage.Watch(noReplyGroups, nil, func(old, new, key string) (fin *storage.Final) {
|
||||
if new == "" {
|
||||
logs.Info("已删除禁言群组%s", key)
|
||||
StaticNoReplyGroups.Delete(key)
|
||||
return
|
||||
}
|
||||
info := &GroupInfo{}
|
||||
json.Unmarshal([]byte(new), info)
|
||||
if info.Enable {
|
||||
logs.Info("已设置禁言群组%s/%s", info.Platform, key)
|
||||
StaticNoReplyGroups.Store(key, info.Platform)
|
||||
} else {
|
||||
logs.Info("已取消禁言群组%s%s", info.Platform, key)
|
||||
StaticNoReplyGroups.Delete(key)
|
||||
}
|
||||
return
|
||||
})
|
||||
noListenUsers.Foreach(func(b1, data []byte) error {
|
||||
groupCode := string(b1)
|
||||
info := &GroupInfo{}
|
||||
err := json.Unmarshal(data, info)
|
||||
if err != nil {
|
||||
noListenUsers.Set(groupCode, "")
|
||||
} else {
|
||||
info := &GroupInfo{}
|
||||
json.Unmarshal(data, info)
|
||||
fmt.Println(string(b1), string(utils.JsonMarshal(info)))
|
||||
if info.Enable {
|
||||
NoListenUsers.Store(string(b1), info.Platform)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
storage.Watch(noListenUsers, nil, func(old, new, key string) (fin *storage.Final) {
|
||||
if new == "" {
|
||||
logs.Info("已取消屏蔽用户%s", key)
|
||||
NoListenUsers.Delete(key)
|
||||
return
|
||||
}
|
||||
info := &GroupInfo{}
|
||||
json.Unmarshal([]byte(new), info)
|
||||
if info.Enable {
|
||||
logs.Info("已屏蔽用户%s/%s", info.Platform, key)
|
||||
NoListenUsers.Store(key, info.Platform)
|
||||
} else {
|
||||
logs.Info("已取消屏蔽用户%s%s", info.Platform, key)
|
||||
NoListenUsers.Delete(key)
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func initToHandleMessage() {
|
||||
Messages = make(chan common.Sender)
|
||||
go func() {
|
||||
for {
|
||||
s := <-Messages
|
||||
ignore := false
|
||||
cid := s.GetChatID()
|
||||
uid := s.GetUserID()
|
||||
imType := s.GetImType()
|
||||
isAdmin := s.IsAdmin()
|
||||
uname := s.GetUserName()
|
||||
ctt := s.GetContent()
|
||||
if !utils.IsZeroOrEmpty(cid) {
|
||||
cname := s.GetChatName()
|
||||
if cname != "" {
|
||||
CreateNickName(&Nickname{
|
||||
ID: cid,
|
||||
Group: true,
|
||||
Value: cname,
|
||||
Platform: imType,
|
||||
BotsID: []string{s.GetBotID()},
|
||||
})
|
||||
}
|
||||
if isAdmin {
|
||||
switch ctt {
|
||||
case "listen":
|
||||
if data := listenOnGroups.GetBytes(cid); len(data) == 0 {
|
||||
listenOnGroups.Set(cid, utils.JsonMarshal(&GroupInfo{
|
||||
Platform: imType,
|
||||
Enable: true,
|
||||
Desc: s.GetChatName(),
|
||||
}))
|
||||
} else {
|
||||
info := &GroupInfo{}
|
||||
json.Unmarshal(data, info)
|
||||
if !info.Enable {
|
||||
info.Enable = !info.Enable
|
||||
listenOnGroups.Set(cid, utils.JsonMarshal(info))
|
||||
}
|
||||
}
|
||||
s.Reply("ok")
|
||||
case "unlisten", "nolisten":
|
||||
if data := listenOnGroups.GetBytes(cid); len(data) != 0 {
|
||||
info := &GroupInfo{}
|
||||
json.Unmarshal(data, info)
|
||||
if info.Enable {
|
||||
info.Enable = !info.Enable
|
||||
listenOnGroups.Set(cid, utils.JsonMarshal(info))
|
||||
}
|
||||
}
|
||||
s.Reply("ok")
|
||||
case "reply":
|
||||
if data := noReplyGroups.GetBytes(cid); len(data) != 0 {
|
||||
info := &GroupInfo{}
|
||||
if info.Enable {
|
||||
info.Enable = !info.Enable
|
||||
noReplyGroups.Set(cid, utils.JsonMarshal(info))
|
||||
}
|
||||
}
|
||||
s.Reply("ok")
|
||||
case "noreply", "unreply":
|
||||
if data := noReplyGroups.GetBytes(cid); len(data) == 0 {
|
||||
noReplyGroups.Set(cid, utils.JsonMarshal(&GroupInfo{
|
||||
Platform: imType,
|
||||
Enable: true,
|
||||
Desc: s.GetChatName(),
|
||||
}))
|
||||
} else {
|
||||
info := &GroupInfo{}
|
||||
json.Unmarshal(data, info)
|
||||
if !info.Enable {
|
||||
info.Enable = !info.Enable
|
||||
noReplyGroups.Set(cid, utils.JsonMarshal(info))
|
||||
}
|
||||
}
|
||||
s.Reply("ok")
|
||||
}
|
||||
}
|
||||
_, ok1 := ListenOnGroups.Load(cid)
|
||||
if !ok1 {
|
||||
_, ok2 := StaticListenOnGroups.Load(cid)
|
||||
if !ok2 {
|
||||
ignore = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if isAdmin {
|
||||
switch ctt {
|
||||
case "unlisten", "nolisten":
|
||||
if data := noListenUsers.GetBytes(uid); len(data) == 0 {
|
||||
noListenUsers.Set(uid, utils.JsonMarshal(&GroupInfo{
|
||||
Platform: imType,
|
||||
Enable: true,
|
||||
Desc: s.GetChatName(),
|
||||
}))
|
||||
} else {
|
||||
info := &GroupInfo{}
|
||||
json.Unmarshal(data, info)
|
||||
if !info.Enable {
|
||||
info.Enable = !info.Enable
|
||||
noListenUsers.Set(uid, utils.JsonMarshal(info))
|
||||
}
|
||||
}
|
||||
s.Reply("ok")
|
||||
case "listen":
|
||||
if data := noListenUsers.GetBytes(uid); len(data) != 0 {
|
||||
info := &GroupInfo{}
|
||||
json.Unmarshal(data, info)
|
||||
if info.Enable {
|
||||
info.Enable = !info.Enable
|
||||
noListenUsers.Set(uid, utils.JsonMarshal(info))
|
||||
}
|
||||
}
|
||||
s.Reply("ok")
|
||||
}
|
||||
}
|
||||
}
|
||||
_, ok2 := NoListenUsers.Load(uid)
|
||||
if ok2 {
|
||||
ignore = true
|
||||
}
|
||||
|
||||
if uname != "" {
|
||||
CreateNickName(&Nickname{
|
||||
ID: uid,
|
||||
Group: false,
|
||||
Value: uname,
|
||||
Platform: imType,
|
||||
BotsID: []string{s.GetBotID()},
|
||||
})
|
||||
}
|
||||
if imType != "terminal" {
|
||||
if !ignore {
|
||||
logs.Info("接收到消息 %v/%v@%v:%s", imType, uid, cid, ctt)
|
||||
} else {
|
||||
logs.Info("屏蔽的消息 %v/%v@%v:%s", imType, uid, cid, ctt)
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
go HandleMessage(s)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func fmtRule(cmd *common.Function) {
|
||||
for i := range cmd.Rules {
|
||||
cmd.Rules[i] = strings.Trim(cmd.Rules[i], "")
|
||||
cmd.Params = append(cmd.Params, []string{})
|
||||
if strings.HasPrefix(cmd.Rules[i], "raw") {
|
||||
cmd.Rules[i] = strings.Replace(cmd.Rules[i], "raw ", "", -1)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(cmd.Rules[i], "^") {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(cmd.Rules[i], "$") {
|
||||
continue
|
||||
}
|
||||
cmd.Rules[i] = strings.ReplaceAll(cmd.Rules[i], `\r\a\w`, "raw")
|
||||
cmd.Rules[i] = strings.Replace(cmd.Rules[i], "(", `[(]`, -1)
|
||||
cmd.Rules[i] = strings.Replace(cmd.Rules[i], ")", `[)]`, -1)
|
||||
ress := regexp.MustCompile(`\[([^\s\[\]]+)\]`).FindAllStringSubmatch(cmd.Rules[i], -1)
|
||||
for _, res := range ress {
|
||||
var inner = res[1]
|
||||
vv := strings.SplitN(inner, ":", 2)
|
||||
name := vv[0]
|
||||
if len(vv) == 1 {
|
||||
cmd.Rules[i] = strings.ReplaceAll(cmd.Rules[i], res[0], "?")
|
||||
} else {
|
||||
cmd.Rules[i] = strings.ReplaceAll(cmd.Rules[i], res[0], fmt.Sprintf("(%s)", strings.ReplaceAll(vv[1], ",", "|")))
|
||||
}
|
||||
cmd.Params[i] = append(cmd.Params[i], name)
|
||||
}
|
||||
cmd.Rules[i] = regexp.MustCompile(`\?$`).ReplaceAllString(cmd.Rules[i], `([\s\S]+)`)
|
||||
cmd.Rules[i] = strings.Replace(cmd.Rules[i], " ", `\s+`, -1)
|
||||
cmd.Rules[i] = strings.Replace(cmd.Rules[i], "?", `(\S+)`, -1)
|
||||
cmd.Rules[i] = "^" + cmd.Rules[i] + "$"
|
||||
}
|
||||
}
|
||||
|
||||
func AddCommand(cmds []*common.Function) {
|
||||
for j := range cmds {
|
||||
if cmds[j].OnStart && !cmds[j].Disable {
|
||||
go func(f *common.Function) {
|
||||
time.Sleep(time.Second)
|
||||
console.Log("初始化%v服务", f.Title)
|
||||
f.Handle(&Faker{
|
||||
Type: "*",
|
||||
})
|
||||
}(cmds[j])
|
||||
}
|
||||
fmtRule(cmds[j])
|
||||
{
|
||||
if cmds[j].Cron != "" && !cmds[j].Disable && !cmds[j].Module && !cmds[j].OnStart {
|
||||
cron := strings.TrimSpace(cmds[j].Cron)
|
||||
if len(regexp.MustCompile(`\S+`).FindAllString(cron, -1)) == 5 {
|
||||
cmds[j].Cron = "0 " + cron
|
||||
}
|
||||
cronId, err := C.AddFunc(cmds[j].Cron, func() {
|
||||
cmds[j].Handle(&Faker{
|
||||
Admin: true,
|
||||
Type: "cron",
|
||||
})
|
||||
})
|
||||
if err == nil {
|
||||
cmds[j].CronId = int(cronId)
|
||||
// console["log"]("脚本%s添加定时器。", cmds[j].Title)
|
||||
} else {
|
||||
console.Error("脚本%s定时器错误,%v", cmds[j].Title, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
lf := len(Functions)
|
||||
for i := range Functions {
|
||||
f := lf - i - 1
|
||||
if Functions[f].Priority > cmds[j].Priority {
|
||||
Functions = append(Functions[:f+1], append([]*common.Function{cmds[j]}, Functions[f+1:]...)...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(Functions) == lf {
|
||||
if lf > 0 {
|
||||
apd := false
|
||||
for i := range Functions {
|
||||
if cmds[j].Priority >= Functions[i].Priority {
|
||||
apd = true
|
||||
Functions = append(Functions[:i], append([]*common.Function{cmds[j]}, Functions[i:]...)...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !apd {
|
||||
Functions = append(Functions, cmds[j])
|
||||
}
|
||||
} else {
|
||||
Functions = append(Functions, cmds[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HandleMessage(sender common.Sender) {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
console.Error("HandleMessage error: %v", err)
|
||||
}
|
||||
}()
|
||||
num := atomic.AddUint64(&total, 1)
|
||||
defer atomic.AddUint64(&finished, 1)
|
||||
ct := sender.GetContent()
|
||||
contents.Store(num, ct)
|
||||
defer func() {
|
||||
contents.Delete(num)
|
||||
}()
|
||||
content := utils.TrimHiddenCharacter(ct)
|
||||
defer func() {
|
||||
sender.Finish()
|
||||
if sender.IsAtLast() {
|
||||
s := sender.MessagesToSend()
|
||||
if s != "" {
|
||||
sender.Reply(s)
|
||||
}
|
||||
}
|
||||
}()
|
||||
u, g, i, a := sender.GetUserID(), sender.GetChatID(), sender.GetImType(), sender.IsAdmin()
|
||||
con := true
|
||||
mtd := false
|
||||
|
||||
for _, wait := range waits {
|
||||
wait.Foreach(func(k int64, c *Carry) bool {
|
||||
// userID := vs.Get("u")
|
||||
// chatID := vs.Get("c")
|
||||
// imType := vs.Get("i")
|
||||
// forGroup := vs.Get("f")
|
||||
// if chatID != g && (forGroup != "me" || g != "0") {
|
||||
// return true
|
||||
// }
|
||||
// if userID != u && (forGroup == "" || forGroup == "me") {
|
||||
// return true
|
||||
// }
|
||||
if c.RequireAdmin && !a {
|
||||
return true
|
||||
}
|
||||
if len(c.AllowPlatforms) != 0 && !Contains(c.AllowPlatforms, i) {
|
||||
return true
|
||||
}
|
||||
if len(c.ProhibitPlatforms) != 0 && Contains(c.ProhibitPlatforms, i) {
|
||||
return true
|
||||
}
|
||||
if len(c.AllowUsers) != 0 && !Contains(c.AllowUsers, u) {
|
||||
return true
|
||||
}
|
||||
if len(c.ProhibitUsers) != 0 && Contains(c.ProhibitUsers, u) {
|
||||
return true
|
||||
}
|
||||
if len(c.AllowGroups) != 0 && !Contains(c.AllowGroups, g) {
|
||||
return true
|
||||
}
|
||||
if len(c.ProhibitGroups) != 0 && Contains(c.ProhibitGroups, g) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if c.ChatID != g && (!c.AllowPrivate || g != "") {
|
||||
// return true
|
||||
// }
|
||||
// if c.UserID != u && (c.AllowGroupUsers || c.AllowPrivate) {
|
||||
// return true
|
||||
// }
|
||||
|
||||
if c.ChatID != "" { //群聊监听
|
||||
if g == "" { //私聊时
|
||||
if !c.ListenPrivate { //如果未设置允许私聊则拒绝
|
||||
return true
|
||||
}
|
||||
} else { //群聊时
|
||||
if u != c.UserID { //群员发言
|
||||
if !c.ListenGroup { //未设置允许群员加入拒绝
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range c.Function.Rules {
|
||||
reg, err := regexp.Compile(c.Function.Rules[i])
|
||||
if err != nil {
|
||||
console.Error("监听器正则错误,%v", err)
|
||||
continue
|
||||
}
|
||||
// logs.Info("%s规则:%s", c.Function.Title, c.Function.Rules[i])
|
||||
if res := reg.FindStringSubmatch(content); len(res) > 0 {
|
||||
logs.Info("匹配到%s规则:%s", c.Function.Title, c.Function.Rules[i])
|
||||
sender.SetMatch(res[1:])
|
||||
sender.SetParams(c.Function.Params[i])
|
||||
mtd = true
|
||||
if f, ok := c.Message.(*Faker); ok && f.Carry != nil {
|
||||
if s1, o := sender.(*Faker); o && s1.Carry != nil {
|
||||
f.Carry = s1.Carry
|
||||
c := make(chan string)
|
||||
oc := s1.Carry
|
||||
s1.Carry = c
|
||||
go func() {
|
||||
for {
|
||||
r, o := <-c
|
||||
if !o {
|
||||
break
|
||||
}
|
||||
oc <- r
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
c.Chan <- sender
|
||||
sender.Reply(<-c.Result)
|
||||
if !sender.IsContinue() {
|
||||
con = false
|
||||
return false
|
||||
}
|
||||
content = utils.TrimHiddenCharacter(sender.GetContent())
|
||||
break
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
if mtd && !con {
|
||||
return
|
||||
}
|
||||
|
||||
for _, reply := range replies {
|
||||
if reply.Keyword == "" || reply.Value == "" {
|
||||
continue
|
||||
}
|
||||
if reply.Number != "" && reply.Number != u && reply.Number != g {
|
||||
continue
|
||||
}
|
||||
if len(reply.Platforms) != 0 && !Contains(reply.Platforms, i) {
|
||||
continue
|
||||
}
|
||||
// if reply.Class == 1 && g != "" {
|
||||
// continue
|
||||
// } else if reply.Class == 2 && g == "" {
|
||||
// continue
|
||||
// }
|
||||
reg, err := regexp.Compile(reply.Keyword)
|
||||
if err == nil {
|
||||
if reg.FindString(content) != "" {
|
||||
//todo 支持JS语法
|
||||
output := parseReply(reply.Value)
|
||||
sender.Reply(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, function := range Functions {
|
||||
if function.Disable || function.Module || function.OnStart {
|
||||
continue
|
||||
}
|
||||
imType := sender.GetImType()
|
||||
if (imType != "cron" && imType != "carry" && black(function.ImType, imType)) || black(function.UserId, sender.GetUserID()) || black(function.GroupId, fmt.Sprint(sender.GetChatID())) {
|
||||
continue
|
||||
}
|
||||
for i := range function.Rules {
|
||||
var matched bool
|
||||
if function.FindAll {
|
||||
reg, err := regexp.Compile(function.Rules[i])
|
||||
if err != nil {
|
||||
console.Error("脚本%s正则错误,%v", function.Title, err)
|
||||
continue
|
||||
}
|
||||
if res := reg.FindAllStringSubmatch(content, -1); len(res) > 0 {
|
||||
tmp := [][]string{}
|
||||
for i := range res {
|
||||
tmp = append(tmp, res[i][1:])
|
||||
}
|
||||
if !function.Hidden {
|
||||
logs.Info("匹配到规则:%s", function.Rules[i])
|
||||
}
|
||||
sender.SetAllMatch(tmp)
|
||||
matched = true
|
||||
}
|
||||
} else {
|
||||
reg, err := regexp.Compile(function.Rules[i])
|
||||
if err != nil {
|
||||
console.Error("脚本%s正则错误,%v", function.Title, err)
|
||||
continue
|
||||
}
|
||||
if res := reg.FindStringSubmatch(content); len(res) > 0 {
|
||||
if !function.Hidden {
|
||||
logs.Info("匹配到规则:%s", function.Rules[i])
|
||||
}
|
||||
sender.SetMatch(res[1:])
|
||||
sender.SetParams(function.Params[i])
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
if function.Admin && !a {
|
||||
return
|
||||
}
|
||||
rt := function.Handle(sender)
|
||||
if rt != nil {
|
||||
sender.Reply(rt)
|
||||
}
|
||||
if sender.IsContinue() {
|
||||
sender.ClearContinue()
|
||||
content = utils.TrimHiddenCharacter(sender.GetContent())
|
||||
if !function.Hidden {
|
||||
logs.Info("继续去处理:%s", content)
|
||||
}
|
||||
goto next
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
next:
|
||||
}
|
||||
|
||||
recall := sillyGirl.GetString("recall")
|
||||
if recall != "" {
|
||||
recalled := false
|
||||
for _, v := range strings.Split(recall, "&") {
|
||||
reg, err := regexp.Compile(v)
|
||||
if err == nil {
|
||||
if reg.FindString(content) != "" {
|
||||
if !a && sender.GetImType() != "wx" {
|
||||
sender.Delete()
|
||||
recalled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if recalled {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
func black(filter *common.Filter, str string) bool {
|
||||
if filter != nil {
|
||||
if filter.BlackMode {
|
||||
if utils.Contains(filter.Items, str) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if !utils.Contains(filter.Items, str) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/adapter/httplib"
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
)
|
||||
|
||||
var DataHome = utils.GetDataHome()
|
||||
|
||||
func Init() {
|
||||
sillyGirl = MakeBucket("sillyGirl")
|
||||
_, err := os.Stat(DataHome)
|
||||
if err != nil {
|
||||
os.MkdirAll(DataHome, os.ModePerm)
|
||||
}
|
||||
utils.ReadYaml(utils.ExecPath+"/conf/", &Config, "https://raw.githubusercontent.com/cdle/sillyplus/main/conf/demo_config.yaml")
|
||||
initToHandleMessage()
|
||||
|
||||
sillyGirl.Set("started_at", time.Now().Format("2006-01-02 15:04:05"))
|
||||
api_key := sillyGirl.GetString("api_key")
|
||||
if api_key == "" {
|
||||
api_key := time.Now().UnixNano()
|
||||
sillyGirl.Set("api_key", api_key)
|
||||
}
|
||||
// if sillyplus.GetString("uuid") == "" {
|
||||
sillyGirl.Set("uuid", utils.GenUUID())
|
||||
// }
|
||||
httplib.SetDefaultSetting(httplib.BeegoHTTPSettings{
|
||||
ConnectTimeout: time.Second * 10,
|
||||
ReadWriteTimeout: time.Second * 10,
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36",
|
||||
})
|
||||
initPlugins()
|
||||
initReboot()
|
||||
initListenReply()
|
||||
// initPluginFile()
|
||||
initWebPluginList()
|
||||
go initPluginList()
|
||||
initPluginPublish()
|
||||
if compiled_at != "" {
|
||||
console.Log("编译时间戳,", compiled_at)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
## logs
|
||||
|
||||
logs is a Go logs manager. It can use many logs adapters. The repo is inspired by `database/sql` .
|
||||
|
||||
## How to install?
|
||||
|
||||
go get github.com/cdle/sillyplus/core/logs
|
||||
|
||||
## What adapters are supported?
|
||||
|
||||
As of now this logs support console, file,smtp and conn.
|
||||
|
||||
## How to use it?
|
||||
|
||||
First you must import it
|
||||
|
||||
```golang
|
||||
import (
|
||||
"github.com/cdle/sillyplus/core/logs"
|
||||
)
|
||||
```
|
||||
|
||||
Then init a Log (example with console adapter)
|
||||
|
||||
```golang
|
||||
log := logs.NewLogger(10000)
|
||||
log.SetLogger("console", "")
|
||||
```
|
||||
|
||||
> the first params stand for how many channel
|
||||
|
||||
Use it like this:
|
||||
|
||||
```golang
|
||||
log.Trace("trace")
|
||||
log.Info("info")
|
||||
log.Warn("warning")
|
||||
log.Debug("debug")
|
||||
log.Critical("critical")
|
||||
```
|
||||
|
||||
## File adapter
|
||||
|
||||
Configure file adapter like this:
|
||||
|
||||
```golang
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("file", `{"filename":"test.log"}`)
|
||||
```
|
||||
|
||||
## Conn adapter
|
||||
|
||||
Configure like this:
|
||||
|
||||
```golang
|
||||
log := NewLogger(1000)
|
||||
log.SetLogger("conn", `{"net":"tcp","addr":":7020"}`)
|
||||
log.Info("info")
|
||||
```
|
||||
|
||||
## Smtp adapter
|
||||
|
||||
Configure like this:
|
||||
|
||||
```golang
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("smtp", `{"username":"beegotest@gmail.com","password":"xxxxxxxx","host":"smtp.gmail.com:587","sendTos":["xiemengjun@gmail.com"]}`)
|
||||
log.Critical("sendmail critical")
|
||||
time.Sleep(time.Second * 30)
|
||||
```
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2014 beego Author. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
apacheFormatPattern = "%s - - [%s] \"%s %d %d\" %f %s %s"
|
||||
apacheFormat = "APACHE_FORMAT"
|
||||
jsonFormat = "JSON_FORMAT"
|
||||
)
|
||||
|
||||
// AccessLogRecord is astruct for holding access log data.
|
||||
type AccessLogRecord struct {
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
RequestTime time.Time `json:"request_time"`
|
||||
RequestMethod string `json:"request_method"`
|
||||
Request string `json:"request"`
|
||||
ServerProtocol string `json:"server_protocol"`
|
||||
Host string `json:"host"`
|
||||
Status int `json:"status"`
|
||||
BodyBytesSent int64 `json:"body_bytes_sent"`
|
||||
ElapsedTime time.Duration `json:"elapsed_time"`
|
||||
HTTPReferrer string `json:"http_referrer"`
|
||||
HTTPUserAgent string `json:"http_user_agent"`
|
||||
RemoteUser string `json:"remote_user"`
|
||||
}
|
||||
|
||||
func (r *AccessLogRecord) json() ([]byte, error) {
|
||||
buffer := &bytes.Buffer{}
|
||||
encoder := json.NewEncoder(buffer)
|
||||
disableEscapeHTML(encoder)
|
||||
|
||||
err := encoder.Encode(r)
|
||||
return buffer.Bytes(), err
|
||||
}
|
||||
|
||||
func disableEscapeHTML(i interface{}) {
|
||||
if e, ok := i.(interface {
|
||||
SetEscapeHTML(bool)
|
||||
}); ok {
|
||||
e.SetEscapeHTML(false)
|
||||
}
|
||||
}
|
||||
|
||||
// AccessLog - Format and print access log.
|
||||
func AccessLog(r *AccessLogRecord, format string) {
|
||||
msg := r.format(format)
|
||||
lm := &LogMsg{
|
||||
Msg: strings.TrimSpace(msg),
|
||||
When: time.Now(),
|
||||
Level: levelLoggerImpl,
|
||||
}
|
||||
beeLogger.writeMsg(lm)
|
||||
}
|
||||
|
||||
func (r *AccessLogRecord) format(format string) string {
|
||||
msg := ""
|
||||
switch format {
|
||||
case apacheFormat:
|
||||
timeFormatted := r.RequestTime.Format("02/Jan/2006 03:04:05")
|
||||
msg = fmt.Sprintf(apacheFormatPattern, r.RemoteAddr, timeFormatted, r.Request, r.Status, r.BodyBytesSent,
|
||||
r.ElapsedTime.Seconds(), r.HTTPReferrer, r.HTTPUserAgent)
|
||||
case jsonFormat:
|
||||
fallthrough
|
||||
default:
|
||||
jsonData, err := r.json()
|
||||
if err != nil {
|
||||
msg = fmt.Sprintf(`{"Error": "%s"}`, err)
|
||||
} else {
|
||||
msg = string(jsonData)
|
||||
}
|
||||
}
|
||||
return msg
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2020
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAccessLog_format(t *testing.T) {
|
||||
alc := &AccessLogRecord{
|
||||
RequestTime: time.Date(2020, 9, 19, 21, 21, 21, 11, time.UTC),
|
||||
}
|
||||
|
||||
res := alc.format(apacheFormat)
|
||||
println(res)
|
||||
assert.Equal(t, " - - [19/Sep/2020 09:21:21] \" 0 0\" 0.000000 ", res)
|
||||
|
||||
res = alc.format(jsonFormat)
|
||||
assert.Equal(t,
|
||||
"{\"remote_addr\":\"\",\"request_time\":\"2020-09-19T21:21:21.000000011Z\",\"request_method\":\"\",\"request\":\"\",\"server_protocol\":\"\",\"host\":\"\",\"status\":0,\"body_bytes_sent\":0,\"elapsed_time\":0,\"http_referrer\":\"\",\"http_user_agent\":\"\",\"remote_user\":\"\"}\n", res)
|
||||
|
||||
AccessLog(alc, jsonFormat)
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package alils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cdle/sillyplus/core/logs"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// CacheSize sets the flush size
|
||||
CacheSize int = 64
|
||||
// Delimiter defines the topic delimiter
|
||||
Delimiter string = "##"
|
||||
)
|
||||
|
||||
// Config is the Config for Ali Log
|
||||
type Config struct {
|
||||
Project string `json:"project"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
KeyID string `json:"key_id"`
|
||||
KeySecret string `json:"key_secret"`
|
||||
LogStore string `json:"log_store"`
|
||||
Topics []string `json:"topics"`
|
||||
Source string `json:"source"`
|
||||
Level int `json:"level"`
|
||||
FlushWhen int `json:"flush_when"`
|
||||
Formatter string `json:"formatter"`
|
||||
}
|
||||
|
||||
// aliLSWriter implements LoggerInterface.
|
||||
// Writes messages in keep-live tcp connection.
|
||||
type aliLSWriter struct {
|
||||
store *LogStore
|
||||
group []*LogGroup
|
||||
withMap bool
|
||||
groupMap map[string]*LogGroup
|
||||
lock *sync.Mutex
|
||||
Config
|
||||
formatter logs.LogFormatter
|
||||
}
|
||||
|
||||
// NewAliLS creates a new Logger
|
||||
func NewAliLS() logs.Logger {
|
||||
alils := new(aliLSWriter)
|
||||
alils.Level = logs.LevelTrace
|
||||
alils.formatter = alils
|
||||
return alils
|
||||
}
|
||||
|
||||
// Init parses config and initializes struct
|
||||
func (c *aliLSWriter) Init(config string) error {
|
||||
err := json.Unmarshal([]byte(config), c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.FlushWhen > CacheSize {
|
||||
c.FlushWhen = CacheSize
|
||||
}
|
||||
|
||||
prj := &LogProject{
|
||||
Name: c.Project,
|
||||
Endpoint: c.Endpoint,
|
||||
AccessKeyID: c.KeyID,
|
||||
AccessKeySecret: c.KeySecret,
|
||||
}
|
||||
|
||||
store, err := prj.GetLogStore(c.LogStore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.store = store
|
||||
|
||||
// Create default Log Group
|
||||
c.group = append(c.group, &LogGroup{
|
||||
Topic: proto.String(""),
|
||||
Source: proto.String(c.Source),
|
||||
Logs: make([]*Log, 0, c.FlushWhen),
|
||||
})
|
||||
|
||||
// Create other Log Group
|
||||
c.groupMap = make(map[string]*LogGroup)
|
||||
for _, topic := range c.Topics {
|
||||
|
||||
lg := &LogGroup{
|
||||
Topic: proto.String(topic),
|
||||
Source: proto.String(c.Source),
|
||||
Logs: make([]*Log, 0, c.FlushWhen),
|
||||
}
|
||||
|
||||
c.group = append(c.group, lg)
|
||||
c.groupMap[topic] = lg
|
||||
}
|
||||
|
||||
if len(c.group) == 1 {
|
||||
c.withMap = false
|
||||
} else {
|
||||
c.withMap = true
|
||||
}
|
||||
|
||||
c.lock = &sync.Mutex{}
|
||||
|
||||
if len(c.Formatter) > 0 {
|
||||
fmtr, ok := logs.GetFormatter(c.Formatter)
|
||||
if !ok {
|
||||
return errors.New(fmt.Sprintf("the formatter with name: %s not found", c.Formatter))
|
||||
}
|
||||
c.formatter = fmtr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *aliLSWriter) Format(lm *logs.LogMsg) string {
|
||||
return lm.OldStyleFormat()
|
||||
}
|
||||
|
||||
func (c *aliLSWriter) SetFormatter(f logs.LogFormatter) {
|
||||
c.formatter = f
|
||||
}
|
||||
|
||||
// WriteMsg writes a message in connection.
|
||||
// If connection is down, try to re-connect.
|
||||
func (c *aliLSWriter) WriteMsg(lm *logs.LogMsg) error {
|
||||
if lm.Level > c.Level {
|
||||
return nil
|
||||
}
|
||||
|
||||
var topic string
|
||||
var content string
|
||||
var lg *LogGroup
|
||||
if c.withMap {
|
||||
|
||||
// Topic,LogGroup
|
||||
strs := strings.SplitN(lm.Msg, Delimiter, 2)
|
||||
if len(strs) == 2 {
|
||||
pos := strings.LastIndex(strs[0], " ")
|
||||
topic = strs[0][pos+1 : len(strs[0])]
|
||||
lg = c.groupMap[topic]
|
||||
}
|
||||
|
||||
// send to empty Topic
|
||||
if lg == nil {
|
||||
lg = c.group[0]
|
||||
}
|
||||
} else {
|
||||
lg = c.group[0]
|
||||
}
|
||||
|
||||
content = c.formatter.Format(lm)
|
||||
|
||||
c1 := &LogContent{
|
||||
Key: proto.String("msg"),
|
||||
Value: proto.String(content),
|
||||
}
|
||||
|
||||
l := &Log{
|
||||
Time: proto.Uint32(uint32(lm.When.Unix())),
|
||||
Contents: []*LogContent{
|
||||
c1,
|
||||
},
|
||||
}
|
||||
|
||||
c.lock.Lock()
|
||||
lg.Logs = append(lg.Logs, l)
|
||||
c.lock.Unlock()
|
||||
|
||||
if len(lg.Logs) >= c.FlushWhen {
|
||||
c.flush(lg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush implementing method. empty.
|
||||
func (c *aliLSWriter) Flush() {
|
||||
// flush all group
|
||||
for _, lg := range c.group {
|
||||
c.flush(lg)
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy destroy connection writer and close tcp listener.
|
||||
func (c *aliLSWriter) Destroy() {
|
||||
}
|
||||
|
||||
func (c *aliLSWriter) flush(lg *LogGroup) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
err := c.store.PutLogs(lg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
lg.Logs = make([]*Log, 0, c.FlushWhen)
|
||||
}
|
||||
|
||||
func init() {
|
||||
logs.Register(logs.AdapterAliLS, NewAliLS)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package alils
|
||||
|
||||
const (
|
||||
version = "0.5.0" // SDK version
|
||||
signatureMethod = "hmac-sha1" // Signature method
|
||||
|
||||
// OffsetNewest is the log head offset, i.e. the offset that will be
|
||||
// assigned to the next message that will be produced to the shard.
|
||||
OffsetNewest = "end"
|
||||
// OffsetOldest is the the oldest offset available on the logstore for a
|
||||
// shard.
|
||||
OffsetOldest = "begin"
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
package alils
|
||||
|
||||
// InputDetail defines log detail
|
||||
type InputDetail struct {
|
||||
LogType string `json:"logType"`
|
||||
LogPath string `json:"logPath"`
|
||||
FilePattern string `json:"filePattern"`
|
||||
LocalStorage bool `json:"localStorage"`
|
||||
TimeFormat string `json:"timeFormat"`
|
||||
LogBeginRegex string `json:"logBeginRegex"`
|
||||
Regex string `json:"regex"`
|
||||
Keys []string `json:"key"`
|
||||
FilterKeys []string `json:"filterKey"`
|
||||
FilterRegex []string `json:"filterRegex"`
|
||||
TopicFormat string `json:"topicFormat"`
|
||||
}
|
||||
|
||||
// OutputDetail defines the output detail
|
||||
type OutputDetail struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
LogStoreName string `json:"logstoreName"`
|
||||
}
|
||||
|
||||
// LogConfig defines Log Config
|
||||
type LogConfig struct {
|
||||
Name string `json:"configName"`
|
||||
InputType string `json:"inputType"`
|
||||
InputDetail InputDetail `json:"inputDetail"`
|
||||
OutputType string `json:"outputType"`
|
||||
OutputDetail OutputDetail `json:"outputDetail"`
|
||||
|
||||
CreateTime uint32
|
||||
LastModifyTime uint32
|
||||
|
||||
project *LogProject
|
||||
}
|
||||
|
||||
// GetAppliedMachineGroup returns applied machine group of this config.
|
||||
func (c *LogConfig) GetAppliedMachineGroup(confName string) (groupNames []string, err error) {
|
||||
groupNames, err = c.project.GetAppliedMachineGroups(c.Name)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,813 @@
|
||||
/*
|
||||
Package alils implements the SDK(v0.5.0) of Simple Log Service(abbr. SLS).
|
||||
|
||||
For more description about SLS, please read this article:
|
||||
http://gitlab.alibaba-inc.com/sls/doc.
|
||||
*/
|
||||
package alils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
)
|
||||
|
||||
// Error message in SLS HTTP response.
|
||||
type errorMessage struct {
|
||||
Code string `json:"errorCode"`
|
||||
Message string `json:"errorMessage"`
|
||||
}
|
||||
|
||||
// LogProject defines the Ali Project detail
|
||||
type LogProject struct {
|
||||
Name string // Project name
|
||||
Endpoint string // IP or hostname of SLS endpoint
|
||||
AccessKeyID string
|
||||
AccessKeySecret string
|
||||
}
|
||||
|
||||
// NewLogProject creates a new SLS project.
|
||||
func NewLogProject(name, endpoint, AccessKeyID, accessKeySecret string) (p *LogProject, err error) {
|
||||
p = &LogProject{
|
||||
Name: name,
|
||||
Endpoint: endpoint,
|
||||
AccessKeyID: AccessKeyID,
|
||||
AccessKeySecret: accessKeySecret,
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// ListLogStore returns all logstore names of project p.
|
||||
func (p *LogProject) ListLogStore() (storeNames []string, err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("/logstores")
|
||||
r, err := request(p, "GET", uri, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(buf, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to list logstore")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
Count int
|
||||
LogStores []string
|
||||
}
|
||||
body := &Body{}
|
||||
|
||||
err = json.Unmarshal(buf, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
storeNames = body.LogStores
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetLogStore returns logstore according by logstore name.
|
||||
func (p *LogProject) GetLogStore(name string) (s *LogStore, err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
r, err := request(p, "GET", "/logstores/"+name, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(buf, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to get logstore")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
s = &LogStore{}
|
||||
err = json.Unmarshal(buf, s)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.project = p
|
||||
return
|
||||
}
|
||||
|
||||
// CreateLogStore creates a new logstore in SLS,
|
||||
// where name is logstore name,
|
||||
// and ttl is time-to-live(in day) of logs,
|
||||
// and shardCnt is the number of shards.
|
||||
func (p *LogProject) CreateLogStore(name string, ttl, shardCnt int) (err error) {
|
||||
type Body struct {
|
||||
Name string `json:"logstoreName"`
|
||||
TTL int `json:"ttl"`
|
||||
ShardCount int `json:"shardCount"`
|
||||
}
|
||||
|
||||
store := &Body{
|
||||
Name: name,
|
||||
TTL: ttl,
|
||||
ShardCount: shardCnt,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(store)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": fmt.Sprintf("%v", len(body)),
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Encoding": "deflate", // TODO: support lz4
|
||||
}
|
||||
|
||||
r, err := request(p, "POST", "/logstores", h, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
body, err = ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(body, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to create logstore")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteLogStore deletes a logstore according by logstore name.
|
||||
func (p *LogProject) DeleteLogStore(name string) (err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
r, err := request(p, "DELETE", "/logstores/"+name, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(body, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to delete logstore")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateLogStore updates a logstore according by logstore name,
|
||||
// obviously we can't modify the logstore name itself.
|
||||
func (p *LogProject) UpdateLogStore(name string, ttl, shardCnt int) (err error) {
|
||||
type Body struct {
|
||||
Name string `json:"logstoreName"`
|
||||
TTL int `json:"ttl"`
|
||||
ShardCount int `json:"shardCount"`
|
||||
}
|
||||
|
||||
store := &Body{
|
||||
Name: name,
|
||||
TTL: ttl,
|
||||
ShardCount: shardCnt,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(store)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": fmt.Sprintf("%v", len(body)),
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Encoding": "deflate", // TODO: support lz4
|
||||
}
|
||||
|
||||
r, err := request(p, "PUT", "/logstores", h, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
body, err = ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(body, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to update logstore")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ListMachineGroup returns machine group name list and the total number of machine groups.
|
||||
// The offset starts from 0 and the size is the max number of machine groups could be returned.
|
||||
func (p *LogProject) ListMachineGroup(offset, size int) (m []string, total int, err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
if size <= 0 {
|
||||
size = 500
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("/machinegroups?offset=%v&size=%v", offset, size)
|
||||
r, err := request(p, "GET", uri, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(buf, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to list machine group")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
MachineGroups []string
|
||||
Count int
|
||||
Total int
|
||||
}
|
||||
body := &Body{}
|
||||
|
||||
err = json.Unmarshal(buf, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
m = body.MachineGroups
|
||||
total = body.Total
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetMachineGroup retruns machine group according by machine group name.
|
||||
func (p *LogProject) GetMachineGroup(name string) (m *MachineGroup, err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
r, err := request(p, "GET", "/machinegroups/"+name, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(buf, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to get machine group:%v", name)
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
m = &MachineGroup{}
|
||||
err = json.Unmarshal(buf, m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
m.project = p
|
||||
return
|
||||
}
|
||||
|
||||
// CreateMachineGroup creates a new machine group in SLS.
|
||||
func (p *LogProject) CreateMachineGroup(m *MachineGroup) (err error) {
|
||||
body, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": fmt.Sprintf("%v", len(body)),
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Encoding": "deflate", // TODO: support lz4
|
||||
}
|
||||
|
||||
r, err := request(p, "POST", "/machinegroups", h, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
body, err = ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(body, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to create machine group")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateMachineGroup updates a machine group.
|
||||
func (p *LogProject) UpdateMachineGroup(m *MachineGroup) (err error) {
|
||||
body, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": fmt.Sprintf("%v", len(body)),
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Encoding": "deflate", // TODO: support lz4
|
||||
}
|
||||
|
||||
r, err := request(p, "PUT", "/machinegroups/"+m.Name, h, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
body, err = ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(body, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to update machine group")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteMachineGroup deletes machine group according machine group name.
|
||||
func (p *LogProject) DeleteMachineGroup(name string) (err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
r, err := request(p, "DELETE", "/machinegroups/"+name, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(body, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to delete machine group")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ListConfig returns config names list and the total number of configs.
|
||||
// The offset starts from 0 and the size is the max number of configs could be returned.
|
||||
func (p *LogProject) ListConfig(offset, size int) (cfgNames []string, total int, err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
if size <= 0 {
|
||||
size = 100
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("/configs?offset=%v&size=%v", offset, size)
|
||||
r, err := request(p, "GET", uri, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(buf, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to delete machine group")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
Total int
|
||||
Configs []string
|
||||
}
|
||||
body := &Body{}
|
||||
|
||||
err = json.Unmarshal(buf, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfgNames = body.Configs
|
||||
total = body.Total
|
||||
return
|
||||
}
|
||||
|
||||
// GetConfig returns config according by config name.
|
||||
func (p *LogProject) GetConfig(name string) (c *LogConfig, err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
r, err := request(p, "GET", "/configs/"+name, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(buf, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to delete config")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
c = &LogConfig{}
|
||||
err = json.Unmarshal(buf, c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.project = p
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateConfig updates a config.
|
||||
func (p *LogProject) UpdateConfig(c *LogConfig) (err error) {
|
||||
body, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": fmt.Sprintf("%v", len(body)),
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Encoding": "deflate", // TODO: support lz4
|
||||
}
|
||||
|
||||
r, err := request(p, "PUT", "/configs/"+c.Name, h, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
body, err = ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(body, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to update config")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CreateConfig creates a new config in SLS.
|
||||
func (p *LogProject) CreateConfig(c *LogConfig) (err error) {
|
||||
body, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": fmt.Sprintf("%v", len(body)),
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Encoding": "deflate", // TODO: support lz4
|
||||
}
|
||||
|
||||
r, err := request(p, "POST", "/configs", h, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
body, err = ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(body, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to update config")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteConfig deletes a config according by config name.
|
||||
func (p *LogProject) DeleteConfig(name string) (err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
r, err := request(p, "DELETE", "/configs/"+name, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(body, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to delete config")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetAppliedMachineGroups returns applied machine group names list according config name.
|
||||
func (p *LogProject) GetAppliedMachineGroups(confName string) (groupNames []string, err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("/configs/%v/machinegroups", confName)
|
||||
r, err := request(p, "GET", uri, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(buf, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to get applied machine groups")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
Count int
|
||||
Machinegroups []string
|
||||
}
|
||||
|
||||
body := &Body{}
|
||||
err = json.Unmarshal(buf, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
groupNames = body.Machinegroups
|
||||
return
|
||||
}
|
||||
|
||||
// GetAppliedConfigs returns applied config names list according machine group name groupName.
|
||||
func (p *LogProject) GetAppliedConfigs(groupName string) (confNames []string, err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("/machinegroups/%v/configs", groupName)
|
||||
r, err := request(p, "GET", uri, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(buf, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to applied configs")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
type Cfg struct {
|
||||
Count int `json:"count"`
|
||||
Configs []string `json:"configs"`
|
||||
}
|
||||
|
||||
body := &Cfg{}
|
||||
err = json.Unmarshal(buf, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
confNames = body.Configs
|
||||
return
|
||||
}
|
||||
|
||||
// ApplyConfigToMachineGroup applies config to machine group.
|
||||
func (p *LogProject) ApplyConfigToMachineGroup(confName, groupName string) (err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("/machinegroups/%v/configs/%v", groupName, confName)
|
||||
r, err := request(p, "PUT", uri, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(buf, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to apply config to machine group")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RemoveConfigFromMachineGroup removes config from machine group.
|
||||
func (p *LogProject) RemoveConfigFromMachineGroup(confName, groupName string) (err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("/machinegroups/%v/configs/%v", groupName, confName)
|
||||
r, err := request(p, "DELETE", uri, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(buf, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to remove config from machine group")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Printf("%s\n", dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package alils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strconv"
|
||||
|
||||
lz4 "github.com/cloudflare/golz4"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
)
|
||||
|
||||
// LogStore stores the logs
|
||||
type LogStore struct {
|
||||
Name string `json:"logstoreName"`
|
||||
TTL int
|
||||
ShardCount int
|
||||
|
||||
CreateTime uint32
|
||||
LastModifyTime uint32
|
||||
|
||||
project *LogProject
|
||||
}
|
||||
|
||||
// Shard defines the Log Shard
|
||||
type Shard struct {
|
||||
ShardID int `json:"shardID"`
|
||||
}
|
||||
|
||||
// ListShards returns shard id list of this logstore.
|
||||
func (s *LogStore) ListShards() (shardIDs []int, err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("/logstores/%v/shards", s.Name)
|
||||
r, err := request(s.project, "GET", uri, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(buf, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to list logstore")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Println(dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
var shards []*Shard
|
||||
err = json.Unmarshal(buf, &shards)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range shards {
|
||||
shardIDs = append(shardIDs, v.ShardID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// PutLogs puts logs into logstore.
|
||||
// The callers should transform user logs into LogGroup.
|
||||
func (s *LogStore) PutLogs(lg *LogGroup) (err error) {
|
||||
body, err := proto.Marshal(lg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Compresse body with lz4
|
||||
out := make([]byte, lz4.CompressBound(body))
|
||||
n, err := lz4.Compress(body, out)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h := map[string]string{
|
||||
"x-sls-compresstype": "lz4",
|
||||
"x-sls-bodyrawsize": fmt.Sprintf("%v", len(body)),
|
||||
"Content-Type": "application/x-protobuf",
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("/logstores/%v", s.Name)
|
||||
r, err := request(s.project, "POST", uri, h, out[:n])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(buf, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to put logs")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Println(dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetCursor gets log cursor of one shard specified by shardID.
|
||||
// The from can be in three form: a) unix timestamp in seccond, b) "begin", c) "end".
|
||||
// For more detail please read: http://gitlab.alibaba-inc.com/sls/doc/blob/master/api/shard.md#logstore
|
||||
func (s *LogStore) GetCursor(shardID int, from string) (cursor string, err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("/logstores/%v/shards/%v?type=cursor&from=%v",
|
||||
s.Name, shardID, from)
|
||||
|
||||
r, err := request(s.project, "GET", uri, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(buf, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to get cursor")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Println(dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
Cursor string
|
||||
}
|
||||
body := &Body{}
|
||||
|
||||
err = json.Unmarshal(buf, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cursor = body.Cursor
|
||||
return
|
||||
}
|
||||
|
||||
// GetLogsBytes gets logs binary data from shard specified by shardID according cursor.
|
||||
// The logGroupMaxCount is the max number of logGroup could be returned.
|
||||
// The nextCursor is the next curosr can be used to read logs at next time.
|
||||
func (s *LogStore) GetLogsBytes(shardID int, cursor string,
|
||||
logGroupMaxCount int) (out []byte, nextCursor string, err error) {
|
||||
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
"Accept": "application/x-protobuf",
|
||||
"Accept-Encoding": "lz4",
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("/logstores/%v/shards/%v?type=logs&cursor=%v&count=%v",
|
||||
s.Name, shardID, cursor, logGroupMaxCount)
|
||||
|
||||
r, err := request(s.project, "GET", uri, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(buf, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to get cursor")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Println(dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
v, ok := r.Header["X-Sls-Compresstype"]
|
||||
if !ok || len(v) == 0 {
|
||||
err = fmt.Errorf("can't find 'x-sls-compresstype' header")
|
||||
return
|
||||
}
|
||||
if v[0] != "lz4" {
|
||||
err = fmt.Errorf("unexpected compress type:%v", v[0])
|
||||
return
|
||||
}
|
||||
|
||||
v, ok = r.Header["X-Sls-Cursor"]
|
||||
if !ok || len(v) == 0 {
|
||||
err = fmt.Errorf("can't find 'x-sls-cursor' header")
|
||||
return
|
||||
}
|
||||
nextCursor = v[0]
|
||||
|
||||
v, ok = r.Header["X-Sls-Bodyrawsize"]
|
||||
if !ok || len(v) == 0 {
|
||||
err = fmt.Errorf("can't find 'x-sls-bodyrawsize' header")
|
||||
return
|
||||
}
|
||||
bodyRawSize, err := strconv.Atoi(v[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
out = make([]byte, bodyRawSize)
|
||||
err = lz4.Uncompress(buf, out)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// LogsBytesDecode decodes logs binary data retruned by GetLogsBytes API
|
||||
func LogsBytesDecode(data []byte) (gl *LogGroupList, err error) {
|
||||
gl = &LogGroupList{}
|
||||
err = proto.Unmarshal(data, gl)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetLogs gets logs from shard specified by shardID according cursor.
|
||||
// The logGroupMaxCount is the max number of logGroup could be returned.
|
||||
// The nextCursor is the next curosr can be used to read logs at next time.
|
||||
func (s *LogStore) GetLogs(shardID int, cursor string,
|
||||
logGroupMaxCount int) (gl *LogGroupList, nextCursor string, err error) {
|
||||
|
||||
out, nextCursor, err := s.GetLogsBytes(shardID, cursor, logGroupMaxCount)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
gl, err = LogsBytesDecode(out)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package alils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
)
|
||||
|
||||
// MachineGroupAttribute defines the Attribute
|
||||
type MachineGroupAttribute struct {
|
||||
ExternalName string `json:"externalName"`
|
||||
TopicName string `json:"groupTopic"`
|
||||
}
|
||||
|
||||
// MachineGroup defines the machine Group
|
||||
type MachineGroup struct {
|
||||
Name string `json:"groupName"`
|
||||
Type string `json:"groupType"`
|
||||
MachineIDType string `json:"machineIdentifyType"`
|
||||
MachineIDList []string `json:"machineList"`
|
||||
|
||||
Attribute MachineGroupAttribute `json:"groupAttribute"`
|
||||
|
||||
CreateTime uint32
|
||||
LastModifyTime uint32
|
||||
|
||||
project *LogProject
|
||||
}
|
||||
|
||||
// Machine defines the Machine
|
||||
type Machine struct {
|
||||
IP string
|
||||
UniqueID string `json:"machine-uniqueid"`
|
||||
UserdefinedID string `json:"userdefined-id"`
|
||||
}
|
||||
|
||||
// MachineList defines the Machine List
|
||||
type MachineList struct {
|
||||
Total int
|
||||
Machines []*Machine
|
||||
}
|
||||
|
||||
// ListMachines returns the machine list of this machine group.
|
||||
func (m *MachineGroup) ListMachines() (ms []*Machine, total int, err error) {
|
||||
h := map[string]string{
|
||||
"x-sls-bodyrawsize": "0",
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("/machinegroups/%v/machines", m.Name)
|
||||
r, err := request(m.project, "GET", uri, h, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
errMsg := &errorMessage{}
|
||||
err = json.Unmarshal(buf, errMsg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to remove config from machine group")
|
||||
dump, _ := httputil.DumpResponse(r, true)
|
||||
fmt.Println(dump)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message)
|
||||
return
|
||||
}
|
||||
|
||||
body := &MachineList{}
|
||||
err = json.Unmarshal(buf, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ms = body.Machines
|
||||
total = body.Total
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetAppliedConfigs returns applied configs of this machine group.
|
||||
func (m *MachineGroup) GetAppliedConfigs() (confNames []string, err error) {
|
||||
confNames, err = m.project.GetAppliedConfigs(m.Name)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package alils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// request sends a request to SLS.
|
||||
func request(project *LogProject, method, uri string, headers map[string]string,
|
||||
body []byte) (resp *http.Response, err error) {
|
||||
|
||||
// The caller should provide 'x-sls-bodyrawsize' header
|
||||
if _, ok := headers["x-sls-bodyrawsize"]; !ok {
|
||||
err = fmt.Errorf("Can't find 'x-sls-bodyrawsize' header")
|
||||
return
|
||||
}
|
||||
|
||||
// SLS public request headers
|
||||
headers["Host"] = project.Name + "." + project.Endpoint
|
||||
headers["Date"] = nowRFC1123()
|
||||
headers["x-sls-apiversion"] = version
|
||||
headers["x-sls-signaturemethod"] = signatureMethod
|
||||
if body != nil {
|
||||
bodyMD5 := fmt.Sprintf("%X", md5.Sum(body))
|
||||
headers["Content-MD5"] = bodyMD5
|
||||
|
||||
if _, ok := headers["Content-Type"]; !ok {
|
||||
err = fmt.Errorf("Can't find 'Content-Type' header")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Calc Authorization
|
||||
// Authorization = "SLS <AccessKeyID>:<Signature>"
|
||||
digest, err := signature(project, method, uri, headers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
auth := fmt.Sprintf("SLS %v:%v", project.AccessKeyID, digest)
|
||||
headers["Authorization"] = auth
|
||||
|
||||
// Initialize http request
|
||||
reader := bytes.NewReader(body)
|
||||
urlStr := fmt.Sprintf("http://%v.%v%v", project.Name, project.Endpoint, uri)
|
||||
req, err := http.NewRequest(method, urlStr, reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
|
||||
// Get ready to do request
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package alils
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GMT location
|
||||
var gmtLoc = time.FixedZone("GMT", 0)
|
||||
|
||||
// NowRFC1123 returns now time in RFC1123 format with GMT timezone,
|
||||
// eg. "Mon, 02 Jan 2006 15:04:05 GMT".
|
||||
func nowRFC1123() string {
|
||||
return time.Now().In(gmtLoc).Format(time.RFC1123)
|
||||
}
|
||||
|
||||
// signature calculates a request's signature digest.
|
||||
func signature(project *LogProject, method, uri string,
|
||||
headers map[string]string) (digest string, err error) {
|
||||
var contentMD5, contentType, date, canoHeaders, canoResource string
|
||||
var slsHeaderKeys sort.StringSlice
|
||||
|
||||
// SignString = VERB + "\n"
|
||||
// + CONTENT-MD5 + "\n"
|
||||
// + CONTENT-TYPE + "\n"
|
||||
// + DATE + "\n"
|
||||
// + CanonicalizedSLSHeaders + "\n"
|
||||
// + CanonicalizedResource
|
||||
|
||||
if val, ok := headers["Content-MD5"]; ok {
|
||||
contentMD5 = val
|
||||
}
|
||||
|
||||
if val, ok := headers["Content-Type"]; ok {
|
||||
contentType = val
|
||||
}
|
||||
|
||||
date, ok := headers["Date"]
|
||||
if !ok {
|
||||
err = fmt.Errorf("Can't find 'Date' header")
|
||||
return
|
||||
}
|
||||
|
||||
// Calc CanonicalizedSLSHeaders
|
||||
slsHeaders := make(map[string]string, len(headers))
|
||||
for k, v := range headers {
|
||||
l := strings.TrimSpace(strings.ToLower(k))
|
||||
if strings.HasPrefix(l, "x-sls-") {
|
||||
slsHeaders[l] = strings.TrimSpace(v)
|
||||
slsHeaderKeys = append(slsHeaderKeys, l)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(slsHeaderKeys)
|
||||
for i, k := range slsHeaderKeys {
|
||||
canoHeaders += k + ":" + slsHeaders[k]
|
||||
if i+1 < len(slsHeaderKeys) {
|
||||
canoHeaders += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
// Calc CanonicalizedResource
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
canoResource += url.QueryEscape(u.Path)
|
||||
if u.RawQuery != "" {
|
||||
var keys sort.StringSlice
|
||||
|
||||
vals := u.Query()
|
||||
for k := range vals {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
sort.Sort(keys)
|
||||
canoResource += "?"
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
canoResource += "&"
|
||||
}
|
||||
|
||||
for _, v := range vals[k] {
|
||||
canoResource += k + "=" + v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signStr := method + "\n" +
|
||||
contentMD5 + "\n" +
|
||||
contentType + "\n" +
|
||||
date + "\n" +
|
||||
canoHeaders + "\n" +
|
||||
canoResource
|
||||
|
||||
// Signature = base64(hmac-sha1(UTF8-Encoding-Of(SignString),AccessKeySecret))
|
||||
mac := hmac.New(sha1.New, []byte(project.AccessKeySecret))
|
||||
_, err = mac.Write([]byte(signStr))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
digest = base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright 2014 beego Author. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// connWriter implements LoggerInterface.
|
||||
// Writes messages in keep-live tcp connection.
|
||||
type connWriter struct {
|
||||
lg *logWriter
|
||||
innerWriter io.WriteCloser
|
||||
formatter LogFormatter
|
||||
Formatter string `json:"formatter"`
|
||||
ReconnectOnMsg bool `json:"reconnectOnMsg"`
|
||||
Reconnect bool `json:"reconnect"`
|
||||
Net string `json:"net"`
|
||||
Addr string `json:"addr"`
|
||||
Level int `json:"level"`
|
||||
}
|
||||
|
||||
// NewConn creates new ConnWrite returning as LoggerInterface.
|
||||
func NewConn() Logger {
|
||||
conn := new(connWriter)
|
||||
conn.Level = LevelTrace
|
||||
conn.formatter = conn
|
||||
return conn
|
||||
}
|
||||
|
||||
func (c *connWriter) Format(lm *LogMsg) string {
|
||||
return lm.OldStyleFormat()
|
||||
}
|
||||
|
||||
// Init initializes a connection writer with json config.
|
||||
// json config only needs they "level" key
|
||||
func (c *connWriter) Init(config string) error {
|
||||
res := json.Unmarshal([]byte(config), c)
|
||||
if res == nil && len(c.Formatter) > 0 {
|
||||
fmtr, ok := GetFormatter(c.Formatter)
|
||||
if !ok {
|
||||
return errors.New(fmt.Sprintf("the formatter with name: %s not found", c.Formatter))
|
||||
}
|
||||
c.formatter = fmtr
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (c *connWriter) SetFormatter(f LogFormatter) {
|
||||
c.formatter = f
|
||||
}
|
||||
|
||||
// WriteMsg writes message in connection.
|
||||
// If connection is down, try to re-connect.
|
||||
func (c *connWriter) WriteMsg(lm *LogMsg) error {
|
||||
if lm.Level > c.Level {
|
||||
return nil
|
||||
}
|
||||
if c.needToConnectOnMsg() {
|
||||
err := c.connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.ReconnectOnMsg {
|
||||
defer c.innerWriter.Close()
|
||||
}
|
||||
|
||||
msg := c.formatter.Format(lm)
|
||||
|
||||
_, err := c.lg.writeln(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush implementing method. empty.
|
||||
func (c *connWriter) Flush() {
|
||||
}
|
||||
|
||||
// Destroy destroy connection writer and close tcp listener.
|
||||
func (c *connWriter) Destroy() {
|
||||
if c.innerWriter != nil {
|
||||
c.innerWriter.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connWriter) connect() error {
|
||||
if c.innerWriter != nil {
|
||||
c.innerWriter.Close()
|
||||
c.innerWriter = nil
|
||||
}
|
||||
|
||||
conn, err := net.Dial(c.Net, c.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
||||
tcpConn.SetKeepAlive(true)
|
||||
}
|
||||
|
||||
c.innerWriter = conn
|
||||
c.lg = newLogWriter(conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *connWriter) needToConnectOnMsg() bool {
|
||||
if c.Reconnect {
|
||||
return true
|
||||
}
|
||||
|
||||
if c.innerWriter == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return c.ReconnectOnMsg
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(AdapterConn, NewConn)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2014 beego Author. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// ConnTCPListener takes a TCP listener and accepts n TCP connections
|
||||
// Returns connections using connChan
|
||||
func connTCPListener(t *testing.T, n int, ln net.Listener, connChan chan<- net.Conn) {
|
||||
// Listen and accept n incoming connections
|
||||
for i := 0; i < n; i++ {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Log("Error accepting connection: ", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Send accepted connection to channel
|
||||
connChan <- conn
|
||||
}
|
||||
ln.Close()
|
||||
close(connChan)
|
||||
}
|
||||
|
||||
func TestConn(t *testing.T) {
|
||||
log := NewLogger(1000)
|
||||
log.SetLogger("conn", `{"net":"tcp","addr":":7020"}`)
|
||||
log.Informational("informational")
|
||||
}
|
||||
|
||||
// need to rewrite this test, it's not stable
|
||||
func TestReconnect(t *testing.T) {
|
||||
// Setup connection listener
|
||||
newConns := make(chan net.Conn)
|
||||
connNum := 2
|
||||
ln, err := net.Listen("tcp", ":6002")
|
||||
if err != nil {
|
||||
t.Log("Error listening:", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
go connTCPListener(t, connNum, ln, newConns)
|
||||
|
||||
// Setup logger
|
||||
log := NewLogger(1000)
|
||||
log.SetPrefix("test")
|
||||
log.SetLogger(AdapterConn, `{"net":"tcp","reconnect":true,"level":6,"addr":":6002"}`)
|
||||
log.Informational("informational 1")
|
||||
|
||||
// Refuse first connection
|
||||
first := <-newConns
|
||||
first.Close()
|
||||
|
||||
// Send another log after conn closed
|
||||
log.Informational("informational 2")
|
||||
|
||||
// Check if there was a second connection attempt
|
||||
select {
|
||||
case second := <-newConns:
|
||||
second.Close()
|
||||
default:
|
||||
t.Error("Did not reconnect")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnWriter_Format(t *testing.T) {
|
||||
lg := &LogMsg{
|
||||
Level: LevelDebug,
|
||||
Msg: "Hello, world",
|
||||
When: time.Date(2020, 9, 19, 20, 12, 37, 9, time.UTC),
|
||||
FilePath: "/user/home/main.go",
|
||||
LineNumber: 13,
|
||||
Prefix: "Cus",
|
||||
}
|
||||
cw := NewConn().(*connWriter)
|
||||
res := cw.Format(lg)
|
||||
assert.Equal(t, "[D] Cus Hello, world", res)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright 2014 beego Author. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/shiena/ansicolor"
|
||||
)
|
||||
|
||||
// brush is a color join function
|
||||
type brush func(string) string
|
||||
|
||||
// newBrush returns a fix color Brush
|
||||
func newBrush(color string) brush {
|
||||
pre := "\033["
|
||||
reset := "\033[0m"
|
||||
return func(text string) string {
|
||||
return pre + color + "m" + text + reset
|
||||
}
|
||||
}
|
||||
|
||||
var colors = []brush{
|
||||
newBrush("1;37"), // Emergency white
|
||||
newBrush("1;36"), // Alert cyan
|
||||
newBrush("1;35"), // Critical magenta
|
||||
newBrush("1;31"), // Error red
|
||||
newBrush("1;33"), // Warning yellow
|
||||
newBrush("1;32"), // Notice green
|
||||
newBrush("1;34"), // Informational blue
|
||||
newBrush("1;44"), // Debug Background blue
|
||||
}
|
||||
|
||||
// consoleWriter implements LoggerInterface and writes messages to terminal.
|
||||
type consoleWriter struct {
|
||||
lg *logWriter
|
||||
formatter LogFormatter
|
||||
Formatter string `json:"formatter"`
|
||||
Level int `json:"level"`
|
||||
Colorful bool `json:"color"` // this filed is useful only when system's terminal supports color
|
||||
}
|
||||
|
||||
func (c *consoleWriter) Format(lm *LogMsg) string {
|
||||
msg := lm.OldStyleFormat()
|
||||
if c.Colorful {
|
||||
msg = strings.Replace(msg, levelPrefix[lm.Level], colors[lm.Level](levelPrefix[lm.Level]), 1)
|
||||
}
|
||||
h, _, _ := formatTimeHeader(lm.When)
|
||||
return string(append(h, msg...))
|
||||
}
|
||||
|
||||
func (c *consoleWriter) SetFormatter(f LogFormatter) {
|
||||
c.formatter = f
|
||||
}
|
||||
|
||||
// NewConsole creates ConsoleWriter returning as LoggerInterface.
|
||||
func NewConsole() Logger {
|
||||
return newConsole()
|
||||
}
|
||||
|
||||
func newConsole() *consoleWriter {
|
||||
cw := &consoleWriter{
|
||||
lg: newLogWriter(ansicolor.NewAnsiColorWriter(os.Stdout)),
|
||||
Level: LevelDebug,
|
||||
Colorful: true,
|
||||
}
|
||||
cw.formatter = cw
|
||||
return cw
|
||||
}
|
||||
|
||||
// Init initianlizes the console logger.
|
||||
// jsonConfig must be in the format '{"level":LevelTrace}'
|
||||
func (c *consoleWriter) Init(config string) error {
|
||||
if len(config) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
res := json.Unmarshal([]byte(config), c)
|
||||
if res == nil && len(c.Formatter) > 0 {
|
||||
fmtr, ok := GetFormatter(c.Formatter)
|
||||
if !ok {
|
||||
return errors.New(fmt.Sprintf("the formatter with name: %s not found", c.Formatter))
|
||||
}
|
||||
c.formatter = fmtr
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// WriteMsg writes message in console.
|
||||
func (c *consoleWriter) WriteMsg(lm *LogMsg) error {
|
||||
if lm.Level > c.Level {
|
||||
return nil
|
||||
}
|
||||
msg := c.formatter.Format(lm)
|
||||
c.lg.writeln(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy implementing method. empty.
|
||||
func (c *consoleWriter) Destroy() {
|
||||
}
|
||||
|
||||
// Flush implementing method. empty.
|
||||
func (c *consoleWriter) Flush() {
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(AdapterConsole, NewConsole)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2014 beego Author. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Try each log level in decreasing order of priority.
|
||||
func testConsoleCalls(bl *BeeLogger) {
|
||||
bl.Emergency("emergency")
|
||||
bl.Alert("alert")
|
||||
bl.Critical("critical")
|
||||
bl.Error("error")
|
||||
bl.Warning("warning")
|
||||
bl.Notice("notice")
|
||||
bl.Informational("informational")
|
||||
bl.Debug("debug")
|
||||
}
|
||||
|
||||
// Test console logging by visually comparing the lines being output with and
|
||||
// without a log level specification.
|
||||
func TestConsole(t *testing.T) {
|
||||
log1 := NewLogger(10000)
|
||||
log1.EnableFuncCallDepth(true)
|
||||
log1.SetLogger("console", "")
|
||||
testConsoleCalls(log1)
|
||||
|
||||
log2 := NewLogger(100)
|
||||
log2.SetLogger("console", `{"level":3}`)
|
||||
testConsoleCalls(log2)
|
||||
}
|
||||
|
||||
// Test console without color
|
||||
func TestConsoleNoColor(t *testing.T) {
|
||||
log := NewLogger(100)
|
||||
log.SetLogger("console", `{"color":false}`)
|
||||
testConsoleCalls(log)
|
||||
}
|
||||
|
||||
// Test console async
|
||||
func TestConsoleAsync(t *testing.T) {
|
||||
log := NewLogger(100)
|
||||
log.SetLogger("console")
|
||||
log.Async()
|
||||
// log.Close()
|
||||
testConsoleCalls(log)
|
||||
for len(log.msgChan) != 0 {
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
log.Flush()
|
||||
log.Close()
|
||||
}
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
log := newConsole()
|
||||
lm := &LogMsg{
|
||||
Level: LevelDebug,
|
||||
Msg: "Hello, world",
|
||||
When: time.Date(2020, 9, 19, 20, 12, 37, 9, time.UTC),
|
||||
FilePath: "/user/home/main.go",
|
||||
LineNumber: 13,
|
||||
Prefix: "Cus",
|
||||
}
|
||||
res := log.Format(lm)
|
||||
assert.Equal(t, "2020/09/19 20:12:37.000 \x1b[1;44m[D]\x1b[0m Cus Hello, world", res)
|
||||
err := log.WriteMsg(lm)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package es
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/elastic/go-elasticsearch/v6"
|
||||
"github.com/elastic/go-elasticsearch/v6/esapi"
|
||||
|
||||
"github.com/cdle/sillyplus/core/logs"
|
||||
)
|
||||
|
||||
// NewES returns a LoggerInterface
|
||||
func NewES() logs.Logger {
|
||||
cw := &esLogger{
|
||||
Level: logs.LevelDebug,
|
||||
indexNaming: indexNaming,
|
||||
}
|
||||
return cw
|
||||
}
|
||||
|
||||
// esLogger will log msg into ES
|
||||
// before you using this implementation,
|
||||
// please import this package
|
||||
// usually means that you can import this package in your main package
|
||||
// for example, anonymous:
|
||||
// import _ "github.com/cdle/sillyplus/core/logs/es"
|
||||
type esLogger struct {
|
||||
*elasticsearch.Client
|
||||
DSN string `json:"dsn"`
|
||||
Level int `json:"level"`
|
||||
formatter logs.LogFormatter
|
||||
Formatter string `json:"formatter"`
|
||||
|
||||
indexNaming IndexNaming
|
||||
}
|
||||
|
||||
func (el *esLogger) Format(lm *logs.LogMsg) string {
|
||||
msg := lm.OldStyleFormat()
|
||||
idx := LogDocument{
|
||||
Timestamp: lm.When.Format(time.RFC3339),
|
||||
Msg: msg,
|
||||
}
|
||||
body, err := json.Marshal(idx)
|
||||
if err != nil {
|
||||
return msg
|
||||
}
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func (el *esLogger) SetFormatter(f logs.LogFormatter) {
|
||||
el.formatter = f
|
||||
}
|
||||
|
||||
// {"dsn":"http://localhost:9200/","level":1}
|
||||
func (el *esLogger) Init(config string) error {
|
||||
err := json.Unmarshal([]byte(config), el)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if el.DSN == "" {
|
||||
return errors.New("empty dsn")
|
||||
} else if u, err := url.Parse(el.DSN); err != nil {
|
||||
return err
|
||||
} else if u.Path == "" {
|
||||
return errors.New("missing prefix")
|
||||
} else {
|
||||
conn, err := elasticsearch.NewClient(elasticsearch.Config{
|
||||
Addresses: []string{el.DSN},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
el.Client = conn
|
||||
}
|
||||
if len(el.Formatter) > 0 {
|
||||
fmtr, ok := logs.GetFormatter(el.Formatter)
|
||||
if !ok {
|
||||
return errors.New(fmt.Sprintf("the formatter with name: %s not found", el.Formatter))
|
||||
}
|
||||
el.formatter = fmtr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteMsg writes the msg and level into es
|
||||
func (el *esLogger) WriteMsg(lm *logs.LogMsg) error {
|
||||
if lm.Level > el.Level {
|
||||
return nil
|
||||
}
|
||||
|
||||
msg := el.formatter.Format(lm)
|
||||
|
||||
req := esapi.IndexRequest{
|
||||
Index: indexNaming.IndexName(lm),
|
||||
DocumentType: "logs",
|
||||
Body: strings.NewReader(msg),
|
||||
}
|
||||
_, err := req.Do(context.Background(), el.Client)
|
||||
return err
|
||||
}
|
||||
|
||||
// Destroy is an empty method
|
||||
func (el *esLogger) Destroy() {
|
||||
}
|
||||
|
||||
// Flush is an empty method
|
||||
func (el *esLogger) Flush() {
|
||||
}
|
||||
|
||||
type LogDocument struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
logs.Register(logs.AdapterEs, NewES)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2020
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package es
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cdle/sillyplus/core/logs"
|
||||
)
|
||||
|
||||
// IndexNaming generate the index name
|
||||
type IndexNaming interface {
|
||||
IndexName(lm *logs.LogMsg) string
|
||||
}
|
||||
|
||||
var indexNaming IndexNaming = &defaultIndexNaming{}
|
||||
|
||||
// SetIndexNaming will register global IndexNaming
|
||||
func SetIndexNaming(i IndexNaming) {
|
||||
indexNaming = i
|
||||
}
|
||||
|
||||
type defaultIndexNaming struct{}
|
||||
|
||||
func (d *defaultIndexNaming) IndexName(lm *logs.LogMsg) string {
|
||||
return fmt.Sprintf("%04d.%02d.%02d", lm.When.Year(), lm.When.Month(), lm.When.Day())
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2020
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package es
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cdle/sillyplus/core/logs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDefaultIndexNaming_IndexName(t *testing.T) {
|
||||
tm := time.Date(2020, 9, 12, 1, 34, 45, 234, time.UTC)
|
||||
lm := &logs.LogMsg{
|
||||
When: tm,
|
||||
}
|
||||
|
||||
res := (&defaultIndexNaming{}).IndexName(lm)
|
||||
assert.Equal(t, "2020.09.12", res)
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
// Copyright 2014 beego Author. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// fileLogWriter implements LoggerInterface.
|
||||
// Writes messages by lines limit, file size limit, or time frequency.
|
||||
type fileLogWriter struct {
|
||||
sync.RWMutex // write log order by order and atomic incr maxLinesCurLines and maxSizeCurSize
|
||||
|
||||
Rotate bool `json:"rotate"`
|
||||
Daily bool `json:"daily"`
|
||||
Hourly bool `json:"hourly"`
|
||||
|
||||
// The opened file
|
||||
Filename string `json:"filename"`
|
||||
fileWriter *os.File
|
||||
|
||||
// Rotate at line
|
||||
MaxLines int `json:"maxlines"`
|
||||
maxLinesCurLines int
|
||||
|
||||
MaxFiles int `json:"maxfiles"`
|
||||
MaxFilesCurFiles int
|
||||
|
||||
// Rotate at size
|
||||
MaxSize int `json:"maxsize"`
|
||||
maxSizeCurSize int
|
||||
|
||||
// Rotate daily
|
||||
MaxDays int64 `json:"maxdays"`
|
||||
dailyOpenDate int
|
||||
dailyOpenTime time.Time
|
||||
|
||||
// Rotate hourly
|
||||
MaxHours int64 `json:"maxhours"`
|
||||
hourlyOpenDate int
|
||||
hourlyOpenTime time.Time
|
||||
|
||||
Level int `json:"level"`
|
||||
// Permissions for log file
|
||||
Perm string `json:"perm"`
|
||||
// Permissions for directory if it is specified in FileName
|
||||
DirPerm string `json:"dirperm"`
|
||||
|
||||
RotatePerm string `json:"rotateperm"`
|
||||
|
||||
fileNameOnly, suffix string // like "project.log", project is fileNameOnly and .log is suffix
|
||||
|
||||
logFormatter LogFormatter
|
||||
Formatter string `json:"formatter"`
|
||||
}
|
||||
|
||||
// newFileWriter creates a FileLogWriter returning as LoggerInterface.
|
||||
func newFileWriter() Logger {
|
||||
w := &fileLogWriter{
|
||||
Daily: true,
|
||||
MaxDays: 7,
|
||||
Hourly: false,
|
||||
MaxHours: 168,
|
||||
Rotate: true,
|
||||
RotatePerm: "0440",
|
||||
Level: LevelTrace,
|
||||
Perm: "0660",
|
||||
DirPerm: "0770",
|
||||
MaxLines: 10000000,
|
||||
MaxFiles: 999,
|
||||
MaxSize: 1 << 28,
|
||||
}
|
||||
w.logFormatter = w
|
||||
return w
|
||||
}
|
||||
|
||||
func (*fileLogWriter) Format(lm *LogMsg) string {
|
||||
msg := lm.OldStyleFormat()
|
||||
hd, _, _ := formatTimeHeader(lm.When)
|
||||
msg = fmt.Sprintf("%s %s\n", string(hd), msg)
|
||||
return msg
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) SetFormatter(f LogFormatter) {
|
||||
w.logFormatter = f
|
||||
}
|
||||
|
||||
// Init file logger with json config.
|
||||
// jsonConfig like:
|
||||
// {
|
||||
// "filename":"logs/beego.log",
|
||||
// "maxLines":10000,
|
||||
// "maxsize":1024,
|
||||
// "daily":true,
|
||||
// "maxDays":15,
|
||||
// "rotate":true,
|
||||
// "perm":"0600"
|
||||
// }
|
||||
func (w *fileLogWriter) Init(config string) error {
|
||||
err := json.Unmarshal([]byte(config), w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if w.Filename == "" {
|
||||
return errors.New("jsonconfig must have filename")
|
||||
}
|
||||
w.suffix = filepath.Ext(w.Filename)
|
||||
w.fileNameOnly = strings.TrimSuffix(w.Filename, w.suffix)
|
||||
if w.suffix == "" {
|
||||
w.suffix = ".log"
|
||||
}
|
||||
|
||||
if len(w.Formatter) > 0 {
|
||||
fmtr, ok := GetFormatter(w.Formatter)
|
||||
if !ok {
|
||||
return fmt.Errorf("the formatter with name: %s not found", w.Formatter)
|
||||
}
|
||||
w.logFormatter = fmtr
|
||||
}
|
||||
err = w.startLogger()
|
||||
return err
|
||||
}
|
||||
|
||||
// start file logger. create log file and set to locker-inside file writer.
|
||||
func (w *fileLogWriter) startLogger() error {
|
||||
file, err := w.createLogFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if w.fileWriter != nil {
|
||||
w.fileWriter.Close()
|
||||
}
|
||||
w.fileWriter = file
|
||||
return w.initFd()
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) needRotateDaily(day int) bool {
|
||||
return (w.MaxLines > 0 && w.maxLinesCurLines >= w.MaxLines) ||
|
||||
(w.MaxSize > 0 && w.maxSizeCurSize >= w.MaxSize) ||
|
||||
(w.Daily && day != w.dailyOpenDate)
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) needRotateHourly(hour int) bool {
|
||||
return (w.MaxLines > 0 && w.maxLinesCurLines >= w.MaxLines) ||
|
||||
(w.MaxSize > 0 && w.maxSizeCurSize >= w.MaxSize) ||
|
||||
(w.Hourly && hour != w.hourlyOpenDate)
|
||||
}
|
||||
|
||||
// WriteMsg writes logger message into file.
|
||||
func (w *fileLogWriter) WriteMsg(lm *LogMsg) error {
|
||||
if lm.Level > w.Level {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, d, h := formatTimeHeader(lm.When)
|
||||
|
||||
msg := w.logFormatter.Format(lm)
|
||||
if w.Rotate {
|
||||
w.RLock()
|
||||
if w.needRotateHourly(h) {
|
||||
w.RUnlock()
|
||||
w.Lock()
|
||||
if w.needRotateHourly(h) {
|
||||
if err := w.doRotate(lm.When); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
|
||||
}
|
||||
}
|
||||
w.Unlock()
|
||||
} else if w.needRotateDaily(d) {
|
||||
w.RUnlock()
|
||||
w.Lock()
|
||||
if w.needRotateDaily(d) {
|
||||
if err := w.doRotate(lm.When); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
|
||||
}
|
||||
}
|
||||
w.Unlock()
|
||||
} else {
|
||||
w.RUnlock()
|
||||
}
|
||||
}
|
||||
|
||||
w.Lock()
|
||||
_, err := w.fileWriter.Write([]byte(msg))
|
||||
if err == nil {
|
||||
w.maxLinesCurLines++
|
||||
w.maxSizeCurSize += len(msg)
|
||||
}
|
||||
w.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) createLogFile() (*os.File, error) {
|
||||
// Open the log file
|
||||
perm, err := strconv.ParseInt(w.Perm, 8, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dirperm, err := strconv.ParseInt(w.DirPerm, 8, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filepath := path.Dir(w.Filename)
|
||||
os.MkdirAll(filepath, os.FileMode(dirperm))
|
||||
|
||||
fd, err := os.OpenFile(w.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(perm))
|
||||
if err == nil {
|
||||
// Make sure file perm is user set perm cause of `os.OpenFile` will obey umask
|
||||
os.Chmod(w.Filename, os.FileMode(perm))
|
||||
}
|
||||
return fd, err
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) initFd() error {
|
||||
fd := w.fileWriter
|
||||
fInfo, err := fd.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get stat err: %s", err)
|
||||
}
|
||||
w.maxSizeCurSize = int(fInfo.Size())
|
||||
w.dailyOpenTime = time.Now()
|
||||
w.dailyOpenDate = w.dailyOpenTime.Day()
|
||||
w.hourlyOpenTime = time.Now()
|
||||
w.hourlyOpenDate = w.hourlyOpenTime.Hour()
|
||||
w.maxLinesCurLines = 0
|
||||
if w.Hourly {
|
||||
go w.hourlyRotate(w.hourlyOpenTime)
|
||||
} else if w.Daily {
|
||||
go w.dailyRotate(w.dailyOpenTime)
|
||||
}
|
||||
if fInfo.Size() > 0 && w.MaxLines > 0 {
|
||||
count, err := w.lines()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.maxLinesCurLines = count
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) dailyRotate(openTime time.Time) {
|
||||
y, m, d := openTime.Add(24 * time.Hour).Date()
|
||||
nextDay := time.Date(y, m, d, 0, 0, 0, 0, openTime.Location())
|
||||
tm := time.NewTimer(time.Duration(nextDay.UnixNano() - openTime.UnixNano() + 100))
|
||||
<-tm.C
|
||||
w.Lock()
|
||||
if w.needRotateDaily(time.Now().Day()) {
|
||||
if err := w.doRotate(time.Now()); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
|
||||
}
|
||||
}
|
||||
w.Unlock()
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) hourlyRotate(openTime time.Time) {
|
||||
y, m, d := openTime.Add(1 * time.Hour).Date()
|
||||
h, _, _ := openTime.Add(1 * time.Hour).Clock()
|
||||
nextHour := time.Date(y, m, d, h, 0, 0, 0, openTime.Location())
|
||||
tm := time.NewTimer(time.Duration(nextHour.UnixNano() - openTime.UnixNano() + 100))
|
||||
<-tm.C
|
||||
w.Lock()
|
||||
if w.needRotateHourly(time.Now().Hour()) {
|
||||
if err := w.doRotate(time.Now()); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
|
||||
}
|
||||
}
|
||||
w.Unlock()
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) lines() (int, error) {
|
||||
fd, err := os.Open(w.Filename)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
buf := make([]byte, 32768) // 32k
|
||||
count := 0
|
||||
lineSep := []byte{'\n'}
|
||||
|
||||
for {
|
||||
c, err := fd.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return count, err
|
||||
}
|
||||
|
||||
count += bytes.Count(buf[:c], lineSep)
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// DoRotate means it needs to write logs into a new file.
|
||||
// new file name like xx.2013-01-01.log (daily) or xx.001.log (by line or size)
|
||||
func (w *fileLogWriter) doRotate(logTime time.Time) error {
|
||||
// file exists
|
||||
// Find the next available number
|
||||
num := w.MaxFilesCurFiles + 1
|
||||
fName := ""
|
||||
format := ""
|
||||
var openTime time.Time
|
||||
rotatePerm, err := strconv.ParseInt(w.RotatePerm, 8, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = os.Lstat(w.Filename)
|
||||
if err != nil {
|
||||
// even if the file is not exist or other ,we should RESTART the logger
|
||||
goto RESTART_LOGGER
|
||||
}
|
||||
|
||||
if w.Hourly {
|
||||
format = "2006010215"
|
||||
openTime = w.hourlyOpenTime
|
||||
} else if w.Daily {
|
||||
format = "2006-01-02"
|
||||
openTime = w.dailyOpenTime
|
||||
}
|
||||
|
||||
// only when one of them be setted, then the file would be splited
|
||||
if w.MaxLines > 0 || w.MaxSize > 0 {
|
||||
for ; err == nil && num <= w.MaxFiles; num++ {
|
||||
fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", logTime.Format(format), num, w.suffix)
|
||||
_, err = os.Lstat(fName)
|
||||
}
|
||||
} else {
|
||||
fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", openTime.Format(format), num, w.suffix)
|
||||
_, err = os.Lstat(fName)
|
||||
w.MaxFilesCurFiles = num
|
||||
}
|
||||
|
||||
// return error if the last file checked still existed
|
||||
if err == nil {
|
||||
return fmt.Errorf("Rotate: Cannot find free log number to rename %s", w.Filename)
|
||||
}
|
||||
|
||||
// close fileWriter before rename
|
||||
w.fileWriter.Close()
|
||||
|
||||
// Rename the file to its new found name
|
||||
// even if occurs error,we MUST guarantee to restart new logger
|
||||
err = os.Rename(w.Filename, fName)
|
||||
if err != nil {
|
||||
goto RESTART_LOGGER
|
||||
}
|
||||
|
||||
err = os.Chmod(fName, os.FileMode(rotatePerm))
|
||||
|
||||
RESTART_LOGGER:
|
||||
|
||||
startLoggerErr := w.startLogger()
|
||||
go w.deleteOldLog()
|
||||
|
||||
if startLoggerErr != nil {
|
||||
return fmt.Errorf("Rotate StartLogger: %s", startLoggerErr)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("Rotate: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) deleteOldLog() {
|
||||
dir := filepath.Dir(w.Filename)
|
||||
absolutePath, err := filepath.EvalSymlinks(w.Filename)
|
||||
if err == nil {
|
||||
dir = filepath.Dir(absolutePath)
|
||||
}
|
||||
filepath.Walk(dir, func(path string, info os.FileInfo, err error) (returnErr error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "Unable to delete old log '%s', error: %v\n", path, r)
|
||||
}
|
||||
}()
|
||||
|
||||
if info == nil {
|
||||
return
|
||||
}
|
||||
if w.Hourly {
|
||||
if !info.IsDir() && info.ModTime().Add(1*time.Hour*time.Duration(w.MaxHours)).Before(time.Now()) {
|
||||
if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) &&
|
||||
strings.HasSuffix(filepath.Base(path), w.suffix) {
|
||||
os.Remove(path)
|
||||
}
|
||||
}
|
||||
} else if w.Daily {
|
||||
if !info.IsDir() && info.ModTime().Add(24*time.Hour*time.Duration(w.MaxDays)).Before(time.Now()) {
|
||||
if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) &&
|
||||
strings.HasSuffix(filepath.Base(path), w.suffix) {
|
||||
os.Remove(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Destroy close the file description, close file writer.
|
||||
func (w *fileLogWriter) Destroy() {
|
||||
w.fileWriter.Close()
|
||||
}
|
||||
|
||||
// Flush flushes file logger.
|
||||
// there are no buffering messages in file logger in memory.
|
||||
// flush file means sync file from disk.
|
||||
func (w *fileLogWriter) Flush() {
|
||||
w.fileWriter.Sync()
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(AdapterFile, newFileWriter)
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
// Copyright 2014 beego Author. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilePerm(t *testing.T) {
|
||||
log := NewLogger(10000)
|
||||
// use 0666 as test perm cause the default umask is 022
|
||||
log.SetLogger("file", `{"filename":"test.log", "perm": "0666"}`)
|
||||
log.Debug("debug")
|
||||
log.Informational("info")
|
||||
log.Notice("notice")
|
||||
log.Warning("warning")
|
||||
log.Error("error")
|
||||
log.Alert("alert")
|
||||
log.Critical("critical")
|
||||
log.Emergency("emergency")
|
||||
file, err := os.Stat("test.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if file.Mode() != 0o666 {
|
||||
t.Fatal("unexpected log file permission")
|
||||
}
|
||||
os.Remove("test.log")
|
||||
}
|
||||
|
||||
func TestFileWithPrefixPath(t *testing.T) {
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("file", `{"filename":"log/test.log"}`)
|
||||
log.Debug("debug")
|
||||
log.Informational("info")
|
||||
log.Notice("notice")
|
||||
log.Warning("warning")
|
||||
log.Error("error")
|
||||
log.Alert("alert")
|
||||
log.Critical("critical")
|
||||
log.Emergency("emergency")
|
||||
_, err := os.Stat("log/test.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Remove("log/test.log")
|
||||
os.Remove("log")
|
||||
}
|
||||
|
||||
func TestFilePermWithPrefixPath(t *testing.T) {
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("file", `{"filename":"mylogpath/test.log", "perm": "0220", "dirperm": "0770"}`)
|
||||
log.Debug("debug")
|
||||
log.Informational("info")
|
||||
log.Notice("notice")
|
||||
log.Warning("warning")
|
||||
log.Error("error")
|
||||
log.Alert("alert")
|
||||
log.Critical("critical")
|
||||
log.Emergency("emergency")
|
||||
|
||||
dir, err := os.Stat("mylogpath")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !dir.IsDir() {
|
||||
t.Fatal("mylogpath expected to be a directory")
|
||||
}
|
||||
|
||||
file, err := os.Stat("mylogpath/test.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if file.Mode() != 0o0220 {
|
||||
t.Fatal("unexpected file permission")
|
||||
}
|
||||
|
||||
os.Remove("mylogpath/test.log")
|
||||
os.Remove("mylogpath")
|
||||
}
|
||||
|
||||
func TestFile1(t *testing.T) {
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("file", `{"filename":"test.log"}`)
|
||||
log.Debug("debug")
|
||||
log.Informational("info")
|
||||
log.Notice("notice")
|
||||
log.Warning("warning")
|
||||
log.Error("error")
|
||||
log.Alert("alert")
|
||||
log.Critical("critical")
|
||||
log.Emergency("emergency")
|
||||
f, err := os.Open("test.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := bufio.NewReader(f)
|
||||
lineNum := 0
|
||||
for {
|
||||
line, _, err := b.ReadLine()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if len(line) > 0 {
|
||||
lineNum++
|
||||
}
|
||||
}
|
||||
expected := LevelDebug + 1
|
||||
if lineNum != expected {
|
||||
t.Fatal(lineNum, "not "+strconv.Itoa(expected)+" lines")
|
||||
}
|
||||
os.Remove("test.log")
|
||||
}
|
||||
|
||||
func TestFile2(t *testing.T) {
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("file", fmt.Sprintf(`{"filename":"test2.log","level":%d}`, LevelError))
|
||||
log.Debug("debug")
|
||||
log.Info("info")
|
||||
log.Notice("notice")
|
||||
log.Warning("warning")
|
||||
log.Error("error")
|
||||
log.Alert("alert")
|
||||
log.Critical("critical")
|
||||
log.Emergency("emergency")
|
||||
f, err := os.Open("test2.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := bufio.NewReader(f)
|
||||
lineNum := 0
|
||||
for {
|
||||
line, _, err := b.ReadLine()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if len(line) > 0 {
|
||||
lineNum++
|
||||
}
|
||||
}
|
||||
expected := LevelError + 1
|
||||
if lineNum != expected {
|
||||
t.Fatal(lineNum, "not "+strconv.Itoa(expected)+" lines")
|
||||
}
|
||||
os.Remove("test2.log")
|
||||
}
|
||||
|
||||
func TestFileDailyRotate_01(t *testing.T) {
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("file", `{"filename":"test3.log","maxlines":4}`)
|
||||
log.Debug("debug")
|
||||
log.Info("info")
|
||||
log.Notice("notice")
|
||||
log.Warning("warning")
|
||||
log.Error("error")
|
||||
log.Alert("alert")
|
||||
log.Critical("critical")
|
||||
log.Emergency("emergency")
|
||||
rotateName := "test3" + fmt.Sprintf(".%s.%03d", time.Now().Format("2006-01-02"), 1) + ".log"
|
||||
b, err := exists(rotateName)
|
||||
if !b || err != nil {
|
||||
os.Remove("test3.log")
|
||||
t.Fatal("rotate not generated")
|
||||
}
|
||||
os.Remove(rotateName)
|
||||
os.Remove("test3.log")
|
||||
}
|
||||
|
||||
func TestFileDailyRotate_02(t *testing.T) {
|
||||
fn1 := "rotate_day.log"
|
||||
fn2 := "rotate_day." + time.Now().Add(-24*time.Hour).Format("2006-01-02") + ".001.log"
|
||||
testFileRotate(t, fn1, fn2, true, false)
|
||||
}
|
||||
|
||||
func TestFileDailyRotate_03(t *testing.T) {
|
||||
fn1 := "rotate_day.log"
|
||||
fn := "rotate_day." + time.Now().Add(-24*time.Hour).Format("2006-01-02") + ".log"
|
||||
os.Create(fn)
|
||||
fn2 := "rotate_day." + time.Now().Add(-24*time.Hour).Format("2006-01-02") + ".001.log"
|
||||
testFileRotate(t, fn1, fn2, true, false)
|
||||
os.Remove(fn)
|
||||
}
|
||||
|
||||
func TestFileDailyRotate_04(t *testing.T) {
|
||||
fn1 := "rotate_day.log"
|
||||
fn2 := "rotate_day." + time.Now().Add(-24*time.Hour).Format("2006-01-02") + ".001.log"
|
||||
testFileDailyRotate(t, fn1, fn2)
|
||||
}
|
||||
|
||||
func TestFileDailyRotate_05(t *testing.T) {
|
||||
fn1 := "rotate_day.log"
|
||||
fn := "rotate_day." + time.Now().Add(-24*time.Hour).Format("2006-01-02") + ".log"
|
||||
os.Create(fn)
|
||||
fn2 := "rotate_day." + time.Now().Add(-24*time.Hour).Format("2006-01-02") + ".001.log"
|
||||
testFileDailyRotate(t, fn1, fn2)
|
||||
os.Remove(fn)
|
||||
}
|
||||
|
||||
func TestFileDailyRotate_06(t *testing.T) { // test file mode
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("file", `{"filename":"test3.log","maxlines":4}`)
|
||||
log.Debug("debug")
|
||||
log.Info("info")
|
||||
log.Notice("notice")
|
||||
log.Warning("warning")
|
||||
log.Error("error")
|
||||
log.Alert("alert")
|
||||
log.Critical("critical")
|
||||
log.Emergency("emergency")
|
||||
rotateName := "test3" + fmt.Sprintf(".%s.%03d", time.Now().Format("2006-01-02"), 1) + ".log"
|
||||
s, _ := os.Lstat(rotateName)
|
||||
if s.Mode() != 0o440 {
|
||||
os.Remove(rotateName)
|
||||
os.Remove("test3.log")
|
||||
t.Fatal("rotate file mode error")
|
||||
}
|
||||
os.Remove(rotateName)
|
||||
os.Remove("test3.log")
|
||||
}
|
||||
|
||||
func TestFileHourlyRotate_01(t *testing.T) {
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("file", `{"filename":"test3.log","hourly":true,"maxlines":4}`)
|
||||
log.Debug("debug")
|
||||
log.Info("info")
|
||||
log.Notice("notice")
|
||||
log.Warning("warning")
|
||||
log.Error("error")
|
||||
log.Alert("alert")
|
||||
log.Critical("critical")
|
||||
log.Emergency("emergency")
|
||||
rotateName := "test3" + fmt.Sprintf(".%s.%03d", time.Now().Format("2006010215"), 1) + ".log"
|
||||
b, err := exists(rotateName)
|
||||
if !b || err != nil {
|
||||
os.Remove("test3.log")
|
||||
t.Fatal("rotate not generated")
|
||||
}
|
||||
os.Remove(rotateName)
|
||||
os.Remove("test3.log")
|
||||
}
|
||||
|
||||
func TestFileHourlyRotate_02(t *testing.T) {
|
||||
fn1 := "rotate_hour.log"
|
||||
fn2 := "rotate_hour." + time.Now().Add(-1*time.Hour).Format("2006010215") + ".001.log"
|
||||
testFileRotate(t, fn1, fn2, false, true)
|
||||
}
|
||||
|
||||
func TestFileHourlyRotate_03(t *testing.T) {
|
||||
fn1 := "rotate_hour.log"
|
||||
fn := "rotate_hour." + time.Now().Add(-1*time.Hour).Format("2006010215") + ".log"
|
||||
os.Create(fn)
|
||||
fn2 := "rotate_hour." + time.Now().Add(-1*time.Hour).Format("2006010215") + ".001.log"
|
||||
testFileRotate(t, fn1, fn2, false, true)
|
||||
os.Remove(fn)
|
||||
}
|
||||
|
||||
func TestFileHourlyRotate_04(t *testing.T) {
|
||||
fn1 := "rotate_hour.log"
|
||||
fn2 := "rotate_hour." + time.Now().Add(-1*time.Hour).Format("2006010215") + ".001.log"
|
||||
testFileHourlyRotate(t, fn1, fn2)
|
||||
}
|
||||
|
||||
func TestFileHourlyRotate_05(t *testing.T) {
|
||||
fn1 := "rotate_hour.log"
|
||||
fn := "rotate_hour." + time.Now().Add(-1*time.Hour).Format("2006010215") + ".log"
|
||||
os.Create(fn)
|
||||
fn2 := "rotate_hour." + time.Now().Add(-1*time.Hour).Format("2006010215") + ".001.log"
|
||||
testFileHourlyRotate(t, fn1, fn2)
|
||||
os.Remove(fn)
|
||||
}
|
||||
|
||||
func TestFileHourlyRotate_06(t *testing.T) { // test file mode
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("file", `{"filename":"test3.log", "hourly":true, "maxlines":4}`)
|
||||
log.Debug("debug")
|
||||
log.Info("info")
|
||||
log.Notice("notice")
|
||||
log.Warning("warning")
|
||||
log.Error("error")
|
||||
log.Alert("alert")
|
||||
log.Critical("critical")
|
||||
log.Emergency("emergency")
|
||||
rotateName := "test3" + fmt.Sprintf(".%s.%03d", time.Now().Format("2006010215"), 1) + ".log"
|
||||
s, _ := os.Lstat(rotateName)
|
||||
if s.Mode() != 0o440 {
|
||||
os.Remove(rotateName)
|
||||
os.Remove("test3.log")
|
||||
t.Fatal("rotate file mode error")
|
||||
}
|
||||
os.Remove(rotateName)
|
||||
os.Remove("test3.log")
|
||||
}
|
||||
|
||||
func testFileRotate(t *testing.T, fn1, fn2 string, daily, hourly bool) {
|
||||
fw := &fileLogWriter{
|
||||
Daily: daily,
|
||||
MaxDays: 7,
|
||||
Hourly: hourly,
|
||||
MaxHours: 168,
|
||||
Rotate: true,
|
||||
Level: LevelTrace,
|
||||
Perm: "0660",
|
||||
DirPerm: "0770",
|
||||
RotatePerm: "0440",
|
||||
}
|
||||
fw.logFormatter = fw
|
||||
|
||||
if fw.Daily {
|
||||
fw.Init(fmt.Sprintf(`{"filename":"%v","maxdays":1}`, fn1))
|
||||
fw.dailyOpenTime = time.Now().Add(-24 * time.Hour)
|
||||
fw.dailyOpenDate = fw.dailyOpenTime.Day()
|
||||
}
|
||||
|
||||
if fw.Hourly {
|
||||
fw.Init(fmt.Sprintf(`{"filename":"%v","maxhours":1}`, fn1))
|
||||
fw.hourlyOpenTime = time.Now().Add(-1 * time.Hour)
|
||||
fw.hourlyOpenDate = fw.hourlyOpenTime.Day()
|
||||
}
|
||||
lm := &LogMsg{
|
||||
Msg: "Test message",
|
||||
Level: LevelDebug,
|
||||
When: time.Now(),
|
||||
}
|
||||
|
||||
fw.WriteMsg(lm)
|
||||
|
||||
for _, file := range []string{fn1, fn2} {
|
||||
_, err := os.Stat(file)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.FailNow()
|
||||
}
|
||||
os.Remove(file)
|
||||
}
|
||||
fw.Destroy()
|
||||
}
|
||||
|
||||
func testFileDailyRotate(t *testing.T, fn1, fn2 string) {
|
||||
fw := &fileLogWriter{
|
||||
Daily: true,
|
||||
MaxDays: 7,
|
||||
Rotate: true,
|
||||
Level: LevelTrace,
|
||||
Perm: "0660",
|
||||
DirPerm: "0770",
|
||||
RotatePerm: "0440",
|
||||
}
|
||||
fw.logFormatter = fw
|
||||
|
||||
fw.Init(fmt.Sprintf(`{"filename":"%v","maxdays":1}`, fn1))
|
||||
fw.dailyOpenTime = time.Now().Add(-24 * time.Hour)
|
||||
fw.dailyOpenDate = fw.dailyOpenTime.Day()
|
||||
today, _ := time.ParseInLocation("2006-01-02", time.Now().Format("2006-01-02"), fw.dailyOpenTime.Location())
|
||||
today = today.Add(-1 * time.Second)
|
||||
fw.dailyRotate(today)
|
||||
for _, file := range []string{fn1, fn2} {
|
||||
_, err := os.Stat(file)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if len(content) > 0 {
|
||||
t.FailNow()
|
||||
}
|
||||
os.Remove(file)
|
||||
}
|
||||
fw.Destroy()
|
||||
}
|
||||
|
||||
func testFileHourlyRotate(t *testing.T, fn1, fn2 string) {
|
||||
fw := &fileLogWriter{
|
||||
Hourly: true,
|
||||
MaxHours: 168,
|
||||
Rotate: true,
|
||||
Level: LevelTrace,
|
||||
Perm: "0660",
|
||||
DirPerm: "0770",
|
||||
RotatePerm: "0440",
|
||||
}
|
||||
|
||||
fw.logFormatter = fw
|
||||
fw.Init(fmt.Sprintf(`{"filename":"%v","maxhours":1}`, fn1))
|
||||
fw.hourlyOpenTime = time.Now().Add(-1 * time.Hour)
|
||||
fw.hourlyOpenDate = fw.hourlyOpenTime.Hour()
|
||||
hour, _ := time.ParseInLocation("2006010215", time.Now().Format("2006010215"), fw.hourlyOpenTime.Location())
|
||||
hour = hour.Add(-1 * time.Second)
|
||||
fw.hourlyRotate(hour)
|
||||
for _, file := range []string{fn1, fn2} {
|
||||
_, err := os.Stat(file)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if len(content) > 0 {
|
||||
t.FailNow()
|
||||
}
|
||||
os.Remove(file)
|
||||
}
|
||||
fw.Destroy()
|
||||
}
|
||||
|
||||
func exists(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
func BenchmarkFile(b *testing.B) {
|
||||
log := NewLogger(100000)
|
||||
log.SetLogger("file", `{"filename":"test4.log"}`)
|
||||
for i := 0; i < b.N; i++ {
|
||||
log.Debug("debug")
|
||||
}
|
||||
os.Remove("test4.log")
|
||||
}
|
||||
|
||||
func BenchmarkFileAsynchronous(b *testing.B) {
|
||||
log := NewLogger(100000)
|
||||
log.SetLogger("file", `{"filename":"test4.log"}`)
|
||||
log.Async()
|
||||
for i := 0; i < b.N; i++ {
|
||||
log.Debug("debug")
|
||||
}
|
||||
os.Remove("test4.log")
|
||||
}
|
||||
|
||||
func BenchmarkFileCallDepth(b *testing.B) {
|
||||
log := NewLogger(100000)
|
||||
log.SetLogger("file", `{"filename":"test4.log"}`)
|
||||
log.EnableFuncCallDepth(true)
|
||||
log.SetLogFuncCallDepth(2)
|
||||
for i := 0; i < b.N; i++ {
|
||||
log.Debug("debug")
|
||||
}
|
||||
os.Remove("test4.log")
|
||||
}
|
||||
|
||||
func BenchmarkFileAsynchronousCallDepth(b *testing.B) {
|
||||
log := NewLogger(100000)
|
||||
log.SetLogger("file", `{"filename":"test4.log"}`)
|
||||
log.EnableFuncCallDepth(true)
|
||||
log.SetLogFuncCallDepth(2)
|
||||
log.Async()
|
||||
for i := 0; i < b.N; i++ {
|
||||
log.Debug("debug")
|
||||
}
|
||||
os.Remove("test4.log")
|
||||
}
|
||||
|
||||
func BenchmarkFileOnGoroutine(b *testing.B) {
|
||||
log := NewLogger(100000)
|
||||
log.SetLogger("file", `{"filename":"test4.log"}`)
|
||||
for i := 0; i < b.N; i++ {
|
||||
go log.Debug("debug")
|
||||
}
|
||||
os.Remove("test4.log")
|
||||
}
|
||||
|
||||
func TestFileLogWriter_Format(t *testing.T) {
|
||||
lg := &LogMsg{
|
||||
Level: LevelDebug,
|
||||
Msg: "Hello, world",
|
||||
When: time.Date(2020, 9, 19, 20, 12, 37, 9, time.UTC),
|
||||
FilePath: "/user/home/main.go",
|
||||
LineNumber: 13,
|
||||
Prefix: "Cus",
|
||||
}
|
||||
|
||||
fw := newFileWriter().(*fileLogWriter)
|
||||
res := fw.Format(lg)
|
||||
assert.Equal(t, "2020/09/19 20:12:37.000 [D] Cus Hello, world\n", res)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright 2020
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var formatterMap = make(map[string]LogFormatter, 4)
|
||||
|
||||
type LogFormatter interface {
|
||||
Format(lm *LogMsg) string
|
||||
}
|
||||
|
||||
// PatternLogFormatter provides a quick format method
|
||||
// for example:
|
||||
// tes := &PatternLogFormatter{Pattern: "%F:%n|%w %t>> %m", WhenFormat: "2006-01-02"}
|
||||
// RegisterFormatter("tes", tes)
|
||||
// SetGlobalFormatter("tes")
|
||||
type PatternLogFormatter struct {
|
||||
Pattern string
|
||||
WhenFormat string
|
||||
}
|
||||
|
||||
func (p *PatternLogFormatter) getWhenFormatter() string {
|
||||
s := p.WhenFormat
|
||||
if s == "" {
|
||||
s = "2006/01/02 15:04:05.123" // default style
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *PatternLogFormatter) Format(lm *LogMsg) string {
|
||||
return p.ToString(lm)
|
||||
}
|
||||
|
||||
// RegisterFormatter register an formatter. Usually you should use this to extend your custom formatter
|
||||
// for example:
|
||||
// RegisterFormatter("my-fmt", &MyFormatter{})
|
||||
// logs.SetFormatter(Console, `{"formatter": "my-fmt"}`)
|
||||
func RegisterFormatter(name string, fmtr LogFormatter) {
|
||||
formatterMap[name] = fmtr
|
||||
}
|
||||
|
||||
func GetFormatter(name string) (LogFormatter, bool) {
|
||||
res, ok := formatterMap[name]
|
||||
return res, ok
|
||||
}
|
||||
|
||||
// ToString 'w' when, 'm' msg,'f' filename,'F' full path,'n' line number
|
||||
// 'l' level number, 't' prefix of level type, 'T' full name of level type
|
||||
func (p *PatternLogFormatter) ToString(lm *LogMsg) string {
|
||||
s := []rune(p.Pattern)
|
||||
msg := fmt.Sprintf(lm.Msg, lm.Args...)
|
||||
m := map[rune]string{
|
||||
'w': lm.When.Format(p.getWhenFormatter()),
|
||||
'm': msg,
|
||||
'n': strconv.Itoa(lm.LineNumber),
|
||||
'l': strconv.Itoa(lm.Level),
|
||||
't': levelPrefix[lm.Level],
|
||||
'T': levelNames[lm.Level],
|
||||
'F': lm.FilePath,
|
||||
}
|
||||
_, m['f'] = path.Split(lm.FilePath)
|
||||
res := ""
|
||||
for i := 0; i < len(s)-1; i++ {
|
||||
if s[i] == '%' {
|
||||
if k, ok := m[s[i+1]]; ok {
|
||||
res += k
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
res += string(s[i])
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright 2020
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type CustomFormatter struct{}
|
||||
|
||||
func (*CustomFormatter) Format(lm *LogMsg) string {
|
||||
return "hello, msg: " + lm.Msg
|
||||
}
|
||||
|
||||
type TestLogger struct {
|
||||
Formatter string `json:"formatter"`
|
||||
Expected string
|
||||
formatter LogFormatter
|
||||
}
|
||||
|
||||
func (t *TestLogger) Init(config string) error {
|
||||
er := json.Unmarshal([]byte(config), t)
|
||||
t.formatter, _ = GetFormatter(t.Formatter)
|
||||
return er
|
||||
}
|
||||
|
||||
func (t *TestLogger) WriteMsg(lm *LogMsg) error {
|
||||
msg := t.formatter.Format(lm)
|
||||
if msg != t.Expected {
|
||||
return errors.New("not equal")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*TestLogger) Destroy() {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (*TestLogger) Flush() {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (*TestLogger) SetFormatter(_ LogFormatter) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func TestCustomFormatter(t *testing.T) {
|
||||
RegisterFormatter("custom", &CustomFormatter{})
|
||||
tl := &TestLogger{
|
||||
Expected: "hello, msg: world",
|
||||
}
|
||||
assert.Nil(t, tl.Init(`{"formatter": "custom"}`))
|
||||
assert.Nil(t, tl.WriteMsg(&LogMsg{
|
||||
Msg: "world",
|
||||
}))
|
||||
}
|
||||
|
||||
func TestPatternLogFormatter(t *testing.T) {
|
||||
tes := &PatternLogFormatter{
|
||||
Pattern: "%F:%n|%w%t>> %m",
|
||||
WhenFormat: "2006-01-02",
|
||||
}
|
||||
when, _ := time.Parse(tes.WhenFormat, "2022-04-17")
|
||||
testCases := []struct {
|
||||
msg *LogMsg
|
||||
want string
|
||||
}{
|
||||
{
|
||||
msg: &LogMsg{
|
||||
Msg: "hello %s",
|
||||
FilePath: "/User/go/beego/main.go",
|
||||
Level: LevelWarn,
|
||||
LineNumber: 10,
|
||||
When: when,
|
||||
Args: []interface{}{"world"},
|
||||
},
|
||||
want: "/User/go/beego/main.go:10|2022-04-17[W]>> hello world",
|
||||
},
|
||||
{
|
||||
msg: &LogMsg{
|
||||
Msg: "hello",
|
||||
FilePath: "/User/go/beego/main.go",
|
||||
Level: LevelWarn,
|
||||
LineNumber: 10,
|
||||
When: when,
|
||||
},
|
||||
want: "/User/go/beego/main.go:10|2022-04-17[W]>> hello",
|
||||
},
|
||||
{
|
||||
msg: &LogMsg{},
|
||||
want: ":0|0001-01-01[M]>> ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
got := tes.ToString(tc.msg)
|
||||
assert.Equal(t, tc.want, got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package logs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// JLWriter implements beego LoggerInterface and is used to send jiaoliao webhook
|
||||
type JLWriter struct {
|
||||
AuthorName string `json:"authorname"`
|
||||
Title string `json:"title"`
|
||||
WebhookURL string `json:"webhookurl"`
|
||||
RedirectURL string `json:"redirecturl,omitempty"`
|
||||
ImageURL string `json:"imageurl,omitempty"`
|
||||
Level int `json:"level"`
|
||||
|
||||
formatter LogFormatter
|
||||
Formatter string `json:"formatter"`
|
||||
}
|
||||
|
||||
// newJLWriter creates jiaoliao writer.
|
||||
func newJLWriter() Logger {
|
||||
res := &JLWriter{Level: LevelTrace}
|
||||
res.formatter = res
|
||||
return res
|
||||
}
|
||||
|
||||
// Init JLWriter with json config string
|
||||
func (s *JLWriter) Init(config string) error {
|
||||
res := json.Unmarshal([]byte(config), s)
|
||||
if res == nil && len(s.Formatter) > 0 {
|
||||
fmtr, ok := GetFormatter(s.Formatter)
|
||||
if !ok {
|
||||
return errors.New(fmt.Sprintf("the formatter with name: %s not found", s.Formatter))
|
||||
}
|
||||
s.formatter = fmtr
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *JLWriter) Format(lm *LogMsg) string {
|
||||
msg := lm.OldStyleFormat()
|
||||
msg = fmt.Sprintf("%s %s", lm.When.Format("2006-01-02 15:04:05"), msg)
|
||||
return msg
|
||||
}
|
||||
|
||||
func (s *JLWriter) SetFormatter(f LogFormatter) {
|
||||
s.formatter = f
|
||||
}
|
||||
|
||||
// WriteMsg writes message in smtp writer.
|
||||
// Sends an email with subject and only this message.
|
||||
func (s *JLWriter) WriteMsg(lm *LogMsg) error {
|
||||
if lm.Level > s.Level {
|
||||
return nil
|
||||
}
|
||||
|
||||
text := s.formatter.Format(lm)
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("authorName", s.AuthorName)
|
||||
form.Add("title", s.Title)
|
||||
form.Add("text", text)
|
||||
if s.RedirectURL != "" {
|
||||
form.Add("redirectUrl", s.RedirectURL)
|
||||
}
|
||||
if s.ImageURL != "" {
|
||||
form.Add("imageUrl", s.ImageURL)
|
||||
}
|
||||
|
||||
resp, err := http.PostForm(s.WebhookURL, form)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("Post webhook failed %s %d", resp.Status, resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush implementing method. empty.
|
||||
func (s *JLWriter) Flush() {
|
||||
}
|
||||
|
||||
// Destroy implementing method. empty.
|
||||
func (s *JLWriter) Destroy() {
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(AdapterJianLiao, newJLWriter)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2020
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestJLWriter_Format(t *testing.T) {
|
||||
lg := &LogMsg{
|
||||
Level: LevelDebug,
|
||||
Msg: "Hello, world",
|
||||
When: time.Date(2020, 9, 19, 20, 12, 37, 9, time.UTC),
|
||||
FilePath: "/user/home/main.go",
|
||||
LineNumber: 13,
|
||||
Prefix: "Cus",
|
||||
}
|
||||
jl := newJLWriter().(*JLWriter)
|
||||
res := jl.Format(lg)
|
||||
assert.Equal(t, "2020-09-19 20:12:37 [D] Cus Hello, world", res)
|
||||
}
|
||||
@@ -0,0 +1,810 @@
|
||||
// Copyright 2014 beego Author. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package logs provide a general log interface
|
||||
// Usage:
|
||||
//
|
||||
// import "github.com/cdle/sillyplus/core/logs"
|
||||
//
|
||||
// log := NewLogger(10000)
|
||||
// log.SetLogger("console", "")
|
||||
//
|
||||
// > the first params stand for how many channel
|
||||
//
|
||||
// Use it like this:
|
||||
//
|
||||
// log.Trace("trace")
|
||||
// log.Info("info")
|
||||
// log.Warn("warning")
|
||||
// log.Debug("debug")
|
||||
// log.Critical("critical")
|
||||
package logs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RFC5424 log message levels.
|
||||
const (
|
||||
LevelEmergency = iota
|
||||
LevelAlert
|
||||
LevelCritical
|
||||
LevelError
|
||||
LevelWarning
|
||||
LevelNotice
|
||||
LevelInformational
|
||||
LevelDebug
|
||||
)
|
||||
|
||||
// levelLogLogger is defined to implement log.Logger
|
||||
// the real log level will be LevelEmergency
|
||||
const levelLoggerImpl = -1
|
||||
|
||||
// Name for adapter with beego official support
|
||||
const (
|
||||
AdapterConsole = "console"
|
||||
AdapterFile = "file"
|
||||
AdapterMultiFile = "multifile"
|
||||
AdapterMail = "smtp"
|
||||
AdapterConn = "conn"
|
||||
AdapterEs = "es"
|
||||
AdapterJianLiao = "jianliao"
|
||||
AdapterSlack = "slack"
|
||||
AdapterAliLS = "alils"
|
||||
)
|
||||
|
||||
// Legacy log level constants to ensure backwards compatibility.
|
||||
const (
|
||||
LevelInfo = LevelInformational
|
||||
LevelTrace = LevelDebug
|
||||
LevelWarn = LevelWarning
|
||||
)
|
||||
|
||||
type newLoggerFunc func() Logger
|
||||
|
||||
// Logger defines the behavior of a log provider.
|
||||
type Logger interface {
|
||||
Init(config string) error
|
||||
WriteMsg(lm *LogMsg) error
|
||||
Destroy()
|
||||
Flush()
|
||||
SetFormatter(f LogFormatter)
|
||||
}
|
||||
|
||||
var (
|
||||
adapters = make(map[string]newLoggerFunc)
|
||||
levelPrefix = [LevelDebug + 1]string{"[M]", "[A]", "[C]", "[E]", "[W]", "[N]", "[I]", "[D]"}
|
||||
)
|
||||
|
||||
// Register makes a log provide available by the provided name.
|
||||
// If Register is called twice with the same name or if driver is nil,
|
||||
// it panics.
|
||||
func Register(name string, log newLoggerFunc) {
|
||||
if log == nil {
|
||||
panic("logs: Register provide is nil")
|
||||
}
|
||||
if _, dup := adapters[name]; dup {
|
||||
panic("logs: Register called twice for provider " + name)
|
||||
}
|
||||
adapters[name] = log
|
||||
}
|
||||
|
||||
// BeeLogger is default logger in beego application.
|
||||
// Can contain several providers and log message into all providers.
|
||||
type BeeLogger struct {
|
||||
lock sync.Mutex
|
||||
init bool
|
||||
enableFuncCallDepth bool
|
||||
enableFullFilePath bool
|
||||
asynchronous bool
|
||||
// Whether to discard logs when buffer is full and asynchronous is true
|
||||
// No discard by default
|
||||
logWithNonBlocking bool
|
||||
wg sync.WaitGroup
|
||||
level int
|
||||
loggerFuncCallDepth int
|
||||
prefix string
|
||||
msgChanLen int64
|
||||
msgChan chan *LogMsg
|
||||
closeChan chan struct{}
|
||||
flushChan chan struct{}
|
||||
outputs []*nameLogger
|
||||
globalFormatter string
|
||||
}
|
||||
|
||||
const defaultAsyncMsgLen = 1e3
|
||||
|
||||
type nameLogger struct {
|
||||
Logger
|
||||
name string
|
||||
}
|
||||
|
||||
var logMsgPool *sync.Pool
|
||||
|
||||
// NewLogger returns a new BeeLogger.
|
||||
// channelLen: the number of messages in chan(used where asynchronous is true).
|
||||
// if the buffering chan is full, logger adapters write to file or other way.
|
||||
func NewLogger(channelLens ...int64) *BeeLogger {
|
||||
bl := new(BeeLogger)
|
||||
bl.level = LevelDebug
|
||||
bl.loggerFuncCallDepth = 3
|
||||
bl.msgChanLen = append(channelLens, 0)[0]
|
||||
if bl.msgChanLen <= 0 {
|
||||
bl.msgChanLen = defaultAsyncMsgLen
|
||||
}
|
||||
bl.flushChan = make(chan struct{}, 1)
|
||||
bl.closeChan = make(chan struct{}, 1)
|
||||
bl.setLogger(AdapterConsole)
|
||||
return bl
|
||||
}
|
||||
|
||||
// Async sets the log to asynchronous and start the goroutine
|
||||
func (bl *BeeLogger) Async(msgLen ...int64) *BeeLogger {
|
||||
bl.lock.Lock()
|
||||
defer bl.lock.Unlock()
|
||||
if bl.asynchronous {
|
||||
return bl
|
||||
}
|
||||
bl.asynchronous = true
|
||||
if len(msgLen) > 0 && msgLen[0] > 0 {
|
||||
bl.msgChanLen = msgLen[0]
|
||||
}
|
||||
bl.msgChan = make(chan *LogMsg, bl.msgChanLen)
|
||||
logMsgPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &LogMsg{}
|
||||
},
|
||||
}
|
||||
bl.wg.Add(1)
|
||||
go bl.startLogger()
|
||||
return bl
|
||||
}
|
||||
|
||||
// AsyncNonBlockWrite Non-blocking write in asynchronous mode
|
||||
// Only works if asynchronous write logging is set
|
||||
func (bl *BeeLogger) AsyncNonBlockWrite() *BeeLogger {
|
||||
if !bl.asynchronous {
|
||||
return bl
|
||||
}
|
||||
bl.logWithNonBlocking = true
|
||||
return bl
|
||||
}
|
||||
|
||||
// SetLogger provides a given logger adapter into BeeLogger with config string.
|
||||
// config must in in JSON format like {"interval":360}}
|
||||
func (bl *BeeLogger) setLogger(adapterName string, configs ...string) error {
|
||||
config := append(configs, "{}")[0]
|
||||
for _, l := range bl.outputs {
|
||||
if l.name == adapterName {
|
||||
return fmt.Errorf("logs: duplicate adaptername %q (you have set this logger before)", adapterName)
|
||||
}
|
||||
}
|
||||
|
||||
logAdapter, ok := adapters[adapterName]
|
||||
if !ok {
|
||||
return fmt.Errorf("logs: unknown adaptername %q (forgotten Register?)", adapterName)
|
||||
}
|
||||
|
||||
lg := logAdapter()
|
||||
|
||||
err := lg.Init(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Global formatter overrides the default set formatter
|
||||
if len(bl.globalFormatter) > 0 {
|
||||
fmtr, ok := GetFormatter(bl.globalFormatter)
|
||||
if !ok {
|
||||
return fmt.Errorf("the formatter with name: %s not found", bl.globalFormatter)
|
||||
}
|
||||
lg.SetFormatter(fmtr)
|
||||
}
|
||||
|
||||
bl.outputs = append(bl.outputs, &nameLogger{name: adapterName, Logger: lg})
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLogger provides a given logger adapter into BeeLogger with config string.
|
||||
// config must in in JSON format like {"interval":360}}
|
||||
func (bl *BeeLogger) SetLogger(adapterName string, configs ...string) error {
|
||||
bl.lock.Lock()
|
||||
defer bl.lock.Unlock()
|
||||
if !bl.init {
|
||||
bl.outputs = []*nameLogger{}
|
||||
bl.init = true
|
||||
}
|
||||
return bl.setLogger(adapterName, configs...)
|
||||
}
|
||||
|
||||
// DelLogger removes a logger adapter in BeeLogger.
|
||||
func (bl *BeeLogger) DelLogger(adapterName string) error {
|
||||
bl.lock.Lock()
|
||||
defer bl.lock.Unlock()
|
||||
outputs := make([]*nameLogger, 0, len(bl.outputs))
|
||||
for _, lg := range bl.outputs {
|
||||
if lg.name == adapterName {
|
||||
lg.Destroy()
|
||||
} else {
|
||||
outputs = append(outputs, lg)
|
||||
}
|
||||
}
|
||||
if len(outputs) == len(bl.outputs) {
|
||||
return fmt.Errorf("logs: unknown adaptername %q (forgotten Register?)", adapterName)
|
||||
}
|
||||
bl.outputs = outputs
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bl *BeeLogger) writeToLoggers(lm *LogMsg) {
|
||||
for _, l := range bl.outputs {
|
||||
err := l.WriteMsg(lm)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unable to WriteMsg to adapter:%v,error:%v\n", l.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bl *BeeLogger) Write(p []byte) (n int, err error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
// writeMsg will always add a '\n' character
|
||||
if p[len(p)-1] == '\n' {
|
||||
p = p[0 : len(p)-1]
|
||||
}
|
||||
lm := &LogMsg{
|
||||
Msg: string(p),
|
||||
Level: levelLoggerImpl,
|
||||
When: time.Now(),
|
||||
}
|
||||
|
||||
// set levelLoggerImpl to ensure all log message will be write out
|
||||
err = bl.writeMsg(lm)
|
||||
if err == nil {
|
||||
return len(p), nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
func (bl *BeeLogger) writeMsg(lm *LogMsg) error {
|
||||
if !bl.init {
|
||||
bl.lock.Lock()
|
||||
bl.setLogger(AdapterConsole)
|
||||
bl.lock.Unlock()
|
||||
}
|
||||
|
||||
var (
|
||||
file string
|
||||
line int
|
||||
ok bool
|
||||
)
|
||||
|
||||
_, file, line, ok = runtime.Caller(bl.loggerFuncCallDepth)
|
||||
if !ok {
|
||||
file = "???"
|
||||
line = 0
|
||||
}
|
||||
lm.FilePath = file
|
||||
lm.LineNumber = line
|
||||
lm.Prefix = bl.prefix
|
||||
|
||||
lm.enableFullFilePath = bl.enableFullFilePath
|
||||
lm.enableFuncCallDepth = bl.enableFuncCallDepth
|
||||
|
||||
// set level info in front of filename info
|
||||
if lm.Level == levelLoggerImpl {
|
||||
// set to emergency to ensure all log will be print out correctly
|
||||
lm.Level = LevelEmergency
|
||||
}
|
||||
|
||||
if bl.asynchronous {
|
||||
logM := logMsgPool.Get().(*LogMsg)
|
||||
logM.Level = lm.Level
|
||||
logM.Msg = lm.Msg
|
||||
logM.When = lm.When
|
||||
logM.Args = lm.Args
|
||||
logM.FilePath = lm.FilePath
|
||||
logM.LineNumber = lm.LineNumber
|
||||
logM.Prefix = lm.Prefix
|
||||
|
||||
if bl.outputs != nil {
|
||||
if bl.logWithNonBlocking {
|
||||
select {
|
||||
case bl.msgChan <- lm:
|
||||
// discard log when channel is full
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
bl.msgChan <- lm
|
||||
}
|
||||
} else {
|
||||
logMsgPool.Put(lm)
|
||||
}
|
||||
} else {
|
||||
bl.writeToLoggers(lm)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLevel sets log message level.
|
||||
// If message level (such as LevelDebug) is higher than logger level (such as LevelWarning),
|
||||
// log providers will not be sent the message.
|
||||
func (bl *BeeLogger) SetLevel(l int) {
|
||||
bl.level = l
|
||||
}
|
||||
|
||||
// GetLevel Get Current log message level.
|
||||
func (bl *BeeLogger) GetLevel() int {
|
||||
return bl.level
|
||||
}
|
||||
|
||||
// SetLogFuncCallDepth set log funcCallDepth
|
||||
func (bl *BeeLogger) SetLogFuncCallDepth(d int) {
|
||||
bl.loggerFuncCallDepth = d
|
||||
}
|
||||
|
||||
// GetLogFuncCallDepth return log funcCallDepth for wrapper
|
||||
func (bl *BeeLogger) GetLogFuncCallDepth() int {
|
||||
return bl.loggerFuncCallDepth
|
||||
}
|
||||
|
||||
// EnableFuncCallDepth enable log funcCallDepth
|
||||
func (bl *BeeLogger) EnableFuncCallDepth(b bool) {
|
||||
bl.enableFuncCallDepth = b
|
||||
}
|
||||
|
||||
// set prefix
|
||||
func (bl *BeeLogger) SetPrefix(s string) {
|
||||
bl.prefix = s
|
||||
}
|
||||
|
||||
// start logger chan reading.
|
||||
// when chan is not empty, write logs.
|
||||
func (bl *BeeLogger) startLogger() {
|
||||
gameOver := false
|
||||
for {
|
||||
select {
|
||||
case bm, ok := <-bl.msgChan:
|
||||
// this is a terrible design to have a signal channel that accept two inputs
|
||||
// so we only handle the msg if the channel is not closed
|
||||
if ok {
|
||||
bl.writeToLoggers(bm)
|
||||
logMsgPool.Put(bm)
|
||||
}
|
||||
case <-bl.closeChan:
|
||||
bl.flush()
|
||||
for _, l := range bl.outputs {
|
||||
l.Destroy()
|
||||
}
|
||||
bl.outputs = nil
|
||||
gameOver = true
|
||||
bl.wg.Done()
|
||||
case <-bl.flushChan:
|
||||
bl.flush()
|
||||
bl.wg.Done()
|
||||
}
|
||||
if gameOver {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bl *BeeLogger) setGlobalFormatter(fmtter string) error {
|
||||
bl.globalFormatter = fmtter
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetGlobalFormatter sets the global formatter for all log adapters
|
||||
// don't forget to register the formatter by invoking RegisterFormatter
|
||||
func SetGlobalFormatter(fmtter string) error {
|
||||
return beeLogger.setGlobalFormatter(fmtter)
|
||||
}
|
||||
|
||||
// Emergency Log EMERGENCY level message.
|
||||
func (bl *BeeLogger) Emergency(format string, v ...interface{}) {
|
||||
if LevelEmergency > bl.level {
|
||||
return
|
||||
}
|
||||
|
||||
lm := &LogMsg{
|
||||
Level: LevelEmergency,
|
||||
Msg: format,
|
||||
When: time.Now(),
|
||||
}
|
||||
if len(v) > 0 {
|
||||
lm.Msg = fmt.Sprintf(lm.Msg, v...)
|
||||
}
|
||||
|
||||
bl.writeMsg(lm)
|
||||
}
|
||||
|
||||
// Alert Log ALERT level message.
|
||||
func (bl *BeeLogger) Alert(format string, v ...interface{}) {
|
||||
if LevelAlert > bl.level {
|
||||
return
|
||||
}
|
||||
|
||||
lm := &LogMsg{
|
||||
Level: LevelAlert,
|
||||
Msg: format,
|
||||
When: time.Now(),
|
||||
Args: v,
|
||||
}
|
||||
bl.writeMsg(lm)
|
||||
}
|
||||
|
||||
// Critical Log CRITICAL level message.
|
||||
func (bl *BeeLogger) Critical(format string, v ...interface{}) {
|
||||
if LevelCritical > bl.level {
|
||||
return
|
||||
}
|
||||
lm := &LogMsg{
|
||||
Level: LevelCritical,
|
||||
Msg: format,
|
||||
When: time.Now(),
|
||||
Args: v,
|
||||
}
|
||||
|
||||
bl.writeMsg(lm)
|
||||
}
|
||||
|
||||
// Error Log ERROR level message.
|
||||
func (bl *BeeLogger) Error(format string, v ...interface{}) {
|
||||
if LevelError > bl.level {
|
||||
return
|
||||
}
|
||||
lm := &LogMsg{
|
||||
Level: LevelError,
|
||||
Msg: format,
|
||||
When: time.Now(),
|
||||
Args: v,
|
||||
}
|
||||
|
||||
bl.writeMsg(lm)
|
||||
}
|
||||
|
||||
// Warning Log WARNING level message.
|
||||
func (bl *BeeLogger) Warning(format string, v ...interface{}) {
|
||||
if LevelWarn > bl.level {
|
||||
return
|
||||
}
|
||||
lm := &LogMsg{
|
||||
Level: LevelWarn,
|
||||
Msg: format,
|
||||
When: time.Now(),
|
||||
Args: v,
|
||||
}
|
||||
|
||||
bl.writeMsg(lm)
|
||||
}
|
||||
|
||||
// Notice Log NOTICE level message.
|
||||
func (bl *BeeLogger) Notice(format string, v ...interface{}) {
|
||||
if LevelNotice > bl.level {
|
||||
return
|
||||
}
|
||||
lm := &LogMsg{
|
||||
Level: LevelNotice,
|
||||
Msg: format,
|
||||
When: time.Now(),
|
||||
Args: v,
|
||||
}
|
||||
|
||||
bl.writeMsg(lm)
|
||||
}
|
||||
|
||||
// Informational Log INFORMATIONAL level message.
|
||||
func (bl *BeeLogger) Informational(format string, v ...interface{}) {
|
||||
if LevelInfo > bl.level {
|
||||
return
|
||||
}
|
||||
lm := &LogMsg{
|
||||
Level: LevelInfo,
|
||||
Msg: format,
|
||||
When: time.Now(),
|
||||
Args: v,
|
||||
}
|
||||
|
||||
bl.writeMsg(lm)
|
||||
}
|
||||
|
||||
// Debug Log DEBUG level message.
|
||||
func (bl *BeeLogger) Debug(format string, v ...interface{}) {
|
||||
if LevelDebug > bl.level {
|
||||
return
|
||||
}
|
||||
lm := &LogMsg{
|
||||
Level: LevelDebug,
|
||||
Msg: format,
|
||||
When: time.Now(),
|
||||
Args: v,
|
||||
}
|
||||
|
||||
bl.writeMsg(lm)
|
||||
}
|
||||
|
||||
// Warn Log WARN level message.
|
||||
// compatibility alias for Warning()
|
||||
func (bl *BeeLogger) Warn(format string, v ...interface{}) {
|
||||
if LevelWarn > bl.level {
|
||||
return
|
||||
}
|
||||
lm := &LogMsg{
|
||||
Level: LevelWarn,
|
||||
Msg: format,
|
||||
When: time.Now(),
|
||||
Args: v,
|
||||
}
|
||||
|
||||
bl.writeMsg(lm)
|
||||
}
|
||||
|
||||
// Info Log INFO level message.
|
||||
// compatibility alias for Informational()
|
||||
func (bl *BeeLogger) Info(format string, v ...interface{}) {
|
||||
if LevelInfo > bl.level {
|
||||
return
|
||||
}
|
||||
lm := &LogMsg{
|
||||
Level: LevelInfo,
|
||||
Msg: format,
|
||||
When: time.Now(),
|
||||
Args: v,
|
||||
}
|
||||
|
||||
bl.writeMsg(lm)
|
||||
}
|
||||
|
||||
// Trace Log TRACE level message.
|
||||
// compatibility alias for Debug()
|
||||
func (bl *BeeLogger) Trace(format string, v ...interface{}) {
|
||||
if LevelDebug > bl.level {
|
||||
return
|
||||
}
|
||||
lm := &LogMsg{
|
||||
Level: LevelDebug,
|
||||
Msg: format,
|
||||
When: time.Now(),
|
||||
Args: v,
|
||||
}
|
||||
|
||||
bl.writeMsg(lm)
|
||||
}
|
||||
|
||||
// Flush flush all chan data.
|
||||
func (bl *BeeLogger) Flush() {
|
||||
if bl.asynchronous {
|
||||
bl.flushChan <- struct{}{}
|
||||
bl.wg.Wait()
|
||||
bl.wg.Add(1)
|
||||
return
|
||||
}
|
||||
bl.flush()
|
||||
}
|
||||
|
||||
// Close close logger, flush all chan data and destroy all adapters in BeeLogger.
|
||||
func (bl *BeeLogger) Close() {
|
||||
if bl.asynchronous {
|
||||
bl.closeChan <- struct{}{}
|
||||
bl.wg.Wait()
|
||||
close(bl.msgChan)
|
||||
} else {
|
||||
bl.flush()
|
||||
for _, l := range bl.outputs {
|
||||
l.Destroy()
|
||||
}
|
||||
bl.outputs = nil
|
||||
}
|
||||
close(bl.flushChan)
|
||||
close(bl.closeChan)
|
||||
}
|
||||
|
||||
// Reset close all outputs, and set bl.outputs to nil
|
||||
func (bl *BeeLogger) Reset() {
|
||||
bl.Flush()
|
||||
for _, l := range bl.outputs {
|
||||
l.Destroy()
|
||||
}
|
||||
bl.outputs = nil
|
||||
}
|
||||
|
||||
func (bl *BeeLogger) flush() {
|
||||
if bl.asynchronous {
|
||||
for {
|
||||
if len(bl.msgChan) > 0 {
|
||||
bm, ok := <-bl.msgChan
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
bl.writeToLoggers(bm)
|
||||
logMsgPool.Put(bm)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, l := range bl.outputs {
|
||||
l.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// beeLogger references the used application logger.
|
||||
var beeLogger = NewLogger()
|
||||
|
||||
// GetBeeLogger returns the default BeeLogger
|
||||
func GetBeeLogger() *BeeLogger {
|
||||
return beeLogger
|
||||
}
|
||||
|
||||
var beeLoggerMap = struct {
|
||||
sync.RWMutex
|
||||
logs map[string]*log.Logger
|
||||
}{
|
||||
logs: map[string]*log.Logger{},
|
||||
}
|
||||
|
||||
// GetLogger returns the default BeeLogger
|
||||
func GetLogger(prefixes ...string) *log.Logger {
|
||||
prefix := append(prefixes, "")[0]
|
||||
if prefix != "" {
|
||||
prefix = fmt.Sprintf(`[%s] `, strings.ToUpper(prefix))
|
||||
}
|
||||
beeLoggerMap.RLock()
|
||||
l, ok := beeLoggerMap.logs[prefix]
|
||||
if ok {
|
||||
beeLoggerMap.RUnlock()
|
||||
return l
|
||||
}
|
||||
beeLoggerMap.RUnlock()
|
||||
beeLoggerMap.Lock()
|
||||
defer beeLoggerMap.Unlock()
|
||||
l, ok = beeLoggerMap.logs[prefix]
|
||||
if !ok {
|
||||
l = log.New(beeLogger, prefix, 0)
|
||||
beeLoggerMap.logs[prefix] = l
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// EnableFullFilePath enables full file path logging. Disabled by default
|
||||
// e.g "/home/Documents/GitHub/beego/mainapp/" instead of "mainapp"
|
||||
func EnableFullFilePath(b bool) {
|
||||
beeLogger.enableFullFilePath = b
|
||||
}
|
||||
|
||||
// Reset will remove all the adapter
|
||||
func Reset() {
|
||||
beeLogger.Reset()
|
||||
}
|
||||
|
||||
// Async set the beelogger with Async mode and hold msglen messages
|
||||
func Async(msgLen ...int64) *BeeLogger {
|
||||
return beeLogger.Async(msgLen...)
|
||||
}
|
||||
|
||||
// SetLevel sets the global log level used by the simple logger.
|
||||
func SetLevel(l int) {
|
||||
beeLogger.SetLevel(l)
|
||||
}
|
||||
|
||||
// SetPrefix sets the prefix
|
||||
func SetPrefix(s string) {
|
||||
beeLogger.SetPrefix(s)
|
||||
}
|
||||
|
||||
// EnableFuncCallDepth enable log funcCallDepth
|
||||
func EnableFuncCallDepth(b bool) {
|
||||
beeLogger.enableFuncCallDepth = b
|
||||
}
|
||||
|
||||
// SetLogFuncCall set the CallDepth, default is 4
|
||||
func SetLogFuncCall(b bool) {
|
||||
beeLogger.EnableFuncCallDepth(b)
|
||||
beeLogger.SetLogFuncCallDepth(3)
|
||||
}
|
||||
|
||||
// SetLogFuncCallDepth set log funcCallDepth
|
||||
func SetLogFuncCallDepth(d int) {
|
||||
beeLogger.loggerFuncCallDepth = d
|
||||
}
|
||||
|
||||
// SetLogger sets a new logger.
|
||||
func SetLogger(adapter string, config ...string) error {
|
||||
return beeLogger.SetLogger(adapter, config...)
|
||||
}
|
||||
|
||||
// Emergency logs a message at emergency level.
|
||||
func Emergency(f interface{}, v ...interface{}) {
|
||||
beeLogger.Emergency(formatPattern(f, v...), v...)
|
||||
}
|
||||
|
||||
// Alert logs a message at alert level.
|
||||
func Alert(f interface{}, v ...interface{}) {
|
||||
beeLogger.Alert(formatPattern(f, v...), v...)
|
||||
}
|
||||
|
||||
// Critical logs a message at critical level.
|
||||
func Critical(f interface{}, v ...interface{}) {
|
||||
beeLogger.Critical(formatPattern(f, v...), v...)
|
||||
}
|
||||
|
||||
// Error logs a message at error level.
|
||||
func Error(f interface{}, v ...interface{}) {
|
||||
beeLogger.Error(formatPattern(f, v...), v...)
|
||||
}
|
||||
|
||||
// Warning logs a message at warning level.
|
||||
func Warning(f interface{}, v ...interface{}) {
|
||||
beeLogger.Warn(formatPattern(f, v...), v...)
|
||||
}
|
||||
|
||||
// Warn compatibility alias for Warning()
|
||||
func Warn(f interface{}, v ...interface{}) {
|
||||
beeLogger.Warn(formatPattern(f, v...), v...)
|
||||
}
|
||||
|
||||
// Notice logs a message at notice level.
|
||||
func Notice(f interface{}, v ...interface{}) {
|
||||
beeLogger.Notice(formatPattern(f, v...), v...)
|
||||
}
|
||||
|
||||
// Informational logs a message at info level.
|
||||
func Informational(f interface{}, v ...interface{}) {
|
||||
beeLogger.Info(formatPattern(f, v...), v...)
|
||||
}
|
||||
|
||||
// Info compatibility alias for Warning()
|
||||
func Info(f interface{}, v ...interface{}) {
|
||||
beeLogger.Info(formatPattern(f, v...), v...)
|
||||
}
|
||||
|
||||
// Debug logs a message at debug level.
|
||||
func Debug(f interface{}, v ...interface{}) {
|
||||
beeLogger.Debug(formatPattern(f, v...), v...)
|
||||
}
|
||||
|
||||
// Trace logs a message at trace level.
|
||||
// compatibility alias for Warning()
|
||||
func Trace(f interface{}, v ...interface{}) {
|
||||
beeLogger.Trace(formatPattern(f, v...), v...)
|
||||
}
|
||||
|
||||
func formatPattern(f interface{}, v ...interface{}) string {
|
||||
var msg string
|
||||
switch f.(type) {
|
||||
case string:
|
||||
msg = f.(string)
|
||||
if len(v) == 0 {
|
||||
return msg
|
||||
}
|
||||
if !strings.Contains(msg, "%") {
|
||||
// do not contain format char
|
||||
msg += strings.Repeat(" %v", len(v))
|
||||
}
|
||||
default:
|
||||
msg = fmt.Sprint(f)
|
||||
if len(v) == 0 {
|
||||
return msg
|
||||
}
|
||||
msg += strings.Repeat(" %v", len(v))
|
||||
}
|
||||
return msg
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2020
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogMsg struct {
|
||||
Level int
|
||||
Msg string
|
||||
When time.Time
|
||||
FilePath string
|
||||
LineNumber int
|
||||
Args []interface{}
|
||||
Prefix string
|
||||
enableFullFilePath bool
|
||||
enableFuncCallDepth bool
|
||||
}
|
||||
|
||||
// OldStyleFormat you should never invoke this
|
||||
func (lm *LogMsg) OldStyleFormat() string {
|
||||
msg := lm.Msg
|
||||
|
||||
if len(lm.Args) > 0 {
|
||||
msg = fmt.Sprintf(lm.Msg, lm.Args...)
|
||||
}
|
||||
|
||||
msg = lm.Prefix + " " + msg
|
||||
|
||||
if lm.enableFuncCallDepth {
|
||||
filePath := lm.FilePath
|
||||
if !lm.enableFullFilePath {
|
||||
_, filePath = path.Split(filePath)
|
||||
}
|
||||
msg = fmt.Sprintf("[%s:%d] %s", filePath, lm.LineNumber, msg)
|
||||
}
|
||||
|
||||
msg = levelPrefix[lm.Level] + " " + msg
|
||||
return msg
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2020
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLogMsg_OldStyleFormat(t *testing.T) {
|
||||
lg := &LogMsg{
|
||||
Level: LevelDebug,
|
||||
Msg: "Hello, world",
|
||||
When: time.Date(2020, 9, 19, 20, 12, 37, 9, time.UTC),
|
||||
FilePath: "/user/home/main.go",
|
||||
LineNumber: 13,
|
||||
Prefix: "Cus",
|
||||
}
|
||||
res := lg.OldStyleFormat()
|
||||
assert.Equal(t, "[D] Cus Hello, world", res)
|
||||
|
||||
lg.enableFuncCallDepth = true
|
||||
res = lg.OldStyleFormat()
|
||||
assert.Equal(t, "[D] [main.go:13] Cus Hello, world", res)
|
||||
|
||||
lg.enableFullFilePath = true
|
||||
|
||||
res = lg.OldStyleFormat()
|
||||
assert.Equal(t, "[D] [/user/home/main.go:13] Cus Hello, world", res)
|
||||
|
||||
lg.Msg = "hello, %s"
|
||||
lg.Args = []interface{}{"world"}
|
||||
assert.Equal(t, "[D] [/user/home/main.go:13] Cus hello, world", lg.OldStyleFormat())
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright 2020
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBeeLoggerDelLogger(t *testing.T) {
|
||||
prefix := "My-Cus"
|
||||
l := GetLogger(prefix)
|
||||
assert.NotNil(t, l)
|
||||
l.Print("hello")
|
||||
|
||||
GetLogger().Print("hello")
|
||||
SetPrefix("aaa")
|
||||
Info("hello")
|
||||
}
|
||||
|
||||
type mockLogger struct {
|
||||
*logWriter
|
||||
WriteCost time.Duration `json:"write_cost"` // Simulated log writing time consuming
|
||||
writeCnt int // Count add 1 when writing log success, just for test result
|
||||
}
|
||||
|
||||
func NewMockLogger() Logger {
|
||||
return &mockLogger{
|
||||
logWriter: &logWriter{writer: io.Discard},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockLogger) Init(config string) error {
|
||||
return json.Unmarshal([]byte(config), m)
|
||||
}
|
||||
|
||||
func (m *mockLogger) WriteMsg(lm *LogMsg) error {
|
||||
m.Lock()
|
||||
msg := lm.Msg
|
||||
msg += "\n"
|
||||
|
||||
time.Sleep(m.WriteCost)
|
||||
if _, err := m.writer.Write([]byte(msg)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.writeCnt++
|
||||
m.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockLogger) GetCnt() int {
|
||||
return m.writeCnt
|
||||
}
|
||||
|
||||
func (*mockLogger) Destroy() {}
|
||||
func (*mockLogger) Flush() {}
|
||||
func (*mockLogger) SetFormatter(_ LogFormatter) {}
|
||||
|
||||
func TestBeeLogger_AsyncNonBlockWrite(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
before func()
|
||||
after func()
|
||||
msgLen int64
|
||||
writeCost time.Duration
|
||||
sendInterval time.Duration
|
||||
writeCnt int
|
||||
}{
|
||||
{
|
||||
// Write log time is less than send log time, no blocking
|
||||
name: "mock1",
|
||||
after: func() {
|
||||
_ = beeLogger.DelLogger("mock1")
|
||||
},
|
||||
msgLen: 5,
|
||||
writeCnt: 10,
|
||||
writeCost: 200 * time.Millisecond,
|
||||
sendInterval: 300 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
// Write log time is less than send log time, discarded when blocking
|
||||
name: "mock2",
|
||||
after: func() {
|
||||
_ = beeLogger.DelLogger("mock2")
|
||||
},
|
||||
writeCnt: 5,
|
||||
msgLen: 5,
|
||||
writeCost: 200 * time.Millisecond,
|
||||
sendInterval: 10 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
Register(tc.name, NewMockLogger)
|
||||
err := beeLogger.SetLogger(tc.name, fmt.Sprintf(`{"write_cost": %d}`, tc.writeCost))
|
||||
assert.Nil(t, err)
|
||||
|
||||
l := beeLogger.Async(tc.msgLen)
|
||||
l.AsyncNonBlockWrite()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
time.Sleep(tc.sendInterval)
|
||||
l.Info(fmt.Sprintf("----%d----", i))
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
assert.Equal(t, tc.writeCnt, l.outputs[0].Logger.(*mockLogger).writeCnt)
|
||||
tc.after()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// Copyright 2014 beego Author. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type logWriter struct {
|
||||
sync.Mutex
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func newLogWriter(wr io.Writer) *logWriter {
|
||||
return &logWriter{writer: wr}
|
||||
}
|
||||
|
||||
func (lg *logWriter) writeln(msg string) (int, error) {
|
||||
lg.Lock()
|
||||
msg += "\n"
|
||||
n, err := lg.writer.Write([]byte(msg))
|
||||
lg.Unlock()
|
||||
return n, err
|
||||
}
|
||||
|
||||
const (
|
||||
y1 = `0123456789`
|
||||
y2 = `0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789`
|
||||
y3 = `0000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999`
|
||||
y4 = `0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789`
|
||||
mo1 = `000000000111`
|
||||
mo2 = `123456789012`
|
||||
d1 = `0000000001111111111222222222233`
|
||||
d2 = `1234567890123456789012345678901`
|
||||
h1 = `000000000011111111112222`
|
||||
h2 = `012345678901234567890123`
|
||||
mi1 = `000000000011111111112222222222333333333344444444445555555555`
|
||||
mi2 = `012345678901234567890123456789012345678901234567890123456789`
|
||||
s1 = `000000000011111111112222222222333333333344444444445555555555`
|
||||
s2 = `012345678901234567890123456789012345678901234567890123456789`
|
||||
ns1 = `0123456789`
|
||||
)
|
||||
|
||||
func formatTimeHeader(when time.Time) ([]byte, int, int) {
|
||||
y, mo, d := when.Date()
|
||||
h, mi, s := when.Clock()
|
||||
ns := when.Nanosecond() / 1000000
|
||||
// len("2006/01/02 15:04:05.123 ")==24
|
||||
var buf [24]byte
|
||||
|
||||
buf[0] = y1[y/1000%10]
|
||||
buf[1] = y2[y/100]
|
||||
buf[2] = y3[y-y/100*100]
|
||||
buf[3] = y4[y-y/100*100]
|
||||
buf[4] = '/'
|
||||
buf[5] = mo1[mo-1]
|
||||
buf[6] = mo2[mo-1]
|
||||
buf[7] = '/'
|
||||
buf[8] = d1[d-1]
|
||||
buf[9] = d2[d-1]
|
||||
buf[10] = ' '
|
||||
buf[11] = h1[h]
|
||||
buf[12] = h2[h]
|
||||
buf[13] = ':'
|
||||
buf[14] = mi1[mi]
|
||||
buf[15] = mi2[mi]
|
||||
buf[16] = ':'
|
||||
buf[17] = s1[s]
|
||||
buf[18] = s2[s]
|
||||
buf[19] = '.'
|
||||
buf[20] = ns1[ns/100]
|
||||
buf[21] = ns1[ns%100/10]
|
||||
buf[22] = ns1[ns%10]
|
||||
|
||||
buf[23] = ' '
|
||||
|
||||
return buf[0:], d, h
|
||||
}
|
||||
|
||||
var (
|
||||
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
|
||||
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
|
||||
yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109})
|
||||
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
|
||||
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
|
||||
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
|
||||
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
|
||||
|
||||
w32Green = string([]byte{27, 91, 52, 50, 109})
|
||||
w32White = string([]byte{27, 91, 52, 55, 109})
|
||||
w32Yellow = string([]byte{27, 91, 52, 51, 109})
|
||||
w32Red = string([]byte{27, 91, 52, 49, 109})
|
||||
w32Blue = string([]byte{27, 91, 52, 52, 109})
|
||||
w32Magenta = string([]byte{27, 91, 52, 53, 109})
|
||||
w32Cyan = string([]byte{27, 91, 52, 54, 109})
|
||||
|
||||
reset = string([]byte{27, 91, 48, 109})
|
||||
)
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
colorMap map[string]string
|
||||
)
|
||||
|
||||
func initColor() {
|
||||
if runtime.GOOS == "windows" {
|
||||
green = w32Green
|
||||
white = w32White
|
||||
yellow = w32Yellow
|
||||
red = w32Red
|
||||
blue = w32Blue
|
||||
magenta = w32Magenta
|
||||
cyan = w32Cyan
|
||||
}
|
||||
colorMap = map[string]string{
|
||||
// by color
|
||||
"green": green,
|
||||
"white": white,
|
||||
"yellow": yellow,
|
||||
"red": red,
|
||||
// by method
|
||||
"GET": blue,
|
||||
"POST": cyan,
|
||||
"PUT": yellow,
|
||||
"DELETE": red,
|
||||
"PATCH": green,
|
||||
"HEAD": magenta,
|
||||
"OPTIONS": white,
|
||||
}
|
||||
}
|
||||
|
||||
// ColorByStatus return color by http code
|
||||
// 2xx return Green
|
||||
// 3xx return White
|
||||
// 4xx return Yellow
|
||||
// 5xx return Red
|
||||
func ColorByStatus(code int) string {
|
||||
once.Do(initColor)
|
||||
switch {
|
||||
case code >= 200 && code < 300:
|
||||
return colorMap["green"]
|
||||
case code >= 300 && code < 400:
|
||||
return colorMap["white"]
|
||||
case code >= 400 && code < 500:
|
||||
return colorMap["yellow"]
|
||||
default:
|
||||
return colorMap["red"]
|
||||
}
|
||||
}
|
||||
|
||||
// ColorByMethod return color by http code
|
||||
func ColorByMethod(method string) string {
|
||||
once.Do(initColor)
|
||||
if c := colorMap[method]; c != "" {
|
||||
return c
|
||||
}
|
||||
return reset
|
||||
}
|
||||
|
||||
// ResetColor return reset color
|
||||
func ResetColor() string {
|
||||
return reset
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2016 beego Author. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFormatHeader_0(t *testing.T) {
|
||||
tm := time.Now()
|
||||
if tm.Year() >= 2100 {
|
||||
t.FailNow()
|
||||
}
|
||||
dur := time.Second
|
||||
for {
|
||||
if tm.Year() >= 2100 {
|
||||
break
|
||||
}
|
||||
h, _, _ := formatTimeHeader(tm)
|
||||
if tm.Format("2006/01/02 15:04:05.000 ") != string(h) {
|
||||
t.Log(tm)
|
||||
t.FailNow()
|
||||
}
|
||||
tm = tm.Add(dur)
|
||||
dur *= 2
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatHeader_1(t *testing.T) {
|
||||
tm := time.Now()
|
||||
year := tm.Year()
|
||||
dur := time.Second
|
||||
for {
|
||||
if tm.Year() >= year+1 {
|
||||
break
|
||||
}
|
||||
h, _, _ := formatTimeHeader(tm)
|
||||
if tm.Format("2006/01/02 15:04:05.000 ") != string(h) {
|
||||
t.Log(tm)
|
||||
t.FailNow()
|
||||
}
|
||||
tm = tm.Add(dur)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright 2014 beego Author. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// A filesLogWriter manages several fileLogWriter
|
||||
// filesLogWriter will write logs to the file in json configuration and write the same level log to correspond file
|
||||
// means if the file name in configuration is project.log filesLogWriter will create project.error.log/project.debug.log
|
||||
// and write the error-level logs to project.error.log and write the debug-level logs to project.debug.log
|
||||
// the rotate attribute also acts like fileLogWriter
|
||||
type multiFileLogWriter struct {
|
||||
writers [LevelDebug + 1 + 1]*fileLogWriter // the last one for fullLogWriter
|
||||
fullLogWriter *fileLogWriter
|
||||
Separate []string `json:"separate"`
|
||||
}
|
||||
|
||||
var levelNames = [...]string{"emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"}
|
||||
|
||||
// Init file logger with json config.
|
||||
// jsonConfig like:
|
||||
// {
|
||||
// "filename":"logs/beego.log",
|
||||
// "maxLines":0,
|
||||
// "maxsize":0,
|
||||
// "daily":true,
|
||||
// "maxDays":15,
|
||||
// "rotate":true,
|
||||
// "perm":0600,
|
||||
// "separate":["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"],
|
||||
// }
|
||||
|
||||
func (f *multiFileLogWriter) Init(config string) error {
|
||||
writer := newFileWriter().(*fileLogWriter)
|
||||
err := writer.Init(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.fullLogWriter = writer
|
||||
f.writers[LevelDebug+1] = writer
|
||||
|
||||
// unmarshal "separate" field to f.Separate
|
||||
err = json.Unmarshal([]byte(config), f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jsonMap := map[string]interface{}{}
|
||||
err = json.Unmarshal([]byte(config), &jsonMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := LevelEmergency; i < LevelDebug+1; i++ {
|
||||
for _, v := range f.Separate {
|
||||
if v == levelNames[i] {
|
||||
jsonMap["filename"] = f.fullLogWriter.fileNameOnly + "." + levelNames[i] + f.fullLogWriter.suffix
|
||||
jsonMap["level"] = i
|
||||
bs, _ := json.Marshal(jsonMap)
|
||||
writer = newFileWriter().(*fileLogWriter)
|
||||
err := writer.Init(string(bs))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.writers[i] = writer
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*multiFileLogWriter) Format(lm *LogMsg) string {
|
||||
return lm.OldStyleFormat()
|
||||
}
|
||||
|
||||
func (f *multiFileLogWriter) SetFormatter(fmt LogFormatter) {
|
||||
f.fullLogWriter.SetFormatter(fmt)
|
||||
}
|
||||
|
||||
func (f *multiFileLogWriter) Destroy() {
|
||||
for i := 0; i < len(f.writers); i++ {
|
||||
if f.writers[i] != nil {
|
||||
f.writers[i].Destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *multiFileLogWriter) WriteMsg(lm *LogMsg) error {
|
||||
if f.fullLogWriter != nil {
|
||||
f.fullLogWriter.WriteMsg(lm)
|
||||
}
|
||||
for i := 0; i < len(f.writers)-1; i++ {
|
||||
if f.writers[i] != nil {
|
||||
if lm.Level == f.writers[i].Level {
|
||||
f.writers[i].WriteMsg(lm)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *multiFileLogWriter) Flush() {
|
||||
for i := 0; i < len(f.writers); i++ {
|
||||
if f.writers[i] != nil {
|
||||
f.writers[i].Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newFilesWriter create a FileLogWriter returning as LoggerInterface.
|
||||
func newFilesWriter() Logger {
|
||||
res := &multiFileLogWriter{}
|
||||
return res
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(AdapterMultiFile, newFilesWriter)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2014 beego Author. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFiles_1(t *testing.T) {
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("multifile", `{"filename":"test.log","separate":["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"]}`)
|
||||
log.Debug("debug")
|
||||
log.Informational("info")
|
||||
log.Notice("notice")
|
||||
log.Warning("warning")
|
||||
log.Error("error")
|
||||
log.Alert("alert")
|
||||
log.Critical("critical")
|
||||
log.Emergency("emergency")
|
||||
fns := []string{""}
|
||||
fns = append(fns, levelNames[0:]...)
|
||||
name := "test"
|
||||
suffix := ".log"
|
||||
for _, fn := range fns {
|
||||
|
||||
file := name + suffix
|
||||
if fn != "" {
|
||||
file = name + "." + fn + suffix
|
||||
}
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := bufio.NewReader(f)
|
||||
lineNum := 0
|
||||
lastLine := ""
|
||||
for {
|
||||
line, _, err := b.ReadLine()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if len(line) > 0 {
|
||||
lastLine = string(line)
|
||||
lineNum++
|
||||
}
|
||||
}
|
||||
expected := 1
|
||||
if fn == "" {
|
||||
expected = LevelDebug + 1
|
||||
}
|
||||
if lineNum != expected {
|
||||
t.Fatal(file, "has", lineNum, "lines not "+strconv.Itoa(expected)+" lines")
|
||||
}
|
||||
if lineNum == 1 {
|
||||
if !strings.Contains(lastLine, fn) {
|
||||
t.Fatal(file + " " + lastLine + " not contains the log msg " + fn)
|
||||
}
|
||||
}
|
||||
os.Remove(file)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package logs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// SLACKWriter implements beego LoggerInterface and is used to send jiaoliao webhook
|
||||
type SLACKWriter struct {
|
||||
WebhookURL string `json:"webhookurl"`
|
||||
Level int `json:"level"`
|
||||
formatter LogFormatter
|
||||
Formatter string `json:"formatter"`
|
||||
}
|
||||
|
||||
// newSLACKWriter creates jiaoliao writer.
|
||||
func newSLACKWriter() Logger {
|
||||
res := &SLACKWriter{Level: LevelTrace}
|
||||
res.formatter = res
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *SLACKWriter) Format(lm *LogMsg) string {
|
||||
// text := fmt.Sprintf("{\"text\": \"%s\"}", msg)
|
||||
return lm.When.Format("2006-01-02 15:04:05") + " " + lm.OldStyleFormat()
|
||||
}
|
||||
|
||||
func (s *SLACKWriter) SetFormatter(f LogFormatter) {
|
||||
s.formatter = f
|
||||
}
|
||||
|
||||
// Init SLACKWriter with json config string
|
||||
func (s *SLACKWriter) Init(config string) error {
|
||||
res := json.Unmarshal([]byte(config), s)
|
||||
|
||||
if res == nil && len(s.Formatter) > 0 {
|
||||
fmtr, ok := GetFormatter(s.Formatter)
|
||||
if !ok {
|
||||
return errors.New(fmt.Sprintf("the formatter with name: %s not found", s.Formatter))
|
||||
}
|
||||
s.formatter = fmtr
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// WriteMsg write message in smtp writer.
|
||||
// Sends an email with subject and only this message.
|
||||
func (s *SLACKWriter) WriteMsg(lm *LogMsg) error {
|
||||
if lm.Level > s.Level {
|
||||
return nil
|
||||
}
|
||||
msg := s.Format(lm)
|
||||
m := make(map[string]string, 1)
|
||||
m["text"] = msg
|
||||
|
||||
body, _ := json.Marshal(m)
|
||||
// resp, err := http.PostForm(s.WebhookURL, form)
|
||||
resp, err := http.Post(s.WebhookURL, "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("Post webhook failed %s %d", resp.Status, resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush implementing method. empty.
|
||||
func (s *SLACKWriter) Flush() {
|
||||
}
|
||||
|
||||
// Destroy implementing method. empty.
|
||||
func (s *SLACKWriter) Destroy() {
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(AdapterSlack, newSLACKWriter)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2020
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
// func TestSLACKWriter_WriteMsg(t *testing.T) {
|
||||
// sc := `
|
||||
// {
|
||||
// "webhookurl":"",
|
||||
// "level":7
|
||||
// }
|
||||
// `
|
||||
// l := newSLACKWriter()
|
||||
// err := l.Init(sc)
|
||||
// if err != nil {
|
||||
// Debug(err)
|
||||
// }
|
||||
//
|
||||
// err = l.WriteMsg(&LogMsg{
|
||||
// Level: 7,
|
||||
// Msg: `{ "abs"`,
|
||||
// When: time.Now(),
|
||||
// FilePath: "main.go",
|
||||
// LineNumber: 100,
|
||||
// enableFullFilePath: true,
|
||||
// enableFuncCallDepth: true,
|
||||
// })
|
||||
//
|
||||
// if err != nil {
|
||||
// Debug(err)
|
||||
// }
|
||||
//
|
||||
// }
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright 2014 beego Author. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// SMTPWriter implements LoggerInterface and is used to send emails via given SMTP-server.
|
||||
type SMTPWriter struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Host string `json:"host"`
|
||||
Subject string `json:"subject"`
|
||||
FromAddress string `json:"fromAddress"`
|
||||
RecipientAddresses []string `json:"sendTos"`
|
||||
Level int `json:"level"`
|
||||
formatter LogFormatter
|
||||
Formatter string `json:"formatter"`
|
||||
}
|
||||
|
||||
// NewSMTPWriter creates the smtp writer.
|
||||
func newSMTPWriter() Logger {
|
||||
res := &SMTPWriter{Level: LevelTrace}
|
||||
res.formatter = res
|
||||
return res
|
||||
}
|
||||
|
||||
// Init smtp writer with json config.
|
||||
// config like:
|
||||
// {
|
||||
// "username":"example@gmail.com",
|
||||
// "password:"password",
|
||||
// "host":"smtp.gmail.com:465",
|
||||
// "subject":"email title",
|
||||
// "fromAddress":"from@example.com",
|
||||
// "sendTos":["email1","email2"],
|
||||
// "level":LevelError
|
||||
// }
|
||||
func (s *SMTPWriter) Init(config string) error {
|
||||
res := json.Unmarshal([]byte(config), s)
|
||||
if res == nil && len(s.Formatter) > 0 {
|
||||
fmtr, ok := GetFormatter(s.Formatter)
|
||||
if !ok {
|
||||
return errors.New(fmt.Sprintf("the formatter with name: %s not found", s.Formatter))
|
||||
}
|
||||
s.formatter = fmtr
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *SMTPWriter) getSMTPAuth(host string) smtp.Auth {
|
||||
if len(strings.Trim(s.Username, " ")) == 0 && len(strings.Trim(s.Password, " ")) == 0 {
|
||||
return nil
|
||||
}
|
||||
return smtp.PlainAuth(
|
||||
"",
|
||||
s.Username,
|
||||
s.Password,
|
||||
host,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *SMTPWriter) SetFormatter(f LogFormatter) {
|
||||
s.formatter = f
|
||||
}
|
||||
|
||||
func (s *SMTPWriter) sendMail(hostAddressWithPort string, auth smtp.Auth, fromAddress string, recipients []string, msgContent []byte) error {
|
||||
client, err := smtp.Dial(hostAddressWithPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host, _, _ := net.SplitHostPort(hostAddressWithPort)
|
||||
tlsConn := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: host,
|
||||
}
|
||||
if err = client.StartTLS(tlsConn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if auth != nil {
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = client.Mail(fromAddress); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rec := range recipients {
|
||||
if err = client.Rcpt(rec); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(msgContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return client.Quit()
|
||||
}
|
||||
|
||||
func (s *SMTPWriter) Format(lm *LogMsg) string {
|
||||
return lm.OldStyleFormat()
|
||||
}
|
||||
|
||||
// WriteMsg writes message in smtp writer.
|
||||
// Sends an email with subject and only this message.
|
||||
func (s *SMTPWriter) WriteMsg(lm *LogMsg) error {
|
||||
if lm.Level > s.Level {
|
||||
return nil
|
||||
}
|
||||
|
||||
hp := strings.Split(s.Host, ":")
|
||||
|
||||
// Set up authentication information.
|
||||
auth := s.getSMTPAuth(hp[0])
|
||||
|
||||
msg := s.Format(lm)
|
||||
|
||||
// Connect to the server, authenticate, set the sender and recipient,
|
||||
// and send the email all in one step.
|
||||
contentType := "Content-Type: text/plain" + "; charset=UTF-8"
|
||||
mailmsg := []byte("To: " + strings.Join(s.RecipientAddresses, ";") + "\r\nFrom: " + s.FromAddress + "<" + s.FromAddress +
|
||||
">\r\nSubject: " + s.Subject + "\r\n" + contentType + "\r\n\r\n" + fmt.Sprintf(".%s", lm.When.Format("2006-01-02 15:04:05")) + msg)
|
||||
|
||||
return s.sendMail(s.Host, auth, s.FromAddress, s.RecipientAddresses, mailmsg)
|
||||
}
|
||||
|
||||
// Flush implementing method. empty.
|
||||
func (s *SMTPWriter) Flush() {
|
||||
}
|
||||
|
||||
// Destroy implementing method. empty.
|
||||
func (s *SMTPWriter) Destroy() {
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(AdapterMail, newSMTPWriter)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2014 beego Author. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSmtp(t *testing.T) {
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("smtp", `{"username":"beegotest@gmail.com","password":"xxxxxxxx","host":"smtp.gmail.com:587","sendTos":["xiemengjun@gmail.com"]}`)
|
||||
log.Critical("sendmail critical")
|
||||
time.Sleep(time.Second * 30)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/cdle/sillyplus/core/storage"
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
)
|
||||
|
||||
func protect(appID, id string) string {
|
||||
mac := hmac.New(sha256.New, []byte(id))
|
||||
mac.Write([]byte(appID))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func init() {
|
||||
GetMachineID()
|
||||
storage.Watch(sillyGirl, "machine_id", func(old, new, key string) *storage.Final {
|
||||
if old == "" {
|
||||
return nil
|
||||
}
|
||||
return &storage.Final{
|
||||
Now: old,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var machine_id = ""
|
||||
|
||||
var GetMachineID = func() string {
|
||||
var id = ""
|
||||
if id == "" {
|
||||
id = sillyGirl.GetString("machine_id")
|
||||
}
|
||||
if id == "" {
|
||||
id = protect(utils.GenUUID(), "sillyplus")
|
||||
sillyGirl.Set("machine_id", id)
|
||||
}
|
||||
machine_id = id
|
||||
return id
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Master struct {
|
||||
Platform string `json:"platform"`
|
||||
Nickname string `json:"nickname"`
|
||||
ID string `json:"number"`
|
||||
Index int `json:"id"`
|
||||
Unix int `json:"unix"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
GinApi(GET, "/api/master/list", func(c *gin.Context) {
|
||||
plts := getPltsArray()
|
||||
ms := []Master{}
|
||||
i := 1
|
||||
for _, plt := range plts {
|
||||
v := MakeBucket(plt)
|
||||
masters := strings.Split(v.GetString("masters"), "&")
|
||||
for _, master := range masters {
|
||||
if master == "" {
|
||||
continue
|
||||
}
|
||||
nk := Nickname{ID: master}
|
||||
nickname.First(&nk)
|
||||
ms = append(ms, Master{
|
||||
Platform: plt,
|
||||
Nickname: nk.Value,
|
||||
ID: master,
|
||||
Index: i,
|
||||
Unix: nk.Unix,
|
||||
})
|
||||
i++
|
||||
}
|
||||
}
|
||||
c.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
"data": ms,
|
||||
"platforms": getPltsLabel(),
|
||||
})
|
||||
})
|
||||
GinApi(POST, "/api/master", func(c *gin.Context) {
|
||||
m := Master{}
|
||||
c.BindJSON(&m)
|
||||
if m.ID == "" {
|
||||
c.JSON(200, map[string]interface{}{
|
||||
"success": false,
|
||||
"errorMessage": "缺少号码字段",
|
||||
})
|
||||
return
|
||||
}
|
||||
if m.Platform == "" {
|
||||
nk := Nickname{ID: m.ID}
|
||||
nickname.First(&nk)
|
||||
if nk.Platform != "" {
|
||||
m.Platform = nk.Platform
|
||||
}
|
||||
}
|
||||
if m.Platform == "" {
|
||||
c.JSON(200, map[string]interface{}{
|
||||
"success": false,
|
||||
"errorMessage": "缺少平台字段",
|
||||
})
|
||||
return
|
||||
}
|
||||
v := MakeBucket(m.Platform)
|
||||
masters := strings.Split(v.GetString("masters"), "&")
|
||||
v.Set("masters", strings.Join(utils.Unique(masters, m.ID), "&"))
|
||||
|
||||
c.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
})
|
||||
GinApi(DELETE, "/api/master", func(c *gin.Context) {
|
||||
m := Master{}
|
||||
c.BindJSON(&m)
|
||||
if m.ID == "" {
|
||||
c.JSON(200, map[string]interface{}{
|
||||
"success": false,
|
||||
"errorMessage": "缺少账号字段",
|
||||
})
|
||||
return
|
||||
}
|
||||
if m.Platform == "" {
|
||||
nk := Nickname{ID: m.ID}
|
||||
nickname.First(&nk)
|
||||
if nk.Platform != "" {
|
||||
m.Platform = nk.Platform
|
||||
}
|
||||
}
|
||||
if m.Platform == "" {
|
||||
c.JSON(200, map[string]interface{}{
|
||||
"success": false,
|
||||
"errorMessage": "缺少平台字段",
|
||||
})
|
||||
return
|
||||
}
|
||||
v := MakeBucket(m.Platform)
|
||||
masters := strings.Split(v.GetString("masters"), "&")
|
||||
v.Set("masters", strings.Join(utils.Remove(masters, m.ID), "&"))
|
||||
|
||||
c.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Nickname struct {
|
||||
ID string `json:"i"`
|
||||
Group bool `json:"g"`
|
||||
Unix int `json:"u"`
|
||||
Value string `json:"v"`
|
||||
Platform string `json:"p"`
|
||||
BotsID []string `json:"bs"`
|
||||
}
|
||||
|
||||
var nickname = MakeBucket("nickname")
|
||||
|
||||
type NicklabeL struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
Platform string `json:"platform"`
|
||||
ChatName string `json:"chat_name"`
|
||||
}
|
||||
|
||||
func CreateNickName(nick *Nickname) {
|
||||
nick.Unix = int(time.Now().Unix())
|
||||
nickname.Create(nick)
|
||||
}
|
||||
|
||||
func init() {
|
||||
GinApi(GET, "/api/nickname/labels", RequireAuth, func(ctx *gin.Context) {
|
||||
group := true
|
||||
keyword := ctx.Query("gkeyword")
|
||||
if keyword == "" {
|
||||
keyword = ctx.Query("ukeyword")
|
||||
if keyword != "" {
|
||||
group = false
|
||||
}
|
||||
}
|
||||
platform := ctx.Query("platform")
|
||||
data := []NicklabeL{}
|
||||
if keyword != "" {
|
||||
full := false
|
||||
nickname.Foreach(func(b1, b2 []byte) error {
|
||||
v := &Nickname{}
|
||||
code := string(b1)
|
||||
err := json.Unmarshal(b2, v)
|
||||
if err == nil {
|
||||
if v.Group != group {
|
||||
return nil
|
||||
}
|
||||
if platform != "" && v.Platform != platform {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(code, keyword) || strings.Contains(v.Value, keyword) {
|
||||
nl := NicklabeL{
|
||||
ChatName: v.Value,
|
||||
Value: code,
|
||||
Platform: v.Platform,
|
||||
}
|
||||
if !group {
|
||||
nl.Label = fmt.Sprintf("%s(%s)", v.Value, code)
|
||||
} else {
|
||||
nl.Label = fmt.Sprintf("%s %s@%s", v.Value, code, v.Platform)
|
||||
}
|
||||
data = append(data, nl)
|
||||
if code == keyword {
|
||||
full = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if !full {
|
||||
data = append([]NicklabeL{{
|
||||
Label: keyword,
|
||||
Value: keyword,
|
||||
}}, data...)
|
||||
}
|
||||
}
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
"data": data,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/cdle/sillyplus/core/storage"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func MakeBucketObject(vm *goja.Runtime, uuid string, on_start bool, bucket storage.Bucket) *goja.Object {
|
||||
obj := vm.NewObject()
|
||||
obj.Set("get", func(v ...interface{}) interface{} {
|
||||
|
||||
return GetBucketKeyValue(bucket, v...)
|
||||
})
|
||||
obj.Set("foreach", func(v ...interface{}) map[string]interface{} {
|
||||
var rt = map[string]interface{}{}
|
||||
bucket.Foreach(func(b1, b2 []byte) error {
|
||||
rt[string(b1)] = TransformBucketKeyValue(string(b2))
|
||||
return nil
|
||||
})
|
||||
return rt
|
||||
})
|
||||
obj.Set("set", func(key, value interface{}) error {
|
||||
_, err := SetBucketKeyValue(bucket, key, value)
|
||||
return err
|
||||
})
|
||||
obj.Set("delete", func(key interface{}) error {
|
||||
_, err := bucket.Set(key, "")
|
||||
return err
|
||||
})
|
||||
obj.Set("deleteAll", func() error {
|
||||
return bucket.Delete()
|
||||
})
|
||||
obj.Set("keys", func() []string {
|
||||
keys, err := bucket.Keys()
|
||||
if err != nil {
|
||||
panic(vm.NewGoError(err))
|
||||
}
|
||||
return keys
|
||||
})
|
||||
obj.Set("len", func() int {
|
||||
keys, err := bucket.Keys()
|
||||
if err != nil {
|
||||
panic(vm.NewGoError(err))
|
||||
}
|
||||
return len(keys)
|
||||
})
|
||||
obj.Set("buckets", func() []string {
|
||||
return bucket.Buckets()
|
||||
})
|
||||
obj.Set("_name", func() string {
|
||||
return bucket.GetName()
|
||||
})
|
||||
obj.Set("watch", func(key string, f func(old, new interface{}, key string) *storage.Final) {
|
||||
if on_start {
|
||||
storage.Watch(bucket, key, func(old, new, key string) *storage.Final {
|
||||
// mutex := GetMutex(uuid)
|
||||
// mutex.Lock()
|
||||
// defer mutex.Unlock()
|
||||
return f(TransformBucketKeyValue(old), TransformBucketKeyValue(new), key)
|
||||
}, uuid)
|
||||
}
|
||||
})
|
||||
return obj
|
||||
}
|
||||
|
||||
// vm.Set("Bucket", func(name string) interface{} {
|
||||
// return vm.NewProxy(MakeBucketObject(vm, uuid, on_start, MakeBucket(name)), &goja.ProxyTrapConfig{
|
||||
// Get: func(target *goja.Object, property string, receiver goja.Value) (value goja.Value) {
|
||||
// return nil
|
||||
// },
|
||||
// Set: func(target *goja.Object, property string, value, receiver goja.Value) (success bool) {
|
||||
// return true
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
@@ -0,0 +1,109 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func Buffer(vm *goja.Runtime, call goja.ConstructorCall) *goja.Object {
|
||||
var buffer []byte
|
||||
switch arg := call.Arguments[0].Export().(type) {
|
||||
case string:
|
||||
buffer = []byte(arg)
|
||||
case []byte:
|
||||
buffer = arg
|
||||
case int:
|
||||
buffer = make([]byte, arg)
|
||||
case int64:
|
||||
buffer = make([]byte, arg)
|
||||
default:
|
||||
panic(vm.NewTypeError("invalid argument type"))
|
||||
}
|
||||
obj := call.This.ToObject(vm)
|
||||
obj.Set("length", func() int {
|
||||
return len(buffer)
|
||||
})
|
||||
|
||||
obj.Set("write", func(value interface{}, offset int, length int) {
|
||||
var buf []byte
|
||||
switch value := value.(type) {
|
||||
case string:
|
||||
buf = []byte(value)
|
||||
case []byte:
|
||||
buf = value
|
||||
default:
|
||||
panic(vm.NewTypeError("invalid argument type"))
|
||||
}
|
||||
|
||||
if offset+length > len(buffer) {
|
||||
panic(vm.NewGoError(errors.New("out of range")))
|
||||
}
|
||||
|
||||
copy(buffer[offset:], buf[:length])
|
||||
})
|
||||
|
||||
obj.Set("toString", func() string {
|
||||
return string(buffer)
|
||||
})
|
||||
|
||||
obj.Set("slice", func(start int, end int) []byte {
|
||||
if start < 0 {
|
||||
start += len(buffer)
|
||||
}
|
||||
if end < 0 {
|
||||
end += len(buffer)
|
||||
}
|
||||
if end > len(buffer) {
|
||||
end = len(buffer)
|
||||
}
|
||||
if start >= end || start >= len(buffer) {
|
||||
return nil
|
||||
}
|
||||
return buffer[start:end]
|
||||
})
|
||||
|
||||
obj.Set("copy", func(target []byte, sourceStart int, targetStart int, sourceEnd int) int {
|
||||
if sourceStart < 0 {
|
||||
sourceStart += len(buffer)
|
||||
}
|
||||
if sourceEnd < 0 {
|
||||
sourceEnd += len(buffer)
|
||||
}
|
||||
if targetStart < 0 {
|
||||
targetStart += len(target)
|
||||
}
|
||||
if sourceEnd > len(buffer) {
|
||||
sourceEnd = len(buffer)
|
||||
}
|
||||
if sourceStart >= sourceEnd || sourceStart >= len(buffer) {
|
||||
return 0
|
||||
}
|
||||
return copy(target[targetStart:], buffer[sourceStart:sourceEnd])
|
||||
})
|
||||
|
||||
obj.Set("join", func(sep []byte, buffers ...[]byte) []byte {
|
||||
var totalLen int
|
||||
for _, buf := range buffers {
|
||||
totalLen += len(buf)
|
||||
}
|
||||
if len(sep) > 0 && len(buffers) > 1 {
|
||||
totalLen += len(sep) * (len(buffers) - 1)
|
||||
}
|
||||
|
||||
result := make([]byte, totalLen)
|
||||
offset := 0
|
||||
for i, buf := range buffers {
|
||||
copy(result[offset:], buf)
|
||||
offset += len(buf)
|
||||
if i < len(buffers)-1 && len(sep) > 0 {
|
||||
copy(result[offset:], sep)
|
||||
offset += len(sep)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
return call.This
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func Error(vm *goja.Runtime, errs ...interface{}) *goja.Object {
|
||||
var msg error
|
||||
for _, err := range errs {
|
||||
switch err := err.(type) {
|
||||
case string:
|
||||
msg = errors.New(err)
|
||||
case error:
|
||||
msg = err
|
||||
}
|
||||
}
|
||||
return vm.NewGoError(msg)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func MakeHeadersObject(vm *goja.Runtime, header http.Header) *goja.Object {
|
||||
obj := vm.NewObject()
|
||||
obj.Set("get", func(name string) string {
|
||||
return header.Get(name)
|
||||
})
|
||||
|
||||
obj.Set("has", func(name string) bool {
|
||||
return header.Get(name) != ""
|
||||
})
|
||||
|
||||
obj.Set("set", func(name, value string) {
|
||||
header.Set(name, value)
|
||||
})
|
||||
|
||||
obj.Set("append", func(name, value string) {
|
||||
header.Add(name, value)
|
||||
})
|
||||
|
||||
obj.Set("delete", func(name string) {
|
||||
header.Del(name)
|
||||
})
|
||||
|
||||
obj.Set("keys", func() []string {
|
||||
keys := make([]string, 0, len(header))
|
||||
for k := range header {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
})
|
||||
|
||||
obj.Set("values", func() []string {
|
||||
values := make([]string, 0, len(header))
|
||||
for _, v := range header {
|
||||
values = append(values, v...)
|
||||
}
|
||||
return values
|
||||
})
|
||||
|
||||
obj.Set("entries", func() [][2]string {
|
||||
entries := make([][2]string, 0, len(header))
|
||||
for k, v := range header {
|
||||
for _, value := range v {
|
||||
entries = append(entries, [2]string{k, value})
|
||||
}
|
||||
}
|
||||
return entries
|
||||
})
|
||||
|
||||
obj.Set("forEach", func(callback func(value, name string)) {
|
||||
for k, v := range header {
|
||||
for _, value := range v {
|
||||
callback(value, k)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return obj
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func Regexp(call goja.ConstructorCall) *goja.Object {
|
||||
pattern := call.Arguments[0].String()
|
||||
call.This.Set("find", regexp.MustCompile(pattern).FindString)
|
||||
call.This.Set("findSubmatch", regexp.MustCompile(pattern).FindStringSubmatch)
|
||||
call.This.Set("findAll", regexp.MustCompile(pattern).FindAllString)
|
||||
call.This.Set("findAllSubmatch", regexp.MustCompile(pattern).FindAllStringSubmatch)
|
||||
call.This.Set("replaceAll", regexp.MustCompile(pattern).ReplaceAllString)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
u "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
func request(vm *goja.Runtime, resolve func(result interface{}), reject func(reason interface{}), wts ...interface{}) {
|
||||
var method = "get"
|
||||
var url = ""
|
||||
var req *http.Request
|
||||
|
||||
var headers map[string]interface{}
|
||||
var transport *http.Transport
|
||||
var formData map[string]interface{}
|
||||
// var isJson bool
|
||||
var isJsonBody bool
|
||||
var body string
|
||||
var allow_redirects bool = true
|
||||
var responseType = ""
|
||||
// var err error
|
||||
// var useproxy bool
|
||||
var timeout time.Duration = 0
|
||||
var login = false
|
||||
for _, wt := range wts {
|
||||
switch wt := wt.(type) {
|
||||
case string:
|
||||
url = wt
|
||||
case map[string]interface{}:
|
||||
props := wt
|
||||
for i := range props {
|
||||
switch strings.ToLower(i) {
|
||||
case "timeout":
|
||||
timeout = time.Duration(utils.Int64(props[i])) * time.Millisecond
|
||||
case "headers":
|
||||
headers = props[i].(map[string]interface{})
|
||||
case "method":
|
||||
method = strings.ToLower(props[i].(string))
|
||||
case "url":
|
||||
if f, ok := props[i].(func(i goja.FunctionCall) goja.Value); ok {
|
||||
url = f(goja.FunctionCall{}).ToString().String()
|
||||
} else {
|
||||
url = props[i].(string)
|
||||
}
|
||||
case "json":
|
||||
if props[i].(bool) {
|
||||
responseType = "json"
|
||||
}
|
||||
case "responsetype":
|
||||
responseType = props[i].(string)
|
||||
case "datatype":
|
||||
responseType = props[i].(string)
|
||||
case "allowredirects":
|
||||
allow_redirects = props[i].(bool)
|
||||
case "body":
|
||||
if v, ok := props[i].(string); !ok {
|
||||
d, _ := json.Marshal(props[i])
|
||||
body = string(d)
|
||||
isJsonBody = true
|
||||
} else {
|
||||
body = v
|
||||
}
|
||||
case "login":
|
||||
login = true
|
||||
case "formdata":
|
||||
formData = props[i].(map[string]interface{})
|
||||
case "form":
|
||||
formData = props[i].(map[string]interface{})
|
||||
case "proxy":
|
||||
var url, user, password string
|
||||
for k, v := range props[i].(map[string]interface{}) {
|
||||
if k == "url" {
|
||||
url = fmt.Sprint(v)
|
||||
}
|
||||
if k == "user" {
|
||||
user = fmt.Sprint(v)
|
||||
}
|
||||
if k == "password" {
|
||||
password = fmt.Sprint(v)
|
||||
}
|
||||
}
|
||||
if url != "" {
|
||||
var err error
|
||||
transport, err = GetTransport(url, user, password)
|
||||
if err != nil {
|
||||
reject(Error(vm, "proxy config error "+err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
method = strings.ToUpper(method)
|
||||
|
||||
if len(formData) > 0 {
|
||||
data := u.Values{}
|
||||
for key, value := range formData {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
data.Set(key, v)
|
||||
case bool:
|
||||
data.Set(key, strconv.FormatBool(v))
|
||||
case int:
|
||||
data.Set(key, strconv.Itoa(v))
|
||||
// 可以根据需要添加其他类型的处理
|
||||
default:
|
||||
data.Set(key, fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
|
||||
req, _ = http.NewRequest(method, url, strings.NewReader(data.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
} else {
|
||||
req, _ = http.NewRequest(method, url, bytes.NewBuffer([]byte(body)))
|
||||
if isJsonBody {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
}
|
||||
if login {
|
||||
req.Header.Set("Cookie", "uuid=40e67d5e-f6f3-11ed-8bc2-dca9049272e5; token="+getTempAuth())
|
||||
}
|
||||
for i := range headers {
|
||||
req.Header.Set(i, fmt.Sprint(headers[i]))
|
||||
}
|
||||
}
|
||||
|
||||
var rspObj goja.Proxy
|
||||
var rsp *http.Response
|
||||
var err error
|
||||
|
||||
var client = &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
if transport != nil {
|
||||
client.Transport = transport
|
||||
}
|
||||
|
||||
if !allow_redirects {
|
||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
}
|
||||
rsp, err = client.Do(req)
|
||||
if err == nil {
|
||||
defer rsp.Body.Close()
|
||||
rspObj = vm.NewProxy(MakeResponseObject(vm, reject, rsp, responseType), &goja.ProxyTrapConfig{
|
||||
Get: func(target *goja.Object, property string, receiver goja.Value) (value goja.Value) {
|
||||
obj := target.Get(property)
|
||||
if obj != nil {
|
||||
return obj
|
||||
}
|
||||
switch property {
|
||||
case "statusText", "statusMessage":
|
||||
return vm.ToValue("")
|
||||
case "statusCode":
|
||||
return target.Get("status")
|
||||
// case "body":
|
||||
// return vm.ToValue(target.Get("getBody").Export().(func() interface{})())
|
||||
case "then":
|
||||
return goja.Undefined()
|
||||
}
|
||||
console.Error("response has no property " + property)
|
||||
return goja.Undefined()
|
||||
},
|
||||
})
|
||||
resolve(rspObj)
|
||||
} else {
|
||||
reject(Error(vm, err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
// type Reason map[string]interface{}
|
||||
|
||||
func MakeResponseObject(vm *goja.Runtime, reject func(reason interface{}), resp *http.Response, responseType string) *goja.Object {
|
||||
obj := vm.NewObject()
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
reject(Error(vm, err))
|
||||
return obj
|
||||
}
|
||||
var body interface{}
|
||||
if Contains([]string{"blob", "arraybuffer"}, responseType) {
|
||||
body = data
|
||||
} else if Contains([]string{"text", "document"}, responseType) {
|
||||
body = string(data)
|
||||
} else if responseType == "json" {
|
||||
var v interface{}
|
||||
err := json.Unmarshal(data, &v)
|
||||
if err != nil {
|
||||
// console.Error("response body is not json data")
|
||||
// panic("response body is not json data")
|
||||
// reject()
|
||||
defer reject(Error(vm, "response body is not json data"))
|
||||
} else {
|
||||
body = v
|
||||
}
|
||||
}
|
||||
|
||||
if body == nil {
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(contentType, "text/") {
|
||||
body = string(data)
|
||||
} else if strings.HasPrefix(contentType, "image/") {
|
||||
body = data
|
||||
}
|
||||
}
|
||||
|
||||
if body == nil {
|
||||
isBinary := false
|
||||
for _, b := range data {
|
||||
if b < 32 || b > 126 {
|
||||
isBinary = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isBinary {
|
||||
body = data
|
||||
} else {
|
||||
body = string(data)
|
||||
}
|
||||
}
|
||||
|
||||
obj.Set("body", body)
|
||||
obj.Set("getBody", func() interface{} {
|
||||
return body
|
||||
})
|
||||
obj.Set("json", func() interface{} {
|
||||
var v interface{}
|
||||
json.Unmarshal(data, &v)
|
||||
return v
|
||||
})
|
||||
obj.Set("text", func() string {
|
||||
return string(data)
|
||||
})
|
||||
obj.Set("status", resp.StatusCode)
|
||||
obj.Set("headers", vm.NewProxy(MakeHeadersObject(vm, resp.Header), &goja.ProxyTrapConfig{
|
||||
Get: func(target *goja.Object, property string, receiver goja.Value) (value goja.Value) {
|
||||
obj := target.Get(property)
|
||||
if obj != nil {
|
||||
return obj
|
||||
}
|
||||
result := target.Get("get").Export().(func(name string) string)(property)
|
||||
return vm.ToValue(result)
|
||||
},
|
||||
Set: func(target *goja.Object, property string, value, receiver goja.Value) (success bool) {
|
||||
target.Get("set").Export().(func(name, value string))(
|
||||
property, value.String(),
|
||||
)
|
||||
return true
|
||||
},
|
||||
}))
|
||||
return obj
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
func GetTransport(proxyUrl string, user, password string) (*http.Transport, error) {
|
||||
var auth *proxy.Auth
|
||||
if user != "" && password != "" {
|
||||
auth = &proxy.Auth{User: user, Password: password}
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(proxyUrl, "socks5://"):
|
||||
dialer, err := proxy.SOCKS5("tcp", strings.TrimPrefix(proxyUrl, "socks5://"), auth, proxy.Direct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Transport{Dial: dialer.Dial}, nil
|
||||
case strings.HasPrefix(proxyUrl, "http://"):
|
||||
proxyUrl = strings.TrimPrefix(proxyUrl, "http://")
|
||||
url, err := url.Parse("http://" + proxyUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transport := &http.Transport{Proxy: http.ProxyURL(url)}
|
||||
if auth != nil {
|
||||
// Encode the credentials in Base64
|
||||
authHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password))
|
||||
transport.ProxyConnectHeader = http.Header{
|
||||
"Proxy-Authorization": {authHeader},
|
||||
}
|
||||
}
|
||||
return transport, nil
|
||||
case strings.HasPrefix(proxyUrl, "https://"):
|
||||
proxyUrl = strings.TrimPrefix(proxyUrl, "https://")
|
||||
url, err := url.Parse("https://" + proxyUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transport := &http.Transport{Proxy: http.ProxyURL(url)}
|
||||
if auth != nil {
|
||||
// Encode the credentials in Base64
|
||||
authHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password))
|
||||
transport.ProxyConnectHeader = http.Header{
|
||||
"Proxy-Authorization": {authHeader},
|
||||
}
|
||||
}
|
||||
return transport, nil
|
||||
default:
|
||||
return nil, errors.New("proxy url schema error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
)
|
||||
|
||||
var platforms = []string{}
|
||||
|
||||
func init() {
|
||||
nickname.Foreach(func(b1, b2 []byte) error {
|
||||
v := &Nickname{}
|
||||
err := json.Unmarshal(b2, v)
|
||||
if err == nil {
|
||||
platforms = append(platforms, v.Platform)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
platforms = utils.Unique(platforms)
|
||||
}
|
||||
|
||||
func getPltsArray() []string {
|
||||
return utils.Unique(platforms, GetAdapterBotPlts())
|
||||
}
|
||||
|
||||
func getPltsLabel() []map[string]string {
|
||||
ms := []map[string]string{}
|
||||
for _, v := range getPltsArray() {
|
||||
ms = append(ms, map[string]string{"label": v, "value": v})
|
||||
}
|
||||
return ms
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cdle/sillyplus/core/common"
|
||||
"github.com/cdle/sillyplus/core/storage"
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/dop251/goja/parser"
|
||||
"github.com/dop251/goja_nodejs/eventloop"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
type myFieldNameMapper struct{}
|
||||
|
||||
var mutexMap = make(map[string]*sync.Mutex)
|
||||
var mutexMapMutex sync.Mutex
|
||||
|
||||
func GetMutex(uuid string) *sync.Mutex {
|
||||
mutexMapMutex.Lock()
|
||||
defer mutexMapMutex.Unlock()
|
||||
|
||||
if mutex, ok := mutexMap[uuid]; ok {
|
||||
return mutex
|
||||
}
|
||||
|
||||
mutex := &sync.Mutex{}
|
||||
mutexMap[uuid] = mutex
|
||||
return mutex
|
||||
}
|
||||
|
||||
func (tfm myFieldNameMapper) FieldName(_ reflect.Type, f reflect.StructField) string {
|
||||
tag := f.Tag.Get(`json`)
|
||||
if idx := strings.IndexByte(tag, ','); idx != -1 {
|
||||
tag = tag[:idx]
|
||||
}
|
||||
if parser.IsIdentifier(tag) {
|
||||
return tag
|
||||
}
|
||||
return uncapitalize(f.Name)
|
||||
}
|
||||
|
||||
func uncapitalize(s string) string {
|
||||
return strings.ToLower(s[0:1]) + s[1:]
|
||||
}
|
||||
|
||||
func (tfm myFieldNameMapper) MethodName(_ reflect.Type, m reflect.Method) string {
|
||||
return uncapitalize(m.Name)
|
||||
}
|
||||
|
||||
var RegistFuncs = map[string]interface{}{}
|
||||
|
||||
var plugins = MakeBucket("plugins")
|
||||
|
||||
type Route struct {
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Component string `json:"component,omitempty"`
|
||||
Routes []Route `json:"routes,omitempty"`
|
||||
// Key string `json:"key,omitempty"`
|
||||
CreateAt string `json:"create_at"`
|
||||
}
|
||||
|
||||
func CancelPluginlistening(uuid string) {
|
||||
// logs.Info(`k, c.Function, c.Function.Rules`)
|
||||
for _, wait := range waits {
|
||||
wait.Foreach(func(k int64, c *Carry) bool {
|
||||
if uuid == c.UUID {
|
||||
c.Chan <- errors.New("uinstall")
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func initPlugins() {
|
||||
plugins.Foreach(func(key, data []byte) error {
|
||||
f, err := initPlugin(string(data), string(key))
|
||||
if err != nil {
|
||||
console.Error("初始化插件%s错误: %v", key, err)
|
||||
}
|
||||
AddCommand([]*common.Function{f})
|
||||
// os.WriteFile(fmt.Sprintf("%s/%s.js", pluginPath, f.Title), data, 0600)
|
||||
return nil
|
||||
})
|
||||
var pluginLock = new(sync.Mutex)
|
||||
storage.Watch(plugins, nil, func(old, new, key string) (fin *storage.Final) {
|
||||
pluginLock.Lock()
|
||||
defer pluginLock.Unlock()
|
||||
if new == "install" {
|
||||
for _, p := range plugin_list {
|
||||
if p.UUID != key {
|
||||
continue
|
||||
}
|
||||
script := string(fetchScript(p.Address, key))
|
||||
if f, _ := initPlugin(script, p.UUID); len(f.Rules) != 0 || f.Cron != "" || f.Module || f.OnStart {
|
||||
fin = &storage.Final{
|
||||
Now: script,
|
||||
}
|
||||
fin = &storage.Final{}
|
||||
su := &ScriptUtils{script: script}
|
||||
su.SetValue("origin", p.Organization)
|
||||
new = su.script
|
||||
break
|
||||
} else {
|
||||
return &storage.Final{
|
||||
Error: errors.New("订阅源异常。"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if new != "" {
|
||||
fin = &storage.Final{}
|
||||
su := &ScriptUtils{script: new}
|
||||
if version := su.GetValue("version"); version == "" || regexp.MustCompile(`v\d+\.\d+\.\d`).FindString(version) != version {
|
||||
su.SetValue("version", "v1.0.0")
|
||||
}
|
||||
if auhtor := su.GetValue("author"); auhtor == "" {
|
||||
su.SetValue("author", "佚名")
|
||||
}
|
||||
if su.GetValue("description") == "" {
|
||||
su.SetValue("description", "🐒这个人很懒什么都没有留下。")
|
||||
} //module
|
||||
title := su.GetValue("title")
|
||||
if title == "" {
|
||||
su.SetValue("title", "无名脚本")
|
||||
}
|
||||
if title != "无名脚本" && title != "" {
|
||||
onStart := su.GetValue("on_start")
|
||||
if onStart != "true" {
|
||||
module := su.GetValue("module")
|
||||
if module != "true" {
|
||||
if module != "" {
|
||||
su.DeleteValue("module")
|
||||
}
|
||||
web := su.GetValue("web")
|
||||
if web != "true" {
|
||||
if web != "" {
|
||||
su.DeleteValue("web")
|
||||
}
|
||||
// rule := su.GetValue("rule")
|
||||
// if rule == "" {
|
||||
// su.SetValue("rule", title)
|
||||
// }
|
||||
} else {
|
||||
su.DeleteValue("rule")
|
||||
su.DeleteValue("cron")
|
||||
su.DeleteValue("admin")
|
||||
su.DeleteValue("priority")
|
||||
su.DeleteValue("platform")
|
||||
}
|
||||
} else {
|
||||
su.DeleteValue("rule")
|
||||
su.DeleteValue("cron")
|
||||
su.DeleteValue("web")
|
||||
su.DeleteValue("admin")
|
||||
su.DeleteValue("priority")
|
||||
su.DeleteValue("platform")
|
||||
}
|
||||
} else {
|
||||
su.DeleteValue("rule")
|
||||
su.DeleteValue("cron")
|
||||
su.DeleteValue("web")
|
||||
su.DeleteValue("admin")
|
||||
su.DeleteValue("priority")
|
||||
su.DeleteValue("platform")
|
||||
su.DeleteValue("module")
|
||||
}
|
||||
}
|
||||
create_at := su.GetValue("create_at")
|
||||
if _, err := time.Parse("2006-01-02 15:04:05", create_at); err != nil {
|
||||
su.SetValue("create_at", time.Now().Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
fin.Now = su.script
|
||||
if su.script != new {
|
||||
fin.Message = su.script
|
||||
} else if title != (&ScriptUtils{script: old}).GetValue("title") {
|
||||
fin.Message = "标题变更。"
|
||||
}
|
||||
new = su.script
|
||||
}
|
||||
f, err := initPlugin(new, key)
|
||||
if err != nil && new != "" {
|
||||
console.Error(err)
|
||||
}
|
||||
apd := false
|
||||
for i := range Functions {
|
||||
if Functions[i].UUID == key {
|
||||
DestroyAdapterByUUID(key)
|
||||
Functions[i].Running = false
|
||||
if Functions[i].CronId != 0 {
|
||||
C.Remove(cron.EntryID(Functions[i].CronId))
|
||||
|
||||
}
|
||||
Functions = append(Functions[:i], Functions[i+1:]...)
|
||||
CancelPluginCrons(key)
|
||||
CancelPluginWebs(key)
|
||||
CancelPluginlistening(key)
|
||||
storage.DisableHandle(key)
|
||||
if new != "" {
|
||||
AddCommand([]*common.Function{f})
|
||||
if old == "" {
|
||||
console.Log("已加载 %s.js", f.Title)
|
||||
} else if !f.OnStart {
|
||||
console.Log("已重载 %s.js", f.Title)
|
||||
}
|
||||
} else {
|
||||
of, _ := initPlugin(old, key)
|
||||
console.Log("已卸载 %s.js", of.Title)
|
||||
}
|
||||
apd = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !apd {
|
||||
AddCommand([]*common.Function{f})
|
||||
}
|
||||
if f.UUID != "" && f.Public {
|
||||
go func() {
|
||||
os.WriteFile(fmt.Sprintf("%s/%s.js", plugin_download_file, f.UUID), []byte(publicScript(plugins.GetString(f.UUID))), 0666)
|
||||
os.WriteFile(plugin_path+"list.json", utils.JsonMarshal(GetPublicResponse()), 0666)
|
||||
}()
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func initPlugin(data string, uuid string) (*common.Function, error) {
|
||||
var err error
|
||||
var rules []string
|
||||
var imType *common.Filter
|
||||
var userId *common.Filter
|
||||
var groupId *common.Filter
|
||||
var cron string
|
||||
var admin bool
|
||||
var disable bool
|
||||
var priority int
|
||||
var title string
|
||||
var public bool
|
||||
var description string
|
||||
var icon string
|
||||
var version string = "v1.0.0"
|
||||
var author string
|
||||
var create_at string
|
||||
var module bool
|
||||
var web bool
|
||||
var encrypt bool
|
||||
var onStart bool
|
||||
var origin = "自定义"
|
||||
ress := regexp.MustCompile(
|
||||
`\*\s?@([\d\w+-]+)\s+([^\n]+?)\n`,
|
||||
).FindAllStringSubmatch(data, -1)
|
||||
for _, res := range ress {
|
||||
switch res[1] {
|
||||
case "rule", "match", "regex", "pattern":
|
||||
rule := strings.TrimSpace(res[2])
|
||||
_rs := []string{}
|
||||
FR:
|
||||
ress := regexp.MustCompile(`\[([^\s\[\]]+)\]`).FindAllStringSubmatch(rule, -1)
|
||||
if len(ress) != 0 {
|
||||
res := ress[len(ress)-1]
|
||||
var inner = res[1]
|
||||
slice := strings.SplitN(inner, ":", 2)
|
||||
name := slice[0]
|
||||
ps := ""
|
||||
if len(slice) == 2 {
|
||||
ps = slice[1]
|
||||
}
|
||||
if strings.HasSuffix(name, "?") {
|
||||
name = strings.TrimRight(name, "?")
|
||||
rep := ""
|
||||
if ps == "" {
|
||||
rep = fmt.Sprintf("[%s]", name)
|
||||
} else {
|
||||
rep = fmt.Sprintf("[%s:%s]", name, ps)
|
||||
}
|
||||
for l := range _rs {
|
||||
_rs[l] = strings.Replace(_rs[l], res[0], rep, 1)
|
||||
}
|
||||
rule1 := strings.Replace(rule, res[0], rep, 1)
|
||||
if len(_rs) == 0 {
|
||||
_rs = append(_rs, rule1)
|
||||
}
|
||||
rule = strings.Replace(rule, res[0], "", 1)
|
||||
rule = regexp.MustCompile("\x20{2,}").ReplaceAllString(rule, " ")
|
||||
rule = strings.TrimSpace(rule)
|
||||
_rs = append(_rs, rule)
|
||||
goto FR
|
||||
}
|
||||
}
|
||||
if len(_rs) != 0 {
|
||||
rules = append(rules, _rs...)
|
||||
} else {
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
case "platform", "imType", "platform+", "imType+":
|
||||
var item []string
|
||||
for _, i := range regexp.MustCompile(`[\d\w-]+`).FindAllString(res[2], -1) {
|
||||
item = append(item, strings.TrimSpace(i))
|
||||
}
|
||||
imType = &common.Filter{
|
||||
BlackMode: false,
|
||||
Items: item,
|
||||
}
|
||||
case "platform-", "imType-":
|
||||
var item []string
|
||||
for _, i := range regexp.MustCompile(`[\d\w-]+`).FindAllString(res[2], -1) {
|
||||
item = append(item, strings.TrimSpace(i))
|
||||
}
|
||||
imType = &common.Filter{
|
||||
BlackMode: true,
|
||||
Items: item,
|
||||
}
|
||||
case "userId", "userID", "uid", "userId+", "userID+", "uid+":
|
||||
var item []string
|
||||
for _, i := range regexp.MustCompile(`[\d\w-]+`).FindAllString(res[2], -1) {
|
||||
item = append(item, strings.TrimSpace(i))
|
||||
}
|
||||
userId = &common.Filter{
|
||||
BlackMode: false,
|
||||
Items: item,
|
||||
}
|
||||
case "userId-", "userID-", "uid-":
|
||||
var item []string
|
||||
for _, i := range regexp.MustCompile(`[\d\w-]+`).FindAllString(res[2], -1) {
|
||||
item = append(item, strings.TrimSpace(i))
|
||||
}
|
||||
userId = &common.Filter{
|
||||
BlackMode: true,
|
||||
Items: item,
|
||||
}
|
||||
case "groupId", "groupID", "groupCode", "chat_id", "chat_id+", "chatId", "chatID", "gid", "groupId+", "groupID+", "groupCode+", "chatId+", "chatID+", "gid+":
|
||||
var item []string
|
||||
for _, i := range regexp.MustCompile(`[\d\w-]+`).FindAllString(res[2], -1) {
|
||||
item = append(item, strings.TrimSpace(i))
|
||||
}
|
||||
groupId = &common.Filter{
|
||||
BlackMode: false,
|
||||
Items: item,
|
||||
}
|
||||
case "groupId-", "groupID-", "groupCode-", "chatId-", "chat_id-", "chatID-", "gid-":
|
||||
var item []string
|
||||
for _, i := range regexp.MustCompile(`[\d\w-]+`).FindAllString(res[2], -1) {
|
||||
item = append(item, strings.TrimSpace(i))
|
||||
}
|
||||
groupId = &common.Filter{
|
||||
BlackMode: true,
|
||||
Items: item,
|
||||
}
|
||||
case "cron", "crontab":
|
||||
cron = strings.TrimSpace(res[2])
|
||||
cron = strings.ReplaceAll(cron, `\/`, "/")
|
||||
case "admin":
|
||||
admin = strings.TrimSpace(res[2]) == "true"
|
||||
case "disable":
|
||||
disable = strings.TrimSpace(res[2]) == "true"
|
||||
case "priority":
|
||||
priority = utils.Int(strings.TrimSpace(res[2]))
|
||||
case "title", "name", "show":
|
||||
title = strings.TrimSpace(res[2])
|
||||
case "public":
|
||||
public = strings.TrimSpace(res[2]) == "true"
|
||||
case "description":
|
||||
description = strings.TrimSpace(res[2])
|
||||
case "icon":
|
||||
icon = strings.TrimSpace(res[2])
|
||||
case "version":
|
||||
version = strings.TrimSpace(res[2])
|
||||
case "author":
|
||||
author = strings.TrimSpace(res[2])
|
||||
case "create_at":
|
||||
create_at = strings.TrimSpace(res[2])
|
||||
case "origin":
|
||||
origin = strings.TrimSpace(res[2])
|
||||
case "module":
|
||||
module = strings.TrimSpace(res[2]) == "true"
|
||||
case "web":
|
||||
web = strings.TrimSpace(res[2]) == "true"
|
||||
case "encrypt":
|
||||
encrypt = strings.TrimSpace(res[2]) == "true"
|
||||
case "on_start":
|
||||
onStart = strings.TrimSpace(res[2]) == "true"
|
||||
}
|
||||
}
|
||||
script := ""
|
||||
if encrypt {
|
||||
script = DecryptPlugin(string(data))
|
||||
} else {
|
||||
script = string(data)
|
||||
}
|
||||
script = halfDeEct(script)
|
||||
script = strings.ReplaceAll(script, "new Sender", "Sender")
|
||||
script = strings.ReplaceAll(script, "new Bucket", "Bucket")
|
||||
prg, err2 := goja.Compile(title+".js", script, false)
|
||||
if err == nil && err2 != nil {
|
||||
err = err2
|
||||
}
|
||||
if err == nil && len(rules) == 0 && cron != "" {
|
||||
err = fmt.Errorf("无效的脚本%s。", title)
|
||||
}
|
||||
if web {
|
||||
onStart = true
|
||||
}
|
||||
if icon == "" {
|
||||
icon = "https://joeschmoe.io/api/v1/random?t=" + fmt.Sprint(time.Now().Nanosecond())
|
||||
}
|
||||
var running func() bool
|
||||
f := &common.Function{
|
||||
Handle: func(s common.Sender) interface{} {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
console.Error("脚本错误:", err)
|
||||
s.Reply(fmt.Sprint(err))
|
||||
}
|
||||
}()
|
||||
if err2 != nil {
|
||||
panic(err2)
|
||||
}
|
||||
loop := eventloop.NewEventLoop()
|
||||
loop.Run(func(vm *goja.Runtime) {
|
||||
SetPluginMethod(vm, uuid, onStart)
|
||||
ss := &SenderJsIplm{
|
||||
Message: s,
|
||||
Vm: vm,
|
||||
Private: "private",
|
||||
Group: "group",
|
||||
Routine: "routine",
|
||||
Persistent: "persistent",
|
||||
UUID: uuid,
|
||||
}
|
||||
vm.Set("sender", ss)
|
||||
vm.Set("s", ss)
|
||||
vm.Set("InitAdapter", func(plt, botid string) *Factory {
|
||||
f := &Factory{
|
||||
uuid: uuid,
|
||||
vm: vm,
|
||||
}
|
||||
f.Init(plt, botid)
|
||||
return f
|
||||
})
|
||||
vm.Set("initAdapter", func(plt, botid string) *Factory {
|
||||
f := &Factory{
|
||||
uuid: uuid,
|
||||
vm: vm,
|
||||
}
|
||||
f.Init(plt, botid)
|
||||
return f
|
||||
})
|
||||
vm.Set("GetAdapter", func(plt, botid string) map[string]interface{} {
|
||||
adapter, err := GetAdapter(plt, botid)
|
||||
errstr := ""
|
||||
if err != nil {
|
||||
errstr = err.Error()
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"error": errstr,
|
||||
"adapter": adapter,
|
||||
}
|
||||
})
|
||||
vm.Set("getAdapterBotsID", GetAdapterBotsID)
|
||||
vm.Set("getAdapterBotPlts", GetAdapterBotPlts)
|
||||
vm.Set("running", running)
|
||||
vm.Set("uuid", func() string {
|
||||
return uuid
|
||||
})
|
||||
|
||||
_, err := vm.RunProgram(prg)
|
||||
if err != nil {
|
||||
console.Error(strings.ReplaceAll(strings.ReplaceAll(err.Error(), "node_modules/", ""), "github.com/dop251/goja_nodejs/require", ""))
|
||||
}
|
||||
})
|
||||
return nil
|
||||
},
|
||||
Rules: rules,
|
||||
ImType: imType,
|
||||
UserId: userId,
|
||||
GroupId: groupId,
|
||||
Cron: cron,
|
||||
Admin: admin,
|
||||
Priority: priority,
|
||||
Disable: disable,
|
||||
UUID: uuid,
|
||||
Title: title,
|
||||
Public: public,
|
||||
Description: description,
|
||||
Icon: icon,
|
||||
Version: version,
|
||||
Author: author,
|
||||
CreateAt: create_at,
|
||||
Module: module,
|
||||
Encrypt: encrypt,
|
||||
OnStart: onStart,
|
||||
Origin: origin,
|
||||
Running: onStart,
|
||||
}
|
||||
running = func() bool {
|
||||
return f.Running
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
|
||||
func ChatID(p interface{}) string {
|
||||
switch p := p.(type) {
|
||||
case int:
|
||||
if p == 0 {
|
||||
return ""
|
||||
} else {
|
||||
return fmt.Sprint(p)
|
||||
}
|
||||
case string:
|
||||
return p
|
||||
case nil:
|
||||
return ""
|
||||
default:
|
||||
return fmt.Sprint(p)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cdle/sillyplus/core/common"
|
||||
"github.com/cdle/sillyplus/core/logs"
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/dop251/goja"
|
||||
// "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
var sleep = func(i int) {
|
||||
time.Sleep(time.Duration(i) * time.Millisecond)
|
||||
}
|
||||
|
||||
func GetScriptNameByUUID(uuid string) string {
|
||||
for _, f := range Functions {
|
||||
if f.UUID == uuid {
|
||||
return fmt.Sprintf("%s.js", f.Title)
|
||||
}
|
||||
}
|
||||
return "未知脚本.js"
|
||||
}
|
||||
|
||||
type SenderJsIplm struct {
|
||||
Message common.Sender
|
||||
Private string
|
||||
Group string
|
||||
Routine string
|
||||
Persistent string
|
||||
Vm *goja.Runtime
|
||||
UUID string
|
||||
NewContent bool
|
||||
}
|
||||
|
||||
type Console struct {
|
||||
}
|
||||
|
||||
var console = &Console{}
|
||||
var Logs = &Console{}
|
||||
|
||||
func Broadcast2WebUser(content, class string) {
|
||||
if (RegistFuncs["Broadcast2WebUser"]) == nil {
|
||||
return
|
||||
}
|
||||
RegistFuncs["Broadcast2WebUser"].(func(string, string))(content, class)
|
||||
}
|
||||
|
||||
func (c *Console) Info(v ...interface{}) {
|
||||
if len(v) == 0 {
|
||||
return
|
||||
}
|
||||
log := utils.FormatLog(v[0], v[1:]...)
|
||||
logs.Info(log)
|
||||
Broadcast2WebUser(log, "info")
|
||||
}
|
||||
|
||||
func (c *Console) Debug(v ...interface{}) {
|
||||
if len(v) == 0 {
|
||||
return
|
||||
}
|
||||
log := utils.FormatLog(v[0], v[1:]...)
|
||||
logs.Debug(log)
|
||||
Broadcast2WebUser(log, "debug")
|
||||
}
|
||||
|
||||
func (c *Console) Warn(v ...interface{}) {
|
||||
if len(v) == 0 {
|
||||
return
|
||||
}
|
||||
log := utils.FormatLog(v[0], v[1:]...)
|
||||
logs.Warn(log)
|
||||
Broadcast2WebUser(log, "warn")
|
||||
}
|
||||
|
||||
func (c *Console) Error(v ...interface{}) {
|
||||
if len(v) == 0 {
|
||||
return
|
||||
}
|
||||
log := utils.FormatLog(v[0], v[1:]...)
|
||||
logs.Error(log)
|
||||
Broadcast2WebUser(log, "error")
|
||||
}
|
||||
|
||||
func (c *Console) Log(v ...interface{}) {
|
||||
if len(v) == 0 {
|
||||
return
|
||||
}
|
||||
log := utils.FormatLog(v[0], v[1:]...)
|
||||
logs.Info(log)
|
||||
Broadcast2WebUser(log, "log")
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) Continue() {
|
||||
if sender.NewContent {
|
||||
go func() {
|
||||
Messages <- sender.Message
|
||||
}()
|
||||
return
|
||||
}
|
||||
sender.Message.Continue()
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) GetUserID() string {
|
||||
|
||||
return sender.Message.GetUserID()
|
||||
}
|
||||
func (sender *SenderJsIplm) GetBotID() string {
|
||||
|
||||
return sender.Message.GetBotID()
|
||||
}
|
||||
func (sender *SenderJsIplm) GetBotId() string {
|
||||
|
||||
return sender.Message.GetBotID()
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) GetUserId() string {
|
||||
return sender.Message.GetUserID()
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) SetContent(s string) {
|
||||
if s != sender.GetContent() {
|
||||
sender.NewContent = true
|
||||
}
|
||||
sender.Message.SetContent(s)
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) GetContent() string {
|
||||
return sender.Message.GetContent()
|
||||
}
|
||||
func (sender *SenderJsIplm) GetImType() string {
|
||||
return sender.Message.GetImType()
|
||||
}
|
||||
func (sender *SenderJsIplm) GetPlatform() string {
|
||||
return sender.Message.GetImType()
|
||||
}
|
||||
func (sender *SenderJsIplm) RecallMessage(p ...interface{}) {
|
||||
np := []interface{}{}
|
||||
var i = 0
|
||||
for _, v := range p {
|
||||
switch v.(type) {
|
||||
case int, int32, int64, uint, int16:
|
||||
i = utils.Int(v)
|
||||
default:
|
||||
np = append(np, v)
|
||||
}
|
||||
}
|
||||
if i != 0 {
|
||||
go func() {
|
||||
sleep(i)
|
||||
sender.Message.RecallMessage(np...)
|
||||
}()
|
||||
} else {
|
||||
go sender.Message.RecallMessage(np...)
|
||||
}
|
||||
}
|
||||
func (sender *SenderJsIplm) GetUserName() string {
|
||||
return sender.Message.GetUserName()
|
||||
}
|
||||
func (sender *SenderJsIplm) GetUsername() string {
|
||||
return sender.Message.GetUserName()
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) GetReplyUserID() int {
|
||||
return sender.Message.GetReplyUserID()
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) GetReplyUserId() int {
|
||||
return sender.Message.GetReplyUserID()
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) GetLevel() int {
|
||||
return sender.Message.GetLevel()
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) SetLevel(l int) {
|
||||
sender.Message.SetLevel(l)
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) GetChatName() string {
|
||||
return sender.Message.GetChatName()
|
||||
}
|
||||
func (sender *SenderJsIplm) GetMessageID() *goja.Promise {
|
||||
promise, resolve, _ := sender.Vm.NewPromise()
|
||||
go func() {
|
||||
resolve(sender.Message.GetMessageID())
|
||||
}()
|
||||
return promise
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) GetMessageId() string {
|
||||
return sender.Message.GetMessageID()
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) GetGroupCode() string {
|
||||
return sender.Message.GetChatID()
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) GetChatID() string {
|
||||
return sender.Message.GetChatID()
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) GetChatId() string {
|
||||
return sender.Message.GetChatID()
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) Kick(uid string) {
|
||||
sender.Message.GroupKick(uid, false)
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) Unkick(uid string) {
|
||||
sender.Message.GroupUnkick(uid)
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) Ban(uid string, duration int) {
|
||||
sender.Message.GroupBan(uid, duration)
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) Param(i interface{}) string {
|
||||
switch i := i.(type) {
|
||||
case int:
|
||||
return sender.Message.Get(i - 1)
|
||||
case int64:
|
||||
return sender.Message.Get(int(i - 1))
|
||||
case string:
|
||||
return sender.Message.Get(i)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (Sender *SenderJsIplm) Get(i interface{}) string {
|
||||
return Sender.Param(i)
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) IsAdmin() bool {
|
||||
return sender.Message.IsAdmin()
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) Reply(text string) *goja.Promise {
|
||||
promise, resolve, reject := sender.Vm.NewPromise()
|
||||
go func() {
|
||||
if text == "" {
|
||||
resolve("")
|
||||
return
|
||||
}
|
||||
i, err := sender.Message.Reply(text)
|
||||
if err != nil {
|
||||
reject(err.Error())
|
||||
}
|
||||
resolve(i)
|
||||
}()
|
||||
return promise
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) HoldOn(str ...string) string {
|
||||
if len(str) != 0 {
|
||||
return "go_again_" + str[0]
|
||||
}
|
||||
return "go_again_"
|
||||
}
|
||||
|
||||
func arrayss(i interface{}) []string {
|
||||
var ss = []string{}
|
||||
for _, v := range i.([]interface{}) {
|
||||
ss = append(ss, v.(string))
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
func (sender *SenderJsIplm) Listen(ps ...interface{}) interface{} {
|
||||
// promise, resolve, reject := vm.NewPromise()
|
||||
options := []interface{}{}
|
||||
var handle func(goja.FunctionCall) goja.Value
|
||||
var persistent = sender.Message.GetImType() == "*"
|
||||
var routine = persistent
|
||||
var carry = &Carry{}
|
||||
// var sustainable = false
|
||||
// rs := []string{}
|
||||
for _, p := range ps {
|
||||
switch p := p.(type) {
|
||||
case map[string]interface{}:
|
||||
props := p
|
||||
for i, p := range props {
|
||||
switch strings.ToLower(i) {
|
||||
case "rules":
|
||||
vs := p.([]interface{})
|
||||
for _, v := range vs {
|
||||
rule := v.(string)
|
||||
_rs := formatRule(rule)
|
||||
if len(_rs) != 0 {
|
||||
carry.Function.Rules = append(carry.Function.Rules, _rs...)
|
||||
} else {
|
||||
carry.Function.Rules = append(carry.Function.Rules, rule)
|
||||
}
|
||||
}
|
||||
case "timeout":
|
||||
options = append(options, time.Duration(utils.Int(p))*time.Millisecond)
|
||||
case "handle":
|
||||
handle = p.(func(goja.FunctionCall) goja.Value)
|
||||
case "listen_private", "private":
|
||||
carry.ListenPrivate = p.(bool)
|
||||
case "listen_group", "group":
|
||||
carry.ListenGroup = p.(bool)
|
||||
case "require_admin", "admin":
|
||||
carry.RequireAdmin = p.(bool)
|
||||
case "allow_platforms":
|
||||
carry.AllowPlatforms = arrayss(p)
|
||||
case "prohibit_platforms":
|
||||
carry.ProhibitPlatforms = arrayss(p)
|
||||
case "allow_groups":
|
||||
carry.AllowGroups = arrayss(p)
|
||||
case "allow_users":
|
||||
carry.AllowUsers = arrayss(p)
|
||||
case "prohibt_groups":
|
||||
carry.ProhibitGroups = arrayss(p)
|
||||
case "prohibt_users":
|
||||
carry.ProhibitUsers = arrayss(p)
|
||||
}
|
||||
}
|
||||
case bool:
|
||||
case []interface{}:
|
||||
for i := range p {
|
||||
rule := p[i].(string)
|
||||
_rs := formatRule(rule)
|
||||
if len(_rs) != 0 {
|
||||
carry.Function.Rules = append(carry.Function.Rules, _rs...)
|
||||
} else {
|
||||
carry.Function.Rules = append(carry.Function.Rules, rule)
|
||||
}
|
||||
}
|
||||
case string:
|
||||
if p == "private" {
|
||||
carry.ListenPrivate = true
|
||||
continue
|
||||
}
|
||||
if p == "group" {
|
||||
carry.ListenGroup = true
|
||||
continue
|
||||
}
|
||||
if p == "routine" {
|
||||
routine = true
|
||||
continue
|
||||
}
|
||||
if p == "persistent" {
|
||||
persistent = true
|
||||
continue
|
||||
}
|
||||
// if p == "sustainable" {
|
||||
// persistent = false
|
||||
// sustainable = true
|
||||
// continue
|
||||
// }
|
||||
carry.Function.Rules = append(carry.Function.Rules, p)
|
||||
case int, int64, int32:
|
||||
options = append(options, time.Duration(utils.Int(p))*time.Millisecond)
|
||||
case func(goja.FunctionCall) goja.Value:
|
||||
handle = p
|
||||
}
|
||||
}
|
||||
if persistent {
|
||||
options = append(options, "persistent")
|
||||
}
|
||||
if !persistent {
|
||||
carry.AllowPlatforms = []string{sender.GetPlatform()}
|
||||
}
|
||||
carry.UUID = sender.UUID
|
||||
options = append(options, carry)
|
||||
var newJsSender *SenderJsIplm
|
||||
var await = func() {
|
||||
sender.Message.Await(sender.Message, func(newSender common.Sender) interface{} {
|
||||
newSender.SetLevel(sender.Message.GetLevel() + 1)
|
||||
newJsSender = &SenderJsIplm{
|
||||
UUID: sender.UUID,
|
||||
Vm: sender.Vm,
|
||||
}
|
||||
newJsSender.Message = newSender
|
||||
if handle != nil {
|
||||
if persistent {
|
||||
func() {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
console.Error("%v at %v", err, GetScriptNameByUUID(sender.UUID))
|
||||
}
|
||||
}()
|
||||
// fmt.Println(newJsSender)
|
||||
rt := handle(goja.FunctionCall{
|
||||
Arguments: []goja.Value{
|
||||
sender.Vm.ToValue(newJsSender),
|
||||
},
|
||||
})
|
||||
reply := ""
|
||||
if rt != goja.Undefined() {
|
||||
reply = rt.ToString().String()
|
||||
} else {
|
||||
reply = ""
|
||||
}
|
||||
newSender.Reply(reply)
|
||||
}()
|
||||
// return GoAgain("")
|
||||
} else {
|
||||
var rt = handle(goja.FunctionCall{
|
||||
Arguments: []goja.Value{
|
||||
sender.Vm.ToValue(newJsSender),
|
||||
},
|
||||
})
|
||||
reply := ""
|
||||
if rt != goja.Undefined() {
|
||||
reply = rt.ToString().String()
|
||||
} else {
|
||||
reply = ""
|
||||
}
|
||||
if strings.HasPrefix(reply, "go_again_") {
|
||||
reply = strings.Replace(reply, "go_again_", "", 1)
|
||||
return GoAgain(reply)
|
||||
} else {
|
||||
// if sustainable {
|
||||
// return GoAgain(reply)
|
||||
// }
|
||||
if reply == "" {
|
||||
return nil
|
||||
}
|
||||
return reply
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, options...)
|
||||
}
|
||||
if !routine {
|
||||
await()
|
||||
} else {
|
||||
go await()
|
||||
}
|
||||
return newJsSender
|
||||
}
|
||||
|
||||
type TimeJsImpl struct {
|
||||
Second time.Duration
|
||||
Minute time.Duration
|
||||
Hour time.Duration
|
||||
Day time.Duration
|
||||
}
|
||||
|
||||
func (t *TimeJsImpl) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (t *TimeJsImpl) Sleep(i int) {
|
||||
time.Sleep(time.Duration(i) * time.Millisecond)
|
||||
}
|
||||
|
||||
func (t *TimeJsImpl) Unix(sec int64) time.Time {
|
||||
return time.Unix(sec, 0)
|
||||
}
|
||||
|
||||
func (t *TimeJsImpl) UnixMilli(sec int64) time.Time {
|
||||
return time.UnixMilli(sec)
|
||||
}
|
||||
|
||||
func (t *TimeJsImpl) Parse(timeStr, layout, locale string) (tt time.Time, err error) {
|
||||
if locale == "" {
|
||||
locale = "Asia/Shanghai"
|
||||
}
|
||||
local, err := time.LoadLocation(locale) //设置时区
|
||||
if err != nil {
|
||||
return tt, err
|
||||
}
|
||||
tt, err = time.ParseInLocation(layout, timeStr, local)
|
||||
if err != nil {
|
||||
return tt, err
|
||||
}
|
||||
return tt, nil
|
||||
}
|
||||
|
||||
type Strings struct {
|
||||
}
|
||||
|
||||
func (sender *Strings) Contains(s, substr string) bool {
|
||||
return strings.Contains(s, substr)
|
||||
}
|
||||
|
||||
func (sender *Strings) Replace(s string, old string, new string, n int) string {
|
||||
if n == 0 {
|
||||
n = -1
|
||||
}
|
||||
return strings.Replace(s, old, new, n)
|
||||
}
|
||||
|
||||
func (sender *Strings) ReplaceAll(s string, old string, new string) string {
|
||||
return strings.ReplaceAll(s, old, new)
|
||||
}
|
||||
|
||||
func (sender *Strings) Split(s string, sep string, n int) []string {
|
||||
return strings.SplitN(s, sep, n)
|
||||
}
|
||||
|
||||
type Fmt struct {
|
||||
}
|
||||
|
||||
func (sender *Fmt) Sprintf(format string, a ...interface{}) string {
|
||||
return fmt.Sprintf(format, a...)
|
||||
}
|
||||
|
||||
func (sender *Fmt) Printf(format string, a ...interface{}) {
|
||||
fmt.Printf(format, a...)
|
||||
}
|
||||
|
||||
func (sender *Fmt) Println(a ...interface{}) (int, error) {
|
||||
return fmt.Println(a...)
|
||||
}
|
||||
|
||||
func (sender *Fmt) Print(a ...interface{}) (int, error) {
|
||||
return fmt.Print(a...)
|
||||
}
|
||||
|
||||
func Url2Base64(imageUrl string) map[string]interface{} {
|
||||
response, err := http.Get(imageUrl)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error()}
|
||||
}
|
||||
defer response.Body.Close()
|
||||
data, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error()}
|
||||
}
|
||||
imageBase64 := base64.StdEncoding.EncodeToString(data)
|
||||
return map[string]interface{}{"result": imageBase64}
|
||||
}
|
||||
|
||||
func stringToBase64(str string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(str))
|
||||
}
|
||||
|
||||
func base64ToString(b64 string) string {
|
||||
data, err := base64.StdEncoding.DecodeString(b64)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func formatRule(rule string) []string {
|
||||
_rs := []string{}
|
||||
FR:
|
||||
ress := regexp.MustCompile(`\[([^\s\[\]]+)\]`).FindAllStringSubmatch(rule, -1)
|
||||
if len(ress) != 0 {
|
||||
res := ress[len(ress)-1]
|
||||
var inner = res[1]
|
||||
slice := strings.SplitN(inner, ":", 2)
|
||||
name := slice[0]
|
||||
ps := ""
|
||||
if len(slice) == 2 {
|
||||
ps = slice[1]
|
||||
}
|
||||
if strings.HasSuffix(name, "?") {
|
||||
name = strings.TrimRight(name, "?")
|
||||
rep := ""
|
||||
if ps == "" {
|
||||
rep = fmt.Sprintf("[%s]", name)
|
||||
} else {
|
||||
rep = fmt.Sprintf("[%s:%s]", name, ps)
|
||||
}
|
||||
for l := range _rs {
|
||||
_rs[l] = strings.Replace(_rs[l], res[0], rep, 1)
|
||||
}
|
||||
rule1 := strings.Replace(rule, res[0], rep, 1)
|
||||
if len(_rs) == 0 {
|
||||
_rs = append(_rs, rule1)
|
||||
}
|
||||
rule = strings.Replace(rule, res[0], "", 1)
|
||||
rule = regexp.MustCompile("\x20{2,}").ReplaceAllString(rule, " ")
|
||||
rule = strings.TrimSpace(rule)
|
||||
_rs = append(_rs, rule)
|
||||
goto FR
|
||||
}
|
||||
}
|
||||
return _rs
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/adapter/httplib"
|
||||
"github.com/dop251/goja_nodejs/require"
|
||||
)
|
||||
|
||||
func mapFileSystemSourceLoader(uuid string) require.SourceLoader {
|
||||
return func(path string) ([]byte, error) {
|
||||
path = strings.ReplaceAll(path, `node_modules/`, "")
|
||||
var data []byte
|
||||
var address = ""
|
||||
ls := plugin_list
|
||||
for _, f := range ls {
|
||||
if f.UUID == uuid {
|
||||
address = f.Address
|
||||
break
|
||||
}
|
||||
}
|
||||
if address != "" {
|
||||
for _, f := range ls {
|
||||
if f.Address == address && f.Title == path {
|
||||
data = plugins.GetBytes(f.UUID)
|
||||
}
|
||||
}
|
||||
if data == nil {
|
||||
for _, l := range ls {
|
||||
if l.Address == address && l.Title == path {
|
||||
data = fetchScript(l.Address, l.UUID)
|
||||
if data == nil {
|
||||
return nil, fmt.Errorf("无法从订阅源获取%s模块", path)
|
||||
} else {
|
||||
console.Log("已从订阅源获取%s模块", path)
|
||||
plugins.Set(l.UUID, string(data))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(data) == 0 {
|
||||
fs := Functions
|
||||
for _, f := range fs {
|
||||
if f.Title == path {
|
||||
data = plugins.GetBytes(f.UUID)
|
||||
}
|
||||
}
|
||||
}
|
||||
if data == nil {
|
||||
return nil, fmt.Errorf("缺少%s模块", path) //require.ModuleFileDoesNotExistError
|
||||
}
|
||||
su := &ScriptUtils{
|
||||
script: string(data),
|
||||
}
|
||||
if su.GetValue("encrypt") == "true" {
|
||||
data = []byte(DecryptPlugin(su.script))
|
||||
}
|
||||
data = []byte(halfDeEct(string(data)))
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
func fetchScript(address, uuid string) (data []byte) {
|
||||
var prefix = "?uuid=" + uuid
|
||||
if !strings.HasSuffix(address, "list.json") {
|
||||
address = address + "/api/plugins/download" + prefix
|
||||
} else {
|
||||
address = strings.ReplaceAll(address, "list.json", "download"+prefix)
|
||||
}
|
||||
data, _ = httplib.Get(address).Bytes()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/adapter/httplib"
|
||||
"github.com/cdle/sillyplus/core/common"
|
||||
"github.com/cdle/sillyplus/core/storage"
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
var plugin_path = "/etc/sillyplus/public/"
|
||||
var plugin_download_file = plugin_path + "download"
|
||||
|
||||
func CheckPluginAddress(address string) error {
|
||||
if !strings.HasSuffix(address, "list.json") {
|
||||
address += "/api/plugins/list.json"
|
||||
}
|
||||
data, _ := httplib.Get(address).Bytes()
|
||||
rr := RequestPluginResult{}
|
||||
json.Unmarshal(data, &rr)
|
||||
if !rr.Success {
|
||||
return errors.New("无效的地址。")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initPluginPublish() {
|
||||
storage.Watch(sillyGirl, "plugin_sublink", func(old, address, key string) *storage.Final {
|
||||
if strings.HasPrefix(address, "sub://") {
|
||||
return nil
|
||||
}
|
||||
if err := CheckPluginAddress(address); err != nil {
|
||||
return &storage.Final{
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
str, err := EncryptByAes(utils.JsonMarshal(common.PluginPublisher{
|
||||
Address: address,
|
||||
// MachineID: GetMachineID(),
|
||||
}))
|
||||
sublink := fmt.Sprintf("sub://%s", str)
|
||||
return &storage.Final{
|
||||
Now: sublink,
|
||||
Message: sublink,
|
||||
Error: err,
|
||||
}
|
||||
})
|
||||
|
||||
os.MkdirAll(plugin_download_file, 0666)
|
||||
os.WriteFile(plugin_path+"list.json", utils.JsonMarshal(GetPublicResponse()), 0666)
|
||||
for _, f := range Functions {
|
||||
if f.UUID != "" && f.Public {
|
||||
os.WriteFile(fmt.Sprintf("%s/%s.js", plugin_download_file, f.UUID), []byte(publicScript(plugins.GetString(f.UUID))), 0666)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func publicScript(str string) string {
|
||||
su := &ScriptUtils{
|
||||
script: str,
|
||||
}
|
||||
if version := su.GetValue("version"); regexp.MustCompile(`v\d+\.\d+\.\d`).FindString(version) != version {
|
||||
su.SetValue("version", "v1.0.0")
|
||||
}
|
||||
if su.GetValue("author") == "" {
|
||||
su.SetValue("author", "佚名")
|
||||
}
|
||||
if su.GetValue("description") == "" {
|
||||
su.SetValue("description", "🐒这个人很懒什么都没有留下。")
|
||||
}
|
||||
if su.GetValue("public") == "true" {
|
||||
su.SetValue("public", "false")
|
||||
}
|
||||
if su.GetValue("title") == "" {
|
||||
su.SetValue("title", "无名脚本")
|
||||
}
|
||||
create_at := su.GetValue("create_at")
|
||||
if _, err := time.Parse("2006-01-02 15:04:05", create_at); create_at == "" || err != nil {
|
||||
su.SetValue("create_at", time.Now().Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
if su.GetValue("encrypt") == "true" {
|
||||
su.script = EncryptPlugin(su.script)
|
||||
}
|
||||
su.script = halfEct(su.script)
|
||||
return su.script
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/httplib"
|
||||
"github.com/cdle/sillyplus/core/common"
|
||||
"github.com/cdle/sillyplus/core/storage"
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var plugin_subcribe_addresses = sillyGirl.GetString("plugin_subcribe_addresses")
|
||||
var plugin_subcribe_data = MakeBucket("plugin_subcribe_data")
|
||||
|
||||
type RequestPluginResult struct {
|
||||
Success bool `json:"success"`
|
||||
Data []*common.Function `json:"data"`
|
||||
Page int `json:"page"`
|
||||
Total int `json:"total"`
|
||||
Time time.Time `json:"time"`
|
||||
}
|
||||
|
||||
var plugin_list = []*common.Function{}
|
||||
|
||||
var cdle_sublink = "link://T4EywWN46ztYBhHNdOl6TjL4plwqQWRUoqr8w0KFmMqAdblZX3/xtrZARf3VKKQmH6iQNfyWvB2bqf6P1n/CMh1KLHLbTvUzh9zBQS2u9GeYwAp0APEZvQV1O6pb5g9V/dd6TLH54ssD92DAuMa1xw=="
|
||||
|
||||
func initPluginList() {
|
||||
list := []*common.Function{}
|
||||
var carrys []chan []*common.Function
|
||||
for _, v := range regexp.MustCompile(`link://([^\s#]+)`).FindAllStringSubmatch(cdle_sublink+"\n"+plugin_subcribe_addresses+"\n", -1) {
|
||||
sublink := v[1]
|
||||
ppr := common.PluginPublisher{}
|
||||
var data []byte
|
||||
func() {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
console.Error("initPluginList:", err)
|
||||
}
|
||||
}()
|
||||
data, _ = DecryptByAes(sublink)
|
||||
}()
|
||||
|
||||
if data == nil {
|
||||
continue
|
||||
}
|
||||
json.Unmarshal(data, &ppr)
|
||||
if ppr.Address != "" {
|
||||
carry := make(chan []*common.Function)
|
||||
carrys = append(carrys, carry)
|
||||
go func() {
|
||||
rr := RequestPluginResult{}
|
||||
data := plugin_subcribe_data.GetBytes(sublink)
|
||||
json.Unmarshal(data, &rr)
|
||||
if !rr.Success || rr.Time.Add(time.Second*3).Before(time.Now()) {
|
||||
address := ""
|
||||
if !strings.HasSuffix(ppr.Address, "list.json") {
|
||||
address = ppr.Address + "/api/plugins/list.json"
|
||||
} else {
|
||||
address = ppr.Address
|
||||
}
|
||||
req := httplib.Get(address)
|
||||
req.SetTimeout(time.Second*2, time.Second*2)
|
||||
data, _ := req.Bytes()
|
||||
json.Unmarshal(data, &rr)
|
||||
if rr.Success {
|
||||
rr.Time = time.Now()
|
||||
plugin_subcribe_data.Set(sublink, string(utils.JsonMarshal(rr)))
|
||||
}
|
||||
}
|
||||
for i := range rr.Data {
|
||||
rr.Data[i].Address = ppr.Address
|
||||
rr.Data[i].Organization = ppr.Organization
|
||||
rr.Data[i].Identified = ppr.Identified
|
||||
}
|
||||
n := len(rr.Data)
|
||||
flag := true
|
||||
for i := 0; i < n && flag; i++ {
|
||||
flag = false
|
||||
for j := 0; j < n-i-1; j++ {
|
||||
if rr.Data[j].CreateAt < rr.Data[j+1].CreateAt {
|
||||
rr.Data[j], rr.Data[j+1] = rr.Data[j+1], rr.Data[j]
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
}
|
||||
carry <- rr.Data
|
||||
}()
|
||||
}
|
||||
}
|
||||
for _, carry := range carrys {
|
||||
list = append(list, <-carry...)
|
||||
}
|
||||
plugin_list = list
|
||||
// if sillyGirl.GetString("password") == "" && plugins.GetString("7642f5de-3300-11ed-8a79-52540066b468") == "" { //自动安装老版命令
|
||||
// plugins.Set("7642f5de-3300-11ed-8a79-52540066b468", "install")
|
||||
// }
|
||||
// if plugins.GetString("78b15932-334f-11ed-8b59-aaaa00117a5c") == "" { //自动安装比价文案
|
||||
// plugins.Set("78b15932-334f-11ed-8b59-aaaa00117a5c", "install")
|
||||
// }
|
||||
}
|
||||
|
||||
func initWebPluginList() {
|
||||
storage.Watch(sillyGirl, "plugin_subcribe_addresses", func(old, new, key string) *storage.Final {
|
||||
plugin_subcribe_addresses = new
|
||||
return nil
|
||||
})
|
||||
GinApi(GET, "/api/plugins/download", func(c *gin.Context) {
|
||||
uuid := c.Query("uuid")
|
||||
for _, f := range Functions {
|
||||
if f.UUID == uuid && f.Public {
|
||||
c.String(200, publicScript(plugins.GetString(f.UUID)))
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
GinApi(GET, "/api/plugins/list.json", func(ctx *gin.Context) {
|
||||
current := utils.Int(ctx.Query("current"))
|
||||
pageSize := utils.Int(ctx.Query("pageSize"))
|
||||
init := ctx.Query("init")
|
||||
title := ctx.Query("title")
|
||||
rr := RequestPluginResult{
|
||||
Success: true,
|
||||
}
|
||||
if pageSize == 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
rr.Page = current
|
||||
if current != 0 {
|
||||
var list []*common.Function
|
||||
if title == "" {
|
||||
list = plugin_list
|
||||
} else {
|
||||
for _, f := range plugin_list {
|
||||
if strings.Contains(f.Title, title) || strings.Contains(f.Organization, title) {
|
||||
list = append(list, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
if current == 1 && init != "false" {
|
||||
initPluginList()
|
||||
}
|
||||
rr.Total = len(list)
|
||||
begin := (current - 1) * pageSize
|
||||
end := (current) * pageSize
|
||||
if end > rr.Total {
|
||||
end = rr.Total
|
||||
}
|
||||
if begin > end {
|
||||
begin = end
|
||||
}
|
||||
rr.Data = append(rr.Data, list[begin:end]...)
|
||||
fc := []*common.Function{}
|
||||
fc = append(fc, Functions...)
|
||||
publics := []string{}
|
||||
for _, f := range Functions {
|
||||
if f.Public && f.UUID != "" {
|
||||
publics = append(publics, f.UUID)
|
||||
}
|
||||
}
|
||||
for i := range rr.Data {
|
||||
for j := range fc {
|
||||
if rr.Data[i].UUID == fc[j].UUID {
|
||||
if rr.Data[i].Version != fc[j].Version {
|
||||
rr.Data[i].Status = 1
|
||||
} else {
|
||||
rr.Data[i].Status = 2
|
||||
}
|
||||
if Contains(publics, rr.Data[i].UUID) {
|
||||
rr.Data[i].Status = 6
|
||||
}
|
||||
// if rr.Data[i].MachineID == machine_id {
|
||||
// rr.Data[i].MachineID = ""
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.JSON(200, rr)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(200, GetPublicResponse())
|
||||
})
|
||||
}
|
||||
|
||||
func GetPublicResponse() *RequestPluginResult {
|
||||
rr := &RequestPluginResult{
|
||||
Success: true,
|
||||
}
|
||||
fs := []*common.Function{}
|
||||
for _, f := range Functions {
|
||||
if f.Public {
|
||||
fs = append(fs, f)
|
||||
}
|
||||
}
|
||||
rr.Total = len(fs)
|
||||
rr.Data = fs
|
||||
rr.Page = 1
|
||||
return rr
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/dop251/goja_nodejs/require"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
type ScriptUtils struct {
|
||||
matched bool
|
||||
ress [][]string
|
||||
script string
|
||||
}
|
||||
|
||||
func (su *ScriptUtils) match() {
|
||||
su.ress = regexp.MustCompile(
|
||||
`(\x20?\*[ ]?@([^\s]+)\s+([^\n]+?)\n)`,
|
||||
).FindAllStringSubmatch(su.script, -1)
|
||||
su.matched = true
|
||||
}
|
||||
|
||||
func (su *ScriptUtils) GetValue(key string) string {
|
||||
if !su.matched {
|
||||
su.match()
|
||||
}
|
||||
value := ""
|
||||
for _, res := range su.ress {
|
||||
if res[2] == key {
|
||||
value = res[3]
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// func Error(err error) interface{} {
|
||||
// if err != nil {
|
||||
// return map[string]string{"name": "Error", "message": err.Error()}
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
|
||||
func (su *ScriptUtils) SetValue(key, value string) {
|
||||
if !su.matched {
|
||||
su.match()
|
||||
}
|
||||
exists := []string{}
|
||||
first := ""
|
||||
for _, res := range su.ress {
|
||||
if first == "" {
|
||||
first = res[1]
|
||||
}
|
||||
if res[2] == key {
|
||||
exists = append(exists, res[1])
|
||||
}
|
||||
}
|
||||
if len(exists) != 0 {
|
||||
for i := range exists {
|
||||
if i == len(exists)-1 {
|
||||
su.script = strings.Replace(su.script, exists[i], fmt.Sprintf(" * @%s %s\n", key, value), 1)
|
||||
} else {
|
||||
su.script = strings.Replace(su.script, exists[i], "", 1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if first != "" {
|
||||
su.script = strings.Replace(su.script, first, first+fmt.Sprintf(" * @%s %s\n", key, value), 1)
|
||||
} else {
|
||||
su.script = fmt.Sprintf("/**\n%s */\n", fmt.Sprintf(" * @%s %s\n", key, value)) + su.script
|
||||
}
|
||||
}
|
||||
su.match()
|
||||
}
|
||||
|
||||
func (su *ScriptUtils) DeleteValue(key string) {
|
||||
if !su.matched {
|
||||
su.match()
|
||||
}
|
||||
exists := []string{}
|
||||
for _, res := range su.ress {
|
||||
if res[2] == key {
|
||||
exists = append(exists, res[1])
|
||||
}
|
||||
}
|
||||
if len(exists) != 0 {
|
||||
for i := range exists {
|
||||
su.script = strings.Replace(su.script, exists[i], "", 1)
|
||||
}
|
||||
}
|
||||
su.match()
|
||||
}
|
||||
|
||||
var store sync.Map
|
||||
var crons sync.Map
|
||||
|
||||
func CancelPluginCrons(uuid string) {
|
||||
v, ok := crons.Load(uuid)
|
||||
if ok {
|
||||
for _, id := range *v.(*[]cron.EntryID) {
|
||||
C.Remove(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SetPluginMethod(vm *goja.Runtime, uuid string, on_start bool) {
|
||||
vm.Set("Bucket", func(name string) interface{} {
|
||||
return vm.NewProxy(MakeBucketObject(vm, uuid, on_start, MakeBucket(name)), &goja.ProxyTrapConfig{
|
||||
Get: func(target *goja.Object, property string, receiver goja.Value) (value goja.Value) {
|
||||
obj := target.Get(property)
|
||||
if obj != nil {
|
||||
return obj
|
||||
}
|
||||
result := target.Get("get").Export().(func(...interface{}) interface{})(property)
|
||||
return vm.ToValue(result)
|
||||
},
|
||||
Set: func(target *goja.Object, property string, value, receiver goja.Value) (success bool) {
|
||||
result := target.Get("set").Export().(func(interface{}, interface{}) error)(
|
||||
property, value.Export(),
|
||||
)
|
||||
return result == nil
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
sillyplusJsIplm := func(call goja.ConstructorCall) *goja.Object {
|
||||
userId := fmt.Sprintf("%d", rand.Int63())
|
||||
call.This.Set("isSlaveMode", func() bool {
|
||||
return utils.SlaveMode
|
||||
})
|
||||
call.This.Set("uuid", func() string {
|
||||
return uuid
|
||||
})
|
||||
call.This.Set("store", func(str string, v interface{}) {
|
||||
store.Store(str, v)
|
||||
})
|
||||
call.This.Set("load", func(str string) interface{} {
|
||||
v, _ := store.Load(str)
|
||||
return v
|
||||
})
|
||||
call.This.Set("delete", func(str string) {
|
||||
store.Delete(str)
|
||||
})
|
||||
call.This.Set("session", func(info interface{}) func(...int) interface{} {
|
||||
msg := ""
|
||||
imTpye := "carry"
|
||||
var chatId string
|
||||
switch info := info.(type) {
|
||||
case string:
|
||||
msg = info
|
||||
default:
|
||||
props := info.(map[string]interface{})
|
||||
for i := range props {
|
||||
switch strings.ToLower(i) {
|
||||
case "imtype":
|
||||
imTpye = props[i].(string)
|
||||
case "platform":
|
||||
imTpye = props[i].(string)
|
||||
case "msg", "message":
|
||||
msg = props[i].(string)
|
||||
case "chatid", "chat_id":
|
||||
chatId = fmt.Sprint(props[i].(string))
|
||||
case "userid", "user_id":
|
||||
userId = props[i].(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
if msg == "" {
|
||||
return nil
|
||||
}
|
||||
c := &Faker{
|
||||
Type: imTpye,
|
||||
Message: msg,
|
||||
Carry: make(chan string),
|
||||
UserID: userId,
|
||||
ChatID: chatId,
|
||||
Admin: true,
|
||||
}
|
||||
Messages <- c
|
||||
var f = func(i ...int) interface{} {
|
||||
timeOut := 1000 * 100
|
||||
if len(i) > 0 {
|
||||
timeOut = i[0]
|
||||
}
|
||||
select {
|
||||
case v, ok := <-c.Listen():
|
||||
return map[string]interface{}{
|
||||
"hasNext": ok,
|
||||
"message": v,
|
||||
}
|
||||
case <-time.After(time.Millisecond * time.Duration(timeOut)):
|
||||
return map[string]interface{}{
|
||||
"hasNext": false,
|
||||
"message": "已超时",
|
||||
}
|
||||
}
|
||||
}
|
||||
return f
|
||||
})
|
||||
return nil
|
||||
}
|
||||
registry := require.NewRegistry(require.WithLoader(mapFileSystemSourceLoader(uuid)))
|
||||
if on_start {
|
||||
var ids = &[]cron.EntryID{}
|
||||
crons.Store(uuid, ids)
|
||||
vm.Set("Cron", func() *goja.Object {
|
||||
o := vm.NewObject()
|
||||
o.Set("add", func(cron string, f func()) interface{} {
|
||||
if f == nil {
|
||||
return map[string]interface{}{
|
||||
"id": 0,
|
||||
"error": "未传入handler",
|
||||
}
|
||||
}
|
||||
cron = strings.TrimSpace(cron)
|
||||
if len(regexp.MustCompile(`\S+`).FindAllString(cron, -1)) == 5 {
|
||||
cron = "0 " + cron
|
||||
}
|
||||
id, err := C.AddFunc(cron, func() {
|
||||
// mutex := GetMutex(uuid)
|
||||
// mutex.Lock()
|
||||
// defer mutex.Unlock()
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
console.Error("C.AddFunc err: %v", err)
|
||||
}
|
||||
}()
|
||||
f()
|
||||
})
|
||||
if err == nil {
|
||||
*ids = append(*ids, id)
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": id,
|
||||
"error": err,
|
||||
}
|
||||
})
|
||||
o.Set("remove", func(id cron.EntryID) {
|
||||
C.Remove(id)
|
||||
})
|
||||
return o
|
||||
})
|
||||
|
||||
vm.Set("Express", func() *goja.Object {
|
||||
o := vm.NewObject()
|
||||
methods := []string{"get", "post", "delete", "put", "fetch"}
|
||||
for i := range methods {
|
||||
method := methods[i]
|
||||
o.Set(method, func(path string, handles ...func(*Request, *Response)) {
|
||||
webs = append(webs, Web{
|
||||
uuid: uuid,
|
||||
method: strings.ToUpper(method),
|
||||
path: path,
|
||||
handles: handles,
|
||||
})
|
||||
})
|
||||
}
|
||||
return o
|
||||
})
|
||||
registry.RegisterNativeModule("express", func(runtime *goja.Runtime, module *goja.Object) {
|
||||
o := module.Get("exports").(*goja.Object)
|
||||
methods := []string{"get", "post", "delete", "put", "fetch"}
|
||||
for i := range methods {
|
||||
method := methods[i]
|
||||
o.Set(method, func(path string, handles ...func(*Request, *Response)) {
|
||||
webs = append(webs, Web{
|
||||
uuid: uuid,
|
||||
method: strings.ToUpper(method),
|
||||
path: path,
|
||||
handles: handles,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
vm.Set("gofor", func(running func() bool, handle func()) {
|
||||
go func() {
|
||||
for {
|
||||
if !func() bool {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
console.Error("gofor running error:", err)
|
||||
}
|
||||
}()
|
||||
return running()
|
||||
}() {
|
||||
return
|
||||
}
|
||||
func() {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
console.Error("gofor error:", err)
|
||||
}
|
||||
}()
|
||||
handle()
|
||||
}()
|
||||
}
|
||||
}()
|
||||
})
|
||||
// func (f *Factory) Request(running func() bool, wt interface{}, handles ...func(error, map[string]interface{}, interface{}) interface{}) {
|
||||
// go func() {
|
||||
// for {
|
||||
// if !running() {
|
||||
// return
|
||||
// }
|
||||
// func() {
|
||||
// defer func() {
|
||||
// if debug_mode {
|
||||
// return
|
||||
// }
|
||||
// err := recover()
|
||||
// if err != nil {
|
||||
// console.Error("Sender(\""+f.platform+"\").request error:", err)
|
||||
// }
|
||||
// }()
|
||||
// request(wt, handles...)
|
||||
// }()
|
||||
|
||||
// }
|
||||
// }()
|
||||
// }
|
||||
}
|
||||
registry.Enable(vm)
|
||||
vm.SetFieldNameMapper(myFieldNameMapper{})
|
||||
// vm.Set("sleep", sleep)
|
||||
vm.Set("md5", utils.Md5)
|
||||
vm.Set("image", utils.ToImageQrcode)
|
||||
vm.Set("video", utils.ToVideoQrcode)
|
||||
vm.Set("console", console)
|
||||
vm.Set("sillyplus", sillyplusJsIplm)
|
||||
vm.Set("call", func(str string) interface{} {
|
||||
return RegistFuncs[str]
|
||||
})
|
||||
vm.Set("fmt", &Fmt{})
|
||||
vm.Set("strings", &Strings{})
|
||||
// vm.Set("Bucket", BucketJsImpl)
|
||||
vm.Set("time", &TimeJsImpl{
|
||||
Second: time.Second,
|
||||
Minute: time.Minute,
|
||||
Hour: time.Hour,
|
||||
Day: time.Hour * 24,
|
||||
})
|
||||
vm.Set("Regexp", Regexp)
|
||||
vm.Set("Form", func(...interface{}) {
|
||||
|
||||
})
|
||||
vm.Set("url2Base64", Url2Base64)
|
||||
// 字符串转Base64编码
|
||||
vm.Set("stringToBase64", stringToBase64)
|
||||
vm.Set("base64ToString", base64ToString)
|
||||
vm.Set("Buffer", func(call goja.ConstructorCall) *goja.Object {
|
||||
return Buffer(vm, call)
|
||||
})
|
||||
vm.Set("request", func(wts ...interface{}) interface{} {
|
||||
promise, resolve, reject := vm.NewPromise()
|
||||
func() {
|
||||
func() {
|
||||
v := recover()
|
||||
if v != nil {
|
||||
if err, ok := v.(error); ok {
|
||||
reject(Error(vm, err))
|
||||
} else {
|
||||
reject(Error(vm, fmt.Sprint(v)))
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
request(vm, resolve, reject, wts...)
|
||||
}()
|
||||
return promise
|
||||
})
|
||||
}
|
||||
|
||||
func EncryptPlugin(script string) string {
|
||||
res := strings.SplitN(script, "*/\n", 2)
|
||||
if len(res) != 2 {
|
||||
// logs.Info(len(res))
|
||||
return script
|
||||
}
|
||||
str, err := EncryptByAes([]byte(res[1]))
|
||||
if err != nil {
|
||||
// logs.Info(err)
|
||||
return script
|
||||
}
|
||||
su := ScriptUtils{script: res[0]}
|
||||
if su.GetValue("encrypt_data") != "" {
|
||||
return script
|
||||
}
|
||||
su.SetValue("encrypt_data", str)
|
||||
return su.script + "*/\n"
|
||||
}
|
||||
|
||||
func DecryptPlugin(script string) string {
|
||||
su := ScriptUtils{script: script}
|
||||
encrypt_data := su.GetValue("encrypt_data")
|
||||
if encrypt_data == "" {
|
||||
return script
|
||||
}
|
||||
su.DeleteValue("encrypt_data")
|
||||
str, err := DecryptByAes(encrypt_data)
|
||||
if err != nil {
|
||||
return script
|
||||
}
|
||||
return fmt.Sprintf("%s%s", su.script, str)
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
var web_sessions = MakeBucket("web_sessions")
|
||||
|
||||
type Web struct {
|
||||
uuid string
|
||||
method string
|
||||
path string
|
||||
handles []func(*Request, *Response)
|
||||
}
|
||||
|
||||
var webs []Web
|
||||
|
||||
func CancelPluginWebs(uuid string) {
|
||||
for i := range webs {
|
||||
if webs[i].uuid == uuid {
|
||||
webs[i].handles = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
c *gin.Context
|
||||
ress [][]string
|
||||
parsedForm bool
|
||||
handled bool
|
||||
uuid string
|
||||
bodyData []byte
|
||||
}
|
||||
|
||||
func (r *Request) Body() string {
|
||||
if len(r.bodyData) == 0 {
|
||||
r.bodyData, _ = ioutil.ReadAll(r.c.Request.Body)
|
||||
}
|
||||
return string(r.bodyData)
|
||||
}
|
||||
|
||||
func (r *Request) Json() interface{} {
|
||||
var i interface{}
|
||||
if len(r.bodyData) == 0 {
|
||||
r.bodyData, _ = ioutil.ReadAll(r.c.Request.Body)
|
||||
}
|
||||
if json.Unmarshal(r.bodyData, &i) != nil {
|
||||
return nil
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func (r *Request) Ip() string {
|
||||
return r.c.ClientIP()
|
||||
}
|
||||
|
||||
func (r *Request) OriginalUrl() string {
|
||||
return r.c.Request.URL.String()
|
||||
}
|
||||
|
||||
func (r *Request) Query(param string) string {
|
||||
return r.c.Query(param)
|
||||
}
|
||||
|
||||
func (r *Request) Param(i int) string {
|
||||
return r.ress[i-1][1]
|
||||
}
|
||||
|
||||
func (r *Request) Querys() map[string][]string {
|
||||
return r.c.Request.URL.Query()
|
||||
}
|
||||
|
||||
func (r *Request) PostForm(s string) string {
|
||||
if !r.parsedForm {
|
||||
r.c.Request.ParseForm()
|
||||
}
|
||||
return r.c.PostForm(s)
|
||||
}
|
||||
|
||||
func (r *Request) PostForms() map[string][]string {
|
||||
if !r.parsedForm {
|
||||
r.c.Request.ParseForm()
|
||||
}
|
||||
return r.c.Request.PostForm
|
||||
}
|
||||
|
||||
func (r *Request) Path() string {
|
||||
return r.c.Request.URL.Path
|
||||
}
|
||||
|
||||
func (r *Request) Header(s string) string {
|
||||
return r.c.GetHeader(s)
|
||||
}
|
||||
|
||||
func (r *Request) Get(s string) string {
|
||||
return r.c.GetHeader(s)
|
||||
}
|
||||
|
||||
func (r *Request) Is(s string) bool { //判断是否传入的MIME类型
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Request) Headers() map[string][]string {
|
||||
return r.c.Request.Header
|
||||
}
|
||||
|
||||
func (r *Request) Method() string {
|
||||
return r.c.Request.Method
|
||||
}
|
||||
|
||||
func (r *Request) Logined() bool {
|
||||
auth, _ := CheckAuth(r.Cookie("token"))
|
||||
return auth != nil
|
||||
}
|
||||
|
||||
func (r *Request) Cookie(s string) string {
|
||||
var cookie, _ = r.c.Cookie(s)
|
||||
return cookie
|
||||
}
|
||||
|
||||
func (r *Request) Cookies() map[string]string {
|
||||
var cookies = map[string]string{}
|
||||
for _, v := range r.c.Request.Cookies() {
|
||||
cookies[v.Name] = v.Value
|
||||
}
|
||||
return cookies
|
||||
}
|
||||
|
||||
func (r *Request) Continue() {
|
||||
r.handled = false
|
||||
}
|
||||
|
||||
func (r *Request) SetSession(k, v string) string {
|
||||
j := map[string]interface{}{}
|
||||
json.Unmarshal(web_sessions.GetBytes(r.uuid), &j)
|
||||
j[k] = v
|
||||
j["time"] = time.Now().Unix()
|
||||
_, err := web_sessions.Set(r.uuid, utils.JsonMarshal(j))
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *Request) GetSession(k string) string {
|
||||
j := map[string]interface{}{}
|
||||
json.Unmarshal(web_sessions.GetBytes(r.uuid), &j)
|
||||
v, ok := j[k].(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (r *Request) GetSessionID() string {
|
||||
return r.uuid
|
||||
}
|
||||
|
||||
func (r *Request) DestroySession() string {
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
// Send func(goja.Value) `json:"send"`
|
||||
// SendStatus func(int) `json:"sendStatus"`
|
||||
// Json func(...interface{}) `json:"json"`
|
||||
// Header func(string, string) `json:"header"`
|
||||
// Render func(string, map[string]interface{}) `json:"render"`
|
||||
// Redirect func(...interface{}) `json:"redirect"`
|
||||
// Status func(int, ...string) goja.Value `json:"status"`
|
||||
// GetStatus func() int `json:"getStatus"`
|
||||
// IsComplete func() bool `json:"isComplete"`
|
||||
// SetCookie func(string, string, ...interface{}) `json:"setCookie"`
|
||||
c *gin.Context
|
||||
content string
|
||||
isJson bool
|
||||
status int
|
||||
isRedirect bool
|
||||
}
|
||||
|
||||
func (r *Response) Send(gv goja.Value) *Response {
|
||||
gve := gv.Export()
|
||||
switch gve := gve.(type) {
|
||||
case string:
|
||||
r.content += gve
|
||||
default:
|
||||
d, err := json.Marshal(gve)
|
||||
if err == nil {
|
||||
r.content += string(d)
|
||||
r.isJson = true
|
||||
} else {
|
||||
r.content += fmt.Sprint(gve)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Response) SendStatus(st int) *Response {
|
||||
r.status = st
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Response) Json(ps ...interface{}) *Response {
|
||||
var status int64 = 200
|
||||
var data interface{}
|
||||
if len(ps) == 1 {
|
||||
data = ps[0]
|
||||
} else {
|
||||
status = ps[0].(int64)
|
||||
data = ps[1]
|
||||
}
|
||||
d, err := json.Marshal(data)
|
||||
f := string(d)
|
||||
if err == nil {
|
||||
if strings.Contains(f, `"error"`) {
|
||||
f = strings.Replace(f, "error", "msg", 1)
|
||||
if status == 200 {
|
||||
status = -1
|
||||
}
|
||||
}
|
||||
r.content = strings.Replace(f, "{", fmt.Sprintf(`{"status":%d, `, status), 1)
|
||||
r.isJson = true
|
||||
} else {
|
||||
r.content += fmt.Sprint(data)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Response) Header(str, value string) *Response {
|
||||
r.c.Header(str, value)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Response) Set(str, value string) *Response {
|
||||
r.c.Header(str, value)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Response) Render(path string, obj map[string]interface{}) *Response {
|
||||
r.c.HTML(http.StatusOK, path, obj)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Response) Redirect(is ...interface{}) {
|
||||
a := 302
|
||||
b := ""
|
||||
for _, i := range is {
|
||||
switch i := i.(type) {
|
||||
case string:
|
||||
b = i
|
||||
default:
|
||||
a = utils.Int(i)
|
||||
}
|
||||
}
|
||||
r.c.Redirect(a, b)
|
||||
r.isRedirect = true
|
||||
}
|
||||
|
||||
func (r *Response) Status(i int, s ...string) *Response {
|
||||
r.status = i
|
||||
if len(s) != 0 {
|
||||
for _, v := range s {
|
||||
r.content += v
|
||||
}
|
||||
panic("stop")
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Response) SetCookie(name, value string, i ...interface{}) *Response {
|
||||
r.c.SetCookie(name, value, 8640000, "/", "", false, true)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Response) Stop() {
|
||||
panic("stop")
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
)
|
||||
|
||||
func initReboot() {
|
||||
go func() {
|
||||
data, _ := os.ReadFile(utils.ExecPath + "/rebootInfo")
|
||||
if v := string(data); v != "" {
|
||||
defer os.RemoveAll(utils.ExecPath + "/rebootInfo")
|
||||
vv := strings.Split(v, " ")
|
||||
tp, cd, ud := vv[0], vv[1], vv[2]
|
||||
if tp == "fake" {
|
||||
return
|
||||
}
|
||||
msg := "重启完成。"
|
||||
for i := 0; i < 10; i++ {
|
||||
dapter, _ := GetAdapter(tp, "")
|
||||
if dapter != nil {
|
||||
break
|
||||
}
|
||||
dapter.Push(Message{
|
||||
USER_ID: ud,
|
||||
CHAT_ID: cd,
|
||||
CONETNT: msg,
|
||||
})
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
type Reply struct {
|
||||
Index int `json:"index,omitempty"` // 排序
|
||||
ID int `json:"id"` // ID,主键
|
||||
Nickname string `json:"nickname"` // 类型 0全部 1用户 2群聊
|
||||
Number string `json:"number"` // 号码 明确用户和群聊
|
||||
Priority int `json:"priority"` // 决定 replies 排序,优先级越高排的越靠前
|
||||
Keyword string `json:"keyword"` // 关键词,模糊查询
|
||||
Value string `json:"value"` // 值,模糊查询
|
||||
CreatedAt int `json:"created_at"` // 创建时间
|
||||
Platforms []string `json:"platforms"` // 平台
|
||||
}
|
||||
|
||||
var replies []Reply //一切增删查改只需作用到这个变量
|
||||
var repliesLock sync.RWMutex
|
||||
|
||||
func init() {
|
||||
REPLY.Foreach(func(b1, b2 []byte) error {
|
||||
repliesLock.Lock()
|
||||
defer repliesLock.Unlock()
|
||||
rp := Reply{}
|
||||
err := json.Unmarshal(b2, &rp)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
replies = append(replies, rp)
|
||||
sort.Slice(replies, func(i, j int) bool {
|
||||
return replies[i].Priority > replies[j].Priority
|
||||
})
|
||||
return nil
|
||||
})
|
||||
GinApi(GET, "/api/reply/list", func(ctx *gin.Context) {
|
||||
repliesLock.RLock()
|
||||
defer repliesLock.RUnlock()
|
||||
page, _ := strconv.Atoi(ctx.DefaultQuery("current", "1"))
|
||||
perPage, _ := strconv.Atoi(ctx.DefaultQuery("pageSize", "20"))
|
||||
keyword := ctx.Query("keyword")
|
||||
value := ctx.Query("value")
|
||||
// class_ := ctx.Query("class")
|
||||
// class := utils.Int(class_)
|
||||
number := ctx.Query("number")
|
||||
// filter replies based on the query parameters
|
||||
filteredReplies := make([]Reply, 0, len(replies))
|
||||
for _, reply := range replies {
|
||||
if keyword != "" && !strings.Contains(reply.Keyword, keyword) {
|
||||
continue
|
||||
}
|
||||
if value != "" && !strings.Contains(reply.Value, value) {
|
||||
continue
|
||||
}
|
||||
// if class_ != "" && reply.Class != class {
|
||||
// continue
|
||||
// }
|
||||
if number != "" && reply.Number != number {
|
||||
continue
|
||||
}
|
||||
filteredReplies = append(filteredReplies, reply)
|
||||
}
|
||||
sort.Slice(filteredReplies, func(i, j int) bool {
|
||||
return filteredReplies[i].CreatedAt > filteredReplies[j].CreatedAt
|
||||
})
|
||||
// paginate the filtered replies
|
||||
start := (page - 1) * perPage
|
||||
end := start + perPage
|
||||
if end > len(filteredReplies) {
|
||||
end = len(filteredReplies)
|
||||
}
|
||||
paginatedReplies := filteredReplies[start:end]
|
||||
index := start + 1
|
||||
for i := range paginatedReplies {
|
||||
filteredReplies[i].Index = index
|
||||
index++
|
||||
if filteredReplies[i].Nickname == "" || len(filteredReplies[i].Platforms) == 0 {
|
||||
nk := Nickname{ID: filteredReplies[i].Number}
|
||||
nickname.First(&nk)
|
||||
if nk.Value != "" && filteredReplies[i].Nickname == "" {
|
||||
filteredReplies[i].Nickname = nk.Value
|
||||
}
|
||||
if nk.Platform != "" && len(filteredReplies[i].Platforms) == 0 {
|
||||
filteredReplies[i].Platforms = []string{nk.Platform}
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
"data": paginatedReplies,
|
||||
"page": page,
|
||||
"total": len(filteredReplies),
|
||||
"platforms": getPltsLabel(),
|
||||
})
|
||||
})
|
||||
|
||||
GinApi(POST, "/api/reply", func(ctx *gin.Context) {
|
||||
repliesLock.Lock()
|
||||
defer repliesLock.Unlock()
|
||||
var reply Reply
|
||||
data, _ := ioutil.ReadAll(ctx.Request.Body)
|
||||
var v = map[string]interface{}{}
|
||||
if err := json.Unmarshal(data, &reply); err != nil {
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": false,
|
||||
"errorMessage": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
json.Unmarshal(data, &v)
|
||||
has := func(str string) bool {
|
||||
_, ok := v[str]
|
||||
return ok
|
||||
}
|
||||
if reply.ID < 0 {
|
||||
reply.ID = 0
|
||||
}
|
||||
// find existing reply with the same ID
|
||||
var existingReply *Reply
|
||||
for i, r := range replies {
|
||||
if r.ID == reply.ID {
|
||||
existingReply = &replies[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if existingReply != nil {
|
||||
// update existing reply
|
||||
if has("nickname") {
|
||||
existingReply.Nickname = reply.Nickname
|
||||
}
|
||||
if has("number") {
|
||||
existingReply.Number = reply.Number
|
||||
}
|
||||
if has("keyword") {
|
||||
existingReply.Keyword = reply.Keyword
|
||||
}
|
||||
if has("value") {
|
||||
existingReply.Value = reply.Value
|
||||
}
|
||||
if has("priority") {
|
||||
existingReply.Priority = reply.Priority
|
||||
}
|
||||
if has("platforms") {
|
||||
existingReply.Platforms = reply.Platforms
|
||||
}
|
||||
reply = *existingReply
|
||||
err := REPLY.Create(&reply)
|
||||
if err != nil {
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": false,
|
||||
"errorMessage": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
reply.CreatedAt = int(time.Now().Unix())
|
||||
err := REPLY.Create(&reply)
|
||||
if err != nil {
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": false,
|
||||
"errorMessage": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
replies = append(replies, reply)
|
||||
}
|
||||
sort.Slice(replies, func(i, j int) bool {
|
||||
return replies[i].Priority > replies[j].Priority
|
||||
})
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
})
|
||||
//删除功能
|
||||
GinApi(DELETE, "/api/reply", func(ctx *gin.Context) {
|
||||
repliesLock.Lock()
|
||||
defer repliesLock.Unlock()
|
||||
id := utils.Int(ctx.Query("id"))
|
||||
for i, r := range replies {
|
||||
if r.ID == id {
|
||||
REPLY.Set(r.ID, nil)
|
||||
replies = append(replies[:i], replies[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
var REPLY = MakeBucket("reply")
|
||||
|
||||
// 能处理字符:你好,我是${ user.name }
|
||||
func parseReply(str string) string {
|
||||
re := regexp.MustCompile(`\$\{\s*([^\s{}]+)\s*\}`)
|
||||
return re.ReplaceAllStringFunc(str, func(match string) string {
|
||||
bk := match[2 : len(match)-1]
|
||||
b_k := strings.Split(bk, ".")
|
||||
if len(b_k) != 3 {
|
||||
return fmt.Sprintf("${%s}", bk)
|
||||
}
|
||||
return MakeBucket(b_k[1]).GetString(b_k[2])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/cdle/sillyplus/core/logs"
|
||||
"github.com/cdle/sillyplus/core/storage"
|
||||
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
)
|
||||
|
||||
type Bucket struct {
|
||||
name string
|
||||
}
|
||||
|
||||
var db *bolt.DB
|
||||
|
||||
func (b Bucket) String() string {
|
||||
return b.name
|
||||
}
|
||||
|
||||
var expirationA = Bucket{
|
||||
name: "timeouts",
|
||||
}
|
||||
|
||||
type Expire struct {
|
||||
Value string `json:"value"`
|
||||
Time time.Time `json:"deadline"`
|
||||
}
|
||||
|
||||
func Set(key string, value string, expiration time.Duration) error {
|
||||
_, err := expirationA.Set(key, utils.JsonMarshal(&Expire{
|
||||
Value: value,
|
||||
Time: time.Now().Add(expiration),
|
||||
}))
|
||||
return err
|
||||
}
|
||||
|
||||
func Get(key string) string {
|
||||
e := Expire{}
|
||||
data := expirationA.GetBytes(key)
|
||||
json.Unmarshal(data, &e)
|
||||
if e.Time.Before(time.Now()) {
|
||||
return ""
|
||||
}
|
||||
return e.Value
|
||||
}
|
||||
|
||||
var Buckets = []Bucket{}
|
||||
|
||||
func Initsillyplus() storage.Bucket {
|
||||
var err error
|
||||
if runtime.GOOS == "darwin" {
|
||||
db, err = bolt.Open("./sillyGirl.db", 0600, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
bd := utils.GetDataHome() + "sillyGirl.db"
|
||||
_, err := os.Stat(bd)
|
||||
if err != nil {
|
||||
f, err := os.Create(bd)
|
||||
if err != nil {
|
||||
logs.Info("傻妞无法创建数据文件 %s ,请手动创建。", bd)
|
||||
os.Exit(0)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
db, err = bolt.Open(bd, 0600, nil)
|
||||
if err != nil {
|
||||
logs.Info("傻妞无法创建数据文件 %s ,请手动创建。", bd)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
v := &Bucket{
|
||||
name: "sillyplus",
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (bucket *Bucket) GetName() string {
|
||||
return bucket.name
|
||||
}
|
||||
|
||||
func (bucket *Bucket) Copy(name string) storage.Bucket {
|
||||
return &Bucket{name: name}
|
||||
}
|
||||
|
||||
func (bucket *Bucket) Type() string {
|
||||
return "boltdb"
|
||||
}
|
||||
|
||||
func (bucket *Bucket) Buckets() []string {
|
||||
var r []string
|
||||
db.View(func(tx *bolt.Tx) error {
|
||||
tx.ForEach(func(name []byte, _ *bolt.Bucket) error {
|
||||
r = append(r, string(name))
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
func (bucket *Bucket) Delete() error {
|
||||
err := db.Update(func(tx *bolt.Tx) error {
|
||||
k := fmt.Sprint(bucket.name)
|
||||
e := tx.DeleteBucket([]byte(k))
|
||||
if e == bolt.ErrBucketNotFound {
|
||||
return nil
|
||||
}
|
||||
return e
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (bucket *Bucket) Set(key interface{}, value interface{}) (string, error) {
|
||||
new := ""
|
||||
msg := ""
|
||||
k := fmt.Sprint(key)
|
||||
switch value := value.(type) {
|
||||
case []byte:
|
||||
new = string(value)
|
||||
case string:
|
||||
new = value
|
||||
case nil:
|
||||
default:
|
||||
new = fmt.Sprint(value)
|
||||
}
|
||||
var handles []func(string, string, string) *storage.Final
|
||||
for _, listen := range storage.Listens {
|
||||
if listen.Name == bucket.name && (listen.Key == key || listen.Key == "*") {
|
||||
handles = append(handles, listen.Handle)
|
||||
}
|
||||
}
|
||||
if len(handles) > 0 {
|
||||
old := bucket.GetString(key)
|
||||
for _, handle := range handles {
|
||||
fin := handle(old, new, k)
|
||||
if fin != nil {
|
||||
if fin.Message != "" {
|
||||
msg = fin.Message
|
||||
}
|
||||
if fin.Error != nil {
|
||||
return msg, fin.Error
|
||||
}
|
||||
if fin.Now != "" {
|
||||
new = fin.Now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return msg, db.Update(func(tx *bolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte(bucket.name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if new == "" {
|
||||
if err := b.Delete([]byte(k)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := b.Put([]byte(k), []byte(new)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (bucket *Bucket) GetString(kv ...interface{}) string {
|
||||
// logs.Info(kv)
|
||||
var key, value string
|
||||
for i := range kv {
|
||||
if i == 0 {
|
||||
key = fmt.Sprint(kv[0])
|
||||
} else {
|
||||
value = fmt.Sprint(kv[1])
|
||||
}
|
||||
}
|
||||
db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(bucket.name))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
if v := string(b.Get([]byte(key))); v != "" {
|
||||
value = v
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
func (bucket *Bucket) GetBytes(key string) []byte {
|
||||
var value []byte
|
||||
db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(bucket.name))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
if v := b.Get([]byte(key)); v != nil {
|
||||
value = v
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
func (bucket *Bucket) GetInt(key string, vs ...int) int {
|
||||
var value int
|
||||
if len(vs) != 0 {
|
||||
value = vs[0]
|
||||
}
|
||||
db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(bucket.name))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
v := utils.Int(string(b.Get([]byte(fmt.Sprint(key)))))
|
||||
if v != 0 {
|
||||
value = v
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
func (bucket *Bucket) GetBool(key string, vs ...bool) bool {
|
||||
var value bool
|
||||
if len(vs) != 0 {
|
||||
value = vs[0]
|
||||
}
|
||||
db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(bucket.name))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
v := string(b.Get([]byte(fmt.Sprint(key))))
|
||||
if v == "true" {
|
||||
value = true
|
||||
} else if v == "false" {
|
||||
value = false
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
func (bucket *Bucket) Foreach(f func(k, v []byte) error) {
|
||||
var bs = [][][]byte{}
|
||||
db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(bucket.name))
|
||||
if b != nil {
|
||||
err := b.ForEach(func(k, v []byte) error {
|
||||
bs = append(bs, [][]byte{k, v})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
for i := range bs {
|
||||
f(bs[i][0], bs[i][1])
|
||||
}
|
||||
}
|
||||
|
||||
func (bucket *Bucket) Keys() ([]string, error) {
|
||||
var bs = []string{}
|
||||
err := db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(bucket.name))
|
||||
if b != nil {
|
||||
err := b.ForEach(func(k, _ []byte) error {
|
||||
if string(k) == "plugins" {
|
||||
return nil
|
||||
}
|
||||
bs = append(bs, string(k))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return bs, err
|
||||
}
|
||||
|
||||
func (bucket *Bucket) Create(i interface{}) error {
|
||||
// logs.Info("-", i)
|
||||
s := reflect.ValueOf(i).Elem()
|
||||
id := s.FieldByName("ID")
|
||||
sequence := s.FieldByName("Sequence")
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte(bucket.name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := id.Interface().(int); ok {
|
||||
key := id.Int()
|
||||
sq, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if key == 0 {
|
||||
key = int64(sq)
|
||||
id.SetInt(key)
|
||||
}
|
||||
if sequence != reflect.ValueOf(nil) {
|
||||
sequence.SetInt(int64(sq))
|
||||
}
|
||||
buf, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put(utils.Itob(uint64(key)), buf)
|
||||
} else {
|
||||
key := id.String()
|
||||
sq, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if key == "" {
|
||||
key = fmt.Sprint(sq)
|
||||
id.SetString(key)
|
||||
}
|
||||
if sequence != reflect.ValueOf(nil) {
|
||||
sequence.SetInt(int64(sq))
|
||||
}
|
||||
buf, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put([]byte(key), buf)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (bucket *Bucket) First(i interface{}) error {
|
||||
var err error
|
||||
s := reflect.ValueOf(i).Elem()
|
||||
id := s.FieldByName("ID")
|
||||
if v, ok := id.Interface().(int); ok {
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(bucket.name))
|
||||
if b == nil {
|
||||
err = errors.New("bucket not find")
|
||||
return nil
|
||||
}
|
||||
data := b.Get([]byte(fmt.Sprint(v)))
|
||||
if len(data) == 0 {
|
||||
err = errors.New("record not find")
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(data, i)
|
||||
})
|
||||
} else {
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(bucket.name))
|
||||
if b == nil {
|
||||
err = errors.New("bucket not find")
|
||||
return nil
|
||||
}
|
||||
data := b.Get([]byte(id.Interface().(string)))
|
||||
if len(data) == 0 {
|
||||
err = errors.New("record not find")
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(data, i)
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (bucket *Bucket) Size() (int64, error) {
|
||||
var r int64
|
||||
bucket.Foreach(func(k, v []byte) error {
|
||||
r++
|
||||
return nil
|
||||
})
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (bucket *Bucket) IsEmpty() (bool, error) {
|
||||
r := true
|
||||
db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(bucket.name))
|
||||
if b != nil {
|
||||
o, _ := b.Cursor().First()
|
||||
r = o == nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return r, nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package storage
|
||||
|
||||
type Listen struct {
|
||||
UUID string
|
||||
Name string
|
||||
Key string
|
||||
Handle func(old string, new string, key string) *Final
|
||||
}
|
||||
|
||||
var Listens []Listen
|
||||
|
||||
var DisableHandle = func(uuid string) {
|
||||
for i := range Listens {
|
||||
if Listens[i].UUID == uuid {
|
||||
Listens[i].Handle = func(old, new, key string) *Final {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Bucket interface {
|
||||
Set(interface{}, interface{}) (string, error)
|
||||
Copy(string) Bucket
|
||||
IsEmpty() (bool, error)
|
||||
Size() (int64, error)
|
||||
Delete() error
|
||||
Type() string
|
||||
Buckets() []string
|
||||
GetString(...interface{}) string
|
||||
GetBytes(string) []byte
|
||||
GetInt(string, ...int) int
|
||||
GetBool(string, ...bool) bool
|
||||
Foreach(func([]byte, []byte) error)
|
||||
Create(interface{}) error
|
||||
First(interface{}) error
|
||||
String() string
|
||||
GetName() string
|
||||
Keys() ([]string, error)
|
||||
}
|
||||
|
||||
type Final struct {
|
||||
Now string
|
||||
Message string
|
||||
Error error
|
||||
}
|
||||
|
||||
func Watch(bucket Bucket, key interface{}, handle func(old string, new string, key string) *Final, uuid ...string) {
|
||||
k := "*"
|
||||
if key != nil {
|
||||
k = key.(string)
|
||||
}
|
||||
listen := Listen{
|
||||
Name: bucket.GetName(),
|
||||
Key: k,
|
||||
Handle: handle,
|
||||
}
|
||||
if len(uuid) != 0 {
|
||||
listen.UUID = uuid[0]
|
||||
}
|
||||
Listens = append(Listens, listen)
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/httplib"
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/cdle/sillyplus/core/storage"
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
var ctx = context.Background()
|
||||
|
||||
var master = ""
|
||||
|
||||
var sillyplus *Bucket
|
||||
|
||||
type Bucket struct {
|
||||
name string
|
||||
}
|
||||
|
||||
var toMaster = func(bucket, key, value string) (string, error) {
|
||||
req := httplib.Put(master + "/api/storage")
|
||||
web_token := sillyplus.GetString("web_token")
|
||||
if web_token == "" {
|
||||
return "", errors.New("请先在本体可视化登录!")
|
||||
}
|
||||
token := strings.Split(web_token, "&")[0]
|
||||
req.SetCookie(&http.Cookie{
|
||||
Name: "token",
|
||||
Value: token,
|
||||
})
|
||||
key = fmt.Sprintf("%s.%s", bucket, key)
|
||||
req.JSONBody(map[string]interface{}{
|
||||
key: value,
|
||||
})
|
||||
data, err := req.Bytes()
|
||||
if err != nil {
|
||||
return "", errors.New("啊,与本体失联了~")
|
||||
}
|
||||
var message string
|
||||
message, _ = jsonparser.GetString(data, "messages", key)
|
||||
errStr, _ := jsonparser.GetString(data, "errors", key)
|
||||
if errStr != "" {
|
||||
err = errors.New(errStr)
|
||||
}
|
||||
return message, err
|
||||
}
|
||||
|
||||
var db *redis.Client
|
||||
|
||||
func (b *Bucket) String() string {
|
||||
return b.name
|
||||
}
|
||||
|
||||
var Buckets = []Bucket{}
|
||||
|
||||
func (bucket *Bucket) Type() string {
|
||||
return "redis"
|
||||
}
|
||||
|
||||
func Initsillyplus(RedisAddr, RedisPassword string) storage.Bucket {
|
||||
db = redis.NewClient(&redis.Options{
|
||||
Addr: RedisAddr,
|
||||
Password: RedisPassword, // no password set
|
||||
DB: 0, // use default DB
|
||||
})
|
||||
if db == nil {
|
||||
panic("redis is not ok")
|
||||
}
|
||||
sillyplus = &Bucket{
|
||||
name: "sillyplus",
|
||||
}
|
||||
port := sillyplus.GetString("port", "8080")
|
||||
if utils.SlaveMode {
|
||||
is, _ := db.ConfigGet(ctx, "slaveof").Result()
|
||||
db.ConfigSet(ctx, "notify-keyspace-events", "KEh")
|
||||
if len(is) > 1 {
|
||||
ip := strings.Split(fmt.Sprint(is[1]), " ")[0]
|
||||
master = fmt.Sprintf("http://%s:%s", ip, port)
|
||||
}
|
||||
go func() {
|
||||
again:
|
||||
subscriber := db.Subscribe(ctx, "__keyevent@0__:hset")
|
||||
for {
|
||||
msg, err := subscriber.ReceiveMessage(ctx)
|
||||
if err != nil {
|
||||
time.Sleep(time.Second)
|
||||
goto again
|
||||
}
|
||||
bk := msg.Payload
|
||||
|
||||
// logs.Info(bk, msg.Pattern, msg.Channel, msg.Payload)
|
||||
if bk := strings.Split(bk, "."); len(bk) == 2 {
|
||||
bucket, key := bk[0], bk[1]
|
||||
for _, listen := range storage.Listens {
|
||||
if listen.Name == bucket && (listen.Key == key || listen.Key == "*") {
|
||||
str, err := db.HGet(ctx, bucket, key).Result()
|
||||
if err == nil {
|
||||
listen.Handle("", str, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
return sillyplus
|
||||
}
|
||||
|
||||
func (bucket *Bucket) Copy(name string) storage.Bucket {
|
||||
return &Bucket{name: name}
|
||||
}
|
||||
|
||||
func (bucket *Bucket) Buckets() []string {
|
||||
var r []string
|
||||
r, _ = db.Keys(ctx, "*").Result()
|
||||
var e []string
|
||||
for _, v := range r {
|
||||
if strings.HasSuffix(v, "_Sequence") {
|
||||
continue
|
||||
}
|
||||
if regexp.MustCompile(`^\d+@\d+$`).FindString(v) != "" {
|
||||
continue
|
||||
}
|
||||
e = append(e, v)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (bucket *Bucket) Delete() error {
|
||||
db.Del(ctx, bucket.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bucket *Bucket) GetName() string {
|
||||
return bucket.name
|
||||
}
|
||||
|
||||
func Set(key string, value string, expiration time.Duration) error {
|
||||
db.Set(ctx, key, value, expiration)
|
||||
return nil
|
||||
}
|
||||
|
||||
func Get(key string) string {
|
||||
v, _ := db.Get(ctx, key).Result()
|
||||
return v
|
||||
}
|
||||
|
||||
func (bucket *Bucket) Set(key interface{}, value interface{}) (string, error) {
|
||||
new := ""
|
||||
msg := ""
|
||||
k := fmt.Sprint(key)
|
||||
switch value := value.(type) {
|
||||
case []byte:
|
||||
new = string(value)
|
||||
case string:
|
||||
new = value
|
||||
case nil:
|
||||
default:
|
||||
new = fmt.Sprint(value)
|
||||
}
|
||||
if !utils.SlaveMode {
|
||||
var handles []func(string, string, string) *storage.Final
|
||||
for _, listen := range storage.Listens {
|
||||
if listen.Name == bucket.name && (listen.Key == key || listen.Key == "*") {
|
||||
handles = append(handles, listen.Handle)
|
||||
}
|
||||
}
|
||||
if len(handles) > 0 {
|
||||
old := bucket.GetString(key)
|
||||
if old == new {
|
||||
return msg, nil
|
||||
}
|
||||
for _, handle := range handles {
|
||||
fin := handle(old, new, k)
|
||||
if fin != nil {
|
||||
if fin.Message != "" {
|
||||
msg = fin.Message
|
||||
}
|
||||
if fin.Error != nil {
|
||||
return msg, fin.Error
|
||||
}
|
||||
if fin.Now != "" {
|
||||
new = fin.Now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if new == "" {
|
||||
return msg, db.HDel(ctx, bucket.name, k).Err()
|
||||
} else {
|
||||
return msg, db.HSet(ctx, bucket.name, k, new).Err()
|
||||
}
|
||||
} else {
|
||||
return toMaster(bucket.name, k, new)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (bucket *Bucket) GetString(kv ...interface{}) string {
|
||||
var key, value string
|
||||
for i := range kv {
|
||||
if i == 0 {
|
||||
key = fmt.Sprint(kv[0])
|
||||
} else {
|
||||
value = fmt.Sprint(kv[1])
|
||||
}
|
||||
}
|
||||
cs := db.HGet(ctx, bucket.name, key)
|
||||
if cs != nil {
|
||||
v, _ := cs.Result()
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (bucket *Bucket) GetBytes(key string) (v []byte) {
|
||||
cs := db.HGet(ctx, bucket.name, key)
|
||||
if cs != nil {
|
||||
v, _ = cs.Bytes()
|
||||
if len(v) != 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (bucket Bucket) GetInt(key string, vs ...int) int {
|
||||
var value int
|
||||
if len(vs) != 0 {
|
||||
value = vs[0]
|
||||
}
|
||||
cs := db.HGet(ctx, bucket.name, key)
|
||||
if cs != nil {
|
||||
v, _ := cs.Result()
|
||||
if v != "" {
|
||||
return utils.Int(v)
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (bucket Bucket) GetBool(key string, vs ...bool) bool {
|
||||
var value bool
|
||||
if len(vs) != 0 {
|
||||
value = vs[0]
|
||||
}
|
||||
cs := db.HGet(ctx, bucket.name, key)
|
||||
if cs != nil {
|
||||
v, _ := cs.Result()
|
||||
if v == "true" {
|
||||
value = true
|
||||
} else if v == "false" {
|
||||
value = false
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (bucket Bucket) Foreach(f func(k, v []byte) error) {
|
||||
vs, _ := db.HGetAll(ctx, bucket.name).Result()
|
||||
for key, value := range vs {
|
||||
f([]byte(key), []byte(value))
|
||||
}
|
||||
}
|
||||
func (bucket Bucket) Create(i interface{}) error {
|
||||
sq, err := db.Incr(ctx, bucket.name+"_Sequence").Uint64()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
s := reflect.ValueOf(i).Elem()
|
||||
id := s.FieldByName("ID")
|
||||
sequence := s.FieldByName("Sequence")
|
||||
if _, ok := id.Interface().(int); ok {
|
||||
key := id.Int()
|
||||
if key == 0 {
|
||||
key = int64(sq)
|
||||
id.SetInt(key)
|
||||
}
|
||||
if sequence != reflect.ValueOf(nil) {
|
||||
sequence.SetInt(int64(sq))
|
||||
}
|
||||
buf, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.HSet(ctx, bucket.name, key, buf).Err()
|
||||
} else {
|
||||
key := id.String()
|
||||
if key == "" {
|
||||
key = fmt.Sprint(sq)
|
||||
id.SetString(key)
|
||||
}
|
||||
if sequence != reflect.ValueOf(nil) {
|
||||
sequence.SetInt(int64(sq))
|
||||
}
|
||||
buf, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.HSet(ctx, bucket.name, key, buf).Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (bucket Bucket) First(i interface{}) error {
|
||||
var err error
|
||||
s := reflect.ValueOf(i).Elem()
|
||||
id := s.FieldByName("ID")
|
||||
if v, ok := id.Interface().(int); ok {
|
||||
data, _ := db.HGet(ctx, bucket.name, fmt.Sprint(v)).Bytes()
|
||||
if len(data) == 0 {
|
||||
err = errors.New("record not find")
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, i)
|
||||
} else {
|
||||
data, _ := db.HGet(ctx, bucket.name, id.Interface().(string)).Bytes()
|
||||
if len(data) == 0 {
|
||||
err = errors.New("record not find")
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, i)
|
||||
}
|
||||
}
|
||||
|
||||
func (bucket Bucket) Size() (size int64, err error) {
|
||||
var ks []string
|
||||
ks, err = db.HKeys(ctx, bucket.name).Result()
|
||||
return int64(len(ks)), err
|
||||
}
|
||||
|
||||
func (bucket Bucket) IsEmpty() (empty bool, err error) {
|
||||
var ks []string
|
||||
ks, err = db.HKeys(ctx, bucket.name).Result()
|
||||
return len(ks) == 0, err
|
||||
}
|
||||
|
||||
func (bucket *Bucket) Keys() ([]string, error) {
|
||||
return db.HKeys(ctx, bucket.name).Result()
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package core
|
||||
|
||||
import "net/http"
|
||||
|
||||
var Transport *http.Transport
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
newVersion=`wget -q -O - https://raw.githubusercontent.com/cdle/binary/main/compile_time.go | tr -cd "[0-9]"`
|
||||
oldVersion=`cat /root/amd64/compile_time.go | tr -cd "[0-9]"`
|
||||
if [[ ${#newVersion} == 13 && $newVersion != $oldVersion ]]; then
|
||||
rm -rf /root/binary
|
||||
cd /root
|
||||
git clone git@github.com:cdle/binary.git
|
||||
|
||||
cd /root/amd64
|
||||
git checkout --orphan latest_branch
|
||||
rm -rf *
|
||||
cp /root/binary/sillyplus_linux_amd64_$newVersion /root/amd64
|
||||
cp /root/binary/compile_time.go /root/amd64
|
||||
git add -A
|
||||
git commit -am "commit message"
|
||||
git branch -D main
|
||||
git branch -m main
|
||||
git push -f origin main
|
||||
|
||||
cd /root/arm64
|
||||
git checkout --orphan latest_branch
|
||||
rm -rf *
|
||||
cp /root/binary/sillyplus_linux_arm64_$newVersion /root/arm64
|
||||
cp /root/binary/compile_time.go /root/arm64
|
||||
git add -A
|
||||
git commit -am "commit message"
|
||||
git branch -D main
|
||||
git branch -m main
|
||||
git push -f origin main
|
||||
|
||||
rm -rf /root/binary
|
||||
fi
|
||||
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cdle/sillyplus/core/logs"
|
||||
"github.com/cdle/sillyplus/core/storage"
|
||||
"github.com/cdle/sillyplus/utils"
|
||||
"github.com/gin-contrib/gzip"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// //go:embed admin/*
|
||||
// var static embed.FS
|
||||
|
||||
var Handle = make(map[string]func(c *gin.Context))
|
||||
|
||||
func Cors() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
method := c.Request.Method
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token")
|
||||
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") //服务器支持的所有跨域请求的方
|
||||
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
if method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
}
|
||||
// 处理请求
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
var Server = gin.New()
|
||||
|
||||
func init() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
// Server.Use(gin.Recovery())
|
||||
Server.Use(Cors())
|
||||
Server.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||
Server.NoRoute(func(c *gin.Context) {
|
||||
if c.Request.URL.Path != "/api/web_chat" {
|
||||
logs.Debug(c.Request.URL.Path)
|
||||
}
|
||||
c.Status(200)
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/admin") {
|
||||
// if file, err := static.Open(strings.Trim(c.Request.URL.Path, "/")); err == nil {
|
||||
// fs, _ := file.Stat()
|
||||
// if !fs.IsDir() {
|
||||
// defer file.Close()
|
||||
// c.Header("cache-control", "max-age=864000")
|
||||
// io.Copy(c.Writer, file)
|
||||
// return
|
||||
// } else {
|
||||
// file.Close()
|
||||
// }
|
||||
// }
|
||||
// data, err := static.ReadFile("admin/index.html")
|
||||
// if err == nil {
|
||||
// c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
// c.Writer.Write(data)
|
||||
// return
|
||||
// }
|
||||
}
|
||||
for _, req := range ss {
|
||||
if c.Request.URL.Path == req.Path && (req.Method == c.Request.Method || req.Method == "ANY") {
|
||||
req.Handle(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
uuid, _ := c.Cookie("uuid")
|
||||
if uuid == "" {
|
||||
uuid = utils.GenUUID()
|
||||
c.SetCookie("uuid", uuid, 8640000, "/", "", false, true)
|
||||
}
|
||||
var req = &Request{
|
||||
c: c,
|
||||
uuid: uuid,
|
||||
}
|
||||
var res = &Response{
|
||||
c: c,
|
||||
}
|
||||
for _, web := range webs {
|
||||
if web.handles == nil {
|
||||
continue
|
||||
}
|
||||
if web.method == c.Request.Method {
|
||||
var matched = web.path == c.Request.URL.Path
|
||||
if !matched && strings.HasPrefix(web.path, "^") {
|
||||
reg, err := regexp.Compile(web.path)
|
||||
if err != nil {
|
||||
console.Error(err)
|
||||
continue
|
||||
}
|
||||
req.ress = reg.FindAllStringSubmatch(c.Request.URL.Path, -1)
|
||||
matched = len(req.ress) != 0
|
||||
}
|
||||
if matched {
|
||||
req.handled = true
|
||||
func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if fmt.Sprint(err) != "stop" {
|
||||
console.Error(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
for _, handle := range web.handles {
|
||||
handle(req, res)
|
||||
}
|
||||
}()
|
||||
if req.handled {
|
||||
if res.isRedirect {
|
||||
return
|
||||
}
|
||||
if res.isJson {
|
||||
c.Header("Content-Type", "application/json")
|
||||
}
|
||||
c.String(res.status, res.content)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.String(404, "页面被喵咪劫走了。") //
|
||||
//开启代理模式
|
||||
|
||||
// handleHTTP(c.Writer, c.Request)
|
||||
})
|
||||
|
||||
port := sillyGirl.GetString("port", "8080")
|
||||
srvs := []*http.Server{{
|
||||
Addr: ":" + port,
|
||||
Handler: Server,
|
||||
}}
|
||||
storage.Watch(sillyGirl, "port", func(old, new, key string) *storage.Final {
|
||||
if new == "" {
|
||||
new = "8080"
|
||||
}
|
||||
if old == new {
|
||||
return nil
|
||||
}
|
||||
port := new
|
||||
console.Log("port", new)
|
||||
srv := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: Server,
|
||||
}
|
||||
var ch = make(chan error, 1)
|
||||
srvs = append(srvs, srv)
|
||||
go func() {
|
||||
logs.Info("Http服务(%s)重新运行。", port)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logs.Error("Http服务(%s)运行失败:%s", port, err.Error())
|
||||
ch <- err
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case err := <-ch:
|
||||
srvs = srvs[:len(srvs)-1]
|
||||
return &storage.Final{
|
||||
Error: err,
|
||||
}
|
||||
case <-time.After(1 * time.Millisecond * 100):
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := srvs[0].Shutdown(ctx); err == nil {
|
||||
logs.Info("Http服务(%s)关闭。", old)
|
||||
}
|
||||
srvs = srvs[1:]
|
||||
}
|
||||
return &storage.Final{
|
||||
Now: new,
|
||||
}
|
||||
})
|
||||
go func() {
|
||||
logs.Info("Http服务(%s)开始运行。", port)
|
||||
if err := srvs[0].ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logs.Error("Http服务运行失败:%s。", err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// var httpProxys = MakeBucket("httpProxys")
|
||||
|
||||
func handleHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// console.Info("%s", utils.JsonMarshal(req.Header))
|
||||
// u, err := url.Parse("addr")
|
||||
// if err != nil {
|
||||
// core.Logs.Warn("can't connect to the http proxy:", err)
|
||||
// return
|
||||
// }
|
||||
// Transport = &http.Transport{Proxy: http.ProxyURL(u)}
|
||||
// resp, err := Transport.RoundTrip(req)
|
||||
// fmt.Println("====")
|
||||
resp, err := http.DefaultTransport.RoundTrip(req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
copyHeader(w.Header(), resp.Header)
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
||||
|
||||
func copyHeader(dst, src http.Header) {
|
||||
for k, vv := range src {
|
||||
for _, v := range vv {
|
||||
dst.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Req struct {
|
||||
Method string
|
||||
Path string
|
||||
Handle func(c *gin.Context)
|
||||
}
|
||||
|
||||
var ss = []Req{}
|
||||
|
||||
type Auth struct {
|
||||
ID int
|
||||
IP string
|
||||
UserAgent string
|
||||
Token string
|
||||
CreatedAt int
|
||||
ExpiredAt int
|
||||
}
|
||||
|
||||
const (
|
||||
GET = "GET"
|
||||
POST = "POST"
|
||||
DELETE = "DELETE"
|
||||
PUT = "PUT"
|
||||
ANY = "ANY"
|
||||
)
|
||||
|
||||
func GinApi(method string, path string, fs ...func(c *gin.Context)) {
|
||||
ss = append(ss, Req{
|
||||
Method: method,
|
||||
Path: path,
|
||||
Handle: func(c *gin.Context) {
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
for _, f := range fs {
|
||||
f(c)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user