diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go
index 88a3920f6e..636e655b9e 100644
--- a/modules/web/middleware/binding.go
+++ b/modules/web/middleware/binding.go
@@ -136,7 +136,16 @@ func Validate(errs binding.Errors, data map[string]interface{}, f Form, l transl
 			case validation.ErrRegexPattern:
 				data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
 			default:
-				data["ErrorMsg"] = l.Tr("form.unknown_error") + " " + errs[0].Classification
+				msg := errs[0].Classification
+				if msg != "" && errs[0].Message != "" {
+					msg += ": "
+				}
+
+				msg += errs[0].Message
+				if msg == "" {
+					msg = l.Tr("form.unknown_error")
+				}
+				data["ErrorMsg"] = trName + ": " + msg
 			}
 			return errs
 		}
diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go
index f0dc595ad5..ba008f587c 100644
--- a/routers/api/v1/utils/hook.go
+++ b/routers/api/v1/utils/hook.go
@@ -15,7 +15,6 @@ import (
 	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
-	"code.gitea.io/gitea/routers/utils"
 	webhook_service "code.gitea.io/gitea/services/webhook"
 )
 
@@ -141,14 +140,15 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, orgID, repoID
 			ctx.Error(http.StatusUnprocessableEntity, "", "Missing config option: channel")
 			return nil, false
 		}
+		channel = strings.TrimSpace(channel)
 
