Compare commits

..

7 Commits

Author SHA1 Message Date
Evan Chen a5322f8d92 Merge branch 'tmp' 2021-12-16 23:45:49 +08:00
Evan Chen 4ccf2db1a2 update 2021-12-16 23:45:31 +08:00
Evan Chen 716f19133d update 2021-12-16 21:43:56 +08:00
Evan Chen 7e89c07951 save 2021-12-16 19:44:07 +08:00
Evan Chen 905e1779a5 save 2021-12-16 19:11:46 +08:00
Evan Chen c81b014919 save 2021-12-16 18:11:34 +08:00
Evan Chen b232685b63 save 2021-12-16 14:25:57 +08:00
16 changed files with 767 additions and 8 deletions

View File

@ -5,4 +5,4 @@ run:
APP_LOG_PRETTY=true \ APP_LOG_PRETTY=true \
APP_DB_TYPE=sqlite \ APP_DB_TYPE=sqlite \
APP_DATA=work \ APP_DATA=work \
go run main.go go run cmd/test/main.go

View File

@ -1,3 +1,9 @@
# Kumoly App # Kumoly App
combine all needed module into one lib combine all needed module into one lib
## Run
```
```

View File

@ -2,6 +2,7 @@ package main
import ( import (
"kumoly.io/kumoly/app/auth" "kumoly.io/kumoly/app/auth"
"kumoly.io/kumoly/app/history"
"kumoly.io/kumoly/app/server" "kumoly.io/kumoly/app/server"
"kumoly.io/kumoly/app/store" "kumoly.io/kumoly/app/store"
"kumoly.io/kumoly/app/system" "kumoly.io/kumoly/app/system"
@ -17,7 +18,7 @@ func main() {
auth.SetDB(store.DB) auth.SetDB(store.DB)
sys.Inject(auth.Injector(server.API)) sys.Inject(auth.Injector(server.API))
sys.Append(server, auth.New(server), &task.Service{}) sys.Append(server, auth.New(server), &task.Service{}, &history.Service{})
sys.Start() sys.Start()
} }

27
history/helper.go Normal file
View File

@ -0,0 +1,27 @@
package history
import "kumoly.io/kumoly/app/util"
func Error() *History {
h := getBase()
h.Type = ERROR
if !PROD {
h.Trace = util.Stack()
}
return h
}
func Info() *History {
h := getBase()
h.Type = INFO
return h
}
func getBase() *History {
mod, file := util.CallerMod(3)
h := &History{
Module: mod,
Caller: file,
}
return h
}

102
history/history.go Normal file
View File

@ -0,0 +1,102 @@
package history
import (
"encoding/json"
"fmt"
"time"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"kumoly.io/kumoly/app/errors"
"kumoly.io/kumoly/app/store"
)
var PROD = false
const (
ERROR = "ERROR"
INFO = "INFO"
)
type History struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
Module string
Type string
Message string
BodyJson string
Body interface{} `gorm:"-"`
Issuer string
Caller string
Trace string
}
func (h *History) BeforeCreate(tx *gorm.DB) (err error) {
if h.Body != nil {
body, err := json.Marshal(h.Body)
if err != nil {
h.BodyJson = string(body)
}
}
return
}
var Tunnel chan *History
var quit chan struct{}
var started chan struct{}
func init() {
Tunnel = make(chan *History)
quit = make(chan struct{}, 1)
started = make(chan struct{}, 1)
}
func Start(r Receiver) {
select {
case started <- struct{}{}:
default:
panic(errors.New(500, "history reporter has already started!"))
}
go func() {
for {
select {
case h := <-Tunnel:
if Interceptor != nil {
go Interceptor(h)
}
r(h)
case <-quit:
<-started
return
}
}
}()
}
func Stop() {
quit <- struct{}{}
}
func Send(h *History) {
if h.Type == "" {
h.Type = INFO
}
Tunnel <- h
}
type Receiver func(*History)
var Interceptor func(*History) = nil
var DBReceiver Receiver = func(h *History) {
err := store.DB.Create(h).Error
if err != nil {
log.Error().Str("mod", "history").Err(err).Msg("DBReceiver error")
}
}
var ConsoleReceiver Receiver = func(h *History) {
fmt.Printf("%+v\n", h)
}

