first commit

This commit is contained in:
cdle
2023-06-01 08:41:54 +08:00
parent 61b0e99833
commit d253ed8d5b
80 changed files with 14245 additions and 0 deletions
+500
View File
@@ -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())
}
+232
View File
@@ -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
}
+194
View File
@@ -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,
})
})
}
+555
View File
@@ -0,0 +1,555 @@
package core
import (
"errors"
"fmt"
"reflect"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/cdle/sillyplus/core/common"
"github.com/cdle/sillyplus/utils"
)
var RegistedSenders = map[string]func() common.Sender{}
type Faker struct {
Message string
Type string
UserID string
ChatID string
Carry chan string
BaseSender
Admin bool
}
func (sender *Faker) Listen() chan string {
return sender.Carry
}
func (sender *Faker) GetContent() string {
if sender.Fsps.Content != "" {
return sender.Fsps.Content
}
return sender.Message
}
func (sender *Faker) GetUserID() string {
return sender.UserID
}
func (sender *Faker) GetBotID() string {
return ""
}
func (sender *Faker) GetChatID() string {
return sender.ChatID
}
func (sender *Faker) GetImType() string {
if sender.Type == "" {
return "fake"
}
return sender.Type
}
func (sender *Faker) GetMessageID() string {
return ""
}
func (sender *Faker) GetUserName() string {
return ""
}
func (sender *Faker) GetChatName() string {
return ""
}
func (sender *Faker) IsReply() bool {
return false
}
func (sender *Faker) GetReplyUserID() int {
return 0
}
func (sender *Faker) GetRawMessage() interface{} {
return sender.Message
}
func (sender *Faker) IsAdmin() bool {
return sender.Admin
}
func (sender *Faker) IsMedia() bool {
return false
}
func (sender *Faker) Reply(msgs ...interface{}) (string, error) {
rt := ""
for _, msg := range msgs {
switch msg := msg.(type) {
case []byte:
rt = (string(msg))
case string:
rt = msg
}
}
{
for _, v := range regexp.MustCompile(`\[CQ:image,file=([^\[\]]+)\]`).FindAllStringSubmatch(rt, -1) {
// qr := qrcode2console.NewQRCode2ConsoleWithUrl(v[1], true)
// defer qr.Output()
rt = strings.Replace(rt, fmt.Sprintf(`[CQ:image,file=%s]`, v[1]), "", -1)
}
}
// if rt != "" && n != nil {
// NotifyMasters(rt)
// }
// if rt != "" && sender.Carry != nil {
// sender.Carry <- rt
// }
if rt != "" && sender.Type == "terminal" {
fmt.Printf("\x1b[%dm%s \x1b[0m\n", 31, rt)
}
return "", nil
}
func (sender *Faker) Delete() error {
return nil
}
func (sender *Faker) Disappear(lifetime ...time.Duration) {
}
func (sender *Faker) Finish() {
if sender.Carry != nil {
close(sender.Carry)
}
}
func (sender *Faker) Copy() common.Sender {
new := reflect.Indirect(reflect.ValueOf(interface{}(sender))).Interface().(Faker)
return &new
}
func (sender *Faker) GroupKick(uid string, reject_add_request bool) {
}
func (sender *Faker) GroupBan(uid string, duration int) {
}
type BaseSender struct {
matches [][]string
goon bool
Fsps common.FakerSenderParams
Atlast bool
ToSendMessages []string
IsFinished bool
Duration *time.Duration
mark interface{}
params []string
level int
}
func (sender *BaseSender) SetLevel(l int) {
sender.level = l
}
func (sender *BaseSender) GetLevel() int {
return sender.level
}
func (sender *BaseSender) SetMark(mark interface{}) {
sender.mark = mark
}
func (sender *BaseSender) GetMark() interface{} {
return sender.mark
}
func (sender *BaseSender) SetMatch(ss []string) {
sender.matches = [][]string{ss}
}
func (sender *BaseSender) SetParams(ss []string) {
sender.params = ss
}
func (sender *BaseSender) SetAllMatch(ss [][]string) {
sender.matches = ss
}
func (sender *BaseSender) SetContent(content string) {
sender.Fsps.Content = content
}
func (sender *BaseSender) SetFsps(fsps *common.FakerSenderParams) {
sender.Fsps = *fsps
}
func (sender *BaseSender) GetMatch() []string {
return sender.matches[0]
}
func (sender *BaseSender) GetAllMatch() [][]string {
return sender.matches
}
func (sender *BaseSender) Continue() {
sender.goon = true
}
func (sender *BaseSender) IsContinue() bool {
return sender.goon
}
func (sender *BaseSender) ClearContinue() {
sender.goon = false
}
func (sender *BaseSender) Get(i interface{}) string {
switch i := i.(type) {
case int:
if len(sender.matches) == 0 {
return ""
}
if len(sender.matches[0]) < i+1 {
return ""
}
return sender.matches[0][i]
case string:
for j := range sender.params {
if sender.params[j] == i {
return sender.Get(j)
}
}
return ""
}
return ""
}
func (sender *BaseSender) Delete() error {
return nil
}
func (sender *BaseSender) Disappear(lifetime ...time.Duration) {
}
func (sender *BaseSender) Finish() {
sender.IsFinished = true
}
func (sender *BaseSender) IsMedia() bool {
return false
}
func (sender *BaseSender) GetRawMessage() interface{} {
return nil
}
func (sender *BaseSender) IsReply() bool {
return false
}
func (sender *BaseSender) GetMessageID() string {
return ""
}
func (sender *BaseSender) RecallMessage(...interface{}) error {
return nil
}
func (sender *BaseSender) GetUserID() string {
return ""
}
func (sender *BaseSender) GetChatID() string {
return ""
}
func (sender *BaseSender) Push(msg map[string]string) (string, error) {
return "", nil
}
func (sender *BaseSender) GetImType() string {
return ""
}
func (sender *BaseSender) GroupKick(uid string, reject_add_request bool) {
}
func (sender *BaseSender) GroupUnkick(uid string) {
}
func (sender *BaseSender) GroupBan(uid string, duration int) {
}
func (sender *BaseSender) GroupUnban(uid string) {
}
func (sender *BaseSender) GetUserName() string {
return ""
}
func (sender *BaseSender) IsAdmin() bool {
return false
}
func (sender *BaseSender) GetChatName() string {
return ""
}
func (sender *BaseSender) GetReplyUserID() int {
return 0
}
func (sender *BaseSender) GetReplyMessageID() int {
return 0
}
func (sender *BaseSender) AtLast() {
sender.Atlast = true
}
func (sender *BaseSender) UAtLast() {
sender.Atlast = false
}
func (sender *BaseSender) Stop() {
panic("stop")
}
func (sender *BaseSender) IsAtLast() bool {
return sender.Atlast
}
func (sender *BaseSender) MessagesToSend() string {
return strings.Join(sender.ToSendMessages, "\n")
}
var ErrorTimeOut = errors.New("指令超时")
var ErrorInterrupt = errors.New("被其他指令中断")
type Carrys struct {
list map[int64]*Carry
sync.RWMutex
}
func (cs *Carrys) Add(key int64, c *Carry) {
// logs.Info("add", c.Function.Rules)
cs.Lock()
defer cs.Unlock()
cs.list[key] = c
}
func (cs *Carrys) Remove(Key1 int64) {
cs.Lock()
defer cs.Unlock()
for key := range cs.list {
if key == Key1 {
// logs.Info("rem", cs.list[key].Function.Rules)
delete(cs.list, Key1)
}
}
}
func (cs *Carrys) RemoveByUUID(uuid string) {
cs.Foreach(func(key int64, c *Carry) bool {
if c.UUID == uuid {
cs.Remove(key)
}
return true
})
}
func (cs *Carrys) Foreach(f func(key int64, c *Carry) bool) {
cs.RLock()
defer cs.RUnlock()
for key, c := range cs.list {
f(key, c)
}
}
var waits = map[int]*Carrys{
1: {
list: map[int64]*Carry{},
},
2: {
list: map[int64]*Carry{},
},
3: {
list: map[int64]*Carry{},
},
4: {
list: map[int64]*Carry{},
},
5: {
list: map[int64]*Carry{},
},
}
type Carry struct {
// Rules []string
Chan chan interface{}
Result chan interface{}
Message common.Sender
Function common.Function
RequireAdmin bool
AllowPlatforms []string
ProhibitPlatforms []string
AllowGroups []string
ProhibitGroups []string
AllowUsers []string
ProhibitUsers []string
ListenPrivate bool
ListenGroup bool
UserID string
ChatID string
UUID string
}
type again string
var Again again = ""
var GoAgain = func(str string) again {
return again(str)
}
type YesOrNo string
var YesNo YesOrNo = "yeson"
var Yes YesOrNo = "yes"
var No YesOrNo = "no"
type Range []int
type Switch []string
var listenCounter int64
func (s *BaseSender) Await(message common.Sender, callback func(common.Sender) interface{}, params ...interface{}) interface{} {
timeout := time.Hour * 999999
var handleErr func(error)
var persistent = false
var c *Carry
for _, param := range params {
switch param := param.(type) {
case string:
if param == "persistent" {
persistent = true
} else {
c.Function.Rules = append(c.Function.Rules, param)
}
case []string:
c.Function.Rules = append(c.Function.Rules, param...)
case time.Duration:
du := param
if du != 0 {
timeout = du
}
case func(error):
handleErr = param
case *Carry:
c = param
}
}
c.Message = message
if len(c.Function.Rules) == 0 {
c.Function.Rules = []string{`raw [\s\S]+`}
}
fmtRule(&c.Function)
c.Chan = make(chan interface{}, 1)
c.Result = make(chan interface{}, 1)
key := atomic.AddInt64(&listenCounter, 1)
// key := fmt.Sprintf("u=%v&c=%v&i=%v&t=%v&p=%v", message.GetUserID(), message.GetChatID(), message.GetImType(), atomic.LoadInt64(&listenCounter))
// if fg != nil {
// if *fg == "me" {
// key += "&f=me"
// } else {
// key += "&f=true"
// }
// }
waits[4-s.level].Add(key, c)
defer func() {
waits[4-s.level].Remove(key)
}()
for {
select {
case result := <-c.Chan:
switch s := result.(type) {
case common.Sender:
if callback == nil {
return s.GetContent()
}
if persistent {
go func() {
c.Result <- callback(s)
}()
continue
}
result := callback(s)
if v, ok := result.(again); ok { //阻塞
if v == "" {
c.Result <- nil
} else {
c.Result <- string(v)
}
} else if _, ok := result.(YesOrNo); ok {
o := strings.ToLower(regexp.MustCompile("[yYnN]").FindString(s.GetContent()))
if o == "y" {
return Yes
}
if o == "n" {
return No
}
c.Result <- "Y or n ?"
} else if vv, ok := result.(Switch); ok {
ct := s.GetContent()
for _, v := range vv {
if ct == v {
return v
}
}
c.Result <- fmt.Sprintf("请从%s中选择一个。", strings.Join(vv, "、"))
} else if vv, ok := result.(Range); ok {
ct := s.GetContent()
n := utils.Int(ct)
if fmt.Sprint(n) == ct {
if (n >= vv[0]) && (n <= vv[1]) {
return n
}
}
c.Result <- fmt.Sprintf("请从%d~%d中选择一个整数。", vv[0], vv[1])
} else {
c.Result <- result
return s.GetContent()
}
case error:
if handleErr != nil {
handleErr(s)
}
c.Result <- nil
return nil
}
case <-time.After(timeout):
if handleErr != nil {
handleErr(ErrorTimeOut)
}
c.Result <- nil
return nil
}
}
}
+127
View File
@@ -0,0 +1,127 @@
package core
import (
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/cdle/sillyplus/core/logs"
"github.com/cdle/sillyplus/core/storage"
"github.com/cdle/sillyplus/core/storage/boltdb"
"github.com/cdle/sillyplus/core/storage/redis"
"github.com/cdle/sillyplus/utils"
"github.com/goccy/go-json"
)
var bkt storage.Bucket
var HttpPort string
var sillyGirl = MakeBucket("sillyGirl")
var Get = func(key string) string {
return ""
}
var Set = func(key, value string, expiration time.Duration) error {
return nil
}
var MakeBucketlocker sync.Mutex
func MakeBucket(name string) storage.Bucket {
MakeBucketlocker.Lock()
defer MakeBucketlocker.Unlock()
if bkt == nil {
utils.ReadYaml(utils.ExecPath+"/conf/", &Config, "https://raw.githubusercontent.com/cdle/sillyplus/main/conf/demo_config.yaml")
utils.SlaveMode = Config.SlaveMode
HttpPort = Config.HttpPort
if !Config.EnableRedis {
bkt = boltdb.Initsillyplus()
Get = boltdb.Get
Set = boltdb.Set
logs.Info("默认使用boltdb进行数据存储。")
} else {
bkt = redis.Initsillyplus(Config.RedisAddr, Config.RedisPassword)
Get = redis.Get
Set = redis.Set
logs.Info("已使用redis进行数据存储。")
}
for _, name := range bkt.Buckets() {
b := bkt.Copy(name)
keys, err := b.Keys()
if len(keys) == 0 && err == nil {
b.Delete()
}
}
}
if name == "" {
name = "sillyGirl"
}
if name == "silly" || name == "app" {
name = "sillyGirl"
}
return bkt.Copy(name)
}
func TransformBucketKeyValue(v string) interface{} {
var result interface{}
if strings.HasPrefix(v, "f:") {
result, _ = strconv.ParseFloat(strings.Replace(v, "f:", "", 1), 64)
return result
}
if strings.HasPrefix(v, "d:") {
result = utils.Int(strings.Replace(v, "d:", "", 1))
return result
}
if strings.HasPrefix(v, "b:") {
result = strings.Replace(v, "b:", "", 1) == "true"
return result
}
if strings.HasPrefix(v, "o:") {
json.Unmarshal([]byte(strings.Replace(v, "o:", "", 1)), &result)
return result
}
if v == "" {
return nil
}
return v
}
func GetBucketKeyValue(bucket storage.Bucket, ps ...interface{}) interface{} {
var key interface{}
var value interface{}
if len(ps) == 0 {
return nil
}
if len(ps) > 0 {
key = ps[0]
}
if len(ps) > 1 {
value = ps[1]
}
v := bucket.GetString(key)
var result = TransformBucketKeyValue(v)
if result == nil {
return value
}
return result
}
func SetBucketKeyValue(bucket storage.Bucket, key interface{}, value interface{}) (string, error) {
new := ""
switch value := value.(type) {
case int, int64, int32, uint:
new = fmt.Sprintf("d:%d", value)
case float32, float64:
new = fmt.Sprintf("f:%f", value)
case string, []byte:
new = fmt.Sprintf("%s", value)
case bool:
new = fmt.Sprintf("b:%t", value)
case nil:
new = ""
default:
new = fmt.Sprintf("o:%s", utils.JsonMarshal(value))
}
return bucket.Set(key, new)
}
+578
View File
@@ -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 ""
}
+67
View File
@@ -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"`
// }
+44
View File
@@ -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"`
}
+55
View File
@@ -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
}
+3
View File
@@ -0,0 +1,3 @@
package core
var compiled_at = ""
+11
View File
@@ -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
+10
View File
@@ -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()
}
+98
View File
@@ -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
}
+678
View File
@@ -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
}
+46
View File
@@ -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)
}
}
+70
View File
@@ -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)
```
+93
View File
@@ -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
}
+38
View File
@@ -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)
}
+205
View File
@@ -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 {
// TopicLogGroup
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)
}
+13
View File
@@ -0,0 +1,13 @@
package alils
const (
version = "0.5.0" // SDK version
signatureMethod = "hmac-sha1" // Signature method
// OffsetNewest is the log head offset, i.e. the offset that will be
// assigned to the next message that will be produced to the shard.
OffsetNewest = "end"
// OffsetOldest is the the oldest offset available on the logstore for a
// shard.
OffsetOldest = "begin"
)
File diff suppressed because it is too large Load Diff
+42
View File
@@ -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
}
+813
View File
@@ -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
}
+270
View File
@@ -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
}
+91
View File
@@ -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
}
+62
View File
@@ -0,0 +1,62 @@
package alils
import (
"bytes"
"crypto/md5"
"fmt"
"net/http"
)
// request sends a request to SLS.
func request(project *LogProject, method, uri string, headers map[string]string,
body []byte) (resp *http.Response, err error) {
// The caller should provide 'x-sls-bodyrawsize' header
if _, ok := headers["x-sls-bodyrawsize"]; !ok {
err = fmt.Errorf("Can't find 'x-sls-bodyrawsize' header")
return
}
// SLS public request headers
headers["Host"] = project.Name + "." + project.Endpoint
headers["Date"] = nowRFC1123()
headers["x-sls-apiversion"] = version
headers["x-sls-signaturemethod"] = signatureMethod
if body != nil {
bodyMD5 := fmt.Sprintf("%X", md5.Sum(body))
headers["Content-MD5"] = bodyMD5
if _, ok := headers["Content-Type"]; !ok {
err = fmt.Errorf("Can't find 'Content-Type' header")
return
}
}
// Calc Authorization
// Authorization = "SLS <AccessKeyID>:<Signature>"
digest, err := signature(project, method, uri, headers)
if err != nil {
return
}
auth := fmt.Sprintf("SLS %v:%v", project.AccessKeyID, digest)
headers["Authorization"] = auth
// Initialize http request
reader := bytes.NewReader(body)
urlStr := fmt.Sprintf("http://%v.%v%v", project.Name, project.Endpoint, uri)
req, err := http.NewRequest(method, urlStr, reader)
if err != nil {
return
}
for k, v := range headers {
req.Header.Add(k, v)
}
// Get ready to do request
resp, err = http.DefaultClient.Do(req)
if err != nil {
return
}
return
}
+111
View File
@@ -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
}
+141
View File
@@ -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)
}
+96
View File
@@ -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)
}
+125
View File
@@ -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)
}
+84
View File
@@ -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)
}
+123
View File
@@ -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)
}
+39
View File
@@ -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())
}
+33
View File
@@ -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)
}
+442
View File
@@ -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)
}
+503
View File
@@ -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)
}
+91
View File
@@ -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
}
+116
View File
@@ -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)
}
}
+96
View File
@@ -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)
}
+36
View File
@@ -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)
}
+810
View File
@@ -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
}
+55
View File
@@ -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
}
+48
View File
@@ -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())
}
+128
View File
@@ -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()
})
}
}
+178
View File
@@ -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
}
+57
View File
@@ -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)
}
}
+132
View File
@@ -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)
}
+77
View File
@@ -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)
}
}
+84
View File
@@ -0,0 +1,84 @@
package logs
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"github.com/pkg/errors"
)
// SLACKWriter implements beego LoggerInterface and is used to send jiaoliao webhook
type SLACKWriter struct {
WebhookURL string `json:"webhookurl"`
Level int `json:"level"`
formatter LogFormatter
Formatter string `json:"formatter"`
}
// newSLACKWriter creates jiaoliao writer.
func newSLACKWriter() Logger {
res := &SLACKWriter{Level: LevelTrace}
res.formatter = res
return res
}
func (s *SLACKWriter) Format(lm *LogMsg) string {
// text := fmt.Sprintf("{\"text\": \"%s\"}", msg)
return lm.When.Format("2006-01-02 15:04:05") + " " + lm.OldStyleFormat()
}
func (s *SLACKWriter) SetFormatter(f LogFormatter) {
s.formatter = f
}
// Init SLACKWriter with json config string
func (s *SLACKWriter) Init(config string) error {
res := json.Unmarshal([]byte(config), s)
if res == nil && len(s.Formatter) > 0 {
fmtr, ok := GetFormatter(s.Formatter)
if !ok {
return errors.New(fmt.Sprintf("the formatter with name: %s not found", s.Formatter))
}
s.formatter = fmtr
}
return res
}
// WriteMsg write message in smtp writer.
// Sends an email with subject and only this message.
func (s *SLACKWriter) WriteMsg(lm *LogMsg) error {
if lm.Level > s.Level {
return nil
}
msg := s.Format(lm)
m := make(map[string]string, 1)
m["text"] = msg
body, _ := json.Marshal(m)
// resp, err := http.PostForm(s.WebhookURL, form)
resp, err := http.Post(s.WebhookURL, "application/json", bytes.NewReader(body))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Post webhook failed %s %d", resp.Status, resp.StatusCode)
}
return nil
}
// Flush implementing method. empty.
func (s *SLACKWriter) Flush() {
}
// Destroy implementing method. empty.
func (s *SLACKWriter) Destroy() {
}
func init() {
Register(AdapterSlack, newSLACKWriter)
}
+44
View File
@@ -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)
// }
//
// }
+172
View File
@@ -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)
}
+27
View File
@@ -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)
}
+43
View File
@@ -0,0 +1,43 @@
package core
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"github.com/cdle/sillyplus/core/storage"
"github.com/cdle/sillyplus/utils"
)
func protect(appID, id string) string {
mac := hmac.New(sha256.New, []byte(id))
mac.Write([]byte(appID))
return hex.EncodeToString(mac.Sum(nil))
}
func init() {
GetMachineID()
storage.Watch(sillyGirl, "machine_id", func(old, new, key string) *storage.Final {
if old == "" {
return nil
}
return &storage.Final{
Now: old,
}
})
}
var machine_id = ""
var GetMachineID = func() string {
var id = ""
if id == "" {
id = sillyGirl.GetString("machine_id")
}
if id == "" {
id = protect(utils.GenUUID(), "sillyplus")
sillyGirl.Set("machine_id", id)
}
machine_id = id
return id
}
+112
View File
@@ -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,
})
})
}
+91
View File
@@ -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,
})
})
}
+75
View File
@@ -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
// },
// })
// })
+109
View File
@@ -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
}
+20
View File
@@ -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)
}
+66
View File
@@ -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
}
+17
View File
@@ -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
}
+179
View File
@@ -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))
}
}
+93
View File
@@ -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
}
+58
View File
@@ -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")
}
}
+33
View File
@@ -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
}
+526
View File
@@ -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)
}
}
+586
View File
@@ -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
}
+75
View File
@@ -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
}
+93
View File
@@ -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
}
+204
View File
@@ -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
}
+413
View File
@@ -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)
}
+166
View File
@@ -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 ""
}
+128
View File
@@ -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")
}
+36
View File
@@ -0,0 +1,36 @@
package core
import (
"os"
"strings"
"time"
"github.com/cdle/sillyplus/utils"
)
func initReboot() {
go func() {
data, _ := os.ReadFile(utils.ExecPath + "/rebootInfo")
if v := string(data); v != "" {
defer os.RemoveAll(utils.ExecPath + "/rebootInfo")
vv := strings.Split(v, " ")
tp, cd, ud := vv[0], vv[1], vv[2]
if tp == "fake" {
return
}
msg := "重启完成。"
for i := 0; i < 10; i++ {
dapter, _ := GetAdapter(tp, "")
if dapter != nil {
break
}
dapter.Push(Message{
USER_ID: ud,
CHAT_ID: cd,
CONETNT: msg,
})
time.Sleep(time.Second)
}
}
}()
}
+217
View File
@@ -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])
})
}
+403
View File
@@ -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
}
+62
View File
@@ -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)
}
+353
View File
@@ -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()
}
+5
View File
@@ -0,0 +1,5 @@
package core
import "net/http"
var Transport *http.Transport
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
newVersion=`wget -q -O - https://raw.githubusercontent.com/cdle/binary/main/compile_time.go | tr -cd "[0-9]"`
oldVersion=`cat /root/amd64/compile_time.go | tr -cd "[0-9]"`
if [[ ${#newVersion} == 13 && $newVersion != $oldVersion ]]; then
rm -rf /root/binary
cd /root
git clone git@github.com:cdle/binary.git
cd /root/amd64
git checkout --orphan latest_branch
rm -rf *
cp /root/binary/sillyplus_linux_amd64_$newVersion /root/amd64
cp /root/binary/compile_time.go /root/amd64
git add -A
git commit -am "commit message"
git branch -D main
git branch -m main
git push -f origin main
cd /root/arm64
git checkout --orphan latest_branch
rm -rf *
cp /root/binary/sillyplus_linux_arm64_$newVersion /root/arm64
cp /root/binary/compile_time.go /root/arm64
git add -A
git commit -am "commit message"
git branch -D main
git branch -m main
git push -f origin main
rm -rf /root/binary
fi
+260
View File
@@ -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)
}
},
})
}