-		if !utils.IsValidSlackChannel(channel) {
+		if !webhook_service.IsValidSlackChannel(channel) {
 			ctx.Error(http.StatusBadRequest, "", "Invalid slack channel name")
 			return nil, false
 		}
 
 		meta, err := json.Marshal(&webhook_service.SlackMeta{
-			Channel:  strings.TrimSpace(channel),
+			Channel:  channel,
 			Username: form.Config["username"],
 			IconURL:  form.Config["icon_url"],
 			Color:    form.Config["color"],
diff --git a/routers/utils/utils.go b/routers/utils/utils.go
index f15bc1e62e..66eaa1d9ce 100644
--- a/routers/utils/utils.go
+++ b/routers/utils/utils.go
@@ -20,25 +20,6 @@ func RemoveUsernameParameterSuffix(name string) string {
 	return name
 }
 
-// IsValidSlackChannel validates a channel name conforms to what slack expects.
-// It makes sure a channel name cannot be empty and invalid ( only an # )
-func IsValidSlackChannel(channelName string) bool {
-	switch len(strings.TrimSpace(channelName)) {
-	case 0:
-		return false
-	case 1:
-		// Keep default behaviour where a channel name is still
-		// valid without an #
-		// But if it contains only an #, it should be regarded as
-		// invalid
-		if channelName[0] == '#' {
-			return false
-		}
-	}
-
-	return true
-}
-
 // SanitizeFlashErrorString will sanitize a flash error string
 func SanitizeFlashErrorString(x string) string {
 	return strings.ReplaceAll(html.EscapeString(x), "\n", "<br>")
diff --git a/routers/utils/utils_test.go b/routers/utils/utils_test.go
index f49ed77b6f..42cf948e30 100644
--- a/routers/utils/utils_test.go
+++ b/routers/utils/utils_test.go
@@ -18,23 +18,6 @@ func TestRemoveUsernameParameterSuffix(t *testing.T) {
 	assert.Equal(t, "", RemoveUsernameParameterSuffix(""))
 }
 
-func TestIsValidSlackChannel(t *testing.T) {
-	tt := []struct {
-		channelName string
-		expected    bool
-	}{
-		{"gitea", true},
-		{"  ", false},
-		{"#", false},
-		{"gitea   ", true},
-		{"  gitea", true},
-	}
-
-	for _, v := range tt {
-		assert.Equal(t, v.expected, IsValidSlackChannel(v.channelName))
-	}
-}
-
 func TestIsExternalURL(t *testing.T) {
 	setting.AppURL = "https://try.gitea.io/"
 	type test struct {
diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go
index a9b14ee21f..d4419a1e10 100644
--- a/routers/web/repo/webhook.go
+++ b/routers/web/repo/webhook.go
@@ -185,14 +185,22 @@ func ParseHookEvent(form forms.WebhookForm) *webhook.HookEvent {
 	}
 }
 
-// GiteaHooksNewPost response for creating Gitea webhook
-func GiteaHooksNewPost(ctx *context.Context) {
-	form := web.GetForm(ctx).(*forms.NewWebhookForm)
+type webhookCreationParams struct {
+	URL         string
+	ContentType webhook.HookContentType
+	Secret      string
+	HTTPMethod  string
+	WebhookForm forms.WebhookForm
+	Type        string
+	Meta        interface{}
+}
+
+func createWebhook(ctx *context.Context, params webhookCreationParams) {
 	ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
 	ctx.Data["PageIsSettingsHooks"] = true
 	ctx.Data["PageIsSettingsHooksNew"] = true
 	ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
-	ctx.Data["HookType"] = webhook.GITEA
+	ctx.Data["HookType"] = params.Type
 
 	orCtx, err := getOrgRepoCtx(ctx)
 	if err != nil {
@@ -206,20 +214,25 @@ func GiteaHooksNewPost(ctx *context.Context) {
 		return
 	}
 
-	contentType := webhook.ContentTypeJSON
-	if webhook.HookContentType(form.ContentType) == webhook.ContentTypeForm {
-		contentType = webhook.ContentTypeForm
+	var meta []byte
+	if params.Meta != nil {
+		meta, err = json.Marshal(params.Meta)
+		if err != nil {
+			ctx.ServerError("Marshal", err)
+			return
+		}
 	}
 
 	w := &webhook.Webhook{
 		RepoID:          orCtx.RepoID,
-		URL:             form.PayloadURL,
-		HTTPMethod:      form.HTTPMethod,
-		ContentType:     contentType,
-		Secret:          form.Secret,
-		HookEvent:       ParseHookEvent(form.WebhookForm),
-		IsActive:        form.Active,
-		Type:            webhook.GITEA,
+		URL:             params.URL,
+		HTTPMethod:      params.HTTPMethod,
+		ContentType:     params.ContentType,
+		Secret:          params.Secret,
+		HookEvent:       ParseHookEvent(params.WebhookForm),
+		IsActive:        params.WebhookForm.Active,
+		Type:            params.Type,
+		Meta:            string(meta),
 		OrgID:           orCtx.OrgID,
 		IsSystemWebhook: orCtx.IsSystemWebhook,
 	}
@@ -235,503 +248,175 @@ func GiteaHooksNewPost(ctx *context.Context) {
 	ctx.Redirect(orCtx.Link)
 }
 
+// GiteaHooksNewPost response for creating Gitea webhook
+func GiteaHooksNewPost(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.NewWebhookForm)
+
+	contentType := webhook.ContentTypeJSON
+	if webhook.HookContentType(form.ContentType) == webhook.ContentTypeForm {
+		contentType = webhook.ContentTypeForm
+	}
+
+	createWebhook(ctx, webhookCreationParams{
+		URL:         form.PayloadURL,
+		ContentType: contentType,
+		Secret:      form.Secret,
+		HTTPMethod:  form.HTTPMethod,
+		WebhookForm: form.WebhookForm,
+		Type:        webhook.GITEA,
+	})
+}
+
 // GogsHooksNewPost response for creating webhook
 func GogsHooksNewPost(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.NewGogshookForm)
-	newGogsWebhookPost(ctx, *form, webhook.GOGS)
-}
-
-// newGogsWebhookPost response for creating gogs hook
-func newGogsWebhookPost(ctx *context.Context, form forms.NewGogshookForm, kind webhook.HookType) {
-	ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
-	ctx.Data["PageIsSettingsHooks"] = true
-	ctx.Data["PageIsSettingsHooksNew"] = true
-	ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
-	ctx.Data["HookType"] = webhook.GOGS
-
-	orCtx, err := getOrgRepoCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getOrgRepoCtx", err)
-		return
-	}
-	ctx.Data["BaseLink"] = orCtx.LinkNew
-
-	if ctx.HasError() {
-		ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-		return
-	}
 
 	contentType := webhook.ContentTypeJSON
 	if webhook.HookContentType(form.ContentType) == webhook.ContentTypeForm {
 		contentType = webhook.ContentTypeForm
 	}
 
-	w := &webhook.Webhook{
-		RepoID:          orCtx.RepoID,
-		URL:             form.PayloadURL,
-		ContentType:     contentType,
-		Secret:          form.Secret,
-		HookEvent:       ParseHookEvent(form.WebhookForm),
-		IsActive:        form.Active,
-		Type:            kind,
-		OrgID:           orCtx.OrgID,
-		IsSystemWebhook: orCtx.IsSystemWebhook,
-	}
-	if err := w.UpdateEvent(); err != nil {
-		ctx.ServerError("UpdateEvent", err)
-		return
-	} else if err := webhook.CreateWebhook(ctx, w); err != nil {
-		ctx.ServerError("CreateWebhook", err)
-		return
-	}
-
-	ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-	ctx.Redirect(orCtx.Link)
+	createWebhook(ctx, webhookCreationParams{
+		URL:         form.PayloadURL,
+		ContentType: contentType,
+		Secret:      form.Secret,
+		WebhookForm: form.WebhookForm,
+		Type:        webhook.GOGS,
+	})
 }
 
 // DiscordHooksNewPost response for creating discord hook
 func DiscordHooksNewPost(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.NewDiscordHookForm)
-	ctx.Data["Title"] = ctx.Tr("repo.settings")
-	ctx.Data["PageIsSettingsHooks"] = true
-	ctx.Data["PageIsSettingsHooksNew"] = true
-	ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
-	ctx.Data["HookType"] = webhook.DISCORD
 
-	orCtx, err := getOrgRepoCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getOrgRepoCtx", err)
-		return
-	}
-
-	if ctx.HasError() {
-		ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-		return
-	}
-
-	meta, err := json.Marshal(&webhook_service.DiscordMeta{
-		Username: form.Username,
-		IconURL:  form.IconURL,
+	createWebhook(ctx, webhookCreationParams{
+		URL:         form.PayloadURL,
+		ContentType: webhook.ContentTypeJSON,
+		WebhookForm: form.WebhookForm,
+		Type:        webhook.DISCORD,
+		Meta: &webhook_service.DiscordMeta{
+			Username: form.Username,
+			IconURL:  form.IconURL,
+		},
 	})
-	if err != nil {
-		ctx.ServerError("Marshal", err)
-		return
-	}
-
-	w := &webhook.Webhook{
-		RepoID:          orCtx.RepoID,
-		URL:             form.PayloadURL,
-		ContentType:     webhook.ContentTypeJSON,
-		HookEvent:       ParseHookEvent(form.WebhookForm),
-		IsActive:        form.Active,
-		Type:            webhook.DISCORD,
-		Meta:            string(meta),
-		OrgID:           orCtx.OrgID,
-		IsSystemWebhook: orCtx.IsSystemWebhook,
-	}
-	if err := w.UpdateEvent(); err != nil {
-		ctx.ServerError("UpdateEvent", err)
-		return
-	} else if err := webhook.CreateWebhook(ctx, w); err != nil {
-		ctx.ServerError("CreateWebhook", err)
-		return
-	}
-
-	ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-	ctx.Redirect(orCtx.Link)
 }
 
 // DingtalkHooksNewPost response for creating dingtalk hook
 func DingtalkHooksNewPost(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.NewDingtalkHookForm)
-	ctx.Data["Title"] = ctx.Tr("repo.settings")
-	ctx.Data["PageIsSettingsHooks"] = true
-	ctx.Data["PageIsSettingsHooksNew"] = true
-	ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
-	ctx.Data["HookType"] = webhook.DINGTALK
 
-	orCtx, err := getOrgRepoCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getOrgRepoCtx", err)
-		return
-	}
-
-	if ctx.HasError() {
-		ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-		return
-	}
-
-	w := &webhook.Webhook{
-		RepoID:          orCtx.RepoID,
-		URL:             form.PayloadURL,
-		ContentType:     webhook.ContentTypeJSON,
-		HookEvent:       ParseHookEvent(form.WebhookForm),
-		IsActive:        form.Active,
-		Type:            webhook.DINGTALK,
-		Meta:            "",
-		OrgID:           orCtx.OrgID,
-		IsSystemWebhook: orCtx.IsSystemWebhook,
-	}
-	if err := w.UpdateEvent(); err != nil {
-		ctx.ServerError("UpdateEvent", err)
-		return
-	} else if err := webhook.CreateWebhook(ctx, w); err != nil {
-		ctx.ServerError("CreateWebhook", err)
-		return
-	}
-
-	ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-	ctx.Redirect(orCtx.Link)
+	createWebhook(ctx, webhookCreationParams{
+		URL:         form.PayloadURL,
+		ContentType: webhook.ContentTypeJSON,
+		WebhookForm: form.WebhookForm,
+		Type:        webhook.DINGTALK,
+	})
 }
 
 // TelegramHooksNewPost response for creating telegram hook
 func TelegramHooksNewPost(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.NewTelegramHookForm)
-	ctx.Data["Title"] = ctx.Tr("repo.settings")
-	ctx.Data["PageIsSettingsHooks"] = true
-	ctx.Data["PageIsSettingsHooksNew"] = true
-	ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
-	ctx.Data["HookType"] = webhook.TELEGRAM
 
-	orCtx, err := getOrgRepoCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getOrgRepoCtx", err)
-		return
-	}
-
-	if ctx.HasError() {
-		ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-		return
-	}
-
-	meta, err := json.Marshal(&webhook_service.TelegramMeta{
-		BotToken: form.BotToken,
-		ChatID:   form.ChatID,
+	createWebhook(ctx, webhookCreationParams{
+		URL:         fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID)),
+		ContentType: webhook.ContentTypeJSON,
+		WebhookForm: form.WebhookForm,
+		Type:        webhook.TELEGRAM,
+		Meta: &webhook_service.TelegramMeta{
+			BotToken: form.BotToken,
+			ChatID:   form.ChatID,
+		},
 	})
-	if err != nil {
-		ctx.ServerError("Marshal", err)
-		return
-	}
-
-	w := &webhook.Webhook{
-		RepoID:          orCtx.RepoID,
-		URL:             fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID)),
-		ContentType:     webhook.ContentTypeJSON,
-		HookEvent:       ParseHookEvent(form.WebhookForm),
-		IsActive:        form.Active,
-		Type:            webhook.TELEGRAM,
-		Meta:            string(meta),
-		OrgID:           orCtx.OrgID,
-		IsSystemWebhook: orCtx.IsSystemWebhook,
-	}
-	if err := w.UpdateEvent(); err != nil {
-		ctx.ServerError("UpdateEvent", err)
-		return
-	} else if err := webhook.CreateWebhook(ctx, w); err != nil {
-		ctx.ServerError("CreateWebhook", err)
-		return
-	}
-
-	ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-	ctx.Redirect(orCtx.Link)
 }
 
 // MatrixHooksNewPost response for creating a Matrix hook
 func MatrixHooksNewPost(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.NewMatrixHookForm)
