diff --git a/server/karaenv.go b/server/karaenv.go index ddddfe3..b2489ff 100644 --- a/server/karaenv.go +++ b/server/karaenv.go @@ -115,6 +115,7 @@ type KaraberusConfig struct { Dakara KaraberusDakaraConfig `env_prefix:"DAKARA"` Mugen KaraberusMugenConfig `env_prefix:"MUGEN"` UIDistDir string `envkey:"UI_DIST_DIR" default:"/usr/share/karaberus/ui_dist"` + Webhooks []string `envkey:"WEBHOOKS" separator:" " example:"discord= discord= json="` } func getEnvDefault(name string, defaultValue string) string { @@ -143,9 +144,13 @@ func setConfigValue(config_value reflect.Value, config_type reflect.Type, prefix switch field_type.Type { case reflect.TypeOf([]string{}): value := getFieldValue(field_type, prefix) - sep := field_type.Tag.Get("separator") - arrval := strings.Split(value, sep) - field.Set(reflect.ValueOf(arrval)) + if value == "" { + field.Set(reflect.ValueOf([]string{})) + } else { + sep := field_type.Tag.Get("separator") + arrval := strings.Split(value, sep) + field.Set(reflect.ValueOf(arrval)) + } case reflect.TypeOf(""): field.SetString(getFieldValue(field_type, prefix)) case reflect.TypeOf(0): diff --git a/server/meson.build b/server/meson.build index 811a22e..5a83d5d 100644 --- a/server/meson.build +++ b/server/meson.build @@ -18,6 +18,7 @@ karaberus_server_files = files( 'token.go', 'upload.go', 'user.go', + 'webhooks.go', ) karaberus_server_tests = files('karaberus_test.go') diff --git a/server/model.go b/server/model.go index c282aeb..6c919a8 100644 --- a/server/model.go +++ b/server/model.go @@ -367,6 +367,16 @@ func CurrentKaras(tx *gorm.DB) *gorm.DB { return tx.Where("current_kara_info_id IS NULL") } +type NewKaraUpdate struct{} + +func WithNewKaraUpdate(tx *gorm.DB) *gorm.DB { + return tx.WithContext(context.WithValue(tx.Statement.Context, NewKaraUpdate{}, true)) +} + +func isNewKaraUpdate(tx *gorm.DB) bool { + return tx.Statement.Context.Value(NewKaraUpdate{}) != nil +} + type UpdateAssociations struct{} func WithAssociationsUpdate(tx *gorm.DB) *gorm.DB { @@ -417,13 +427,29 @@ func (ki *KaraInfoDB) AfterUpdate(tx *gorm.DB) error { return err } - if ki.CurrentKaraInfoID == nil && CONFIG.Dakara.BaseURL != "" && ki.UploadInfo.VideoUploaded && ki.UploadInfo.SubtitlesUploaded { - SyncDakaraNotify() - } - if !ki.KaraokeCreationTime.IsZero() { - err = UploadHookGitlab(tx, ki) - if err != nil { - return err + if ki.CurrentKaraInfoID == nil { + if CONFIG.Dakara.BaseURL != "" && ki.UploadInfo.VideoUploaded && ki.UploadInfo.SubtitlesUploaded { + SyncDakaraNotify() + } + + if isNewKaraUpdate(tx) { + err = UploadHookGitlab(tx, ki) + if err != nil { + return err + } + + // ignore imported karas + mugen_import := &MugenImport{} + err := tx.Where(&MugenImport{KaraID: ki.ID}).First(mugen_import).Error + if err == nil { + // kara was imported + return nil + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + + go PostWebhooks(*ki) } } return nil @@ -439,13 +465,6 @@ func (ki *KaraInfoDB) BeforeUpdate(tx *gorm.DB) error { return err } - // check for unix time 0 is for older karaokes, because we also used - // that at some point - if ki.VideoUploaded && ki.SubtitlesUploaded && - ki.KaraokeCreationTime.IsZero() || ki.KaraokeCreationTime.Unix() == 0 { - ki.KaraokeCreationTime = time.Now().UTC() - } - // create historic entry with the current value orig_kara_info.ID = 0 orig_kara_info.CurrentKaraInfo = ki diff --git a/server/s3.go b/server/s3.go index 529d62f..ef3afe6 100644 --- a/server/s3.go +++ b/server/s3.go @@ -135,9 +135,30 @@ func SaveFileToS3WithMetadata(ctx context.Context, tx *gorm.DB, fd io.Reader, ka return nil, err } - err = updateKaraokeAfterUpload(tx, kara, type_directory, filesize, crc32) - if err != nil { - return nil, err + currentTime := time.Now().UTC() + switch type_directory { + case "video": + kara.VideoUploaded = true + kara.VideoModTime = currentTime + kara.VideoSize = filesize + kara.VideoCRC32 = crc32 + case "inst": + kara.InstrumentalUploaded = true + kara.InstrumentalModTime = currentTime + kara.InstrumentalSize = filesize + kara.InstrumentalCRC32 = crc32 + case "sub": + kara.SubtitlesUploaded = true + kara.SubtitlesModTime = currentTime + kara.SubtitlesSize = filesize + kara.SubtitlesCRC32 = crc32 + } + // check for unix time 0 is for older karaokes, because we also used + // that at some point + if kara.VideoUploaded && kara.SubtitlesUploaded && + kara.KaraokeCreationTime.IsZero() || kara.KaraokeCreationTime.Unix() == 0 { + kara.KaraokeCreationTime = currentTime + tx = WithNewKaraUpdate(tx) } res, err := CheckKara(ctx, *kara) diff --git a/server/upload.go b/server/upload.go index 55c145c..4e0a79b 100644 --- a/server/upload.go +++ b/server/upload.go @@ -15,7 +15,6 @@ import ( "os" "strconv" "strings" - "time" "github.com/danielgtaylor/huma/v2" "github.com/gofiber/fiber/v2" @@ -151,31 +150,6 @@ type UploadOutput struct { } } -func updateKaraokeAfterUpload(tx *gorm.DB, kara *KaraInfoDB, filetype string, filesize int64, crc32 uint32) error { - currentTime := time.Now().UTC() - switch filetype { - case "video": - kara.VideoUploaded = true - kara.VideoModTime = currentTime - kara.VideoSize = filesize - kara.VideoCRC32 = crc32 - return nil - case "inst": - kara.InstrumentalUploaded = true - kara.InstrumentalModTime = currentTime - kara.InstrumentalSize = filesize - kara.InstrumentalCRC32 = crc32 - return nil - case "sub": - kara.SubtitlesUploaded = true - kara.SubtitlesModTime = currentTime - kara.SubtitlesSize = filesize - kara.SubtitlesCRC32 = crc32 - return nil - } - return errors.New("Unknown file type " + filetype) -} - func UploadKaraFile(ctx context.Context, input *UploadInput) (*UploadOutput, error) { db := GetDB(ctx) var err error diff --git a/server/webhooks.go b/server/webhooks.go new file mode 100644 index 0000000..991c920 --- /dev/null +++ b/server/webhooks.go @@ -0,0 +1,123 @@ +package server + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "strings" +) + +type Webhook struct { + Type string + URL string +} + +type WebhookTemplateContext struct { + Kara KaraInfoDB + Server string + Title string + Description string + Resource string +} + +func parseWebhooksConfig() []Webhook { + var webhooks []Webhook + for _, kv := range CONFIG.Webhooks { + typ, url, found := strings.Cut(kv, "=") + if !found { + getLogger().Printf("invalid webhook value: %s", kv) + continue + } + webhooks = append(webhooks, Webhook{typ, url}) + } + return webhooks +} + +func PostWebhooks(kara KaraInfoDB) { + title := kara.FriendlyName() + desc, err := karaDescription(kara) + if err != nil { + getLogger().Printf("error generating description for webhooks: %s", err) + return + } + + tmplCtx := WebhookTemplateContext{ + Kara: kara, + Server: CONFIG.Listen.Addr(), + Title: title, + Description: desc, + Resource: fmt.Sprintf("%s/karaoke/browse/%d", CONFIG.Listen.Addr(), kara.ID), + } + + for _, webhook := range parseWebhooksConfig() { + var err error + switch webhook.Type { + case "json": + err = postJsonWebhook(webhook.URL, tmplCtx) + case "discord": + err = postDiscordWebhook(webhook.URL, tmplCtx) + default: + err = fmt.Errorf("unknown webhook type %s", webhook.Type) + } + if err != nil { + getLogger().Printf("error during %s webhook: %s", webhook, err) + } + } +} + +func postJsonWebhook(url string, tmplCtx WebhookTemplateContext) error { + b, err := json.Marshal(tmplCtx) + if err != nil { + return err + } + resp, err := http.Post(url, "application/json", bytes.NewReader(b)) + if err != nil { + return err + } + defer Closer(resp.Body) + return nil +} + +type DiscordEmbedAuthor struct { + Name string `json:"name"` + IconURL string `json:"icon_url"` +} + +type DiscordEmbed struct { + Author DiscordEmbedAuthor `json:"author,omitempty"` + Title string `json:"title"` + URL string `json:"url"` + Description string `json:"description"` + Color uint `json:"color"` +} + +type DiscordWebhook struct { + Embeds []DiscordEmbed `json:"embeds"` +} + +func postDiscordWebhook(url string, tmplCtx WebhookTemplateContext) error { + webhook_data := DiscordWebhook{ + Embeds: []DiscordEmbed{DiscordEmbed{ + Author: DiscordEmbedAuthor{ + Name: "New Karaoke!", + IconURL: fmt.Sprintf("%s/vite.svg", tmplCtx.Server), + }, + Title: tmplCtx.Title, + URL: tmplCtx.Resource, + Description: tmplCtx.Description, + Color: 10053324, + }}, + } + + body, err := json.Marshal(webhook_data) + if err != nil { + return err + } + resp, err := http.Post(url, "application/json", bytes.NewBuffer(body)) + if err != nil { + return err + } + defer Closer(resp.Body) + return nil +}