feat: add Lark and Lark Custom notification support

This commit is contained in:
Jacky 2025-04-09 13:13:41 +00:00
parent e364353cd1
commit 7a0972495f
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
17 changed files with 174 additions and 38 deletions

View file

@ -4,6 +4,12 @@
const notifications: Record<string, { title: () => string, content: (args: any) => string }> = { const notifications: Record<string, { title: () => string, content: (args: any) => string }> = {
// user module notifications
'All Recovery Codes Have Been Used': {
title: () => $gettext('All Recovery Codes Have Been Used'),
content: (args: any) => $gettext('Please generate new recovery codes in the preferences immediately to prevent lockout.', args),
},
// cluster module notifications // cluster module notifications
'Reload Remote Nginx Error': { 'Reload Remote Nginx Error': {
title: () => $gettext('Reload Remote Nginx Error'), title: () => $gettext('Reload Remote Nginx Error'),
@ -149,12 +155,6 @@ const notifications: Record<string, { title: () => string, content: (args: any)
title: () => $gettext('Save Remote Stream Success'), title: () => $gettext('Save Remote Stream Success'),
content: (args: any) => $gettext('Save stream %{name} to %{node} successfully', args), content: (args: any) => $gettext('Save stream %{name} to %{node} successfully', args),
}, },
// user module notifications
'All Recovery Codes Have Been Used': {
title: () => $gettext('All Recovery Codes Have Been Used'),
content: (args: any) => $gettext('Please generate new recovery codes in the preferences immediately to prevent lockout.', args),
},
} }
export default notifications export default notifications

View file

@ -26,7 +26,6 @@ const columns = computed<Column[]>(() => {
type: input, type: input,
config: { config: {
label: item.label, label: item.label,
required: true,
}, },
}, },
})) }))

View file

@ -6,11 +6,11 @@ const BarkConfig: ExternalNotifyConfig = {
config: [ config: [
{ {
key: 'device_key', key: 'device_key',
label: () => $gettext('Device Key'), label: 'Device Key',
}, },
{ {
key: 'server_url', key: 'server_url',
label: () => $gettext('Server URL'), label: 'Server URL',
}, },
], ],
} }

View file

@ -6,11 +6,11 @@ const DingTalkConfig: ExternalNotifyConfig = {
config: [ config: [
{ {
key: 'access_token', key: 'access_token',
label: () => $gettext('Access Token'), label: 'Access Token',
}, },
{ {
key: 'secret', key: 'secret',
label: () => $gettext('Secret (Optional)'), label: 'Secret (Optional)',
}, },
], ],
} }

View file

@ -1,11 +1,15 @@
// This file is auto-generated by notification generator. DO NOT EDIT. // This file is auto-generated by notification generator. DO NOT EDIT.
import BarkConfig from './bark' import BarkConfig from './bark'
import DingTalkConfig from './dingtalk' import DingTalkConfig from './dingtalk'
import LarkConfig from './lark'
import LarkCustomConfig from './lark_custom'
import TelegramConfig from './telegram' import TelegramConfig from './telegram'
const configMap = { const configMap = {
bark: BarkConfig, bark: BarkConfig,
dingtalk: DingTalkConfig, dingtalk: DingTalkConfig,
lark: LarkConfig,
lark_custom: LarkCustomConfig,
telegram: TelegramConfig, telegram: TelegramConfig,
} }

View file

@ -0,0 +1,14 @@
// This file is auto-generated by notification generator. DO NOT EDIT.
import type { ExternalNotifyConfig } from './types'
const LarkConfig: ExternalNotifyConfig = {
name: () => $gettext('Lark'),
config: [
{
key: 'webhook_url',
label: 'Webhook URL',
},
],
}
export default LarkConfig

View file

@ -0,0 +1,38 @@
// This file is auto-generated by notification generator. DO NOT EDIT.
import type { ExternalNotifyConfig } from './types'
const LarkCustomConfig: ExternalNotifyConfig = {
name: () => $gettext('Lark Custom'),
config: [
{
key: 'app_id',
label: 'App ID',
},
{
key: 'app_secret',
label: 'App Secret',
},
{
key: 'open_id',
label: 'Open ID',
},
{
key: 'user_id',
label: 'User ID',
},
{
key: 'union_id',
label: 'Union ID',
},
{
key: 'email',
label: 'Email',
},
{
key: 'chat_id',
label: 'Chat ID',
},
],
}
export default LarkCustomConfig