-	ctx.Data["Title"] = ctx.Tr("repo.settings")
-	ctx.Data["PageIsSettingsHooks"] = true
-	ctx.Data["PageIsSettingsHooksNew"] = true
-	ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
-	ctx.Data["HookType"] = webhook.MATRIX
 
-	orCtx, err := getOrgRepoCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getOrgRepoCtx", err)
-		return
-	}
-
-	if ctx.HasError() {
-		ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-		return
-	}
-
-	meta, err := json.Marshal(&webhook_service.MatrixMeta{
-		HomeserverURL: form.HomeserverURL,
-		Room:          form.RoomID,
-		AccessToken:   form.AccessToken,
-		MessageType:   form.MessageType,
+	createWebhook(ctx, webhookCreationParams{
+		URL:         fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)),
+		ContentType: webhook.ContentTypeJSON,
+		HTTPMethod:  http.MethodPut,
+		WebhookForm: form.WebhookForm,
+		Type:        webhook.MATRIX,
+		Meta: &webhook_service.MatrixMeta{
+			HomeserverURL: form.HomeserverURL,
+			Room:          form.RoomID,
+			AccessToken:   form.AccessToken,
+			MessageType:   form.MessageType,
+		},
 	})
-	if err != nil {
-		ctx.ServerError("Marshal", err)
-		return
-	}
-
-	w := &webhook.Webhook{
-		RepoID:          orCtx.RepoID,
-		URL:             fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)),
-		ContentType:     webhook.ContentTypeJSON,
-		HTTPMethod:      "PUT",
-		HookEvent:       ParseHookEvent(form.WebhookForm),
-		IsActive:        form.Active,
-		Type:            webhook.MATRIX,
-		Meta:            string(meta),
-		OrgID:           orCtx.OrgID,
-		IsSystemWebhook: orCtx.IsSystemWebhook,
-	}
-	if err := w.UpdateEvent(); err != nil {
-		ctx.ServerError("UpdateEvent", err)
-		return
-	} else if err := webhook.CreateWebhook(ctx, w); err != nil {
-		ctx.ServerError("CreateWebhook", err)
-		return
-	}
-
-	ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-	ctx.Redirect(orCtx.Link)
 }
 
 // MSTeamsHooksNewPost response for creating MS Teams hook
 func MSTeamsHooksNewPost(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.NewMSTeamsHookForm)
