diff --git a/model/gmodel/fs_feishu_user_gen.go b/model/gmodel/fs_feishu_user_gen.go deleted file mode 100644 index b1922e6a..00000000 --- a/model/gmodel/fs_feishu_user_gen.go +++ /dev/null @@ -1,44 +0,0 @@ -package gmodel - -import ( - "gorm.io/gorm" - "time" -) - -// fs_feishu_user 飞书用户信息表 -type FsFeishuUser struct { - Id int64 `gorm:"primary_key;default:0;auto_increment;" json:"id"` // ID - AppId *string `gorm:"default:'';" json:"app_id"` // - OpenId *string `gorm:"default:'';" json:"open_id"` // - UnionId *string `gorm:"default:'';" json:"union_id"` // - Name *string `gorm:"default:'';" json:"name"` // - EnName *string `gorm:"default:'';" json:"en_name"` // - Nickname *string `gorm:"default:'';" json:"nickname"` // - Email *string `gorm:"default:'';" json:"email"` // - EnterpriseEmail *string `gorm:"default:'';" json:"enterprise_email"` // - JobTitle *string `gorm:"default:'';" json:"job_title"` // - Mobile *string `gorm:"default:'';" json:"mobile"` // - Gender *int64 `gorm:"default:0;" json:"gender"` // 性别 0未知 1男 2女 - Avatar *[]byte `gorm:"default:'';" json:"avatar"` // - IsFrozen *int64 `gorm:"default:0;" json:"is_frozen"` // 是否冻结 - IsResigned *int64 `gorm:"default:0;" json:"is_resigned"` // 是否离职 - IsActivated *int64 `gorm:"default:0;" json:"is_activated"` // 是否激活 - IsExited *int64 `gorm:"default:0;" json:"is_exited"` // 是否主动退出 - IsUnjoin *int64 `gorm:"default:0;" json:"is_unjoin"` // 是否未加入 - DepartmentIds *[]byte `gorm:"default:'';" json:"department_ids"` // - WorkStation *string `gorm:"default:'';" json:"work_station"` // - EmployeeNo *string `gorm:"default:'';" json:"employee_no"` // - EmployeeType *int64 `gorm:"default:0;" json:"employee_type"` // 0:未设置 1:正式员工 2:实习生 3:外包 4:劳务 5:顾问 - Orders *[]byte `gorm:"default:'';" json:"orders"` // - JoinTime *time.Time `gorm:"default:'0000-00-00 00:00:00';" json:"join_time"` // - Ctime *time.Time `gorm:"default:'0000-00-00 00:00:00';" json:"ctime"` // - Utime *time.Time `gorm:"default:'0000-00-00 00:00:00';" json:"utime"` // -} -type FsFeishuUserModel struct { - db *gorm.DB - name string -} - -func NewFsFeishuUserModel(db *gorm.DB) *FsFeishuUserModel { - return &FsFeishuUserModel{db: db, name: "fs_feishu_user"} -} diff --git a/model/gmodel/fs_feishu_user_logic.go b/model/gmodel/fs_feishu_user_logic.go deleted file mode 100644 index b2d7f45b..00000000 --- a/model/gmodel/fs_feishu_user_logic.go +++ /dev/null @@ -1,31 +0,0 @@ -package gmodel - -import ( - "context" - "errors" - "gorm.io/gorm" -) - -// TODO: 使用model的属性做你想做的 - -func (u *FsFeishuUserModel) Create(ctx context.Context, data *FsFeishuUser) error { - return u.db.WithContext(ctx).Model(&FsFeishuUser{}).Create(&data).Error -} -func (u *FsFeishuUserModel) Update(ctx context.Context, data *FsFeishuUser) error { - return u.db.WithContext(ctx).Model(&FsFeishuUser{}).Where("`app_id` = ? and `open_id` = ?", data.AppId, data.OpenId).Updates(&data).Error -} -func (u *FsFeishuUserModel) Find(ctx context.Context, appId, openId string) (resp *FsFeishuUser, err error) { - err = u.db.WithContext(ctx).Model(&FsFeishuUser{}).Where("`app_id` = ? and `open_id` = ?", appId, openId).Take(&resp).Error - return resp, err -} -func (u *FsFeishuUserModel) CreateOrUpdate(ctx context.Context, appId, openId string, data *FsFeishuUser) error { - _, err := u.Find(ctx, appId, openId) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return u.Create(ctx, data) - } - return err - } - data.Ctime = nil - return u.Update(ctx, data) -} diff --git a/model/gmodel/var_gen.go b/model/gmodel/var_gen.go index 1e8270c9..570c33ef 100644 --- a/model/gmodel/var_gen.go +++ b/model/gmodel/var_gen.go @@ -49,7 +49,6 @@ type AllModelsGen struct { FsFactoryProduct *FsFactoryProductModel // fs_factory_product 工厂生产表(废弃) FsFactoryShipTmp *FsFactoryShipTmpModel // fs_factory_ship_tmp FsFaq *FsFaqModel // fs_faq 常见问题 - FsFeishuUser *FsFeishuUserModel // fs_feishu_user 飞书用户信息表 FsFeishuWebhookLog *FsFeishuWebhookLogModel // fs_feishu_webhook_log 飞书webhook记录表 FsFont *FsFontModel // fs_font 字体配置 FsGerent *FsGerentModel // fs_gerent 管理员表 @@ -169,7 +168,6 @@ func NewAllModels(gdb *gorm.DB) *AllModelsGen { FsFactoryProduct: NewFsFactoryProductModel(gdb), FsFactoryShipTmp: NewFsFactoryShipTmpModel(gdb), FsFaq: NewFsFaqModel(gdb), - FsFeishuUser: NewFsFeishuUserModel(gdb), FsFeishuWebhookLog: NewFsFeishuWebhookLogModel(gdb), FsFont: NewFsFontModel(gdb), FsGerent: NewFsGerentModel(gdb), diff --git a/server/feishu-sync/etc/feishu-sync.yaml b/server/feishu-sync/etc/feishu-sync.yaml deleted file mode 100644 index 03a43b25..00000000 --- a/server/feishu-sync/etc/feishu-sync.yaml +++ /dev/null @@ -1,10 +0,0 @@ -Name: feishu-sync -Host: 0.0.0.0 -Port: 9925 -Timeout: 15000 #服务超时时间(毫秒) -SourceMysql: fsreaderwriter:XErSYmLELKMnf3Dh@tcp(fusen.cdmigcvz3rle.us-east-2.rds.amazonaws.com:3306)/fusen -SourceRabbitMq: "" -Auth: - AccessSecret: fusen2023 - AccessExpire: 2592000 - RefreshAfter: 1592000 diff --git a/server/feishu-sync/feishu-sync.go b/server/feishu-sync/feishu-sync.go deleted file mode 100644 index 140e58b8..00000000 --- a/server/feishu-sync/feishu-sync.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "net/http" - "time" - - "fusenapi/utils/auth" - "fusenapi/utils/fsconfig" - - "fusenapi/server/feishu-sync/internal/config" - "fusenapi/server/feishu-sync/internal/handler" - "fusenapi/server/feishu-sync/internal/svc" - - "github.com/zeromicro/go-zero/rest" -) - -var configFile = flag.String("f", "etc/feishu-sync.yaml", "the config file") - -func main() { - flag.Parse() - - var c config.Config - fsconfig.StartNacosConfig(*configFile, &c, nil) - - c.Timeout = int64(time.Second * 15) - server := rest.MustNewServer(c.RestConf, rest.WithCustomCors(auth.FsCors, func(w http.ResponseWriter) { - })) - defer server.Stop() - - ctx := svc.NewServiceContext(c) - handler.RegisterHandlers(server, ctx) - - fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port) - server.Start() -} diff --git a/server/feishu-sync/internal/config/config.go b/server/feishu-sync/internal/config/config.go deleted file mode 100644 index 1224b8b8..00000000 --- a/server/feishu-sync/internal/config/config.go +++ /dev/null @@ -1,18 +0,0 @@ -package config - -import ( - "fusenapi/server/feishu-sync/internal/types" - "github.com/zeromicro/go-zero/rest" -) - -type Config struct { - rest.RestConf - SourceMysql string - Auth types.Auth - SourceRabbitMq string - FeiShu struct { - ApiHost string - EncryptKey string - VerificationToken string - } -} diff --git a/server/feishu-sync/internal/handler/routes.go b/server/feishu-sync/internal/handler/routes.go deleted file mode 100644 index db0e1b4c..00000000 --- a/server/feishu-sync/internal/handler/routes.go +++ /dev/null @@ -1,22 +0,0 @@ -// Code generated by goctl. DO NOT EDIT. -package handler - -import ( - "net/http" - - "fusenapi/server/feishu-sync/internal/svc" - - "github.com/zeromicro/go-zero/rest" -) - -func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { - server.AddRoutes( - []rest.Route{ - { - Method: http.MethodPost, - Path: "/api/feishu/webhook", - Handler: WebhookHandler(serverCtx), - }, - }, - ) -} diff --git a/server/feishu-sync/internal/handler/webhookhandler.go b/server/feishu-sync/internal/handler/webhookhandler.go deleted file mode 100644 index 5ed0e568..00000000 --- a/server/feishu-sync/internal/handler/webhookhandler.go +++ /dev/null @@ -1,15 +0,0 @@ -package handler - -import ( - "fusenapi/server/feishu-sync/internal/logic" - "fusenapi/server/feishu-sync/internal/svc" - "net/http" -) - -func WebhookHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // 创建一个业务逻辑层实例 - l := logic.NewWebhookLogic(r.Context(), svcCtx) - l.Webhook(w, r) - } -} diff --git a/server/feishu-sync/internal/logic/user_webhook.go b/server/feishu-sync/internal/logic/user_webhook.go deleted file mode 100644 index 3be19f97..00000000 --- a/server/feishu-sync/internal/logic/user_webhook.go +++ /dev/null @@ -1,146 +0,0 @@ -package logic - -import ( - "encoding/json" - "fusenapi/model/gmodel" - "strconv" - "time" -) - -type UserWebhookMsg struct { - Schema string `json:"schema"` - Header struct { - EventId string `json:"event_id"` - EventType string `json:"event_type"` - CreateTime string `json:"create_time"` - Token string `json:"token"` - AppId string `json:"app_id"` - TenantKey string `json:"tenant_key"` - } `json:"header"` - Event struct { - Object struct { - OpenId string `json:"open_id"` - UnionId string `json:"union_id"` - UserId string `json:"user_id"` - Name string `json:"name"` - EnName string `json:"en_name"` - Nickname string `json:"nickname"` - Email string `json:"email"` - EnterpriseEmail string `json:"enterprise_email"` - JobTitle string `json:"job_title"` - Mobile string `json:"mobile"` - Gender int64 `json:"gender"` - Avatar struct { - Avatar72 string `json:"avatar_72"` - Avatar240 string `json:"avatar_240"` - Avatar640 string `json:"avatar_640"` - AvatarOrigin string `json:"avatar_origin"` - } `json:"avatar"` - Status struct { - IsFrozen bool `json:"is_frozen"` - IsResigned bool `json:"is_resigned"` - IsActivated bool `json:"is_activated"` - IsExited bool `json:"is_exited"` - IsUnjoin bool `json:"is_unjoin"` - } `json:"status"` - DepartmentIds []string `json:"department_ids"` - LeaderUserId string `json:"leader_user_id"` - City string `json:"city"` - Country string `json:"country"` - WorkStation string `json:"work_station"` - Joint64ime int64 `json:"join_time"` - EmployeeNo string `json:"employee_no"` - EmployeeType int64 `json:"employee_type"` - Orders []struct { - DepartmentId string `json:"department_id"` - UserOrder int64 `json:"user_order"` - DepartmentOrder int64 `json:"department_order"` - IsPrimaryDept bool `json:"is_primary_dept"` - } `json:"orders"` - CustomAttrs []struct { - Type string `json:"type"` - Id string `json:"id"` - Value struct { - Text string `json:"text"` - Url string `json:"url"` - PcUrl string `json:"pc_url"` - OptionId string `json:"option_id"` - OptionValue string `json:"option_value"` - Name string `json:"name"` - PictureUrl string `json:"picture_url"` - GenericUser struct { - Id string `json:"id"` - Type int64 `json:"type"` - } `json:"generic_user"` - } `json:"value"` - } `json:"custom_attrs"` - JobLevelId string `json:"job_level_id"` - JobFamilyId string `json:"job_family_id"` - DottedLineLeaderUserIds []string `json:"dotted_line_leader_user_ids"` - } `json:"object"` - } `json:"event"` -} - -// 员工增删改信息 -func (l *WebhookLogic) OnUserChange(data []byte) error { - var msg UserWebhookMsg - if err := json.Unmarshal(data, &msg); err != nil { - return err - } - avatar, _ := json.Marshal(msg.Event.Object.Avatar) - isFrozen := int64(0) - if msg.Event.Object.Status.IsFrozen { - isFrozen = 1 - } - isResigned := int64(0) - if msg.Event.Object.Status.IsResigned { - isResigned = 1 - } - isActivated := int64(0) - if msg.Event.Object.Status.IsActivated { - isActivated = 1 - } - isExited := int64(0) - if msg.Event.Object.Status.IsExited { - isExited = 1 - } - isUnjoin := int64(0) - if msg.Event.Object.Status.IsUnjoin { - isUnjoin = 1 - } - departmentIds, _ := json.Marshal(msg.Event.Object.DepartmentIds) - orders, _ := json.Marshal(msg.Event.Object.Orders) - feiShuMsgCreateTimeInt64, err := strconv.ParseInt(msg.Header.CreateTime, 10, 64) - if err != nil { - return err - } - feiShuMsgCreateTime := time.UnixMilli(feiShuMsgCreateTimeInt64) - joinTime := time.Unix(msg.Event.Object.Joint64ime, 0) - return l.svcCtx.AllModels.FsFeishuUser.CreateOrUpdate(l.ctx, msg.Header.AppId, msg.Event.Object.OpenId, &gmodel.FsFeishuUser{ - AppId: &msg.Header.AppId, - OpenId: &msg.Event.Object.OpenId, - UnionId: &msg.Event.Object.UnionId, - Name: &msg.Event.Object.Name, - EnName: &msg.Event.Object.EnName, - Nickname: &msg.Event.Object.Nickname, - Email: &msg.Event.Object.Email, - EnterpriseEmail: &msg.Event.Object.EnterpriseEmail, - JobTitle: &msg.Event.Object.JobTitle, - Mobile: &msg.Event.Object.Mobile, - Gender: &msg.Event.Object.Gender, - Avatar: &avatar, - IsFrozen: &isFrozen, - IsResigned: &isResigned, - IsActivated: &isActivated, - IsExited: &isExited, - IsUnjoin: &isUnjoin, - DepartmentIds: &departmentIds, - WorkStation: &msg.Event.Object.WorkStation, - EmployeeNo: &msg.Event.Object.EmployeeNo, - EmployeeType: &msg.Event.Object.EmployeeType, - Orders: &orders, - JoinTime: &joinTime, - Ctime: &feiShuMsgCreateTime, - Utime: &feiShuMsgCreateTime, - }) -} diff --git a/server/feishu-sync/internal/logic/webhooklogic.go b/server/feishu-sync/internal/logic/webhooklogic.go deleted file mode 100644 index 9977575f..00000000 --- a/server/feishu-sync/internal/logic/webhooklogic.go +++ /dev/null @@ -1,150 +0,0 @@ -package logic - -import ( - "context" - "encoding/json" - "fusenapi/model/gmodel" - "fusenapi/server/feishu-sync/internal/svc" - "fusenapi/utils/feishu" - "github.com/zeromicro/go-zero/core/logx" - "io" - "net/http" - "strconv" - "time" -) - -type WebhookLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewWebhookLogic(ctx context.Context, svcCtx *svc.ServiceContext) *WebhookLogic { - return &WebhookLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -type EncryptWebhookMsg struct { - Encrypt string `json:"encrypt"` //加密的消息 -} -type WebhookMsg struct { - Type string `json:"type"` - Challenge string `json:"challenge"` - Header map[string]interface{} `json:"header"` -} - -// webhook消息事件header(body参数)基础信息 -type BaseWebhookMsgHeaderType struct { - EventId string `json:"event_id"` //事件id(可作为消息唯一性确认) - EventType string `json:"event_type"` //事件类型 - CreateTime string `json:"create_time"` //创建时间 - Token string `json:"token"` //事件token - AppId string `json:"app_id"` //app id - TenantKey string `json:"tenant_key"` //租户key -} - -func (l *WebhookLogic) Webhook(w http.ResponseWriter, r *http.Request) { - bodyBytes, err := io.ReadAll(r.Body) - if err != nil { - logx.Error("读取请求body失败", err) - return - } - defer r.Body.Close() - //计算签名 - timestamp := r.Header.Get("X-Lark-Request-Timestamp") - nonce := r.Header.Get("X-Lark-Request-Nonce") - signature := r.Header.Get("X-Lark-Signature") - sign := feishu.CalculateFeiShuWebhookSignature(timestamp, nonce, l.svcCtx.Config.FeiShu.EncryptKey, bodyBytes) - if signature != sign { - logx.Error("非法的消息,签名验证不通过", sign, "====", signature) - return - } - var encryptMsg EncryptWebhookMsg - if err = json.Unmarshal(bodyBytes, &encryptMsg); err != nil { - logx.Error("反序列化body失败", err, "body数据:", string(bodyBytes)) - return - } - if encryptMsg.Encrypt == "" { - logx.Error("消息加密信息是空的") - return - } - //解密 - realMsgBytes, err := feishu.DecryptFeiShuWebhookMsg(encryptMsg.Encrypt, l.svcCtx.Config.FeiShu.EncryptKey) - if err != nil { - logx.Error(err) - return - } - //如果只是验证http连接的消息 - var webhookMsg WebhookMsg - if err = json.Unmarshal(realMsgBytes, &webhookMsg); err != nil { - logx.Error("反序列化请求body失败", err, "解密数据:", string(realMsgBytes)) - return - } - //验证连接(直接返回) - if webhookMsg.Type == "url_verification" { - challengeRsp := map[string]string{ - "challenge": webhookMsg.Challenge, - } - b, _ := json.Marshal(challengeRsp) - w.Write(b) - return - } - bodyHeaderByte, err := json.Marshal(webhookMsg.Header) - if err != nil { - logx.Error("序列化请求体header失败:", err) - return - } - var msgHeader BaseWebhookMsgHeaderType - if err = json.Unmarshal(bodyHeaderByte, &msgHeader); err != nil { - logx.Error("反序列化请求体中的header失败", err) - return - } - httpHeaderBytes, _ := json.Marshal(r.Header) - httpHeaderStr := string(httpHeaderBytes) - //解密后的数据 - decryptMsgStr := string(realMsgBytes) - feiShuMsgCreateTimeInt64, err := strconv.ParseInt(msgHeader.CreateTime, 10, 64) - if err != nil { - logx.Error("解析消息时间错误:", err) - return - } - feiShuMsgCreateTime := time.UnixMilli(feiShuMsgCreateTimeInt64) - now := time.Now().UTC() - //把事件加入日志 - err = l.svcCtx.AllModels.FsFeishuWebhookLog.Create(l.ctx, &gmodel.FsFeishuWebhookLog{ - AppId: &msgHeader.AppId, - EventId: &msgHeader.EventId, - EventType: &msgHeader.EventType, - HttpHeader: &httpHeaderStr, - Data: &encryptMsg.Encrypt, - DecryptData: &decryptMsgStr, - MsgCtime: &feiShuMsgCreateTime, - Ctime: &now, - }) - if err != nil { - logx.Error("保存webhook消息日志失败:", err) - } - switch msgHeader.EventType { - case "contact.department.created_v3": //部门新建 - case "contact.department.deleted_v3": //部门删除 - case "contact.department.updated_v3": //部门信息变化 - case "contact.employee_type_enum.actived_v3": //启动人员类型事件 - case "contact.employee_type_enum.created_v3": //新建人员类型事件 - case "contact.employee_type_enum.deactivated_v3": //停用人员类型事件 - case "contact.employee_type_enum.deleted_v3": //删除人员类型事件 - case "contact.employee_type_enum.updated_v3": //修改人员类型名称事件 - case "contact.user.created_v3": //员工入职 - err = l.OnUserChange(realMsgBytes) - case "contact.user.deleted_v3": //员工离职 - err = l.OnUserChange(realMsgBytes) - case "contact.user.updated_v3": //员工信息变化 - err = l.OnUserChange(realMsgBytes) - } - if err != nil { - logx.Error("处理事件错误:", err) - } - return -} diff --git a/server/feishu-sync/internal/svc/servicecontext.go b/server/feishu-sync/internal/svc/servicecontext.go deleted file mode 100644 index 6e5b8149..00000000 --- a/server/feishu-sync/internal/svc/servicecontext.go +++ /dev/null @@ -1,27 +0,0 @@ -package svc - -import ( - "fusenapi/initalize" - "fusenapi/model/gmodel" - "fusenapi/server/feishu-sync/internal/config" - "gorm.io/gorm" -) - -type ServiceContext struct { - Config config.Config - - MysqlConn *gorm.DB - AllModels *gmodel.AllModelsGen - RabbitMq *initalize.RabbitMqHandle -} - -func NewServiceContext(c config.Config) *ServiceContext { - conn := initalize.InitMysql(c.SourceMysql) - - return &ServiceContext{ - Config: c, - MysqlConn: conn, - AllModels: gmodel.NewAllModels(initalize.InitMysql(c.SourceMysql)), - RabbitMq: initalize.InitRabbitMq(c.SourceRabbitMq, nil), - } -} diff --git a/server/feishu-sync/internal/types/types.go b/server/feishu-sync/internal/types/types.go deleted file mode 100644 index b5fd43e5..00000000 --- a/server/feishu-sync/internal/types/types.go +++ /dev/null @@ -1,75 +0,0 @@ -// Code generated by goctl. DO NOT EDIT. -package types - -import ( - "fusenapi/utils/basic" -) - -type Request struct { -} - -type Response struct { - Code int `json:"code"` - Message string `json:"msg"` - Data interface{} `json:"data"` -} - -type Auth struct { - AccessSecret string `json:"accessSecret"` - AccessExpire int64 `json:"accessExpire"` - RefreshAfter int64 `json:"refreshAfter"` -} - -type File struct { - Filename string `fsfile:"filename"` - Header map[string][]string `fsfile:"header"` - Size int64 `fsfile:"size"` - Data []byte `fsfile:"data"` -} - -type Meta struct { - TotalCount int64 `json:"total_count"` - PageCount int64 `json:"page_count"` - CurrentPage int `json:"current_page"` - PerPage int `json:"per_page"` -} - -// Set 设置Response的Code和Message值 -func (resp *Response) Set(Code int, Message string) *Response { - return &Response{ - Code: Code, - Message: Message, - } -} - -// Set 设置整个Response -func (resp *Response) SetWithData(Code int, Message string, Data interface{}) *Response { - return &Response{ - Code: Code, - Message: Message, - Data: Data, - } -} - -// SetStatus 设置默认StatusResponse(内部自定义) 默认msg, 可以带data, data只使用一个参数 -func (resp *Response) SetStatus(sr *basic.StatusResponse, data ...interface{}) *Response { - newResp := &Response{ - Code: sr.Code, - } - if len(data) == 1 { - newResp.Data = data[0] - } - return newResp -} - -// SetStatusWithMessage 设置默认StatusResponse(内部自定义) 非默认msg, 可以带data, data只使用一个参数 -func (resp *Response) SetStatusWithMessage(sr *basic.StatusResponse, msg string, data ...interface{}) *Response { - newResp := &Response{ - Code: sr.Code, - Message: msg, - } - if len(data) == 1 { - newResp.Data = data[0] - } - return newResp -} diff --git a/server_api/feishu-sync.api b/server_api/feishu-sync.api deleted file mode 100644 index 31db4bea..00000000 --- a/server_api/feishu-sync.api +++ /dev/null @@ -1,16 +0,0 @@ -syntax = "v1" - -info ( - title: "飞书同步服务"// TODO: add title - desc: // TODO: add description - author: "" - email: "" -) - -import "basic.api" - -service feishu-sync { - //飞书ticket webhook事件接口 - @handler WebhookHandler - post /api/feishu/webhook(request) returns (response); -} \ No newline at end of file