feat: enforce youth mode policy and improve search

This commit is contained in:
richjohnrambo
2026-09-18 14:51:40 +08:00
parent 9482bb19f6
commit 8b20118570
13 changed files with 567 additions and 70 deletions
+91 -2
View File
@@ -16,12 +16,17 @@ import (
const (
imReasonFriendAddDisabled = "FRIEND_ADD_DISABLED"
imReasonMessageCurfew = "MESSAGE_CURFEW_ACTIVE"
imReasonYouthRestricted = "YOUTH_MODE_RESTRICTED"
imYouthActionMessage = "message"
imYouthActionCall = "call"
)
type imPolicyDecision struct {
Allowed bool
Reason string
Message string
Details map[string]any
}
func imAllowDecision() imPolicyDecision {
@@ -32,6 +37,66 @@ func imDenyDecision(reason, message string) imPolicyDecision {
return imPolicyDecision{Allowed: false, Reason: reason, Message: strings.TrimSpace(message)}
}
func imDenyDecisionDetails(reason, message string, details map[string]any) imPolicyDecision {
decision := imDenyDecision(reason, message)
decision.Details = details
return decision
}
func imYouthModePolicyDecision(tenantID types.TenantID, actor types.Uid, action string, now time.Time) imPolicyDecision {
if store.Security == nil || actor.IsZero() {
return imAllowDecision()
}
settings, err := store.Security.GetYouthMode(tenantID, actor)
if err != nil {
if !errors.Is(err, types.ErrNotFound) && !errors.Is(err, types.ErrUnsupported) {
logs.Warn.Printf("youth mode policy lookup failed: tenant=%d user=%s action=%s err=%v", tenantID, actor.UserId(), action, err)
}
return imAllowDecision()
}
if settings == nil || !settings.Enabled {
return imAllowDecision()
}
switch strings.TrimSpace(action) {
case imYouthActionCall:
if youthModeHasRestriction(settings, "night_quiet") && imYouthNightQuietActive(now) {
return imDenyDecisionDetails(imReasonYouthRestricted, "restricted by youth mode", map[string]any{
"restriction": "night_quiet",
"policy_version": settings.PolicyVersion,
})
}
case imYouthActionMessage:
// content_filter is enforced by routing messages through content moderation
// before persistence; the decision remains allow unless moderation rejects it.
return imAllowDecision()
}
return imAllowDecision()
}
func youthModeHasRestriction(settings *types.YouthModeSettings, code string) bool {
if settings == nil {
return false
}
code = strings.TrimSpace(code)
for _, item := range settings.Restrictions {
if strings.TrimSpace(item) == code {
return true
}
}
return false
}
func imYouthNightQuietActive(now time.Time) bool {
if now.IsZero() {
now = time.Now()
}
local := now.In(time.Local)
minute := local.Hour()*60 + local.Minute()
start := 22 * 60
end := 6 * 60
return minute >= start || minute < end
}
func imTenantCodeForPolicy(tenantID types.TenantID) (string, error) {
tenant, err := store.Tenants.GetByID(tenantID)
if err != nil || tenant == nil {
@@ -313,10 +378,14 @@ func writeIMPolicyDecision(wrt http.ResponseWriter, req *http.Request, decision
if reason == "" {
reason = imReasonPermissionDenied
}
writeIMError(wrt, req, http.StatusForbidden, 30005, reason, message, map[string]any{
details := map[string]any{
"reason": reason,
"message": message,
})
}
for key, value := range decision.Details {
details[key] = value
}
writeIMError(wrt, req, http.StatusForbidden, 30005, reason, message, details)
}
func imCurfewPolicyReply(msg *ClientComMessage, decision imPolicyDecision) *ServerComMessage {
@@ -328,3 +397,23 @@ func imCurfewPolicyReply(msg *ClientComMessage, decision imPolicyDecision) *Serv
}
return reply
}
func imYouthPolicyReply(msg *ClientComMessage, decision imPolicyDecision) *ServerComMessage {
reply := ErrPolicyReply(msg, types.TimeNow())
reply.Ctrl.Text = "restricted by youth mode"
params := map[string]any{
"reason": imReasonYouthRestricted,
"message": "restricted by youth mode",
}
if strings.TrimSpace(decision.Reason) != "" {
params["reason"] = decision.Reason
}
if strings.TrimSpace(decision.Message) != "" {
params["message"] = decision.Message
}
for key, value := range decision.Details {
params[key] = value
}
reply.Ctrl.Params = params
return reply
}