-	ctx.Data["Title"] = ctx.Tr("repo.settings")
-	ctx.Data["PageIsSettingsHooks"] = true
-	ctx.Data["PageIsSettingsHooksNew"] = true
-	ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
-	ctx.Data["HookType"] = webhook.MSTEAMS
 
-	orCtx, err := getOrgRepoCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getOrgRepoCtx", err)
-		return
-	}
-
-	if ctx.HasError() {
-		ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-		return
-	}
-
-	w := &webhook.Webhook{
-		RepoID:          orCtx.RepoID,
-		URL:             form.PayloadURL,
-		ContentType:     webhook.ContentTypeJSON,
-		HookEvent:       ParseHookEvent(form.WebhookForm),
-		IsActive:        form.Active,
-		Type:            webhook.MSTEAMS,
-		Meta:            "",
-		OrgID:           orCtx.OrgID,
-		IsSystemWebhook: orCtx.IsSystemWebhook,
-	}
-	if err := w.UpdateEvent(); err != nil {
-		ctx.ServerError("UpdateEvent", err)
-		return
-	} else if err := webhook.CreateWebhook(ctx, w); err != nil {
-		ctx.ServerError("CreateWebhook", err)
-		return
-	}
-
-	ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-	ctx.Redirect(orCtx.Link)
+	createWebhook(ctx, webhookCreationParams{
+		URL:         form.PayloadURL,
+		ContentType: webhook.ContentTypeJSON,
+		WebhookForm: form.WebhookForm,
+		Type:        webhook.MSTEAMS,
+	})
 }
 
 // SlackHooksNewPost response for creating slack hook
 func SlackHooksNewPost(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.NewSlackHookForm)