View file

@ -6,11 +6,11 @@ const TelegramConfig: ExternalNotifyConfig = {
config: [ config: [
{ {
key: 'bot_token', key: 'bot_token',
label: () => $gettext('Bot Token'), label: 'Bot Token',
}, },
{ {
key: 'chat_id', key: 'chat_id',
label: () => $gettext('Chat ID'), label: 'Chat ID',
}, },
], ],
} }

View file

@ -1,6 +1,6 @@
export interface ExternalNotifyConfigItem { export interface ExternalNotifyConfigItem {
key: string key: string
label: () => string label: string
} }
export interface ExternalNotifyConfig { export interface ExternalNotifyConfig {

View file

@ -0,0 +1,6 @@
// This file is auto-generated by notification generator. DO NOT EDIT.
const configMap = {
}
export default configMap

View file

@ -32,23 +32,23 @@ type FieldInfo struct {
const tsConfigTemplate = `// This file is auto-generated by notification generator. DO NOT EDIT. const tsConfigTemplate = `// This file is auto-generated by notification generator. DO NOT EDIT.
import type { ExternalNotifyConfig } from './types' import type { ExternalNotifyConfig } from './types'
const {{.Name}}Config: ExternalNotifyConfig = { const {{.Name | replaceSpaces}}Config: ExternalNotifyConfig = {
name: () => $gettext('{{.Name}}'), name: () => $gettext('{{.Name}}'),
config: [ config: [
{{- range .Fields}} {{- range .Fields}}
{ {
key: '{{.Key}}', key: '{{.Key}}',
label: () => $gettext('{{.Title}}'), label: '{{.Title}}',
}, },
{{- end}} {{- end}}
], ],
} }
export default {{.Name}}Config export default {{.Name | replaceSpaces}}Config
` `
// Regular expression to extract @external_notifier annotation // Regular expression to extract @external_notifier annotation
var externalNotifierRegex = regexp.MustCompile(`@external_notifier\((\w+)\)`) var externalNotifierRegex = regexp.MustCompile(`@external_notifier\(([a-zA-Z0-9 _]+)\)`)
func main() { func main() {
if err := GenerateExternalNotifiers(); err != nil { if err := GenerateExternalNotifiers(); err != nil {
@ -142,7 +142,7 @@ func extractNotifierInfo(filePath string) (NotifierInfo, bool) {
if len(matches) > 1 { if len(matches) > 1 {
notifierInfo.Name = matches[1] notifierInfo.Name = matches[1]
notifierInfo.ConfigKey = strings.ToLower(typeSpec.Name.Name) notifierInfo.ConfigKey = strings.ToLower(typeSpec.Name.Name)
notifierInfo.FileName = strings.ToLower(matches[1]) notifierInfo.FileName = strings.ToLower(strings.ReplaceAll(matches[1], " ", "_"))
found = true found = true
// Extract fields // Extract fields
@ -205,8 +205,15 @@ func extractNotifierInfo(filePath string) (NotifierInfo, bool) {
// Generate TypeScript config file for a notifier // Generate TypeScript config file for a notifier
func generateTSConfig(notifier NotifierInfo, outputDir string) error { func generateTSConfig(notifier NotifierInfo, outputDir string) error {
// Create template // Create function map for template
tmpl, err := template.New("tsConfig").Parse(tsConfigTemplate) funcMap := template.FuncMap{
"replaceSpaces": func(s string) string {
return strings.ReplaceAll(s, " ", "")
},
}
// Create template with function map
tmpl, err := template.New("tsConfig").Funcs(funcMap).Parse(tsConfigTemplate)
if err != nil { if err != nil {
return fmt.Errorf("error creating template: %w", err) return fmt.Errorf("error creating template: %w", err)
} }
@ -242,7 +249,7 @@ func updateIndexFile(notifiers []NotifierInfo, outputDir string) error {
for _, notifier := range notifiers { for _, notifier := range notifiers {
fileName := notifier.FileName fileName := notifier.FileName
configName := notifier.Name + "Config" configName := strings.ReplaceAll(notifier.Name, " ", "") + "Config"
imports.WriteString(fmt.Sprintf("import %s from './%s'\n", configName, fileName)) imports.WriteString(fmt.Sprintf("import %s from './%s'\n", configName, fileName))
} }
@ -250,7 +257,8 @@ func updateIndexFile(notifiers []NotifierInfo, outputDir string) error {
// Generate the map // Generate the map
configMap.WriteString("const configMap = {\n") configMap.WriteString("const configMap = {\n")
for _, notifier := range notifiers { for _, notifier := range notifiers {
configMap.WriteString(fmt.Sprintf(" %s: %sConfig", strings.ToLower(notifier.Name), notifier.Name)) configKey := strings.ToLower(strings.ReplaceAll(notifier.Name, " ", "_"))
configMap.WriteString(fmt.Sprintf(" %s: %sConfig", configKey, strings.ReplaceAll(notifier.Name, " ", "")))
configMap.WriteString(",\n") configMap.WriteString(",\n")
} }
configMap.WriteString("}\n") configMap.WriteString("}\n")

View file

@ -4,7 +4,6 @@ import (
"context" "context"
"github.com/0xJacky/Nginx-UI/model" "github.com/0xJacky/Nginx-UI/model"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/bark" "github.com/nikoksr/notify/service/bark"
"github.com/uozi-tech/cosy/map2struct" "github.com/uozi-tech/cosy/map2struct"
) )
@ -26,8 +25,6 @@ func init() {
return ErrInvalidNotifierConfig return ErrInvalidNotifierConfig
} }
barkService := bark.NewWithServers(barkConfig.DeviceKey, barkConfig.ServerURL) barkService := bark.NewWithServers(barkConfig.DeviceKey, barkConfig.ServerURL)
externalNotify := notify.New() return barkService.Send(ctx, msg.GetTitle(n.Language), msg.GetContent(n.Language))
externalNotify.UseServices(barkService)
return externalNotify.Send(ctx, msg.GetTitle(n.Language), msg.GetContent(n.Language))
}) })
} }

View file

@ -4,7 +4,6 @@ import (
"context" "context"
"github.com/0xJacky/Nginx-UI/model" "github.com/0xJacky/Nginx-UI/model"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/dingding" "github.com/nikoksr/notify/service/dingding"
"github.com/uozi-tech/cosy/map2struct" "github.com/uozi-tech/cosy/map2struct"
) )
@ -31,9 +30,6 @@ func init() {
Token: dingTalkConfig.AccessToken, Token: dingTalkConfig.AccessToken,
Secret: dingTalkConfig.Secret, Secret: dingTalkConfig.Secret,
}) })
// Use the service return dingTalkService.Send(ctx, msg.GetTitle(n.Language), msg.GetContent(n.Language))
externalNotify := notify.New()
externalNotify.UseServices(dingTalkService)
return externalNotify.Send(ctx, msg.GetTitle(n.Language), msg.GetContent(n.Language))
}) })
} }

