diff --git a/core/adapter.go b/core/adapter.go new file mode 100644 index 0000000..9e302b2 --- /dev/null +++ b/core/adapter.go @@ -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()) +} diff --git a/core/api_auth.go b/core/api_auth.go new file mode 100644 index 0000000..8ae5f68 --- /dev/null +++ b/core/api_auth.go @@ -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 +} diff --git a/core/api_storage.go b/core/api_storage.go new file mode 100644 index 0000000..ca2b796 --- /dev/null +++ b/core/api_storage.go @@ -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, + }) + }) +} diff --git a/core/base_sender.go b/core/base_sender.go new file mode 100644 index 0000000..a411f81 --- /dev/null +++ b/core/base_sender.go @@ -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 + } + } +} diff --git a/core/bucket.go b/core/bucket.go new file mode 100644 index 0000000..5bf15db --- /dev/null +++ b/core/bucket.go @@ -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) +} diff --git a/core/carry.go b/core/carry.go new file mode 100644 index 0000000..c293a79 --- /dev/null +++ b/core/carry.go @@ -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 "" +} diff --git a/core/cdle.go b/core/cdle.go new file mode 100644 index 0000000..2819952 --- /dev/null +++ b/core/cdle.go @@ -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"` +// } diff --git a/core/common/function.go b/core/common/function.go new file mode 100644 index 0000000..a64c5bc --- /dev/null +++ b/core/common/function.go @@ -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"` +} diff --git a/core/common/sender.go b/core/common/sender.go new file mode 100644 index 0000000..e97c157 --- /dev/null +++ b/core/common/sender.go @@ -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 +} diff --git a/core/compile_time.go b/core/compile_time.go new file mode 100644 index 0000000..6b1d2e8 --- /dev/null +++ b/core/compile_time.go @@ -0,0 +1,3 @@ +package core + +var compiled_at = "" diff --git a/core/config.yaml.go b/core/config.yaml.go new file mode 100644 index 0000000..f03f9a6 --- /dev/null +++ b/core/config.yaml.go @@ -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 diff --git a/core/cron.go b/core/cron.go new file mode 100644 index 0000000..4f5dde3 --- /dev/null +++ b/core/cron.go @@ -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() +} diff --git a/core/encrypt.go b/core/encrypt.go new file mode 100644 index 0000000..cff3a16 --- /dev/null +++ b/core/encrypt.go @@ -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 +} diff --git a/core/function.go b/core/function.go new file mode 100644 index 0000000..06cea30 --- /dev/null +++ b/core/function.go @@ -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 +} diff --git a/core/init.go b/core/init.go new file mode 100644 index 0000000..9ffb219 --- /dev/null +++ b/core/init.go @@ -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) + } +} diff --git a/core/logs/README.md b/core/logs/README.md new file mode 100644 index 0000000..173b464 --- /dev/null +++ b/core/logs/README.md @@ -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) +``` diff --git a/core/logs/access_log.go b/core/logs/access_log.go new file mode 100644 index 0000000..10455fe --- /dev/null +++ b/core/logs/access_log.go @@ -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 +} diff --git a/core/logs/access_log_test.go b/core/logs/access_log_test.go new file mode 100644 index 0000000..f78a00a --- /dev/null +++ b/core/logs/access_log_test.go @@ -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) +} diff --git a/core/logs/alils/alils.go b/core/logs/alils/alils.go new file mode 100644 index 0000000..c2e623c --- /dev/null +++ b/core/logs/alils/alils.go @@ -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) +} diff --git a/core/logs/alils/config.go b/core/logs/alils/config.go new file mode 100644 index 0000000..d0b67c2 --- /dev/null +++ b/core/logs/alils/config.go @@ -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" +) diff --git a/core/logs/alils/log.pb.go b/core/logs/alils/log.pb.go new file mode 100644 index 0000000..d57f36b --- /dev/null +++ b/core/logs/alils/log.pb.go @@ -0,0 +1,1046 @@ +package alils + +import ( + "fmt" + "io" + "math" + + "github.com/gogo/protobuf/proto" + github_com_gogo_protobuf_proto "github.com/gogo/protobuf/proto" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal + +var _ = fmt.Errorf + +var _ = math.Inf + +var ( + // ErrInvalidLengthLog invalid proto + ErrInvalidLengthLog = fmt.Errorf("proto: negative length found during unmarshaling") + // ErrIntOverflowLog overflow + ErrIntOverflowLog = fmt.Errorf("proto: integer overflow") +) + +// Log define the proto Log +type Log struct { + Time *uint32 `protobuf:"varint,1,req,name=Time" json:"Time,omitempty"` + Contents []*LogContent `protobuf:"bytes,2,rep,name=Contents" json:"Contents,omitempty"` + XXXUnrecognized []byte `json:"-"` +} + +// Reset the Log +func (m *Log) Reset() { *m = Log{} } + +// String returns the Compact Log +func (m *Log) String() string { return proto.CompactTextString(m) } + +// ProtoMessage not implemented +func (*Log) ProtoMessage() {} + +// GetTime returns the Log's Time +func (m *Log) GetTime() uint32 { + if m != nil && m.Time != nil { + return *m.Time + } + return 0 +} + +// GetContents returns the Log's Contents +func (m *Log) GetContents() []*LogContent { + if m != nil { + return m.Contents + } + return nil +} + +// LogContent defines the Log content struct +type LogContent struct { + Key *string `protobuf:"bytes,1,req,name=Key" json:"Key,omitempty"` + Value *string `protobuf:"bytes,2,req,name=Value" json:"Value,omitempty"` + XXXUnrecognized []byte `json:"-"` +} + +// Reset LogContent +func (m *LogContent) Reset() { *m = LogContent{} } + +// String returns the compact text +func (m *LogContent) String() string { return proto.CompactTextString(m) } + +// ProtoMessage not implemented +func (*LogContent) ProtoMessage() {} + +// GetKey returns the key +func (m *LogContent) GetKey() string { + if m != nil && m.Key != nil { + return *m.Key + } + return "" +} + +// GetValue returns the value +func (m *LogContent) GetValue() string { + if m != nil && m.Value != nil { + return *m.Value + } + return "" +} + +// LogGroup defines the logs struct +type LogGroup struct { + Logs []*Log `protobuf:"bytes,1,rep,name=Logs" json:"Logs,omitempty"` + Reserved *string `protobuf:"bytes,2,opt,name=Reserved" json:"Reserved,omitempty"` + Topic *string `protobuf:"bytes,3,opt,name=Topic" json:"Topic,omitempty"` + Source *string `protobuf:"bytes,4,opt,name=Source" json:"Source,omitempty"` + XXXUnrecognized []byte `json:"-"` +} + +// Reset LogGroup +func (m *LogGroup) Reset() { *m = LogGroup{} } + +// String returns the compact text +func (m *LogGroup) String() string { return proto.CompactTextString(m) } + +// ProtoMessage not implemented +func (*LogGroup) ProtoMessage() {} + +// GetLogs returns the loggroup logs +func (m *LogGroup) GetLogs() []*Log { + if m != nil { + return m.Logs + } + return nil +} + +// GetReserved returns Reserved. An empty string is returned +// if an error occurs +func (m *LogGroup) GetReserved() string { + if m != nil && m.Reserved != nil { + return *m.Reserved + } + return "" +} + +// GetTopic returns Topic. An empty string is returned +// if an error occurs +func (m *LogGroup) GetTopic() string { + if m != nil && m.Topic != nil { + return *m.Topic + } + return "" +} + +// GetSource returns source. An empty string is returned +// if an error occurs +func (m *LogGroup) GetSource() string { + if m != nil && m.Source != nil { + return *m.Source + } + return "" +} + +// LogGroupList defines the LogGroups +type LogGroupList struct { + LogGroups []*LogGroup `protobuf:"bytes,1,rep,name=logGroups" json:"logGroups,omitempty"` + XXXUnrecognized []byte `json:"-"` +} + +// Reset LogGroupList +func (m *LogGroupList) Reset() { *m = LogGroupList{} } + +// String returns compact text +func (m *LogGroupList) String() string { return proto.CompactTextString(m) } + +// ProtoMessage not implemented +func (*LogGroupList) ProtoMessage() {} + +// GetLogGroups returns the LogGroups +func (m *LogGroupList) GetLogGroups() []*LogGroup { + if m != nil { + return m.LogGroups + } + return nil +} + +// Marshal marshals the logs to byte slice +func (m *Log) Marshal() (data []byte, err error) { + size := m.Size() + data = make([]byte, size) + n, err := m.MarshalTo(data) + if err != nil { + return nil, err + } + return data[:n], nil +} + +// MarshalTo data +func (m *Log) MarshalTo(data []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if m.Time == nil { + return 0, github_com_gogo_protobuf_proto.NewRequiredNotSetError("Time") + } + data[i] = 0x8 + i++ + i = encodeVarintLog(data, i, uint64(*m.Time)) + if len(m.Contents) > 0 { + for _, msg := range m.Contents { + data[i] = 0x12 + i++ + i = encodeVarintLog(data, i, uint64(msg.Size())) + n, err := msg.MarshalTo(data[i:]) + if err != nil { + return 0, err + } + i += n + } + } + if m.XXXUnrecognized != nil { + i += copy(data[i:], m.XXXUnrecognized) + } + return i, nil +} + +// Marshal LogContent +func (m *LogContent) Marshal() (data []byte, err error) { + size := m.Size() + data = make([]byte, size) + n, err := m.MarshalTo(data) + if err != nil { + return nil, err + } + return data[:n], nil +} + +// MarshalTo logcontent to data +func (m *LogContent) MarshalTo(data []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if m.Key == nil { + return 0, github_com_gogo_protobuf_proto.NewRequiredNotSetError("Key") + } + data[i] = 0xa + i++ + i = encodeVarintLog(data, i, uint64(len(*m.Key))) + i += copy(data[i:], *m.Key) + + if m.Value == nil { + return 0, github_com_gogo_protobuf_proto.NewRequiredNotSetError("Value") + } + data[i] = 0x12 + i++ + i = encodeVarintLog(data, i, uint64(len(*m.Value))) + i += copy(data[i:], *m.Value) + if m.XXXUnrecognized != nil { + i += copy(data[i:], m.XXXUnrecognized) + } + return i, nil +} + +// Marshal LogGroup +func (m *LogGroup) Marshal() (data []byte, err error) { + size := m.Size() + data = make([]byte, size) + n, err := m.MarshalTo(data) + if err != nil { + return nil, err + } + return data[:n], nil +} + +// MarshalTo LogGroup to data +func (m *LogGroup) MarshalTo(data []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.Logs) > 0 { + for _, msg := range m.Logs { + data[i] = 0xa + i++ + i = encodeVarintLog(data, i, uint64(msg.Size())) + n, err := msg.MarshalTo(data[i:]) + if err != nil { + return 0, err + } + i += n + } + } + if m.Reserved != nil { + data[i] = 0x12 + i++ + i = encodeVarintLog(data, i, uint64(len(*m.Reserved))) + i += copy(data[i:], *m.Reserved) + } + if m.Topic != nil { + data[i] = 0x1a + i++ + i = encodeVarintLog(data, i, uint64(len(*m.Topic))) + i += copy(data[i:], *m.Topic) + } + if m.Source != nil { + data[i] = 0x22 + i++ + i = encodeVarintLog(data, i, uint64(len(*m.Source))) + i += copy(data[i:], *m.Source) + } + if m.XXXUnrecognized != nil { + i += copy(data[i:], m.XXXUnrecognized) + } + return i, nil +} + +// Marshal LogGroupList +func (m *LogGroupList) Marshal() (data []byte, err error) { + size := m.Size() + data = make([]byte, size) + n, err := m.MarshalTo(data) + if err != nil { + return nil, err + } + return data[:n], nil +} + +// MarshalTo LogGroupList to data +func (m *LogGroupList) MarshalTo(data []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.LogGroups) > 0 { + for _, msg := range m.LogGroups { + data[i] = 0xa + i++ + i = encodeVarintLog(data, i, uint64(msg.Size())) + n, err := msg.MarshalTo(data[i:]) + if err != nil { + return 0, err + } + i += n + } + } + if m.XXXUnrecognized != nil { + i += copy(data[i:], m.XXXUnrecognized) + } + return i, nil +} + +func encodeFixed64Log(data []byte, offset int, v uint64) int { + data[offset] = uint8(v) + data[offset+1] = uint8(v >> 8) + data[offset+2] = uint8(v >> 16) + data[offset+3] = uint8(v >> 24) + data[offset+4] = uint8(v >> 32) + data[offset+5] = uint8(v >> 40) + data[offset+6] = uint8(v >> 48) + data[offset+7] = uint8(v >> 56) + return offset + 8 +} + +func encodeFixed32Log(data []byte, offset int, v uint32) int { + data[offset] = uint8(v) + data[offset+1] = uint8(v >> 8) + data[offset+2] = uint8(v >> 16) + data[offset+3] = uint8(v >> 24) + return offset + 4 +} + +func encodeVarintLog(data []byte, offset int, v uint64) int { + for v >= 1<<7 { + data[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + data[offset] = uint8(v) + return offset + 1 +} + +// Size returns the log's size +func (m *Log) Size() (n int) { + var l int + _ = l + if m.Time != nil { + n += 1 + sovLog(uint64(*m.Time)) + } + if len(m.Contents) > 0 { + for _, e := range m.Contents { + l = e.Size() + n += 1 + l + sovLog(uint64(l)) + } + } + if m.XXXUnrecognized != nil { + n += len(m.XXXUnrecognized) + } + return n +} + +// Size returns LogContent size based on Key and Value +func (m *LogContent) Size() (n int) { + var l int + _ = l + if m.Key != nil { + l = len(*m.Key) + n += 1 + l + sovLog(uint64(l)) + } + if m.Value != nil { + l = len(*m.Value) + n += 1 + l + sovLog(uint64(l)) + } + if m.XXXUnrecognized != nil { + n += len(m.XXXUnrecognized) + } + return n +} + +// Size returns LogGroup size based on Logs +func (m *LogGroup) Size() (n int) { + var l int + _ = l + if len(m.Logs) > 0 { + for _, e := range m.Logs { + l = e.Size() + n += 1 + l + sovLog(uint64(l)) + } + } + if m.Reserved != nil { + l = len(*m.Reserved) + n += 1 + l + sovLog(uint64(l)) + } + if m.Topic != nil { + l = len(*m.Topic) + n += 1 + l + sovLog(uint64(l)) + } + if m.Source != nil { + l = len(*m.Source) + n += 1 + l + sovLog(uint64(l)) + } + if m.XXXUnrecognized != nil { + n += len(m.XXXUnrecognized) + } + return n +} + +// Size returns LogGroupList size +func (m *LogGroupList) Size() (n int) { + var l int + _ = l + if len(m.LogGroups) > 0 { + for _, e := range m.LogGroups { + l = e.Size() + n += 1 + l + sovLog(uint64(l)) + } + } + if m.XXXUnrecognized != nil { + n += len(m.XXXUnrecognized) + } + return n +} + +func sovLog(x uint64) (n int) { + for { + n++ + x >>= 7 + if x == 0 { + break + } + } + return n +} + +func sozLog(x uint64) (n int) { + return sovLog((x << 1) ^ (x >> 63)) +} + +// Unmarshal unmarshals data to log +func (m *Log) Unmarshal(data []byte) error { + var hasFields [1]uint64 + l := len(data) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLog + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Log: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Log: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Time", wireType) + } + var v uint32 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLog + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + v |= (uint32(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + m.Time = &v + hasFields[0] |= uint64(0x00000001) + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Contents", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLog + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthLog + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Contents = append(m.Contents, &LogContent{}) + if err := m.Contents[len(m.Contents)-1].Unmarshal(data[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipLog(data[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthLog + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXXUnrecognized = append(m.XXXUnrecognized, data[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + if hasFields[0]&uint64(0x00000001) == 0 { + return github_com_gogo_protobuf_proto.NewRequiredNotSetError("Time") + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} + +// Unmarshal unmarshals data to LogContent +func (m *LogContent) Unmarshal(data []byte) error { + var hasFields [1]uint64 + l := len(data) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLog + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Content: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Content: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLog + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLog + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + s := string(data[iNdEx:postIndex]) + m.Key = &s + iNdEx = postIndex + hasFields[0] |= uint64(0x00000001) + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLog + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLog + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + s := string(data[iNdEx:postIndex]) + m.Value = &s + iNdEx = postIndex + hasFields[0] |= uint64(0x00000002) + default: + iNdEx = preIndex + skippy, err := skipLog(data[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthLog + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXXUnrecognized = append(m.XXXUnrecognized, data[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + if hasFields[0]&uint64(0x00000001) == 0 { + return github_com_gogo_protobuf_proto.NewRequiredNotSetError("Key") + } + if hasFields[0]&uint64(0x00000002) == 0 { + return github_com_gogo_protobuf_proto.NewRequiredNotSetError("Value") + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} + +// Unmarshal unmarshals data to LogGroup +func (m *LogGroup) Unmarshal(data []byte) error { + l := len(data) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLog + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: LogGroup: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: LogGroup: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Logs", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLog + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthLog + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Logs = append(m.Logs, &Log{}) + if err := m.Logs[len(m.Logs)-1].Unmarshal(data[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Reserved", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLog + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLog + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + s := string(data[iNdEx:postIndex]) + m.Reserved = &s + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Topic", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLog + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLog + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + s := string(data[iNdEx:postIndex]) + m.Topic = &s + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Source", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLog + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLog + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + s := string(data[iNdEx:postIndex]) + m.Source = &s + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipLog(data[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthLog + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXXUnrecognized = append(m.XXXUnrecognized, data[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} + +// Unmarshal unmarshals data to LogGroupList +func (m *LogGroupList) Unmarshal(data []byte) error { + l := len(data) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLog + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: LogGroupList: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: LogGroupList: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LogGroups", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLog + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthLog + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.LogGroups = append(m.LogGroups, &LogGroup{}) + if err := m.LogGroups[len(m.LogGroups)-1].Unmarshal(data[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipLog(data[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthLog + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXXUnrecognized = append(m.XXXUnrecognized, data[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} + +func skipLog(data []byte) (n int, err error) { + l := len(data) + iNdEx := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowLog + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowLog + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if data[iNdEx-1] < 0x80 { + break + } + } + return iNdEx, nil + case 1: + iNdEx += 8 + return iNdEx, nil + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowLog + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + iNdEx += length + if length < 0 { + return 0, ErrInvalidLengthLog + } + return iNdEx, nil + case 3: + for { + var innerWire uint64 + var start = iNdEx + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowLog + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + innerWire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + innerWireType := int(innerWire & 0x7) + if innerWireType == 4 { + break + } + next, err := skipLog(data[start:]) + if err != nil { + return 0, err + } + iNdEx = start + next + } + return iNdEx, nil + case 4: + return iNdEx, nil + case 5: + iNdEx += 4 + return iNdEx, nil + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + } + panic("unreachable") +} diff --git a/core/logs/alils/log_config.go b/core/logs/alils/log_config.go new file mode 100644 index 0000000..7daeb86 --- /dev/null +++ b/core/logs/alils/log_config.go @@ -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 +} diff --git a/core/logs/alils/log_project.go b/core/logs/alils/log_project.go new file mode 100644 index 0000000..f993003 --- /dev/null +++ b/core/logs/alils/log_project.go @@ -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 +} diff --git a/core/logs/alils/log_store.go b/core/logs/alils/log_store.go new file mode 100644 index 0000000..fd9d9ea --- /dev/null +++ b/core/logs/alils/log_store.go @@ -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 +} diff --git a/core/logs/alils/machine_group.go b/core/logs/alils/machine_group.go new file mode 100644 index 0000000..101faeb --- /dev/null +++ b/core/logs/alils/machine_group.go @@ -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 +} diff --git a/core/logs/alils/request.go b/core/logs/alils/request.go new file mode 100644 index 0000000..50d9c43 --- /dev/null +++ b/core/logs/alils/request.go @@ -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 :" + 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 +} diff --git a/core/logs/alils/signature.go b/core/logs/alils/signature.go new file mode 100644 index 0000000..2d61130 --- /dev/null +++ b/core/logs/alils/signature.go @@ -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 +} diff --git a/core/logs/conn.go b/core/logs/conn.go new file mode 100644 index 0000000..cfeb3f9 --- /dev/null +++ b/core/logs/conn.go @@ -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) +} diff --git a/core/logs/conn_test.go b/core/logs/conn_test.go new file mode 100644 index 0000000..85b95c1 --- /dev/null +++ b/core/logs/conn_test.go @@ -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) +} diff --git a/core/logs/console.go b/core/logs/console.go new file mode 100644 index 0000000..ff4fcf4 --- /dev/null +++ b/core/logs/console.go @@ -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) +} diff --git a/core/logs/console_test.go b/core/logs/console_test.go new file mode 100644 index 0000000..2f2be0e --- /dev/null +++ b/core/logs/console_test.go @@ -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) +} diff --git a/core/logs/es/es.go b/core/logs/es/es.go new file mode 100644 index 0000000..f5734de --- /dev/null +++ b/core/logs/es/es.go @@ -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) +} diff --git a/core/logs/es/index.go b/core/logs/es/index.go new file mode 100644 index 0000000..21a7a26 --- /dev/null +++ b/core/logs/es/index.go @@ -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()) +} diff --git a/core/logs/es/index_test.go b/core/logs/es/index_test.go new file mode 100644 index 0000000..8483fae --- /dev/null +++ b/core/logs/es/index_test.go @@ -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) +} diff --git a/core/logs/file.go b/core/logs/file.go new file mode 100644 index 0000000..3234a67 --- /dev/null +++ b/core/logs/file.go @@ -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) +} diff --git a/core/logs/file_test.go b/core/logs/file_test.go new file mode 100644 index 0000000..6aac919 --- /dev/null +++ b/core/logs/file_test.go @@ -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) +} diff --git a/core/logs/formatter.go b/core/logs/formatter.go new file mode 100644 index 0000000..6fa4f70 --- /dev/null +++ b/core/logs/formatter.go @@ -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 +} diff --git a/core/logs/formatter_test.go b/core/logs/formatter_test.go new file mode 100644 index 0000000..f78a286 --- /dev/null +++ b/core/logs/formatter_test.go @@ -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) + } +} diff --git a/core/logs/jianliao.go b/core/logs/jianliao.go new file mode 100644 index 0000000..95835c0 --- /dev/null +++ b/core/logs/jianliao.go @@ -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) +} diff --git a/core/logs/jianliao_test.go b/core/logs/jianliao_test.go new file mode 100644 index 0000000..a1b2d07 --- /dev/null +++ b/core/logs/jianliao_test.go @@ -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) +} diff --git a/core/logs/log.go b/core/logs/log.go new file mode 100644 index 0000000..2bee81c --- /dev/null +++ b/core/logs/log.go @@ -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 +} diff --git a/core/logs/log_msg.go b/core/logs/log_msg.go new file mode 100644 index 0000000..f1a0b55 --- /dev/null +++ b/core/logs/log_msg.go @@ -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 +} diff --git a/core/logs/log_msg_test.go b/core/logs/log_msg_test.go new file mode 100644 index 0000000..dcc7e7d --- /dev/null +++ b/core/logs/log_msg_test.go @@ -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()) +} diff --git a/core/logs/log_test.go b/core/logs/log_test.go new file mode 100644 index 0000000..06f8b03 --- /dev/null +++ b/core/logs/log_test.go @@ -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() + }) + } +} diff --git a/core/logs/logger.go b/core/logs/logger.go new file mode 100644 index 0000000..29e33a8 --- /dev/null +++ b/core/logs/logger.go @@ -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 +} diff --git a/core/logs/logger_test.go b/core/logs/logger_test.go new file mode 100644 index 0000000..15be500 --- /dev/null +++ b/core/logs/logger_test.go @@ -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) + } +} diff --git a/core/logs/multifile.go b/core/logs/multifile.go new file mode 100644 index 0000000..009bd32 --- /dev/null +++ b/core/logs/multifile.go @@ -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) +} diff --git a/core/logs/multifile_test.go b/core/logs/multifile_test.go new file mode 100644 index 0000000..8050bc6 --- /dev/null +++ b/core/logs/multifile_test.go @@ -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) + } +} diff --git a/core/logs/slack.go b/core/logs/slack.go new file mode 100644 index 0000000..ce892a1 --- /dev/null +++ b/core/logs/slack.go @@ -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) +} diff --git a/core/logs/slack_test.go b/core/logs/slack_test.go new file mode 100644 index 0000000..31f5cf9 --- /dev/null +++ b/core/logs/slack_test.go @@ -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) +// } +// +// } diff --git a/core/logs/smtp.go b/core/logs/smtp.go new file mode 100644 index 0000000..40891a7 --- /dev/null +++ b/core/logs/smtp.go @@ -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) +} diff --git a/core/logs/smtp_test.go b/core/logs/smtp_test.go new file mode 100644 index 0000000..28e762d --- /dev/null +++ b/core/logs/smtp_test.go @@ -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) +} diff --git a/core/machineId.go b/core/machineId.go new file mode 100644 index 0000000..8fae764 --- /dev/null +++ b/core/machineId.go @@ -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 +} diff --git a/core/masters.go b/core/masters.go new file mode 100644 index 0000000..308d873 --- /dev/null +++ b/core/masters.go @@ -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, + }) + }) +} diff --git a/core/nickname.go b/core/nickname.go new file mode 100644 index 0000000..3105e88 --- /dev/null +++ b/core/nickname.go @@ -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, + }) + }) +} diff --git a/core/node_bucket.go b/core/node_bucket.go new file mode 100644 index 0000000..4d64d50 --- /dev/null +++ b/core/node_bucket.go @@ -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 +// }, +// }) +// }) diff --git a/core/node_buffer.go b/core/node_buffer.go new file mode 100644 index 0000000..17457bd --- /dev/null +++ b/core/node_buffer.go @@ -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 +} diff --git a/core/node_error.go b/core/node_error.go new file mode 100644 index 0000000..c708763 --- /dev/null +++ b/core/node_error.go @@ -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) +} diff --git a/core/node_headers.go b/core/node_headers.go new file mode 100644 index 0000000..bb86721 --- /dev/null +++ b/core/node_headers.go @@ -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 +} diff --git a/core/node_regexp.go b/core/node_regexp.go new file mode 100644 index 0000000..55e5ec5 --- /dev/null +++ b/core/node_regexp.go @@ -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 +} diff --git a/core/node_request.go b/core/node_request.go new file mode 100644 index 0000000..f5491a3 --- /dev/null +++ b/core/node_request.go @@ -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)) + } +} diff --git a/core/node_response.go b/core/node_response.go new file mode 100644 index 0000000..9698358 --- /dev/null +++ b/core/node_response.go @@ -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 +} diff --git a/core/node_transport.go b/core/node_transport.go new file mode 100644 index 0000000..de877d0 --- /dev/null +++ b/core/node_transport.go @@ -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") + } +} diff --git a/core/platforms.go b/core/platforms.go new file mode 100644 index 0000000..242542c --- /dev/null +++ b/core/platforms.go @@ -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 +} diff --git a/core/plugin_core.go b/core/plugin_core.go new file mode 100644 index 0000000..27f13ca --- /dev/null +++ b/core/plugin_core.go @@ -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) + } +} diff --git a/core/plugin_impl.go b/core/plugin_impl.go new file mode 100644 index 0000000..073c708 --- /dev/null +++ b/core/plugin_impl.go @@ -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 +} diff --git a/core/plugin_module.go b/core/plugin_module.go new file mode 100644 index 0000000..8164bb6 --- /dev/null +++ b/core/plugin_module.go @@ -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 +} diff --git a/core/plugin_publish.go b/core/plugin_publish.go new file mode 100644 index 0000000..9c4267a --- /dev/null +++ b/core/plugin_publish.go @@ -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 +} diff --git a/core/plugin_subscribe.go b/core/plugin_subscribe.go new file mode 100644 index 0000000..b21e8df --- /dev/null +++ b/core/plugin_subscribe.go @@ -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 +} diff --git a/core/plugin_utils.go b/core/plugin_utils.go new file mode 100644 index 0000000..eebd13f --- /dev/null +++ b/core/plugin_utils.go @@ -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) +} diff --git a/core/plugin_web_request.go b/core/plugin_web_request.go new file mode 100644 index 0000000..5e3ee29 --- /dev/null +++ b/core/plugin_web_request.go @@ -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 "" +} diff --git a/core/plugin_web_response.go b/core/plugin_web_response.go new file mode 100644 index 0000000..4ffda0e --- /dev/null +++ b/core/plugin_web_response.go @@ -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") +} diff --git a/core/reboot.go b/core/reboot.go new file mode 100644 index 0000000..3c075a7 --- /dev/null +++ b/core/reboot.go @@ -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) + } + } + }() +} diff --git a/core/reply.go b/core/reply.go new file mode 100644 index 0000000..8a90a69 --- /dev/null +++ b/core/reply.go @@ -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]) + }) +} diff --git a/core/storage/boltdb/init.go b/core/storage/boltdb/init.go new file mode 100644 index 0000000..3709f34 --- /dev/null +++ b/core/storage/boltdb/init.go @@ -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 +} diff --git a/core/storage/main.go b/core/storage/main.go new file mode 100644 index 0000000..2f1b1ee --- /dev/null +++ b/core/storage/main.go @@ -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) +} diff --git a/core/storage/redis/init.go b/core/storage/redis/init.go new file mode 100644 index 0000000..b920a33 --- /dev/null +++ b/core/storage/redis/init.go @@ -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() +} diff --git a/core/transport.go b/core/transport.go new file mode 100644 index 0000000..9d7bdfd --- /dev/null +++ b/core/transport.go @@ -0,0 +1,5 @@ +package core + +import "net/http" + +var Transport *http.Transport diff --git a/core/update.sh b/core/update.sh new file mode 100644 index 0000000..bb7cffc --- /dev/null +++ b/core/update.sh @@ -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 + diff --git a/core/web.go b/core/web.go new file mode 100644 index 0000000..3f1f846 --- /dev/null +++ b/core/web.go @@ -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) + } + }, + }) +}