-	ctx.Data["Title"] = ctx.Tr("repo.settings")
-	ctx.Data["PageIsSettingsHooks"] = true
-	ctx.Data["PageIsSettingsHooksNew"] = true
-	ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
-	ctx.Data["HookType"] = webhook.SLACK
 
-	orCtx, err := getOrgRepoCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getOrgRepoCtx", err)
-		return
-	}
-
-	if ctx.HasError() {
-		ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-		return
-	}
-
-	if form.HasInvalidChannel() {
-		ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
-		ctx.Redirect(orCtx.LinkNew + "/slack/new")
-		return
-	}
-
-	meta, err := json.Marshal(&webhook_service.SlackMeta{
-		Channel:  strings.TrimSpace(form.Channel),
-		Username: form.Username,
-		IconURL:  form.IconURL,
-		Color:    form.Color,
+	createWebhook(ctx, webhookCreationParams{
+		URL:         form.PayloadURL,
+		ContentType: webhook.ContentTypeJSON,
+		WebhookForm: form.WebhookForm,
+		Type:        webhook.SLACK,
+		Meta: &webhook_service.SlackMeta{
+			Channel:  strings.TrimSpace(form.Channel),
+			Username: form.Username,
+			IconURL:  form.IconURL,
+			Color:    form.Color,
+		},
 	})
-	if err != nil {
-		ctx.ServerError("Marshal", err)
-		return
-	}
-
-	w := &webhook.Webhook{
-		RepoID:          orCtx.RepoID,
-		URL:             form.PayloadURL,
-		ContentType:     webhook.ContentTypeJSON,
-		HookEvent:       ParseHookEvent(form.WebhookForm),
-		IsActive:        form.Active,
-		Type:            webhook.SLACK,
-		Meta:            string(meta),
-		OrgID:           orCtx.OrgID,
-		IsSystemWebhook: orCtx.IsSystemWebhook,
-	}
-	if err := w.UpdateEvent(); err != nil {
-		ctx.ServerError("UpdateEvent", err)
-		return
-	} else if err := webhook.CreateWebhook(ctx, w); err != nil {
-		ctx.ServerError("CreateWebhook", err)
-		return
-	}
-
-	ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-	ctx.Redirect(orCtx.Link)
 }
 
 // FeishuHooksNewPost response for creating feishu hook
 func FeishuHooksNewPost(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.NewFeishuHookForm)
-	ctx.Data["Title"] = ctx.Tr("repo.settings")
-	ctx.Data["PageIsSettingsHooks"] = true
-	ctx.Data["PageIsSettingsHooksNew"] = true
-	ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
-	ctx.Data["HookType"] = webhook.FEISHU
 
-	orCtx, err := getOrgRepoCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getOrgRepoCtx", err)
-		return
-	}
-
-	if ctx.HasError() {
-		ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-		return
-	}
-
-	w := &webhook.Webhook{
-		RepoID:          orCtx.RepoID,
-		URL:             form.PayloadURL,
-		ContentType:     webhook.ContentTypeJSON,
-		HookEvent:       ParseHookEvent(form.WebhookForm),
-		IsActive:        form.Active,
-		Type:            webhook.FEISHU,
-		Meta:            "",
-		OrgID:           orCtx.OrgID,
-		IsSystemWebhook: orCtx.IsSystemWebhook,
-	}
-	if err := w.UpdateEvent(); err != nil {
-		ctx.ServerError("UpdateEvent", err)
-		return
-	} else if err := webhook.CreateWebhook(ctx, w); err != nil {
-		ctx.ServerError("CreateWebhook", err)
-		return
-	}
-
-	ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-	ctx.Redirect(orCtx.Link)
+	createWebhook(ctx, webhookCreationParams{
+		URL:         form.PayloadURL,
+		ContentType: webhook.ContentTypeJSON,
+		WebhookForm: form.WebhookForm,
+		Type:        webhook.FEISHU,
+	})
 }
 
 // WechatworkHooksNewPost response for creating wechatwork hook
 func WechatworkHooksNewPost(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.NewWechatWorkHookForm)
 