32
history/history_test.go Normal file
View File

@ -0,0 +1,32 @@
package history
import (
"testing"
"time"
"kumoly.io/kumoly/app/util"
)
func TestHistory(t *testing.T) {
t.Log("start")
Start(func(h *History) {
t.Logf("%+v\n", h)
})
go func() {
for {
Send(&History{
Module: "test",
Type: INFO,
Message: "testing",
Body: time.Now().String(),
Caller: util.Caller(2),
})
time.Sleep(time.Second)
}
}()
time.Sleep(time.Second * 20)
Stop()
t.Log("stoped")
time.Sleep(time.Second * 2)
}

34
history/service.go Normal file
View File

@ -0,0 +1,34 @@
package history
import (
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"kumoly.io/kumoly/app/store"
"kumoly.io/kumoly/app/system"
)
type Service struct {
system.BaseService
}
func (srv Service) GetName() string { return "history.Service" }
func (srv Service) IsService() bool { return true }
func (srv Service) Init() error {
PROD = viper.GetBool("prod")
l := log.With().Str("mod", "history").
Str("service", "history.Service").
Logger()
l.Debug().Msg("Migrating database for history.Service ...")
if err := store.Migrate(&History{}); err != nil {
l.Error().Err(err).Msg("Migrating database")
return err
}
return nil
}
func (srv Service) Main() error {
Start(DBReceiver)
return nil
}
func (srv Service) Del() {
Stop()
}

476
messenger/base.gohtml Normal file
View File

