feat: add safety reporting APIs
This commit is contained in:
@@ -1252,6 +1252,91 @@ func createIMUserProductTables(tx *sql.Tx) error {
|
||||
UNIQUE INDEX uk_im_device_push_tokens_provider_token(provider, token),
|
||||
INDEX idx_im_device_push_tokens_user(user_id, updated_at)
|
||||
)`,
|
||||
`CREATE TABLE im_risk_challenges(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
challenge_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
level VARCHAR(16) NOT NULL,
|
||||
notice_code VARCHAR(64) NOT NULL,
|
||||
notice_params JSON,
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
decision VARCHAR(16) NOT NULL DEFAULT '',
|
||||
action_token_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
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),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_risk_challenges_challenge(challenge_id),
|
||||
INDEX idx_im_risk_challenges_user_state(user_id, state, expires_at)
|
||||
)`,
|
||||
`CREATE TABLE im_abuse_reasons(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
target_type VARCHAR(16) NOT NULL,
|
||||
reason_code VARCHAR(64) NOT NULL,
|
||||
label VARCHAR(128) NOT NULL,
|
||||
requires_description TINYINT NOT NULL DEFAULT 0,
|
||||
allows_evidence TINYINT NOT NULL DEFAULT 1,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
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),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_abuse_reasons_type_code(target_type, reason_code),
|
||||
INDEX idx_im_abuse_reasons_type_order(target_type, enabled, sort_order)
|
||||
)`,
|
||||
`CREATE TABLE im_abuse_reports(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
report_id VARCHAR(64) NOT NULL,
|
||||
reporter_id BIGINT NOT NULL,
|
||||
target_type VARCHAR(16) NOT NULL,
|
||||
target_user_id BIGINT NOT NULL DEFAULT 0,
|
||||
target_topic VARCHAR(32) NOT NULL DEFAULT '',
|
||||
target_seq INT NOT NULL DEFAULT 0,
|
||||
reason_code VARCHAR(64) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
evidence_file_ids JSON,
|
||||
snapshot JSON,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'received',
|
||||
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),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_abuse_reports_report(report_id),
|
||||
UNIQUE INDEX uk_im_abuse_reports_idempotency(reporter_id, idempotency_key),
|
||||
INDEX idx_im_abuse_reports_target(target_type, target_topic, target_seq, target_user_id),
|
||||
INDEX idx_im_abuse_reports_reporter(reporter_id, id DESC)
|
||||
)`,
|
||||
`CREATE TABLE im_feedback_config(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
categories JSON,
|
||||
max_length INT NOT NULL DEFAULT 1000,
|
||||
attachment_policy JSON,
|
||||
allow_contact TINYINT NOT NULL DEFAULT 1,
|
||||
diagnostics_notice TEXT NOT NULL,
|
||||
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),
|
||||
PRIMARY KEY(id)
|
||||
)`,
|
||||
`CREATE TABLE im_feedback_submissions(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
feedback_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
category VARCHAR(64) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
contact VARCHAR(128) NOT NULL DEFAULT '',
|
||||
attachment_file_ids JSON,
|
||||
client_context JSON,
|
||||
include_diagnostics TINYINT NOT NULL DEFAULT 0,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'received',
|
||||
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),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_feedback_submissions_feedback(feedback_id),
|
||||
UNIQUE INDEX uk_im_feedback_submissions_idempotency(user_id, idempotency_key),
|
||||
INDEX idx_im_feedback_submissions_user(user_id, id DESC)
|
||||
)`,
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
if _, err := tx.Exec(stmt); err != nil {
|
||||
|
||||
@@ -3890,6 +3890,399 @@ func (a *adapter) TenantDevicePushTokenUpsert(id t.TenantID, token *t.DevicePush
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantRiskChallengeCreate(id t.TenantID, challenge *t.RiskChallenge) (*t.RiskChallenge, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantRiskChallengeCreate(id, challenge)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || challenge == nil || challenge.UserID.IsZero() ||
|
||||
strings.TrimSpace(challenge.ChallengeID) == "" || strings.TrimSpace(challenge.Action) == "" || challenge.ExpiresAt.IsZero() {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
params, err := json.Marshal(challenge.NoticeParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := t.TimeNow()
|
||||
if challenge.State == "" {
|
||||
challenge.State = t.RiskChallengeStatePending
|
||||
}
|
||||
challenge.Level = strings.ToLower(strings.TrimSpace(challenge.Level))
|
||||
challenge.NoticeCode = strings.TrimSpace(challenge.NoticeCode)
|
||||
_, err = a.db.ExecContext(ctx,
|
||||
`INSERT INTO im_risk_challenges(challenge_id,user_id,action,level,notice_code,notice_params,state,expires_at,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE updated_at=updated_at`,
|
||||
strings.TrimSpace(challenge.ChallengeID), decodeUID(challenge.UserID), strings.TrimSpace(challenge.Action),
|
||||
challenge.Level, challenge.NoticeCode, string(params), challenge.State, challenge.ExpiresAt, now, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.riskChallengeByID(ctx, challenge.ChallengeID)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantRiskChallengeConfirm(id t.TenantID, uid t.Uid, challengeID, decision, actionTokenID string, now time.Time) (*t.RiskChallenge, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantRiskChallengeConfirm(id, uid, challengeID, decision, actionTokenID, now)
|
||||
}
|
||||
}
|
||||
decision = strings.ToLower(strings.TrimSpace(decision))
|
||||
if err := validTenant(id); err != nil || uid.IsZero() || strings.TrimSpace(challengeID) == "" ||
|
||||
(decision != "continue" && decision != "cancel") {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
if now.IsZero() {
|
||||
now = t.TimeNow()
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
tx, err := a.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if tx != nil {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
challenge, err := a.riskChallengeByIDTx(ctx, tx, challengeID, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if challenge.UserID != uid || challenge.State != t.RiskChallengeStatePending || !now.Before(challenge.ExpiresAt) {
|
||||
return nil, t.ErrInvalidResponse
|
||||
}
|
||||
nextState := t.RiskChallengeStateCanceled
|
||||
if decision == "continue" {
|
||||
nextState = t.RiskChallengeStateContinued
|
||||
}
|
||||
_, err = tx.ExecContext(ctx,
|
||||
`UPDATE im_risk_challenges SET state=?,decision=?,action_token_id=?,updated_at=?
|
||||
WHERE challenge_id=? AND state=?`,
|
||||
nextState, decision, strings.TrimSpace(actionTokenID), now, challengeID, t.RiskChallengeStatePending)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx = nil
|
||||
return a.riskChallengeByID(ctx, challengeID)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantAbuseReasons(id t.TenantID, targetType string) ([]t.AbuseReason, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantAbuseReasons(id, targetType)
|
||||
}
|
||||
}
|
||||
targetType = strings.ToLower(strings.TrimSpace(targetType))
|
||||
if err := validTenant(id); err != nil || targetType == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
var reasons []t.AbuseReason
|
||||
err := sqlx.SelectContext(ctx, a.db, &reasons,
|
||||
`SELECT target_type,reason_code,label,requires_description,allows_evidence,sort_order
|
||||
FROM im_abuse_reasons WHERE target_type=? AND enabled=1 ORDER BY sort_order,id`, targetType)
|
||||
return reasons, err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantAbuseReportCreate(id t.TenantID, report *t.AbuseReport, idempotencyKey string) (*t.AbuseReport, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantAbuseReportCreate(id, report, idempotencyKey)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || report == nil || report.ReporterID.IsZero() ||
|
||||
strings.TrimSpace(report.ReportID) == "" || strings.TrimSpace(report.ReasonCode) == "" || strings.TrimSpace(idempotencyKey) == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
evidence, err := json.Marshal(report.EvidenceFileIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
snapshot, err := json.Marshal(report.Snapshot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := t.TimeNow()
|
||||
report.Status = t.AbuseReportStatusReceived
|
||||
_, err = a.db.ExecContext(ctx,
|
||||
`INSERT INTO im_abuse_reports(report_id,reporter_id,target_type,target_user_id,target_topic,target_seq,reason_code,description,evidence_file_ids,snapshot,idempotency_key,status,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE updated_at=updated_at`,
|
||||
report.ReportID, decodeUID(report.ReporterID), report.TargetType, decodeUID(report.TargetUserID), report.TargetTopic,
|
||||
report.TargetSeq, report.ReasonCode, report.Description, string(evidence), string(snapshot), strings.TrimSpace(idempotencyKey),
|
||||
report.Status, now, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.abuseReportByIdempotency(ctx, report.ReporterID, idempotencyKey)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFeedbackConfigGet(id t.TenantID, uid t.Uid) (*t.FeedbackConfig, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantFeedbackConfigGet(id, uid)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || uid.IsZero() {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
return a.feedbackConfig(ctx)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFeedbackCreate(id t.TenantID, feedback *t.FeedbackSubmission, idempotencyKey string) (*t.FeedbackSubmission, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantFeedbackCreate(id, feedback, idempotencyKey)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || feedback == nil || feedback.UserID.IsZero() ||
|
||||
strings.TrimSpace(feedback.FeedbackID) == "" || strings.TrimSpace(feedback.Category) == "" ||
|
||||
strings.TrimSpace(feedback.Content) == "" || strings.TrimSpace(idempotencyKey) == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
attachments, err := json.Marshal(feedback.AttachmentFileIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientContext, err := json.Marshal(feedback.ClientContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := t.TimeNow()
|
||||
feedback.Status = t.FeedbackStatusReceived
|
||||
_, err = a.db.ExecContext(ctx,
|
||||
`INSERT INTO im_feedback_submissions(feedback_id,user_id,category,content,contact,attachment_file_ids,client_context,include_diagnostics,idempotency_key,status,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE updated_at=updated_at`,
|
||||
feedback.FeedbackID, decodeUID(feedback.UserID), feedback.Category, feedback.Content, feedback.Contact,
|
||||
string(attachments), string(clientContext), feedback.IncludeDiagnostics, strings.TrimSpace(idempotencyKey),
|
||||
feedback.Status, now, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.feedbackByIdempotency(ctx, feedback.UserID, idempotencyKey)
|
||||
}
|
||||
|
||||
func (a *adapter) riskChallengeByID(ctx context.Context, challengeID string) (*t.RiskChallenge, error) {
|
||||
return a.riskChallengeByIDTx(ctx, a.db, challengeID, false)
|
||||
}
|
||||
|
||||
func (a *adapter) riskChallengeByIDTx(ctx context.Context, q sqlx.QueryerContext, challengeID string, forUpdate bool) (*t.RiskChallenge, error) {
|
||||
row := struct {
|
||||
ID int64 `db:"id"`
|
||||
ChallengeID string `db:"challenge_id"`
|
||||
UserID int64 `db:"user_id"`
|
||||
Action string `db:"action"`
|
||||
Level string `db:"level"`
|
||||
NoticeCode string `db:"notice_code"`
|
||||
NoticeParams []byte `db:"notice_params"`
|
||||
State string `db:"state"`
|
||||
ExpiresAt time.Time `db:"expires_at"`
|
||||
Decision string `db:"decision"`
|
||||
ActionTokenID string `db:"action_token_id"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}{}
|
||||
query := `SELECT id,challenge_id,user_id,action,level,notice_code,notice_params,state,expires_at,decision,action_token_id,created_at,updated_at
|
||||
FROM im_risk_challenges WHERE challenge_id=?`
|
||||
if forUpdate {
|
||||
query += " FOR UPDATE"
|
||||
}
|
||||
if err := sqlx.GetContext(ctx, q, &row, query, challengeID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
challenge := &t.RiskChallenge{
|
||||
ID: row.ID,
|
||||
ChallengeID: row.ChallengeID,
|
||||
UserID: store.EncodeUid(row.UserID),
|
||||
Action: row.Action,
|
||||
Level: row.Level,
|
||||
NoticeCode: row.NoticeCode,
|
||||
NoticeParams: map[string]any{},
|
||||
State: row.State,
|
||||
ExpiresAt: row.ExpiresAt,
|
||||
Decision: row.Decision,
|
||||
ActionTokenID: row.ActionTokenID,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
_ = json.Unmarshal(row.NoticeParams, &challenge.NoticeParams)
|
||||
return challenge, nil
|
||||
}
|
||||
|
||||
func (a *adapter) abuseReportByIdempotency(ctx context.Context, uid t.Uid, idempotencyKey string) (*t.AbuseReport, error) {
|
||||
row := struct {
|
||||
ID int64 `db:"id"`
|
||||
ReportID string `db:"report_id"`
|
||||
ReporterID int64 `db:"reporter_id"`
|
||||
TargetType string `db:"target_type"`
|
||||
TargetUserID int64 `db:"target_user_id"`
|
||||
TargetTopic string `db:"target_topic"`
|
||||
TargetSeq int `db:"target_seq"`
|
||||
ReasonCode string `db:"reason_code"`
|
||||
Description string `db:"description"`
|
||||
EvidenceFileIDs []byte `db:"evidence_file_ids"`
|
||||
Snapshot []byte `db:"snapshot"`
|
||||
Status string `db:"status"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}{}
|
||||
if err := sqlx.GetContext(ctx, a.db, &row,
|
||||
`SELECT id,report_id,reporter_id,target_type,target_user_id,target_topic,target_seq,reason_code,description,evidence_file_ids,snapshot,status,created_at,updated_at
|
||||
FROM im_abuse_reports WHERE reporter_id=? AND idempotency_key=?`,
|
||||
decodeUID(uid), strings.TrimSpace(idempotencyKey)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
report := &t.AbuseReport{
|
||||
ID: row.ID,
|
||||
ReportID: row.ReportID,
|
||||
ReporterID: store.EncodeUid(row.ReporterID),
|
||||
TargetType: row.TargetType,
|
||||
TargetUserID: store.EncodeUid(row.TargetUserID),
|
||||
TargetTopic: row.TargetTopic,
|
||||
TargetSeq: row.TargetSeq,
|
||||
ReasonCode: row.ReasonCode,
|
||||
Description: row.Description,
|
||||
EvidenceFileIDs: []string{},
|
||||
Snapshot: map[string]any{},
|
||||
Status: row.Status,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
_ = json.Unmarshal(row.EvidenceFileIDs, &report.EvidenceFileIDs)
|
||||
_ = json.Unmarshal(row.Snapshot, &report.Snapshot)
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func (a *adapter) feedbackConfig(ctx context.Context) (*t.FeedbackConfig, error) {
|
||||
row := struct {
|
||||
Categories []byte `db:"categories"`
|
||||
MaxLength int `db:"max_length"`
|
||||
AttachmentPolicy []byte `db:"attachment_policy"`
|
||||
AllowContact bool `db:"allow_contact"`
|
||||
DiagnosticsNotice string `db:"diagnostics_notice"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}{}
|
||||
if err := sqlx.GetContext(ctx, a.db, &row,
|
||||
`SELECT categories,max_length,attachment_policy,allow_contact,diagnostics_notice,updated_at
|
||||
FROM im_feedback_config ORDER BY id LIMIT 1`); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
now := t.TimeNow()
|
||||
return &t.FeedbackConfig{
|
||||
Categories: []string{"bug", "suggestion", "complaint", "other"},
|
||||
MaxLength: 1000,
|
||||
AttachmentPolicy: map[string]any{"max_files": float64(5), "purpose": "feedback"},
|
||||
AllowContact: true,
|
||||
DiagnosticsNotice: "仅在你同意时上传脱敏诊断信息。",
|
||||
UpdatedAt: now,
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
config := &t.FeedbackConfig{
|
||||
Categories: []string{"bug", "suggestion", "complaint", "other"},
|
||||
MaxLength: row.MaxLength,
|
||||
AttachmentPolicy: map[string]any{},
|
||||
AllowContact: row.AllowContact,
|
||||
DiagnosticsNotice: row.DiagnosticsNotice,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
_ = json.Unmarshal(row.Categories, &config.Categories)
|
||||
_ = json.Unmarshal(row.AttachmentPolicy, &config.AttachmentPolicy)
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (a *adapter) feedbackByIdempotency(ctx context.Context, uid t.Uid, idempotencyKey string) (*t.FeedbackSubmission, error) {
|
||||
row := struct {
|
||||
ID int64 `db:"id"`
|
||||
FeedbackID string `db:"feedback_id"`
|
||||
UserID int64 `db:"user_id"`
|
||||
Category string `db:"category"`
|
||||
Content string `db:"content"`
|
||||
Contact string `db:"contact"`
|
||||
AttachmentFileIDs []byte `db:"attachment_file_ids"`
|
||||
ClientContext []byte `db:"client_context"`
|
||||
IncludeDiagnostics bool `db:"include_diagnostics"`
|
||||
Status string `db:"status"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}{}
|
||||
if err := sqlx.GetContext(ctx, a.db, &row,
|
||||
`SELECT id,feedback_id,user_id,category,content,contact,attachment_file_ids,client_context,include_diagnostics,status,created_at,updated_at
|
||||
FROM im_feedback_submissions WHERE user_id=? AND idempotency_key=?`,
|
||||
decodeUID(uid), strings.TrimSpace(idempotencyKey)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
feedback := &t.FeedbackSubmission{
|
||||
ID: row.ID,
|
||||
FeedbackID: row.FeedbackID,
|
||||
UserID: store.EncodeUid(row.UserID),
|
||||
Category: row.Category,
|
||||
Content: row.Content,
|
||||
Contact: row.Contact,
|
||||
AttachmentFileIDs: []string{},
|
||||
ClientContext: map[string]any{},
|
||||
IncludeDiagnostics: row.IncludeDiagnostics,
|
||||
Status: row.Status,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
_ = json.Unmarshal(row.AttachmentFileIDs, &feedback.AttachmentFileIDs)
|
||||
_ = json.Unmarshal(row.ClientContext, &feedback.ClientContext)
|
||||
return feedback, nil
|
||||
}
|
||||
|
||||
func (a *adapter) userProfileByUID(ctx context.Context, uid t.Uid) (*t.UserProfile, error) {
|
||||
row := struct {
|
||||
UserID int64 `db:"user_id"`
|
||||
|
||||
Reference in New Issue
Block a user