-	ctx.Data["Title"] = ctx.Tr("repo.settings")
-	ctx.Data["PageIsSettingsHooks"] = true
-	ctx.Data["PageIsSettingsHooksNew"] = true
-	ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
-	ctx.Data["HookType"] = webhook.WECHATWORK
-
-	orCtx, err := getOrgRepoCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getOrgRepoCtx", err)
-		return
-	}
-
-	if ctx.HasError() {
-		ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-		return
-	}
-
-	w := &webhook.Webhook{
-		RepoID:          orCtx.RepoID,
-		URL:             form.PayloadURL,
-		ContentType:     webhook.ContentTypeJSON,
-		HookEvent:       ParseHookEvent(form.WebhookForm),
-		IsActive:        form.Active,
-		Type:            webhook.WECHATWORK,
-		Meta:            "",
-		OrgID:           orCtx.OrgID,
-		IsSystemWebhook: orCtx.IsSystemWebhook,
-	}
-	if err := w.UpdateEvent(); err != nil {
-		ctx.ServerError("UpdateEvent", err)
-		return
-	} else if err := webhook.CreateWebhook(ctx, w); err != nil {
-		ctx.ServerError("CreateWebhook", err)
-		return
-	}
-
-	ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-	ctx.Redirect(orCtx.Link)
+	createWebhook(ctx, webhookCreationParams{
+		URL:         form.PayloadURL,
+		ContentType: webhook.ContentTypeJSON,
+		WebhookForm: form.WebhookForm,
+		Type:        webhook.WECHATWORK,
+	})
 }
 
 // PackagistHooksNewPost response for creating packagist hook
 func PackagistHooksNewPost(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.NewPackagistHookForm)
-	ctx.Data["Title"] = ctx.Tr("repo.settings")
-	ctx.Data["PageIsSettingsHooks"] = true
-	ctx.Data["PageIsSettingsHooksNew"] = true
-	ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
-	ctx.Data["HookType"] = webhook.PACKAGIST
 