@ -0,0 +1,476 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style type="text/css" rel="stylesheet" media="all">
/* Base ------------------------------ */
*:not(br):not(tr):not(html) {
font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
body {
width: 100% !important;
height: 100%;
margin: 0;
line-height: 1.4;
background-color: #F2F4F6;
color: #74787E;
-webkit-text-size-adjust: none;
}
a {
color: #3869D4;
}
/* Layout ------------------------------ */
.email-wrapper {
width: 100%;
margin: 0;
padding: 0;
background-color: #F2F4F6;
}
.email-content {
width: 100%;
margin: 0;
padding: 0;
}
/* Masthead ----------------------- */
.email-masthead {
padding: 25px 0;
text-align: center;
}
.email-masthead_logo {
max-width: 400px;
border: 0;
}
.email-masthead_name {
font-size: 16px;
font-weight: bold;
color: #2F3133;
text-decoration: none;
text-shadow: 0 1px 0 white;
}
.email-logo {
max-height: 50px;
}
/* Body ------------------------------ */
.email-body {
width: 100%;
margin: 0;
padding: 0;
border-top: 1px solid #EDEFF2;
border-bottom: 1px solid #EDEFF2;
background-color: #FFF;
}
.email-body_inner {
width: 570px;
margin: 0 auto;
padding: 0;
}
.email-footer {
width: 570px;
margin: 0 auto;
padding: 0;
text-align: center;
}
.email-footer p {
color: #AEAEAE;
}
.body-action {
width: 100%;
margin: 30px auto;
padding: 0;
text-align: center;
}
.body-dictionary {
width: 100%;
overflow: hidden;
margin: 20px auto 10px;
padding: 0;
}
.body-dictionary dd {
margin: 0 0 10px 0;
}
.body-dictionary dt {
clear: both;
color: #000;
font-weight: bold;
}
.body-dictionary dd {
margin-left: 0;
margin-bottom: 10px;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #EDEFF2;
table-layout: fixed;
}
.body-sub a {
word-break: break-all;
}
.content-cell {
padding: 35px;
}
.align-right {
text-align: right;
}
/* Type ------------------------------ */
h1 {
margin-top: 0;
color: #2F3133;
font-size: 19px;
font-weight: bold;
}
h2 {
margin-top: 0;
color: #2F3133;
font-size: 16px;
font-weight: bold;
}
h3 {
margin-top: 0;
color: #2F3133;
font-size: 14px;
font-weight: bold;
}
blockquote {
margin: 25px 0;
padding-left: 10px;
border-left: 10px solid #F0F2F4;
}
blockquote p {
font-size: 1.1rem;
color: #999;
}
blockquote cite {
display: block;
text-align: right;
color: #666;
font-size: 1.2rem;
}
cite {
display: block;
font-size: 0.925rem;
}
cite:before {
content: "\2014 \0020";
}
p {
margin-top: 0;
color: #74787E;
font-size: 16px;
line-height: 1.5em;
}
p.sub {
font-size: 12px;
}
p.center {
text-align: center;
}
table {
width: 100%;
}
th {
padding: 0px 5px;
padding-bottom: 8px;
border-bottom: 1px solid #EDEFF2;
}
th p {
margin: 0;
color: #9BA2AB;
font-size: 12px;
}
td {
padding: 10px 5px;
color: #74787E;
font-size: 15px;
line-height: 18px;
}
.content {
align: center;
padding: 0;
}
/* Data table ------------------------------ */
.data-wrapper {
width: 100%;
margin: 0;
padding: 35px 0;
}
.data-table {
width: 100%;
margin: 0;
}
.data-table th {
text-align: left;
padding: 0px 5px;
padding-bottom: 8px;
border-bottom: 1px solid #EDEFF2;
}
.data-table th p {
margin: 0;
color: #9BA2AB;
font-size: 12px;
}
.data-table td {
padding: 10px 5px;
color: #74787E;
font-size: 15px;
line-height: 18px;
}
/* Invite Code ------------------------------ */
.invite-code {
display: inline-block;
padding-top: 20px;
padding-right: 36px;
padding-bottom: 16px;
padding-left: 36px;
border-radius: 3px;
font-family: Consolas, monaco, monospace;
font-size: 28px;
text-align: center;
letter-spacing: 8px;
color: #555;
background-color: #eee;
}
/* Buttons ------------------------------ */
.button {
display: inline-block;
background-color: #3869D4;
border-radius: 3px;
color: #ffffff !important;
font-size: 15px;
line-height: 45px;
text-align: center;
text-decoration: none;
-webkit-text-size-adjust: none;
mso-hide: all;
}
/*Media Queries ------------------------------ */
@media only screen and (max-width: 600px) {
.email-body_inner,
.email-footer {
width: 100% !important;
}
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
}
}
</style>
</head>
<body>
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td class="content">
<table class="email-content" width="100%" cellpadding="0" cellspacing="0">
<!-- Logo -->
<tr>
<td class="email-masthead">
<a class="email-masthead_name" href="{{.Hermes.Product.Link}}" target="_blank">
{{ if .Hermes.Product.Logo }}
<img src="{{.Hermes.Product.Logo | url }}" class="email-logo" />
{{ else }}
{{ .Hermes.Product.Name }}
{{ end }}
</a>
</td>
</tr>
<!-- Email Body -->
<tr>
<td class="email-body" width="100%">
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0">
<!-- Body content -->
<tr>
<td class="content-cell">
<h1>{{if .Email.Body.Title }}{{ .Email.Body.Title }}{{ else }}{{ .Email.Body.Greeting }} {{ .Email.Body.Name }},{{ end }}</h1>
{{ with .Email.Body.Intros }}
{{ if gt (len .) 0 }}
{{ range $line := . }}
<p>{{ $line }}</p>
{{ end }}
{{ end }}
{{ end }}
{{ if (ne .Email.Body.FreeMarkdown "") }}
{{ .Email.Body.FreeMarkdown.ToHTML }}
{{ else }}
{{ with .Email.Body.Dictionary }}
{{ if gt (len .) 0 }}
<dl class="body-dictionary">
{{ range $entry := . }}
<dt>{{ $entry.Key }}:</dt>
<dd>{{ $entry.Value }}</dd>
{{ end }}
</dl>
{{ end }}
{{ end }}
<!-- Table -->
{{ with .Email.Body.Table }}
{{ $data := .Data }}
{{ $columns := .Columns }}
{{ if gt (len $data) 0 }}
<table class="data-wrapper" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td colspan="2">
<table class="data-table" width="100%" cellpadding="0" cellspacing="0">
<tr>
{{ $col := index $data 0 }}
{{ range $entry := $col }}
<th
{{ with $columns }}
{{ $width := index .CustomWidth $entry.Key }}
{{ with $width }}
width="{{ . }}"
{{ end }}
{{ $align := index .CustomAlignment $entry.Key }}
{{ with $align }}
style="text-align:{{ . }}"
{{ end }}
{{ end }}
>
<p>{{ $entry.Key }}</p>
</th>
{{ end }}
</tr>
{{ range $row := $data }}
<tr>
{{ range $cell := $row }}
<td
{{ with $columns }}
{{ $align := index .CustomAlignment $cell.Key }}
{{ with $align }}
style="text-align:{{ . }}"
{{ end }}
{{ end }}
>
{{ $cell.Value }}
</td>
{{ end }}
</tr>
{{ end }}
</table>
</td>
</tr>
</table>
{{ end }}
{{ end }}
<!-- Action -->
{{ with .Email.Body.Actions }}
{{ if gt (len .) 0 }}
{{ range $action := . }}
<p>{{ $action.Instructions }}</p>
{{ $length := len $action.Button.Text }}
{{ $width := add (mul $length 9) 20 }}
{{if (lt $width 200)}}{{$width = 200}}{{else if (gt $width 570)}}{{$width = 570}}{{else}}{{end}}
{{safe "<!--[if mso]>" }}
{{ if $action.Button.Text }}
<div style="margin: 30px auto;v-text-anchor:middle;text-align:center">
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:w="urn:schemas-microsoft-com:office:word"
href="{{ $action.Button.Link }}"
style="height:45px;v-text-anchor:middle;width:{{$width}}px;background-color:{{ if $action.Button.Color }}{{ $action.Button.Color }}{{ else }}#3869D4{{ end }};"
arcsize="10%"
{{ if $action.Button.Color }}strokecolor="{{ $action.Button.Color }}" fillcolor="{{ $action.Button.Color }}"{{ else }}strokecolor="#3869D4" fillcolor="#3869D4"{{ end }}
>
<w:anchorlock/>
<center style="color: {{ if $action.Button.TextColor }}{{ $action.Button.TextColor }}{{else}}#FFFFFF{{ end }};font-size: 15px;text-align: center;font-family:sans-serif;font-weight:bold;">
{{ $action.Button.Text }}
</center>
</v:roundrect>
</div>
{{ end }}
{{ if $action.InviteCode }}
<div style="margin-top:30px;margin-bottom:30px">
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<table align="center" cellpadding="0" cellspacing="0" style="padding:0;text-align:center">
<tr>
<td style="display:inline-block;border-radius:3px;font-family:Consolas, monaco, monospace;font-size:28px;text-align:center;letter-spacing:8px;color:#555;background-color:#eee;padding:20px">
{{ $action.InviteCode }}
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
{{ end }}
{{safe "<![endif]-->" }}
{{safe "<!--[if !mso]><!-- -->"}}
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<div>
{{ if $action.Button.Text }}
<a href="{{ $action.Button.Link }}" class="button" style="{{ with $action.Button.Color }}background-color: {{ . }};{{ end }} {{ with $action.Button.TextColor }}color: {{ . }};{{ end }} width: {{$width}}px;" target="_blank">
{{ $action.Button.Text }}
</a>
{{end}}
{{ if $action.InviteCode }}
<span class="invite-code">{{ $action.InviteCode }}</span>
{{end}}
</div>
</td>
</tr>
</table>
{{safe "<![endif]-->" }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ with .Email.Body.Outros }}
{{ if gt (len .) 0 }}
{{ range $line := . }}
<p>{{ $line }}</p>
{{ end }}
{{ end }}
{{ end }}
<p>
{{.Email.Body.Signature}},
<br />
{{.Hermes.Product.Name}}
</p>
{{ if (eq .Email.Body.FreeMarkdown "") }}
{{ with .Email.Body.Actions }}
<table class="body-sub">
<tbody>
{{ range $action := . }}
{{if $action.Button.Text}}
<tr>
<td>
<p class="sub">{{$.Hermes.Product.TroubleText | replace "{ACTION}" $action.Button.Text}}</p>
<p class="sub"><a href="{{ $action.Button.Link }}">{{ $action.Button.Link }}</a></p>
</td>
</tr>
{{ end }}
{{ end }}
</tbody>
</table>
{{ end }}
{{ end }}
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0">
<tr>
<td class="content-cell">
<p class="sub center">
{{.Hermes.Product.Copyright}}
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

