From b9be165fd2a2e7996765fe280f81a5fb78c933e2 Mon Sep 17 00:00:00 2001 From: ideaa Date: Tue, 18 Jun 2024 18:08:39 +0800 Subject: [PATCH] init --- .gitignore | 33 +++ README.md | 210 +++++++++++++++ apic/api.go | 24 ++ apic/api_id.go | 14 + apic/api_options.go | 11 + apic/api_request.go | 56 ++++ apic/api_response.go | 26 ++ apic/api_util.go | 12 + apic/apic.go | 89 +++++++ apic/http_client.go | 230 ++++++++++++++++ apic/http_method.go | 18 ++ apic/http_params.go | 30 +++ app.go | 209 +++++++++++++++ app_asciilogo.go | 35 +++ app_config.go | 40 +++ app_server.go | 34 +++ app_use.go | 11 + config/config.go | 50 ++++ config/config.yaml | 36 +++ config/config_dialog.go | 11 + config/config_logger.go | 49 ++++ config/config_mysql.go | 35 +++ config/config_redis.go | 15 ++ config/config_sqlite.go | 14 + config/config_sqlserver.go | 35 +++ docs/assets/img.png | Bin 0 -> 246779 bytes docs/zh-CN.md | 212 +++++++++++++++ examples/Makefile | 46 ++++ examples/main.go | 39 +++ go.mod | 111 ++++++++ go.sum | 417 ++++++++++++++++++++++++++++++ logger/zap.go | 59 +++++ options.go | 84 ++++++ remote_provider.go | 66 +++++ stats/stats.go | 231 +++++++++++++++++ store/use.go | 17 ++ store/use_mysql.go | 65 +++++ store/use_redis.go | 40 +++ store/use_sqlite.go | 64 +++++ store/use_sqlserver.go | 67 +++++ utils/ali_ocr.go | 139 ++++++++++ utils/bytefmt/bytefmt.go | 123 +++++++++ utils/createfile.go | 33 +++ utils/encrypt/azdg.go | 63 +++++ utils/encrypt/azdg_test.go | 26 ++ utils/encrypt/encrypt_aes.go | 73 ++++++ utils/encrypt/encrypt_aes_test.go | 25 ++ utils/encrypt/md5.go | 12 + utils/encrypt/sha256_rsa.go | 27 ++ utils/file_exists.go | 15 ++ utils/filter_string.go | 35 +++ utils/format/format_byte.go | 18 ++ utils/format/format_byte_test.go | 8 + utils/format/format_time.go | 64 +++++ utils/format/format_time_test.go | 11 + utils/geo/coordinate.go | 39 +++ utils/geo/distance.go | 69 +++++ utils/helper.go | 8 + utils/i18n/gorm.go | 37 +++ utils/ip/real_ip.go | 103 ++++++++ utils/jwt/jwt.go | 49 ++++ utils/jwt/login_claims.go | 28 ++ utils/path.go | 31 +++ utils/pem/pem.go | 112 ++++++++ utils/pem/pem_test.go | 234 +++++++++++++++++ utils/random.go | 16 ++ utils/random_nickname.go | 38 +++ utils/random_nickname_test.go | 9 + utils/regx/id.go | 85 ++++++ utils/regx/regx_date.go | 29 +++ utils/regx/regx_phone.go | 23 ++ utils/regx/regx_time.go | 30 +++ utils/rmb/yuan.go | 58 +++++ utils/sensitive/README.md | 5 + utils/sensitive/embed.go | 6 + utils/sensitive/sensitive_init.go | 33 +++ utils/sensitive/words/dict.txt | 0 utils/slice_compare.go | 33 +++ utils/slice_find.go | 13 + utils/slice_unique.go | 13 + utils/string2id.go | 48 ++++ utils/string2slice.go | 24 ++ utils/string_limit.go | 14 + utils/struct2map.go | 30 +++ utils/struct2map_json.go | 42 +++ utils/t.go | 138 ++++++++++ utils/time.go | 18 ++ utils/tree/trie_tree.go | 131 ++++++++++ utils/trie_sensitive.go | 172 ++++++++++++ utils/trie_sensitive_test.go | 21 ++ utils/wxc/wxbizmsgcrypt.go | 315 ++++++++++++++++++++++ utils/zip.go | 73 ++++++ validate/util_gin.go | 23 ++ validate/util_normal.go | 32 +++ validate/validate_init.go | 97 +++++++ validate/validate_manager.go | 79 ++++++ worker/task.go | 12 + worker/worker.go | 82 ++++++ ws/client.go | 188 ++++++++++++++ ws/client_keyval.go | 22 ++ ws/client_stack.go | 51 ++++ ws/context.go | 34 +++ ws/context_binding.go | 40 +++ ws/context_i18n.go | 9 + ws/context_lang.go | 109 ++++++++ ws/context_lang_i18n_hash_test.go | 41 +++ ws/context_params.go | 93 +++++++ ws/context_send.go | 105 ++++++++ ws/context_value.go | 160 ++++++++++++ ws/data_api.go | 9 + ws/data_appid.go | 17 ++ ws/data_error.go | 64 +++++ ws/data_h.go | 62 +++++ ws/data_paginate.go | 45 ++++ ws/dispatcher.go | 63 +++++ ws/hubc.go | 151 +++++++++++ ws/manager.go | 38 +++ ws/pubsub.go | 90 +++++++ ws/pubsub_topic.go | 42 +++ ws/pubsub_topic_msg.go | 7 + ws/router.go | 52 ++++ ws/server.go | 61 +++++ ws/server_handler_http.go | 82 ++++++ ws/user.go | 181 +++++++++++++ ws/user_location.go | 16 ++ ws/user_resource.go | 11 + ws/user_status.go | 13 + ws/w.go | 34 +++ ws/w_response.go | 42 +++ ws/w_util.go | 29 +++ ws/w_util_binding.go | 12 + ws/w_util_chain.go | 22 ++ 132 files changed, 7964 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 apic/api.go create mode 100644 apic/api_id.go create mode 100644 apic/api_options.go create mode 100644 apic/api_request.go create mode 100644 apic/api_response.go create mode 100644 apic/api_util.go create mode 100644 apic/apic.go create mode 100644 apic/http_client.go create mode 100644 apic/http_method.go create mode 100644 apic/http_params.go create mode 100644 app.go create mode 100644 app_asciilogo.go create mode 100644 app_config.go create mode 100644 app_server.go create mode 100644 app_use.go create mode 100644 config/config.go create mode 100644 config/config.yaml create mode 100644 config/config_dialog.go create mode 100644 config/config_logger.go create mode 100644 config/config_mysql.go create mode 100644 config/config_redis.go create mode 100644 config/config_sqlite.go create mode 100644 config/config_sqlserver.go create mode 100644 docs/assets/img.png create mode 100644 docs/zh-CN.md create mode 100644 examples/Makefile create mode 100644 examples/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 logger/zap.go create mode 100644 options.go create mode 100644 remote_provider.go create mode 100644 stats/stats.go create mode 100644 store/use.go create mode 100644 store/use_mysql.go create mode 100644 store/use_redis.go create mode 100644 store/use_sqlite.go create mode 100644 store/use_sqlserver.go create mode 100644 utils/ali_ocr.go create mode 100644 utils/bytefmt/bytefmt.go create mode 100644 utils/createfile.go create mode 100644 utils/encrypt/azdg.go create mode 100644 utils/encrypt/azdg_test.go create mode 100644 utils/encrypt/encrypt_aes.go create mode 100644 utils/encrypt/encrypt_aes_test.go create mode 100644 utils/encrypt/md5.go create mode 100644 utils/encrypt/sha256_rsa.go create mode 100644 utils/file_exists.go create mode 100644 utils/filter_string.go create mode 100644 utils/format/format_byte.go create mode 100644 utils/format/format_byte_test.go create mode 100644 utils/format/format_time.go create mode 100644 utils/format/format_time_test.go create mode 100644 utils/geo/coordinate.go create mode 100644 utils/geo/distance.go create mode 100644 utils/helper.go create mode 100644 utils/i18n/gorm.go create mode 100644 utils/ip/real_ip.go create mode 100644 utils/jwt/jwt.go create mode 100644 utils/jwt/login_claims.go create mode 100644 utils/path.go create mode 100644 utils/pem/pem.go create mode 100644 utils/pem/pem_test.go create mode 100644 utils/random.go create mode 100644 utils/random_nickname.go create mode 100644 utils/random_nickname_test.go create mode 100644 utils/regx/id.go create mode 100644 utils/regx/regx_date.go create mode 100644 utils/regx/regx_phone.go create mode 100644 utils/regx/regx_time.go create mode 100644 utils/rmb/yuan.go create mode 100644 utils/sensitive/README.md create mode 100644 utils/sensitive/embed.go create mode 100644 utils/sensitive/sensitive_init.go create mode 100644 utils/sensitive/words/dict.txt create mode 100644 utils/slice_compare.go create mode 100644 utils/slice_find.go create mode 100644 utils/slice_unique.go create mode 100644 utils/string2id.go create mode 100644 utils/string2slice.go create mode 100644 utils/string_limit.go create mode 100644 utils/struct2map.go create mode 100644 utils/struct2map_json.go create mode 100644 utils/t.go create mode 100644 utils/time.go create mode 100644 utils/tree/trie_tree.go create mode 100644 utils/trie_sensitive.go create mode 100644 utils/trie_sensitive_test.go create mode 100644 utils/wxc/wxbizmsgcrypt.go create mode 100644 utils/zip.go create mode 100644 validate/util_gin.go create mode 100644 validate/util_normal.go create mode 100644 validate/validate_init.go create mode 100644 validate/validate_manager.go create mode 100644 worker/task.go create mode 100644 worker/worker.go create mode 100644 ws/client.go create mode 100644 ws/client_keyval.go create mode 100644 ws/client_stack.go create mode 100644 ws/context.go create mode 100644 ws/context_binding.go create mode 100644 ws/context_i18n.go create mode 100644 ws/context_lang.go create mode 100644 ws/context_lang_i18n_hash_test.go create mode 100644 ws/context_params.go create mode 100644 ws/context_send.go create mode 100644 ws/context_value.go create mode 100644 ws/data_api.go create mode 100644 ws/data_appid.go create mode 100644 ws/data_error.go create mode 100644 ws/data_h.go create mode 100644 ws/data_paginate.go create mode 100644 ws/dispatcher.go create mode 100644 ws/hubc.go create mode 100644 ws/manager.go create mode 100644 ws/pubsub.go create mode 100644 ws/pubsub_topic.go create mode 100644 ws/pubsub_topic_msg.go create mode 100644 ws/router.go create mode 100644 ws/server.go create mode 100644 ws/server_handler_http.go create mode 100644 ws/user.go create mode 100644 ws/user_location.go create mode 100644 ws/user_resource.go create mode 100644 ws/user_status.go create mode 100755 ws/w.go create mode 100644 ws/w_response.go create mode 100644 ws/w_util.go create mode 100644 ws/w_util_binding.go create mode 100644 ws/w_util_chain.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa1afa8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Project +examples/data +examples/**.yaml + +# Binaries +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Mac +.DS_Store +**/.DS_Store + +# VS Code +.vscode +*.project +*.factorypath + +# IntelliJ IDEA +.idea/* +!.idea/icon.png +*.iws +*.iml +*.ipr + +# Test binary +*.test + +# Output +*.out + diff --git a/README.md b/README.md new file mode 100644 index 0000000..f1143ae --- /dev/null +++ b/README.md @@ -0,0 +1,210 @@ +# Aqi + +Aqi is a Golang Websocket business framework that supports net/http, gin, chi, etc. It integrates underlying third-party libraries such as viper, gorm, gobwa/ws, gjson, zap, asynq, which facilitates the rapid development of Websocket applications. + +### Installation + +`go get -u github.com/wonli/aqi` + +[简体中文](./docs/zh-CN.md) + +### Usage + +On the first run, a `config-dev.yaml` configuration file will be automatically generated in the working directory. You can configure the application start port, database, and other settings. + +After the service starts, use [wscat](https://github.com/websockets/wscat) to establish a websocket connection with the server. Here is a screenshot of the operation. + + + +![img](./docs/assets/img.png) + + + +### Interaction Protocol + +Input and output uniformly use `JSON`. Here, `Action` is the name registered in the routing, and `Params` are in JSON string format. + +```go + +type Context struct { + ... + Action string + Params string + ... +} +``` + + + +Response Format: + +```go +type Action struct { + Action string `json:"action"` + + Code int `json:"code"` + Msg string `json:"msg,omitempty"` + Data any `json:"data,omitempty"` +} +``` + + + +### Quick Start + +In `ahi.Init`, specify the configuration file via `aqi.ConfigFile`, which defaults to `yaml` format. The service name and port are specified in the `yaml` file path through `aqi.HttpServer`. The contents of the entry file are as follows: + +```go +package main + +import ( + "net/http" + "time" + + "github.com/wonli/aqi" + "github.com/wonli/aqi/ws" +) + +func main() { + app := aqi.Init( + aqi.ConfigFile("config.yaml"), + aqi.HttpServer("Aqi", "port"), + ) + + // Create router + mux := http.NewServeMux() + // WebSocket Handler + mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { + ws.HttpHandler(w, r) + }) + + // Register router + wsr := ws.NewRouter() + wsr.Add("hi", func(a *ws.Context) { + a.Send(ws.H{ + "hi": time.Now(), + }) + }) + + app.WithHttpServer(mux) + + // 启动应用 + app.Start() +} +``` + + + +### With Gin + +`aqi` can be very conveniently integrated with other WEB frameworks. You just need to correctly register `handler` and `app.WithHttpServer`. It supports any implementation of `http.Handler`. + + + +```go +package main + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + + "github.com/wonli/aqi" + "github.com/wonli/aqi/ws" +) + +func main() { + app := aqi.Init( + aqi.ConfigFile("config.yaml"), + aqi.HttpServer("Aqi", "port"), + ) + + engine := gin.Default() + // Handler + engine.GET("/ws", func(c *gin.Context) { + ws.HttpHandler(c.Writer, c.Request) + }) + + // Router + wsr := ws.NewRouter() + wsr.Add("hi", func(a *ws.Context) { + a.Send(ws.H{ + "hi": time.Now(), + }) + }) + + app.WithHttpServer(engine) + app.Start() +} +``` + + + +### Middlewares + +First, define a simple logging middleware that prints the received `action` before processing the request and prints the response content after processing. + +```go +func logMiddleware() func(a *ws.Context) { + return func(a *ws.Context) { + log.Printf("Request action: %s ", a.Action) + a.Next() + log.Printf("Reqponse data: %s ", a.Response.Data) + } +} +``` + +Register the middleware to the router using the `Use` method. + +```go +// 注册WebSocket路由 +wsr := ws.NewRouter() +wsr.Use(logMiddleware()).Add("hi", func(a *ws.Context) { + a.Send(ws.H{ + "hi": time.Now(), + }) +}) +``` + +You can also use the form of router groups. + +```go +// Router +wsr := ws.NewRouter() +r1 := wsr.Use(logMiddleware()) +{ + r1.Add("hi", func(a *ws.Context) { + a.Send(ws.H{ + "hi": time.Now(), + }) + }) + + r1.Add("say", func(a *ws.Context) { + a.Send(ws.H{ + "say": "hi", + }) + }) +} +``` + +This way, the console will print logs before and after each request. + + + +### Production Mode + +Compiling `Aqi` directly will run in `dev` mode. To run in production mode, pass the following parameters during compilation. For more details, please refer to the `examples/Makefile` file. + + + +```shell +LDFLAGS = "-X '$(FLAGS_PKG).BuildDate=$(BUILD_DATE)' \ + -X '$(FLAGS_PKG).Branch=$(GIT_BRANCH)' \ + -X '$(FLAGS_PKG).CommitVersion=$(GIT_COMMIT)' \ + -X '$(FLAGS_PKG).Revision=$(GIT_REVISION)' \ + -extldflags '-static -s -w'" +``` + + + diff --git a/apic/api.go b/apic/api.go new file mode 100644 index 0000000..ca305d5 --- /dev/null +++ b/apic/api.go @@ -0,0 +1,24 @@ +package apic + +import ( + "context" + "net/url" +) + +// Api api client interface. +type Api interface { + Url() string // Request API full URL. + Path() string // Request path. + Query() url.Values // URL query parameters. + Headers() Params // Headers required for the request. + PostBody() Params // Request parameters. + FormData() Params // Form data as map[string]string. + WWWFormData() Params // Form data as map[string]string. + Setup(api Api, op *Options) (Api, error) // Setup for the API. + HttpMethod() HttpMethod // HTTP method of the request. + Debug() bool // Whether to run in debug mode. + UseContext(ctx context.Context) error // Use context. + OnRequest() error // Handle request data. + OnHttpStatusError(code int, resp []byte) error // Handle HTTP status errors. + OnResponse(resp []byte) (*ResponseData, error) // Process response data. +} diff --git a/apic/api_id.go b/apic/api_id.go new file mode 100644 index 0000000..39ac700 --- /dev/null +++ b/apic/api_id.go @@ -0,0 +1,14 @@ +package apic + +type ApiId struct { + Name string + Client Api + Request *RequestData + Response *ResponseData +} + +// Named registers as a named interface. +func (a *ApiId) Named() *ApiId { + Apis.Named(a) + return a +} diff --git a/apic/api_options.go b/apic/api_options.go new file mode 100644 index 0000000..0f75c87 --- /dev/null +++ b/apic/api_options.go @@ -0,0 +1,11 @@ +package apic + +import "net/url" + +// Options request options +type Options struct { + Query url.Values + PostBody Params + Headers Params + Setup Params +} diff --git a/apic/api_request.go b/apic/api_request.go new file mode 100644 index 0000000..eb3a37a --- /dev/null +++ b/apic/api_request.go @@ -0,0 +1,56 @@ +package apic + +import "net/url" + +type RequestData struct { + Url string `json:"url"` + HttpMethod HttpMethod `json:"httpMethod,omitempty"` + ApiId string `json:"apiId"` + Path string `json:"path,omitempty"` + Query url.Values `json:"query,omitempty"` + Form Params `json:"form,omitempty"` + WWWForm Params `json:"WWWForm,omitempty"` + PostBody Params `json:"post_body,omitempty"` + Header Params `json:"header,omitempty"` + Debug bool `json:"debug"` +} + +func (a *RequestData) InitFromApiClient(api Api) { + if a.Url == "" { + a.Url = api.Url() + } + + if a.Path == "" { + a.Path = api.Path() + } + + if a.HttpMethod == "" { + a.HttpMethod = api.HttpMethod() + } + + if a.Query == nil { + a.Query = api.Query() + } + + if a.PostBody == nil { + a.PostBody = api.PostBody() + } + + if a.Header == nil { + a.Header = api.Headers() + } + + if a.Form == nil { + a.Form = api.FormData() + } + + if a.WWWForm == nil { + a.WWWForm = api.WWWFormData() + } + + a.Debug = api.Debug() +} + +func (a *RequestData) MarshalToString() (string, error) { + return marshal(a) +} diff --git a/apic/api_response.go b/apic/api_response.go new file mode 100644 index 0000000..289d449 --- /dev/null +++ b/apic/api_response.go @@ -0,0 +1,26 @@ +package apic + +import ( + "encoding/json" + "net/http" +) + +type ResponseData struct { + HttpStatus int `json:"http_status"` + Header http.Header `json:"header,omitempty"` + Data []byte `json:"data,omitempty"` + Text string `json:"text,omitempty"` +} + +func (a *ResponseData) MarshalToString() (string, error) { + return marshal(a) +} + +func (a *ResponseData) BindJson(d any) error { + err := json.Unmarshal(a.Data, d) + if err != nil { + return err + } + + return nil +} diff --git a/apic/api_util.go b/apic/api_util.go new file mode 100644 index 0000000..40858a4 --- /dev/null +++ b/apic/api_util.go @@ -0,0 +1,12 @@ +package apic + +import "encoding/json" + +var marshal = func(a any) (string, error) { + data, err := json.Marshal(a) + if err != nil { + return "", err + } + + return string(data), nil +} diff --git a/apic/apic.go b/apic/apic.go new file mode 100644 index 0000000..60698b1 --- /dev/null +++ b/apic/apic.go @@ -0,0 +1,89 @@ +package apic + +import ( + "context" + "encoding/json" + "net/url" + + "github.com/wonli/aqi/logger" +) + +// Apic is an empty implementation of the Api interface. +// Introducing this in business logic can avoid writing too much boilerplate code. +type Apic struct { + Api +} + +func (a *Apic) Url() string { + return "" +} + +func (a *Apic) Path() string { + return "" +} + +func (a *Apic) Query() url.Values { + return nil +} + +func (a *Apic) Headers() Params { + return nil +} + +func (a *Apic) PostBody() Params { + return nil +} + +func (a *Apic) FormData() Params { + return nil +} + +func (a *Apic) WWWFormData() Params { + return nil +} + +func (a *Apic) Setup(api Api, op *Options) (Api, error) { + return api, nil +} + +func (a *Apic) HttpMethod() HttpMethod { + return POST +} + +func (a *Apic) Debug() bool { + return false +} + +func (a *Apic) UseContext(ctx context.Context) error { + return nil +} + +func (a *Apic) OnRequest() error { + return nil +} + +func (a *Apic) OnResponse(resp []byte) (*ResponseData, error) { + return &ResponseData{Data: resp}, nil +} + +func (a *Apic) OnHttpStatusError(code int, resp []byte) error { + return nil +} + +// AnyToParams converts any type to Params. +func (a *Apic) AnyToParams(d any) Params { + dByte, err := json.Marshal(d) + if err != nil { + logger.SugarLog.Errorf("Failed to convert type to byte %s", err.Error()) + return nil + } + + var p Params + err = json.Unmarshal(dByte, &p) + if err != nil { + logger.SugarLog.Errorf("Failed to convert to Params %s", err.Error()) + return nil + } + + return p +} diff --git a/apic/http_client.go b/apic/http_client.go new file mode 100644 index 0000000..bffb0c6 --- /dev/null +++ b/apic/http_client.go @@ -0,0 +1,230 @@ +package apic + +import ( + "context" + "fmt" + "net/http" + "sync" + + "github.com/guonaihong/gout" + "github.com/guonaihong/gout/dataflow" + "github.com/tidwall/gjson" +) + +var Apis *ApiClients +var once sync.Once + +// ApiClients api clients list +type ApiClients struct { + ctx context.Context + proxy string + named map[string]*ApiId +} + +func Init() *ApiClients { + once.Do(func() { + Apis = &ApiClients{ + ctx: context.Background(), + named: map[string]*ApiId{}, + } + }) + + return Apis +} + +func (a *ApiClients) Named(api *ApiId) { + _, ok := a.named[api.Name] + if ok { + panic("ApiId registered multiple times") + } + + a.named[api.Name] = api +} + +func (a *ApiClients) WithContext(ctx context.Context) *ApiClients { + a.ctx = ctx + return a +} + +func (a *ApiClients) WithProxy(proxy string) *ApiClients { + a.proxy = proxy + return a +} + +func (a *ApiClients) Call(id *ApiId, op *Options) error { + _, err := a.getApiData(id, op) + if err != nil { + return err + } + + return nil +} + +func (a *ApiClients) CallApi(id *ApiId, op *Options) (*ResponseData, error) { + return a.getApiData(id, op) +} + +func (a *ApiClients) CallNamed(name string, op *Options) (*ResponseData, error) { + id, ok := a.named[name] + if !ok { + return nil, fmt.Errorf("named api not registered") + } + + return a.getApiData(id, op) +} + +func (a *ApiClients) CallGJson(id *ApiId, op *Options) (*gjson.Result, error) { + apiData, err := a.getApiData(id, op) + if err != nil { + return nil, err + } + + g := gjson.ParseBytes(apiData.Data) + return &g, nil +} + +func (a *ApiClients) CallBindJson(id *ApiId, resp any, op *Options) error { + _, err := a.getApiData(id, op) + if err != nil { + return err + } + + return id.Response.BindJson(resp) +} + +func (a *ApiClients) CallFunc(id *ApiId, op *Options, callback func(a *Api, data []byte) error) error { + apiData, err := a.getApiData(id, op) + if err != nil { + return err + } + + return callback(&id.Client, apiData.Data) +} + +func (a *ApiClients) getApiData(id *ApiId, op *Options) (*ResponseData, error) { + api := id.Client + if id.Request == nil { + id.Request = &RequestData{} + } + + if op == nil { + op = &Options{} + } + + id.Request.ApiId = id.Name + id.Request.InitFromApiClient(id.Client) + + //setup + api, err := api.Setup(id.Client, op) + if err != nil { + return nil, err + } + + err = api.UseContext(a.ctx) + if err != nil { + return nil, err + } + + // Merge query parameters + if op.Query != nil { + if id.Request.Query == nil { + id.Request.Query = op.Query + } else { + for key, valData := range op.Query { + if len(valData) == 1 { + id.Request.Query.Add(key, valData[0]) + } else { + for _, val := range valData { + id.Request.Query.Add(key, val) + } + } + } + } + } + + //set postBody + if op.PostBody != nil { + if id.Request.PostBody == nil { + id.Request.PostBody = op.PostBody + } else { + for key, val := range op.PostBody { + id.Request.PostBody[key] = val + } + } + } + + //set header + if op.Headers != nil { + if id.Request.Header == nil { + id.Request.Header = op.Headers + } else { + for key, val := range op.Headers { + id.Request.Header[key] = val + } + } + } + + err = api.OnRequest() + if err != nil { + return nil, err + } + + var apiAddress = id.Request.Url + id.Request.Path + var client *dataflow.DataFlow + switch id.Request.HttpMethod { + case POST: + client = gout.POST(apiAddress) + case DELETE: + client = gout.DELETE(apiAddress) + case HEAD: + client = gout.HEAD(apiAddress) + case OPTIONS: + client = gout.OPTIONS(apiAddress) + case PATCH: + client = gout.OPTIONS(apiAddress) + default: + client = gout.GET(apiAddress) + } + + if a.proxy != "" { + client.SetProxy(a.proxy) + } + + client.Debug(id.Request.Debug) + if id.Request.Form != nil { + client.SetForm(id.Request.Form) + } else if id.Request.WWWForm != nil { + client.SetWWWForm(id.Request.WWWForm) + } else if id.Request.PostBody != nil { + client.SetJSON(id.Request.PostBody) + } + + if id.Request.Query != nil { + client.SetQuery(id.Request.Query) + } + + if id.Request.Header != nil { + client.SetHeader(id.Request.Header) + } + + id.Response = &ResponseData{} + err = client.Code(&id.Response.HttpStatus). + BindHeader(&id.Response.Header).BindBody(&id.Response.Data).Do() + if err != nil { + return nil, err + } + + if id.Response.HttpStatus != http.StatusOK { + err = api.OnHttpStatusError(id.Response.HttpStatus, id.Response.Data) + if err != nil { + return id.Response, err + } + } + + responseData, err := api.OnResponse(id.Response.Data) + if err != nil { + return nil, err + } + + return responseData, nil +} diff --git a/apic/http_method.go b/apic/http_method.go new file mode 100644 index 0000000..c78f215 --- /dev/null +++ b/apic/http_method.go @@ -0,0 +1,18 @@ +package apic + +import "net/http" + +// HttpMethod http method name +type HttpMethod string + +const ( + GET HttpMethod = http.MethodGet + HEAD HttpMethod = http.MethodHead + POST HttpMethod = http.MethodPost + PUT HttpMethod = http.MethodPut + PATCH HttpMethod = http.MethodPatch + DELETE HttpMethod = http.MethodDelete + CONNECT HttpMethod = http.MethodConnect + OPTIONS HttpMethod = http.MethodOptions + TRACE HttpMethod = http.MethodTrace +) diff --git a/apic/http_params.go b/apic/http_params.go new file mode 100644 index 0000000..7946f39 --- /dev/null +++ b/apic/http_params.go @@ -0,0 +1,30 @@ +package apic + +import ( + "encoding/json" +) + +// Params map[string]any +type Params map[string]any + +func (p Params) With(key string, val any) Params { + p[key] = val + return p +} + +func (p Params) WithParams(params map[string]any) Params { + for key, val := range params { + p[key] = val + } + + return p +} + +func (p Params) Marshal() []byte { + bytes, err := json.Marshal(p) + if err != nil { + return nil + } + + return bytes +} diff --git a/app.go b/app.go new file mode 100644 index 0000000..30a04ae --- /dev/null +++ b/app.go @@ -0,0 +1,209 @@ +package aqi + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/fatih/color" + "github.com/fsnotify/fsnotify" + "github.com/spf13/viper" + + "github.com/wonli/aqi/config" + "github.com/wonli/aqi/logger" + "github.com/wonli/aqi/validate" +) + +type AppConfig struct { + + //运行时数据存储基础路径 + DataPath string + + //应用日志文件配置路径 + LogPathKey string + + //默认语言 + Language string + + //开发模式 + devMode bool + + //服务名称,support.Version + //当指定 HttpServerPortFindPath 时,在配置读取之后从配置路径获取http端口 + Servername []string + ServerPort string + HttpServerPortFindPath string + + ConfigType string //配置文件类型 + ConfigPath string //配置文件路径 + ConfigName string //配置文件名称 + + Configs map[string]any + + HttpServer http.Handler //http server + + RemoteProvider *RemoteProvider //远程配置支持etcd, consul + + WatchHandler func() +} + +var acf *AppConfig + +func Init(options ...Option) *AppConfig { + acf = &AppConfig{ + Language: "zh", + ConfigType: "yaml", + ConfigName: "config", + ServerPort: "1091", + LogPathKey: "log", + DataPath: "data", + } + + for _, opt := range options { + if opt != nil { + err := opt(acf) + if err != nil { + color.Red("error %s", err.Error()) + os.Exit(1) + } + } + } + + if acf.ConfigPath == "" { + workerDir, err := os.Getwd() + if err != nil { + color.Red("Failed to get the configuration file directory: %s", err.Error()) + os.Exit(1) + } + + acf.ConfigPath = workerDir + } + + if CommitVersion == "" { + acf.devMode = true + acf.ConfigName = fmt.Sprintf("%s-dev", acf.ConfigName) + } + + // 设置环境变量的前缀 + // 自动将环境变量绑定到 Viper 配置中 + viper.SetEnvPrefix("") + viper.AutomaticEnv() + + //设置配置文件 + viper.SetConfigName(acf.ConfigName) + viper.SetConfigType(acf.ConfigType) + + viper.AddConfigPath(acf.ConfigPath) + err := viper.ReadInConfig() + if err != nil { + if acf.RemoteProvider == nil { + err = acf.WriteDefaultConfig() + if err != nil { + color.Red("Error gen default config file: %s", err.Error()) + os.Exit(1) + } + + color.Red("failed to read config file: %s", err.Error()) + os.Exit(1) + } + + color.Red("Remote configuration will be used: %s", err.Error()) + } else { + acf.Configs = viper.AllSettings() + } + + if acf.LogPathKey == "" { + color.Red("Please specify LogPathKey") + os.Exit(1) + } + + isSetDevMode := viper.IsSet("devMode") + if isSetDevMode { + setDevModel := viper.GetBool("devMode") + acf.devMode = setDevModel + } + + viper.Set("devMode", acf.devMode) + if acf.RemoteProvider != nil { + _ = viper.AddRemoteProvider(string(acf.RemoteProvider.Name), acf.RemoteProvider.Endpoint, acf.RemoteProvider.Path) + viper.SetConfigType(acf.RemoteProvider.Type) + + err := viper.ReadRemoteConfig() + if err != nil { + color.Red("Failed to read remote config") + os.Exit(1) + } + + go func() { + t := time.NewTicker(time.Minute * 30) + for range t.C { + err2 := viper.WatchRemoteConfig() + if err2 != nil { + logger.SugarLog.Errorf("unable to read remote config: %v", err2) + continue + } + + if acf.WatchHandler != nil { + acf.WatchHandler() + } + } + }() + } + + //处理http服务端口信息 + if acf.HttpServerPortFindPath != "" { + port := viper.GetString(acf.HttpServerPortFindPath) + if port == "" { + port = acf.ServerPort + } + + if strings.Contains(port, ":") { + s := strings.Split(port, ":") + port = s[len(s)-1] + } + + acf.ServerPort = port + acf.Servername = append(acf.Servername, "is now running at http://0.0.0.0:"+port) + } + + //打印系统信息 + if acf.Servername != nil { + AsciiLogo(acf.Servername...) + } + + if CommitVersion == "" { + color.Green("dev mode -- use config %s", acf.ConfigName+"."+acf.ConfigType) + } + + var c config.Logger + err = viper.UnmarshalKey(acf.LogPathKey, &c) + if err != nil { + color.Red("failed to init app log") + os.Exit(1) + } + + if !filepath.IsAbs(c.LogPath) { + c.LogPath = acf.GetDataPath(c.LogPath) + } + + //初始化日志库 + logger.Init(c) + + //validate语言配置 + validate.InitTranslator(acf.Language) + + //配置文件更新回调 + viper.OnConfigChange(func(e fsnotify.Event) { + logger.SugarLog.Infof("config file changed: %s", e.Name) + if acf.WatchHandler != nil { + acf.WatchHandler() + } + }) + + //监听配置 + viper.WatchConfig() + return acf +} diff --git a/app_asciilogo.go b/app_asciilogo.go new file mode 100644 index 0000000..e65ab28 --- /dev/null +++ b/app_asciilogo.go @@ -0,0 +1,35 @@ +package aqi + +import ( + "strings" + "time" + + "github.com/fatih/color" +) + +var ( + Branch string + Revision string + BuildDate string + CommitVersion string +) + +var asciiLogo = ` +%s Started on %s + +██▀▄─██─▄▄▄─█▄─▄█ Branch : %s-%s +██─▀─██─██▀─██─██ Commit : %s +▀▄▄▀▄▄▀───▄▄▀▄▄▄▀ Build at : %s + +` + +func AsciiLogo(serverName ...string) { + color.Cyan(asciiLogo, + strings.TrimSpace(strings.Join(serverName, " ")), + time.Now().Format("2006-01-02 15:04:05"), + Branch, + Revision, + CommitVersion, + BuildDate, + ) +} diff --git a/app_config.go b/app_config.go new file mode 100644 index 0000000..57c811e --- /dev/null +++ b/app_config.go @@ -0,0 +1,40 @@ +package aqi + +import ( + "os" + "path/filepath" + + "github.com/fatih/color" + + "github.com/wonli/aqi/config" +) + +func (a *AppConfig) GetDataPath(dir string) string { + return filepath.Join(a.ConfigPath, a.DataPath, dir) +} + +func (a *AppConfig) IsDevMode() bool { + return a.devMode +} + +func (a *AppConfig) WriteDefaultConfig() error { + workerDir, err := os.Getwd() + if err != nil { + return err + } + + ctx, err := config.GetDefaultConfig() + if err != nil { + return err + } + + filename := filepath.Join(workerDir, a.ConfigName+"."+a.ConfigType) + err = os.WriteFile(filename, []byte(ctx), 0755) + if err != nil { + return err + } + + color.Green("Configuration file has been created: " + filename) + os.Exit(0) + return nil +} diff --git a/app_server.go b/app_server.go new file mode 100644 index 0000000..37bb47b --- /dev/null +++ b/app_server.go @@ -0,0 +1,34 @@ +package aqi + +import ( + "net/http" + "os" + + "github.com/fatih/color" + + "github.com/wonli/aqi/ws" +) + +func (a *AppConfig) WithHttpServer(svr http.Handler) { + a.HttpServer = svr +} + +func (a *AppConfig) Start() { + if a.HttpServer == nil { + color.Red("HttpServer not config") + os.Exit(0) + } + + if a.HttpServer != nil { + server := ws.NewServer(a.HttpServer) + server.SetDataPath(a.DataPath) + server.SetIsDev(a.devMode) + server.Init() + } + + err := http.ListenAndServe(":"+a.ServerPort, a.HttpServer) + if err != nil { + color.Red("Listener error: %s", err.Error()) + os.Exit(0) + } +} diff --git a/app_use.go b/app_use.go new file mode 100644 index 0000000..3914bcd --- /dev/null +++ b/app_use.go @@ -0,0 +1,11 @@ +package aqi + +type Aqi struct { + AppConfig *AppConfig +} + +func Use() *Aqi { + return &Aqi{ + AppConfig: acf, + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..bc561ba --- /dev/null +++ b/config/config.go @@ -0,0 +1,50 @@ +package config + +import ( + "bytes" + "crypto/rand" + _ "embed" + "encoding/hex" + "html/template" +) + +//go:embed config.yaml +var defaultConfig []byte + +// DefaultConfigTpl store template data. +type DefaultConfigTpl struct { + JwtSecurity string +} + +func generateRandomHex(n int) (string, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} + +func GetDefaultConfig() (string, error) { + jwtSecurity, err := generateRandomHex(16) + if err != nil { + return "", err + } + + config := DefaultConfigTpl{ + JwtSecurity: jwtSecurity, + } + + tmpl, err := template.New("config").Parse(string(defaultConfig)) + if err != nil { + return "", err + } + + var renderedConfig bytes.Buffer + err = tmpl.Execute(&renderedConfig, config) + if err != nil { + return "", err + } + + return renderedConfig.String(), nil +} diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..74cd765 --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,36 @@ +port: 2015 +devMode: true +jwtSecurity: {{.JwtSecurity}} +jwtLifetime: 30d +log: + logFile: error.log + logPath: logs + maxSize: 200 + maxBackups: 3 + maxAge: 30 + compress: true + useCaller: true +redis: + store: + addr: 127.0.0.1:6379 + username: "" + pwd: "" + db: 1 + minIdleConns: 10 + idleTimeout: 5m0s +mysql: + logic: + host: 127.0.0.1 + port: 3306 + user: root + password: 123456 + database: test + prefix: t_ + idle: 10 + idleTime: 1h0m0s + maxLifetime: 1h0m0s + heartBeatTime: 30s + LogLevel: 4 + active: 50 + maxOpen: 20 + enable: 1 \ No newline at end of file diff --git a/config/config_dialog.go b/config/config_dialog.go new file mode 100644 index 0000000..38d4c94 --- /dev/null +++ b/config/config_dialog.go @@ -0,0 +1,11 @@ +package config + +import "time" + +type Dialog struct { + OpInterval time.Duration `yaml:"opInterval" json:"opInterval,omitempty"` // Interval for sending op messages + IdleInterval time.Duration `yaml:"idleInterval" json:"idleInterval,omitempty"` // Interval for inserting system time in the session list + SessionExpire time.Duration `yaml:"sessionExpire" json:"sessionExpire,omitempty"` // Session expiration duration + GuardInterval time.Duration `yaml:"guardInterval" json:"guardInterval,omitempty"` // Scan interval duration + AssignInterval time.Duration `yaml:"assignInterval" json:"assignInterval,omitempty"` // Assignment interval time +} diff --git a/config/config_logger.go b/config/config_logger.go new file mode 100644 index 0000000..b3cb0fa --- /dev/null +++ b/config/config_logger.go @@ -0,0 +1,49 @@ +package config + +import ( + "time" + + "go.uber.org/zap/zapcore" +) + +type Logger struct { + LogFile string + LogPath string `yaml:"logPath"` // Path of the log file + MaxSize int `yaml:"maxSize"` // Maximum log file size in MB + MaxBackups int `yaml:"maxBackups"` // Maximum number of log file backups + MaxAge int `yaml:"maxAge"` // Maximum number of days to retain log files + Compress bool `yaml:"compress"` // Whether to enable gzip compression + UseCaller bool `yaml:"useCaller"` // Whether to enable Zap Caller +} + +// GetEncoder 根据模式获取编码器 +func (config *Logger) GetEncoder(mode string) zapcore.Encoder { + encoderConfig := zapcore.EncoderConfig{ + TimeKey: "time", + LevelKey: "level", + NameKey: "logger", + MessageKey: "msg", + StacktraceKey: "stacktrace", + LineEnding: zapcore.DefaultLineEnding, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + EncodeCaller: zapcore.FullCallerEncoder, + } + + if config.UseCaller { + encoderConfig.CallerKey = "caller" + encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder + } + + encoderConfig.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Format("2006-01-02 15:04:05.000")) + } + + if mode == "file" { + return zapcore.NewConsoleEncoder(encoderConfig) + } + + //控制台模式下显示颜色 + encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + return zapcore.NewConsoleEncoder(encoderConfig) +} diff --git a/config/config_mysql.go b/config/config_mysql.go new file mode 100644 index 0000000..12e65c1 --- /dev/null +++ b/config/config_mysql.go @@ -0,0 +1,35 @@ +package config + +import ( + "fmt" + "time" +) + +type MySQL struct { + Host string `yaml:"host" json:"host"` + Port int `yaml:"port" json:"port"` + User string `yaml:"user" json:"user"` + Password string `yaml:"password" json:"password"` + Database string `yaml:"database" json:"database"` + Prefix string `yaml:"prefix" json:"prefix"` + LogLevel int `yaml:"logLevel"` + Idle int `yaml:"idle" json:"idle"` + IdleTime time.Duration `yaml:"idleTime" json:"idleTime,omitempty"` + MaxLifetime time.Duration `yaml:"maxLifetime" json:"maxLifetime,omitempty"` // Maximum time a connection can be reused + HeartBeatTime time.Duration `yaml:"heartBeatTime" json:"heartBeatTime,omitempty"` // Heartbeat check time for MySQL server connections + Active int `yaml:"active" json:"active"` // Active connections + MaxOpen int `yaml:"maxOpen" json:"maxOpen"` // Maximum open connections + Enable int `yaml:"enable" json:"enable"` // 0, disabled; 1, enabled + + AutoMigrateTables bool // Whether to synchronize table structures +} + +func (dbc *MySQL) GetDsn() string { + return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + dbc.User, + dbc.Password, + dbc.Host, + dbc.Port, + dbc.Database, + ) +} diff --git a/config/config_redis.go b/config/config_redis.go new file mode 100644 index 0000000..0a0df56 --- /dev/null +++ b/config/config_redis.go @@ -0,0 +1,15 @@ +package config + +import ( + "time" +) + +type Redis struct { + Addr string `yaml:"addr" json:"addr"` + Username string `yaml:"username" json:"username"` + Pwd string `yaml:"pwd" json:"pwd"` + Db int `yaml:"db" json:"db"` + LogLevel int `yaml:"logLevel"` + MinIdleConns int `yaml:"minIdleConns" json:"minIdleConns"` // Minimum number of idle connections, useful when establishing new connections is slow. + IdleTimeout time.Duration `yaml:"idleTimeout" json:"idleTimeout,omitempty"` // Time after which idle connections are closed by the client, default is 5 minutes, -1 disables the setting. +} diff --git a/config/config_sqlite.go b/config/config_sqlite.go new file mode 100644 index 0000000..566b748 --- /dev/null +++ b/config/config_sqlite.go @@ -0,0 +1,14 @@ +package config + +import ( + "time" +) + +type Sqlite struct { + Database string `yaml:"database"` // Path to the database file + Prefix string `yaml:"prefix"` // Table prefix + MaxIdleConns int `yaml:"maxIdleConns"` // Maximum number of idle connections in the pool + MaxOpenConns int `yaml:"maxOpenConns"` // Maximum number of open connections to the database + LogLevel int `yaml:"logLevel"` // Log level + ConnMaxLifetime time.Duration `yaml:"connMaxLifetime"` // Maximum lifetime of connections +} diff --git a/config/config_sqlserver.go b/config/config_sqlserver.go new file mode 100644 index 0000000..86b3d1e --- /dev/null +++ b/config/config_sqlserver.go @@ -0,0 +1,35 @@ +package config + +import ( + "fmt" + "net/url" + "time" +) + +type SqlServer struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + User string `yaml:"user"` + Pwd string `yaml:"pwd"` + Database string `yaml:"database"` + Prefix string `yaml:"prefix"` + Encrypt string `yaml:"encrypt"` + LogLevel int `yaml:"logLevel"` + TrustServerCertificate string `yaml:"trustServerCertificate"` + Idle int `yaml:"idle"` + IdleTime time.Duration `yaml:"idleTime"` + MaxLifetime time.Duration `yaml:"maxLifetime"` // Maximum time a connection can be reused + MaxOpen int `yaml:"maxOpen"` +} + +func (m *SqlServer) GetDsn() string { + return fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s&encrypt=%s&trustServerCertificate=%s", + m.User, + url.QueryEscape(m.Pwd), + m.Host, + m.Port, + m.Database, + m.Encrypt, + m.TrustServerCertificate, + ) +} diff --git a/docs/assets/img.png b/docs/assets/img.png new file mode 100644 index 0000000000000000000000000000000000000000..c20d8d71170ad623e6833edd9a3a727c487d0975 GIT binary patch literal 246779 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%0D7Fp>KP>A? zOhA;KM8=$;!peBIRv@gS=X=0fdCaSVuf@+4G@<1Q504GUdNaHRERBlxL!LqaVmQzl zk^R%9{Y#eT&3fjxNYJ2113wvn&T0V^XhjcV8xn|wT#nI+cUR;+0UY^cm)$6CP6@t0 zpm-j?L?3Nkp8V$gpSrGD1Dt>HczHBI5DFlT$fJD}{)>h$AyEuh)tJYYLEotYbj1xV z4KS;9c~5!(W_b{|1@=RK^Y$@=e7qCM6Lt^4hx=-_9U_&%*9deAYL5&-7>*J^{>Ht@ zqBEG_L$Cj0OZ;``yju4_z{<=u*=!X%6|j7gL)(kLdu347QFQ`BqpxjxI6Wea(dg9y zu?^Q<^{o|PP)0I9O}seWr@e0L;s-`FIJ|HEPaeMXVRnuPWL{;yJqrBK1PRg>L@a;I ze10M#gw+!tL-a)ro`=XR-HGU>ig_6K$mLM15gNS-74j)DqcGEHj2OZCYluYSWm_MU zD#k@E#>FBJb$DHG58O zAnhosK)7{nt#EMOnTWXf$(um$AM4#K9)9^0;@?+_cjBOHSq6Zqmk@;pP>0lJpkk#> zn1R}(2e#oKqW+WnU#e*6#&{lNqfPs@^`Vf9yKXgs=)WLC9B9tqe|4fCqe7rfG0Q zx!>!Phe;;lzV~=yDUm#3P*Ip&WRgoEC_FnTGc)Mw{o;U!-zRW(I3&Qyr?7Nwyf@5y zcWV_Xw($eGwK|6Z$GX1QQs~IpZ8G!1xe(Y6cfzSK-?2V9#~)s5>%EZ-JufnI{l(YM zn)!cIqyN@#MM9{Fj*gr~ zvaIcw(3>JshkCM-}r7c!I^Cr~aylU^c z2nlc`q=g^IxA89D@LDKQEb{aalCEC%3RkJQ=(7AXndZrTLiVNvf8GPD-gqiS`uu!7 zQ&Sm6U|0_u-;7N1ose%*tZPAcCZXh`;+oh++Vy-6Bn>C47a92nN+^@m8_j`T6}ya!oCqi zWAcB~JOH&EQD^#vjRp9rL1cjU?lw6I6lRS+Ohlyqlp_lT5{=)#;=B0&|Eile5rLy; z1O}Fm!dUJp_Q6vq2k;LfhX4Hs;6R_t zp$VM-U#BYvG(`4q7V6=> zlh8ZQ@bLwz3h6~4VV3}@;os37z!%`OZ*_#8yyjEafvDr+(%&u-^*&k=j187~s%v<0 zC)%z_I@QRF4YtA>)?HM7M5HSLf1rdekPZmyWZ8}5tCG_Ce?CNK5`w5{v7Ut|F?wER2VkrSc4o*F zrwHhofo9`iT^SjjXGV+Gc@IiZ>KX<`CCTbZS*g!hA-D?w)BpeEEEt%nau0x;c;Kn@ zLko}Dd{*Pmx9lgm00b{XJ-A)Lq+vhrU7udG$ptBM1&fL;`ra!c*e)+t7-l;9`IZ5| zuXECneg^hEqV(C>BLvtHbRx~?Lr*=9&uCy}>HXR*ZR6wQ_c$TCW2{E%4=cYt$}UqH zHh83?=avc+ih2)F>yKu}_y%Ti@BWvExgX4{_gM^5D#*)oKy^16VJmB`fgy3c3kDKi zLC}O+apXr~r{CoK&5`Uknf4?IHU1>49z6dfM7^XkSZ*0;et&S1x@J(^+#q|TYi1FZ zk*Z}Uj}LycQ($1$rg%)w?Y? z_WTaBQ(!a$`EFre({o%^j-roiYN;JQG+il0A3PLzIu`$`mH#8PMB#UYl~jXM2xujx z-N`w(KY2ap=Y__cQ3Im_a=iDsi>)_CFmGRQck!KB(sw?}Fx0>)58zXt@3t=Zh=2pm zh~8u0Pb}N9S#I^}>Q{Yd_q!+2QJO0X9XCC`sDO3R-+Te`S-5C3YC^tr$Se;%#r}>?R3xCnY(eaJni&q-_H$@~fM0=U-6mp?(Tg*Lk$Lnm) zo#*>S-0V~udgoAfj73pLt&2;=RCS)XrLTPU#0yt7``gN7Yj$=(vH6SAx||j@u1d$O zSJLM3;SmaQqx~iI9v1^xqTUyNgc@1)#c+qlDLq;03;Q8#21t^zg_{bk0G(rD?TXCB z22yG)M=Y@5{lxiJdB?R{OM_#6v#No1=ljYz>~D5$2X~+LMg|LPfUQckQ~TC8_VC~9 z>>fR0V)2@^X!U$*xrZE^h37 zt7UK80fce?=U)K}C5tuo_z63ioj&${-USb2S>rfbWh+i-u{4A2``m>t%kvL1ug1QO zxrD43T*nnV&8=v%F*Hu^$c}wZR00>i;>@I=c>QWXP6|j?H>g{~nAasUkAsxJ4sPu7 z`rHNerB%8G%o*($E4nk6SDPQ&Hz_ZW!*UJ=m3jR$P(9xz<>=VP(~JO&u}2|A6}1^E z{pG9zjG56Y96zZrCfAGYy^TQl#bxXJn4C;(8g9T$8mo;pchQ)ExjSUlZ^7DNBof>h z#x%sns)H=il2$e%On=j7!DP|X05uY+b6yOXsc|>RE#gM^^>Y+lP#&{2R)467_b)25 zgjMDi$2-^)PVeCTR~$tYnvdC8=>P4(QhJawFjWfIGxzH{i1wQ8Vf0zJc3$y27L#%| zVbO8o{GIuH{B+dmy#_Y!^40?#p^8`Z>IXkRIQgGQfDz{g87-UCq@)|)+cVF%hBl?f z_S4!+y7BiOS*oA2L+G_*`q{VoMhiXG`$_Fv{L;_%XEis6a!X+|&_~Yd7U{kxQ?B*~ z$SMzyk!423UmqbgF}5?6vtA=LtG$#cuOk#&Mu*EYO)*phIZyrX=`pR`a_BG2s@p4H zV@$m+*FB9tIZ2%3s7}Um8n*=81waAEM}@GVPh4Dej=k}K^>}yThO1KMbm5mHqc({e zEI2~@d`GlD`5!02zLb4&OwDsXM+c380DMXH?b13C!g2OuGc8{uWLm~)( zcHL%~wY@~l-0D+}u}TvmZ%KJBDc2*WY*GIVbIa`1RDDsO6AHxxghG=Hib3E@y~Cv8 z8UuoC$E?jKg^bf!W*sD8N?63J!=?t!?b(5Y8-o}_$a%m+RltWrP1Lu#mp@1VQ!2`3 z)F8vfQ+i4zG&-0mWQT0n^G`*btpZg$w(lDwAHqfS^g%Ypy$1iQ8W;ucmJjy`RWci0 zwx2}K1M+ZHz=t>=>dmVBoGJCS_Ly93>q{{~)y_7MJyzbFs({5=xhh@}uV;DZ)@rs7 zzRqq{oVJn7i{;w;VB8U@)4%i0SWg~k;=NXdNt&v)5wddn|!|%rLHoVcQx>6rnNo&NVC-`t(mB$bkQo`0a;G(&}F4!d`QO3 z<(K)MtV4AIt179!BK-~2P6B{`ROaaP+7Y`v=m{B#Msh$4KP4ACb;YJ<-F)r~b0~87 zET~&LxFl-26B0V#X2YXu(7MnHp`wv;4NiMHHD+sNnJkOm7`q9Hu)Q40&RXf5qD3ZM z&~r4tEA&ZaOCe*`i;*7<(M=bR>a!e5zY#C^Q?%LwgA2$$1K!VqzM^V1elZlKuJ$bt zQSiXH$pg!Z)<3&elTzngLZ#ANP5C>w?#pp5AK_QD5+ow#>YHP%v)0kz6Uu2o<08Rw zT|g)07!4Y7cSt~~m&_pU`2dq7psk41?_5uG8jA}0Db-`1KVX%mqdH0rs|J#4^CIp7 zH$oc8-GM5+<`z4ozNqWbqYeeyMMp7DSMT?tPA2v&*`kn)1|aV&v0kelsVs9`nzZHV z=62h)-c0KY8T;3h17AY1qs=d*?Wl{zoQd0RHZJvag(?laa1F>v$dqkNX1CDwCX$Lf zo|~v^QdKZfTB4k^_vwf4NBR8McNieL?2lRPIx|m{*6v)CDsrB*Bt;EKNsuWn^B*Kt zzNVbk+0(AM$Cf_ru{QM#a*nPto0A} z4Q_*V}df5Y@dMMa?v?Ll-I z4K6=)5U#8D9OoArvk+OTLLH%W(WzMrpTw)mTFsmPWANLbskbav>qGI?&&S)r?W*8i zIvp}?1Q2Dcd8$3Q|B4(P2jIW8?W_iGhZp=M6(J2f!RL~v`q}&~_UM5DI8;=i)IRg| z!y!yF=t<1#$7HpSJk}GG{QdKH5 zX_vf$wHUme_@v)x?BeRqBYyFVaGX$>$!*PkzB7NH)d!s;4#DV+&jmgF1ePL6e*>01 z=D>#>bZPB3FepF&0xY80pC5TFi2H7=MJKqM3 z>n!g!yH7Pcq~RKvkaOy!w1AO5t~Xz>vP68zY;uk3UbBtWK^$pfeq5gm)ffdE?wt}i zn&bGNY8g1$dv1+bKsa$AiwjLSr5d>kuHGr4UPe5i4w4fUXLGwMEn}m~nHNg$T5S%@ z>dl(g){_5OUdZrZ#Z_{HXaF=LE@vePwzPdcEwAausVOY8_lPI}~lPw?EYo$yeE$m4Bu z6C-}Q@nb42GjL3}wW?}(>RDLo1TL0*8PvJ^D5ZR)t%8tLAoT{(jP(^37hLDSS`GtY z*g5(qGYrc^z#;bYdGW<<@G_|GDpI73D$OpT_O@SON3Xbg?tyxTclrq-H+#9BkgG|v z4?g;FhMo&|&*PqW@^FOQ-^X)eCil8I);%20-Ha@%;&0E$A)c!@Ay404{iK8Cjhkr( zTs{r(zw!dfiNUnIJR~56)qd;7AQH~?Zkr+hxKEoMHJNV$&lea0*0Ma!@PZ^c&qMTv zN?B%h%=Hd=0UUPc87W@k$W+Y0d$%?2!263=m2bNkWkFjiO=3J{xXS*wVRJL-#g;#ju`~JcH9(t~$LFmCxnp!{g_}Y}Ip1YJ^6onb7SWM{}g7 zPMFx%>37%4UP`An$<))g$zlJoyN@7+b+4MWjVFX(YZVeg*T{(o_5@4mJds%%3Y7Sqb}*TBlN_7J&?ZrGNCj;@#rX!-eH?`jSOUaNkE{^{KOGl zP2_vNXHZGbG?3zn6O-y8BKHWYNNb(L-Ibh?*V{)*cV=_Me&3``7edO`5F(^>|v$sd4AWEP@5 z*Cr}!KX%}H3$*odu}3)Ol_#?srYnK~K}mbUMf5s-&-3JgESTZ%!;Qd_5aOszOP2c~ z8f?Db%hPd1+ITRhxD))fa7e+iml{&M542Ua&YPL0QUviPavpA>iw1)hWKA6zThzFa>!= zPM=*;#DwKDRIx$u|81K{#1BG9@55Q15st3^xWZl8dDkwj$>wJ$=6`N~^Rt%M94`vu z^Z*j|_l@+42rHM(&J1a#v5@`eb83*FVJh?b;fUw1|F`r>?k*)-lQ`@Us{j(BjYp4? zlP$_r|51#gp#1%&AB6W`m|3uqaqo2%><4h78A|io3{JUdB2`sfr-sS?u_L4W!1)_Q zWy^Nx-j$qv-7;^XY%5=2c{ZN{vlWqV zKItucM`_HxWDUqam(BJB*tt=?4Y_{h0T*^+9`j=4goat-KUf#ON9h%+%G%Hj(CvetP?+(TWcPXIp)1f@$;!aAN92q zAk@!Oo}p--#*?7?dAL4#yUDag5kO47n`%*5`dKJB@HNx3e~!HgZdNhBF@5y4{cUd4 zlTHf-jYk@0I|`decbX87u`AC{JQQWz%5Z;FEOL08nPrBq^kY)Q75Wk~KS_O2UC(b4 z79Vf=gKhGu#8Z+@=hOUF7My#NKX4Qx7^NN6E!k5~$X~iT(;AamcMb>I+H!!#iARH` zynN^z-c*J}l=fCzv`YK(Fh@#4+OV~s1=&l@Ix47cFi0Pr$vRFU8mIZBumf)YRGush zS#ZG*tM{+O;ySnB*M91L_gJL5uIMUG{3toxJCn=MH^y$hl`%}koO5N0!$6TwXrhNFB{hMFp{k;t*wr9V!r#j9j5nFUe z#it85ykw!RywPkm5mDT;6l;(SPxPLG(?#jMQK4Wn=RPvMeV^nC-k{W5YW(%xSZ_#^ zwoDEL>gHC!jQIKLcKU0XliJrg5%}HldJ>|5Bddpk0j7mqLS;W3yTG7j2U4YepJ~n1 zrzN4wyK!cZQoma6n<8%-me(T(G&xMbab5ZkkZ*mymYj{KWPtcOT&}v+_Hgd(&(-;jUFRKBp%8Meq9Kh3n}l-vXLz{pZR{j{S(_7pR)B~?4FNJx0Y9ynrE5A zgk<6moT#$&#ZcJHy$>KgLMjz?CJ9uE3$ph+eiS_m6J};2u~TSO8kHMETqMDBLd-If zl6#jKNmN#H3ZXb=v>{y}@0_AIZ3@d5kqz_Eu{=3`|K%FLA^wBfD{_BIU6`8{6%Mo2{11+KR(PahfnY@J zt`e!<%w?5vZFXH;VzZ4yZ@l__{rrj>tEWLlRh|LRbYi99Xt@`ORzrM*%>*?wkVN-{ zC_J6cdQ-ab{?9B|K&L-sQ?%xd`ZH70So?v{HKM)aRSa_}0O#CN36(nS|%H-qOufG2J*4!hbsPFK~8-Dgl z5hEHgz3j9-*Z(t%afG0yHM=iq-jA!!VZ%y)W-Yw>a1LqbB55)<i=3LPMW0 zFuZ!#(Q#Y0u@*^zj!#x5gwzwhata_WD6k}=%H@UT!$S*6dIG)++}$J_?)E~KtRhQ< zTd%gEiw~=E``m#bxFc^o#gxm@`fg8ci-Pv%>CG2N`Lw33Wh0U&=xKNLosoMHp z_z=#Bx$9kGsq##`*cE9e>h0M6vhD2avZ5jsZ)yfJ+a&LW;VybftQp)rg*Vj<{)}Jd9BIRvAN=eR5=fNjOtGEtq zj=!kYYsfg`bgo|&S#`k+@v4uN#3X0GSKMH{SV&9o5`cMUBPK!!8z##Gx`u9$VZW`J zZ7mS4J*FdB5G1>Ho}0OOd{*(xD=YATNBDTioapgZ!OXKMCmtzZT3T9LN5`*G51)>V zj~CxDo88M%Jc_7Wb2;JRQb1m*aoP=ctxTByym*WM8n2-@xi>zZfLg-m5irE73l45c z{P*KZxmhJ1Y94Hs9&32d4}}0nua1xau+r zX7QOLR++*A?1==y1e*I0lUYjiOxO4CcnW)6kI8d|xAWn8qYFE^;b=L7R$US7Uh;t# z3Qag(shYB&;Br_&3Kh}>(f{n*>#AutIMnDC+(9`Tqm71=dA)qSD z`R`hdlHNJE!OU4HT0)Xj6=nu@y~$bz8;s*kU110k3S<_bD?uM$@CW8%VW!L2rZv4#~rUvvYMcW`LTTy z=@;>Sizs(MlP`s}m6F4V8b0yRr5&6W!bR{&>y_E(QXPwU_NS?kdczi!&Zir=knAD` z!!HwrUV^YZApw5V`t~W;CUvZtea*X8cD~v=BF|BzB%;0DSPlm!_a(1kMqsJg9cK6Y z6AR8byDbJ}1QkLQu`r%$%zB-4P7unWeupW@&m}S!wlROA9EbaP{aRTKAGC>B)oOuh zrS1@uJ}i>t&y<8~-zqJw5B`;QY*_)q>*GD9DAYGh`c&{5%Rtf_Uz^_fmGbFr2%T3) z_)YjNMc>PcW(1<3v61}p{GfNaE275d)V@U){Ohu%D&Z+v{aT)l1;*8NYf>g<2uPxs zwJ(b5ERX>?b>tqf`~^!71cIu}oiF$j0`No0jls-_j~^*Wxzj#;D1-d?@dH-iRS|fQ z$}7yu`g8kMpKg}o3lP-@1l-7evGP}gx-`u9&KSyqaom~9)Q5*;cY@`ou7)Ch+8@wU zi+R&-@|b?z{*>mr)u;SszJUs9P$BIN;VM{tI4;>>DKWhCXFkN3Bm}-G{;Vwn#<6=S z6T3B(8L@ZzX=!geoMZmGcG_CBmbuW}ZgzOjBZC1~h=KI}1Y&w>kx#cx1D}66g0zZJ zs6WpP`EygurtA!l1U;mg6Jg>XA`A9*)GXsbCUAYsAKX?Sucen4o5(0=ZW`{8jKfkKpM1UJT=j|3 zY*x6pyD9d?nz78xJylJx&9xe1vh zqCH_D`*4lDzxx(HP3l63NueDh#MOoB-nSB5JUoy_o$IPX1-NBmU(3LlbIqprrIl9{ zYy_}YViLF<2$hj>?RSY!%s;-lY3G#MapSaWc@lI4fcAohhBS7kYGlI@Y;0^WQ6VWQ zx^LgU9iE*v_)0cBnE6>#^8=W!%j%O>rQb`7TwG-Ci;If-I>F<~$i^l=Xp*`!ZRVvZ zbz#u{6A{OZN_io~#&y?Z;gUkA{T8F+>OyD^;Z6q#ap$O|P0j7nGiq@^lBb&H-6`cl zKbA(F-yR-%0n?H(@CP03181K_be-W@F4^Wgxs?$kk2Wn7yEuP(H!s_td!SgF8O9c4 zXJ;-lO6e=~h64=wsUl{rVL8oq7A#jyuO9O;;=2ea2bv0HnZ4eIigD*V^*(UCv=eS~ zAsgb%2s@6kS<#xMx_|0FaqJ~b@}8yG1ke^zkS54Wh;#&s2F5@aUlFOS?sdq z{h%E!gQ0qVyLeoE#tJ8Wmenfn(mOkg<$8PAMe8AY$4wy?WtF!Vd>syWP z7$fTPNoNf$8a{8D&mO%#SG?l&JRgp@(VP{2K#o>1yv;vb=yP4OQSAueJD)2s+?8rH ztj|t7BQ($6(&D11kOf11Qk$WNexT7jEdx56^>Dtwt2?Wq`P`$(qzV?nHw4@Phl_eU zVR&?{;fe32ZFw#UPmP1;y~J)cL4yu0RiVIip`ov-Tqe5&kaZhD;Sq8$-VtaA3kOoX zE~9T;v)*XwZBEHU5cO%=19e z!(#w%Q+P(pfN%U~k>0Mv$Tm^MV>H3_f=}qTHjfs zS;ZX={mS0SMZbtkLR!ntFUh;zi9jZ@IZRKrnB?@Y^N=$?o z`^L+L@)jSiph{*`b`&9O<8$wK8fi&uNa6E9=jP6tk>gsHP7_m^NZ9jAm}cj$5cnR< zshAH1Tw?HZvCgC}5LajoW$#>6M3jTd%Gn{C=zJ)s?DB>;-|5}?=bFc}{CdR|)S5oc z?WdhfT9acT=XS;WLHZ}Np$I-jXixo(GoDW|ybdQ3QrmXom-5aq9ykeMeZDg|s`#3yYqcr4G}Q%_naUHUq`Qgm;si>c3uN=vk+r;z=zrWU9fdx%>`S~*K} z0;ays&YMEjtW5f;t}#(%eyz?X1u=H^IY-N2hPBV3ZASlql5@576GRe7=gSxP{o`vj zoTgalYcbJB3tR4KHN}C|by0~Ecy7C2#+oeHAu;2=OuZ?Gri2jCBDnDC$Xg$}vu^=C zqlFs~{aKltjsOI5O4&hWJFTT=_4dmiP1SPCuz6RbiMj0J)h)R>=dIM?HF?!?Ps5~( z|4s+Jly^^}J(!+;-Bt#0;I8tn1-QaO+;AJHMSMXT}RBrsq*rMPOMV4?K8tI zq42SQ<$13~P)c(Q8JA9eS>3a~zP=Z4-!|EIJh_G}dT-LSY!oGXmXd4*G@tW|l5uNq zl94=)2Bt}ty)?)T*$dz)IAU}h8my%GX@9HD#9aDHd6JKGOF>R9Ubtg496>UnKQXcT zBRTNpDFXkHkU+ZHlN#Ed(kj4kX0*XxWz~E#fqx%I-pz*$?(b_-Z&h82-}Na6vHA$z z-|lm4-WC`t-Z<~XNlg!!SI}ear%Wt`t+fg%$wi`PuqXJb#SM60*k# zCaNzp=bWbA)l(}1o0n^k~t zK^2Xmwy?VfU8_=UWGa2M!*=3n0fYk5wGzA<=qcSYakhu zl!g1po{6&yDh*y=@8P4biWX~1OCu1Tc)mxd!2`XNs&O8|UnDnrE%%Pv`w)NZy&dy4 z++gobJoei>7WH$jR*!p%^rW^6G$!6F_RpE)Le?MqnY6T%_^2jWrDqPrbpF}r8lB<= zQZO-tLhM=+o0S=x+mbIPq-guIs!KZI>ml=dsmfpXhe&S;Gzp>lG~zvgUi2+ zL4n5A?sOzOQ^V5{QcE0&!-f!4>%7jhq9hdysIzM$!{#0hW^g87Js0b^pV{E|4x2Er z3kV5%h|uk?HNoj}YBD4UUxTIJtvUC72fw3f&qpcxQv&Hpe-*%9HvbAMgH;0Kc>Hy# zU=CD?$p$aaBTlTQ!x4>XCNlF2!QNBjwm6Wgb2UZT+8A6X=CW(2X^*+B>=LIumP(R> zI|J;o0vYo);(^?>@BpwQVhQln)B|;VJ6Wey`lAeq_ z^!2#xe3(S@R3MOv88MrFV4o{YODl^8<4CNBTjnp{*k^|#bfVZnc_h_2%{6MLr75RA zL~KBA+h!KnWQ>*Dz{R*`4KD52>s-dmwhzS7Tu16u>CZzvf<${pk*edZwMT`ZJkIg1 zOAQ{N`Gr~G#@|@c$_k<3%!d<~3#M8OL&MO*4WKW4=lnCL#%;}}VNE9If|j?GNN=Q% zom7pLLpJQ4gkO#Bpfr1!l^I{EDK^fhQ!TG=M}E5!R#2DJQ?#n&2-SgOX4SrwQi|oAuZf=E&jDN>SYXqktJHr~3 zP7=rRUU1JF(y|@}Z0MxV)NDr)#W$S%yUoF|T+(~{^DQ7|cxdC8&+^Ik_xpHwy5lWk zUi)%NN|6_4i7E@g+%T7Jk?q%X&CiUbEuws!vOg(fIU5>=_OJD1bbJeN%ULHKkBm?9 z%s3l#sL0`3KfM@)+?$)-n>cFZjmH8dkkv13*mpZ7xQA~ z>4$F0L~=*1{3=WYUn=d8ioBD2E610*y_Jx-nX^Mn3MFy zzO@2HBB|ZThQ+jb{KKtE8nKCY_Ktx@PSo=9MVQ_suiJlx$H2-yymPhig5$mlk@W+; z;Bdqs(fy+L)v5N(ct)@fzn*i`Mt_)jL{%f%u?sX_!kK2T4JE1f({7>{MiIJ3rTVX5 zt*NR`s0ui0NXK6AT*1`eHBH^u696>F^#k$8~L)o85?6=*G`GZN6b`N0aa0iFHXZpv$g#H^g7>EmhT^Vo%~r zqc1!AAo-9P@-6rG?{tIbM|hnXv8K3l?P2WCdomo0(fSr$wdJ18=$_&8mR; zqTtDkj2%m*j+Y|wrtTtABJcs-1s?x&pW(XAqnmHQlq1)sx4zA=lX^IB1FaAXH%0My zl+svFk^^xSjciZqxjeihOEcY5-OAL?oxZbWPETV<#Pn% zvm^C!C+VublwKs=wNPEkjq@9$6Dd$)c?4oC$D%j-Hh)IIFo6+J?XsPTN?!{tw!l&X z{4=Wij$wKmo?NUi6jVadh2@RrL}NM9FvzLaq>eIG!3?_+WFM%!>E*S!EXzRV^75XH zD;iwv?$0(7Mi9{G2&pBLl{C0})Bck5UJ&W~p~ydR-1n0Qo$v5lq>~qKqRzsy4eN@q zea~ohfW}4<$x*-PH;n_Sx4~44w~@Lw+$-0FQo8ary_#8FQOTQo{aU>!U4t36EvqiX z=Mi2H&r_Ik%?<~k64QD~WF2wVLCd(r!KI1AEkmg>W{<(81jci$BPqbjF5K@Z7^-o#U#FbA5s zenYo6)8>T9d-U`rp*3!dJa#EAhcczGj5tk#pl!SHGQH@T+R8({<=P+9K08Y&(a@A1 zZ_exIJa)GO^hvAi)k3TYj#y_64Z4jJ>_j$3AgMg;$acPP$1+?0fC)>JVO^{?c9;4A zS*4UJy)l)acD|CStCE>5RTeP6=p7hN8}VmLX&30D+rC-rv5~&v zp8B-G1Cu%5ASWYPTbMn4yuVA`s&;1)K6C5acN>tJ+u7HrG>-Iay6W^1Yo<5exl#iq zabUm`H#u5Vs|(84rul#u_{ca}*Qv8u=x~u89Up_Noc#Q*K0mX{&dtq5SQr}_Q3i)F zr@j3}Bq8;wj%N-lNN?#LsO z>wUVivc4nixiZ%sidY_fW^3LgR}cDmnCy60@~16Fd%N4+O&&9OHJMubuTN&N!^6s3 z#+6ChEGNzCv)hMza|40~5BwrgvAaQg=H0|H^WUs#&>B`p#q`&j%xlS1iONzYPz4%p z@nV}bq#nCR_772IEr({Dg%L<0^CnM9kE3cW2y_w?Q1fT@Fgo?Sb}g1XO(Pw^trV3m zQ4rcU4FiYl-R9`8igSt9ft`>?$LOePqFZvjQ$?l3Dn!(Ui=|S1c{RSH6~Tq5@5btb zp2z5$qn$;UgNR%Z0lScfs+zh%7`$_t_&)BZ`veJkly3mAFd5fgCzL-8#Iw+)sF}cx zs&BjwesFOUk-O3}NQa`CzGvDIc1f69_Iz42F9k=ik*k|0;kM+g8Rl4e;C|gA)^v5+ z$e!M?X>I=iax0z>PvpVH`Q_0C3MG+9D27e`x&^e-}OuplHJE^fr_&hO)tFa z)@<}~^LH&;4su74p@=b+<)OBSw?DVMQbKaX{V~k&L!9=p;0ScPq!nicmUS0%0$wr-3}DhQExQzV z`@C|*4v-6Q32AS_&FOCMF3vZ_jPD4-s7m7?rZlqPe;_bx!hgaT1Sho4H_dNg#=L&M zt8fI5QEvgap57#VN0S=q*M9Zy08r^!M-3LoWNR2>uZBr2s^$bmqkb7ijuR8D5B;Q8 zThxjRplIzGc=$^IMB6`D7dL944rt3x#;@-f_DN)N*|&%9)%bO+gdWqF21{OXn92Y~#{?o8sG_$8@k; zCmEFt@bvCq1@icR@q!|_!1>n(Vig!4-0S*W=vQh~MWiyNbWouz{_he?Au1+%xiI!m%o9WVa0H3OiT zf1#Qbe=l-Y3TAUaKtN}o~l_1YS}C^vEEauV#zq-QL-MVT1x5mM#O`18HTDf)W(O1_Lh*Eal(P|`mT zsHDQ*aGW6}m|IA2aAJD8$P*3@JY{8N`!=(hOTK)#0M1+HI#QCOaQP^TdcO*hU7)n^ z0HVmG>~HPK4YNlR(@;g!#d4wq>Ygw$J+dx;6!^an5=T2#QdCsNE5yiX`3|$i!oq_6 zB*n3Z1C@OrXcC;E{OP^+rT|1qWNfs)Gd#4_ic~;JLzzl1L*jpLQh`t0+4(@)0ZP7cK zT8-KH`}b9l*yo(SWK&=;53w#WAsw5qu=+XDh*8!}n{ zRc`G+mw#z}J0Av-*UC#cqj*>FMf?%j8UX{6Z%lV-XEmsrC=e6-qW1dBTbEERgVROc z^t#?sl>2ew9?Sncx{=aXI4?y}DLf7F%-F~0;z=zhH#Y$s4!1Y)as+T#tH6HEf9v`0 zbr&3y8K0e{N>5K`@2+P-uB@(F@KT@}-plf#Xt4hSlD7J%4p{exAu9JCK(YZ8)L)C@ z_U}F$fY1$mD2T`ZJRHA(>^w0$WR>K5V&aEq76q#$5!Bxw|Bt0l`t(aT9HH$R#r`&% zHK_(iHr@_!asOW9x4HgPmKw+7wY-3C`G0#OkxVEhy0#SFW%;j@`d3EZ{l88#nUjM^ zj?3R)3q`OLNc-TP6qrU1WS3ul6$8&{{>wfMgA84A!|OF`H?vVjhJ`u ztH)6tlJ&gwS>v92x3!Dg1U(=X#_ zKY4F`>>3;D?LuO2<0D6}ehJhaw8zZF$M z3`$8OI5a~@N(l%^cbC)vgM^}V2n;!NGj!*`xzXqO&UxSS&7b1$&gc!9BSuRkI6Y-PN4p(*wBt8G6bELu%Uzn19^6o(BgF9|V_mHS0U&rSYrKm2KV zk(`My@72{63kN)bP5?d_xu}mBC#7=tP=HYoo@c_+>vhk?(pNH)STI)3!`t?9f_N(h zzang)Sf&^1L=2h^`UnO^0hhC>(FmbcO7yE>eH%3esrH*`DxBX~!fE}az~}2mQv5>M z%sCm85D24AFT;Lh#MC;NghpBm3*3ujB8q0S{T*GC1jNSM-_Vmk`(;%J;FYowQ&o@M zcSV1cBTlQlX0U;;DlstA+hnasX&MUIB2&NawrN~lob66DuhBiw8rgR2G<%4I^1`VD zYH^-Z5QIgC%gTbOrKH+T4YJsP&eA<=rnctV_t=i7s-3~Vc;7+x@zb`xgagX5V@HiL zbUj$%lGX0}X;U%kTj|A*IgvMC*(B|q?J1-lx&qf!y9 z9L2WVMBHC`7o4x;K7&Yoch;r2Op?IxQbZH0*=vG$$NfQ)~9gm#Ox6 z??`UN=u!iIne4?et`caBva$(?-G6(rMfpU}-7t}mH{-=-qfQy(U;Xsk2Y;e5J%?iO z&`X!1P1{dn*^bV%=0$9P8h|ex(-*v=LFjfYp_dUcAEr=dTtgF+d4&lG24urdrrbn~ z>|38J9pJG&e|}fc6YFVZiL{O9i*L1rfkE+5#H*n1*%hg{_Tr`94_%FCxX0(C0mW-W ztp&D^=!~{(MNmW3vx+a5?@`v^nmS*rA3&Vb6Btw1+rB)4E!S4ckBpVx4IiX}^Xhe+ z9N?*cw?FP^$4qE~E{PES`zpF`?}IA6+d@u1jFyzo(i}*E6eT0-x}PI=2F}+FjRnHk z?VLDe-%&)i_jw8hvYiR)_lVL?)^y{YG5aDK&NE-X}U^QE7^Nb-A ztU6K8L8+Mu30`2CN~`9S8nPrb(~k9ZEI^Z=r);%Gy9LAAgQuo=?!6tMy!d_ai{R(# z=no4Y-C(w*p8g4?i~8*A4~KUnm8h*~ayd2)jH50}9+mApleP05`yy?Ueg}3)Y2F9rt~&bBz;11h4BNJ}6)JoWJKpko!1OL$v)g3cTdGf0_!>EI$|;=X0; zfr&F^RlKGde);-16*Jp0ua$i6^p$k8D$pKGOm?ngfhVYsS!lnuIY-WPZF^A4B?*=s z2W&%WX2qlvQ)_+mV=AV9v$qsU9=E_Bv(zaS8ZC z(qNlUNNOsw7}0Xw=*svf<>rL{0lxu-g*Z|lP((TRaQ7tDvuF z{YZ<$E7xDSqc`E($4-kMkdq}dKN^4W&hIkyr_DIq4&iUvK&PoV7^j1F6II)>y}gjO zYik?jzRrvf=V@soO6m!-zFtb7h5l+2cAOUJIeQG3qtxa} zYM1FA|CLlOF%fa4hlkhqnHfw$YRcaizuXm`I*g9V7n`34%a($9LLy1xJ_Stmcg9YB zTQo4jPEL)nbVQ$;E)k%T`aXQh;fm+QFYp(*r0Cy*id)PyFgohr)%A2#T0m4(IzBow zQf0e|o!ZpY^!ejRTVVh?c}vfHU4Q&pZ)`rk9I?N#CcL*XMtycx!}WEu6w~9Vmv$X# z5Z(6Gxvu<9gJ!n(#sPS1#_xSrtE09t#z5L71<(br7l-R4z{>H#AQ^~(VkwqQqq>Gk zp(sDSaHT|WGY}shZK{uNF$+k$N-L+ZwG{bbmVR7)qjgRNm~+sa1W2Y6f0Dza;4QnU zAx)?e=R99h28?A)k@xm?E;ZF%eLQ>)%^@K%T-lb0`nR)QBtQn1on7pIF6IEHTX4`A zlwY4XeE2}g5ZlqEz>vReHtHn3{`cMj7N5_1VuDO#T?bP3@6LB9GNaiNU@nmzYORvz zQ(Oxxo0S;GL`TMZAV?Z!VJIse_^|TnDlCK8GRZ$0m~@0e^D(y3XGksijHA z!XoeDIxaPjiHYg^+#Ic3?mIK#Zu)J2tB|TU-`6y5g5c>@_{Om5wRQ^VIV~b6-S>N! zTC?+g=>yv{XV6pwXGW|!7xt<~fP8Ej*PRh(DX&Jr?oIQj7;N|TMG=wG1Y~8IfyqQr zR%Vrf;3Bps6Y18@o%VX}4Ua{&f9*`l^3Jc&yVf1e5FGR7(slTkDPa1D%$V^JQoG;X zPW$GAN%SZ_GHdoxY)HcV;a#Lu_M-60vm(N}W8uW&MSLQ9lzhfvLF z)Qg^uVSs0~?ZTo?r4pXNomV2j$9-u7-m6hEX>6Pd`*&~NyS4kIC9D7+bxqh-94lyr z&RPLY5l%p}^Nid;;nkqj8(Se+8eI5>~wf z{Yld)KKr>w>(2?WTzoK-M&dP5b@L0~ zVD0m5K#C8g3#-*x{jTR0C2$YE(+o8iOG+yoLaR9m7%LucKWf%-AhZ9WJ$g81^=ax=cfN6w6}|YQemOq_I4U z#`ol7&2{9r6p4VIBLlC%jZFtdRCN!9h|iJmg>G5Zjhsz6aOo>%8wQONP?WOq^{TLJ z+%iCqnWI8DFx?$%W_B%KDisMQ+XINkp3Jhz%>10H>{&+WfyW2iZaR{0(93_!Ur{Uo zqrjH{h@a~6*K*l(TZ8T9W;+0KuY&dcDXmcSkGO*Xe!K_QkiC7~tFsXq*yMd^#fsW&;` z>>>Qul*Nn*B)F6wfW1)CXA_Xy1T~Tl?T2HIHh8faZMs1v7l@x07isEzb1|=rO!Enn zHTp42Mt&D8kI>Q!hOrDTorSw%E|jaV3s({eIH9`K-2?x#aQ#C`#f7)jEl^*bT(<6w z&b9;oIc>@E<>KmBU597O+DAv_>L_q)lPBTID46kNc(hr&2xzinQ3*qIXR1^mSS72= zxn;m|5e4pIW<|p2|H+SNO#&KA`fZX6z4FePrkK4EROd^Nvpkrjp|u`Y;&J&8AggF4 zG~O!Ks06(z&VO0v_Gp4iP(K!O3!V3e!9X|q>G03`;&M#!t1~=+AM@0HSOpXc|HD@O zWdu(E!ev%opg!x)tp;w5)A)NcEln}e>G$@zy_qQQiKd&oLZt4+qy#5NLv_@CeF-pS z?+a~A(7kuNv)*}ENAui{(^u$BD>|6{kWRe+_3*EzIJ=^LFUDzpN(Y4n0)|YI1Ue2j z`SSQ2eeY_0BmQj1rkU&tSt{FAft-537Z4a!~8zj660O?vo4OOa*nU>9q_s2@);DWsX!G6dt~1kFn#13e*99n zRe`#Fc4_^;5B4w06H2Kbn*CxaFxgVVM}CbSAkCvB|9lltn2sgtjV~=Psj&*AY%42Pof5*2neO8<@qX zSUyre+2RUty;s6YtR*^5riiM{&MGOBnv0be@6Ug}`Ok7)2 z!MX4LEblb}+YJ&A zFbN0By@*aS&){Eo31i@5R0Z@16@XLyyPubDY0uc))PrnCyUTUy8DNXvZXVXM1j}K0 zJujqE5u0R15bVHbDe?67$mdZUXFzu*yF(EORPT6eTr@OvaJaZC^$e?efnfjV_m{)M z&d8v-d+$UVXmX%xw?AO`bZ*PpEIBymJ8rH^nKWyTbG^I+@Q!!!3FHk6n7XL7i<>q~ z&ae!ivGTwl$$`Vam##Z3vwJJ~#Z3&zn_c`5!~dx#iT~27?0Ik8Yp3f_X^e1c%lw@T z-_TLj-=rKHlAVkBKp*H%=~zotXbeQJ`Id)&cYMJC#5p;m@nVOOU}*HOv-Ks1v-70Z zoU)RR4FJay5`NazW)v2fJLv5lC)?QpY%u@DP307;xO6=uLx#xy%CdqmR+u&gcdA`y zi=1y~-j?R2L;|8fx-tCZd<`(^Q;1`INoNiBKtVx8gWgd>A`2J#;e+(Q#~sSN|0klRvB0Ti)lAZZ zpbvHoYi8y$9JaWDE32&J0%R`gI$ALYC5GB#eR=}E=K244-cjTL(+rUAN};gIVT%wL zz%5Jbtb~LFGXl;&?y7f1m%vW6hlzB)EMSC|-~ z7N4Wd`b5Dj?G~XhDadO@e>*;kh0V24V7)>?fNu!+<%l!gQs(PV;8FO5Z9O{=06PaX z)qajgJR*DNmh~G?Y2qjIyFt_WZr9|P^PgdzDxC(Yq>dOjumCn_Bj?Dzv?C4Dq6ycji{vlRD-sat$75<;al5d;JH zK5D9esxOAWeBY2_zT?}lOxU@Bk#ypW#bwU}Z^zGx{zVNx?|pvW$4!nDqW-V#wiGo? zh4x?JHkS8G*9jhAMz8q4mg2wcGG6fnHOF7l^1lhZx2*A0!wb?gV{Rd^0pO2+=$60R zgD*i)_EyGe1e{;!Kh6&rVnwn!*}&8pxd3FUIqCKJvQim%MgQaN^EQ)9`@4wazp;Uj z8+bND+8ux|5O7TQsgIA$5L8K8d3b)iFdMhvvGw!clkLs1oH*Kdyb~Hy3TQz7o_^@; zmU{f-*aa~Dwv`*NUCdaBBd-P(2ORtYmMb<%5~JH1Jz!6EJmr=e&fw>Q&fu~%e3+_)6d_~ zx(Oxol>n@2CGc1!QJySU*jQD%)Y39NsN6Ok;PR?lOZO-Bhn2p2+4~BU>zQtvcP$xz z5RQ|II;-6wc=Yoz_6fD4qwSo=z_u(twq7mxWZ^8~@@YkpRwZckQsVF904k)nw&q2` zub745FOpTd`nfa%998Tw78f59QzoHtIqm9^G8=}%9t4J5CdWRolCtn8C%ISA70zcn zzHT7FDruY73gw(7@zlUR^Ag2#IQD-56oSJqF{ONGK2h09z0Wdmc(`RJCl}N`tSjlg zy|fXcvJL(eV-Q@0K5ONm!dP8vUSQ+8cS=EZ7Gl?_dXM_GWA&@d)U}$%sGpkol>XbH z1LnsCYG%%sS%070QOTd3+xJWgXq!+c11{@4%eG&?WOYI)qAH<&r9iPjQV~&O>|En6 z^olDbcsH@XtsSr*nNOw#luX6xV^+ASb*|*|)^(D(J5(Q6tZDd;?}!}-!OJ)?}%e)T}-EsVU9S2GL4G@9VwiFSs9ZNsRkGfe=_86 zTKVJ7Pw4?Hg%9QL0`o&Ys_SZeScdccWP0*re0(=SHw+NR4|ksIPu<99m9Skk&+NHP zQeD*^fPZ2q&+n`Pf>o=z(e{VYj!<7x_`x$Xn855L`F|U z|5#|uZWB;ZXNnNkC#mU;);>UX;G~Y)O}?6XcIJ^nO!8LB@iXxpK;m8D8%?%Td1O&-`{6`l_>rVgKOKc0Kv-v(Q3w6zEB(UR}+r!R2< z?PR-jY?Y6(x9hg~|Jia?f41D}qd)Fx6`(rq4gMDRCO-#ue?5HuD0DN4uQd@#Ix|$&W@>4-y>1xwh|P$zMtxQYox{#IP063lC>WnF`cU z4uA*LM{B)`*b*AXvMas;rA#!(Xj`>NABfeXknkS3$#tz5cUr=!ces{ z{S9gk=&1miiFG}J)f8w_{cz6SNvI&dds0=a_364^^B7rC9z(#h)KP6yB6Moe$)0BX z67SX;+LTbkJvd)+F%QfJA-C}_?KY_secAjL1^~`J$>?XBfS6b5xctrl)HQ;TzgT_T z&C!U}s{Jj+xhOOUw!sST=U}(~2UYRi`}1J=XR|9TirQBy7KW-=jYtlr-B8ZJ?3ojd zP?$>=Fh8>v%-*rc)cf_V;vimv-Y5gZ)b)P3s$0?K&O-FlZJ*;F<9luq%51S|aXUnS zv=?{>Z0IFJ7WdSdUOK!2>u}s(ma}l-<@J0&I<}br6Vy;+1E#wSx{Kj~?c$A{>-|zn zO465*(PwluUM4h7_rNP2hk2UggAb7W`LLqxI6^S7q<;z)W}!j+<>Es=Fm0 zjSZM5Z8g1^qccT9Sz1wAf^LgtxPJ1uZb0c3s8Rr{kPNQt)vJ(wB}ojZj6xDX#vgwS zyZHF7VBe<^;(mTU^aap9LJWdK<#S+A^n@3O8oaH$7X=II2zH+)`xKZ)3d|sxe-?1L0iNc~ zvsXrtO1~6b$Ca1E6|ZF%MCx{+&|Or*O?VTa3C=k2@1JOBil#Ei${jSq)X$Sew_gQ^2l>`xJ9A7hPr)9IT*&13=6Tk zEtS=EjA8(tMUd<-5j}PNJHV5y9JaOwkm}WsgE7eK;GmG=^9H*^&jiUFAiZqk2m17d z5%AcITdqT;0ue+YUR-nht_YsCImGa_#2l!^Dt`*fKH#tJ(4>XOu}$^_o&aXhFalB7 zNu;?^HAe-0dKWJhh3v4t_dQYGlbXa#^D@fHv60`kCSr0p)BW`+Yg&T~?XCl&^7{B^ z-^agw*qlGJVFdr+rtb6(M);@^J1{*S@3yT5@ZLIaDNBl-9NTYIvOqaj&~|0IYr=Wcj7E<~}$$sEiRSyE^K-#cLyOHVZ$S zLb@0-p+a-5$q^+J0c8)iUP};SX}uGgPiro+%Kre>42);&Se$(^W&K3P=-+PhUw%+) z(qrZ+$4iC>fZXWuUB}Zc;j#n0d*0>`;=Y0roFYDFO@^nFYfbR#*n`Oc_4>N>>E2l{!jAng();Em8fx0IlVJfv7+4;I?QwG@mf1u}I z3?IFt6AX(hR5GJX{S=vU!EBGXX4(N%qD~dF+ULc~ITz3a5w=Pq9KuZP^2gCs!ckZe zi44y;+IMVctVMyjGeTJZ0heQQy-9$qQS9Pc({er9;JnXDgSx`WxH?hWnyw=RqGB|%N=!nhcHaW6IjmXV?Mt6g`>&dlAKd`vaJ!Q7yeh%Qce4y;!Y= z)1|@n9g}Qk*2n+f&D4C_EgfPVZ0AGX_9j;T zsEoOZP6zEsYZ|T=ImH#wnj1^aq?|_qhNWLu1-hd*e}sef`Y9;LQ8)ijmO{Gc>}kLN zBbOfa@iNr>mpirhzMXZ`WN?e0)F(*5iX4BgZ>SSJaXKqT-|Zn-@;- zUeiPL;c*|L3+#c#Hfs6Itse9^+M?_`aV+p4t#qo`epSlmC)_T$DnKHrlWvD-88cl1 zoLb#ZH&W#%6HPuOG$L07NiEl;01yMZ?>~fheaiw(?zsT7n4GuH3_@UB-vQD>&57^~ z>k{99bD0qBpxv^`SPDOWDEjM?V1{tThgiW7naf-^1Li>NL8Ijo?v~r zNaBT{Pxv4rGXV5S;G2&KHY&}K*AC!Sh6!JYlHZNG_w}-QxRvo}7awuV)o#yYYfe!s zAHOnWPf^`}*zIEhT55YKT~D6e()wqIdmTSUUf*mrTW1=NgA0Xe=V!-HA9}DqSZ^D+ zoe3zl?wIA8OMKTK{f@#%a(uH#3HHj2HX^#wx_RXmMhB6dyyJ=af6=wXdXKp_4qUKo+)NZaS4}Njo z1mnl3+LAvbOF;-P4=1p!)Zgs4_6+*%Q=X@`UgDfj3ADH`XLI^BEROGlMpMACm{lr| zyUxe0^Q)SfsbyF_%07jhAdl|yxl=zD{0675i%QrL-$_Q%JV9cLW3Nr+)v}A9^g=Xm z%1*n+4&!m%?j!V2Bjr5c0wZQ!n1x4`C>wrGjP2TZ`C8%fa;~)E+~Va$BI{g54qxuB z^h(D@`rE{Tg$~SsgYA#UZ)hXacjCH7IR)x>n#APzk5%gwI?dWeX<0Z=mOE-5^3;#4 zjfo56w?j_?F3l0m2bicAP@Uf2fx@9H?qBC z5HaEf>N_E#Qw?_+_`T(p+2?qKGzH|w(HMDS^{2gOJ1mwvRrQwhN3zc*M=A8|^(I**_xdY0rrj^I-1 zK66Q$N~REA9RDe@VRxWXpVq>|zA4bLGTmaCS%`f(%V6| z#I|R~I%l~WvC_$@w3)gePHH?s{b{Mw7!i1MU?H0Bw3?Ei#Nwm5B|iQ_&t@69(c2E& z!CS+AzxxxB(*Vjr$z^*$-#WCC%2J>_PKvpc5lH33%YYu}xcMPs5c<~B4%9qQ)mX^i zf1`+)9<13>>2oYVkRIDjHQX~=wB~l$@*R7`O$zz_qF*zELbz8<%*@`N_KQP8x}H_u zw`!F^cezVx`(9c1H%8js)e{q2nO_TQUy&k)cZrj|Ikw{M&B(Nl6t;27;Klop4^eFNLrrnqD8G!f}p1YZ|^WcR1zD{$!OT#k5jIs9jZ|w6F-aP zKx}qOM&E@`L`gw+s+cDsHa$^#D;JeXq`+s!w~s)Ex+NzVOPfX|@fz!*QxYgr9+zvRP2lWa`Od;hgStbu%UEBMI?DO3iL@9}cS19tiVZO^^i-|>>8E9pd9tZ^VIjfY zJcr|1=xUGyck4EbXefb!J=?3v5U!^YQw7_TY6FSPdIjFep7K5GWZosVY7m^^KkJ9~-eNui;D!#W3wA?Z3+@vD=Nm7(hSuJ~P_%nVx<|VA75uL5=X#9h~DobGwqodlhLlMK( z;b`O58QzL52WxYmR-uTsoOixOM|jyELK69dq5IVh3Y`)&!sxK4v{{@)?p^0^Hb1IHd9L)5)y$&o>;;QyU<^4)k?!IL;5f5lk?32a~YDE?;+bc>&_(KOqX{7LD|-)5U2DotHe zFN1BOw3jwfjcAO%fg^gA#I*>nS2i+R7rYN~N8u2bmg<(vgUBg=hl%pGu7vb&B2OqY z)3o)Y)DmfFN7wZ=$z&K;6oe(bf>t`^uIkxs{uS`9qhymIt=fX@mCLxfL}k%1z^yz@yLQL{m!zbI6j4}KP%^j{4&)=|4{hRqRa z_)!kNLc-8y_5&bbapDaHi{}m-sdb`DNYAzn6vdU?+!p10zMM0Qru`XrwS-B!Q8K&9 zV>qPVKd{Ec7Sr^T?Q9ALhku%(EPB`(yh9ow|27AIBSTw%Metdt5?tX zcd-*ceXRkC@%Q2lF4U=^EEQD0i`M$K3tw2DW+03TE2Gc4jO!sN$L9Te(4h=ehw+{) zTJ^;0X;3RIeqX-5utHzCcbS22f|XHzeH4!1gq(%&2Iu2cRHhU_u41dtn z!B?EGm22?H{7Zgi3n4JsAYlo?Rr7+6nZT*hiK9N_4UxU!_(!2O&l?Gx2GcTBjWw#> zUj@YMYp74hxH)dIy-lkitLppUONPhlpvCpnl}X3#EeCFQ$^*s9+U6>T6v)5-1nK5g!dloVLt*cu+VWCyt!0H zdGmz|KMwFu0Q+o)ko=azmzR~j5?|%lS)7{TD8eI47k{V3Uw^vuB}(o%J9`HOmnQ(9 zo9mcpFKyK99%Z~JvS(9G!b%YU|T=5igthEVmf~jG`g+tq*+75=aRwpxT~gV!^Yo99HV8( zd0JaGbg_8-sak~8WLV}O?#bH*{Av#1`-Ial=B8DSYugU?rxoNUr*y`#Pz?n6w9>+5 znZ(37B-Zk3GAlvy*_^T8ljaP5Pk%!*ySk^%&{Nko`3g>Nj{Y?q$vA`AMqsqZD@K3A zWfw_{=JA~$$=f*;pH9J{@R^H0T5xVDsV8VIEc!S$9ikdwJroAFS5HEX3C!diBQB6| zE(x_P{R!UEaXAuooT3QoHm(!GACANLK~<-wslE4*JZ_!pAB?}fD1;Pp4O@y_jD4$c z_`WZ-f%CYj&R%swt`$u@YCvV0_Hk(CBqq*eCCpKT>fL7BH3LQp>SxP>WT@1P`K;5~ z9lC)Fmsx$@0a`n3c)r*WbRr~*%&~t{`py7GqmCMg+U>HzaNLr4A~DPd2hA0GG6o%4 zGw_+4Y<@XSI26wEAlX&siHPq*>JmciX#=#!7(%1aeP+=@iBtJG)8F1`hfmb_?e6v+ zUsfre5?M!{)TA~e@n~(*8c7uw1T>RGGWRH<;8CRXU7B?>%Aqn`m2}$4&1uRok8L-o znNguIUYa?7s;qoX*?k6b>^+SMoJ@rsR z5Lz^sDAKn^(@wqu`T2oG74t!i3KxT6gB0T3t9CIWy2qKL}M)QZ&(8f}A_l2J}f}R(Va5#Ln{ziM15YC~i zj?0Jn2wejskCC=waQ7@M@MGev&}38ceX#UoSGYDUjf@QApw9)C#GUaldrTf?pvLDe zx>UkN-sgbY)w0tmE@*j2)i}ID*XQ%%;q1V%C$1ak9V+9$C~Bn$wwXY`^CrVzjgTmU zZ=9X$2}U(~H(Ihp^>HCfUUh*AJr}XN#&gWam_LKoMeDLB3f0X-a%_jfBcySYr41th zt?HZmaD!>eK7SZRvA1i-8XOEA zF@lmq;##K`5Qj<6$fRJ>F${f{AsI>B4@<+}AS)vL!i1a5FH`Yg1^;bcnu1~0b0UJU zUdRuwtGu*hEJqxKzeis}4Uq{gl1P!=y^|z}n;ov%E|sHZm=e5T)qL!(&91-}p`DORwe%?m1dmV;t^d1P zVFmuCbm-Rtf9-)gD{V?S&&w})(RBsQ z%O0NI9Sincs8laF99y4Y*d~+^XhWby(>uWyBV7Aj^L^5qDvR?nD)i}V)eo8NE%#4hBaUhoA7E{- zPpDn-?eeX&KfS#+#DY)stR=9>J)$d=54oJ4V&Nn}rpR1QfPSS=x&w>Va`j+Sb8n9& z&)r6zU;LQ2xWp-Tz4v$6@V3cx?9AYeq1S4?$a?=eSNt3u&Z;tH@9bnq&V2Yu zTXJyVBG?RL7WKu{RG+`WZ1PyFjf_@f-HWPkB^ImK=u+YRHuQ7*;U_yYxti_B%4O*zCRxV2hgpPIr1ASa z4CnVku!GQ`o^^i9|2h?TJ6{IN)#qnfZy3Y=aNqZMG z7R#eYle7I8KnYRZzjw)o3l5(pAbc)ID*S$KvHrmmtK^E#3gq<*F;R321A;ngFoS(;4{;SLUg1{x^GnZ88@1BN2dP9aAQ!HRZgY> z?G+(>h06I=drHXN>zChV2$2}^=u_|XBhY%vh==Nzg{`s#X=A@KX1OM`@NI7pO zMOay9$4NdIW1%b-#@(j;d2c?Mt@>n@(0ImntunN3pm?pd?32JaS^1x`+5&dTQ)I`J zOk+WPuc$N@)`Sf{edt$IQc`MpycGf;7f^WpI$J9sGE%jt?`pC00TKh&g^o+d?$Mym zICE5bMXx!2@8tsqa5yfO0=7r$9VfLIwwDX8VWz_P$Op1Yf^B$@G2mD^b;sjO*_>ZB zS~I8^zyJ{w@)SN=H1PoI$-;^xY#@c%Kdq=(!9;>HHV${|fin+HF&DmBZT*jF1p?5&>UPDUd~Cybgz} zreT@+PGB}ln9vq)x^B5Rnc173$oZb&NbpT`v=kWUzTVfsh?CDBH#WM;)@Yr1h6sZ* z>B#VCl4X%IfFpnf8w&HPm~O^FcnH#YXIx@Ie~-7QkEc@)@U_Ndtixetf@W!=VK*-{ zlc?$)GlO2}c+g~V+O^MeD#;+3b0*sbv($1j6`dXqXkA?^;}Y%QO>e#jF=_g)qec}O zt3mr3L(}q7bD`m36CpdqlNU!g!+yxXn6h+)dJ#oWcpMW```Jm&MKW1NKDt5F8Hdgp zg(i>s{RS-L38%gCcWJ+^Bd7eEWlzDw3-`3$V#=LQ-~_tnGQaGc)8{-ImM!8|gPa9au=(QU3vm_)!uwUt?tEZ^A5Db4t?DmFQL zflV&3L&V(F$s~x{*qkHpc1Kw=`{p?^6jro|wiNfQN%?|x59@|QsAAB^_B5YJ(6)W% z>Gw49xC>X6FwA;7^igIB<1L?_jd?x>X@BwM*tZ`qb5IJ5L^1YQ%X_2C%XpCwBKaV2Y`6oJ;1KC2)rq-Ld5+JGoe+`XeLT%OpTv)UY>GV~GaD0LX@w1@`9u8e+e$Hv$6IQr z<-(Z{sisP=TP!Ij#GH>#RP*AvT^ZZp}EQB5doQr{cutDvj zyYz6t*dO+pSB&u4gcE%95wYwyv%S)g)GtdbD<%Y zc`Ax4;i!tCs-3deco0UX7j*nyG&PE-f^&!#gi#dtaM99wn$aJ2rs4XsN%Y4Dx)UMd zvx!;3WfcB8%Wzt#cH8@_pA_3T;C*`Q6mM$ZBthW`oUO`FpcJt$I9w;WVxC!5Q7@Kg z0zi|9urwHaNdy4Zn4C0QXX|_L`p1D!30tf|4}N7}T*R!SK8dyq{jQvS_`tW*H~Cp+ zV8GppD0fI+F){0=2Rfx$gE~kXdVY=Lau5&t98y7FY`2?qF+lHo&yW=cl_}tAX0|rH zNrbx-z`TXHc-eFnybl`vSXeS|;zb}h4`si64A&N)RXgZ|CUj+d9Krxre^ja_Lm z%DunK3XC4>L$n=`DTWE@gV$1CYFk3BUhDR^yCQz$x;Lw$+rsOUrTQu72B6`)Dy+$_ zQhddkImCzGYt3)rVr5FdDT%h|$O_8hd^6zx5RC~W%gplZ6S~CRI}0mjXmn<*dv_G@ z?vfjBsoz!b_SN_upNlJ^el{iQtk@by698)~c!GAv7%j}0G6()xZ%zfYm3!{b;;(;E zK=p;!D~Ivkdy*{Ogblo$t~%vfiw7ScohumntL7fsEx!d~2FR6h41P8)N!u zgp|oVhyo6F#Y`MdJR*6QrEs%ce0-SVwiM0w63d54OZ(AfC1`N>#76Wgh5rpW`z&(wWPXM_h5pu__YM)3Iky-js)7JUJjX!h~a;pjUnk1+TEJ zLZUwM5#UIWPK597VS`vuRU}zm-+GO2@&xJB zN_NeaU=ND%F+5gEk0ySlRf#XxT5h44K3gi?o9OCAE!urjJ(A=!qcKIhR2Xb~Nq>xK zhiQxtt#Xr)>BvtDCEb!_@HnU1P8C{Ua;67kLD7%VI?%ec^g>5LXYnu0B5RNEDghnT zgz6*y;}2?RsIqtdurltpLh=zH`d*c4iv?k_U5!0m*lB%;%h2& zPUS~EE^0Z1Q)*>-15(&AunBB4P8d@7sv2CtaF&gugMK_yr+s6W?N^*>Q%T-K3cuM>WyfH?(-Rfxm!I zS24ZwJ(i7o`F?xF1ZWjrP8^?NqY5o{BU%%pP9Y4GyVv^sbC<>T%{KvqrKLmPwmxVI z9$zI{oT}aE8p^NMQKK+|DZM+E`!?s?9BghF1yRG9$_{ohi5i7p=Nnq`9Cjo8xFW{I zmvX@$`gcyVd1KD2=FVeRFVes5RX5S<%J^6RElMvr{uZTUvHrIR$Mu2V*RFD-x!-hk zbzod>mvgm)8BH-slRMLB+89ZXGjxxq(@C zpD($aSyb;nm;(*J8YgYQn0_4-v@V85Fcg8tJoSs-_$!_t`dEn_+tKF2(Zfn)U$dBxgn07Gh3%O;`ajEIMV}i=;_^&Y z;f@hYs(y7En3dUvaEBEpWwGPwDS6j!4CAHqB(5mns zSSrZo>T2y%&e*SIc*MEn&`~CPyReDGC0~d*V$atf78x=3vU4JNdh?3?#IE3!NFc`9 zDLXBc>-FwC4o}iYJOxwyP0f8?q2BlTHPl>@(R>?e26|Bqs%Hr;FOwGRS4^Uh)LwE7w{l1LY(f*%3j2LM0z!wjtoCb zh@#dUg+Hq+pGol}o6{mJG9CTRaBL!t`(}g7ljb9C{x{ewWK(B;g{wjjtDrQ^dI+}? z@X6e4JAGC=$`^jV!#KahsFgicuyIm+T|t?_RP_qf0Pu26Rq&-#+4ms5H6xTI)~VPyzV z^$@rIL@*)bS4n7vg<{M={HcM1)#>qkt=j@Oz9&J6OqYXJO`u)harSm-pU|YtqbFHW zWH!VJipzo;*)&frH^nU}oimGGwY1R23`Zg95IL7GH&Q1W_OFL8uAkdc*wKP&&L%Vv zIZ!KMw*8hR=t_<~bU!)i!@iVY&bV}*$F!TnZbfL(@)5*$uG`MQegtak{=*GI>RIHy zGfEy6$wIBt0l|k%%I7JP3l1&3b|6jF;= zYHsd#xzZ0fEFAwKRr%ii#hI6E%j|GmrTf;zyYlv9=k2q1gzgczc$TkQI_t{Eua69K z9Mobe3mr_F^Ya6iUw|qVNXhGKOgjneh%zJo2@EXP1h4<}6>HAKrCqh6jHi<0A`tO{ z5f^MM*!cxe&JP-^AMlYh+$#^^D=`8f#??)DZlTa4Ga1GOl}Rj#o)xy6f-7S0=Ef%e z#_{f6q-**rcDxkl${Bd1nRrubnVXB4u|#AX45M7Umd_L1Fyo86yx*}(OLcll6Oc|q zVtKHhdw8%_!*(#NY;EZV)$8e1-o38t#Q~)%8aI`VC!*?-Gs?z+*6X>!Y5o3ykHg>W z-u@@M_qgx-l|d5?Juj+c8zGkT)*61+4(vDC6)SL23SfmrgvDWqJ;_c$HyOb!IWw&f zB|GOB3$43Q7<(poG87Og^I80Ky2RdIRTu)yP(n`MB zAOjv7!3yOqgU&oMM5bO}Q=<`JqmU$NVejrfBHE(s);S(FOfbH9_)mxT#^Ux(P?Ol; zHX*dT856ZmE4s2OZjTO7@sz(@M-B)dyG+pJ+x^40w4SBf|9y8fXK66fVsE}b2e<`4 zf8Opn!)?@mHb>%u6g!;AnQo~5R8!Pi!w=EUx}|1BlKvk0pZ~uS@rS_r>xMLN0qk003z2A!gPyV+4P;ULS`=_A( z_aX{>V*hzQx`qtLbqSj8lsOOwRmraHu6dgd$bg5NS*zGbqq zvVQ`)`~vk1H{S1dT-iB+VgC+wgs0x$1_Iugm{5v4YFGi(60k-JViNvg#+~R5Ldl&{DFE! z8w1EL*%fciEZc1d-hjz}*X;R-_z0_(-3(9T6jQr0wdM}Ma&hDkUcrukd`^Rjd_Ll6 zsdOq%e7&Y;_U7APp^bekAhc=1Me1{HQG+((ih29WFDqhVRROuqF!%f2&hQA)#{I3J zK8MrBL%8v1PRo}?JYygT57qs408jHj6#<(l^p{sIZk8emyyaM7d<@5M0t_iFq1_h8XnjsjYFO=8nNYVDq@6qK^RDl|r z^q{u%L3JeqxvB%qQBL5e@#Gw9-*I-?%3FZ7x?opUkA(=|S% zwdxXvab|g}pVVhGit>!?Q_*UMR`a-n8BXq}II5cdzc84UHox`Z3KA{LkZpZ9O=8eA zo+z~RQLcnR^zpsdG46X1W2dt+J_As6%`8Zc)r+(#CKwYW0SPfhrXBl;A*tWu#-1K^oXs3lWDk=gJDg?IDIB!Ff)Q-e1~ zAK^aVUjR+TEA#YL^Whj{TMj=Ax|wlctoBgRzLbt)4@t*DO9}Iw{7w*yi>_t~t11Z0 zreJi-=OJ~G#Dpa78#BHa)=%8GsYH)~?d+~LB(Y;-=sBAQUK>?SG#;vap&h34w#h0 zo{ZWNMox-o$_w#53VS$G+v08O;Z8sKB|gUaFp<`l$swj7if;8S1TTwN#rU)bw_> z6KWGktXZN9`#o$_xS-Gp+Hs2%;ht-U;7JdITk@P8I`zAsfG^rCgXn&E4IFclxvMBM z4Y`q(5SkB#J8g>mrB^4U`Hbwv^Ut8 zf8n&obn>knoz9m^4SqW(kPAQj{Xkg3W|QS*?1yOw4Ql!dD*pv~rXQJ@#%~j!5N`Zx zea+d;lc&w&b(}7gQ+6iJiJkJV-{gWhi|EhVq#V~teVXOh$!jSB8sU!5N*8{6pk$8S zP?koKb^SFaUcNd^A0IfnE|EG44=?r5!Ye+^e#JBTdJ|qpo;UF6V7()c?0i(%vG~9$ ztU@3Dx|GpGg#pDi?E&pXFqE4eBr;7U2UXBUH-8_OjmG|muXHW%#%pb!ycw)@J9dr9 z@N5$~!#FnLLMP|I%jLUj;{!84OqV@RFkdIbA}t(qpbHTSj`{%aHN10YAH4DiI(n2z zr0cDgS#S2WWqfc`tc5<>=eA=p!EY%y=*H$P&3fVf%=z3`xc%UAiekrgy?SxiV^Ra? zDnwDqpy3Kg89WPi@LxvcBP&$NK8+F*k9m?Ik&xtpVqMYmpG=|`v z&489vy^F!wGmkeG4Qm3mD&lob!#z zalZhhXWJ`M%uFy0X?ge8@bBNOgoK?`<@|_p6;E~e6!YnY!pT)9!Wg?fMFL;YoSN!E(zvaDPJ)qKzf(Oi24DJw1B zYbecuw4aAPcXOpah{wKakB%k8AR7|(6S~8~11AM=g3wu=aH5%V-yg?bc4eHj6rZNY zZ$>Qb&N-B1cw%8@6Z&g)LT{1qtrBeD{rb`cF`vu>#0%`_ENAoSoBE$C9VZcJt?BJQzm zV0u}M*llk}4xF|AeQcw$kq*!~LJS|vi;d_zmhT4%uQS?@+8YVHuwxPvAy+5%ld|bB zb+@zx`~8B;FC+J{p4j$d3sjKnQ-Fc+%U)^99**qw)vm1w$=Py)40V40a@dO$haW{9 zyIxp8_}2D&qpIlrHAAZU&N|fGF_JmOxgyif1GVPjP|K(_xivBD!1e*jFYnoJBHy1= zbfBTrPtgl&JN+M61_Gu?*5di<-bh<@a4gBgR*6;t@tPwXEVZ&?pz|=%y}$B&fZXcs z4+5Q@uu8u7e`l5dRH!TZjm0>$Tm%Rm+;MF5=l+<}wlFLv#A#ISq-zpyfBLl%et)cM z`q+Mx6Sj5ULT<}sXNdgRgtT_{8@pV?FX#2EIek-C<0UJbBzZRx{Cswab90Fw0|@6A zEV{yxv=J^4!h*@6U!3dH&1!zb!$a=O_W4boZ=Vhq9JZbeRrsoz1S0F$nVkwNDjs(`%RO)NamU~OgP&C;8jZ@;@MWM zlB{owpQD*IuHBHKpQPX9hBpeL0ND;CW7j?bPq3(Y2a}TSfH!>32=nI>%-0Oaa#g`1S^|-UQDBe#f_}p zc~MO1TD1)avT6iMxfEpri}@4rZ_$U~e;XmC zf3PpGSTtYgop0=3$V2y2f6L(nBu-IFx7-7&EaL@tYMg!5q1(N)Jp%qg=cY5zL&4#R zDa6vYNMzZ1C9;+sf}21rCZo=M9Cybc7e-#o@dC=OJ__=lwMehG<3U9Ga$%%0%dqyyWS z$|?4VVM;R7$*(jH6DiSzjD(;-M_)O5jq9LrdG(GT5Jgz8?A-nfp^``zR*LtL48B(M z^<85)5)v+fVdJVQ`exz@SO)Bc^vKDtyTtIWXk-ek?VCTFitH1D7r$~Hm9jx^@%i_k zQs5E4CbQ)u3qh=uzE(gH{T;f-BU_i!PtJM^Jqx6W6v^C13?&RLi>^Mj>4vy@o| z4W73ZR(2~az=xywhtcJ2a``QMLNznkCSGx3(MP?tr57Jh_;f0lo)zs z{L_$a69u-{<~0t3XW=l>9ImkU1$+w>*|fs~8cYunLnBTQd{JtTQ+G2hvNMkPJ5P;M z#?_C4fQ>*4?9iOvmDrjEDTTI>B>0>E;*j6|C%iJUXabb6&(xj9n)S?k`tu?SXg?sT zl)xuVm@p1&m@JXAE=q9HFkZ;Y+>@|hXx86ZN-+#v9KznPHFfQ$U-;}e$(|}*z#Jz` zfMU)I(ZV&79DPYDwz^7^l=~^xQ5=YQyglF|z|8BPNbD}AlrxZD`wApO_4%mpoZX|c zd%)}E;*;z_i5ZYONXqa&?@db!xM7gB@qLCQZSY_%eKvVCmT=q5n z&{MM|quJx^18OEV?m(m`^ZCNm&z}#i!5(=P6|0sc?s&;*^g?(op!+^x>o^TCnqcQg zw$sCKdF|^zDT0tgh=5()b0FtfBj|>CWRGjDR}q%Q_&PcgbnpBb{u(|eI22M&Fx5sf z<@OmdpzK#Q7#Qx@s+L0w@F$SzV%ItQKjVrUa}Uv^AX}$Gh^TyuxCW`Y%J&D-#J8`F z_fTPip)>*nm=3lKdhM@*vY0zR!W<*Y%%(i;>4K21MpF3%a3bp|%8zk$cwKTQw?>0( z%;tE1-g?`UhBt_I<|{t?hDWA=gOg&OFrE3F^n5*h#*#IS2rRe#!=eFxtiDWbK95<* zRVwPOZQRbr6-&^K@n~J=BrN7CYMdJ#QmFHuqQr;) zfi^K_;4fY9$x;7`7xa7CDlF+P*P!>;>k>LQ8eGF(jk%uGLkf{n`XSmHhm%F}?z>yC z-5;s_gqZ7@@0ZoB{uHaL030pre%sOmi4n!=U4i0hD&3mlpOU=k@RL-bgUG_?CX9>^ z7@7Yz<*rg=(lh+A)3`>N?CrA}b!+MAtt}Sq%l7tVyDk5!_qNn{j0n zx}u|HmClH87Z~7kj}O+bn&yqL`rNzPmG4~)Smc||Ou6A%{8=Q-1XmmiYNO0-i98mm z29wG84dRy#@`fd0*rZLEtu3+#r_xB!ON5pQyCZI_TQH<_b*+?(p?499!?m9eeep3Z zQkpn27zP035$190%N%MjJkt%jXG_4}B8t#Nm0DRmtB5wA0lHZzmB=5XzyJSbQYcpGPL{BzdG|t5-+5_^Rdl@DYHt^l&{#2<`dXT< zD18*BXM@O}I)FHLZV~D;LUW_xmA17z4S=3nDkULk~(5$^b%;Wsv8^3$^+G+?q= zs;E95t-Bb`+GvhB|K@U`Y5Hye1s6~x0a$R7YugmTzg!L`>wyxmr$aDaN(&sTO?_LD zA%_Vs&=*HjM4)l&YcN8L-~?otV0+%v_B_i8e;8jb;M0=s;?s4VP*fSQT2$SvJnHM+hk=Nljr+?}* zbbNoD{OpzLc6ggp-J1vnS#~q~PUkfumz07fw7ZCwnsUf;Ii06=$*APFaao5~BmAAXsB4d?+bP2hWud3=ky?_b8_pu@5^wH8+o1O}`73lrD7+vu@4UlfychZ@Z}b$+)k z1G_Ch1{cci2^{cw7ilZk8itthpwpW`3frl*3Do^jA`cNay3c}+Wn zBcc!8HPYJ?$TjrMVwN2Yq^h$0>&fuR4`q(=-6@!_r95YRVXit`9m3p7)jHCx@|+k3 zD+WDkeFzV^7v{8U=0#MA?*V!ME7+>>*%{i{O5D4FCXCvu^$B|QLkdT*Krzun1LI(l z)8h>!SuDvsFf;O8l78YcF3+|Dm@V}CY16xY(Jm8q;n55cEy2oTq&|2rf|l5oWx4Nu z6Rn#T$X{YN^f;-YpoJ`D^KYqVZ@hua4?F?S-HUBK!LOjv$n(#7XgLgNrHzp_9eDkT zm^0@(P}Np5YJP6zZDl*Q8!6Xsnl{{k5DD$eb>JKeXFCi9VqpPBd@>JdGRU7$cL8u0J2yXmC;@^U1&{rXI+PNdZa_5io=93-}&rS?E*D}(5oZ`46q?KXbRN5?g$IVJ(7 z1w6u2N@;STjyD5TQV|7a<}DLh{2s2sPuccJ6W*v+2uPb`ot+3u{UAEf?l>oh5-L{e z+o&IZFd~Of1~he?2PjZtT%2HO#O`;Ytgq6cq9K-r;_i~oW^`xJ(DCa>kXxAR?;xN% zzrcr}lQ}#EH%N`!*cV7e=4s!SmI5n-V%q_)2K8wQn=OY;C+%EDtds0FyNZjXXEcD> z0sZ)Z2km~ybik_NDGW@PMd8i>Psa`JIE&xxo}s=lJyH9tj<4V9VC9(0JAKq7zfz9E zC08DdlIQuIeSDDXW|EnJP0%ekTQ(c@ zP~-PqWqOU7@4tp-KRHj4C)*va?LUNKpy$Spb1PjP!qyAzsk@K^RN1#aorm&@eHm<*0|=@Vtr~3Pfo)2e$7}o8f9595!uFiL{jbRhcFx~5lKR0G*}TpDpQ=6XOR4D`gWNUcJV^_M>6Z8j5Y)G;x{kK(2xWLr#@^6sA^sQW94R_mh| zOb?GBzN(p`1f^XhB(Uho@?95?I49O4_{rY-ie9#6xGWFR=?AkWRVi{F&De2Ovp&hx znYIyj{FGXB?(D6sf))YQHEI-Rj)+U-`6?I#nO3pNN17&B8woCdPs5zQ^@ynwZV@=R zyY7x);z0{P(4sS{hsHRbQ842t+8AUyCv6wSzoGZJF?GkPl*fnk>n-VJ-jBa46NdDD zg}$z8951BYwm1AUNN=B3pB(H;H5w!wei|t}=DK*mdm1Ih)hklFAyCtg>WgSDIW3#6 zn;on5c23`@o2nm-9}Nnj*c*IGds;jWX$_n4ciE@+xVfW?F#kNAPi~JE;5=Le_(d8e7MMT(Yv{HmR~~bVUauLBvHs^Lhuu}qu;`R zv5vZcoNSbnS5GqDzp+p{Uv|ETKbuKx-X8U# ztDv83+8;O-YcRu2)}-z_&iic^`UW5aMmrI+6VgUDxfmO$`q7p0M>=hk;h{euZ-P1KvlBu&P%cQ7*=o+BZ2coJdy2S3~OU!zZ^sV7cmJ2jf&mx8}*zt9n@N3X2;!Q0Go0?IbT2G8%H6` zSgkBf;uhHb)sNQHmi1mlEw!zE)fvmv`1N=#`9QkY@+(+Zo;&43;Hdy_UrOC9Nz1H| z6}J69PNGxhOuY5K+q7?yyDoQAj1noPCkc$G-z|8Xv&x_r5nd#rZ1ghQK zx);})R@UX7kjgW9j<=+%mI zd5ivsv=E{l?TZuX^{E$&dv}2uGXxpUeo2Lvm^H(oTkBLOKHcGU(4ZXi^F`HQbyedt+KP8Wrk4Kfjy4dZd8+Blha*)pkr+3OANq>yovT zPwL{e!8Y%GwK%M)FA?XFX~_GB_Wd&)Ri-)scajptisnN+Oq}dcCQROba*o&f=aU(c z_v*g*$2el4s{M59S5c~&!Y&8J?`$0qqUbk0;ly{v@|@iLfE75o@c4F@&&$TZ??)jM z?-kn0A0gU&gydfwI`D^t9+M8#br&my>^ofBDz`tS7>^JC6i%EuO?V<`>X8!X7#@Dd zkJ1V~VYWszdwpAc>KTXsIhbCtWd%7XvnJ0qO19XmoF1D5$hayY4 zOyTjG9%qaQR{?i)7C*?|yICJd-6;I|gw&AAXKNg-8PAxHDc9FY?*7}YR_ypCZa$|E zN`7ZQz0uAJ@ZT%jl7zhqL;4i^`nAl z43c;PYN4!VLaVl+>uWY^$j8PhPRY@6Dy8F;(NcU5a2_K|wqk*ln#)ibv;o?6th1{i zAXRVmzFpJG!gpOmJ#N5E-vtEB7+qYOb89KHEkX<4L0k-ddHIPrO6B?3*N9E1*7YO- z(Nms1?>Eyg@LplXPpsyD?>@O$EPVa9&r-`^EQNl2iMPIaO0n-+4=jIVej;&ALS>`I zG}-5bx7-~Xdix=S$PWVUGdKTs)r5`kiyr@tDK%k=2f`Dmf;)0mZT07!^ehV-^ZW~h zy)w^zI?}B+YYd9NaCySRvih8;$BLN0e zX(g{$+p;p6gdVJ9`T@Z)?m%!%0jsxQXUFK@nz_^W3^h~x@BZ)Lm}fo6vD{tp*4o9R z@}@GB>7j?KXW3@&h*fkH!mYb!feo7R-9V8Za>Qpeds;eSv9tTM9HQ+PWIrP?y_;Tn zTnz8ofS(!anc;#HboC0yW))=rj@It?TpENE_0MGhBLg@6CQepO;uO%4$X9dgziP1T z46H!echy^XNXBN8FYT2l>L$0DD$*x9{yo2L7gy681Jyee(gfH;18?GklwFaLj?RPj z+`E;&Z%o9H84JM*OKTlbLLzr`Q|M_Uz7E*FU|tmH;M z*!*r2dz-S+Le|No$_{1Qjq>=0b*V@#N35sc-l)=ZBlkNy>n#$weqi@eJ6U!Rn(2!| z-f-j(_c?Z4BJwe9W6qZs0pnY*8cQW5%iq?!)+_abIwzPdMJxzf6W&z>2wk^r{fSef zd>6uF&Dh7crz+&&r&3eh=%IUsyG-iCV+E;ij7d)B!Jx#fn9Y>b&nzs(VR}-d7allwsGFFNB%IPvAcMXOh8$O1 zbPwMJW9M`0wfzJ+hkgGVfAZUNwqzwX{!?)`_UpLb)Jt}r=t!FcH+~I$+`NOMr^o`m ziT;5%>-_PfsShtZ8#1AyK7PiB{5Y+;iL$Zz8CWSqGTSG}-9r6Ar@)Tp=y>_5)Z@ax z<7FFXUpY!qwc-wl#tI(0-wpH`v4BsJ+#p=^m+-K}qp~qOL$RwC#NQ5g+9~LAZ5F@z z`_~Ix2L7iZTAEYLoseyXVX4{&jqr;^p+j}<>d3k<;w_(pQBSL#GZm*nNFH-?=yYPb znY+*4+lT-8TDo5&A@j4r|7}&iOnGTe;*#>s>Pfc~|Kw})T&DVTMoIDp5fZxW?9r}b zna;+anMmJhbvmw~nDBAe8+D54o&0sTa(B2I4c6z1p*L_y&P*<}qHe&#gvd8OS-mcP zjv(L&NCTotR#$%P5-6D~o7sE-OE583HSgk-nCVL<2?s1#%wK`(-PB#{jmWW%>ti zxwyBza=Tk;!mW+7H#*wJjklO2C${2fYjKH1)X)998G!`y05q{q{d?TWcRBB^PZ7&R zl8tJs#dWm%dO!q^deoOq{--GZ46M2gkiI$K?1LC6GOPW(3&k7e-s0b3PJeRp#%Wv? z8NBuJQ^R9{9ODY??|xc%8#Bc_F`-$0pA9-c?T!eZP1_Z`7G<6*kh%A{|mP($Yd#uBGnMPJ@jR*;x2>B!AXjYofP>%O+&Lt36@yL z%0Gr~QKKk*i|4Ds;mNNBy%!}D^d9PuS6?&7+8{<~Z6Q~H4kKUnfZ_F?T_1y+3 zBUN7)CU;w(<34ZkxNnL3xIjIne?8UEGtlKPMBK9XVT`%G`ocRZ!t1+ELTKK!_XWYK z(PU!ILZ{G(%*x&u2fyVNt*J>qD&hU%ClV{8^DMMtrzsg;>*<$zTTE@XmwAuE1+sR?fM_c3p7=~{m(_6)l%rINbi)dqCOSNqNN8wFn zvh{Zuke;r3x#9#cUc{ai5a&{PzV=c6C4Q3-U0s)gD}FJ^NS5VJ<_(PnLhVi_ezX)` z2hd&iaW$M2HhG?Xi@&=22Y7|*X_DxayqX7hAG&MghD|RAi1SgL4hUxNFy-x}l{7%k z9cuX^i)$`I^iHPIY@QfMc8BU@BGMG+E%`S3M(*vUCeDKPS4e-v(MAyM^!QV#q(kG9j>q4QBoxanTx<;g!B5Z zwGKr-rxby5CI# z-XumQ7V%Yp#pt7ADmA4~T$NqqtBs4N1%;CrlPi9nUi&p0_It+1k)HvRbX23xB`k8@ z^4?yJEpMCN6}dOsBr|=9VPI1lu1$53+)5xHTKaZTDxCE@aO3U09yc<<^k!nm66{@T z7K)`8)6@C|?qw;|mK(?_yew;kAT{Hyzu^a;n1+bxO1UUzwwq&BIU&b#2FUG(YtsA) z$DA|q+JpWRWt>09Q5g|MQ-JB(i3F3g6(@AjR<&>m?Un)^B`s5vLcdOICza_}c}SwW z>Sc%NwaX^^I*;i;0k?Z^=&Du#N{x?Kj5)z~rjjohJJA{>hkbfuWrM3}!5e@=$U!po zFclf`<`zl_EnrV!eB@wj;iZ6OFy8@lNq(Ia0VcR|=x&%)k8+$N{?;`PeA?%-Y(ufI zrUz(0uIuh*O3$XVe|SVD5hCx2O@T}SViquX5-r^y5iT$JX`_}UY#w_aHcV)0No3|s z#4l2B}=KlVh&ggY0c9WWFu1Rg|hHeyi39S2e2TI z{6I;qlnye`pZr2o4b+ZsR!L9OL;dleZVPLOtZ}l&JwGAD5UApfmeLM&jkmVs`I^_N ziog#Asz^?6qPF5)ax&E+NtLPr!nHMdg%;+{w;k^X<#^xAfV^-0b|2PTJyhf2QL#xw z;vaVsVB?lEC`uxh{EBHp%`Vuq?hQaRpM1?{p)g>arhi8vIaTMTRx5?m*8=Q6R5kl) zP8M&EHNk_H#onGb+;bQ9_#L_jfK8A$wI}6h2X>8f&e_&0h}ZZc0>~sSAPZ{R_Ws|* z5iS4U`os%J?Mh6-Xu_ERfzLWIM!LHB6v*BZ?C=zL>v05-d-7IE)?NwU*LH8AKlK5c zbia#&E(Dp)OQ)LJM?n{W1jz(NZqS+K4^m;`w3Ba`Gqc@BKQ|%&^CZjZl`+ECq!t``RC$Ob<6aKK<- zt6I>C6n>!1J#BdrS8ogo0%zXCWK1-jexjnL%mwn1buecvD z=#|Q_u!!%zZ}o=HB<(}C{c>q_B!ACn;f0~Lfb2J0ehu)Hu%n7{NH^8Q@fqT^8`d_c zT6UvgYa}l+SI@d2%XEB#>uG>`FpR zoDtP~w3yJ>CjjNm=Fd`LQ^j&q4&Z%1n)G}Egd+Tv72?oHF47?XspWUAh@^{~ch3>% zTASoX5|41(_X@k*DI$@z$Cjwb0Jl?lze>hLT3<-3ZX9J!{F072tz6I+-!x;itLt`g z?|l&bG8N&D;tJlY*tVx23*fEng&^Lu=z)A$)VBx1wbLd8u;bL*=8?&> zg_*33`bQ5tO_=`BY3NUlB$SlNe#H+7_>(}OS^+1bIifv<{Q*{+uT$`k6}}_>yAw)W zeEYT!6}>1_&ax@eVLD-I<63^a}%VBF&CesoRZ1o zw~C;0pf}ps5qQ88CW{e6Y0CQ#DNfIUPn5VV?w)hJqMHh6Kf3$KXL}SVSF-qVF|2HB zUho+UJs^)Axu}IDhU8azXnLV2ZZ_&6e2x+ zEsx!s;yuX&f|MZRd}(19jH$Q%vtJ;PloDH3Y?*s;mtHH405`!|Y`YNg|$tJ zYeBi8BfSeIjx(_uUp8vkY~^ikn;feIUNDdsarR^EjH-{lV~NxCkKD_qM^PEbJeilM z$FZNG&$8b(%gFhX$Pzswl8$6jBF%;!R8Z6e+V3Gm-Dgd7XnMs!BLUNBJkvXlpZZG~ zJP7o;$TgB(a%Awn$eQlv2zKB(5EZE?UpE(Py@z_Z=3%dwx;1s?gp#`PQ=+CEYN!Uh zod+k$H_%@>Y0}+pmwF}df}x!kcyU^4<-kdiL^f7ZO~1)bx!MKt!d|*S*^#NZjwTl` zO*&{UX>HY;Dx>C;_~bNOwP{N^q;1RzObGPdhEV1Q(x`)-_8{$|nmsws&=C69! z=Xj{W%r^JQ_^T`pzHERGh)0T?&*R}WotJw0*;|w}w|0}-#*2Mud9Ng4e$V5N-*RYk9qIMC%Ubuk|P?W9&1+VUkhAqINE4pXtmI)sb z_1(Fd@j^2;^wITYin14K#uz=Yj$Mo42?rIS2OA)Zz9IspwQP%SrPx^b_KXvUH)1JG z=Lh!-J0-un@7~f#7ABmjy0_Qf+p36oMoW=5er@fw#9BuPUdESM`E*L@d9cg9cMw$Ns^=U*^ z>+_h`EoSy2|(X6+BgJC*Z5Cq-p!o&L5(XSi)X0Q=qG>yqq*aF@dQ8sD}hnM z3A9aKW^XS0KoZy0FB13oGhLU8Gn!S@FT&Q&S7((^$8QFGH)g|lE+ry*fQCwHwIg6C z()HoCPd|HVux$CiW*F*U-xbL5X>8=Yc;hpcwP7(&u4mxYFbPc2Lyd-)#bVgjPMn>HK2e}48byq8YR94VzpC4G*rtQ@XBKh7*t%wc(c8lxw( z?&g?EK+(EJfx#CD3tmsR(@ps@RAG>1ltK-4VfI=Th_wRjfJ z0JF$>{ajW+#0JiS`&dOJ9;QZiZhfnN7t>P_BFu~t8BfMvl1gJJiXN;;Zy6qaaeC+& zHeWFN^!=isV>=X2KIHs347JJxm&YlH@0{QZ29yzhvs~&6don80#|~(R+T&PRhC}s6 z;U-}OIaB>-3;I^!)#O(O$VWw9vF}C<@MNO zMA6-*SDh2|WGpupnOGC4H9G{~9#&dYa>kx2NB-yL1vHAHt4^3?m)M<4uOBI9`N6pj z;g8;pnw^^%{IctB45{=eVu_0$unH%XME@tPP4whQ^G}#HImSk}MIeF>Qj3^QqLK>t z8=MV18s{As)^j~LzNJqtK;p-eie6={L8TvyA%60kT0`jLL^|`9M2~`Ysw;B+IM=ta z@c#0m!OC)jzi{Ybx_{cH<@xlA_nSsu9OJ7gC@X8NPm=*#p;FK{Q?sWlEU&Ah9cuCP z71Z4Rs{E~#Ta|Z@!4VzvH85H@p}U7Kp0Vx-8|{^#)E3JH$Weu3mTPT&!b3P(N9}r;UVy|Bpo9rgs;i4l_ zD{#X*a5?Tlaru8Y<@8RirdxB@;z>x{WOAq1@5;ZAa{8aH!Qg(?PbaUug_&Uoyt@K` z4f+Opj|9(f=|;ngg4}{5(?P1gS9r_iMO^;fRc@OCK4!!4B#15B_x~&(T`u|E|1%{2 zYu+#YFZ2y)UB>;Ng(ILCe2{fz$i8|)|EIS7GY0Q5_hxOF#61HH9{&%?s)y-c^5~-< zQ@RGWw&%|QXYt$rAAhs1jt(!Ms<6xP?`jF8cw$wN|1vrTjUKgn)Z-f?2YA;RTN4~^tQP&4*5EvlK^$FFv7 z@61HsrQW4CwgJ?fko=|`p_?-9j+^aGo3ah#dcQQd5`lvmzS5ihB44#Xp*3BLiH{~z zGVrohI}Cp7uXHZ)nyij}y{dKJWbuS=cvcW|KkKLbrfyKshO32qCT_8F&ryrJrwBL@ z>GDRnL9Vb>2ExR|Bt<{ReDrj1k(tpy_xdsij=cVEY~ml+rh#U(W*`!lkh*W$@YY@} z+lr2c8!Gz+%ad1J#UZIYVuC2*kFU_qBn}J@e{w3Rh<08rPyF-4gNyIf=CId7>%w8L zXPJ{T?*-oMw^Lj5^8S&{*y5_`C*GCXJta^6-1E~J)9pG4*LK15ymdhUM+1!7(Ygbn zFaJcBRX}uc6r3INeYMIfZx?{W0m_FfwI<$s$h@gL z$@}^UZ$87>o{lHKB9|lgT!G)(h_ANBx{g#s`51^RuXKC&$Sd;rt9pAH^re}rln6Y1 z7-vR#<^kJ-Pzx_@WVm$=2jQi<)uQX(LKTS(ISXtX|{NE<@Zo4oqWCqRul~`9bwSSR%(DiZ`CR=~5jaC=>uZZ=P?<|J7Df zc~_5}FZBhrqtln61Y+tsPQY=muTI98sCYTrV~#9!O-=c87vitBMxTw7$~R^rybraG zue%HH#CHuT3K_#9eEB6zAFLCQ5_^8J>y7_;=d0G+QpMn8vBkB>$Ar;Agi>*2cxD4A zDNaX%eM9olyEr>f8n9h>-M5STfXDow@0s#`A?{BWJAbBnfO9%&?Mcyu?5yV09aqJZ z2irbPe*YZ;Futk=zMw4HY2h8-zVJVoIc+Xm6GIO$xuQ4^6t6Ms!LIpiHtWYy`cUZ> zP)8PY<*rVD-j2w8H`#pbqSEpps*yo_%*$mHRo#Lsk0iw>VwO{VK$|d1zDY2j;pbs+ z3QO~9_^KA8ai87{8@H4mSb5x&V(jOsXJw7lR6&qaNFRHL1AzF|WiAs!9e(k%!67w|a!ZsGyQS z?@d|6$eMcdEOFCgVW#*Vzhm9k`Q;zC$Y8XgX<>GzjLp7Kt7+qYht!}`ZJP)j8O=ds4LGvPE(^vfH3 zu#B{gXX0A%aZBI}Wng2U`og%GO@HQGeByl3EU&!A=#;;n!;{R4`nAnEt?1b{s&~E& z@%qn_fYtU0!piMXm#W`7wkOzv2KXkwb57rvS8hR*f9rx=={QIqZWN>cITa#U4N+L4 zeu4RX{o|26<>0ib;^R};_>9=wDasY%JO~`}5!fS8 zd1j|&O8g6~6}Mn$_Ny6D^yTNfFY$@onO(!vNJ;+vG9^HPjk^aH`2Oy3_sd($8b)S( znQW0rsgCeZ0?S=ZFNtWsWjl{f{!AOj#@RlJ#p*1|*$1$~0bljvBvDnIlhRU{&?}|c zK8Bl#oD=-t^V)x4G2()RT6InHaY7CC)d!>YzDLTwDb&Le+XtI!tM^w?{Xa7NZaTiC zJ{=pK&2n_D5)}1ZYM|(UWVN1w@Z4Q~??nI6_s}MOgJD=qz$vwP-Q%6rpx<$Ttll+N z*<){9+eiRDUf2CF!Rl6_=$RRObGtshe)`dP;! zy%CKKcRUZYHr=VsUb#!ueY(4H&zxz6{P$+0(SkSR=k@IAc_~Qv*!9<^H86%9M7C=TuCAy4dBD+fwUE<>TaFT& z%O2TtX-(Ma<8L*ER{fFg-Thpq%S)~mr~Qv^wLiMGplPl(_9$W(w2@m|YiOr^h_@a; zvXX!HL1{Yv#)U?i;~G$-g(oB?He4L@$8Q(YrlzI(&*$+EUkmVFI@T2R_SCDIi;*I7 zR#)+J)mkEbm3SCJ>&rz5R|HSx;T_tE1yq>khpYW664L`3nN+(%74Sc|S2 z(mv+tp8lB5LODFnqkk>3`=?A}RKxa8%Gz{l9F}RI`@1&~4No*r%F=!G`rvWrG?=5R zjTYF#;34o8aLMkL{f);K5v*F^XO7FtR4v|q1sBjUII*)Qbp3~B>f}cEo02Hg4Y$L-lu@%K z+)v+h;|p`W!vvqztxVh08L2>Y|M$ciLF?u*hCYI!Pobj?l6O<5+}=2eo|e5DnofSk zdq?nz+kVwtIlD^g@ix>6HP|V6SHGNXBZE^a)HpGxnA^b5=_P`5BlN|S1(MNs_@P=) z-aY@_q1U*_IQN;x3@i27h{K~vVXdl``%oNK`@8VBieTf91+S%Jnd094E{hG$4bN9S zC#fnMHQ5qGho@On&mrm;;;>U~Lhs+!)PhpGf0*o{VCJl5^x?$!KDf)RAIm90wYnpf z=Q9@xtA(?N6AffNY-@hU;(QG-S>UviKIPe$6LP8b2T7&Pr?a{@;0Hd}xu%4hS*GB) zwjYSmAB!JiG8+J1 z1INOC7&utzp$^YCkDd@{BK%peFJGRn*ES{@Xon`LiEFRyNHHFFFE$aED&5~dI*Tek zVD|%g<=JG)pu-I&3l0hLA2*Tt3^O2XAD5(Dnm#)Gg+dUAKRP;Wms=WivE7BH4|lX5 z6wnTKOk~Tx`uRd^kj!Ui&@P>Q2sT`|#bEwUvKwk;vSz>pEe&{@CFL@b@!wSHGTg4T zYhfhIu5F!bms7gWVP62_hdZ@{I?q1ke79s8G@Fwu-5BGbu)mUNr*BuW^RD-=nTJn? z1Frh1iCqoUx(tJA*ToC*+c&uevHyMLrHVEGlPj1~d3Cb&Sl3Y<$>@$HOPwOIpO|jpN>Ss8RmeMw=v3f$NgWl zY(UP2WMMC|9Ox!xY@BjPSc5O=RN1Wd&Zt9vvBTQ0+0(cA);jx&=dJhk-xNr9+l=>4 z#3ht|k`qCwCw;;9_}$!@NZCPlGJR`%n9%eo-MZC!Cz9$?e-xI(aYTRW_OaUIvBJAi z-t<26ax52pR^TgQ$M?Vgm_3<)vkTRg(3BRt0PCCJ?my zwELI8<@{tM>*|R%J!dKNhm-5<^lp6o@DVOXRj9<#wR>{ZP|%#*{n#-diuFz;?Lz0mCuILl?!}|Vi5?;-5mGh1ppUvYP2RMJxXZVZl;HBfAlHhnJ zaBQld+H*mkj(zKq0CAY_@UPb9x8x99gi1H>|oOB>jEB|!D zvHOfCS(}P9CAE9T{of@D-9sxSPdC=4*FQcEN?gevoIE*WnqogQl5JNMk1(=rC5Zc z`p4v8cc{KSd*X~wq`$Ttmkhm$n)s-z!{TtY^*S(-o)j|ZH}~v!Bl}!IH(pQCT$4(| zIM5@xa^QVbCuh#G=>Y#*0}t#@yYw%K&BvEH7+Ic!pX=UOP)(X&(#dRn+f7`HsahVp z&R;w0c>vWzeQnVFDE4_TM&ehJr`l_5$QMPIGb3nd5+|3TL1MZvyskTJ@wFa0{Gw8n zD9`F5v%NutUWq}OBdJ17aTxzY=4Y7f&^0>I6A7Oj7S8xZ`VckqyXBQ?48xGbhd zzB^O4qvzM%S(*D+)q9xfQ08X)2SNQ8B9vwL`2MkZ7cme6(X>cV|0$-B8Y)2AZ%YdH zwCE2!u$x(!beMezTTI`gJebbpfls^2ODf4ws_~LyRnvP1f+puqj?_u7ccL#3Cd_8= znD@cx`vK_EqO&MhvL>v%2^Za_K416bsy)ctea`<{g4!qXzqzlCwkB=KBFklN z^w>08uNYGh4VXuK;Z*@SGq-$aR~BALsF8Z2PfVVx-n2T{w9x2ntQV}?nHE!rD7j&> z3^gAD(gSS$u+`Dg37_%GF-cZa{xs_hekpTBi_9FXM_xM#G18~#YVoN-g6vOzezl%+ za#-!!AmiA&62)ob!Ev|{O(mGOimzQOxkjuFEIANW@|aPL)Gfu}TowJEyUiUXysd2b z0B0GJTc!cQD0lp&0^(Gt8{EDotV;%2D?N>s2pLp<(=XW;t9Sg?F;vTXrEpLZ;oVFu zx1K*P9lUL^JScIe?<3a}0b{cLgRiENgdowz(a=j%4D`W&DK&D8+Tgu5L(SFJl(&F+;5y0bdc=N#SE66S$4rCku}pQ9 ziQ<>*MQsr2*|#~|`kp;KOv4t;1Kn0TS44G;)`knN?>`0Iy*K@81=hLV;NW_^b3N7_@-Nxw>CDlw?Iafw`Rw&4A4;rf-VRu;! zGG)L_@d(x|hQwpHgYA0f=q};eRo!3~nKU!Z`Osz32}e9(MI#@rxKn>v1oUmU5p#_# zD4$fYtFbs#N%3hrzsIuN)h0DZG3RYqTj;Gwaxw%h&zhlVbyBPM>;6V}Jv{Fh$G0@A zQ0X$FA0T`?z5sL56ks@FT3GkHYH4k1KCM~i&JZ7y{DudIc7QeXZ7nYer!f_mWd5b7 zD1N(0c42-gZdVpm#&#EV7v5oh#Ufn=ADgZ|42i*AWa|03tX?q4Eig!3J}vidP`Np1 zsizFEmtMRxWKYibaKOBZwRQAcC&;x8O95A3?08)n#aUi_s_ z^0M(b8nub~F=N9JsT%0ExBtn^WAUE90y$Uz;Vi?td0q zoHT!`z(k=nr>3Y@69SiLTzgE7b64*_c=v3Tq(%Iw`b9JK9^B&2PMbS*CP&=_Q55rb zj%e0+lvE=pcio5Gm-1RyN+*#CC3v< z?1#_UKN;-|dX?`unadYL3@_VttuDF9Lt^k~0nEXXMa?nUaEkL)NBM11P5M1}SC>G! zwucj!bcx!@440#+=cIK11I(QRiz9Qe_Oe@v*2~ia^L_+Xq&lSOKA!rp9%s=>f^&<& zX^RdUkuUZ001fs_yCItdZamK=wsR;>I6cok(B9wLv&$xpQ&(JHQeU63Pov1F=>7h9 zq1@AOikeQMf@rf#L8l0mCC-37pP}Z%pt4!Tko^wlMp8=Vpw&=>Y|dCq_3&WbUdOvI z>rOr`=N((*nIaOBUPJ9Zl=g|>;1)2eq?n`hBP}u}MAt%JvUqyk@Nh3sE>Ras&B=)1 zNihsdS0-~-KN$(VFI;j_9&XG}l7w*ZXS2;G=ie$}i-@6dgj` z>B^TrSV!QFD{@!n!iAk~^LxgU)x>ARYIA5r+-LnddswiYdw1ZWj3!yy5O}e+#;^t3 zfGgct^QFGfd2U7Kmzrkp?%#Dvko=@OH7vE%!iy0zZsxcU`&vjAj!U}l`DIe87Mp>H zqAI2r!*j7AS^7Wjt=5ZX{hi`Ih(#U#g$S9S_RlUUli`i57B%PXxxd(IrTe8fUji=D zEKz1$rcRR88Rlgw0FFw#j+7sapp?AIu;?~c?>CGTCW=ge?~4rIC~@~RX8g){o)L)q z?nf%upZ;7lI43XKr@F}369$t@WkK1NGete=d9pA|A);inhf{;aTta&4aD5NbRF?K^ zP0kGOtz^rKzL=NW{_aP%t*$)&3F<$*{hnA!E;nUtP=(*F(oGQ0^55RIPfo@md-<4t zb_*q>gy1uGT^iPE-rW){)|d_|*)E_>Lclc2(ictReVpkCw(>3CuU&yOVJI!m6;8|D zV(d7}`mMjOd3vlGV)uIj+Shgjz!T#lk+Ru9qLx5koouicR|j;Q>4lP8rKQ}X)cxAt zSPygBbPO-2kov*2x33p?BmYqwPwx%jTb}M(sb*|dGZl#s;OaTj2agf?s7rkLQrVBQ z4TjgGL?Yt<-9v*;#>LRETdmWbd#|6SsFTDLl8oFn!%qPoefMyg{O7p0;8@%n4~28< zLyo@yznS1u&^`M5U6LQfo9Oi3yC`2?_qQ!oYKoSnIm6VIS5$zG+(j7=U-Om>MpUW02~pv-n30 zHtn3V73*<~wWAHv(Y0d@VyJ2R(6^*|2guK%xnM&hj+>Gew~evlHDjp|mR02SYa;yl zw-7IKg=&f;jav=7IlD33{Nhd3X4Vv+ircOz$NVgn#Uaj8US)+sq_jL2Ii|Wh_2`rC zuk~%(!@$m4snzSBO6nx@wiL|9s@e=U`i0dV?j%&x(Y9I_CYfhCMl@UMwCjMtXwjPK zQ5_p4o>B#+es)(&rv%%_O4covdbMG#qH*-}tfatK->E1PZn66%P}c!t-lNA%XPWCKXs2Wog&6<3 zefBDU>^294$%J`1^OpWxa_`>(=Q@TD;B&l47Z)#$vTCNlD@%9E_O4vBnQ9b3s7SB8 zM`)0Odab$7rX0MhUYF|fi(x5A(BUXUmhK5^J5y|VBm&G{)V-8Hn)7>ZSy1u$`f=IV z;H13nOWh9PS+BoP4|lV}##fOQ>mx7Cdeqo=HtIe(Xk3_5T%fxPO!CtQ`8To4JsG*o zQ184aMP_Fr+LZoayU{2M;p=ifM6n+4PKdo?m?TH*r<|ehYU=cCI1%Xipq2`Ee=G$+LcJ=YbRc%*()M zXE5^H0_3aa7db>=Ecu3&vEFS0t9#7wo&9qa0$pNr_FK$3SKh^3F~z^HOzUdci8F5_ zti{9kDy%y0&fCV|_Oa+bT^3Qr-5Qg&lN< z8V51EhZLuJbFzzmyzh0quQu9izWWD@h?@N#RR%{-VVra|HPmg1nrIO#99& zcT2y)dnzERg2EaKW_>F_g?lmVM6e!&!Mj~~eXxQZRto%K? zI)ZMBDc!R2s^kh%3qoAl#v)QWm+WRAPH%j;QA`alHep9jj(liKr;}XKE*_}sw##W7DVDsM(B)dYIx z0V;nZ=j{(^z!e$U{}z!hIO0;GA5S1C3OkraJk^x)AwEoh>d)yz{0&|;@!meI&(+I5yyox~N7vZ(#&Dd=4?+1spfEVTN z#m3`8HNl4Z91F8hz4f+$znU$TgGy?E<|#2zTL?BK5X^y`A75nL1`Jbw;EX@ia3skb!Kj$&Hrmm;~9f--~t3OrJjL?fv(kQh${Pezoa!nv^`40A!IJ8Ys?b z*QmnIjg8A9-`KL=JaPszu1l5;2uRAMB)3M5`u2USzFTosqMv9u_X_ErE^VHkpHMm^ ztS-sTO)s)U5x1S_Pfwaed7{4v5*xrzrpv|zYMiycytqoaR+pjEQWjt=Zz$*t= z`E6O->_GcLdFPMg#o`ICa_5h=o`l?V) zg@^vT+@@pykOzMkPy3_uzuZS3+<9#PP7X)_CYM6(th8J2-f@YhIhk=w=+aZdxU01_s>55;zitMWtA z8RbVmNX$iPchAx8L-%GsMc;OOr{(2{6*e&z(q~y=eC+JE!@8_OlJOk>7!%eRra#^4 zT3mNLnO44)vpIUZH?o?+oF$TFbulFl^IH5)atUo`AEFHN8vc|(UuPudV|l!9tKu30 zcXGH~EPn=L*5M-L8Bs_~8TCQ>?O2YS?9;k!#h(aRBC1G-!Z)_`uM|74WX45OPLf%D zkDT4srsXvpHH|AzfYF;$yM63dGkA-xLrwXZJ;nwR79bN{I$5K{+1n-#{1WsVtnldC zv5`+6syWear5%qciVol}@H$rEhsE(nWfT4rg@M0oeiTjdCluN!LpCKTk2Vi~Wm(cF zIHyt=@{?Ed?Q{yx$X_9}Z5s4YoRh^A;pPp8Bqb3r6*^Lcks%?LF{=LYKJFf&A~!4d zwhHzk_r7|z3&2hpYt!BanAUm@wQ>t26o+ln%;Rva%nUd+c@1i4nY9$bAloDh>b+7g z#dkaKYCm~2u16DS1oO}rp#t!p<-NtRJZSVb_+$MZ7F(m!b@97}mErp+#bD=J!K{&7 z)2cR9mIOB>yT>QWPmqTx{2OaK=L)l9|28@X9~;umMT+y-v>$|jt^Dq#h|pjj1s^|R zybg+u9e6}Y6OD-i+;VQqfhNnER$mv-B0CSVDb^n2jP)!7mRSL#Lp5-Cw1j3}xUM$& zwNu2{aB|Yn+y|@a8PU3WQcGetWYl2DK%-zxV3s&&lIMEm$#bh+P@JC`w}2?cbrX>! z=|vzA%`=lC*I@EVJxkKa%0df@pomkl$4YZ#eVMD`FsG%ZtBJpl+Rb8`8)}eIj?~zW z?nuO4fwAjyi>U_@*y}9AuPbGBZF6Dz6G7G!2gk%m&v6E=oH__biq+gfLmV(h)dVrO zx~fTT>`ZTn-Ak?)fs~D54rW$!DzRb$a2AA$)oS{ZfvjmAIZ>S|+Z z?Y=tFX-+R0*~@aBAFD_Vo*g5)%6m^h#Lo2@3X zu;bB#GiMcBg*l@f2-#jXV%x5MX^q&>3a1f&zb9pa1^R|2q8c}O`f1eF4?F%*GN2Ix zd~A8I_~EhDSTJRcWQmrZA0_R+HlGoccuME525#->VHLUnQ?NC!fBU5#H8(*}~`wuAEp( ziq!J3dZhUdch!=r=+yehoUwiA$%m7CCTuD7gdVo$AkaedgnX5u081-eVhn z`1v)I6E#BiNxS0iSsjBr_|L|lTQk-7 zweroBXgJ@O)XCqw>KOZ>Liwk>n!#wjgpllXPyK@em@X65aGOlZqlIvI?p+l>!ZWjB zbJoO>bh*f;L`D^8cguyzj=<)aDEAsae(5rkqUKdhuMu8I?~TXMwM(*giReO$7Ki-K zW}jg9!uxcc@Q{eWp*+v&$!+)p7b z4E^X2-nr<~3_>)SsUi;?<-Lj>Jr9%}@SGbj_aYk--W%5sIuMGH+9L-W7MnA$U}rA) z+lZf^!ICi{ns7*V68M(%>!S=499_;z|1!Her8_BXJ}Jw4$sxmTcb}V>&D=)DnxIqdRId{LMyE}6!JnImjDn|M>rHk2wy*bM}Y;k?wO*yfSm zzBrq}*hixw*uBiINm?J$ft-DpAO#M1U`*yLHK7#N@I=smDi(wTI-J3}89%zR=7o(& z1&A~_F_X{ov5LFXO_vy4Uwu=GKtZQ&0`8@}K|AHO9uR(FFe|5v8yTh3yDqHWowc}N z=CZa>)z?3tSC+I+?|)kqiR#o^A?n89%J0=DWP#CJDrVa;-Li&_QbgYz^Y!-g_7JLceIsjcqSPN2n=X_xGc zf)9%aav9OdI4`Tc?aXRKzD9mr1sF2+30W_@d2qV-oo<8?AAZ+c8tHB3VxEYbd=>)?CD5hQ#aKhz7 zpky9lb!Ef%l^sP38kqpI!ugtmiBPh&`BP-JKY{Oj-;bmXCiKdWR?Tcs841CI)2t=-bfkmu!$KzCwFmCSc;kFE4nD$cWKhH48>3g}< z_iQWJoyn9!h@drDpmJ6mbh#t`(@ab{xhv0r4SY()?**G@8{C9xWhI*KLyvK5xz-a= zbeG(RgNwNk-;R*ntBWeH4~0+G4yHoBDw^iK+bzW zm9t*oPNc7#VemaxBzB1xP&!Es1EY1o-j|r-QMTlJtO<;oKZRj`$A*l@A6isq2TTCL zqbJ#OQc`l{&N=337f)mW;Lj^A>aKqlg#{M?3yh8P!bLrqZ^;nCivPX;-{agj^U zezDQsPnDh@pbF&RRMdtj-MN4D zu2P5_P@lxdqG*8LJPD!O!VJNKVajWI`jpvw)XzWvHPUW;2mtHy1B|f8$LmkU>MXD3V8VuQsrARrI=80eD9`JZ;+_5)R#Lpp=(=R&{bht1D9WJ7C3+$KmRvgU011KnE#dnyf`I(68xy7R6H`$$URs(g-7*KU z;vQEu0sUH%_C9wk-+yP*m*8N=m`0Fpbo4BP@>bJq=uA z`2S=nH~(ZQE?0|$;QaO$I4Lh{+lCrBAM>1*zUw0J*s_7LO-y~o;*a>AbU(kDTXXsO z`4vgP#OnYnOVe4#WNxI^%@FJdWguaB9L0H#zDOn~tlKCNht%qxNmT@Rr_Qr?C^7l9 z?7%$JP}Oem0zZ&Iel^eg`~GGHg51X$!AAl%>V3`Z5vq3_jdZvzZBJgNrX#VU5eHye zF@4BIBm4j*M*3!uuqB{=%RiSxdlwp$S8hlCqR+uHTl}u7wAID|x0$XOx&KMD6?K(X zg&O|WIFlu*G?9iOE48ZC&>BqppfHls9m@PwPxSJZLg?AiHM&X%>virEI{DTooU7RF zB00&=*OCM$iCT~yWrB*?ZJD)*Ze#m3&L2nS-TT|vO(4l#tZ1K#G_`C4S4kJapz

l_mydO1!%$O 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\wLa@o^p}cAn82Qgl*p6#4vz0U+(vQ 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 z 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

E%EZV@yupSefL#z*lyVQLq*4 z035O%YaOGZEDe@%;-y&bqT`4;tYTrG(I}We@gfPs**NLfWB%1Xt`L@9EnichlGVZ%R7wZ-Z0W)vvNrvJsEp>TP&RFGAW zN6e5~%~jum83+{!kO?34`=J~fdEd$5?o7SSa9z#aqL$*SOw%tcon1PO0&sY(tM3Zbw8vG zd1TyJagzot@-&R>k!*&rV>AOmk0ql84c1h{SePFfEXXDns+dnbN$|VxovY~JI}H#M zXA`A4rAPD&vz|~H2K3y!^w2kf{#c}W(!3f1=Dyv?ULe{AH4^ES(``i{x1|OpV#weE z7w>t3ggCu7{UI_Zq@Uu$&M_GhUIWjjj61sM1E#z*f@(b$BhMfHxlarw=m^7m3&t+*5R2pqkXpV*KId2XL^R$3Mtvr(ehAPma|4&UlWAlX!5qen0+ga) zE@`43lszNU3fxF(;uw+pfL{JxO{;kG$X!b^Arhsw0O1KvrHSn4D?8GK={ClJW5Kdx z(v3EinyYu9#*4X0393uB5_x8BWw5o`jYiWPn~h+bvWri2g5FqhB$o#noVv>)p{3EQ z=eiJm;PhtnPG(n}(%^#dkT~xUooc>B{#p<+HW1&px6WR;`f)w&`V{pwa6sijp~1rF z+~=d0kJ70+0meBo4(x)fevAVj)l=Jv$U>eG+WE8XH@yymxQ0DQ%^v%Biq~vRe`6N= zu5M2R-nHzm8u-X!NJZB(@bQht{hsWvQA&fEDbJ|4We#l!deD@*6sAXq~rpxMj3;LAc*k-tz%d$80mk2kQRXN$vT(2_PHJ^u~E=y?i zTpF2TwUINmqPwInbo8sHya2Abx5lX!`+@ffmAy_hkYESjd|YtJFo?G30^iaE6;-4? z7hiBWB>K&Z#%uA}BnLEivLA2aqr7x#{%5S`4sZmRMFT==TmC63@30XvcK+)$^5md$ zQMDq~J8xlmTD4-gUdze1alXqx#IMz;9V}m6wh!E?6-=1DnDC1m3H0TfCJcJ6hii{N zu-DgYL82v^2E2Vv)vtFpNh76HAzSwo{8SSAaGcz*^=*^C2^O-ue)}m%G>+#nNiT54 zL$i>qet6+~XfNru)N4r!Z~Y~?U|J~EdbSY_NDJ=DnIIgl^DC&mS1T; zZQPD1S%=VDObD(A4D%4t$GF4qF^a}!ceQoNhf{-B5n|V;;*1+gEVd&*l3vbX{FQkM zt{Z=%-OOvGQe1tfuYJR8$8LyBX)Mb>8wt@oc;MA^ols;aEzF)dIKo0P2{D2CcHaAPd@wgahO_SDC0C0!TE0~l}Q_w`Y(LDTuT z!ZdC~C&9$oVt{8_a->7^B+=C>nA%WR_|!;;`nS>0jGnCCIKzQ&3irKJ%vMV*;m_km z9~1T~3Rvz$BRb&ElO8=l-Xbp`2LfZ$b`_yybDeL_?uLid3fk>d{64byfRDTyCxwITIvc;ZTQH|G`@(Jq{GFs}r(rpXoQ>0S7qCZy70!Um_0L7x(q6Vf-nzH1ukbxPE)n<`#mhHCr47o z^Lpn`9C@;oL7&~9D(P?ijNg82@bjZRPeT^h?s-E%ND-$G&Gm&iaT_!H(rKG#Vo4IN zBGl&b_+N1fhkGMPV$6Q#N3K;Y=0_F#Ecx!skv

O3{ZA$udvRd}7L~cM%h(A?xLn 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 q{XISYSN8AK`hR 0 { + currentStats.SvrCPUUsage = cpuPercentages[0] + } + + // Get average load + avgLoad, err := load.Avg() + if err == nil { + currentStats.LoadAverage = [2]float64{avgLoad.Load1, avgLoad.Load5} + } + + // Get network interface statistics + netIOCounters, err := snet.IOCounters(true) + if err == nil { + var totalBytesSent, totalBytesRecv uint64 + for _, counter := range netIOCounters { + totalBytesSent += counter.BytesSent + totalBytesRecv += counter.BytesRecv + } + + currentTime := time.Now() + if !sc.netStats.LastTime.IsZero() { + // Calculate time difference (seconds) + duration := currentTime.Sub(sc.netStats.LastTime).Seconds() + + // Calculate byte differences and convert to KB + sentDiff := float64(totalBytesSent-sc.netStats.LastSent) / 1024.0 + recvDiff := float64(totalBytesRecv-sc.netStats.LastRecv) / 1024.0 + + // Calculate rate per second + currentStats.SentRate = sentDiff / duration + currentStats.RecvRate = recvDiff / duration + } + + // Update the last statistics + sc.netStats.LastSent = totalBytesSent + sc.netStats.LastRecv = totalBytesRecv + sc.netStats.LastTime = currentTime + } + + // Get memory statistics + vmem, err := mem.VirtualMemory() + if err == nil { + currentStats.SvrMemoryPct = vmem.UsedPercent + } + + // Get current process ID and process object + pid := os.Getpid() + proc, err := process.NewProcess(int32(pid)) + if err == nil { + // Get current process's CPU usage rate + procPercent, err := proc.Percent(interval) + if err == nil { + currentStats.CPUUsage = procPercent + } + + // Get current process's memory statistics + memInfo, err := proc.MemoryInfo() + if err == nil && memInfo != nil { + currentStats.MemoryUsage = formatMegabytes(bytesToMegabytes(memInfo.RSS)) // RSS is Resident Set Size + vmem, err := mem.VirtualMemory() + if err == nil { + currentStats.MemoryUsagePct = float64(memInfo.RSS) / float64(vmem.Total) * 100 + } + + if currentStats.MemoryUsage > currentStats.MaxMemoryUsage { + currentStats.MaxMemoryUsage = currentStats.MemoryUsage + } + } + + // Get current process's thread count + threads, err := proc.NumThreads() + if err == nil { + currentStats.ThreadCount = int(threads) + } + } + + // Get current process's network connection information + connections, err := snet.ConnectionsPid("all", int32(pid)) + if err == nil { + currentStats.Connections = len(connections) + } + + // Get Go runtime memory statistics + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + currentStats.Goroutines = runtime.NumGoroutine() + currentStats.HeapAlloc = formatMegabytes(bytesToMegabytes(memStats.HeapAlloc)) + currentStats.HeapSys = formatMegabytes(bytesToMegabytes(memStats.HeapSys)) + currentStats.HeapInuse = formatMegabytes(bytesToMegabytes(memStats.HeapInuse)) + if currentStats.Goroutines > currentStats.MaxGoroutines { + currentStats.MaxGoroutines = currentStats.Goroutines + } + + // Get heap memory usage + if currentStats.HeapSys > 0 { + currentStats.HeapPct = currentStats.HeapInuse / currentStats.HeapSys * 100 + } + + // Timestamp when statistics are completed + currentStats.Timestamp = time.Now() + + // Lock and update statistical data + sc.mu.Lock() + sc.stats = append(sc.stats, currentStats) + if len(sc.stats) > sc.capacity { + sc.stats = sc.stats[1:] + } + sc.mu.Unlock() + + // Publish data + ws.Hub.PubSub.Pub("sys:status", currentStats) +} + +// GetStats returns all the collected statistical data +func (sc *Collector) GetStats() []Stats { + sc.mu.Lock() + defer sc.mu.Unlock() + // Return a copy of the slice to avoid external modification + statsCopy := make([]Stats, len(sc.stats)) + copy(statsCopy, sc.stats) + return statsCopy +} + +func bytesToMegabytes(bytes uint64) float64 { + return float64(bytes) / 1024.0 / 1024.0 +} + +func formatMegabytes(mb float64) float64 { + return math.Round(mb*100) / 100 +} diff --git a/store/use.go b/store/use.go new file mode 100644 index 0000000..42d75a8 --- /dev/null +++ b/store/use.go @@ -0,0 +1,17 @@ +package store + +func DB(configKey string) *MySQLStore { + return &MySQLStore{configKey: configKey} +} + +func SQLite(configKey string) *SQLiteStore { + return &SQLiteStore{configKey: configKey} +} + +func Redis(configKey string) *RedisStore { + return &RedisStore{configKey: configKey} +} + +func SqlServer(configKey string) *SqlServerStore { + return &SqlServerStore{configKey: configKey} +} diff --git a/store/use_mysql.go b/store/use_mysql.go new file mode 100644 index 0000000..6e7b4c8 --- /dev/null +++ b/store/use_mysql.go @@ -0,0 +1,65 @@ +package store + +import ( + "github.com/spf13/viper" + "go.uber.org/zap" + "gorm.io/driver/mysql" + "gorm.io/gorm" + gormLogger "gorm.io/gorm/logger" + "gorm.io/gorm/schema" + + "github.com/wonli/aqi/config" + "github.com/wonli/aqi/logger" +) + +type MySQLStore struct { + configKey string +} + +func (m *MySQLStore) Config() *config.MySQL { + var r *config.MySQL + err := viper.UnmarshalKey(m.configKey, &r) + if err != nil { + return nil + } + + return r +} + +func (m *MySQLStore) Use() *gorm.DB { + r := m.Config() + if r == nil { + return nil + } + + if r.Enable == 0 { + return nil + } + + conf := &gorm.Config{ + Logger: gormLogger.Default.LogMode(gormLogger.LogLevel(r.LogLevel)), + NamingStrategy: schema.NamingStrategy{ + TablePrefix: r.Prefix, + }, + } + + db, err := gorm.Open(mysql.Open(r.GetDsn()), conf) + if err != nil { + logger.SugarLog.Error("Failed to connect to MySQL database", zap.String("error", err.Error())) + return nil + } + + sqlDB, err := db.DB() + if err != nil { + logger.SugarLog.Error("Error pinging database", zap.String("error", err.Error())) + return nil + } + + sqlDB.SetMaxIdleConns(r.Idle) + sqlDB.SetConnMaxLifetime(r.MaxLifetime) + if r.MaxOpen > 0 { + sqlDB.SetMaxOpenConns(r.MaxOpen) + } + + return db +} diff --git a/store/use_redis.go b/store/use_redis.go new file mode 100644 index 0000000..4488088 --- /dev/null +++ b/store/use_redis.go @@ -0,0 +1,40 @@ +package store + +import ( + "github.com/redis/go-redis/v9" + "github.com/spf13/viper" + + "github.com/wonli/aqi/config" +) + +type RedisStore struct { + configKey string +} + +func (s *RedisStore) Config() *config.Redis { + var r *config.Redis + err := viper.UnmarshalKey(s.configKey, &r) + if err != nil { + return nil + } + + return r +} + +func (s *RedisStore) Use() *redis.Client { + r := s.Config() + if r == nil { + return nil + } + + client := redis.NewClient(&redis.Options{ + Addr: r.Addr, + Username: r.Username, + Password: r.Pwd, + DB: r.Db, + MinIdleConns: r.MinIdleConns, + ConnMaxIdleTime: r.IdleTimeout, + }) + + return client +} diff --git a/store/use_sqlite.go b/store/use_sqlite.go new file mode 100644 index 0000000..3aa5190 --- /dev/null +++ b/store/use_sqlite.go @@ -0,0 +1,64 @@ +package store + +import ( + "github.com/spf13/viper" + "go.uber.org/zap" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + gormLogger "gorm.io/gorm/logger" + "gorm.io/gorm/schema" + + "github.com/wonli/aqi/config" + "github.com/wonli/aqi/logger" +) + +type SQLiteStore struct { + configKey string +} + +func (m *SQLiteStore) Config() *config.Sqlite { + var r *config.Sqlite + err := viper.UnmarshalKey(m.configKey, &r) + if err != nil { + return nil + } + + return r +} + +func (m *SQLiteStore) Use() *gorm.DB { + r := m.Config() + if r == nil { + return nil + } + + conf := &gorm.Config{ + Logger: gormLogger.Default.LogMode(gormLogger.LogLevel(r.LogLevel)), + NamingStrategy: schema.NamingStrategy{ + TablePrefix: r.Prefix, + }, + } + + db, err := gorm.Open(sqlite.Open(r.Database), conf) + if err != nil { + logger.SugarLog.Error("Connect to SQLite error", zap.String("error", err.Error())) + return nil + } + + sqlDB, err := db.DB() + if err != nil { + logger.SugarLog.Error("Ping SQLite error", + zap.String("error", err.Error()), + ) + return nil + } + + // 设置连接池参数 + sqlDB.SetMaxIdleConns(r.MaxIdleConns) + sqlDB.SetConnMaxLifetime(r.ConnMaxLifetime) + if r.MaxOpenConns > 0 { + sqlDB.SetMaxOpenConns(r.MaxOpenConns) + } + + return db +} diff --git a/store/use_sqlserver.go b/store/use_sqlserver.go new file mode 100644 index 0000000..6ebfd2a --- /dev/null +++ b/store/use_sqlserver.go @@ -0,0 +1,67 @@ +package store + +import ( + "github.com/spf13/viper" + "gorm.io/driver/sqlserver" + "gorm.io/gorm" + gormLogger "gorm.io/gorm/logger" + "gorm.io/gorm/schema" + + "github.com/wonli/aqi/config" + "github.com/wonli/aqi/logger" +) + +type SqlServerStore struct { + configKey string +} + +func (m *SqlServerStore) Config() *config.SqlServer { + var r *config.SqlServer + err := viper.UnmarshalKey(m.configKey, &r) + if err != nil { + return nil + } + + return r +} + +func (m *SqlServerStore) Use() *gorm.DB { + r := m.Config() + if r == nil { + return nil + } + + conf := &gorm.Config{ + Logger: gormLogger.Default.LogMode(gormLogger.LogLevel(r.LogLevel)), + NamingStrategy: schema.NamingStrategy{ + TablePrefix: r.Prefix, + }, + } + + conf = &gorm.Config{} + db, err := gorm.Open(sqlserver.Open(r.GetDsn()), conf) + if err != nil { + logger.SugarLog.Errorf("%s (gorm.open)", err.Error()) + return nil + } + + sqlDB, err := db.DB() + if err != nil { + logger.SugarLog.Errorf("%s (ping)", err.Error()) + return nil + } + + if r.Idle > 0 { + sqlDB.SetMaxIdleConns(r.Idle) + } + + if r.MaxLifetime > 0 { + sqlDB.SetConnMaxLifetime(r.MaxLifetime) + } + + if r.MaxOpen > 0 { + sqlDB.SetMaxOpenConns(r.MaxOpen) + } + + return db +} diff --git a/utils/ali_ocr.go b/utils/ali_ocr.go new file mode 100644 index 0000000..e93bbbf --- /dev/null +++ b/utils/ali_ocr.go @@ -0,0 +1,139 @@ +package utils + +import ( + "encoding/json" + "fmt" + + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + stream "github.com/alibabacloud-go/darabonba-stream/client" + util "github.com/alibabacloud-go/tea-utils/v2/service" + "github.com/alibabacloud-go/tea/tea" + "github.com/tidwall/gjson" +) + +type AliOcr struct { + uploadBody []byte + apiParams *openapi.Params + + client *openapi.Client + initError error + + response any + apiResponse any + + accessKey string + secret string +} + +func NewAliOcr(accessKey, secret string) *AliOcr { + instance := &AliOcr{ + accessKey: accessKey, + secret: secret, + } + + //STS see: + //https://help.aliyun.com/document_detail/378661.html + client, err := instance.createClient(tea.String(accessKey), tea.String(secret)) + if err != nil { + instance.initError = err + } + + instance.client = client + return instance +} + +func (ali *AliOcr) Request() error { + if ali.initError != nil { + return ali.initError + } + + if ali.uploadBody == nil { + return fmt.Errorf("获取上传内容失败") + } + + if ali.client == nil { + return fmt.Errorf("初始化阿里客户端失败") + } + + if ali.apiParams == nil { + return fmt.Errorf("请求参数不能为空") + } + + // runtime options + runtime := &util.RuntimeOptions{} + request := &openapi.OpenApiRequest{ + Stream: stream.ReadFromBytes(ali.uploadBody), + } + + // 复制代码运行请自行打印 API 的返回值 + // 返回值为 Map 类型,可从 Map 中获得三类数据:响应体 body、响应头 headers、HTTP 返回的状态码 statusCode。 + res, err := ali.client.CallApi(ali.apiParams, request, runtime) + if err != nil { + return err + } + + ali.apiResponse = res + apiJson, err := json.Marshal(res) + if err != nil { + return err + } + + statusCode := gjson.Get(string(apiJson), "statusCode").Int() + if statusCode != 200 { + return fmt.Errorf("返回状态码不正确") + } + + if ali.response != nil { + bodyData := gjson.Get(string(apiJson), "body.Data").String() + err = json.Unmarshal([]byte(bodyData), ali.response) + if err != nil { + return err + } + } + + return nil +} + +func (ali *AliOcr) WithResponse(s any) { + ali.response = s +} + +func (ali *AliOcr) WithApiName(apiName string) { + //RecognizeDrivingLicense + ali.apiParams = &openapi.Params{ + // 接口名称 + Action: tea.String(apiName), + // 接口版本 + Version: tea.String("2021-07-07"), + // 接口协议 + Protocol: tea.String("HTTPS"), + // 接口 HTTP 方法 + Method: tea.String("POST"), + AuthType: tea.String("AK"), + Style: tea.String("V3"), + // 接口 PATH + Pathname: tea.String("/"), + // 接口请求体内容格式 + ReqBodyType: tea.String("json"), + // 接口响应体内容格式 + BodyType: tea.String("json"), + } +} + +func (ali *AliOcr) WithBody(body []byte) { + ali.uploadBody = body +} + +func (ali *AliOcr) createClient(accessKeyId *string, accessKeySecret *string) (res *openapi.Client, err error) { + config := &openapi.Config{ + // 必填,您的 AccessKey ID + AccessKeyId: accessKeyId, + // 必填,您的 AccessKey Secret + AccessKeySecret: accessKeySecret, + } + // 访问的域名 + config.Endpoint = tea.String("ocr-api.cn-hangzhou.aliyuncs.com") + res = &openapi.Client{} + res, err = openapi.NewClient(config) + return res, err +} diff --git a/utils/bytefmt/bytefmt.go b/utils/bytefmt/bytefmt.go new file mode 100644 index 0000000..82e5530 --- /dev/null +++ b/utils/bytefmt/bytefmt.go @@ -0,0 +1,123 @@ +// Package bytefmt contains helper methods and constants for converting +// to and from a human-readable byte format. +// +// bytefmt.ByteSize(100.5*bytefmt.MEGABYTE) // "100.5M" +// bytefmt.ByteSize(uint64(1024)) // "1K" +package bytefmt + +import ( + "errors" + "strconv" + "strings" + "unicode" +) + +const ( + BYTE = 1 << (10 * iota) + KILOBYTE + MEGABYTE + GIGABYTE + TERABYTE + PETABYTE + EXABYTE +) + +var invalidByteQuantityError = errors.New("byte quantity must be a positive integer with a unit of measurement like M, MB, MiB, G, GiB, or GB") + +// ByteSize returns a human-readable byte string of the form 10M, 12.5K, and so forth. The following units are available: +// +// E: Exabyte +// P: Petabyte +// T: Terabyte +// G: Gigabyte +// M: Megabyte +// K: Kilobyte +// B: Byte +// +// The unit that results in the smallest number greater than or equal to 1 is always chosen. +func ByteSize(bytes uint64) string { + unit := "" + value := float64(bytes) + + switch { + case bytes >= EXABYTE: + unit = "E" + value = value / EXABYTE + case bytes >= PETABYTE: + unit = "P" + value = value / PETABYTE + case bytes >= TERABYTE: + unit = "T" + value = value / TERABYTE + case bytes >= GIGABYTE: + unit = "G" + value = value / GIGABYTE + case bytes >= MEGABYTE: + unit = "M" + value = value / MEGABYTE + case bytes >= KILOBYTE: + unit = "K" + value = value / KILOBYTE + case bytes >= BYTE: + unit = "B" + case bytes == 0: + return "0B" + } + + result := strconv.FormatFloat(value, 'f', 1, 64) + result = strings.TrimSuffix(result, ".0") + return result + unit +} + +// ToMegabytes parses a string formatted by ByteSize as megabytes. +func ToMegabytes(s string) (uint64, error) { + bytes, err := ToBytes(s) + if err != nil { + return 0, err + } + + return bytes / MEGABYTE, nil +} + +// ToBytes parses a string formatted by ByteSize as bytes. Note binary-prefixed and SI prefixed units both mean a base-2 units +// KB = K = KiB = 1024 +// MB = M = MiB = 1024 * K +// GB = G = GiB = 1024 * M +// TB = T = TiB = 1024 * G +// PB = P = PiB = 1024 * T +// EB = E = EiB = 1024 * P +func ToBytes(s string) (uint64, error) { + s = strings.TrimSpace(s) + s = strings.ToUpper(s) + + i := strings.IndexFunc(s, unicode.IsLetter) + + if i == -1 { + return 0, invalidByteQuantityError + } + + bytesString, multiple := s[:i], s[i:] + bytes, err := strconv.ParseFloat(bytesString, 64) + if err != nil || bytes < 0 { + return 0, invalidByteQuantityError + } + + switch multiple { + case "E", "EB", "EIB": + return uint64(bytes * EXABYTE), nil + case "P", "PB", "PIB": + return uint64(bytes * PETABYTE), nil + case "T", "TB", "TIB": + return uint64(bytes * TERABYTE), nil + case "G", "GB", "GIB": + return uint64(bytes * GIGABYTE), nil + case "M", "MB", "MIB": + return uint64(bytes * MEGABYTE), nil + case "K", "KB", "KIB": + return uint64(bytes * KILOBYTE), nil + case "B": + return uint64(bytes), nil + default: + return 0, invalidByteQuantityError + } +} diff --git a/utils/createfile.go b/utils/createfile.go new file mode 100644 index 0000000..2d50e7c --- /dev/null +++ b/utils/createfile.go @@ -0,0 +1,33 @@ +package utils + +import ( + "os" + "path/filepath" +) + +func CreateFileIfNotExists(filePath string) error { + // Check if the file exists + _, err := os.Stat(filePath) + if err == nil { + // File exists + return nil + } + + // Create the directory + dir := filepath.Dir(filePath) + err = os.MkdirAll(dir, os.ModePerm) + if err != nil { + // Failed to create directory + return err + } + + // Create an empty file + f, err := os.Create(filePath) + if err != nil { + // Failed to create file + return err + } + + defer f.Close() + return nil +} diff --git a/utils/encrypt/azdg.go b/utils/encrypt/azdg.go new file mode 100644 index 0000000..092e786 --- /dev/null +++ b/utils/encrypt/azdg.go @@ -0,0 +1,63 @@ +package encrypt + +import ( + "crypto/md5" + "encoding/base64" + "fmt" + + "github.com/wonli/aqi/utils" +) + +type Azdg struct { + cipherHash string +} + +func NewAzdg(key string) *Azdg { + cipherHash := fmt.Sprintf("%x", md5.Sum([]byte(key))) + return &Azdg{cipherHash: cipherHash} +} + +func (a *Azdg) Encrypt(sourceText string) string { + noise := utils.GetRandomString(32) + inputData := []byte(sourceText) + loopCount := len(inputData) + outData := make([]byte, loopCount*2) + + for i, j := 0, 0; i < loopCount; i, j = i+1, j+1 { + outData[j] = noise[i%32] + j++ + outData[j] = inputData[i] ^ noise[i%32] + } + + return base64.RawURLEncoding.EncodeToString([]byte(a.cipherEncode(fmt.Sprintf("%s", outData)))) +} + +func (a *Azdg) Decrypt(sourceText string) string { + buf, err := base64.RawURLEncoding.DecodeString(sourceText) + if err != nil { + fmt.Printf("Decode(%q) failed: %v", sourceText, err) + return "" + } + + inputData := []byte(a.cipherEncode(fmt.Sprintf("%s", buf))) + loopCount := len(inputData) + outData := make([]byte, loopCount) + + var p int + for i, j := 0, 0; i < loopCount; i, j = i+2, j+1 { + p = p + 1 + outData[j] = inputData[i] ^ inputData[i+1] + } + + return fmt.Sprintf("%s", outData[:p]) +} + +func (a *Azdg) cipherEncode(sourceText string) string { + inputData := []byte(sourceText) + loopCount := len(inputData) + outData := make([]byte, loopCount) + for i := 0; i < loopCount; i++ { + outData[i] = inputData[i] ^ a.cipherHash[i%32] + } + return fmt.Sprintf("%s", outData) +} diff --git a/utils/encrypt/azdg_test.go b/utils/encrypt/azdg_test.go new file mode 100644 index 0000000..bab2ed9 --- /dev/null +++ b/utils/encrypt/azdg_test.go @@ -0,0 +1,26 @@ +package encrypt + +import ( + "fmt" + "testing" + "time" +) + +func TestAzdg(t *testing.T) { + t1 := time.Now() + amap := map[string]bool{} + azdg := NewAzdg("123") + for i := 0; i < 10000; i++ { + s := azdg.Encrypt("hello world 我是中文") + _, ok := amap[s] + if !ok { + amap[s] = true + } else { + t.Error("出错了") + } + + _ = azdg.Decrypt(s) + } + + fmt.Println(time.Now().Sub(t1)) +} diff --git a/utils/encrypt/encrypt_aes.go b/utils/encrypt/encrypt_aes.go new file mode 100644 index 0000000..1c31c3f --- /dev/null +++ b/utils/encrypt/encrypt_aes.go @@ -0,0 +1,73 @@ +package encrypt + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/sha1" + "encoding/hex" + "errors" + + "github.com/wonli/aqi/logger" +) + +// AesEncrypt AES Encrypt,CBC +func AesEncrypt(origData []byte, key string) ([]byte, error) { + encryptKey := getKey(key) + block, err := aes.NewCipher(encryptKey) + if err != nil { + return nil, err + } + + blockSize := block.BlockSize() + origData = pkcs7Padding(origData, blockSize) + blockMode := cipher.NewCBCEncrypter(block, encryptKey[:blockSize]) + encrypted := make([]byte, len(origData)) + blockMode.CryptBlocks(encrypted, origData) + return encrypted, nil +} + +// AesDecrypt AES Decrypt +func AesDecrypt(encrypted []byte, key string) ([]byte, error) { + encryptKey := getKey(key) + block, err := aes.NewCipher(encryptKey) + if err != nil { + return nil, err + } + + blockSize := block.BlockSize() + blockMode := cipher.NewCBCDecrypter(block, encryptKey[:blockSize]) + origData := make([]byte, len(encrypted)) + blockMode.CryptBlocks(origData, encrypted) + err, data := pkcs7UnPadding(origData) + return data, err +} + +func getKey(key string) []byte { + sha := sha1.New() + _, err := sha.Write([]byte(key)) + if err != nil { + logger.SugarLog.Errorf("Gen key fail %s", err.Error()) + return nil + } + + byteKey := []byte(hex.EncodeToString(sha.Sum(nil))) + return byteKey[:32] +} + +func pkcs7Padding(ciphertext []byte, blockSize int) []byte { + padding := blockSize - len(ciphertext)%blockSize + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(ciphertext, padText...) +} + +func pkcs7UnPadding(origData []byte) (error, []byte) { + length := len(origData) + unPadding := int(origData[length-1]) + + if length-unPadding < 0 { + return errors.New("PKCS7 fail"), nil + } + + return nil, origData[:(length - unPadding)] +} diff --git a/utils/encrypt/encrypt_aes_test.go b/utils/encrypt/encrypt_aes_test.go new file mode 100644 index 0000000..6704fbc --- /dev/null +++ b/utils/encrypt/encrypt_aes_test.go @@ -0,0 +1,25 @@ +package encrypt + +import ( + "encoding/base64" + "fmt" + "testing" + "time" +) + +func TestAes(t *testing.T) { + t1 := time.Now() + for i := 0; i < 10000; i++ { + s, _ := AesEncrypt([]byte("hello world 我是中文"), "hello") + ss := base64.RawURLEncoding.EncodeToString(s) + //fmt.Println(ss) + + ss1, _ := base64.RawURLEncoding.DecodeString(ss) + _, err := AesDecrypt(ss1, "hello") + if err != nil { + t.Error(err.Error()) + } + } + + fmt.Println(time.Now().Sub(t1)) +} diff --git a/utils/encrypt/md5.go b/utils/encrypt/md5.go new file mode 100644 index 0000000..93fb58b --- /dev/null +++ b/utils/encrypt/md5.go @@ -0,0 +1,12 @@ +package encrypt + +import ( + "crypto/md5" + "encoding/hex" +) + +func MD5(v string) string { + m := md5.New() + m.Write([]byte(v)) + return hex.EncodeToString(m.Sum(nil)) +} diff --git a/utils/encrypt/sha256_rsa.go b/utils/encrypt/sha256_rsa.go new file mode 100644 index 0000000..5137a8b --- /dev/null +++ b/utils/encrypt/sha256_rsa.go @@ -0,0 +1,27 @@ +package encrypt + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "encoding/base64" + "fmt" +) + +// SignSHA256WithRSA generates a signature for a string using the SHA256WithRSA algorithm with a given private key. +func SignSHA256WithRSA(source string, privateKey *rsa.PrivateKey) (signature string, err error) { + if privateKey == nil { + return "", fmt.Errorf("private key should not be nil") + } + h := crypto.Hash.New(crypto.SHA256) + _, err = h.Write([]byte(source)) + if err != nil { + return "", nil + } + hashed := h.Sum(nil) + signatureByte, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(signatureByte), nil +} diff --git a/utils/file_exists.go b/utils/file_exists.go new file mode 100644 index 0000000..d2428b5 --- /dev/null +++ b/utils/file_exists.go @@ -0,0 +1,15 @@ +package utils + +import "os" + +func FileExists(filePath string) (bool, error) { + _, err := os.Stat(filePath) + if err == nil { + return true, nil + } + + if os.IsNotExist(err) { + return false, nil + } + return false, err +} diff --git a/utils/filter_string.go b/utils/filter_string.go new file mode 100644 index 0000000..44a350d --- /dev/null +++ b/utils/filter_string.go @@ -0,0 +1,35 @@ +package utils + +import ( + "fmt" + "strconv" + "strings" + + "github.com/wonli/aqi/logger" +) + +func FilterStringId(stringIds string, filterId int) string { + var result []int + strArray := strings.Split(stringIds, ",") + for _, strId := range strArray { + strIdIntVal, err := strconv.Atoi(strId) + if err != nil { + logger.SugarLog.Errorf("translate id error %s", err.Error()) + continue + } + + if strIdIntVal != filterId { + result = append(result, strIdIntVal) + } + } + + if len(result) > 0 { + return arrayToString(result, ",") + } + + return "" +} + +func arrayToString(a []int, delim string) string { + return strings.Trim(strings.Replace(fmt.Sprint(a), " ", delim, -1), "[]") +} diff --git a/utils/format/format_byte.go b/utils/format/format_byte.go new file mode 100644 index 0000000..55399a7 --- /dev/null +++ b/utils/format/format_byte.go @@ -0,0 +1,18 @@ +package format + +import ( + "fmt" + "math" +) + +func Bites(size float64) string { + unit := []string{"b", "kb", "mb", "gb", "tb", "pb"} + s := math.Floor(math.Log(size) / math.Log(1024)) + i := int(s) + + if i < len(unit) { + return fmt.Sprintf("%.2f %s", size/math.Pow(1024, s), unit[i]) + } + + return fmt.Sprintf("%f %s", size, unit[0]) +} diff --git a/utils/format/format_byte_test.go b/utils/format/format_byte_test.go new file mode 100644 index 0000000..7eed022 --- /dev/null +++ b/utils/format/format_byte_test.go @@ -0,0 +1,8 @@ +package format + +import "testing" + +func TestBite(t *testing.T) { + bytes := Bites(2624954) + t.Log(bytes) +} diff --git a/utils/format/format_time.go b/utils/format/format_time.go new file mode 100644 index 0000000..bd17cf0 --- /dev/null +++ b/utils/format/format_time.go @@ -0,0 +1,64 @@ +package format + +import ( + "fmt" + "time" +) + +type FriendTime struct { + t int64 + format string + startTime int64 + suffix string +} + +func NewFriendTime(unixTime int64) *FriendTime { + return &FriendTime{ + t: unixTime, + format: "2006-01-02 15:04:05", + suffix: "前", + } +} + +func (f *FriendTime) SetFormat(format string) { + f.format = format +} + +func (f *FriendTime) SetStartTime(startTime int64) { + f.startTime = startTime +} + +func (f *FriendTime) SetSuffix(suffix string) { + f.suffix = suffix +} + +func (f *FriendTime) Format() string { + startTime := f.startTime + if startTime == 0 { + startTime = time.Now().Unix() + } + + delta := startTime - f.t + if delta < 63072000 { + conf := []struct { + Duration int64 + Label string + }{ + {31536000, "年"}, + {2592000, "个月"}, + {604800, "星期"}, + {86400, "天"}, + {3600, "小时"}, + {60, "分钟"}, + {1, "秒"}, + } + + for _, diff := range conf { + if c := delta / diff.Duration; c != 0 { + return fmt.Sprintf("%d%s%s", c, diff.Label, f.suffix) + } + } + } + + return time.Unix(f.t, 0).Format(f.format) +} diff --git a/utils/format/format_time_test.go b/utils/format/format_time_test.go new file mode 100644 index 0000000..7fed183 --- /dev/null +++ b/utils/format/format_time_test.go @@ -0,0 +1,11 @@ +package format + +import ( + "testing" +) + +func TestTime(t *testing.T) { + fTime := NewFriendTime(1691724579) + fTime.SetSuffix("哈哈") + t.Log(fTime.Format()) +} diff --git a/utils/geo/coordinate.go b/utils/geo/coordinate.go new file mode 100644 index 0000000..cb1257d --- /dev/null +++ b/utils/geo/coordinate.go @@ -0,0 +1,39 @@ +package geo + +import ( + "fmt" + "math" + "strconv" +) + +// Coordinate represents a specific location on Earth +type Coordinate struct { + Lat, Lng float64 +} + +// Constants needed for distance calculations +const ( + EarthRadius = 6371 * Kilometer + DoubleEarthRadius = 2 * EarthRadius + PiOver180 = math.Pi / 180 +) + +// DistanceBetween calculates the distance between two coordinates +func DistanceBetween(a, b Coordinate) Distance { + value := 0.5 - math.Cos((b.Lat-a.Lat)*PiOver180)/2 + math.Cos(a.Lat*PiOver180)*math.Cos(b.Lat*PiOver180)*(1-math.Cos((b.Lng-a.Lng)*PiOver180))/2 + return DoubleEarthRadius * Distance(math.Asin(math.Sqrt(value))) +} + +// DistanceTo calculates the distance from this coordinate to another coordinate +func (c Coordinate) DistanceTo(other Coordinate) Distance { + return DistanceBetween(c, other) +} + +// String implements Stringer, returns a string representation of the coordinate +func (c Coordinate) String() string { + return fmt.Sprintf( + "(%s, %s)", + strconv.FormatFloat(c.Lat, 'f', -1, 64), + strconv.FormatFloat(c.Lng, 'f', -1, 64), + ) +} diff --git a/utils/geo/distance.go b/utils/geo/distance.go new file mode 100644 index 0000000..2756eca --- /dev/null +++ b/utils/geo/distance.go @@ -0,0 +1,69 @@ +package geo + +import ( + "math" + "strconv" +) + +// Distance represents a spacial distance. Fundamentally, the underlying float64 represents the raw +// number of meters +type Distance float64 + +// MilesPerKilometer Constants for conversions +const MilesPerKilometer = 0.6213712 + +// Standard length constants +const ( + Millimeter = Distance(0.001) + Centimeter = Distance(0.01) + Meter = Distance(1) + Kilometer = Distance(1000) + Mile = Distance(1 / MilesPerKilometer * 1000) +) + +// Millimeters gets the number of total millimeters represented by the distance +func (d Distance) Millimeters() float64 { + return d.Meters() * 1000 +} + +// Centimeters gets the number of total centimeters represented by the distance +func (d Distance) Centimeters() float64 { + return d.Meters() * 100 +} + +// Meters gets the number of total meters represented by the distance +func (d Distance) Meters() float64 { + return float64(d) +} + +// Kilometers gets the number of total kilometers represented by the distance +func (d Distance) Kilometers() float64 { + return float64(d) / 1000 +} + +// Miles gets the number of total miles represented by the distance +func (d Distance) Miles() float64 { + return d.Kilometers() * MilesPerKilometer +} + +// String implements Stringer and returns a formatted string representation of the distance +func (d Distance) String() string { + if d < 0.01 { + return strconv.FormatFloat(d.Millimeters(), 'f', 2, 64) + "mm" + } + + if d < 1 { + return strconv.FormatFloat(d.Centimeters(), 'f', 2, 64) + "cm" + } + + if d < 100 { + return strconv.FormatFloat(d.Meters(), 'f', 2, 64) + "m" + } + + return strconv.FormatFloat(d.Kilometers(), 'f', 2, 64) + "km" +} + +func (d Distance) Equals(other, tolerance Distance) bool { + difference := math.Abs(float64(d - other)) + return difference <= float64(tolerance) +} diff --git a/utils/helper.go b/utils/helper.go new file mode 100644 index 0000000..6eb717a --- /dev/null +++ b/utils/helper.go @@ -0,0 +1,8 @@ +package utils + +func HidePhoneNumber(phoneNumber string) string { + if len(phoneNumber) != 11 { + return phoneNumber + } + return phoneNumber[:3] + "****" + phoneNumber[7:] +} diff --git a/utils/i18n/gorm.go b/utils/i18n/gorm.go new file mode 100644 index 0000000..bbece1a --- /dev/null +++ b/utils/i18n/gorm.go @@ -0,0 +1,37 @@ +package i18n + +import ( + "database/sql/driver" + "fmt" +) + +type GType struct { + Data any + lng string +} + +func NewGType(data any, lng string) *GType { + return >ype{Data: data, lng: lng} +} + +func (i *GType) GormDataType() string { + return "string" +} + +func (i *GType) Scan(value interface{}) error { + v, ok := value.([]byte) + if ok { + i.Data = string(v) + return nil + } + + return nil +} + +func (i *GType) String() string { + return fmt.Sprintf("%s", i.Data) +} + +func (i *GType) Value() (driver.Value, error) { + return driver.Value(i.Data), nil +} diff --git a/utils/ip/real_ip.go b/utils/ip/real_ip.go new file mode 100644 index 0000000..7655d74 --- /dev/null +++ b/utils/ip/real_ip.go @@ -0,0 +1,103 @@ +package ip + +import ( + "bytes" + "net" + "net/http" + "strings" +) + +type HostAndPort struct { + Host string + Port string +} + +// ipRange - a structure that holds the start and end of a range of ip addresses +type ipRange struct { + start net.IP + end net.IP +} + +// inRange - check to see if a given ip address is within a range given +func inRange(r ipRange, ipAddress net.IP) bool { + // strcmp type byte comparison + if bytes.Compare(ipAddress, r.start) >= 0 && bytes.Compare(ipAddress, r.end) < 0 { + return true + } + return false +} + +var privateRanges = []ipRange{ + { + start: net.ParseIP("10.0.0.0"), + end: net.ParseIP("10.255.255.255"), + }, + { + start: net.ParseIP("100.64.0.0"), + end: net.ParseIP("100.127.255.255"), + }, + { + start: net.ParseIP("172.16.0.0"), + end: net.ParseIP("172.31.255.255"), + }, + { + start: net.ParseIP("192.0.0.0"), + end: net.ParseIP("192.0.0.255"), + }, + { + start: net.ParseIP("192.168.0.0"), + end: net.ParseIP("192.168.255.255"), + }, + { + start: net.ParseIP("198.18.0.0"), + end: net.ParseIP("198.19.255.255"), + }, +} + +// isPrivateSubnet - check to see if this ip is in a private subnet +func isPrivateSubnet(ipAddress net.IP) bool { + // my use case is only concerned with ipv4 atm + if ipCheck := ipAddress.To4(); ipCheck != nil { + // iterate over all our ranges + for _, r := range privateRanges { + // check if this ip is in a private range + if inRange(r, ipAddress) { + return true + } + } + } + return false +} + +func GetIPAddress(r *http.Request) string { + for _, h := range []string{"X-Forwarded-For", "X-Real-Ip"} { + addresses := strings.Split(r.Header.Get(h), ",") + // march from right to left until we get a public address + // that will be the address right before our proxy. + for i := len(addresses) - 1; i >= 0; i-- { + ip := strings.TrimSpace(addresses[i]) + // header can contain spaces too, strip those out. + realIP := net.ParseIP(ip) + if !realIP.IsGlobalUnicast() || isPrivateSubnet(realIP) { + // bad address, go to next + continue + } + return ip + } + } + + return r.RemoteAddr +} + +func GetIpAndPort(r *http.Request) (error, *HostAndPort) { + ipAddr := GetIPAddress(r) + host, port, err := net.SplitHostPort(ipAddr) + if err != nil { + return err, nil + } + + return nil, &HostAndPort{ + Host: host, + Port: port, + } +} diff --git a/utils/jwt/jwt.go b/utils/jwt/jwt.go new file mode 100644 index 0000000..73e9636 --- /dev/null +++ b/utils/jwt/jwt.go @@ -0,0 +1,49 @@ +package jwt + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt" + "github.com/spf13/viper" +) + +func GenerateToken(uid, authCode, channel string) (string, error) { + + if viper.GetString("jwtSecurity") == "" { + return "", errors.New("jwt security not configured") + } + + jwtLifetime := viper.GetDuration("jwtLifetime") + if jwtLifetime == 0 { + jwtLifetime = time.Hour * 48 + } + + expire := time.Now().Add(jwtLifetime) + token := jwt.NewWithClaims(jwt.SigningMethodHS256, &LoginClaims{ + UID: uid, + Channel: channel, + AuthCode: authCode, + StandardClaims: jwt.StandardClaims{ + ExpiresAt: expire.Unix(), + }, + }) + + return token.SignedString([]byte(viper.GetString("jwtSecurity"))) +} + +func ValidToken(t string) (*LoginClaims, error) { + token, err := jwt.ParseWithClaims(t, &LoginClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(viper.GetString("jwtSecurity")), nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*LoginClaims); ok && token.Valid { + return claims, nil + } else { + return nil, errors.New("failed to validate token") + } +} diff --git a/utils/jwt/login_claims.go b/utils/jwt/login_claims.go new file mode 100644 index 0000000..bd08266 --- /dev/null +++ b/utils/jwt/login_claims.go @@ -0,0 +1,28 @@ +package jwt + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt" +) + +type LoginClaims struct { + Channel string + UID string + AuthCode string + StandardClaims jwt.StandardClaims +} + +func (l *LoginClaims) Valid() error { + if l.UID == "" || l.Channel == "" { + return errors.New("illegal tokens") + } + + t := time.Now().Unix() + if t > l.StandardClaims.ExpiresAt { + return errors.New("token has expired") + } + + return nil +} diff --git a/utils/path.go b/utils/path.go new file mode 100644 index 0000000..4064162 --- /dev/null +++ b/utils/path.go @@ -0,0 +1,31 @@ +package utils + +import ( + "os" + "strings" + "time" +) + +func GetFilenamePath(prefix string) string { + //gen ymd path + p := time.Now().Format("/2006/01/02/") + if prefix == "" { + return p + } + + return strings.TrimRight(prefix, "/") + p +} + +// PathExists check path +func PathExists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + + if os.IsNotExist(err) { + return false, nil + } + + return false, err +} diff --git a/utils/pem/pem.go b/utils/pem/pem.go new file mode 100644 index 0000000..b52640c --- /dev/null +++ b/utils/pem/pem.go @@ -0,0 +1,112 @@ +package pem + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "os" + "time" +) + +// LoadCertificate loads a certificate from its textual content. +func LoadCertificate(certificateStr string) (certificate *x509.Certificate, err error) { + block, _ := pem.Decode([]byte(certificateStr)) + if block == nil { + return nil, fmt.Errorf("decode certificate err") + } + + if block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("the kind of PEM should be CERTIFICATE") + } + + certificate, err = x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse certificate err:%s", err.Error()) + } + + return certificate, nil +} + +// LoadPrivateKey loads a private key from its textual content. +func LoadPrivateKey(privateKeyStr string) (privateKey *rsa.PrivateKey, err error) { + block, _ := pem.Decode([]byte(privateKeyStr)) + if block == nil { + return nil, fmt.Errorf("decode private key err") + } + if block.Type != "PRIVATE KEY" { + return nil, fmt.Errorf("the kind of PEM should be PRVATE KEY") + } + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse private key err:%s", err.Error()) + } + privateKey, ok := key.(*rsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("%s is not rsa private key", privateKeyStr) + } + return privateKey, nil +} + +// LoadPublicKey loads a public key from its textual text content. +func LoadPublicKey(publicKeyStr string) (publicKey *rsa.PublicKey, err error) { + block, _ := pem.Decode([]byte(publicKeyStr)) + if block == nil { + return nil, errors.New("decode public key error") + } + if block.Type != "PUBLIC KEY" { + return nil, fmt.Errorf("the kind of PEM should be PUBLIC KEY") + } + key, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse public key err:%s", err.Error()) + } + publicKey, ok := key.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("%s is not rsa public key", publicKeyStr) + } + return publicKey, nil +} + +// LoadCertificateWithPath loads a certificate from a file path. +func LoadCertificateWithPath(path string) (certificate *x509.Certificate, err error) { + certificateBytes, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read certificate pem file err:%s", err.Error()) + } + return LoadCertificate(string(certificateBytes)) +} + +// LoadPrivateKeyWithPath loads a private key from a file path. +func LoadPrivateKeyWithPath(path string) (privateKey *rsa.PrivateKey, err error) { + privateKeyBytes, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read private pem file err:%s", err.Error()) + } + return LoadPrivateKey(string(privateKeyBytes)) +} + +// LoadPublicKeyWithPath load public key +func LoadPublicKeyWithPath(path string) (publicKey *rsa.PublicKey, err error) { + publicKeyBytes, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read certificate pem file err:%s", err.Error()) + } + return LoadPublicKey(string(publicKeyBytes)) +} + +// GetCertificateSerialNumber retrieves the serial number from a certificate. +func GetCertificateSerialNumber(certificate x509.Certificate) string { + return fmt.Sprintf("%X", certificate.SerialNumber.Bytes()) +} + +// IsCertificateExpired checks if the certificate is expired at a specific time. +func IsCertificateExpired(certificate x509.Certificate, now time.Time) bool { + return now.After(certificate.NotAfter) +} + +// IsCertificateValid checks if the certificate is valid at a specific time. +func IsCertificateValid(certificate x509.Certificate, now time.Time) bool { + return now.After(certificate.NotBefore) && now.Before(certificate.NotAfter) +} diff --git a/utils/pem/pem_test.go b/utils/pem/pem_test.go new file mode 100644 index 0000000..a85ef09 --- /dev/null +++ b/utils/pem/pem_test.go @@ -0,0 +1,234 @@ +// Copyright 2021 Tencent Inc. All rights reserved. + +package pem + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testPemUtilCertificateStrWithoutTags = `MIIEhDCCA2ygAwIBAgIUDErvNLiTQIgbsrJSJjk9wuR/CUswDQYJKoZIhvcNAQEF +BQAwRjEbMBkGA1UEAwwSVGVucGF5LmNvbSBVc2VyIENBMRIwEAYDVQQLDAlDQSBD +ZW50ZXIxEzARBgNVBAoMClRlbnBheS5jb20wHhcNMjAwODA0MTAwMTI3WhcNMjUw +ODAzMTAwMTI3WjCBlTEYMBYGA1UEAwwPVGVucGF5LmNvbSBzaWduMSUwIwYJKoZI +hvcNAQkBFhZzdXBwb3J0QHN6aXRydXMuY29tLmNuMR0wGwYDVQQLDBRUZW5wYXku +Y29tIENBIENlbnRlcjETMBEGA1UECgwKVGVucGF5LmNvbTERMA8GA1UEBwwIU2hl +blpoZW4xCzAJBgNVBAYTAkNOMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAxTHf8ijqgucDt1PZEZ+FvGxR5po2fmw2pEzr2WK7KlbZYlNFMzo8OlAa38eU +SIWBL6E70gnfVEnKxdMxMgSLrhf8nwy48X90lpm6NX1PYVJX8i/B5n0rS9hgOB35 +x0EjwpOeMHTyx9tWW+5/JmWcaUfF587eGoUpHlT3kciB6nDV1/yNMHoDw5vB2E9w +LaiuGdWREhERYxsUCPyZZ1mltm5ClKAfrpPHWGSvarKI/G8ooDm3jXcgp2ajHNqB +ErWP9yBTes42IT7mjmG++Ss4WyB5H91eTy7Xdj1FNQYgDHtNMVmnoggwdV6X4OBx +biSJoKvpaghIoIdIlV7yTuDc/QIDAQABo4IBGDCCARQwCQYDVR0TBAIwADALBgNV +HQ8EBAMCBsAwTwYIKwYBBQUHAQEEQzBBMD8GCCsGAQUFBzAChjNvY3NwLGh0dHA6 +Ly9Zb3VyX1NlcnZlcl9OYW1lOlBvcnQvVG9wQ0EvbG9kcF9CYXNlRE4waQYDVR0f +BGIwYDBeoFygWoZYaHR0cDovLzkuMTkuMTYxLjQ6ODA4MC9Ub3BDQS9wdWJsaWMv +aXRydXNjcmw/Q0E9MzlCNDk3QUJDOEFFODg1NzQ1QkY1NjgxRTRGMDNCOEI2NDdG +MjhFQTAfBgNVHSMEGDAWgBROc805tvupF/jOiYapcvSklvPrLjAdBgNVHQ4EFgQU +YqSE0znX254pZnVDEe1rpCzs5u8wDQYJKoZIhvcNAQEFBQADggEBABvRHEHSW9KK +e6Dj5LGFO9Av20SWGMYVUNlwN4uWdoYZAesLl7Nog/znwHSVgyieqRUnKjm12L+h +J2mIKtwvoZhGWlN7KA6zLrlio/w22oZfGbKYvU8GEHAQ/N483HvH3byYltDTvd8R +YbxuS2D1GPYI3drRUXuEr9Qq8lcqHi0qVFvVKYm3VwXU+Rr7BOT9ebSGxH456IU8 +D17FsyucjhF/KRBGbN2pul0l7i1qMGkhNY18RkzrhWE8GB3PpaeWufOqgPgqUUPV +Bii2fY18BZkSIos9s4yYMcPrA4ApHG+Fpb2NgfRNICEvIdXbhnEVMeWEqmW5SD9y +mBlsiHvszAM=` + testPemUtilCertificateStr = `-----BEGIN CERTIFICATE----- +MIIEhDCCA2ygAwIBAgIUDErvNLiTQIgbsrJSJjk9wuR/CUswDQYJKoZIhvcNAQEF +BQAwRjEbMBkGA1UEAwwSVGVucGF5LmNvbSBVc2VyIENBMRIwEAYDVQQLDAlDQSBD +ZW50ZXIxEzARBgNVBAoMClRlbnBheS5jb20wHhcNMjAwODA0MTAwMTI3WhcNMjUw +ODAzMTAwMTI3WjCBlTEYMBYGA1UEAwwPVGVucGF5LmNvbSBzaWduMSUwIwYJKoZI +hvcNAQkBFhZzdXBwb3J0QHN6aXRydXMuY29tLmNuMR0wGwYDVQQLDBRUZW5wYXku +Y29tIENBIENlbnRlcjETMBEGA1UECgwKVGVucGF5LmNvbTERMA8GA1UEBwwIU2hl +blpoZW4xCzAJBgNVBAYTAkNOMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAxTHf8ijqgucDt1PZEZ+FvGxR5po2fmw2pEzr2WK7KlbZYlNFMzo8OlAa38eU +SIWBL6E70gnfVEnKxdMxMgSLrhf8nwy48X90lpm6NX1PYVJX8i/B5n0rS9hgOB35 +x0EjwpOeMHTyx9tWW+5/JmWcaUfF587eGoUpHlT3kciB6nDV1/yNMHoDw5vB2E9w +LaiuGdWREhERYxsUCPyZZ1mltm5ClKAfrpPHWGSvarKI/G8ooDm3jXcgp2ajHNqB +ErWP9yBTes42IT7mjmG++Ss4WyB5H91eTy7Xdj1FNQYgDHtNMVmnoggwdV6X4OBx +biSJoKvpaghIoIdIlV7yTuDc/QIDAQABo4IBGDCCARQwCQYDVR0TBAIwADALBgNV +HQ8EBAMCBsAwTwYIKwYBBQUHAQEEQzBBMD8GCCsGAQUFBzAChjNvY3NwLGh0dHA6 +Ly9Zb3VyX1NlcnZlcl9OYW1lOlBvcnQvVG9wQ0EvbG9kcF9CYXNlRE4waQYDVR0f +BGIwYDBeoFygWoZYaHR0cDovLzkuMTkuMTYxLjQ6ODA4MC9Ub3BDQS9wdWJsaWMv +aXRydXNjcmw/Q0E9MzlCNDk3QUJDOEFFODg1NzQ1QkY1NjgxRTRGMDNCOEI2NDdG +MjhFQTAfBgNVHSMEGDAWgBROc805tvupF/jOiYapcvSklvPrLjAdBgNVHQ4EFgQU +YqSE0znX254pZnVDEe1rpCzs5u8wDQYJKoZIhvcNAQEFBQADggEBABvRHEHSW9KK +e6Dj5LGFO9Av20SWGMYVUNlwN4uWdoYZAesLl7Nog/znwHSVgyieqRUnKjm12L+h +J2mIKtwvoZhGWlN7KA6zLrlio/w22oZfGbKYvU8GEHAQ/N483HvH3byYltDTvd8R +YbxuS2D1GPYI3drRUXuEr9Qq8lcqHi0qVFvVKYm3VwXU+Rr7BOT9ebSGxH456IU8 +D17FsyucjhF/KRBGbN2pul0l7i1qMGkhNY18RkzrhWE8GB3PpaeWufOqgPgqUUPV +Bii2fY18BZkSIos9s4yYMcPrA4ApHG+Fpb2NgfRNICEvIdXbhnEVMeWEqmW5SD9y +mBlsiHvszAM= +-----END CERTIFICATE-----` + testPemUtilCertificateSerial = `0C4AEF34B89340881BB2B25226393DC2E47F094B` + + testPemUtilPrivateKeyStrWithoutTags = `MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDZUJN33V+dSfvd +fL0Mu+39XrZNXFFMQSy1V15FpncHeV47SmV0TzTqZc7hHB0ddqAdDi8Z5k3TKqb7 +6sOwYr5TcAfuR6PIPaleyE0/0KrljBum2Isa2Nyq7Dgc3ElBQ6YN4l/a+DpvKaz1 +FSKmKrhLNskqokWVSlu4g8OlKlbPXQ9ibII14MZRQrrkTmHYHzfi7GXXM0thAKuR +0HNvyhTHBh4/lrYM3GaMvmWwkwvsMavnOex6+eioZHBOb1/EIZ/LzC6zuHArPpyW +3daGaZ1rtQB1vVzTyERAVVFsXXgBHvfFud3w3ShsJYk8JvMwK2RpJ5/gV0QSARcm +LDRUAlPzAgMBAAECggEBAMc7rDeUaXiWv6bMGbZ3BTXpg1FhdddnWUnYE8HfX/km +OFI7XtBHXcgYFpcjYz4D5787pcsk7ezPidAj58zqenuclmjKnUmT3pfbI5eCA2v4 +C9HnbYDrmUPK1ZcADtka4D6ScDccpNYNa1g2TFHzkIrEa6H+q7S3O2fqxY/DRVtN +0JIXalBb8daaqL5QVzSmM2BMVnHy+YITJWIkP2a3pKs9C0W65JGDsnG0wVrHinHF ++cnhFZIbaPEI//DAFMc9NkrWOKVRTEgcCUxCFaHOZVNxDWZD7A2ZfJB2rK6eg//y +gEiFDR2h6mTaDowMB4YF2n2dsIO4/dCG8vPHI20jn4ECgYEA/ZGu6lEMlO0XZnam +AZGtiNgLcCfM/C2ZERZE7QTRPZH1WdK92Al9ndldsswFw4baJrJLCmghjF/iG4zi +hhBvLnOLksnZUfjdumxoHDWXo2QBWbI5QsWIE7AuTiWgWj1I7X4fCXSQf6i+M/y2 +6TogQ7d0ANpZFyOkTNMn/tiJvLECgYEA22XqlamG/yfAGWery5KNH2DGlTIyd6xJ +WtJ9j3jU99lZ0bCQ5xhiBbU9ImxCi3zgTsoqLWgA/p00HhNFNoUcTl9ofc0G3zwT +D1y0ZzcnVKxGJdZ6ohW52V0hJStAigtjYAsUgjm7//FH7PiQDBDP1Wa6xSRkDQU/ +aSbQxvEE8+MCgYEA3bb8krW7opyM0XL9RHH0oqsFlVO30Oit5lrqebS0oHl3Zsr2 +ZGgoBlWBsEzk3UqUhTFwm/DhJLTSJ/TQPRkxnhQ5/mewNhS9C7yua7wQkzVmWN+V +YeUGTvDGDF6qDz12/vJAgSwDDRym8x4NcXD5tTw7mmNRcwIfL22SkysThIECgYAV +BgccoEoXWS/HP2/u6fQr9ZIR6eV8Ij5FPbZacTG3LlS1Cz5XZra95UgebFFUHHtC +EY1JHJY7z8SWvTH8r3Su7eWNaIAoFBGffzqqSVazfm6aYZsOvRY6BfqPHT3p/H1h +Tq6AbBffxrcltgvXnCTORjHPglU0CjSxVs7awW3AEQKBgB5WtaC8VLROM7rkfVIq ++RXqE5vtJfa3e3N7W3RqxKp4zHFAPfr82FK5CX2bppEaxY7SEZVvVInKDc5gKdG/ +jWNRBmvvftZhY59PILHO2X5vO4FXh7suEjy6VIh0gsnK36mmRboYIBGsNuDHjXLe +BDa+8mDLkWu5nHEhOxy2JJZl` + testPemUtilPrivateKeyStr = `-----BEGIN TESTING KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDZUJN33V+dSfvd +fL0Mu+39XrZNXFFMQSy1V15FpncHeV47SmV0TzTqZc7hHB0ddqAdDi8Z5k3TKqb7 +6sOwYr5TcAfuR6PIPaleyE0/0KrljBum2Isa2Nyq7Dgc3ElBQ6YN4l/a+DpvKaz1 +FSKmKrhLNskqokWVSlu4g8OlKlbPXQ9ibII14MZRQrrkTmHYHzfi7GXXM0thAKuR +0HNvyhTHBh4/lrYM3GaMvmWwkwvsMavnOex6+eioZHBOb1/EIZ/LzC6zuHArPpyW +3daGaZ1rtQB1vVzTyERAVVFsXXgBHvfFud3w3ShsJYk8JvMwK2RpJ5/gV0QSARcm +LDRUAlPzAgMBAAECggEBAMc7rDeUaXiWv6bMGbZ3BTXpg1FhdddnWUnYE8HfX/km +OFI7XtBHXcgYFpcjYz4D5787pcsk7ezPidAj58zqenuclmjKnUmT3pfbI5eCA2v4 +C9HnbYDrmUPK1ZcADtka4D6ScDccpNYNa1g2TFHzkIrEa6H+q7S3O2fqxY/DRVtN +0JIXalBb8daaqL5QVzSmM2BMVnHy+YITJWIkP2a3pKs9C0W65JGDsnG0wVrHinHF ++cnhFZIbaPEI//DAFMc9NkrWOKVRTEgcCUxCFaHOZVNxDWZD7A2ZfJB2rK6eg//y +gEiFDR2h6mTaDowMB4YF2n2dsIO4/dCG8vPHI20jn4ECgYEA/ZGu6lEMlO0XZnam +AZGtiNgLcCfM/C2ZERZE7QTRPZH1WdK92Al9ndldsswFw4baJrJLCmghjF/iG4zi +hhBvLnOLksnZUfjdumxoHDWXo2QBWbI5QsWIE7AuTiWgWj1I7X4fCXSQf6i+M/y2 +6TogQ7d0ANpZFyOkTNMn/tiJvLECgYEA22XqlamG/yfAGWery5KNH2DGlTIyd6xJ +WtJ9j3jU99lZ0bCQ5xhiBbU9ImxCi3zgTsoqLWgA/p00HhNFNoUcTl9ofc0G3zwT +D1y0ZzcnVKxGJdZ6ohW52V0hJStAigtjYAsUgjm7//FH7PiQDBDP1Wa6xSRkDQU/ +aSbQxvEE8+MCgYEA3bb8krW7opyM0XL9RHH0oqsFlVO30Oit5lrqebS0oHl3Zsr2 +ZGgoBlWBsEzk3UqUhTFwm/DhJLTSJ/TQPRkxnhQ5/mewNhS9C7yua7wQkzVmWN+V +YeUGTvDGDF6qDz12/vJAgSwDDRym8x4NcXD5tTw7mmNRcwIfL22SkysThIECgYAV +BgccoEoXWS/HP2/u6fQr9ZIR6eV8Ij5FPbZacTG3LlS1Cz5XZra95UgebFFUHHtC +EY1JHJY7z8SWvTH8r3Su7eWNaIAoFBGffzqqSVazfm6aYZsOvRY6BfqPHT3p/H1h +Tq6AbBffxrcltgvXnCTORjHPglU0CjSxVs7awW3AEQKBgB5WtaC8VLROM7rkfVIq ++RXqE5vtJfa3e3N7W3RqxKp4zHFAPfr82FK5CX2bppEaxY7SEZVvVInKDc5gKdG/ +jWNRBmvvftZhY59PILHO2X5vO4FXh7suEjy6VIh0gsnK36mmRboYIBGsNuDHjXLe +BDa+8mDLkWu5nHEhOxy2JJZl +-----END TESTING KEY-----` + + testPemUtilPublicKeyStrWithoutTags = `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2VCTd91fnUn73Xy9DLvt +/V62TVxRTEEstVdeRaZ3B3leO0pldE806mXO4RwdHXagHQ4vGeZN0yqm++rDsGK+ +U3AH7kejyD2pXshNP9Cq5YwbptiLGtjcquw4HNxJQUOmDeJf2vg6byms9RUipiq4 +SzbJKqJFlUpbuIPDpSpWz10PYmyCNeDGUUK65E5h2B834uxl1zNLYQCrkdBzb8oU +xwYeP5a2DNxmjL5lsJML7DGr5znsevnoqGRwTm9fxCGfy8wus7hwKz6clt3Whmmd +a7UAdb1c08hEQFVRbF14AR73xbnd8N0obCWJPCbzMCtkaSef4FdEEgEXJiw0VAJT +8wIDAQAB` + testPemUtilPublicKeyStr = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2VCTd91fnUn73Xy9DLvt +/V62TVxRTEEstVdeRaZ3B3leO0pldE806mXO4RwdHXagHQ4vGeZN0yqm++rDsGK+ +U3AH7kejyD2pXshNP9Cq5YwbptiLGtjcquw4HNxJQUOmDeJf2vg6byms9RUipiq4 +SzbJKqJFlUpbuIPDpSpWz10PYmyCNeDGUUK65E5h2B834uxl1zNLYQCrkdBzb8oU +xwYeP5a2DNxmjL5lsJML7DGr5znsevnoqGRwTm9fxCGfy8wus7hwKz6clt3Whmmd +a7UAdb1c08hEQFVRbF14AR73xbnd8N0obCWJPCbzMCtkaSef4FdEEgEXJiw0VAJT +8wIDAQAB +-----END PUBLIC KEY-----` +) + +func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } + +func TestLoadCertificate(t *testing.T) { + type args struct { + certificateStr string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "load certificate from str success", + args: args{certificateStr: testPemUtilCertificateStr}, + wantErr: false, + }, + { + name: "error loading certificate without tags", + args: args{certificateStr: testPemUtilCertificateStrWithoutTags}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := LoadCertificate(tt.args.certificateStr) + assert.Equal(t, tt.wantErr, err != nil) + }) + } +} + +func TestLoadPrivateKey(t *testing.T) { + type args struct { + privateKeyStr string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "load private key from str success", + args: args{privateKeyStr: testingKey(testPemUtilPrivateKeyStr)}, + wantErr: false, + }, + { + name: "error loading private key without tags", + args: args{privateKeyStr: testPemUtilPrivateKeyStrWithoutTags}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := LoadPrivateKey(tt.args.privateKeyStr) + assert.Equal(t, tt.wantErr, err != nil) + }) + } +} + +func TestLoadPublicKey(t *testing.T) { + type args struct { + publicKeyStr string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "load public key from str success", + args: args{publicKeyStr: testPemUtilPublicKeyStr}, + wantErr: false, + }, + { + name: "error loading public key without tags", + args: args{publicKeyStr: testPemUtilPublicKeyStrWithoutTags}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := LoadPublicKey(tt.args.publicKeyStr) + assert.Equal(t, tt.wantErr, err != nil) + }) + } +} + +func TestGetCertificateSerialNumber(t *testing.T) { + certificate, err := LoadCertificate(testPemUtilCertificateStr) + require.NoError(t, err) + + serial := GetCertificateSerialNumber(*certificate) + + assert.Equal(t, testPemUtilCertificateSerial, serial) +} diff --git a/utils/random.go b/utils/random.go new file mode 100644 index 0000000..d48ae16 --- /dev/null +++ b/utils/random.go @@ -0,0 +1,16 @@ +package utils + +import ( + "crypto/rand" + "fmt" +) + +func GetRandomString(n int) string { + randBytes := make([]byte, n/2) + _, err := rand.Read(randBytes) + if err != nil { + return "" + } + + return fmt.Sprintf("%x", randBytes) +} diff --git a/utils/random_nickname.go b/utils/random_nickname.go new file mode 100644 index 0000000..2084c7a --- /dev/null +++ b/utils/random_nickname.go @@ -0,0 +1,38 @@ +package utils + +import ( + "math/rand" +) + +func GetRandomNickname() string { + nicknames := []string{ + "小兔", "蛋黄酥", "奶糖", "糯米团子", "芒果冰", "奶茶", + "小甜心", "棉花糖", "蓝莓布丁", "糖豆", + "蜜桃仙子", "小可爱", "蜜汁糖葫芦", "草莓蛋糕", + "奇异果冰沙", "樱花泡芙", "抹茶红豆", "小橘子", "香蕉酥", "水蜜桃", + "珍珠奶茶", "软糯甜筒", "杏仁曲奇", "蜜糖柚子", + "草莓巧克力", "牛奶小饼干", "蜜桃凉粉", "巧克力酥球", "椰子树", + "柠檬蛋挞", "蜜汁菠萝", "蓝莓芝士", "奇奇怪怪果冻", "绿色小葡萄", + "香草冰淇淋", "奶黄包", "芝士薯片", "小鲸鱼", + "榴莲糖", "红豆沙", "黑森林蛋糕", "脆皮炸鸡", "哈密瓜冰茶", + "蔓越莓曲奇", "奶香葡萄干", "草莓玛奇朵", "爆米花", + "蛋黄派", "牛奶软糖", "紫薯布丁", "可乐果冻", "葡萄柚汁", + "小丸子", "网红", "阿飞", "小喵咪", "文艺青年", "格调", "暖阳", "吐槽王", + "小野猫", "混迹者", "闲云野鹤", "懒癌晚期", "老干部", "奶油小生", "微笑天使", + "快乐侠", "黑桃Ace", "初心者", + "缘来如此", "小清新", "游戏玩家", "思维者", + "匠人", "森林之子", "锐气", "眼镜男", "摄影师", + "时尚达人", "自然主义", "慢活族", "独立思考", "不二神探", + "小资", "创作者", "云游者", "美食家", "旅行达人", + "品酒师", "画家", "宅舞者", "健身达人", "跳蚤市场", "剧情党", + "音乐人", "手作达人", "文艺小清新", "设计师", + "老司机", "资深玩家", "心灵鸡汤", "智者", "小贝壳", "沉稳", "生活行家", "老狼", "爱好者", + "积极向上", "精神领袖", "滴水穿石", "构思家", "厨艺大师", "亲和者", "风度翩翩", "经验分享", + "心灵导师", "文艺清新", + "知识分子", + } + + // random return nickname + nickname := nicknames[rand.Intn(len(nicknames))] + return nickname +} diff --git a/utils/random_nickname_test.go b/utils/random_nickname_test.go new file mode 100644 index 0000000..8f2ab91 --- /dev/null +++ b/utils/random_nickname_test.go @@ -0,0 +1,9 @@ +package utils + +import "testing" + +func TestGetRandomNickname(t *testing.T) { + for i := 0; i <= 100; i++ { + println(GetRandomNickname()) + } +} diff --git a/utils/regx/id.go b/utils/regx/id.go new file mode 100644 index 0000000..e35dd97 --- /dev/null +++ b/utils/regx/id.go @@ -0,0 +1,85 @@ +package regx + +import ( + "strconv" + "strings" + "time" +) + +func CheckID(idCard string, justCheckLength bool) bool { + // verify length + var lengthValidate bool + if len(idCard) == 18 { + lengthValidate = true + } else if len(idCard) == 15 { + lengthValidate = true + } else { + lengthValidate = false + } + + if justCheckLength { + return lengthValidate + } + + if !lengthValidate { + return false + } + + cityCode := map[string]bool{ + "11": true, "12": true, "13": true, "14": true, "15": true, + "21": true, "22": true, "23": true, + "31": true, "32": true, "33": true, "34": true, "35": true, "36": true, "37": true, + "41": true, "42": true, "43": true, "44": true, "45": true, "46": true, + "50": true, "51": true, "52": true, "53": true, "54": true, + "61": true, "62": true, "63": true, "64": true, "65": true, + "71": true, + "81": true, "82": true, + "91": true, + } + + // verify area + if _, ok := cityCode[idCard[0:2]]; !ok { + return false + } + + factor := []int{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2} + verifyNumberList := []string{"1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"} + + makeVerifyBit := func(idCard string) string { + if len(idCard) != 17 { + return "" + } + + checksum := 0 + for i := 0; i < 17; i++ { + b, _ := strconv.Atoi(string(idCard[i])) + checksum += b * factor[i] + } + + mod := checksum % 11 + return verifyNumberList[mod] + } + + if len(idCard) == 15 { + if idCard[12:15] == "996" || idCard[12:15] == "997" || idCard[12:15] == "998" || idCard[12:15] == "999" { + idCard = idCard[0:6] + "18" + idCard[6:9] + } else { + idCard = idCard[0:6] + "19" + idCard[6:9] + } + + idCard += makeVerifyBit(idCard) + } else { + if strings.ToUpper(idCard[17:]) != strings.ToUpper(makeVerifyBit(idCard[0:17])) { + return false + } + } + + // verify birthday + birthDay := idCard[6:14] + d, err := time.Parse("20060102", birthDay) + if err != nil || d.Year() > time.Now().Year() || int(d.Month()) > 12 || d.Day() > 31 { + return false + } + + return true +} diff --git a/utils/regx/regx_date.go b/utils/regx/regx_date.go new file mode 100644 index 0000000..d4c72d7 --- /dev/null +++ b/utils/regx/regx_date.go @@ -0,0 +1,29 @@ +package regx + +import ( + "fmt" + "time" +) + +func ValidateDates(dates []string) error { + if dates == nil || len(dates) != 2 { + return fmt.Errorf("please provide a start date and an end date") + } + + startDate, err1 := time.Parse("2006-01-02", dates[0]) + endDate, err2 := time.Parse("2006-01-02", dates[1]) + + if err1 != nil { + return fmt.Errorf("invalid start date format: %v", err1) + } + + if err2 != nil { + return fmt.Errorf("onvalid end date format: %v", err2) + } + + if startDate.After(endDate) { + return fmt.Errorf("start date cannot be later than end date") + } + + return nil +} diff --git a/utils/regx/regx_phone.go b/utils/regx/regx_phone.go new file mode 100644 index 0000000..76327be --- /dev/null +++ b/utils/regx/regx_phone.go @@ -0,0 +1,23 @@ +package regx + +import ( + "fmt" + "regexp" +) + +func VerifyPhoneNumber(phoneNumber string) error { + if phoneNumber == "" { + return fmt.Errorf("phone number cannot be empty") + } + + matched, err := regexp.MatchString(`^1[3456789]\d{9}$`, phoneNumber) + if err != nil { + return err + } + + if !matched { + return fmt.Errorf("invalid phone number") + } + + return nil +} diff --git a/utils/regx/regx_time.go b/utils/regx/regx_time.go new file mode 100644 index 0000000..931c6d0 --- /dev/null +++ b/utils/regx/regx_time.go @@ -0,0 +1,30 @@ +package regx + +import ( + "fmt" + "time" +) + +func ValidateTimes(times []string) error { + if times == nil || len(times) != 2 { + return fmt.Errorf("incorrect input time format") + } + + startTime, err1 := time.Parse("15:04", times[0]) + endTime, err2 := time.Parse("15:04", times[1]) + + if err1 != nil { + return fmt.Errorf("invalid start time format: %v", err1) + } + + if err2 != nil { + return fmt.Errorf("invalid end time format: %v", err2) + } + + // Check if start time is after end time + if startTime.After(endTime) { + return fmt.Errorf("start time cannot be later than end time") + } + + return nil +} diff --git a/utils/rmb/yuan.go b/utils/rmb/yuan.go new file mode 100644 index 0000000..3008214 --- /dev/null +++ b/utils/rmb/yuan.go @@ -0,0 +1,58 @@ +package rmb + +import ( + "fmt" + "strconv" + "strings" +) + +func YuanToFen(yuan string, decimalPlaces float64) (int64, error) { + parts := strings.Split(yuan, ".") + integerPart, err := strconv.ParseInt(parts[0], 10, 64) + if err != nil { + return 0, err + } + + fen := integerPart * 100 + + // If there is a decimal part + if len(parts) > 1 { + // Only get the first two decimal places, the rest will be ignored + decimalPart := parts[1] + if len(decimalPart) > 2 { + decimalPart = decimalPart[:2] + } + + decimalValue, err := strconv.ParseInt(decimalPart, 10, 64) + if err != nil { + return 0, err + } + + // Decide whether to add 10 or 100 based on the number of decimal places + if len(decimalPart) == 1 { + fen += decimalValue * 10 + } else { + fen += decimalValue + } + } + + // Calculate the percentage of the price + fen = int64(float64(fen) * decimalPlaces) + + return fen, nil +} + +func FloatYuanToFen(yuan float64, decimalPlaces int) (int64, error) { + formatted := fmt.Sprintf("%.*f", decimalPlaces, yuan) + + // Remove the decimal point + formatted = strings.Replace(formatted, ".", "", -1) + + // Convert the formatted string to int64 + fen, err := strconv.ParseInt(formatted, 10, 64) + if err != nil { + return 0, err + } + + return fen, nil +} diff --git a/utils/sensitive/README.md b/utils/sensitive/README.md new file mode 100644 index 0000000..a7d11b2 --- /dev/null +++ b/utils/sensitive/README.md @@ -0,0 +1,5 @@ +## 词库使用说明 + +Please place the word lists in the "words" directory, with the file format being TXT. Each line should contain one word, and you can create multiple word lists. + +### 请将词库放在words目录下,文件格式为txt,一行一个,支持创建多个词库 diff --git a/utils/sensitive/embed.go b/utils/sensitive/embed.go new file mode 100644 index 0000000..592a561 --- /dev/null +++ b/utils/sensitive/embed.go @@ -0,0 +1,6 @@ +package sensitive + +import "embed" + +//go:embed words +var Words embed.FS diff --git a/utils/sensitive/sensitive_init.go b/utils/sensitive/sensitive_init.go new file mode 100644 index 0000000..76d1d73 --- /dev/null +++ b/utils/sensitive/sensitive_init.go @@ -0,0 +1,33 @@ +package sensitive + +import ( + "strings" + "sync" + + "github.com/importcjj/sensitive" +) + +var Entity *sensitive.Filter +var sensitiveOnce sync.Once + +func Init() { + sensitiveOnce.Do(func() { + Entity = sensitive.New() + dd, err := Words.ReadDir("words") + if err != nil { + return + } + for _, v := range dd { + data, err := Words.ReadFile("words/" + v.Name()) + if err != nil { + continue + } + + Entity.AddWord(strings.Split(string(data), "\n")...) + } + }) +} + +func Replace(msg string) string { + return Entity.Replace(msg, '*') +} diff --git a/utils/sensitive/words/dict.txt b/utils/sensitive/words/dict.txt new file mode 100644 index 0000000..e69de29 diff --git a/utils/slice_compare.go b/utils/slice_compare.go new file mode 100644 index 0000000..38db8cc --- /dev/null +++ b/utils/slice_compare.go @@ -0,0 +1,33 @@ +package utils + +// IdCompare +// Change Increase Decrease +func IdCompare(ids1, ids2 []uint) (change []uint, increase []uint, decrease []uint) { + m := make(map[uint]bool) + for _, item := range ids1 { + m[item] = true + } + + sameMap := make(map[uint]bool) + for _, id := range ids2 { + _, ok := m[id] + if ok { + //changed + change = append(change, id) + sameMap[id] = true + } else { + //add + increase = append(increase, id) + } + } + + //decrease + for _, id := range ids1 { + _, ok := sameMap[id] + if !ok { + decrease = append(decrease, id) + } + } + + return change, increase, decrease +} diff --git a/utils/slice_find.go b/utils/slice_find.go new file mode 100644 index 0000000..7ed5d43 --- /dev/null +++ b/utils/slice_find.go @@ -0,0 +1,13 @@ +package utils + +func SliceFindIndex[T comparable](inputSlice []T, a T) int { + findIndex := -1 + for index, element := range inputSlice { + if element == a { + findIndex = index + break + } + } + + return findIndex +} diff --git a/utils/slice_unique.go b/utils/slice_unique.go new file mode 100644 index 0000000..4782762 --- /dev/null +++ b/utils/slice_unique.go @@ -0,0 +1,13 @@ +package utils + +func UniqueSlice[T comparable](inputSlice []T) []T { + uniqueSlice := make([]T, 0, len(inputSlice)) + seen := make(map[T]bool, len(inputSlice)) + for _, element := range inputSlice { + if !seen[element] { + uniqueSlice = append(uniqueSlice, element) + seen[element] = true + } + } + return uniqueSlice +} diff --git a/utils/string2id.go b/utils/string2id.go new file mode 100644 index 0000000..05ebb49 --- /dev/null +++ b/utils/string2id.go @@ -0,0 +1,48 @@ +package utils + +import ( + "strconv" + "strings" +) + +func StringToIntSlice(str string) []uint { + parts := strings.Split(str, ",") + seen := make(map[uint]bool) + nums := make([]uint, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + + num, err := strconv.Atoi(part) + if err != nil { + continue + } + + uNum := uint(num) + if !seen[uNum] { + nums = append(nums, uNum) + seen[uNum] = true + } + } + + return nums +} + +func IntSliceToString(nums ...uint) string { + var strBuilder strings.Builder + seen := make(map[uint]bool) + for i, num := range nums { + if seen[num] { + continue + } + + if i > 0 { + strBuilder.WriteString(",") + } + strBuilder.WriteString(strconv.FormatUint(uint64(num), 10)) + seen[num] = true + } + return strBuilder.String() +} diff --git a/utils/string2slice.go b/utils/string2slice.go new file mode 100644 index 0000000..e07dcb2 --- /dev/null +++ b/utils/string2slice.go @@ -0,0 +1,24 @@ +package utils + +import ( + "strconv" + "strings" +) + +func String2Slice(s string) []string { + slc := strings.Split(s, ",") + for i := range slc { + slc[i] = strings.TrimSpace(slc[i]) + } + + return slc +} + +func UintsToString(numbers ...uint) string { + str := make([]string, len(numbers)) + for i, num := range numbers { + str[i] = strconv.FormatUint(uint64(num), 10) + } + + return strings.Join(str, ",") +} diff --git a/utils/string_limit.go b/utils/string_limit.go new file mode 100644 index 0000000..cdb638c --- /dev/null +++ b/utils/string_limit.go @@ -0,0 +1,14 @@ +package utils + +func LimitString(str string, limit int) string { + // Convert the string to a rune slice to properly handle Unicode characters + runes := []rune(str) + + if len(runes) > limit { + // If the length exceeds the limit + // truncate the slice to the specified length + runes = runes[:limit] + } + + return string(runes) +} diff --git a/utils/struct2map.go b/utils/struct2map.go new file mode 100644 index 0000000..8996971 --- /dev/null +++ b/utils/struct2map.go @@ -0,0 +1,30 @@ +package utils + +import ( + "reflect" +) + +func StructToMap(input any) map[string]any { + out := make(map[string]any) + v := reflect.ValueOf(input) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + // we only accept structs + if v.Kind() != reflect.Struct { + return nil + } + + typ := v.Type() + for i := 0; i < v.NumField(); i++ { + fi := typ.Field(i) + // skip unexported fields + if fi.PkgPath != "" { + continue + } + + out[fi.Name] = v.Field(i).Interface() + } + return out +} diff --git a/utils/struct2map_json.go b/utils/struct2map_json.go new file mode 100644 index 0000000..1f7e422 --- /dev/null +++ b/utils/struct2map_json.go @@ -0,0 +1,42 @@ +package utils + +import ( + "encoding/json" + "reflect" +) + +type JsonMap struct { +} + +func (j *JsonMap) MarshalBinary() (data []byte, err error) { + return json.Marshal(data) +} + +func (j *JsonMap) UnmarshalBinary(data []byte) error { + return json.Unmarshal(data, j) +} + +func (j *JsonMap) StructToMap(input any) map[string]any { + out := make(map[string]any) + v := reflect.ValueOf(input) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + // we only accept structs + if v.Kind() != reflect.Struct { + return nil + } + + typ := v.Type() + for i := 0; i < v.NumField(); i++ { + fi := typ.Field(i) + // skip unexported fields + if fi.PkgPath != "" { + continue + } + + out[fi.Name] = v.Field(i).Interface() + } + return out +} diff --git a/utils/t.go b/utils/t.go new file mode 100644 index 0000000..de621bb --- /dev/null +++ b/utils/t.go @@ -0,0 +1,138 @@ +package utils + +import ( + "fmt" + "math" + "sort" + "time" +) + +// Seconds-based time units +const ( + Day = 24 * time.Hour + Week = 7 * Day + Month = 30 * Day + Year = 12 * Month + LongTime = 37 * Year +) + +// Time formats a time into a relative string. +// +// Time(someT) -> "3 weeks ago" +func Time(then time.Time) string { + //return RelTime(then, time.Now(), "ago", "from now") + return RelTime(then, time.Now(), "前", "刚刚") +} + +// A RelTimeMagnitude struct contains a relative time point at which +// the relative format of time will switch to a new format string. A +// slice of these in ascending order by their "D" field is passed to +// CustomRelTime to format durations. +// +// The Format field is a string that may contain a "%s" which will be +// replaced with the appropriate signed label (e.g. "ago" or "from +// now") and a "%d" that will be replaced by the quantity. +// +// The DivBy field is the amount of time the time difference must be +// divided by in order to display correctly. +// +// e.g. if D is 2*time.Minute and you want to display "%d minutes %s" +// DivBy should be time.Minute so whatever the duration is will be +// expressed in minutes. +type RelTimeMagnitude struct { + D time.Duration + Format string + DivBy time.Duration +} + +//var defaultMagnitudes = []RelTimeMagnitude{ +// {time.Second, "now", time.Second}, +// {2 * time.Second, "1 second %s", 1}, +// {time.Minute, "%d seconds %s", time.Second}, +// {2 * time.Minute, "1 minute %s", 1}, +// {time.Hour, "%d minutes %s", time.Minute}, +// {2 * time.Hour, "1 hour %s", 1}, +// {Day, "%d hours %s", time.Hour}, +// {2 * Day, "1 day %s", 1}, +// {Week, "%d days %s", Day}, +// {2 * Week, "1 week %s", 1}, +// {Month, "%d weeks %s", Week}, +// {2 * Month, "1 month %s", 1}, +// {Year, "%d months %s", Month}, +// {18 * Month, "1 year %s", 1}, +// {2 * Year, "2 years %s", 1}, +// {LongTime, "%d years %s", Year}, +// {math.MaxInt64, "a long while %s", 1}, +//} + +var defaultMagnitudes = []RelTimeMagnitude{ + {time.Second, "刚刚", time.Second}, + {2 * time.Second, "1 秒 %s", 1}, + {time.Minute, "%d 秒 %s", time.Second}, + {2 * time.Minute, "1 分钟 %s", 1}, + {time.Hour, "%d 分钟 %s", time.Minute}, + {2 * time.Hour, "1 小时 %s", 1}, + {Day, "%d 小时 %s", time.Hour}, + {2 * Day, "1 天 %s", 1}, + {Week, "%d 天 %s", Day}, + {2 * Week, "1 周 %s", 1}, + {Month, "%d 周 %s", Week}, + {2 * Month, "1 月 %s", 1}, + {Year, "%d 月 %s", Month}, + {18 * Month, "1 年 %s", 1}, + {2 * Year, "2 年 %s", 1}, + {LongTime, "%d years %s", Year}, + {math.MaxInt64, "%s", 1}, +} + +// RelTime formats a time into a relative string. +// +// It takes two times and two labels. In addition to the generic time +// delta string (e.g. 5 minutes), the labels are used applied so that +// the label corresponding to the smaller time is applied. +// +// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier" +func RelTime(a, b time.Time, albl, blbl string) string { + return CustomRelTime(a, b, albl, blbl, defaultMagnitudes) +} + +// CustomRelTime formats a time into a relative string. +// +// It takes two times two labels and a table of relative time formats. +// In addition to the generic time delta string (e.g. 5 minutes), the +// labels are used applied so that the label corresponding to the +// smaller time is applied. +func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string { + lbl := albl + diff := b.Sub(a) + + if a.After(b) { + lbl = blbl + diff = a.Sub(b) + } + + n := sort.Search(len(magnitudes), func(i int) bool { + return magnitudes[i].D > diff + }) + + if n >= len(magnitudes) { + n = len(magnitudes) - 1 + } + mag := magnitudes[n] + var args []interface{} + escaped := false + for _, ch := range mag.Format { + if escaped { + switch ch { + case 's': + args = append(args, lbl) + case 'd': + args = append(args, diff/mag.DivBy) + } + escaped = false + } else { + escaped = ch == '%' + } + } + return fmt.Sprintf(mag.Format, args...) +} diff --git a/utils/time.go b/utils/time.go new file mode 100644 index 0000000..447b009 --- /dev/null +++ b/utils/time.go @@ -0,0 +1,18 @@ +package utils + +import ( + "time" +) + +func FriendlyTime(t time.Time) string { + return Time(t) +} + +func TodayStartAndEndTime() (beginTime, endTime uint) { + timeStr := time.Now().Format("2006-01-02") + t, _ := time.ParseInLocation("2006-01-02", timeStr, time.Local) + beginTime = uint(t.Unix()) + endTime = beginTime + 86400 + + return beginTime, endTime +} diff --git a/utils/tree/trie_tree.go b/utils/tree/trie_tree.go new file mode 100644 index 0000000..a574d38 --- /dev/null +++ b/utils/tree/trie_tree.go @@ -0,0 +1,131 @@ +package tree + +type Node struct { + Val rune // The value of the node + Depth int // The depth of the node in the tree + Count int // Counts the number of branches + Payload interface{} // The payload associated with the node + Child map[rune]*Node // Children of the node, mapped by rune + IsWord bool // Flag indicating if this node marks the end of a complete string +} + +// NewNode new node +func NewNode() *Node { + return &Node{Child: make(map[rune]*Node)} +} + +type Trie struct { + Root *Node +} + +func NewTrie() *Trie { + return &Trie{Root: NewNode()} +} + +// Insert node +func (t *Trie) Insert(str string, p any) { + if len(str) == 0 { + return + } + + bt := []rune(str) + node := t.Root + for _, val := range bt { + child, ok := node.Child[val] + if !ok { + child = NewNode() + child.Val = val + node.Child[val] = child + node.Count += 1 + child.Depth = node.Depth + 1 + } + node = child + } + + node.Payload = p + node.IsWord = true +} + +func (t *Trie) Find(str string) (bool, interface{}) { + bt := []rune(str) + node := t.Root + + for _, val := range bt { + child, ok := node.Child[val] + if !ok { + return false, nil + } + node = child + } + return node.IsWord, node.Payload +} + +// FindAll finds all strings that start with the given prefix and returns their payloads. +func (t *Trie) FindAll(prefix string) []any { + bt := []rune(prefix) + node := t.Root + + for _, val := range bt { + child, ok := node.Child[val] + if !ok { + return nil + } + + node = child + } + + return t.collect(node) +} + +// collect Recursively collects all strings' payloads in the subtree rooted at the given node. +func (t *Trie) collect(node *Node) (payloads []any) { + if node.IsWord { + payloads = append(payloads, node.Payload) + } + + for _, childNode := range node.Child { + payloads = append(payloads, t.collect(childNode)...) + } + + return payloads +} + +// Del deletion of a node has the following cases: +// 1. Prefix deletion: Check if Count is greater than 0, then set IsWord to false. +// 3. String deletion: +// a. If there is no branching, delete the entire string. +// b. If there is branching, only delete the part that is not a common prefix. +func (t *Trie) Del(str string) { + bt := []rune(str) + if len(str) == 0 { + return + } + + node := t.Root + var lastBranch *Node + var delVal rune + + for index, val := range bt { + child, ok := node.Child[val] + if ok { + if child.Count > 1 { + lastBranch = child + delVal = bt[index+1] + } + } + node = child + } + + if node.Count > 0 { + // del prefix + node.IsWord = false + } else { + if lastBranch == nil { + // del charset + lastBranch = t.Root + delVal = bt[0] + } + delete(lastBranch.Child, delVal) + lastBranch.Count -= 1 + } +} diff --git a/utils/trie_sensitive.go b/utils/trie_sensitive.go new file mode 100644 index 0000000..60842c4 --- /dev/null +++ b/utils/trie_sensitive.go @@ -0,0 +1,172 @@ +package utils + +import ( + "regexp" + "strings" + + "github.com/mozillazg/go-pinyin" +) + +// SensitiveTrie 敏感词前缀树 +type SensitiveTrie struct { + replaceChar rune // 敏感词替换的字符 + root *TrieNode +} + +// NewSensitiveTrie 构造敏感词前缀树实例 +func NewSensitiveTrie() *SensitiveTrie { + return &SensitiveTrie{ + replaceChar: '*', + root: &TrieNode{End: false}, + } +} + +// AddWords 批量添加敏感词 +func (s *SensitiveTrie) AddWords(sensitiveWords ...string) { + a := pinyin.NewArgs() + for _, sensitiveWord := range sensitiveWords { + chnReg := regexp.MustCompile("[\u4e00-\u9fa5]") + if chnReg.Match([]byte(sensitiveWord)) { + // 只有中文才转 + lazyPy := pinyin.LazyPinyin(sensitiveWord, a) + if lazyPy != nil { + sFirstWords := "" + for _, p := range lazyPy { + sFirstWords += p[0:1] + } + + s.addWord(sFirstWords) + s.addWord(strings.Join(lazyPy, "")) + } + } + + s.addWord(sensitiveWord) + } +} + +// Match 查找替换发现的敏感词 +func (s *SensitiveTrie) Match(text string) (sensitiveWords []string, replaceText string) { + if s.root == nil { + return nil, text + } + + // 过滤特殊字符 + filteredText := s.filterSpecialChar(text) + sensitiveMap := make(map[string]*struct{}) // 利用map把相同的敏感词去重 + textChars := []rune(filteredText) + textCharsCopy := make([]rune, len(textChars)) + copy(textCharsCopy, textChars) + for i, textLen := 0, len(textChars); i < textLen; i++ { + trieNode := s.root.findChild(textChars[i]) + if trieNode == nil { + continue + } + + // 匹配到了敏感词的前缀,从后一个位置继续 + j := i + 1 + for ; j < textLen && trieNode != nil; j++ { + if trieNode.End { + // 完整匹配到了敏感词 + if _, ok := sensitiveMap[trieNode.Data]; !ok { + sensitiveWords = append(sensitiveWords, trieNode.Data) + } + sensitiveMap[trieNode.Data] = nil + + // 将匹配的文本的敏感词替换成 * + s.replaceRune(textCharsCopy, i, j) + } + trieNode = trieNode.findChild(textChars[j]) + } + + // 文本尾部命中敏感词情况 + if j == textLen && trieNode != nil && trieNode.End { + if _, ok := sensitiveMap[trieNode.Data]; !ok { + sensitiveWords = append(sensitiveWords, trieNode.Data) + } + sensitiveMap[trieNode.Data] = nil + s.replaceRune(textCharsCopy, i, textLen) + } + } + + if len(sensitiveWords) > 0 { + // 有敏感词 + replaceText = string(textCharsCopy) + } else { + // 没有则返回原来的文本 + replaceText = text + } + + return sensitiveWords, replaceText +} + +// AddWord 添加敏感词 +func (s *SensitiveTrie) addWord(sensitiveWord string) { + // 添加前先过滤一遍 + sensitiveWord = s.filterSpecialChar(sensitiveWord) + + // 将敏感词转换成utf-8编码后的rune类型(int32) + tireNode := s.root + sensitiveChars := []rune(sensitiveWord) + for _, charInt := range sensitiveChars { + // 添加敏感词到前缀树中 + tireNode = tireNode.addChild(charInt) + } + + tireNode.End = true + tireNode.Data = sensitiveWord +} + +// replaceRune 字符替换 +func (s *SensitiveTrie) replaceRune(chars []rune, begin int, end int) { + for i := begin; i < end; i++ { + chars[i] = s.replaceChar + } +} + +// filterSpecialChar 过滤特殊字符 +func (s *SensitiveTrie) filterSpecialChar(text string) string { + text = strings.ToLower(text) + text = strings.Replace(text, " ", "", -1) // 去除空格 + return text +} + +// TrieNode 敏感词前缀树节点 +type TrieNode struct { + childMap map[rune]*TrieNode // 本节点下的所有子节点 + Data string // 在最后一个节点保存完整的一个内容 + End bool // 标识是否最后一个节点 +} + +// addChild 前缀树添加字节点 +func (n *TrieNode) addChild(c rune) *TrieNode { + + if n.childMap == nil { + n.childMap = make(map[rune]*TrieNode) + } + + if trieNode, ok := n.childMap[c]; ok { + // 存在不添加了 + return trieNode + } else { + // 不存在 + n.childMap[c] = &TrieNode{ + childMap: nil, + End: false, + } + + return n.childMap[c] + } +} + +// findChild 前缀树查找字节点 +func (n *TrieNode) findChild(c rune) *TrieNode { + if n.childMap == nil { + return nil + } + + if trieNode, ok := n.childMap[c]; ok { + return trieNode + } + + return nil +} diff --git a/utils/trie_sensitive_test.go b/utils/trie_sensitive_test.go new file mode 100644 index 0000000..cd11d3d --- /dev/null +++ b/utils/trie_sensitive_test.go @@ -0,0 +1,21 @@ +package utils + +import ( + "log" + "testing" +) + +func TestSensitive(t *testing.T) { + sensitiveWords := []string{ + "牛大大", "撒比", + } + + trie := NewSensitiveTrie() + trie.AddWords(sensitiveWords...) + + content := "今天,牛大大挑战sb灰大大" + matchSensitiveWords, replaceText := trie.Match(content) + + log.Printf("%v", trie) + log.Println(matchSensitiveWords, replaceText) +} diff --git a/utils/wxc/wxbizmsgcrypt.go b/utils/wxc/wxbizmsgcrypt.go new file mode 100644 index 0000000..a36903b --- /dev/null +++ b/utils/wxc/wxbizmsgcrypt.go @@ -0,0 +1,315 @@ +package wxc + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/sha1" + "encoding/base64" + "encoding/binary" + "encoding/xml" + "fmt" + "math/rand" + "sort" + "strings" +) + +const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +const ( + ValidateSignatureError int = -40001 + ParseXmlError int = -40002 + ComputeSignatureError int = -40003 + IllegalAesKey int = -40004 + ValidateCorpidError int = -40005 + EncryptAESError int = -40006 + DecryptAESError int = -40007 + IllegalBuffer int = -40008 + EncodeBase64Error int = -40009 + DecodeBase64Error int = -40010 + GenXmlError int = -40010 + ParseJsonError int = -40012 + GenJsonError int = -40013 + IllegalProtocolType int = -40014 +) + +type ProtocolType int + +const ( + XmlType ProtocolType = 1 +) + +type CryptError struct { + ErrCode int + ErrMsg string +} + +func NewCryptError(err_code int, err_msg string) *CryptError { + return &CryptError{ErrCode: err_code, ErrMsg: err_msg} +} + +type WXBizMsg4Recv struct { + Tousername string `xml:"ToUserName"` + Encrypt string `xml:"Encrypt"` + Agentid string `xml:"AgentID"` +} + +type CDATA struct { + Value string `xml:",cdata"` +} + +type WXBizMsg4Send struct { + XMLName xml.Name `xml:"xml"` + Encrypt CDATA `xml:"Encrypt"` + Signature CDATA `xml:"MsgSignature"` + Timestamp string `xml:"TimeStamp"` + Nonce CDATA `xml:"Nonce"` +} + +func NewWXBizMsg4Send(encrypt, signature, timestamp, nonce string) *WXBizMsg4Send { + return &WXBizMsg4Send{Encrypt: CDATA{Value: encrypt}, Signature: CDATA{Value: signature}, Timestamp: timestamp, Nonce: CDATA{Value: nonce}} +} + +type ProtocolProcessor interface { + parse(src_data []byte) (*WXBizMsg4Recv, *CryptError) + serialize(msg_send *WXBizMsg4Send) ([]byte, *CryptError) +} + +type WXBizMsgCrypt struct { + token string + encoding_aeskey string + receiver_id string + protocol_processor ProtocolProcessor +} + +type XmlProcessor struct { +} + +func (self *XmlProcessor) parse(src_data []byte) (*WXBizMsg4Recv, *CryptError) { + var msg4_recv WXBizMsg4Recv + err := xml.Unmarshal(src_data, &msg4_recv) + if nil != err { + return nil, NewCryptError(ParseXmlError, "xml to msg fail") + } + return &msg4_recv, nil +} + +func (self *XmlProcessor) serialize(msg4_send *WXBizMsg4Send) ([]byte, *CryptError) { + xml_msg, err := xml.Marshal(msg4_send) + if nil != err { + return nil, NewCryptError(GenXmlError, err.Error()) + } + return xml_msg, nil +} + +func NewWXBizMsgCrypt(token, encoding_aeskey, receiver_id string, protocol_type ProtocolType) *WXBizMsgCrypt { + var protocol_processor ProtocolProcessor + if protocol_type != XmlType { + panic("unsupport protocal") + } else { + protocol_processor = new(XmlProcessor) + } + + return &WXBizMsgCrypt{token: token, encoding_aeskey: (encoding_aeskey + "="), receiver_id: receiver_id, protocol_processor: protocol_processor} +} + +func (m *WXBizMsgCrypt) randString(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))] + } + return string(b) +} + +func (m *WXBizMsgCrypt) pKCS7Padding(plaintext string, block_size int) []byte { + padding := block_size - (len(plaintext) % block_size) + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + var buffer bytes.Buffer + buffer.WriteString(plaintext) + buffer.Write(padtext) + return buffer.Bytes() +} + +func (m *WXBizMsgCrypt) pKCS7Unpadding(plaintext []byte, block_size int) ([]byte, *CryptError) { + plaintext_len := len(plaintext) + if nil == plaintext || plaintext_len == 0 { + return nil, NewCryptError(DecryptAESError, "pKCS7Unpadding error nil or zero") + } + if plaintext_len%block_size != 0 { + return nil, NewCryptError(DecryptAESError, "pKCS7Unpadding text not a multiple of the block size") + } + padding_len := int(plaintext[plaintext_len-1]) + return plaintext[:plaintext_len-padding_len], nil +} + +func (m *WXBizMsgCrypt) cbcEncrypter(plaintext string) ([]byte, *CryptError) { + aeskey, err := base64.StdEncoding.DecodeString(m.encoding_aeskey) + if nil != err { + return nil, NewCryptError(DecodeBase64Error, err.Error()) + } + const block_size = 32 + pad_msg := m.pKCS7Padding(plaintext, block_size) + + block, err := aes.NewCipher(aeskey) + if err != nil { + return nil, NewCryptError(EncryptAESError, err.Error()) + } + + ciphertext := make([]byte, len(pad_msg)) + iv := aeskey[:aes.BlockSize] + + mode := cipher.NewCBCEncrypter(block, iv) + + mode.CryptBlocks(ciphertext, pad_msg) + base64_msg := make([]byte, base64.StdEncoding.EncodedLen(len(ciphertext))) + base64.StdEncoding.Encode(base64_msg, ciphertext) + + return base64_msg, nil +} + +func (m *WXBizMsgCrypt) cbcDecrypter(base64_encrypt_msg string) ([]byte, *CryptError) { + aeskey, err := base64.StdEncoding.DecodeString(m.encoding_aeskey) + if nil != err { + return nil, NewCryptError(DecodeBase64Error, err.Error()) + } + + encrypt_msg, err := base64.StdEncoding.DecodeString(base64_encrypt_msg) + if nil != err { + return nil, NewCryptError(DecodeBase64Error, err.Error()) + } + + block, err := aes.NewCipher(aeskey) + if err != nil { + return nil, NewCryptError(DecryptAESError, err.Error()) + } + + if len(encrypt_msg) < aes.BlockSize { + return nil, NewCryptError(DecryptAESError, "encrypt_msg size is not valid") + } + + iv := aeskey[:aes.BlockSize] + + if len(encrypt_msg)%aes.BlockSize != 0 { + return nil, NewCryptError(DecryptAESError, "encrypt_msg not a multiple of the block size") + } + + mode := cipher.NewCBCDecrypter(block, iv) + + mode.CryptBlocks(encrypt_msg, encrypt_msg) + + return encrypt_msg, nil +} + +func (m *WXBizMsgCrypt) calSignature(timestamp, nonce, data string) string { + sort_arr := []string{m.token, timestamp, nonce, data} + sort.Strings(sort_arr) + var buffer bytes.Buffer + for _, value := range sort_arr { + buffer.WriteString(value) + } + + sha := sha1.New() + sha.Write(buffer.Bytes()) + signature := fmt.Sprintf("%x", sha.Sum(nil)) + return string(signature) +} + +func (m *WXBizMsgCrypt) ParsePlainText(plaintext []byte) ([]byte, uint32, []byte, []byte, *CryptError) { + const block_size = 32 + plaintext, err := m.pKCS7Unpadding(plaintext, block_size) + if nil != err { + return nil, 0, nil, nil, err + } + + text_len := uint32(len(plaintext)) + if text_len < 20 { + return nil, 0, nil, nil, NewCryptError(IllegalBuffer, "plain is to small 1") + } + random := plaintext[:16] + msg_len := binary.BigEndian.Uint32(plaintext[16:20]) + if text_len < (20 + msg_len) { + return nil, 0, nil, nil, NewCryptError(IllegalBuffer, "plain is to small 2") + } + + msg := plaintext[20 : 20+msg_len] + receiver_id := plaintext[20+msg_len:] + + return random, msg_len, msg, receiver_id, nil +} + +func (m *WXBizMsgCrypt) VerifyURL(msg_signature, timestamp, nonce, echostr string) ([]byte, *CryptError) { + signature := m.calSignature(timestamp, nonce, echostr) + + if strings.Compare(signature, msg_signature) != 0 { + return nil, NewCryptError(ValidateSignatureError, "signature not equal") + } + + plaintext, err := m.cbcDecrypter(echostr) + if nil != err { + return nil, err + } + + _, _, msg, receiver_id, err := m.ParsePlainText(plaintext) + if nil != err { + return nil, err + } + + if len(m.receiver_id) > 0 && strings.Compare(string(receiver_id), m.receiver_id) != 0 { + fmt.Println(string(receiver_id), m.receiver_id, len(receiver_id), len(m.receiver_id)) + return nil, NewCryptError(ValidateCorpidError, "receiver_id is not equil") + } + + return msg, nil +} + +func (m *WXBizMsgCrypt) EncryptMsg(reply_msg, timestamp, nonce string) ([]byte, *CryptError) { + rand_str := m.randString(16) + var buffer bytes.Buffer + buffer.WriteString(rand_str) + + msg_len_buf := make([]byte, 4) + binary.BigEndian.PutUint32(msg_len_buf, uint32(len(reply_msg))) + buffer.Write(msg_len_buf) + buffer.WriteString(reply_msg) + buffer.WriteString(m.receiver_id) + + tmp_ciphertext, err := m.cbcEncrypter(buffer.String()) + if nil != err { + return nil, err + } + ciphertext := string(tmp_ciphertext) + + signature := m.calSignature(timestamp, nonce, ciphertext) + + msg4_send := NewWXBizMsg4Send(ciphertext, signature, timestamp, nonce) + return m.protocol_processor.serialize(msg4_send) +} + +func (m *WXBizMsgCrypt) DecryptMsg(msg_signature, timestamp, nonce string, post_data []byte) ([]byte, *CryptError) { + msg4_recv, crypt_err := m.protocol_processor.parse(post_data) + if nil != crypt_err { + return nil, crypt_err + } + + signature := m.calSignature(timestamp, nonce, msg4_recv.Encrypt) + + if strings.Compare(signature, msg_signature) != 0 { + return nil, NewCryptError(ValidateSignatureError, "signature not equal") + } + + plaintext, crypt_err := m.cbcDecrypter(msg4_recv.Encrypt) + if nil != crypt_err { + return nil, crypt_err + } + + _, _, msg, receiver_id, crypt_err := m.ParsePlainText(plaintext) + if nil != crypt_err { + return nil, crypt_err + } + + if len(m.receiver_id) > 0 && strings.Compare(string(receiver_id), m.receiver_id) != 0 { + return nil, NewCryptError(ValidateCorpidError, "receiver_id is not equil") + } + + return msg, nil +} diff --git a/utils/zip.go b/utils/zip.go new file mode 100644 index 0000000..5e79ee8 --- /dev/null +++ b/utils/zip.go @@ -0,0 +1,73 @@ +package utils + +import ( + "archive/zip" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +func Unzip(src, dest string) ([]string, error) { + r, err := zip.OpenReader(src) + if err != nil { + return nil, err + } + + var unzipFiles []string + defer func() { + _ = r.Close() + }() + + _ = os.MkdirAll(dest, 0755) + + // Closure to address file descriptors issue with all the deferred .Close() methods + extractAndWriteFile := func(f *zip.File) error { + rc, err := f.Open() + if err != nil { + return err + } + + defer func() { + _ = rc.Close() + }() + + path := filepath.Join(dest, f.Name) + // Check for ZipSlip (Directory traversal) + if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) { + return fmt.Errorf("illegal file path: %s", path) + } + + if f.FileInfo().IsDir() { + _ = os.MkdirAll(path, f.Mode()) + } else { + _ = os.MkdirAll(filepath.Dir(path), f.Mode()) + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + + defer func() { + _ = f.Close() + }() + + _, err = io.Copy(f, rc) + if err != nil { + return err + } + } + + unzipFiles = append(unzipFiles, path) + return nil + } + + for _, f := range r.File { + err = extractAndWriteFile(f) + if err != nil { + return nil, err + } + } + + return unzipFiles, nil +} diff --git a/validate/util_gin.go b/validate/util_gin.go new file mode 100644 index 0000000..2579b35 --- /dev/null +++ b/validate/util_gin.go @@ -0,0 +1,23 @@ +package validate + +import ( + "github.com/gin-gonic/gin/binding" + "github.com/go-playground/validator/v10" +) + +var GinBinding *Manager + +func GinValidator() error { + // 修改gin框架中的Validator引擎属性 + if v, ok := binding.Validator.Engine().(*validator.Validate); ok { + v.RegisterTagNameFunc(vc.tagNameFunc) + GinBinding = &Manager{ + Validator: v, + Trans: vc.getTranslator(), + } + + return vc.registerTrans(v, GinBinding.Trans) + } + + return nil +} diff --git a/validate/util_normal.go b/validate/util_normal.go new file mode 100644 index 0000000..5dbd103 --- /dev/null +++ b/validate/util_normal.go @@ -0,0 +1,32 @@ +package validate + +import ( + "sync" + + "github.com/go-playground/validator/v10" +) + +var once sync.Once +var normalManager *Manager + +func Normal(language string) *Manager { + once.Do(func() { + cc := InitTranslator(language) + + validate := validator.New() + validate.RegisterTagNameFunc(cc.tagNameFunc) + + translator := cc.getTranslator() + err := cc.registerTrans(validate, translator) + if err != nil { + panic(err) + } + + normalManager = &Manager{ + Validator: validate, + Trans: translator, + } + }) + + return normalManager +} diff --git a/validate/validate_init.go b/validate/validate_init.go new file mode 100644 index 0000000..036bcda --- /dev/null +++ b/validate/validate_init.go @@ -0,0 +1,97 @@ +package validate + +import ( + "os" + "reflect" + "strings" + "sync" + + "github.com/fatih/color" + "github.com/go-playground/locales" + "github.com/go-playground/locales/en" + "github.com/go-playground/locales/zh" + "github.com/go-playground/validator/v10" + + ut "github.com/go-playground/universal-translator" + enT "github.com/go-playground/validator/v10/translations/en" + zhT "github.com/go-playground/validator/v10/translations/zh" +) + +var sg sync.Once +var vc *validatorConfig + +type validatorConfig struct { + locale string + zh locales.Translator + en locales.Translator +} + +// InitTranslator validator默认仅支持中英文 +func InitTranslator(locale string) *validatorConfig { + sg.Do(func() { + zhl := zh.New() // 中文翻译器 + enl := en.New() // 英文翻译器 + + //赋值给valid + vc = &validatorConfig{ + locale: locale, + zh: zhl, + en: enl, + } + }) + + return vc +} + +// 处理字段名称 +// 中文使用label标签,其他语言label+语言名称,没有设置时使用json名称 +func (a *validatorConfig) tagNameFunc(fld reflect.StructField) string { + var name string + switch a.locale { + case "zh": + name = fld.Tag.Get("label") + default: + name = fld.Tag.Get("label_" + a.locale) + if name == "" { + name = strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] + if name == "-" { + return "" + } + } + } + + return name +} + +// translator +func (a *validatorConfig) getTranslator() ut.Translator { + // 第一个参数是备用(fallback)的语言环境 + // 后面的参数是应该支持的语言环境(支持多个) + // uni := ut.New(zhl, zhl) 也是可以的 + uni := ut.New(a.en, a.zh, a.en) + + // locale 通常取决于 http 请求头的 'Accept-Language' + // 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找 + trans, ok := uni.GetTranslator(a.locale) + if !ok { + color.Red("uni.GetTranslator(%s) failed", a.locale) + os.Exit(0) + } + + return trans +} + +// registerTrans +func (a *validatorConfig) registerTrans(v *validator.Validate, trans ut.Translator) error { + var err error + switch a.locale { + case "en": + err = enT.RegisterDefaultTranslations(v, trans) + case "zh": + err = zhT.RegisterDefaultTranslations(v, trans) + default: + err = enT.RegisterDefaultTranslations(v, trans) + } + + return err +} diff --git a/validate/validate_manager.go b/validate/validate_manager.go new file mode 100644 index 0000000..1ec6054 --- /dev/null +++ b/validate/validate_manager.go @@ -0,0 +1,79 @@ +package validate + +import ( + "fmt" + + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" +) + +type Manager struct { + //Trans + Trans ut.Translator + + //允许外部自定义验证方法 + Validator *validator.Validate +} + +// RegisterValidator 自定义简单验证方法 +func (g *Manager) RegisterValidator(tag, errMsg string, fn validator.Func) error { + err := g.Validator.RegisterValidation(tag, fn) + if err != nil { + return err + } + + return g.Validator.RegisterTranslation(tag, g.Trans, + func(ut ut.Translator) error { + return ut.Add(tag, errMsg, true) + }, + + func(ut ut.Translator, fe validator.FieldError) string { + t, _ := ut.T(tag, fe.Tag()) + return t + }, + ) +} + +// RegisterValidatorFunc 自定义方法封装 +func (g *Manager) RegisterValidatorFunc(tag string, + fn validator.Func, rFn validator.RegisterTranslationsFunc, tFn validator.TranslationFunc) error { + err := g.Validator.RegisterValidation(tag, fn) + if err != nil { + return err + } + + return g.Validator.RegisterTranslation(tag, g.Trans, rFn, tFn) +} + +// Translator 语言翻译 +func (g *Manager) Translator(e error) error { + errs, ok := e.(validator.ValidationErrors) + if !ok { + return e + } + + if g.Trans == nil { + return errs + } + + errorsTranslations := errs.Translate(g.Trans) + for _, err := range errs { + namespace := err.Namespace() + if s, ok := errorsTranslations[namespace]; ok { + return fmt.Errorf(s) + } + } + + return errs +} + +// Validate 执行验证并翻译配置指定的语言 +func (g *Manager) Validate(dataStruct any) error { + //处理数据 + err := g.Validator.Struct(dataStruct) + if err != nil { + return g.Translator(err) + } + + return nil +} diff --git a/worker/task.go b/worker/task.go new file mode 100644 index 0000000..ad6ec89 --- /dev/null +++ b/worker/task.go @@ -0,0 +1,12 @@ +package worker + +import ( + "context" + + "github.com/hibiken/asynq" +) + +type Task interface { + GetName() string + ProcessTask(ctx context.Context, task *asynq.Task) error +} diff --git a/worker/worker.go b/worker/worker.go new file mode 100644 index 0000000..45980f7 --- /dev/null +++ b/worker/worker.go @@ -0,0 +1,82 @@ +package worker + +import ( + "fmt" + "os" + + "github.com/hibiken/asynq" + + "github.com/wonli/aqi/logger" +) + +var Engine *EngineClient + +type EngineClient struct { + Running bool + Router map[string]Task + + Opt *asynq.RedisClientOpt + Server *asynq.Server +} + +func InitEngine(rds *asynq.RedisClientOpt, config asynq.Config) *EngineClient { + server := asynq.NewServer(rds, config) + Engine = &EngineClient{ + Opt: rds, + Server: server, + Router: map[string]Task{}, + } + + return Engine +} + +func (e *EngineClient) Register(t Task) { + if e.Running { + logger.SugarLog.Errorf("please register in router") + return + } + + name := t.GetName() + if name == "" { + logger.SugarLog.Errorf("failed to register, name is empty") + return + } + + e.Router[name] = t +} + +func (e *EngineClient) Add(task *asynq.Task) error { + t := task.Type() + if t == "" { + return fmt.Errorf("task type is undefined") + } + + _, ok := e.Router[t] + if !ok { + return fmt.Errorf("task not registered") + } + + client := asynq.NewClient(e.Opt) + defer client.Close() + + _, err := client.Enqueue(task) + if err != nil { + return err + } + + return nil +} + +func (e *EngineClient) Run() { + s := asynq.NewServeMux() + for name, handler := range e.Router { + s.Handle(name, handler) + } + + e.Running = true + err := e.Server.Run(s) + if err != nil { + logger.SugarLog.Errorf("failed to start asynq service: :%s", err.Error()) + os.Exit(0) + } +} diff --git a/ws/client.go b/ws/client.go new file mode 100644 index 0000000..6c2fcfd --- /dev/null +++ b/ws/client.go @@ -0,0 +1,188 @@ +package ws + +import ( + "fmt" + "net" + "strings" + "sync" + "time" + + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" + + "github.com/wonli/aqi/logger" +) + +type Client struct { + Hub *Hubc `json:"-"` + Conn net.Conn `json:"-"` + Send chan []byte `json:"-"` + Endpoint string `json:"-"` //入口地址 + OnceId string `json:"-"` //临时ID,扫码登录等场景作为客户端唯一标识 + Disconnecting bool `json:"-"` //已被设置为断开状态(消息发送完之后断开连接) + SyncMsg bool `json:"-"` //是否接收消息 + LastMsgId int `json:"-"` //最后一条消息ID + RequiredValid bool `json:"-"` //人机验证标识 + Validated bool `json:"-"` //是否已验证 + ValidExpiry time.Time `json:"-"` //验证有效期 + ValidCacheData any `json:"-"` //验证相关缓存数据 + AuthCode string `json:"-"` //用于校验JWT中的code,如果相等识别为同一个用户的网络地址变更 + ErrorCount int `json:"-"` //错误次数 + Closed bool `json:"-"` //是否已经关闭 + + User *User `json:"user,omitempty"` //关联用户 + Scope string `json:"scope"` //登录jwt scope, 用于判断用户从哪里登录的 + AppId string `json:"appId"` //登录应用Id + StoreId uint `json:"storeId"` //店铺ID + MerchantId uint `json:"merchantId"` //商户ID + TenantId uint `json:"tenantId"` //租户ID + Platform string `json:"platform"` //登录平台 + GroupId string `json:"groupId"` //用户分组Id + IsLogin bool `json:"isLogin"` //是否已登录 + LoginAction string `json:"loginAction"` //登录动作 + ForceDialogId string `json:"forceDialogId"` //打开聊天界面的会话ID + IpAddress string `json:"ipAddress"` //IP地址 + IpLocation string `json:"ipLocation"` //通过IP转换获得的地理位置 + IpConnAddr string `json:"IpConnAddr"` //conn连接IP地址 + ConnectionTime time.Time `json:"connectionTime"` + LastRequestTime time.Time `json:"lastRequestTime"` + LastHeartbeatTime time.Time `json:"lastHeartbeatTime"` + + mu sync.RWMutex + Keys map[string]any +} + +// Reader 读取 +func (c *Client) Reader() { + defer func() { + c.Hub.Disconnect <- c + }() + + for { + request, op, err := wsutil.ReadClientData(c.Conn) + if err != nil { + c.Log("xx", "Error reading data", err.Error()) + return + } + + if op == ws.OpText { + req := string(request) + c.Log("<-", req) + go Dispatcher(c, req) + } else if op == ws.OpPing { + err = wsutil.WriteServerMessage(c.Conn, ws.OpPong, nil) + if err != nil { + c.Log("xx", "Reply pong", err.Error()) + } + } else { + c.Log("xx", "Unrecognized action") + } + } +} + +// Write 发送 +func (c *Client) Write() { + timer := time.NewTicker(5 * time.Second) + defer func() { + timer.Stop() + c.Hub.Disconnect <- c + }() + + for { + select { + case msg, ok := <-c.Send: + if !ok { + return + } + + err := wsutil.WriteServerMessage(c.Conn, ws.OpText, msg) + if err != nil { + c.Log("xx", "Send msg error", err.Error()) + return + } + + //如果设置为断开状态 + //在消息发送完成后将断开与服务器的连接 + if c.Disconnecting { + return + } + + c.Log("->", string(msg)) + case <-timer.C: + err := wsutil.WriteServerMessage(c.Conn, ws.OpPing, nil) + if err != nil { + c.Log("xx", "Error actively pinging the client", err.Error()) + return + } + + c.LastHeartbeatTime = time.Now() + if c.User != nil { + c.User.LastHeartbeatTime = c.LastHeartbeatTime + } + } + } +} + +// Log websocket日志 +func (c *Client) Log(symbol string, msg ...string) { + s := strings.Join(msg, ", ") + if c.IsLogin { + s = fmt.Sprintf("%s %s [%s-%s] %s", c.Conn.RemoteAddr(), symbol, c.User.Suid, c.AppId, s) + } else { + s = fmt.Sprintf("%s %s %s", c.Conn.RemoteAddr(), symbol, s) + } + + if len(s) > 300 { + logger.SugarLog.Info(s[:300] + "...") + } else { + logger.SugarLog.Info(s) + } +} + +// SendMsg 把消息加入发送队列 +func (c *Client) SendMsg(msg []byte) { + defer func() { + if err := recover(); err != nil { + c.Hub.Disconnect <- c + logger.SugarLog.Errorf("SendMsg recover error(%s): %s", c.IpConnAddr, err) + } + }() + + c.Send <- msg +} + +// SendRawMsg 构造消息再发送 +func (c *Client) SendRawMsg(code int, action, msg string, data any) { + a := &Action{ + Action: action, + Code: code, + Msg: msg, + Data: data, + } + + c.SendMsg(a.Encode()) +} + +// Close 关闭客户端 +func (c *Client) Close() { + defer func() { + if err := recover(); err != nil { + c.Log("xx", "recover!! -> ", fmt.Sprintf("%v", err)) + return + } + }() + + if !c.Closed { + //防止重复关闭 + c.Closed = true + + //关闭通道 + close(c.Send) + + //关闭网络连接 + _ = c.Conn.Close() + + //打印日志 + c.Log("xx", fmt.Sprintf("Close client -> %s", c.IpConnAddr)) + } +} diff --git a/ws/client_keyval.go b/ws/client_keyval.go new file mode 100644 index 0000000..1d62dab --- /dev/null +++ b/ws/client_keyval.go @@ -0,0 +1,22 @@ +package ws + +func (c *Client) SetKey(key string, value any) { + c.mu.Lock() + defer c.mu.Unlock() + if c.Keys == nil { + c.Keys = make(map[string]any) + } + + c.Keys[key] = value +} + +func (c *Client) GetKey(key string) *Value { + c.mu.RLock() + defer c.mu.RUnlock() + value, exists := c.Keys[key] + if !exists { + return &Value{} + } + + return &Value{data: value} +} diff --git a/ws/client_stack.go b/ws/client_stack.go new file mode 100644 index 0000000..a9e4d6f --- /dev/null +++ b/ws/client_stack.go @@ -0,0 +1,51 @@ +package ws + +import "sync" + +type ( + Stack struct { + top *node + length int + lock *sync.RWMutex + } + + node struct { + value any + prev *node + } +) + +func NewStack() *Stack { + return &Stack{ + top: nil, + length: 0, + lock: &sync.RWMutex{}, + } +} + +func (s *Stack) Len() int { + return s.length +} + +func (s *Stack) Pop() any { + s.lock.Lock() + defer s.lock.Unlock() + + if s.length == 0 { + return nil + } + + n := s.top + s.top = n.prev + s.length = s.length - 1 + return n.value +} + +func (s *Stack) Push(value any) { + s.lock.Lock() + defer s.lock.Unlock() + + n := &node{value: value, prev: s.top} + s.top = n + s.length = s.length + 1 +} diff --git a/ws/context.go b/ws/context.go new file mode 100644 index 0000000..4317f3f --- /dev/null +++ b/ws/context.go @@ -0,0 +1,34 @@ +package ws + +import ( + "math" +) + +type Context struct { + Client *Client + Action string + Params string + Response *Action + Server *Server + + index int8 + handlers HandlersChain + + language string + defaultLng string +} + +const abortIndex int8 = math.MaxInt8 / 2 + +func (c *Context) Next() { + c.index++ + for c.index < int8(len(c.handlers)) { + c.handlers[c.index](c) + c.index++ + } +} + +// Abort 放弃调用后续方法 +func (c *Context) Abort() { + c.index = abortIndex +} diff --git a/ws/context_binding.go b/ws/context_binding.go new file mode 100644 index 0000000..92106d9 --- /dev/null +++ b/ws/context_binding.go @@ -0,0 +1,40 @@ +package ws + +import ( + "encoding/json" + + "github.com/wonli/aqi/validate" +) + +func (c *Context) BindingJson(s any) error { + err := json.Unmarshal([]byte(c.Params), s) + if err != nil { + return err + } + + return nil +} + +func (c *Context) BindingJsonPath(s any, path string) error { + data := c.Get(path) + err := json.Unmarshal([]byte(data), s) + if err != nil { + return err + } + + return nil +} + +func (c *Context) BindingValidateJson(s any) error { + err := json.Unmarshal([]byte(c.Params), s) + if err != nil { + return err + } + + err = validate.Normal(c.language).Validate(s) + if err != nil { + return err + } + + return nil +} diff --git a/ws/context_i18n.go b/ws/context_i18n.go new file mode 100644 index 0000000..3790f8a --- /dev/null +++ b/ws/context_i18n.go @@ -0,0 +1,9 @@ +package ws + +func (c *Context) i18nLoad(code int, msg string) string { + return languageInit(c).load(code, msg) +} + +func (c *Context) i18nSet(code int, msg string) { + languageInit(c).set(code, msg) +} diff --git a/ws/context_lang.go b/ws/context_lang.go new file mode 100644 index 0000000..f482345 --- /dev/null +++ b/ws/context_lang.go @@ -0,0 +1,109 @@ +package ws + +import ( + "fmt" + "hash/fnv" + "os" + "sync" + + "gopkg.in/yaml.v3" + + "github.com/wonli/aqi/utils" + + "github.com/wonli/aqi/logger" +) + +var langMap sync.Map + +type langInfo struct { + ctx *Context + + langData map[string]string + filePath string +} + +func languageInit(ctx *Context) *langInfo { + lang := ctx.language + value, ok := langMap.Load(lang) + if ok { + return value.(*langInfo) + } + + languageFile := fmt.Sprintf("%s/i18n/%s.yaml", ctx.Server.dataPath, lang) + err := utils.CreateFileIfNotExists(languageFile) + if err != nil { + return nil + } + + file, err := os.ReadFile(languageFile) + if err != nil { + logger.SugarLog.Errorf("Failed to load language file:%s", err.Error()) + return nil + } + + res := &langInfo{ + ctx: ctx, + langData: make(map[string]string), + filePath: languageFile, + } + + if len(file) == 0 { + return res + } + + err = yaml.Unmarshal(file, &res.langData) + if err != nil { + logger.SugarLog.Errorf("Failed to unmarshal language file:%s", err.Error()) + return nil + } + + langMap.Store(lang, res) + return res +} + +func (info *langInfo) set(code int, msg string) { + mHash := info.getMsgHashKey(msg) + cacheKey := fmt.Sprintf("%s.%d.%s", info.ctx.Action, code, mHash) + _, ok := info.langData[cacheKey] + if !ok { + info.langData[cacheKey] = msg + + // 将更新后的语言包数据写入文件 + if err := writeLangFile(info.filePath, info.langData); err != nil { + logger.SugarLog.Errorf("Failed to update language file: %s", err.Error()) + } + } +} + +func (info *langInfo) load(code int, msg string) string { + mHash := info.getMsgHashKey(msg) + cacheKey := fmt.Sprintf("%s.%d.%s", info.ctx.Action, code, mHash) + s, ok := info.langData[cacheKey] + if ok { + return s + } + + return msg +} + +func (info *langInfo) getMsgHashKey(msg string) string { + h := fnv.New32a() + _, err := h.Write([]byte(msg)) + if err != nil { + logger.SugarLog.Errorf("Failed to retrieve hint information hash data:%s", err.Error()) + return "1" + } + + return fmt.Sprintf("%04d", h.Sum32()%10000) +} + +func writeLangFile(filePath string, data map[string]string) error { + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + encoder := yaml.NewEncoder(file) + return encoder.Encode(data) +} diff --git a/ws/context_lang_i18n_hash_test.go b/ws/context_lang_i18n_hash_test.go new file mode 100644 index 0000000..c2b1be9 --- /dev/null +++ b/ws/context_lang_i18n_hash_test.go @@ -0,0 +1,41 @@ +package ws + +import ( + "fmt" + "hash/fnv" + "log" + "testing" +) + +func TestHash(t *testing.T) { + + msgList := []string{ + "1", + "2", + "a", + "aa", + "123", + "12", + "hello", + "hella", + "用户注册失败", + "用户注册失败1", + "用户注册没失败", + "注册失败", + "失败", + "用户失败", + } + + for _, msg := range msgList { + h := fnv.New32a() + _, err := h.Write([]byte(msg)) + if err != nil { + return + } + + mHash := fmt.Sprintf("%03d", h.Sum32()%1000) + mHash1 := fmt.Sprintf("%04d", h.Sum32()%10000) + mHash2 := fmt.Sprintf("%03x", h.Sum32()%4096) + log.Printf("%s -> (%s, %s, %d) -> (%s,%d) \n", msg, mHash, mHash1, h.Sum32(), mHash2, h.Sum32()%4096) + } +} diff --git a/ws/context_params.go b/ws/context_params.go new file mode 100644 index 0000000..e4b2051 --- /dev/null +++ b/ws/context_params.go @@ -0,0 +1,93 @@ +package ws + +import ( + "encoding/json" + + "github.com/tidwall/gjson" +) + +func (c *Context) Get(key string) string { + return gjson.Get(c.Params, key).String() +} + +func (c *Context) GetInt(key string) int { + v := gjson.Get(c.Params, key).Int() + return int(v) +} + +func (c *Context) GetJson(s any) error { + return json.Unmarshal([]byte(c.Params), s) +} + +func (c *Context) GetSliceVal(key string, options ...string) string { + find := false + v := gjson.Get(c.Params, key).String() + for _, val := range options { + if v == val { + find = true + break + } + } + + if find { + return v + } + + return "" +} + +func (c *Context) GetPagination() *Pagination { + p := &Page{} + page := gjson.Get(c.Params, "page").String() + if page != "" { + _ = json.Unmarshal([]byte(page), &p) + } + + return InitPagination(p, 100) +} + +func (c *Context) GetSizePagination(pageSize int) *Pagination { + p := &Page{} + page := gjson.Get(c.Params, "page").String() + if page != "" { + _ = json.Unmarshal([]byte(page), &p) + } + + p.PageSize = pageSize + return InitPagination(p, 0) +} + +func (c *Context) GetMinInt(key string, min int) int { + d := c.GetInt(key) + if d < min { + return min + } + + return d +} + +func (c *Context) GetRangeInt(key string, min, max int) int { + d := c.GetInt(key) + if d < min { + return min + } + + if d > max { + return max + } + + return d +} + +func (c *Context) GetBool(key string) bool { + return gjson.Get(c.Params, key).Bool() +} + +func (c *Context) GetId(key string) uint { + v := gjson.Get(c.Params, key).Int() + if v > 0 { + return uint(v) + } + + return 0 +} diff --git a/ws/context_send.go b/ws/context_send.go new file mode 100644 index 0000000..f0592df --- /dev/null +++ b/ws/context_send.go @@ -0,0 +1,105 @@ +package ws + +// Send 发送数据给用户 +func (c *Context) Send(data any) { + msg := New(c.Action).WithData(data) + + c.Response = msg + c.Client.SendMsg(msg.Encode()) +} + +// SendOk 发送成功消息 +func (c *Context) SendOk() { + msg := New(c.Action) + + c.Response = msg + c.Client.SendMsg(msg.Encode()) +} + +// SendCode 发送状态消息 +func (c *Context) SendCode(code int, msg string) { + //开发模式下更新默认语言文件 + if c.Server.isDev && c.language == c.defaultLng { + c.i18nSet(code, msg) + } + + if c.language != c.defaultLng { + translate := c.i18nLoad(code, msg) + if translate != "" { + msg = translate + } + } + + m := New(c.Action).WithCode(code).WithMsg(msg) + + c.Response = m + c.Client.SendMsg(m.Encode()) +} + +// SendMsg 发送消息给当前用户 +func (c *Context) SendMsg(msg string) { + m := New(c.Action).WithMsg(msg) + + c.Response = m + c.Client.SendMsg(m.Encode()) +} + +// SendActionData 发送数据给当前用户 +func (c *Context) SendActionData(action string, data any) { + m := New(action).WithData(data) + + c.Response = m + c.Client.SendMsg(m.Encode()) +} + +// SendActionMsg 发送消息给当前用户 +func (c *Context) SendActionMsg(action, msg string) { + m := New(action).WithMsg(msg) + + c.Response = m + c.Client.SendMsg(m.Encode()) +} + +// SendTo 发送给指定用户 +func (c *Context) SendTo(uid, action string, data any) { + m := New(action).WithData(data) + c.Response = m + + user := c.Client.Hub.User(uid) + if user != nil { + user.SendMsg(m.Encode()) + } +} + +// SendToApp 发送消息给指定的app +func (c *Context) SendToApp(appId string, msg *Action) { + c.Response = msg + if c.Client.User != nil { + c.Client.User.SendMsgToApp(appId, msg.Encode()) + } +} + +// SendToApps 发送RAW消息给当前用户所有客户端 +func (c *Context) SendToApps(msg *Action) { + c.Response = msg + if c.Client.User != nil { + c.Client.User.SendMsg(msg.Encode()) + } else { + c.Client.SendMsg(msg.Encode()) + } +} + +// SendRawTo 发送RAW消息给指定用户 +func (c *Context) SendRawTo(uid string, msg *Action) { + c.Response = msg + user := c.Client.Hub.User(uid) + if user != nil { + user.SendMsg(msg.Encode()) + } +} + +// Broadcast 发送广播 +func (c *Context) Broadcast(msg *Action) { + c.Response = msg + c.Client.Hub.Broadcast(msg.Encode()) +} diff --git a/ws/context_value.go b/ws/context_value.go new file mode 100644 index 0000000..154dc79 --- /dev/null +++ b/ws/context_value.go @@ -0,0 +1,160 @@ +package ws + +import "time" + +type Value struct { + data any +} + +func (v *Value) Raw() any { + return v.data +} + +func (v *Value) String() string { + if s, ok := v.data.(string); ok { + return s + } + return "" +} + +func (v *Value) Bool() bool { + if b, ok := v.data.(bool); ok { + return b + } + return false +} + +func (v *Value) Int() int { + if i, ok := v.data.(int); ok { + return i + } + return 0 +} + +func (v *Value) Int8() int8 { + if i, ok := v.data.(int8); ok { + return i + } + return 0 +} + +func (v *Value) Int16() int16 { + if i, ok := v.data.(int16); ok { + return i + } + return 0 +} + +func (v *Value) Int32() int32 { + if i, ok := v.data.(int32); ok { + return i + } + return 0 +} + +func (v *Value) Int64() int64 { + if i, ok := v.data.(int64); ok { + return i + } + return 0 +} + +func (v *Value) Uint() uint { + if i, ok := v.data.(uint); ok { + return i + } + return 0 +} + +func (v *Value) Uint8() uint8 { + if i, ok := v.data.(uint8); ok { + return i + } + return 0 +} + +func (v *Value) Uint16() uint16 { + if i, ok := v.data.(uint16); ok { + return i + } + return 0 +} + +func (v *Value) Uint32() uint32 { + if i, ok := v.data.(uint32); ok { + return i + } + return 0 +} + +func (v *Value) Uint64() uint64 { + if i, ok := v.data.(uint64); ok { + return i + } + return 0 +} + +func (v *Value) Float32() float32 { + if f, ok := v.data.(float32); ok { + return f + } + return 0.0 +} + +func (v *Value) Float64() float64 { + if f, ok := v.data.(float64); ok { + return f + } + return 0.0 +} + +func (v *Value) Time() time.Time { + if t, ok := v.data.(time.Time); ok { + return t + } + + return time.Time{} +} + +func (v *Value) Duration() time.Duration { + if t, ok := v.data.(time.Duration); ok { + return t + } + + return 0 +} + +func (v *Value) Slice() []any { + if s, ok := v.data.([]any); ok { + return s + } + return nil +} + +func (v *Value) SliceString() []string { + if s, ok := v.data.([]string); ok { + return s + } + return nil +} + +func (v *Value) SliceInt() []int { + if s, ok := v.data.([]int); ok { + return s + } + return nil +} + +func (v *Value) Map() map[string]any { + if m, ok := v.data.(map[string]any); ok { + return m + } + return nil +} + +func (v *Value) StringMap() map[string]string { + if m, ok := v.data.(map[string]string); ok { + return m + } + return nil +} diff --git a/ws/data_api.go b/ws/data_api.go new file mode 100644 index 0000000..745cf7c --- /dev/null +++ b/ws/data_api.go @@ -0,0 +1,9 @@ +package ws + +type ApiData struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data any `json:"data,omitempty"` + + HttpStatus int +} diff --git a/ws/data_appid.go b/ws/data_appid.go new file mode 100644 index 0000000..de6a4d5 --- /dev/null +++ b/ws/data_appid.go @@ -0,0 +1,17 @@ +package ws + +type Appid int + +// 系统保留<200 +var sys Appid = 0 + +var ( + //appid范围200~999 + minAppid = 200 + maxAppid = 999 + + //code范围100~999 + minCode = 0 + maxCode = 999 + base = 1000 +) diff --git a/ws/data_error.go b/ws/data_error.go new file mode 100644 index 0000000..d315524 --- /dev/null +++ b/ws/data_error.go @@ -0,0 +1,64 @@ +package ws + +import ( + "fmt" +) + +var ( + ErrServerError = NewError(sys, 1100, "Server under maintenance") + ErrUncertified = NewError(sys, 1120, "Please log in first") + ErrCertificationExpired = NewError(sys, 1121, "Login has expired") + ErrJwtBLOCKED = NewError(sys, 1123, "Login has expired") + ErrParamsInvalid = NewError(sys, 1131, "Invalid parameters") + ErrAuthentic = NewError(sys, 1141, "Please login first") +) + +type Error ApiData + +var codes = map[int]string{} + +func NewError(appId Appid, code int, msg string) *Error { + if appId != sys { + a := int(appId) + if a < minAppid || a > maxAppid { + panic(fmt.Sprintf("error AppId %d", appId)) + } + + if code < minCode || code > maxCode { + panic(fmt.Sprintf("error code %d", code)) + } + + code = a*base + code + } + + if _, ok := codes[code]; ok { + panic(fmt.Sprintf("Error code %d already exists, please choose another one", code)) + } + + codes[code] = msg + return &Error{Code: code, Msg: msg} +} + +// WithMsg 覆盖业务错误提示内容 +func (e *Error) WithMsg(msg string) *Error { + e.Msg = msg + return e +} + +// WithError 兼容Binding错误码及多语言翻译 +// 使用前需要调用 validate.GinValidator() 初始化 +// 字段中文名称使用 `label:"名称"` 指定 +func (e *Error) WithError(err error) *Error { + if err == nil { + return e + } + + e.Msg = BindingErrors(err).Error() + return e +} + +// WithHttpStatus 处理HTTP状态码 +func (e *Error) WithHttpStatus(status int) *Error { + e.HttpStatus = status + return e +} diff --git a/ws/data_h.go b/ws/data_h.go new file mode 100644 index 0000000..56ce146 --- /dev/null +++ b/ws/data_h.go @@ -0,0 +1,62 @@ +package ws + +import ( + "encoding/json" + + "go.uber.org/zap" + + "github.com/wonli/aqi/logger" +) + +// H 类似gin.H +type H map[string]any + +func (h *H) Get(key string) (any, bool) { + if *h == nil { + return nil, false + } + + val, ok := (*h)[key] + return val, ok +} + +func (h *H) Set(key string, val any) { + if *h == nil { + *h = make(map[string]any) + } + + (*h)[key] = val +} + +func (h *H) Unmarshal(v any) error { + d, err := json.Marshal(h) + if err != nil { + logger.SugarLog.Error("构造参数失败", + zap.String("error", err.Error()), + ) + return err + } + + err = json.Unmarshal(d, v) + if err != nil { + logger.SugarLog.Error("解析参数失败", + zap.String("error", err.Error()), + ) + return err + } + + return nil +} + +func (h *H) Marshal() []byte { + d, err := json.Marshal(h) + if err != nil { + logger.SugarLog.Error("构造参数失败", + zap.String("error", err.Error()), + ) + + return d + } + + return d +} diff --git a/ws/data_paginate.go b/ws/data_paginate.go new file mode 100644 index 0000000..860be67 --- /dev/null +++ b/ws/data_paginate.go @@ -0,0 +1,45 @@ +package ws + +type Page struct { + Current int `json:"current"` + PageSize int `json:"pageSize"` + Total int64 `json:"total"` +} + +type Pagination struct { + *Page `json:"page"` + + Rows any `json:"rows,omitempty"` + ExData H `json:"exData,omitempty"` + + Limit int `json:"-"` //PageSize alias + Offset int `json:"-"` //避免每次计算offset +} + +func InitPagination(p *Page, maxSize int) *Pagination { + if p.Current <= 1 { + p.Current = 1 + } + + if p.PageSize <= 0 { + p.PageSize = 10 + } + + if maxSize > 0 && p.PageSize > maxSize { + p.PageSize = maxSize + } + + return &Pagination{ + Page: p, + Offset: (p.Current - 1) * p.PageSize, + Limit: p.PageSize, + } +} + +func (p *Pagination) AddExData(key string, val any) { + if p.ExData == nil { + p.ExData = H{} + } + + p.ExData[key] = val +} diff --git a/ws/dispatcher.go b/ws/dispatcher.go new file mode 100644 index 0000000..0212516 --- /dev/null +++ b/ws/dispatcher.go @@ -0,0 +1,63 @@ +package ws + +import ( + "time" + + "github.com/tidwall/gjson" +) + +func Dispatcher(c *Client, request string) { + t := time.Now() + //ping直接回应 + action := gjson.Get(request, "action").String() + if action == "ping" { + c.LastHeartbeatTime = t + c.SendRawMsg(0, "ping", "pong", nil) + return + } + + //是否被禁言 + if c.User != nil { + isBanned, bandTime := c.User.IsBanned() + if isBanned { + c.SendRawMsg(21, "sys.ban", "You have been ban", bandTime) + return + } + } + + //请求频率限制5毫秒 + if t.Sub(c.LastRequestTime).Microseconds() <= 5 { + c.SendRawMsg(23, "sys.requestLimit", "Your requests are too frequent", nil) + return + } else { + //更新最后请求时间 + c.LastRequestTime = t + + //如果心跳时间为0,设置为当前时间 + //防止在连接瞬间被哨兵扫描而断开 + if c.LastHeartbeatTime.IsZero() { + c.LastHeartbeatTime = t + } + } + + handlers := InitManager().Handlers(action) + if handlers == nil || len(handlers) == 0 { + c.SendRawMsg(25, action, "Request not supported", nil) + return + } + + ctx := &Context{ + Client: c, + Action: action, + Params: gjson.Get(request, "params").String(), + Server: wss, + + handlers: handlers, + + language: "zh", + defaultLng: "zh", + } + + ctx.handlers[0](ctx) + ctx.Next() +} diff --git a/ws/hubc.go b/ws/hubc.go new file mode 100644 index 0000000..c763d16 --- /dev/null +++ b/ws/hubc.go @@ -0,0 +1,151 @@ +package ws + +import ( + "sync" + "time" + + "golang.org/x/exp/slices" +) + +var Hub *Hubc + +type Hubc struct { + //访客列表 + Guests []*Client + + //已登录用户 map[string]*User + Users *sync.Map + + //用户数统计 + LoginCount int + GuestCount int + + //发布订阅 + PubSub *PubSub + + //登录和断开通道 + Connection chan *Client + Disconnect chan *Client +} + +func NewHubc() *Hubc { + Hub = &Hubc{ + PubSub: NewPubSub(), + Guests: []*Client{}, + Users: new(sync.Map), + Connection: make(chan *Client), + Disconnect: make(chan *Client), + } + + return Hub +} + +func (h *Hubc) Run() { + go h.PubSub.Start() + go h.guard() + + for { + select { + case c := <-h.Connection: + h.Guests = append(h.Guests, c) + h.PubSub.Pub("connect", c) + c.Log("--", "connection") + + case c := <-h.Disconnect: + if c.User != nil { + err := c.User.appLogout(c.AppId, c) + if err != nil { + c.Log("--", "user disconnect") + } + } else { + c.Close() + h.removeFromGuests(c) + } + } + } +} + +func (h *Hubc) guard() { + timer := time.NewTicker(30 * time.Second) + for range timer.C { + userCount := 0 + guestCount := len(h.Guests) + h.Users.Range(func(key, value any) bool { + userCount++ + return true + }) + + //登录用户数 + h.LoginCount = userCount + h.GuestCount = guestCount + + //发布订阅消息 + h.PubSub.Pub("userCount", userCount) + h.PubSub.Pub("guestsCount", guestCount) + } +} + +// Broadcast 发送广播消息 +func (h *Hubc) Broadcast(msg []byte) { + for _, g := range h.Guests { + g.SendMsg(msg) + } + + if h.Users != nil { + h.Users.Range(func(key, value any) bool { + user, ok := value.(*User) + if ok && user != nil { + user.SendMsg(msg) + } + + return true + }) + } +} + +// User 获取用户信息 +func (h *Hubc) User(uid string) *User { + user, ok := h.Users.Load(uid) + if ok { + return user.(*User) + } + + return nil +} + +// UserClient 获取用户客户端信息 +func (h *Hubc) UserClient(uid, appId string) *Client { + user := h.User(uid) + if user != nil { + return user.AppClient(appId) + } + + return nil +} + +// UserLogin 用户登录 +func (h *Hubc) UserLogin(uid, appId string, client *Client) error { + user := h.User(uid) + if user == nil { + user = NewUser(uid) + } + + //app登录 + err := user.appLogin(appId, client) + if err != nil { + return err + } + + //保存用户 + h.Users.Store(uid, user) + h.removeFromGuests(client) + return nil +} + +// 从访客列表中删除 +func (h *Hubc) removeFromGuests(client *Client) { + index := slices.Index(h.Guests, client) + if index > -1 { + h.Guests = slices.Delete(h.Guests, index, index+1) + } +} diff --git a/ws/manager.go b/ws/manager.go new file mode 100644 index 0000000..4a26738 --- /dev/null +++ b/ws/manager.go @@ -0,0 +1,38 @@ +package ws + +import ( + "sync" +) + +type ActionManager struct { + handlerMap map[string]HandlersChain +} + +var msy sync.Once +var manager *ActionManager + +func InitManager() *ActionManager { + msy.Do(func() { + manager = &ActionManager{ + handlerMap: map[string]HandlersChain{}, + } + + //处理websocket + go NewHubc().Run() + }) + + return manager +} + +func (m *ActionManager) Add(name string, router HandlersChain) { + m.handlerMap[name] = router +} + +func (m *ActionManager) Has(name string) bool { + _, ok := m.handlerMap[name] + return ok +} + +func (m *ActionManager) Handlers(name string) HandlersChain { + return m.handlerMap[name] +} diff --git a/ws/pubsub.go b/ws/pubsub.go new file mode 100644 index 0000000..dadab06 --- /dev/null +++ b/ws/pubsub.go @@ -0,0 +1,90 @@ +package ws + +import ( + "sync" + + "github.com/wonli/aqi/logger" +) + +type PubSub struct { + Topics *sync.Map //Topics map[string]*Topic //主题名称和Top对应map + TopicMsgQueue chan *TopicMsg //主题消息队列 +} + +func NewPubSub() *PubSub { + return &PubSub{ + Topics: new(sync.Map), + TopicMsgQueue: make(chan *TopicMsg, 128), + } +} + +func (a *PubSub) initTopic(topicId string) *Topic { + //主题不存在时先创建主题 + topic, ok := a.Topics.Load(topicId) + if !ok { + t := &Topic{ + Id: topicId, + PubSub: a, + SubUsers: sync.Map{}, + SubHandlers: sync.Map{}, + } + + a.Topics.Store(topicId, t) + return t + } + + return topic.(*Topic) +} + +// Pub 发布主题 +func (a *PubSub) Pub(topicId string, data any) { + msg := Action{ + Action: "subscriber", + Data: H{ + "topicId": topicId, + "message": data, + }, + } + + //主题不存在时先创建主题 + a.initTopic(topicId) + a.TopicMsgQueue <- &TopicMsg{ + Ori: data, + TopicId: topicId, + Msg: msg.Encode(), + } +} + +// Sub 订阅主题 +func (a *PubSub) Sub(topicId string, user *User) { + a.initTopic(topicId).AddSubUser(user) +} + +// SubFunc 以函数方式订阅 +func (a *PubSub) SubFunc(topicId string, f func(msg *TopicMsg)) { + a.initTopic(topicId).AddSubHandle(f) +} + +func (a *PubSub) Start() { + for { + select { + case msg, ok := <-a.TopicMsgQueue: + if !ok { + logger.SugarLog.Info("从订阅主题队列取数据失败") + continue + } + + t, hasTopic := a.Topics.Load(msg.TopicId) + if !hasTopic { + logger.SugarLog.Info("未发布订阅主题收到消息") + continue + } + + //订阅消息的函数处理 + t.(*Topic).ApplyFunc(msg) + + //订阅消息的用户处理 + t.(*Topic).SendToSubUser(msg.Msg) + } + } +} diff --git a/ws/pubsub_topic.go b/ws/pubsub_topic.go new file mode 100644 index 0000000..ce7f016 --- /dev/null +++ b/ws/pubsub_topic.go @@ -0,0 +1,42 @@ +package ws + +import ( + "sync" + "time" +) + +type Topic struct { + Id string //订阅主题ID + PubSub *PubSub //关联PubSub + SubUsers sync.Map //SubUsers map[string]*time.Time //订阅用户uniqueId和订阅时间 + SubHandlers sync.Map //SubHandlers map[string]func(msg *TopicMsg) //内部组件间通知 +} + +func (a *Topic) AddSubUser(user *User) { + user.AddSubTopic(a) + a.SubUsers.LoadOrStore(user.Suid, time.Now()) +} + +func (a *Topic) AddSubHandle(f func(msg *TopicMsg)) { + a.SubHandlers.LoadOrStore(a.Id, f) +} + +func (a *Topic) SendToSubUser(msg []byte) { + a.SubUsers.Range(func(key, value any) bool { + uniqueId := key.(string) + user := Hub.User(uniqueId) + if user != nil { + user.SendMsg(msg) + } + + return true + }) +} + +func (a *Topic) ApplyFunc(msg *TopicMsg) { + a.SubHandlers.Range(func(key, value any) bool { + f := value.(func(msg *TopicMsg)) + f(msg) + return true + }) +} diff --git a/ws/pubsub_topic_msg.go b/ws/pubsub_topic_msg.go new file mode 100644 index 0000000..f0112b6 --- /dev/null +++ b/ws/pubsub_topic_msg.go @@ -0,0 +1,7 @@ +package ws + +type TopicMsg struct { + Ori any //原始数据方便订阅主题的函数处理 + TopicId string //话题ID + Msg []byte //消息内容,方便客户端处理 +} diff --git a/ws/router.go b/ws/router.go new file mode 100644 index 0000000..2fdf35e --- /dev/null +++ b/ws/router.go @@ -0,0 +1,52 @@ +package ws + +import ( + "strings" +) + +type HandlerFunc func(a *Context) +type HandlersChain []HandlerFunc + +type IRouter interface { + Use(middleware ...HandlerFunc) IRouter + Group(name string) IRouter + Add(name string, fn ...HandlerFunc) +} + +type Routers struct { + manager *ActionManager + handlerMembers HandlersChain + groups []string +} + +func NewRouter() Routers { + return Routers{ + manager: InitManager(), + } +} + +func (r Routers) Add(name string, fn ...HandlerFunc) { + if r.groups != nil { + name = strings.Join(r.groups, ".") + "." + name + } + + has := r.manager.Has(name) + if has { + panic("Duplicate route: " + name) + } + + chains := make(HandlersChain, len(r.handlerMembers), len(r.handlerMembers)+len(fn)) + copy(chains, r.handlerMembers) + + r.manager.Add(name, append(chains, fn...)) +} + +func (r Routers) Use(middleware ...HandlerFunc) IRouter { + r.handlerMembers = append(r.handlerMembers, middleware...) + return r +} + +func (r Routers) Group(name string) IRouter { + r.groups = append(r.groups, name) + return r +} diff --git a/ws/server.go b/ws/server.go new file mode 100644 index 0000000..8eb02da --- /dev/null +++ b/ws/server.go @@ -0,0 +1,61 @@ +package ws + +import ( + "net/http" + "sync" +) + +type Server struct { + engine http.Handler + + fn http.HandlerFunc + + port string + isDev bool + dataPath string +} + +var ( + wss *Server + once sync.Once +) + +func NewServer(engine http.Handler) *Server { + once.Do(func() { + InitManager() + wss = &Server{ + engine: engine, + port: ":3322", + fn: HttpHandler, + } + }) + + return wss +} + +func (s *Server) Handler(fn http.HandlerFunc) { + s.fn = fn +} + +func (s *Server) SetPort(p string) { + s.port = p +} + +func (s *Server) SetDataPath(p string) { + s.dataPath = p +} + +func (s *Server) SetIsDev(dev bool) { + s.isDev = dev +} + +func (s *Server) Init() { + +} + +func (s *Server) Run() { + err := http.ListenAndServe(s.port, s.engine) + if err != nil { + panic(err) + } +} diff --git a/ws/server_handler_http.go b/ws/server_handler_http.go new file mode 100644 index 0000000..7993ba8 --- /dev/null +++ b/ws/server_handler_http.go @@ -0,0 +1,82 @@ +package ws + +import ( + "net" + "net/http" + "strings" + "time" + + "github.com/gobwas/ws" + "go.uber.org/zap" + + "github.com/wonli/aqi/logger" +) + +func getRealIP(r *http.Request) string { + // Check if behind a proxy + xForwardedFor := r.Header.Get("X-Forwarded-For") + if xForwardedFor != "" { + // This header can contain multiple IPs separated by comma + // The first one is the original IP + parts := strings.Split(xForwardedFor, ",") + for i, p := range parts { + parts[i] = strings.TrimSpace(p) + } + return parts[0] // Return the first IP which is the client's real IP + } + + // If the X-Real-IP header is set, then use it + xRealIP := r.Header.Get("X-Real-IP") + if xRealIP != "" { + return xRealIP + } + + // Fallback to using RemoteAddr + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return r.RemoteAddr // In case there was an issue parsing, just return the whole thing + } + + return ip +} + +func HttpHandler(w http.ResponseWriter, r *http.Request) { + u := ws.HTTPUpgrader{ + Protocol: func(s string) bool { + return true + }, + } + + conn, _, h, err := u.Upgrade(r, w) + if err != nil { + logger.SugarLog.Error("UpgradeHTTP", + zap.String("error", err.Error()), + ) + return + } + + if h.Protocol != "" { + r.Header.Set("Sec-Websocket-Protocol", h.Protocol) + } + + addr, ok := conn.RemoteAddr().(*net.TCPAddr) + if !ok { + logger.SugarLog.Errorf("获取IP地址错误") + return + } + + c := &Client{ + Hub: Hub, + Conn: conn, + Send: make(chan []byte, 32), + IpAddress: getRealIP(r), + IpConnAddr: addr.String(), + ConnectionTime: time.Now(), + Endpoint: r.RequestURI, + } + + c.Hub.Connection <- c + go c.Reader() + go c.Write() + +} diff --git a/ws/user.go b/ws/user.go new file mode 100644 index 0000000..45f32ce --- /dev/null +++ b/ws/user.go @@ -0,0 +1,181 @@ +package ws + +import ( + "sync" + "time" + + "golang.org/x/exp/slices" +) + +type User struct { + //公共基础信息 + Uid uint `json:"uid"` //整型唯一ID + Suid string `json:"suid"` //字符唯一ID + GroupId string `json:"groupId"` //分组ID + SuperAdmin bool `json:"superAdmin"` //是否超管 + RoleId []uint `json:"roleId,omitempty"` //用户角色 + Nickname string `json:"nickname"` //昵称 + Avatar *Resource `json:"avatar"` //用户头像 + OnlineStatus UserOnlineStatus `json:"onlineStatus"` //在线状态 + Location *Location `json:"location,omitempty"` //地理位置 + + CurrentWindowId string //当前的窗口ID + + //禁言时间 + Ban *time.Time `json:"ban,omitempty"` + + //最后心跳时间 + LastHeartbeatTime time.Time + + //用户相关数据 + Hub *Hubc `json:"-"` + AppClients []*Client `json:"-"` //appId对应客户端 + + SubTopics map[string]*Topic `json:"-"` //topicId订阅的主题名称及信息 + sync.RWMutex +} + +func NewUser(uid string) *User { + user := &User{ + Suid: uid, + Hub: Hub, + AppClients: []*Client{}, + + SubTopics: make(map[string]*Topic), + } + + return user +} + +func (u *User) AddSubTopic(topic *Topic) int { + u.Lock() + defer u.Unlock() + + u.SubTopics[topic.Id] = topic + return len(u.SubTopics) +} + +func (u *User) UnsubTopic(topicId string) int { + u.Lock() + defer u.Unlock() + + _, ok := u.SubTopics[topicId] + if ok { + delete(u.SubTopics, topicId) + } + + return len(u.SubTopics) +} + +// AppLogin 用户APP客户端登录 +func (u *User) appLogin(appId string, client *Client) error { + var index int + var appClient *Client + for i, app := range u.AppClients { + if app.AppId == appId { + index = i + appClient = app + break + } + } + + client.User = u + client.AppId = appId + client.IsLogin = true + if appClient != nil { + if appClient.Conn != client.Conn { + u.AppClients = slices.Delete(u.AppClients, index, index+1) + u.AppClients = append(u.AppClients, client) + + //已登录连接下线 + u.Hub.Disconnect <- appClient + } + } else { + u.AppClients = append(u.AppClients, client) + } + + return nil +} + +// app退出 +func (u *User) appLogout(appId string, logoutClient *Client) error { + removeIndex := -1 + for appIndex, appClient := range u.AppClients { + if appClient.AppId == appId && logoutClient.Conn == appClient.Conn { + removeIndex = appIndex + break + } + } + + if removeIndex > -1 { + //从客户端中移除 + u.AppClients = slices.Delete(u.AppClients, removeIndex, removeIndex+1) + + //关闭客户端 + logoutClient.Close() + } + + return nil +} + +// AppClient 获取APP客户端 +func (u *User) AppClient(appId string) *Client { + for _, app := range u.AppClients { + cc := app + if cc.AppId == appId { + return cc + } + } + + return nil +} + +// IsBanned 是否被封禁 +func (u *User) IsBanned() (bool, *time.Time) { + if u.Ban == nil || u.Ban.IsZero() { + return false, nil + } + + return true, u.Ban +} + +// Banned 禁言用户 +func (u *User) Banned(t time.Duration) *time.Time { + banTime := time.Now().Add(t) + u.Ban = &banTime + return u.Ban +} + +// Unban 禁言解除 +func (u *User) Unban() *time.Time { + u.Ban = nil + return u.Ban +} + +// IsOnline 用户是否在线 +func (u *User) IsOnline() bool { + if u == nil || u.AppClients == nil { + return false + } + + return len(u.AppClients) > 0 +} + +// SendMsg 发送消息 +func (u *User) SendMsg(msg []byte) { + if u == nil { + return + } + + for _, client := range u.AppClients { + client.SendMsg(msg) + } +} + +// SendMsgToApp 发送消息到指定客户端 +func (u *User) SendMsgToApp(appId string, msg []byte) { + client := u.AppClient(appId) + if client != nil { + client.SendMsg(msg) + } +} diff --git a/ws/user_location.go b/ws/user_location.go new file mode 100644 index 0000000..44d5af6 --- /dev/null +++ b/ws/user_location.go @@ -0,0 +1,16 @@ +package ws + +import "time" + +type Location struct { + Latitude float64 `json:"latitude" validate:"required"` + Longitude float64 `json:"longitude" validate:"required"` + AdInfo []string `json:"adInfo" validate:"required"` + RegionId string `json:"regionId" validate:"required"` + CityCode string `json:"cityCode" validate:"required"` + Address string `json:"address" validate:"required"` + + SelectCityCode string `json:"selectCityCode"` //选择的城市编码 + SelectCityName string `json:"selectCityName"` //选择的城市名称 + LatestUpdateAt time.Time +} diff --git a/ws/user_resource.go b/ws/user_resource.go new file mode 100644 index 0000000..3a1c465 --- /dev/null +++ b/ws/user_resource.go @@ -0,0 +1,11 @@ +package ws + +import "gorm.io/datatypes" + +type Resource struct { + Path string `json:"path,omitempty"` // 文件路径 + CdnUrl string `json:"cdnUrl,omitempty"` // cdn地址 + Scenes string `json:"scenes"` // 使用场景 + MimeType string `json:"mimeType,omitempty"` // 文件类型 + ExtData datatypes.JSON `json:"extData,omitempty"` +} diff --git a/ws/user_status.go b/ws/user_status.go new file mode 100644 index 0000000..c3e0790 --- /dev/null +++ b/ws/user_status.go @@ -0,0 +1,13 @@ +package ws + +type UserOnlineStatus byte + +const ( + UserStatusOnline UserOnlineStatus = iota + UserStatusBusy + UserStatusLeaving +) + +func IsValidStatus(s UserOnlineStatus) bool { + return s <= UserStatusLeaving +} diff --git a/ws/w.go b/ws/w.go new file mode 100755 index 0000000..15433e3 --- /dev/null +++ b/ws/w.go @@ -0,0 +1,34 @@ +package ws + +import ( + "encoding/json" + + "go.uber.org/zap" + + "github.com/wonli/aqi/logger" +) + +// Action Websocket通讯协议 +type Action struct { + Action string `json:"action"` + + Code int `json:"code"` + Msg string `json:"msg,omitempty"` + Data any `json:"data,omitempty"` +} + +func (m *Action) Encode() []byte { + return m.json() +} + +// JSON 格式化 +func (m *Action) json() []byte { + r, err := json.Marshal(m) + if err != nil { + logger.SugarLog.Error("JSON格式化失败", + zap.String("error", err.Error()), + ) + } + + return r +} diff --git a/ws/w_response.go b/ws/w_response.go new file mode 100644 index 0000000..45a4abb --- /dev/null +++ b/ws/w_response.go @@ -0,0 +1,42 @@ +package ws + +import ( + "fmt" + + "github.com/gin-gonic/gin" +) + +type Response struct { + Ctx *ApiData + + g *gin.Context + httpCode int +} + +func (r *Response) WithData(data any) *Response { + r.Ctx.Data = data + return r +} + +func (r *Response) WithError(e Error, err error) *Response { + r.Ctx.Code = e.Code + if err != nil { + r.Ctx.Msg = fmt.Sprintf("%s,%s", e.Msg, err.Error()) + } else { + r.Ctx.Msg = e.Msg + } + + return r +} + +func (r *Response) Send() { + r.g.JSON(r.httpCode, r.Ctx) +} + +func NewResponse(g *gin.Context, httpCode int) *Response { + return &Response{ + g: g, + httpCode: httpCode, + Ctx: &ApiData{}, + } +} diff --git a/ws/w_util.go b/ws/w_util.go new file mode 100644 index 0000000..4acbf26 --- /dev/null +++ b/ws/w_util.go @@ -0,0 +1,29 @@ +package ws + +func Msg(action, msg string) []byte { + res := &Action{ + Action: action, + Msg: msg, + } + + return res.json() +} + +func Code(action string, code int, msg string) []byte { + res := &Action{ + Action: action, + Code: code, + Msg: msg, + } + + return res.json() +} + +func Data(action string, data any) []byte { + res := &Action{ + Action: action, + Data: data, + } + + return res.json() +} diff --git a/ws/w_util_binding.go b/ws/w_util_binding.go new file mode 100644 index 0000000..ff518b7 --- /dev/null +++ b/ws/w_util_binding.go @@ -0,0 +1,12 @@ +package ws + +import "github.com/wonli/aqi/validate" + +// BindingErrors 处理错误信息 +func BindingErrors(e error) error { + if validate.GinBinding == nil { + return e + } + + return validate.GinBinding.Translator(e) +} diff --git a/ws/w_util_chain.go b/ws/w_util_chain.go new file mode 100644 index 0000000..6bcb778 --- /dev/null +++ b/ws/w_util_chain.go @@ -0,0 +1,22 @@ +package ws + +func New(action string) *Action { + return &Action{ + Action: action, + } +} + +func (m *Action) WithCode(code int) *Action { + m.Code = code + return m +} + +func (m *Action) WithData(data any) *Action { + m.Data = data + return m +} + +func (m *Action) WithMsg(msg string) *Action { + m.Msg = msg + return m +}