View file

@ -0,0 +1,30 @@
package notification
import (
"context"
"github.com/0xJacky/Nginx-UI/model"
"github.com/nikoksr/notify/service/lark"
"github.com/uozi-tech/cosy/map2struct"
)
// @external_notifier(Lark)
type Lark struct {
WebhookURL string `json:"webhook_url" title:"Webhook URL"`
}
func init() {
RegisterExternalNotifier("lark", func(ctx context.Context, n *model.ExternalNotify, msg *ExternalMessage) error {
larkConfig := &Lark{}
err := map2struct.WeakDecode(n.Config, larkConfig)
if err != nil {
return err
}
if larkConfig.WebhookURL == "" {
return ErrInvalidNotifierConfig
}
larkService := lark.NewWebhookService(larkConfig.WebhookURL)
return larkService.Send(ctx, msg.GetTitle(n.Language), msg.GetContent(n.Language))
})
}

View file

@ -0,0 +1,43 @@
package notification
import (
"context"
"github.com/0xJacky/Nginx-UI/model"
"github.com/nikoksr/notify/service/lark"
"github.com/uozi-tech/cosy/map2struct"
)
// @external_notifier(Lark Custom)
type LarkCustom struct {
AppID string `json:"app_id" title:"App ID"`
AppSecret string `json:"app_secret" title:"App Secret"`
OpenID string `json:"open_id" title:"Open ID"`
UserID string `json:"user_id" title:"User ID"`
UnionID string `json:"union_id" title:"Union ID"`
Email string `json:"email" title:"Email"`
ChatID string `json:"chat_id" title:"Chat ID"`
}
func init() {
RegisterExternalNotifier("lark_custom", func(ctx context.Context, n *model.ExternalNotify, msg *ExternalMessage) error {
larkCustomConfig := &LarkCustom{}
err := map2struct.WeakDecode(n.Config, larkCustomConfig)
if err != nil {
return err
}
if larkCustomConfig.AppID == "" || larkCustomConfig.AppSecret == "" {
return ErrInvalidNotifierConfig
}
larkCustomAppService := lark.NewCustomAppService(larkCustomConfig.AppID, larkCustomConfig.AppSecret)
larkCustomAppService.AddReceivers(
lark.OpenID(larkCustomConfig.OpenID),
lark.UserID(larkCustomConfig.UserID),
lark.UnionID(larkCustomConfig.UnionID),
lark.Email(larkCustomConfig.Email),
lark.ChatID(larkCustomConfig.ChatID),
)
return larkCustomAppService.Send(ctx, msg.GetTitle(n.Language), msg.GetContent(n.Language))
})
}