5
messenger/messenger.go Normal file
View File

@ -0,0 +1,5 @@
package messenger
func init() {
}

9
run.sh Normal file
View File

@ -0,0 +1,9 @@
export APP_SERVER_HOST=127.0.0.1
export APP_SERVER_PORT=8000
export APP_LOG_LEVEL=-1
export APP_PROD=false
export APP_LOG_PRETTY=true
export APP_DB_TYPE=sqlite
export APP_DATA=work
go run cmd/test/main.go

View File

@ -9,7 +9,7 @@ import (
type BaseService struct{} type BaseService struct{}
func (srv BaseService) GetName() string { return "base" } func (srv BaseService) GetName() string { return "base" }
func (srv BaseService) GetDependencies() []string { return []string{""} } func (srv BaseService) GetDependencies() []string { return []string{} }
func (srv BaseService) IsService() bool { return false } func (srv BaseService) IsService() bool { return false }
func (srv BaseService) Init() error { return nil } func (srv BaseService) Init() error { return nil }
func (srv BaseService) Load() error { return nil } func (srv BaseService) Load() error { return nil }

View File

@ -1,6 +1,7 @@
package system package system
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@ -42,6 +43,7 @@ func init() {
viper.SetEnvKeyReplacer(replacer) viper.SetEnvKeyReplacer(replacer)
viper.AutomaticEnv() viper.AutomaticEnv()
viper.SetConfigType("json") viper.SetConfigType("json")
setup()
} }
func setup() { func setup() {
@ -60,9 +62,24 @@ func setup() {
if cc, ok := i.(string); ok { if cc, ok := i.(string); ok {
c = cc c = cc
} }
// if len(c) > 0 { if len(c) > 0 {
// shorten caller to mod/file:line
// } segs := strings.Split(c, ":")
file := segs[len(segs)-2]
short := file
skip := false
for i := len(file) - 1; i > 0; i-- {
if file[i] == '/' {
if !skip {
skip = true
continue
}
short = file[i+1:]
break
}
}
return fmt.Sprintf("%v:%v", short, segs[len(segs)-1])
}
return c return c
}, },
}) })

