Merge branch 'dev' of https://git.118im.com/im_team/im_server into dev
This commit is contained in:
@@ -196,6 +196,21 @@ func (c *adminInternalClient) SensitiveWords(tenantCode string) (*types.Sensitiv
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *adminInternalClient) SecurityPolicy(tenantCode string) (*types.SecurityPolicy, error) {
|
||||||
|
var result types.SecurityPolicy
|
||||||
|
path := "/internal/im-config/tenants/" + url.PathEscape(strings.TrimSpace(tenantCode)) + "/security-policy"
|
||||||
|
if err := c.getJSON(path, nil, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if result.FriendDisabledPolicy == nil {
|
||||||
|
result.FriendDisabledPolicy = map[string]any{}
|
||||||
|
}
|
||||||
|
if result.CurfewRules == nil {
|
||||||
|
result.CurfewRules = []*types.MessageCurfewRule{}
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *adminInternalClient) TenantLoginIPWhitelist(id types.TenantID) (*tenantLoginIPWhitelistConfig, error) {
|
func (c *adminInternalClient) TenantLoginIPWhitelist(id types.TenantID) (*tenantLoginIPWhitelistConfig, error) {
|
||||||
if !id.IsValid() {
|
if !id.IsValid() {
|
||||||
return nil, types.ErrMalformed
|
return nil, types.ErrMalformed
|
||||||
|
|||||||
@@ -864,6 +864,9 @@ func createIMUserProductTables(tx *sql.Tx) error {
|
|||||||
region VARCHAR(128) NOT NULL DEFAULT '',
|
region VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
email VARCHAR(128) NOT NULL DEFAULT '',
|
email VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
public_account VARCHAR(64) NOT NULL DEFAULT '',
|
public_account VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
friend_add_enabled TINYINT NOT NULL DEFAULT 1,
|
||||||
|
friend_add_disabled_reason VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
friend_add_policy_version INT NOT NULL DEFAULT 1,
|
||||||
version INT NOT NULL DEFAULT 1,
|
version INT NOT NULL DEFAULT 1,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
|||||||
@@ -3192,7 +3192,7 @@ func (a *adapter) ensureUserProfile(ctx context.Context, uid t.Uid) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_profiles(id,user_id,public_account) VALUES(?,?,?)`, profileID, decodeUID(uid), uid.UserId()); err != nil {
|
if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_profiles(id,user_id,public_account,friend_add_enabled,friend_add_policy_version) VALUES(?,?,?,?,?)`, profileID, decodeUID(uid), uid.UserId(), true, 1); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -3885,35 +3885,41 @@ func (a *adapter) feedbackByIdempotency(ctx context.Context, uid t.Uid, idempote
|
|||||||
|
|
||||||
func (a *adapter) userProfileByUID(ctx context.Context, uid t.Uid) (*t.UserProfile, error) {
|
func (a *adapter) userProfileByUID(ctx context.Context, uid t.Uid) (*t.UserProfile, error) {
|
||||||
row := struct {
|
row := struct {
|
||||||
UserID int64 `db:"user_id"`
|
UserID int64 `db:"user_id"`
|
||||||
AvatarFileID string `db:"avatar_file_id"`
|
AvatarFileID string `db:"avatar_file_id"`
|
||||||
Nickname string
|
Nickname string
|
||||||
BirthDate sql.NullTime `db:"birth_date"`
|
BirthDate sql.NullTime `db:"birth_date"`
|
||||||
Signature string
|
Signature string
|
||||||
Gender string
|
Gender string
|
||||||
Region string
|
Region string
|
||||||
Email string
|
Email string
|
||||||
PublicAccount string `db:"public_account"`
|
PublicAccount string `db:"public_account"`
|
||||||
Version int
|
FriendAddEnabled bool `db:"friend_add_enabled"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
FriendAddDisabledReason string `db:"friend_add_disabled_reason"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
FriendAddPolicyVersion int `db:"friend_add_policy_version"`
|
||||||
|
Version int
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}{}
|
}{}
|
||||||
if err := sqlx.GetContext(ctx, a.db, &row, `SELECT user_id,avatar_file_id,nickname,birth_date,signature,gender,region,email,
|
if err := sqlx.GetContext(ctx, a.db, &row, `SELECT user_id,avatar_file_id,nickname,birth_date,signature,gender,region,email,
|
||||||
public_account,version,created_at,updated_at FROM im_user_profiles WHERE user_id=?`, decodeUID(uid)); err != nil {
|
public_account,friend_add_enabled,friend_add_disabled_reason,friend_add_policy_version,version,created_at,updated_at FROM im_user_profiles WHERE user_id=?`, decodeUID(uid)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
profile := &t.UserProfile{
|
profile := &t.UserProfile{
|
||||||
UserID: store.EncodeUid(row.UserID),
|
UserID: store.EncodeUid(row.UserID),
|
||||||
AvatarFileID: row.AvatarFileID,
|
AvatarFileID: row.AvatarFileID,
|
||||||
Nickname: row.Nickname,
|
Nickname: row.Nickname,
|
||||||
Signature: row.Signature,
|
Signature: row.Signature,
|
||||||
Gender: row.Gender,
|
Gender: row.Gender,
|
||||||
Region: row.Region,
|
Region: row.Region,
|
||||||
Email: row.Email,
|
Email: row.Email,
|
||||||
PublicAccount: row.PublicAccount,
|
PublicAccount: row.PublicAccount,
|
||||||
Version: row.Version,
|
FriendAddEnabled: row.FriendAddEnabled,
|
||||||
CreatedAt: row.CreatedAt,
|
FriendAddDisabledReason: row.FriendAddDisabledReason,
|
||||||
UpdatedAt: row.UpdatedAt,
|
FriendAddPolicyVersion: row.FriendAddPolicyVersion,
|
||||||
|
Version: row.Version,
|
||||||
|
CreatedAt: row.CreatedAt,
|
||||||
|
UpdatedAt: row.UpdatedAt,
|
||||||
}
|
}
|
||||||
if row.BirthDate.Valid {
|
if row.BirthDate.Valid {
|
||||||
profile.BirthDate = row.BirthDate.Time.Format("2006-01-02")
|
profile.BirthDate = row.BirthDate.Time.Format("2006-01-02")
|
||||||
|
|||||||
@@ -65,7 +65,11 @@ func ensureUserIdentitySchema(db *sqlx.DB) error {
|
|||||||
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',nickname VARCHAR(64) NOT NULL DEFAULT '',
|
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',nickname VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
birth_date DATE,signature VARCHAR(256) NOT NULL DEFAULT '',gender VARCHAR(16) NOT NULL DEFAULT '',
|
birth_date DATE,signature VARCHAR(256) NOT NULL DEFAULT '',gender VARCHAR(16) NOT NULL DEFAULT '',
|
||||||
region VARCHAR(128) NOT NULL DEFAULT '',email VARCHAR(128) NOT NULL DEFAULT '',
|
region VARCHAR(128) NOT NULL DEFAULT '',email VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
public_account VARCHAR(64) NOT NULL DEFAULT '',version INT NOT NULL DEFAULT 1,
|
public_account VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
friend_add_enabled TINYINT NOT NULL DEFAULT 1,
|
||||||
|
friend_add_disabled_reason VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
friend_add_policy_version INT NOT NULL DEFAULT 1,
|
||||||
|
version INT NOT NULL DEFAULT 1,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(id),UNIQUE INDEX uk_im_user_profiles_user(`+profileUniqueKey+`))`); err != nil {
|
PRIMARY KEY(id),UNIQUE INDEX uk_im_user_profiles_user(`+profileUniqueKey+`))`); err != nil {
|
||||||
@@ -99,6 +103,27 @@ func ensureUserIdentitySchema(db *sqlx.DB) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if exists, err := identityColumnExists(ctx, conn, "im_user_profiles", "friend_add_enabled"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !exists {
|
||||||
|
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_user_profiles ADD COLUMN friend_add_enabled TINYINT NOT NULL DEFAULT 1 AFTER public_account"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if exists, err := identityColumnExists(ctx, conn, "im_user_profiles", "friend_add_disabled_reason"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !exists {
|
||||||
|
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_user_profiles ADD COLUMN friend_add_disabled_reason VARCHAR(255) NOT NULL DEFAULT '' AFTER friend_add_enabled"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if exists, err := identityColumnExists(ctx, conn, "im_user_profiles", "friend_add_policy_version"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !exists {
|
||||||
|
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_user_profiles ADD COLUMN friend_add_policy_version INT NOT NULL DEFAULT 1 AFTER friend_add_disabled_reason"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = backfillUserPhones(ctx, conn, usersScoped); err != nil {
|
if err = backfillUserPhones(ctx, conn, usersScoped); err != nil {
|
||||||
|
|||||||
+47
-1
@@ -3635,6 +3635,10 @@ func serveIMFriendRequestCreate(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "invalid friend request source", nil)
|
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "invalid friend request source", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if decision := imFriendAddPolicyDecision(principal.TenantID, "", principal.Uid, source); !decision.Allowed {
|
||||||
|
writeIMPolicyDecision(wrt, req, decision)
|
||||||
|
return
|
||||||
|
}
|
||||||
privacy, err := store.Profiles.GetPrivacy(principal.TenantID, target)
|
privacy, err := store.Profiles.GetPrivacy(principal.TenantID, target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeIMRelationError(wrt, req, err)
|
writeIMRelationError(wrt, req, err)
|
||||||
@@ -4048,10 +4052,14 @@ func serveIMGroupMembers(wrt http.ResponseWriter, req *http.Request, topic strin
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
remarks := groupMemberRemarkMap(principal.TenantID, principal.Uid, members)
|
remarks := groupMemberRemarkMap(principal.TenantID, principal.Uid, members)
|
||||||
|
hideFriendEntrypoint := !imFriendGlobalDisabledDecision(principal.TenantID, "", "group").Allowed
|
||||||
items := make([]map[string]any, 0, limit)
|
items := make([]map[string]any, 0, limit)
|
||||||
for _, member := range members {
|
for _, member := range members {
|
||||||
access := groupMemberAccess(member)
|
access := groupMemberAccess(member)
|
||||||
item := groupBusinessMemberPayload(member, remarks[member.UserID])
|
item := groupBusinessMemberPayload(member, remarks[member.UserID])
|
||||||
|
if hideFriendEntrypoint && member.UserID != principal.Uid {
|
||||||
|
maskIMGroupMemberProfileEntrypoint(item)
|
||||||
|
}
|
||||||
if eligibleRole != "" {
|
if eligibleRole != "" {
|
||||||
eligible, reason := groupRoleEligibility(selfAccess, access, eligibleRole)
|
eligible, reason := groupRoleEligibility(selfAccess, access, eligibleRole)
|
||||||
item["eligible"] = eligible
|
item["eligible"] = eligible
|
||||||
@@ -5467,6 +5475,12 @@ func serveIMGroupMemberProfile(wrt http.ResponseWriter, req *http.Request, topic
|
|||||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "user_id"})
|
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "user_id"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if memberID != principal.Uid {
|
||||||
|
if decision := imFriendGlobalDisabledDecision(principal.TenantID, "", "group"); !decision.Allowed {
|
||||||
|
writeIMPolicyDecision(wrt, req, decision)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
selfSub, err := store.Subs.Get(principal.TenantID, topic, principal.Uid, false)
|
selfSub, err := store.Subs.Get(principal.TenantID, topic, principal.Uid, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||||
@@ -5519,6 +5533,10 @@ func serveIMMeQRCodes(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "purpose"})
|
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "purpose"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if decision := imFriendGlobalDisabledDecision(principal.TenantID, "", "qr"); !decision.Allowed {
|
||||||
|
writeIMPolicyDecision(wrt, req, decision)
|
||||||
|
return
|
||||||
|
}
|
||||||
expiresIn := body.ExpiresInSeconds
|
expiresIn := body.ExpiresInSeconds
|
||||||
if expiresIn <= 0 {
|
if expiresIn <= 0 {
|
||||||
expiresIn = imQRCodeDefaultExpiresInSeconds
|
expiresIn = imQRCodeDefaultExpiresInSeconds
|
||||||
@@ -5624,6 +5642,10 @@ func serveIMQRCodesResolve(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
writeIMRelationError(wrt, req, err)
|
writeIMRelationError(wrt, req, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if decision := imFriendGlobalDisabledDecision(principal.TenantID, "", "qr"); !decision.Allowed {
|
||||||
|
writeIMPolicyDecision(wrt, req, decision)
|
||||||
|
return
|
||||||
|
}
|
||||||
canAdd := ctx.Relationship == types.RelationshipNone && imPrivacyAllowsAdd(privacy, "qr")
|
canAdd := ctx.Relationship == types.RelationshipNone && imPrivacyAllowsAdd(privacy, "qr")
|
||||||
action := imRelationshipAction(ctx.Relationship)
|
action := imRelationshipAction(ctx.Relationship)
|
||||||
if ctx.Relationship == types.RelationshipNone && !canAdd {
|
if ctx.Relationship == types.RelationshipNone && !canAdd {
|
||||||
@@ -5665,8 +5687,14 @@ func serveIMUserSearch(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
payload := make([]map[string]any, 0, len(items))
|
payload := make([]map[string]any, 0, len(items))
|
||||||
|
friendSearchAllowed := imFriendGlobalDisabledDecision(principal.TenantID, "", "search").Allowed
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
payload = append(payload, userSearchItemPayload(item))
|
data := userSearchItemPayload(item)
|
||||||
|
if !friendSearchAllowed && item.Relationship == types.RelationshipNone {
|
||||||
|
data["can_add"] = false
|
||||||
|
data["action"] = "none"
|
||||||
|
}
|
||||||
|
payload = append(payload, data)
|
||||||
}
|
}
|
||||||
writeIMSuccess(wrt, req, map[string]any{"items": payload, "next_cursor": nextCursor})
|
writeIMSuccess(wrt, req, map[string]any{"items": payload, "next_cursor": nextCursor})
|
||||||
}
|
}
|
||||||
@@ -5738,6 +5766,12 @@ func serveIMUserProfile(wrt http.ResponseWriter, req *http.Request, identifier s
|
|||||||
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "profile is private", nil)
|
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "profile is private", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if ctx.Relationship == types.RelationshipNone {
|
||||||
|
if decision := imFriendGlobalDisabledDecision(principal.TenantID, "", "profile"); !decision.Allowed {
|
||||||
|
writeIMPolicyDecision(wrt, req, decision)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
writeIMSuccess(wrt, req, userProfilePayload(user, ctx))
|
writeIMSuccess(wrt, req, userProfilePayload(user, ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5795,6 +5829,10 @@ func serveIMUserQRCodes(wrt http.ResponseWriter, req *http.Request, identifier s
|
|||||||
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
|
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if decision := imFriendGlobalDisabledDecision(principal.TenantID, "", "qr"); !decision.Allowed {
|
||||||
|
writeIMPolicyDecision(wrt, req, decision)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
expiresIn := body.ExpiresInSeconds
|
expiresIn := body.ExpiresInSeconds
|
||||||
if expiresIn <= 0 {
|
if expiresIn <= 0 {
|
||||||
@@ -6932,6 +6970,14 @@ func groupBusinessMemberPayload(member types.GroupMember, remark string) map[str
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func maskIMGroupMemberProfileEntrypoint(data map[string]any) {
|
||||||
|
for _, key := range []string{"avatar_file_id", "public_account", "signature", "gender", "region"} {
|
||||||
|
delete(data, key)
|
||||||
|
}
|
||||||
|
data["can_add"] = false
|
||||||
|
data["action"] = "none"
|
||||||
|
}
|
||||||
|
|
||||||
func copyUserNickname(data map[string]any, public any) {
|
func copyUserNickname(data map[string]any, public any) {
|
||||||
var fields map[string]any
|
var fields map[string]any
|
||||||
if value, ok := public.(map[string]any); ok {
|
if value, ok := public.(map[string]any); ok {
|
||||||
|
|||||||
@@ -0,0 +1,330 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tinode/chat/server/logs"
|
||||||
|
"github.com/tinode/chat/server/store"
|
||||||
|
"github.com/tinode/chat/server/store/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
imReasonFriendAddDisabled = "FRIEND_ADD_DISABLED"
|
||||||
|
imReasonMessageCurfew = "MESSAGE_CURFEW_ACTIVE"
|
||||||
|
)
|
||||||
|
|
||||||
|
type imPolicyDecision struct {
|
||||||
|
Allowed bool
|
||||||
|
Reason string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func imAllowDecision() imPolicyDecision {
|
||||||
|
return imPolicyDecision{Allowed: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func imDenyDecision(reason, message string) imPolicyDecision {
|
||||||
|
return imPolicyDecision{Allowed: false, Reason: reason, Message: strings.TrimSpace(message)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func imTenantCodeForPolicy(tenantID types.TenantID) (string, error) {
|
||||||
|
tenant, err := store.Tenants.GetByID(tenantID)
|
||||||
|
if err != nil || tenant == nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(tenant.Code), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func imSecurityPolicy(tenantID types.TenantID, tenantCode string) (*types.SecurityPolicy, error) {
|
||||||
|
tenantCode = strings.TrimSpace(tenantCode)
|
||||||
|
if tenantCode == "" {
|
||||||
|
code, err := imTenantCodeForPolicy(tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tenantCode = code
|
||||||
|
}
|
||||||
|
if tenantCode == "" {
|
||||||
|
return nil, types.ErrNotFound
|
||||||
|
}
|
||||||
|
policy, err := store.ModerationConfig.SecurityPolicy(tenantCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func imFriendAddPolicyDecision(tenantID types.TenantID, tenantCode string, actor types.Uid, source string) imPolicyDecision {
|
||||||
|
policy, err := imSecurityPolicy(tenantID, tenantCode)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, types.ErrUnsupported) && !errors.Is(err, types.ErrNotFound) {
|
||||||
|
logs.Warn.Printf("friend add policy lookup failed: tenant=%d code=%s user=%s err=%v", tenantID, tenantCode, actor.UserId(), err)
|
||||||
|
}
|
||||||
|
return imAllowDecision()
|
||||||
|
}
|
||||||
|
if policy != nil && !policy.GlobalFriendEnabled {
|
||||||
|
entry := imNormalizeFriendAddEntry(source)
|
||||||
|
if imFriendEntryBlocked(policy.FriendDisabledPolicy, entry) {
|
||||||
|
return imDenyDecision(imReasonFriendAddDisabled, imPolicyString(policy.FriendDisabledPolicy, "message", "当前时段暂不允许添加好友"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profile, err := store.Profiles.Get(tenantID, actor)
|
||||||
|
if err != nil {
|
||||||
|
logs.Warn.Printf("friend add user policy lookup failed: tenant=%d user=%s err=%v", tenantID, actor.UserId(), err)
|
||||||
|
return imAllowDecision()
|
||||||
|
}
|
||||||
|
if profile != nil && !profile.FriendAddEnabled {
|
||||||
|
message := strings.TrimSpace(profile.FriendAddDisabledReason)
|
||||||
|
if message == "" {
|
||||||
|
message = "该用户暂不允许发起加好友"
|
||||||
|
}
|
||||||
|
return imDenyDecision(imReasonFriendAddDisabled, message)
|
||||||
|
}
|
||||||
|
return imAllowDecision()
|
||||||
|
}
|
||||||
|
|
||||||
|
func imFriendGlobalDisabledDecision(tenantID types.TenantID, tenantCode, source string) imPolicyDecision {
|
||||||
|
policy, err := imSecurityPolicy(tenantID, tenantCode)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, types.ErrUnsupported) && !errors.Is(err, types.ErrNotFound) {
|
||||||
|
logs.Warn.Printf("friend global policy lookup failed: tenant=%d code=%s source=%s err=%v", tenantID, tenantCode, source, err)
|
||||||
|
}
|
||||||
|
return imAllowDecision()
|
||||||
|
}
|
||||||
|
if policy != nil && !policy.GlobalFriendEnabled && imFriendEntryBlocked(policy.FriendDisabledPolicy, imNormalizeFriendAddEntry(source)) {
|
||||||
|
return imDenyDecision(imReasonFriendAddDisabled, imPolicyString(policy.FriendDisabledPolicy, "message", "当前时段暂不允许添加好友"))
|
||||||
|
}
|
||||||
|
return imAllowDecision()
|
||||||
|
}
|
||||||
|
|
||||||
|
func imNormalizeFriendAddEntry(source string) string {
|
||||||
|
switch strings.TrimSpace(source) {
|
||||||
|
case "qr":
|
||||||
|
return "qrcode"
|
||||||
|
case "group":
|
||||||
|
return "group_member"
|
||||||
|
case "profile", "profile_card":
|
||||||
|
return "profile_card"
|
||||||
|
case "invite", "invite_auto_add":
|
||||||
|
return "invite_auto_add"
|
||||||
|
case "recommend", "recommendation":
|
||||||
|
return "recommendation"
|
||||||
|
default:
|
||||||
|
return "search"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func imFriendEntryBlocked(policy map[string]any, entry string) bool {
|
||||||
|
if len(policy) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
raw, ok := policy["blocked_entries"]
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
entries := imAnyStringSlice(raw)
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
entry = strings.TrimSpace(entry)
|
||||||
|
for _, item := range entries {
|
||||||
|
if strings.TrimSpace(item) == entry {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func imMessageCurfewDecision(tenantID types.TenantID, tenantCode string, actor types.Uid, topic string, cat types.TopicCat) imPolicyDecision {
|
||||||
|
if cat != types.TopicCatP2P {
|
||||||
|
return imAllowDecision()
|
||||||
|
}
|
||||||
|
policy, err := imSecurityPolicy(tenantID, tenantCode)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, types.ErrUnsupported) && !errors.Is(err, types.ErrNotFound) {
|
||||||
|
logs.Warn.Printf("message curfew policy lookup failed: tenant=%d code=%s user=%s topic=%s err=%v", tenantID, tenantCode, actor.UserId(), topic, err)
|
||||||
|
}
|
||||||
|
return imAllowDecision()
|
||||||
|
}
|
||||||
|
if policy == nil || !policy.MessageCurfewEnabled {
|
||||||
|
return imAllowDecision()
|
||||||
|
}
|
||||||
|
scope := "private_message"
|
||||||
|
if cat == types.TopicCatGrp {
|
||||||
|
scope = "group_message"
|
||||||
|
}
|
||||||
|
for _, rule := range policy.CurfewRules {
|
||||||
|
if rule == nil || rule.Status != 1 || !imCurfewRuleScopeMatches(rule.Scope, scope) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if imCurfewWhitelisted(rule, actor, topic, cat) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if imCurfewRuleActive(rule, time.Now()) {
|
||||||
|
message := strings.TrimSpace(rule.Message)
|
||||||
|
if message == "" {
|
||||||
|
message = "当前时段暂不能发送消息"
|
||||||
|
}
|
||||||
|
return imDenyDecision(imReasonMessageCurfew, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imAllowDecision()
|
||||||
|
}
|
||||||
|
|
||||||
|
func imCurfewRuleScopeMatches(ruleScope, scope string) bool {
|
||||||
|
ruleScope = strings.TrimSpace(ruleScope)
|
||||||
|
return ruleScope == "" || ruleScope == "all" || ruleScope == scope
|
||||||
|
}
|
||||||
|
|
||||||
|
func imCurfewWhitelisted(rule *types.MessageCurfewRule, actor types.Uid, topic string, cat types.TopicCat) bool {
|
||||||
|
uidValues := map[string]bool{
|
||||||
|
actor.UserId(): true,
|
||||||
|
actor.String(): true,
|
||||||
|
}
|
||||||
|
if raw := store.DecodeUid(actor); raw > 0 {
|
||||||
|
uidValues[strconv.FormatInt(raw, 10)] = true
|
||||||
|
}
|
||||||
|
for _, item := range rule.WhitelistUserIds {
|
||||||
|
if uidValues[strings.TrimSpace(item)] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cat == types.TopicCatGrp {
|
||||||
|
for _, item := range rule.WhitelistGroupIds {
|
||||||
|
if strings.TrimSpace(item) == topic {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func imCurfewRuleActive(rule *types.MessageCurfewRule, now time.Time) bool {
|
||||||
|
loc := time.Local
|
||||||
|
if tz := strings.TrimSpace(rule.Timezone); tz != "" {
|
||||||
|
if loaded, err := time.LoadLocation(tz); err == nil {
|
||||||
|
loc = loaded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
localNow := now.In(loc)
|
||||||
|
if !imCurfewDateMatches(rule, localNow) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
start, okStart := imParseClock(rule.StartTime)
|
||||||
|
end, okEnd := imParseClock(rule.EndTime)
|
||||||
|
if !okStart || !okEnd {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
minute := localNow.Hour()*60 + localNow.Minute()
|
||||||
|
if start == end {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if start < end {
|
||||||
|
return minute >= start && minute < end
|
||||||
|
}
|
||||||
|
return minute >= start || minute < end
|
||||||
|
}
|
||||||
|
|
||||||
|
func imCurfewDateMatches(rule *types.MessageCurfewRule, now time.Time) bool {
|
||||||
|
switch strings.TrimSpace(rule.DateType) {
|
||||||
|
case "", "daily":
|
||||||
|
return true
|
||||||
|
case "weekday":
|
||||||
|
return now.Weekday() >= time.Monday && now.Weekday() <= time.Friday
|
||||||
|
case "weekend":
|
||||||
|
return now.Weekday() == time.Saturday || now.Weekday() == time.Sunday
|
||||||
|
case "custom":
|
||||||
|
date := now.Format("2006-01-02")
|
||||||
|
for _, item := range rule.CustomDates {
|
||||||
|
if strings.TrimSpace(item) == date {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
if len(rule.Weekdays) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
weekday := int(now.Weekday())
|
||||||
|
if weekday == 0 {
|
||||||
|
weekday = 7
|
||||||
|
}
|
||||||
|
for _, item := range rule.Weekdays {
|
||||||
|
if item == weekday {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func imParseClock(value string) (int, bool) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
parts := strings.Split(value, ":")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
hour, errHour := strconv.Atoi(parts[0])
|
||||||
|
minute, errMinute := strconv.Atoi(parts[1])
|
||||||
|
if errHour != nil || errMinute != nil || hour < 0 || hour > 23 || minute < 0 || minute > 59 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return hour*60 + minute, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func imPolicyString(policy map[string]any, key, fallback string) string {
|
||||||
|
if value := strings.TrimSpace(fmt.Sprint(policy[key])); value != "" && value != "<nil>" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func imAnyStringSlice(raw any) []string {
|
||||||
|
switch value := raw.(type) {
|
||||||
|
case []string:
|
||||||
|
return value
|
||||||
|
case []any:
|
||||||
|
out := make([]string, 0, len(value))
|
||||||
|
for _, item := range value {
|
||||||
|
if text := strings.TrimSpace(fmt.Sprint(item)); text != "" && text != "<nil>" {
|
||||||
|
out = append(out, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeIMPolicyDecision(wrt http.ResponseWriter, req *http.Request, decision imPolicyDecision) {
|
||||||
|
message := strings.TrimSpace(decision.Message)
|
||||||
|
if message == "" {
|
||||||
|
message = "permission denied"
|
||||||
|
}
|
||||||
|
reason := strings.TrimSpace(decision.Reason)
|
||||||
|
if reason == "" {
|
||||||
|
reason = imReasonPermissionDenied
|
||||||
|
}
|
||||||
|
writeIMError(wrt, req, http.StatusForbidden, 30005, reason, message, map[string]any{
|
||||||
|
"reason": reason,
|
||||||
|
"message": message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func imCurfewPolicyReply(msg *ClientComMessage, decision imPolicyDecision) *ServerComMessage {
|
||||||
|
reply := ErrPolicyReply(msg, types.TimeNow())
|
||||||
|
reply.Ctrl.Text = "message curfew active"
|
||||||
|
reply.Ctrl.Params = map[string]any{
|
||||||
|
"reason": decision.Reason,
|
||||||
|
"message": decision.Message,
|
||||||
|
}
|
||||||
|
return reply
|
||||||
|
}
|
||||||
@@ -239,7 +239,7 @@ func internalAdminQuerySpec(resource string) (*internalQuerySpec, bool) {
|
|||||||
func usersInternalQuerySpec() *internalQuerySpec {
|
func usersInternalQuerySpec() *internalQuerySpec {
|
||||||
return &internalQuerySpec{
|
return &internalQuerySpec{
|
||||||
Select: `SELECT CAST(u.id AS CHAR) AS user_id,u.account_no AS display_no,u.username,u.phone,u.state,u.stateat AS state_at,u.access,u.lastseen AS last_seen,u.useragent AS user_agent,u.public,u.trusted,
|
Select: `SELECT CAST(u.id AS CHAR) AS user_id,u.account_no AS display_no,u.username,u.phone,u.state,u.stateat AS state_at,u.access,u.lastseen AS last_seen,u.useragent AS user_agent,u.public,u.trusted,
|
||||||
p.nickname,p.public_account,p.gender,p.region,p.avatar_file_id,
|
p.nickname,p.public_account,p.gender,p.region,p.avatar_file_id,COALESCE(p.friend_add_enabled,1) AS friend_add_enabled,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
NULLIF(avatar.location,''),
|
NULLIF(avatar.location,''),
|
||||||
NULLIF(CASE WHEN p.avatar_file_id REGEXP '^(https?://|data:)' THEN p.avatar_file_id ELSE '' END,''),
|
NULLIF(CASE WHEN p.avatar_file_id REGEXP '^(https?://|data:)' THEN p.avatar_file_id ELSE '' END,''),
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ func executeKafkaConfigEvent(envelope kafkaEnvelope) error {
|
|||||||
store.InvalidateAppReleaseConfigCache()
|
store.InvalidateAppReleaseConfigCache()
|
||||||
case configEventSecurityPolicyUpdated:
|
case configEventSecurityPolicyUpdated:
|
||||||
store.InvalidateModerationConfigCache(tenantCode, false, true, false)
|
store.InvalidateModerationConfigCache(tenantCode, false, true, false)
|
||||||
|
store.InvalidateSecurityPolicyCache(tenantCode)
|
||||||
case configEventSensitiveWordsUpdated:
|
case configEventSensitiveWordsUpdated:
|
||||||
store.InvalidateModerationConfigCache(tenantCode, false, false, true)
|
store.InvalidateModerationConfigCache(tenantCode, false, false, true)
|
||||||
default:
|
default:
|
||||||
@@ -107,6 +108,7 @@ func executeKafkaConfigEvent(envelope kafkaEnvelope) error {
|
|||||||
store.InvalidateRegistrationConfigCache()
|
store.InvalidateRegistrationConfigCache()
|
||||||
store.InvalidateAppReleaseConfigCache()
|
store.InvalidateAppReleaseConfigCache()
|
||||||
store.InvalidateModerationConfigCache(tenantCode, true, true, true)
|
store.InvalidateModerationConfigCache(tenantCode, true, true, true)
|
||||||
|
store.InvalidateSecurityPolicyCache(tenantCode)
|
||||||
}
|
}
|
||||||
logs.Info.Printf("kafka config event applied: event=%s type=%s tenant=%s group=%s", envelope.EventID, envelope.EventType, tenantCode, payload.Group)
|
logs.Info.Printf("kafka config event applied: event=%s type=%s tenant=%s group=%s", envelope.EventID, envelope.EventType, tenantCode, payload.Group)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ type testConfig struct {
|
|||||||
func (c testConfig) Provider() (*types.ModerationProviderConfig, error) {
|
func (c testConfig) Provider() (*types.ModerationProviderConfig, error) {
|
||||||
return &types.ModerationProviderConfig{}, nil
|
return &types.ModerationProviderConfig{}, nil
|
||||||
}
|
}
|
||||||
func (c testConfig) Policy(string) (*types.ModerationPolicy, error) { return c.policy, nil }
|
func (c testConfig) Policy(string) (*types.ModerationPolicy, error) { return c.policy, nil }
|
||||||
func (c testConfig) Words(string) (*types.SensitiveWordSet, error) { return c.words, nil }
|
func (c testConfig) Words(string) (*types.SensitiveWordSet, error) { return c.words, nil }
|
||||||
|
func (c testConfig) SecurityPolicy(string) (*types.SecurityPolicy, error) { return nil, nil }
|
||||||
|
|
||||||
type testAudit struct{}
|
type testAudit struct{}
|
||||||
|
|
||||||
|
|||||||
@@ -64,9 +64,18 @@ var (
|
|||||||
moderationProviderCache = newConfigCache[*types.ModerationProviderConfig]()
|
moderationProviderCache = newConfigCache[*types.ModerationProviderConfig]()
|
||||||
moderationPolicyCache = newConfigCache[*types.ModerationPolicy]()
|
moderationPolicyCache = newConfigCache[*types.ModerationPolicy]()
|
||||||
moderationWordsCache = newConfigCache[*types.SensitiveWordSet]()
|
moderationWordsCache = newConfigCache[*types.SensitiveWordSet]()
|
||||||
|
securityPolicyCache = newConfigCache[*types.SecurityPolicy]()
|
||||||
thirdPartyConfigAllKeys = "*"
|
thirdPartyConfigAllKeys = "*"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func InvalidateSecurityPolicyCache(tenantCode string) {
|
||||||
|
if strings.TrimSpace(tenantCode) == "" {
|
||||||
|
securityPolicyCache.clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
securityPolicyCache.delete(strings.TrimSpace(tenantCode))
|
||||||
|
}
|
||||||
|
|
||||||
func InvalidateModerationConfigCache(tenantCode string, provider, policy, words bool) {
|
func InvalidateModerationConfigCache(tenantCode string, provider, policy, words bool) {
|
||||||
if provider {
|
if provider {
|
||||||
moderationProviderCache.clear()
|
moderationProviderCache.clear()
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ type AdminInternalConfigProvider interface {
|
|||||||
ModerationProvider() (*types.ModerationProviderConfig, error)
|
ModerationProvider() (*types.ModerationProviderConfig, error)
|
||||||
ModerationPolicy(tenantCode string) (*types.ModerationPolicy, error)
|
ModerationPolicy(tenantCode string) (*types.ModerationPolicy, error)
|
||||||
SensitiveWords(tenantCode string) (*types.SensitiveWordSet, error)
|
SensitiveWords(tenantCode string) (*types.SensitiveWordSet, error)
|
||||||
|
SecurityPolicy(tenantCode string) (*types.SecurityPolicy, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var adminInternalConfigProvider AdminInternalConfigProvider
|
var adminInternalConfigProvider AdminInternalConfigProvider
|
||||||
@@ -185,6 +186,7 @@ type ModerationConfigPersistenceInterface interface {
|
|||||||
Provider() (*types.ModerationProviderConfig, error)
|
Provider() (*types.ModerationProviderConfig, error)
|
||||||
Policy(tenantCode string) (*types.ModerationPolicy, error)
|
Policy(tenantCode string) (*types.ModerationPolicy, error)
|
||||||
Words(tenantCode string) (*types.SensitiveWordSet, error)
|
Words(tenantCode string) (*types.SensitiveWordSet, error)
|
||||||
|
SecurityPolicy(tenantCode string) (*types.SecurityPolicy, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type moderationConfigMapper struct{}
|
type moderationConfigMapper struct{}
|
||||||
@@ -235,6 +237,21 @@ func (moderationConfigMapper) Words(tenantCode string) (*types.SensitiveWordSet,
|
|||||||
return value, err
|
return value, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (moderationConfigMapper) SecurityPolicy(tenantCode string) (*types.SecurityPolicy, error) {
|
||||||
|
tenantCode = strings.TrimSpace(tenantCode)
|
||||||
|
if value, ok := securityPolicyCache.get(tenantCode); ok {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
if adminInternalConfigProvider == nil {
|
||||||
|
return nil, types.ErrUnsupported
|
||||||
|
}
|
||||||
|
value, err := adminInternalConfigProvider.SecurityPolicy(tenantCode)
|
||||||
|
if err == nil && value != nil {
|
||||||
|
securityPolicyCache.set(tenantCode, value)
|
||||||
|
}
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
|
||||||
// RegistrationConfigPersistenceInterface exposes global registration policy.
|
// RegistrationConfigPersistenceInterface exposes global registration policy.
|
||||||
type RegistrationConfigPersistenceInterface interface {
|
type RegistrationConfigPersistenceInterface interface {
|
||||||
SMSEnabled() (bool, error)
|
SMSEnabled() (bool, error)
|
||||||
|
|||||||
@@ -3,18 +3,21 @@ package types
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type UserProfile struct {
|
type UserProfile struct {
|
||||||
UserID Uid
|
UserID Uid
|
||||||
AvatarFileID string
|
AvatarFileID string
|
||||||
Nickname string
|
Nickname string
|
||||||
BirthDate string
|
BirthDate string
|
||||||
Signature string
|
Signature string
|
||||||
Gender string
|
Gender string
|
||||||
Region string
|
Region string
|
||||||
Email string
|
Email string
|
||||||
PublicAccount string
|
PublicAccount string
|
||||||
Version int
|
FriendAddEnabled bool
|
||||||
CreatedAt time.Time
|
FriendAddDisabledReason string
|
||||||
UpdatedAt time.Time
|
FriendAddPolicyVersion int
|
||||||
|
Version int
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserProfilePatch struct {
|
type UserProfilePatch struct {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type SecurityPolicy struct {
|
||||||
|
TenantID TenantID `json:"tenant_id"`
|
||||||
|
TenantCode string `json:"tenant_code"`
|
||||||
|
PolicyVersion uint `json:"policy_version"`
|
||||||
|
GlobalFriendEnabled bool `json:"global_friend_enabled"`
|
||||||
|
FriendDisabledPolicy map[string]any `json:"friend_disabled_policy"`
|
||||||
|
MessageCurfewEnabled bool `json:"message_curfew_enabled"`
|
||||||
|
CurfewRules []*MessageCurfewRule `json:"curfew_rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageCurfewRule struct {
|
||||||
|
RuleCode string `json:"rule_code"`
|
||||||
|
RuleName string `json:"rule_name"`
|
||||||
|
DateType string `json:"date_type"`
|
||||||
|
CustomDates []string `json:"custom_dates"`
|
||||||
|
Weekdays []int `json:"weekdays"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
StartTime string `json:"start_time"`
|
||||||
|
EndTime string `json:"end_time"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
WhitelistUserIds []string `json:"whitelist_user_ids"`
|
||||||
|
WhitelistGroupIds []string `json:"whitelist_group_ids"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
Sort int `json:"sort"`
|
||||||
|
}
|
||||||
@@ -1496,6 +1496,10 @@ func (t *Topic) handlePubBroadcast(msg *ClientComMessage) {
|
|||||||
msg.Pub.Content = content
|
msg.Pub.Content = content
|
||||||
}
|
}
|
||||||
if msg.sess != nil && msg.sess.authLvl != auth.LevelRoot && !isCall {
|
if msg.sess != nil && msg.sess.authLvl != auth.LevelRoot && !isCall {
|
||||||
|
if decision := imMessageCurfewDecision(t.tenantID, msg.sess.tenantCode, asUid, t.name, t.cat); !decision.Allowed {
|
||||||
|
msg.sess.queueOut(imCurfewPolicyReply(msg, decision))
|
||||||
|
return
|
||||||
|
}
|
||||||
scene := moderation.ScenePrivateMessage
|
scene := moderation.ScenePrivateMessage
|
||||||
if t.cat == types.TopicCatGrp {
|
if t.cat == types.TopicCatGrp {
|
||||||
scene = moderation.SceneGroupMessage
|
scene = moderation.SceneGroupMessage
|
||||||
|
|||||||
Reference in New Issue
Block a user