feat: add tenant content moderation controls
This commit is contained in:
@@ -2,7 +2,9 @@ package imconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
api "hotgo/api/admin/imconfiginternal"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
@@ -14,6 +16,18 @@ import (
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
const defaultModerationTenantCode = "default"
|
||||
|
||||
var defaultModerationScenes = map[string]bool{
|
||||
"user_nickname": true, "user_signature": true, "group_name": true,
|
||||
"group_nickname": true, "private_message": true, "group_message": true,
|
||||
"group_announcement": true,
|
||||
}
|
||||
|
||||
var defaultModerationRiskActions = map[string]string{
|
||||
"high": "block", "medium": "block", "low": "audit_only", "none": "allow",
|
||||
}
|
||||
|
||||
var Config = cConfig{}
|
||||
|
||||
type cConfig struct{}
|
||||
@@ -91,6 +105,177 @@ func (c *cConfig) ThirdParty(ctx context.Context, req *api.ThirdPartyReq) (res *
|
||||
return &api.ThirdPartyRes{Values: values}, nil
|
||||
}
|
||||
|
||||
func (c *cConfig) ModerationProvider(ctx context.Context, req *api.ModerationProviderReq) (res *api.ModerationProviderRes, err error) {
|
||||
tenant, err := getTenant(ctx, dao.ImTenant.Columns().Code, defaultModerationTenantCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var row struct {
|
||||
Provider string `json:"provider"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Enabled int `json:"enabled"`
|
||||
ConfigJSON string `json:"config_json"`
|
||||
SecretJSON string `json:"secret_json"`
|
||||
}
|
||||
if err = imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
|
||||
Fields("provider", "endpoint", "enabled", "CAST(config_json AS CHAR) config_json", "CAST(secret_json AS CHAR) secret_json").
|
||||
Where("tenant_id", tenant.Id).Where("service", consts.ImExternalServiceModeration).Scan(&row); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取内容审核服务配置失败")
|
||||
}
|
||||
config := map[string]any{}
|
||||
secret := map[string]any{}
|
||||
_ = json.Unmarshal([]byte(row.ConfigJSON), &config)
|
||||
_ = json.Unmarshal([]byte(row.SecretJSON), &secret)
|
||||
res = &api.ModerationProviderRes{
|
||||
Provider: row.Provider, Endpoint: row.Endpoint, Enabled: row.Enabled == 1,
|
||||
RegionID: stringValue(config, "region_id", "cn-shanghai"),
|
||||
NicknameService: stringValue(config, "nickname_service", "nickname_detection_pro"),
|
||||
ChatService: stringValue(config, "chat_service", "chat_detection_pro"),
|
||||
TimeoutMS: intValue(config, "timeout_ms", 1500),
|
||||
FailureMode: moderationFailureMode(stringValue(config, "failure_mode", "allow")),
|
||||
AccessKeyID: stringValue(secret, "access_key_id", ""),
|
||||
AccessKeySecret: stringValue(secret, "access_key_secret", ""),
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *cConfig) ModerationPolicy(ctx context.Context, req *api.ModerationPolicyReq) (res *api.ModerationPolicyRes, err error) {
|
||||
tenant, err := getTenant(ctx, dao.ImTenant.Columns().Code, strings.TrimSpace(req.Code))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = defaultModerationPolicy(tenant)
|
||||
var tenantConfig struct {
|
||||
AliyunModerationEnabled int `json:"aliyun_moderation_enabled"`
|
||||
LocalModerationEnabled int `json:"local_moderation_enabled"`
|
||||
}
|
||||
if err = imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).
|
||||
Fields("aliyun_moderation_enabled", "local_moderation_enabled").
|
||||
Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).Scan(&tenantConfig); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取商户内容审核开关失败")
|
||||
}
|
||||
res.AliyunEnabled = tenantConfig.AliyunModerationEnabled == 1
|
||||
res.LocalWordsEnabled = tenantConfig.LocalModerationEnabled == 1
|
||||
res.Enabled = res.AliyunEnabled || res.LocalWordsEnabled
|
||||
columns := dao.ImSecurityPolicy.Columns()
|
||||
var row struct {
|
||||
PolicyVersion uint `json:"policy_version"`
|
||||
SensitiveWordPolicy string `json:"sensitive_word_policy"`
|
||||
}
|
||||
if err = imtenant.RegistryDB().Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).
|
||||
Fields(columns.PolicyVersion, "CAST("+columns.SensitiveWordPolicy+" AS CHAR) sensitive_word_policy").
|
||||
Where(columns.TenantId, tenant.Id).Where(columns.PublishStatus, consts.ImSecurityPublishPublished).
|
||||
WhereNull(columns.DeletedAt).OrderDesc(columns.PolicyVersion).Limit(1).Scan(&row); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取商户内容审核策略失败")
|
||||
}
|
||||
if row.PolicyVersion == 0 || strings.TrimSpace(row.SensitiveWordPolicy) == "" {
|
||||
return res, nil
|
||||
}
|
||||
var policy struct {
|
||||
Scenes map[string]bool `json:"scenes"`
|
||||
RiskActions map[string]string `json:"risk_actions"`
|
||||
FailureMode string `json:"failure_mode"`
|
||||
BlockMessage string `json:"block_message"`
|
||||
}
|
||||
if err = json.Unmarshal([]byte(row.SensitiveWordPolicy), &policy); err != nil {
|
||||
return nil, gerror.Wrap(err, "商户内容审核策略格式错误")
|
||||
}
|
||||
res.PolicyVersion = row.PolicyVersion
|
||||
for scene, enabled := range policy.Scenes {
|
||||
res.Scenes[scene] = enabled
|
||||
}
|
||||
for risk, action := range policy.RiskActions {
|
||||
if action == "allow" || action == "block" || action == "audit_only" {
|
||||
res.RiskActions[risk] = action
|
||||
}
|
||||
}
|
||||
res.FailureMode = moderationFailureMode(policy.FailureMode)
|
||||
if strings.TrimSpace(policy.BlockMessage) != "" {
|
||||
res.BlockMessage = strings.TrimSpace(policy.BlockMessage)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *cConfig) SensitiveWords(ctx context.Context, req *api.SensitiveWordsReq) (res *api.SensitiveWordsRes, err error) {
|
||||
tenant, err := getTenant(ctx, dao.ImTenant.Columns().Code, strings.TrimSpace(req.Code))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = &api.SensitiveWordsRes{TenantID: tenant.Id, TenantCode: tenant.Code, Items: []*api.SensitiveWordModel{}}
|
||||
columns := dao.ImSensitiveWords.Columns()
|
||||
groupColumns := dao.ImSensitiveWordGroups.Columns()
|
||||
var rows []struct {
|
||||
ID int64 `json:"id"`
|
||||
Word string `json:"word"`
|
||||
MatchType string `json:"match_type"`
|
||||
SceneJSON string `json:"scene_json"`
|
||||
Action string `json:"action"`
|
||||
Replacement string `json:"replacement"`
|
||||
RiskLevel string `json:"risk_level"`
|
||||
Priority int `json:"priority"`
|
||||
UpdatedAt *gtime.Time `json:"updated_at"`
|
||||
}
|
||||
err = imtenant.RegistryDB().Model(dao.ImSensitiveWords.Table()+" w").Safe().Ctx(ctx).
|
||||
LeftJoin(dao.ImSensitiveWordGroups.Table()+" g", "g."+groupColumns.Id+"=w."+columns.GroupId).
|
||||
Fields("w."+columns.Id, "w."+columns.Word, "w."+columns.MatchType, "CAST(w."+columns.SceneJson+" AS CHAR) scene_json", "w."+columns.Action, "w."+columns.Replacement, "w."+columns.RiskLevel, "w."+columns.Priority, "w."+columns.UpdatedAt).
|
||||
Where("w."+columns.TenantId, tenant.Id).Where("w."+columns.Status, consts.StatusEnabled).WhereNull("w."+columns.DeletedAt).
|
||||
Where("g."+groupColumns.Status, consts.StatusEnabled).WhereNull("g." + groupColumns.DeletedAt).
|
||||
OrderDesc("w." + columns.Priority).OrderAsc("w." + columns.Id).Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取商户有效敏感词失败")
|
||||
}
|
||||
for _, row := range rows {
|
||||
var scenes []string
|
||||
_ = json.Unmarshal([]byte(row.SceneJSON), &scenes)
|
||||
res.Items = append(res.Items, &api.SensitiveWordModel{ID: row.ID, Word: row.Word, MatchType: row.MatchType, Scenes: scenes, Action: row.Action, Replacement: row.Replacement, RiskLevel: row.RiskLevel, Priority: row.Priority})
|
||||
if row.UpdatedAt != nil && row.UpdatedAt.TimestampMilli() > res.Version {
|
||||
res.Version = row.UpdatedAt.TimestampMilli()
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func defaultModerationPolicy(tenant *entity.ImTenant) *api.ModerationPolicyRes {
|
||||
return &api.ModerationPolicyRes{TenantID: tenant.Id, TenantCode: tenant.Code, Enabled: false,
|
||||
AliyunEnabled: false, LocalWordsEnabled: false, Scenes: cloneBoolMap(defaultModerationScenes),
|
||||
RiskActions: cloneStringMap(defaultModerationRiskActions), FailureMode: "allow",
|
||||
BlockMessage: "内容不符合平台规范,请修改后重试"}
|
||||
}
|
||||
|
||||
func cloneBoolMap(src map[string]bool) map[string]bool {
|
||||
dst := make(map[string]bool, len(src))
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
func cloneStringMap(src map[string]string) map[string]string {
|
||||
dst := make(map[string]string, len(src))
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
func stringValue(values map[string]any, key, fallback string) string {
|
||||
if value, ok := values[key].(string); ok && strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
func intValue(values map[string]any, key string, fallback int) int {
|
||||
value := gconv.Int(values[key])
|
||||
if value <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
func moderationFailureMode(value string) string {
|
||||
if strings.TrimSpace(value) == "block" {
|
||||
return "block"
|
||||
}
|
||||
return "allow"
|
||||
}
|
||||
|
||||
func (c *cConfig) TenantLoginIPWhitelist(ctx context.Context, req *api.TenantLoginIPWhitelistReq) (res *api.TenantLoginIPWhitelistRes, err error) {
|
||||
if _, err = getTenant(ctx, dao.ImTenant.Columns().Id, req.Id); err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user