View file

@ -7,7 +7,6 @@ import (
"strconv" "strconv"
"github.com/0xJacky/Nginx-UI/model" "github.com/0xJacky/Nginx-UI/model"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/telegram" "github.com/nikoksr/notify/service/telegram"
"github.com/uozi-tech/cosy/map2struct" "github.com/uozi-tech/cosy/map2struct"
) )
@ -46,9 +45,7 @@ func init() {
} }
telegramService.AddReceivers(chatIDInt) telegramService.AddReceivers(chatIDInt)
externalNotify := notify.New() return telegramService.Send(ctx, msg.GetTitle(n.Language), msg.GetContent(n.Language))
externalNotify.UseServices(telegramService)
return externalNotify.Send(ctx, msg.GetTitle(n.Language), msg.GetContent(n.Language))
}) })
} }

View file

@ -33,6 +33,7 @@ func newExternalNotify(db *gorm.DB, opts ...gen.DOOption) externalNotify {
_externalNotify.UpdatedAt = field.NewTime(tableName, "updated_at") _externalNotify.UpdatedAt = field.NewTime(tableName, "updated_at")
_externalNotify.DeletedAt = field.NewField(tableName, "deleted_at") _externalNotify.DeletedAt = field.NewField(tableName, "deleted_at")
_externalNotify.Type = field.NewString(tableName, "type") _externalNotify.Type = field.NewString(tableName, "type")
_externalNotify.Language = field.NewString(tableName, "language")
_externalNotify.Config = field.NewField(tableName, "config") _externalNotify.Config = field.NewField(tableName, "config")
_externalNotify.fillFieldMap() _externalNotify.fillFieldMap()
@ -49,6 +50,7 @@ type externalNotify struct {
UpdatedAt field.Time UpdatedAt field.Time
DeletedAt field.Field DeletedAt field.Field
Type field.String Type field.String
Language field.String
Config field.Field Config field.Field
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
@ -71,6 +73,7 @@ func (e *externalNotify) updateTableName(table string) *externalNotify {
e.UpdatedAt = field.NewTime(table, "updated_at") e.UpdatedAt = field.NewTime(table, "updated_at")
e.DeletedAt = field.NewField(table, "deleted_at") e.DeletedAt = field.NewField(table, "deleted_at")
e.Type = field.NewString(table, "type") e.Type = field.NewString(table, "type")
e.Language = field.NewString(table, "language")
e.Config = field.NewField(table, "config") e.Config = field.NewField(table, "config")
e.fillFieldMap() e.fillFieldMap()
@ -88,12 +91,13 @@ func (e *externalNotify) GetFieldByName(fieldName string) (field.OrderExpr, bool
} }
func (e *externalNotify) fillFieldMap() { func (e *externalNotify) fillFieldMap() {
e.fieldMap = make(map[string]field.Expr, 6) e.fieldMap = make(map[string]field.Expr, 7)
e.fieldMap["id"] = e.ID e.fieldMap["id"] = e.ID
e.fieldMap["created_at"] = e.CreatedAt e.fieldMap["created_at"] = e.CreatedAt
e.fieldMap["updated_at"] = e.UpdatedAt e.fieldMap["updated_at"] = e.UpdatedAt
e.fieldMap["deleted_at"] = e.DeletedAt e.fieldMap["deleted_at"] = e.DeletedAt
e.fieldMap["type"] = e.Type e.fieldMap["type"] = e.Type
e.fieldMap["language"] = e.Language
e.fieldMap["config"] = e.Config e.fieldMap["config"] = e.Config
} }