-	orCtx, err := getOrgRepoCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getOrgRepoCtx", err)
-		return
-	}
-
-	if ctx.HasError() {
-		ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-		return
-	}
-
-	meta, err := json.Marshal(&webhook_service.PackagistMeta{
-		Username:   form.Username,
-		APIToken:   form.APIToken,
-		PackageURL: form.PackageURL,
+	createWebhook(ctx, webhookCreationParams{
+		URL:         fmt.Sprintf("https://packagist.org/api/update-package?username=%s&apiToken=%s", url.QueryEscape(form.Username), url.QueryEscape(form.APIToken)),
+		ContentType: webhook.ContentTypeJSON,
+		WebhookForm: form.WebhookForm,
+		Type:        webhook.PACKAGIST,
+		Meta: &webhook_service.PackagistMeta{
+			Username:   form.Username,
+			APIToken:   form.APIToken,
+			PackageURL: form.PackageURL,
+		},
 	})
-	if err != nil {
-		ctx.ServerError("Marshal", err)
-		return
-	}
-
-	w := &webhook.Webhook{
-		RepoID:          orCtx.RepoID,
-		URL:             fmt.Sprintf("https://packagist.org/api/update-package?username=%s&apiToken=%s", url.QueryEscape(form.Username), url.QueryEscape(form.APIToken)),
-		ContentType:     webhook.ContentTypeJSON,
-		HookEvent:       ParseHookEvent(form.WebhookForm),
-		IsActive:        form.Active,
-		Type:            webhook.PACKAGIST,
-		Meta:            string(meta),
-		OrgID:           orCtx.OrgID,
-		IsSystemWebhook: orCtx.IsSystemWebhook,
-	}
-	if err := w.UpdateEvent(); err != nil {
-		ctx.ServerError("UpdateEvent", err)
-		return
-	} else if err := webhook.CreateWebhook(ctx, w); err != nil {
-		ctx.ServerError("CreateWebhook", err)
-		return
-	}
-
-	ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-	ctx.Redirect(orCtx.Link)
 }
 
 func checkWebhook(ctx *context.Context) (*orgRepoCtx, *webhook.Webhook) {
@@ -894,12 +579,6 @@ func SlackHooksEditPost(ctx *context.Context) {
 		return
 	}
 
-	if form.HasInvalidChannel() {
-		ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
-		ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-		return
-	}
-
 	meta, err := json.Marshal(&webhook_service.SlackMeta{
 		Channel:  strings.TrimSpace(form.Channel),
 		Username: form.Username,
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index afecc205f3..7a4a2123eb 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -17,7 +17,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web/middleware"
-	"code.gitea.io/gitea/routers/utils"
+	"code.gitea.io/gitea/services/webhook"
 
 	"gitea.com/go-chi/binding"
 )
@@ -305,14 +305,16 @@ type NewSlackHookForm struct {
 // Validate validates the fields
 func (f *NewSlackHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
 	ctx := context.GetContext(req)
+	if !webhook.IsValidSlackChannel(strings.TrimSpace(f.Channel)) {
+		errs = append(errs, binding.Error{
+			FieldNames:     []string{"Channel"},
+			Classification: "",
+			Message:        ctx.Tr("repo.settings.add_webhook.invalid_channel_name"),
+		})
+	}
 	return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
 }
 
-// HasInvalidChannel validates the channel name is in the right format
-func (f NewSlackHookForm) HasInvalidChannel() bool {
-	return !utils.IsValidSlackChannel(f.Channel)
-}
-
 // NewDiscordHookForm form for creating discord hook
 type NewDiscordHookForm struct {
 	PayloadURL string `binding:"Required;ValidUrl"`
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index 11e1d3c081..e3d0d406de 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -7,6 +7,7 @@ package webhook
 import (
 	"errors"
 	"fmt"
+	"regexp"
 	"strings"
 
 	webhook_model "code.gitea.io/gitea/models/webhook"
@@ -286,3 +287,13 @@ func GetSlackPayload(p api.Payloader, event webhook_model.HookEventType, meta st
 
 	return convertPayloader(s, p, event)
 }
+
+var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`)
+
+// IsValidSlackChannel validates a channel name conforms to what slack expects:
+// https://api.slack.com/methods/conversations.rename#naming
+// Conversation names can only contain lowercase letters, numbers, hyphens, and underscores, and must be 80 characters or less.
+// Gitea accepts if it starts with a #.
+func IsValidSlackChannel(name string) bool {
+	return slackChannel.MatchString(name)
+}
diff --git a/services/webhook/slack_test.go b/services/webhook/slack_test.go
index 8278afb69a..0f08785d25 100644
--- a/services/webhook/slack_test.go
+++ b/services/webhook/slack_test.go
@@ -170,3 +170,22 @@ func TestSlackJSONPayload(t *testing.T) {
 	require.NoError(t, err)
 	assert.NotEmpty(t, json)
 }
+
+func TestIsValidSlackChannel(t *testing.T) {
+	tt := []struct {
+		channelName string
+		expected    bool
+	}{
+		{"gitea", true},
+		{"#gitea", true},
+		{"  ", false},
+		{"#", false},
+		{" #", false},
+		{"gitea   ", false},
+		{"  gitea", false},
+	}
+
+	for _, v := range tt {
+		assert.Equal(t, v.expected, IsValidSlackChannel(v.channelName))
+	}
+}