View File

@ -98,7 +98,7 @@ func New() *System {
} }
func (sys *System) Start() { func (sys *System) Start() {
setup() // setup()
sys.order() sys.order()
sys.status = sys_init sys.status = sys_init
go func() { go func() {

View File

@ -7,11 +7,39 @@ import (
"runtime" "runtime"
) )
func Stack() string {
buf := make([]byte, 1024)
for {
n := runtime.Stack(buf, false)
if n < len(buf) {
return string(buf[:n])
}
buf = make([]byte, 2*len(buf))
}
}
func CallerFull(depth int) string { func CallerFull(depth int) string {
_, file, line, _ := runtime.Caller(depth) _, file, line, _ := runtime.Caller(depth)
return fmt.Sprintf("%s:%d", file, line) return fmt.Sprintf("%s:%d", file, line)
} }
func CallerMod(depth int) (mod, file string) {
_, f, l, _ := runtime.Caller(depth)
ptr := 0
for i := len(f) - 1; i > 0; i-- {
if f[i] == '/' {
if ptr == 0 {
file = fmt.Sprintf("%v:%v", f[i+1:], l)
ptr = i
} else {
mod = f[i+1 : ptr]
break
}
}
}
return
}
func Caller(depth int) string { func Caller(depth int) string {
_, file, line, _ := runtime.Caller(depth) _, file, line, _ := runtime.Caller(depth)
short := file short := file

22
util/logging_test.go Normal file
View File

@ -0,0 +1,22 @@
package util
import (
"fmt"
"testing"
)
func TestStack(t *testing.T) {
fmt.Println(Stack())
}
func TestCallerFull(t *testing.T) {
fmt.Println(CallerFull(1))
}
func TestCallerMod(t *testing.T) {
fmt.Println(CallerMod(1))
}
func TestCaller(t *testing.T) {
fmt.Println(Caller(1))
}