feat: implement first batch im enhancements
This commit is contained in:
@@ -321,6 +321,11 @@ type TenantBusinessAdapter interface {
|
||||
TenantMessageGetDeleted(t.TenantID, string, t.Uid, *t.QueryOpt) ([]t.DelMessage, error)
|
||||
TenantMessageSearch(t.TenantID, string, t.Uid, string, string, string, int) ([]t.Message, string, error)
|
||||
|
||||
TenantConversationSettingsGet(t.TenantID, t.Uid, string) (*t.ConversationSettings, error)
|
||||
TenantConversationSettingsUpdate(t.TenantID, t.Uid, string, t.ConversationSettingsPatch) (*t.ConversationSettings, error)
|
||||
TenantTopicDraftGet(t.TenantID, t.Uid, string) (*t.TopicDraft, error)
|
||||
TenantTopicDraftPut(t.TenantID, t.Uid, string, string, []byte, int) (*t.TopicDraft, error)
|
||||
|
||||
TenantCallCreate(t.TenantID, *t.CallRecord) error
|
||||
TenantCallAccept(t.TenantID, string, int, time.Time) error
|
||||
TenantCallFinish(t.TenantID, string, int, string, int64, time.Time) error
|
||||
@@ -334,6 +339,7 @@ type TenantBusinessAdapter interface {
|
||||
TenantGroupMemberList(t.TenantID, string, t.GroupMemberListOptions) ([]t.GroupMember, string, error)
|
||||
TenantGroupMemberUpdate(t.TenantID, string, t.Uid, t.GroupMemberPatch) (*t.GroupMember, error)
|
||||
TenantGroupInvitationCreate(t.TenantID, *t.GroupInvitation) (*t.GroupInvitation, error)
|
||||
TenantGroupInvitationAccept(t.TenantID, string, t.Uid, any, time.Time) (*t.GroupInvitation, error)
|
||||
TenantGroupJoinRequestCreate(t.TenantID, *t.GroupJoinRequest) (*t.GroupJoinRequest, error)
|
||||
TenantGroupJoinRequestList(t.TenantID, string, string, string, int) ([]t.GroupJoinRequest, string, error)
|
||||
TenantGroupJoinRequestResolve(t.TenantID, string, string, t.Uid, string, string, int, any, time.Time) (*t.GroupJoinRequest, error)
|
||||
@@ -343,6 +349,9 @@ type TenantBusinessAdapter interface {
|
||||
TenantGroupMuteList(t.TenantID, string, string, int) ([]t.GroupMute, string, error)
|
||||
TenantGroupMuteUpsert(t.TenantID, *t.GroupMute) (*t.GroupMute, error)
|
||||
TenantGroupMuteDelete(t.TenantID, string, t.Uid, t.Uid, time.Time) error
|
||||
TenantGroupFileList(t.TenantID, string, string, string, string, int) ([]t.GroupFile, string, error)
|
||||
TenantGroupFileCreate(t.TenantID, *t.GroupFile) (*t.GroupFile, error)
|
||||
TenantGroupFileDelete(t.TenantID, string, string, t.Uid, bool, time.Time) (*t.GroupFile, error)
|
||||
TenantGroupCreate(t.TenantID, *t.Topic, t.Uid, any, *t.GroupSettings, []t.Uid, any, time.Time) error
|
||||
TenantGroupInviteMember(t.TenantID, string, t.Uid, t.Uid, string, string, any, time.Time) (bool, error)
|
||||
TenantGroupLeave(t.TenantID, string, t.Uid, time.Time) error
|
||||
@@ -355,6 +364,7 @@ type TenantBusinessAdapter interface {
|
||||
TenantWorkbenchModules(t.TenantID, t.Uid) ([]t.WorkbenchModule, int, error)
|
||||
TenantOrganizationDepartments(t.TenantID, t.Uid, string, string, int) ([]t.OrganizationDepartment, string, error)
|
||||
TenantOrganizationMembers(t.TenantID, t.Uid, string, string, int) ([]t.OrganizationMember, string, error)
|
||||
TenantOrganizationMembersAll(t.TenantID, t.Uid, string, string, string, int) ([]t.OrganizationMember, string, error)
|
||||
TenantOrganizationSearch(t.TenantID, t.Uid, string, string, string, int) ([]t.OrganizationSearchItem, string, error)
|
||||
TenantMeetingCreate(t.TenantID, t.Uid, *t.Meeting, []t.Uid) (*t.Meeting, error)
|
||||
TenantMeetingGet(t.TenantID, t.Uid, string) (*t.Meeting, error)
|
||||
@@ -374,6 +384,7 @@ type TenantBusinessAdapter interface {
|
||||
TenantMonthlyReportGet(t.TenantID, t.Uid, string) (*t.MonthlyReport, error)
|
||||
TenantMonthlyReportSaveDraft(t.TenantID, t.Uid, string, string, string, []string, int) (*t.MonthlyReport, error)
|
||||
TenantMonthlyReportSubmit(t.TenantID, t.Uid, string, int) (*t.MonthlyReport, error)
|
||||
TenantMonthlyReportReopen(t.TenantID, t.Uid, string, int) (*t.MonthlyReport, error)
|
||||
TenantMonthlyReportStats(t.TenantID, t.Uid, string, string) (*t.MonthlyReportStats, error)
|
||||
TenantMailboxConfigGet(t.TenantID, t.Uid) (*t.MailboxConfig, error)
|
||||
TenantMailboxSubmissionCreate(t.TenantID, t.Uid, *t.MailboxSubmission, string) (*t.MailboxSubmission, error)
|
||||
@@ -422,6 +433,7 @@ type TenantBusinessAdapter interface {
|
||||
TenantUserSecurityBumpAuthVersion(t.TenantID, t.Uid, time.Time) (int, error)
|
||||
TenantRealNameGet(t.TenantID, t.Uid) (*t.RealNameVerification, error)
|
||||
TenantRealNameCreate(t.TenantID, *t.RealNameVerification) error
|
||||
TenantRealNameAppealCreate(t.TenantID, *t.RealNameAppeal) (*t.RealNameAppeal, error)
|
||||
TenantActionTokenCreate(t.TenantID, *t.ActionToken) error
|
||||
TenantActionTokenConsume(t.TenantID, t.Uid, string, string, string, time.Time) (*t.ActionToken, error)
|
||||
TenantIndependentPasswordGet(t.TenantID, t.Uid, string) (*t.IndependentPassword, error)
|
||||
|
||||
@@ -1595,6 +1595,24 @@ func createIMSecurityTables(tx *sql.Tx) error {
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(
|
||||
`CREATE TABLE im_realname_appeals(
|
||||
id BIGINT NOT NULL,
|
||||
appeal_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
reason VARCHAR(500) NOT NULL DEFAULT '',
|
||||
attachments_json JSON,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||||
review_comment VARCHAR(500) 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_realname_appeals_appeal(appeal_id),
|
||||
INDEX idx_im_realname_appeals_user(user_id, created_at),
|
||||
INDEX idx_im_realname_appeals_status(status, created_at)
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(
|
||||
`CREATE TABLE im_action_tokens(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
|
||||
@@ -29,6 +29,33 @@ func ensureIMHTTPInfrastructureTables(exec sqlExecutor) error {
|
||||
INDEX idx_im_business_events_outbox_pending(status, next_retry_at, id),
|
||||
INDEX idx_im_business_events_outbox_target(target_type, target, id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_realname_appeals (
|
||||
id BIGINT NOT NULL, appeal_id VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL,
|
||||
reason VARCHAR(500) NOT NULL DEFAULT '', attachments_json JSON,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'pending', review_comment VARCHAR(500) 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_realname_appeals_appeal(appeal_id),
|
||||
INDEX idx_im_realname_appeals_user(user_id, created_at),
|
||||
INDEX idx_im_realname_appeals_status(status, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_conversation_settings (
|
||||
id BIGINT NOT NULL, user_id BIGINT NOT NULL, topic VARCHAR(128) NOT NULL,
|
||||
pinned TINYINT NOT NULL DEFAULT 0, muted TINYINT NOT NULL DEFAULT 0,
|
||||
remark VARCHAR(128) NOT NULL DEFAULT '', message_retention VARCHAR(32) NOT NULL DEFAULT 'default',
|
||||
version INT 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_conversation_settings_user_topic(user_id, topic),
|
||||
INDEX idx_im_conversation_settings_user_pinned(user_id, pinned, updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_topic_drafts (
|
||||
id BIGINT NOT NULL, user_id BIGINT NOT NULL, topic VARCHAR(128) NOT NULL,
|
||||
content TEXT, entities_json JSON, version INT 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_topic_drafts_user_topic(user_id, topic),
|
||||
INDEX idx_im_topic_drafts_user_updated(user_id, updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if _, err := exec.Exec(statement); err != nil {
|
||||
|
||||
@@ -2307,6 +2307,74 @@ func (a *adapter) TenantOrganizationMembers(id t.TenantID, uid t.Uid, department
|
||||
return members, nextCursor, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantOrganizationMembersAll(id t.TenantID, uid t.Uid, departmentID, queryText, cursor string, limit int) ([]t.OrganizationMember, string, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantOrganizationMembersAll(id, uid, departmentID, queryText, cursor, limit)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || uid.IsZero() {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
where := []string{"m.employment_state='active'"}
|
||||
args := []any{}
|
||||
if strings.TrimSpace(departmentID) != "" {
|
||||
where = append(where, "m.department_id=?")
|
||||
args = append(args, strings.TrimSpace(departmentID))
|
||||
}
|
||||
if q := strings.TrimSpace(queryText); q != "" {
|
||||
like := "%" + q + "%"
|
||||
where = append(where, "(m.display_name LIKE ? OR m.mobile LIKE ? OR m.email LIKE ? OR CAST(m.user_id AS CHAR) LIKE ?)")
|
||||
args = append(args, like, like, like, like)
|
||||
}
|
||||
if cursor != "" {
|
||||
parsed, err := strconv.ParseInt(cursor, 10, 64)
|
||||
if err != nil || parsed <= 0 {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
where = append(where, "m.id>?")
|
||||
args = append(args, parsed)
|
||||
}
|
||||
args = append(args, limit+1)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, `SELECT m.id,m.department_id,m.user_id,m.display_name,m.position,m.avatar_file_id,m.mobile,m.email,
|
||||
m.employment_state,m.contactable,m.sort_order,m.version,m.created_at,m.updated_at
|
||||
FROM im_org_members AS m
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY m.sort_order ASC,m.id ASC LIMIT ?`, args...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer rows.Close()
|
||||
members := make([]t.OrganizationMember, 0, limit)
|
||||
nextCursor := ""
|
||||
for rows.Next() {
|
||||
member, err := scanOrganizationMember(rows)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if len(members) == limit {
|
||||
nextCursor = strconv.FormatInt(member.ID, 10)
|
||||
continue
|
||||
}
|
||||
members = append(members, *member)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return members, nextCursor, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantOrganizationSearch(id t.TenantID, uid t.Uid, queryText, resultTypes, cursor string, limit int) ([]t.OrganizationSearchItem, string, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
@@ -3547,6 +3615,37 @@ func (a *adapter) TenantMonthlyReportSubmit(id t.TenantID, uid t.Uid, reportID s
|
||||
return a.TenantMonthlyReportGet(id, uid, reportID)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMonthlyReportReopen(id t.TenantID, uid t.Uid, reportID string, version int) (*t.MonthlyReport, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantMonthlyReportReopen(id, uid, reportID, version)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
res, err := a.db.ExecContext(ctx, `UPDATE im_monthly_reports
|
||||
SET status=?,submitted_at=NULL,version=version+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE report_id=? AND author_id=? AND status=? AND version=?`,
|
||||
t.DailyReportStatusDraft, reportID, decodeUID(uid), t.DailyReportStatusSubmitted, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
affected, _ := res.RowsAffected()
|
||||
if affected == 0 {
|
||||
return nil, t.ErrInvalidResponse
|
||||
}
|
||||
return a.TenantMonthlyReportGet(id, uid, reportID)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMonthlyReportStats(id t.TenantID, uid t.Uid, month, _ string) (*t.MonthlyReportStats, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
@@ -4627,6 +4726,213 @@ func kvMapFromJSON(value any) t.KVMap {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *adapter) TenantConversationSettingsGet(id t.TenantID, uid t.Uid, topic string) (*t.ConversationSettings, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantConversationSettingsGet(id, uid, topic)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || uid.IsZero() || strings.TrimSpace(topic) == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
settings, err := a.conversationSettingsByKey(ctx, uid, strings.TrimSpace(topic))
|
||||
if err == sql.ErrNoRows {
|
||||
settingsID, idErr := nextSnowflakeID()
|
||||
if idErr != nil {
|
||||
return nil, idErr
|
||||
}
|
||||
now := t.TimeNow()
|
||||
_, err = a.db.ExecContext(ctx, `INSERT IGNORE INTO im_conversation_settings
|
||||
(id,user_id,topic,pinned,muted,remark,message_retention,version,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?)`,
|
||||
settingsID, decodeUID(uid), strings.TrimSpace(topic), false, false, "", "default", 1, now, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.conversationSettingsByKey(ctx, uid, strings.TrimSpace(topic))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantConversationSettingsUpdate(id t.TenantID, uid t.Uid, topic string, patch t.ConversationSettingsPatch) (*t.ConversationSettings, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantConversationSettingsUpdate(id, uid, topic, patch)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || uid.IsZero() || strings.TrimSpace(topic) == "" || patch.Version <= 0 {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
if _, err := a.TenantConversationSettingsGet(id, uid, topic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sets := []string{}
|
||||
args := []any{}
|
||||
if patch.Pinned != nil {
|
||||
sets = append(sets, "pinned=?")
|
||||
args = append(args, *patch.Pinned)
|
||||
}
|
||||
if patch.Muted != nil {
|
||||
sets = append(sets, "muted=?")
|
||||
args = append(args, *patch.Muted)
|
||||
}
|
||||
if patch.Remark != nil {
|
||||
sets = append(sets, "remark=?")
|
||||
args = append(args, strings.TrimSpace(*patch.Remark))
|
||||
}
|
||||
if patch.MessageRetention != nil {
|
||||
sets = append(sets, "message_retention=?")
|
||||
args = append(args, strings.TrimSpace(*patch.MessageRetention))
|
||||
}
|
||||
if len(sets) == 0 {
|
||||
return a.TenantConversationSettingsGet(id, uid, topic)
|
||||
}
|
||||
sets = append(sets, "version=version+1", "updated_at=CURRENT_TIMESTAMP(3)")
|
||||
args = append(args, decodeUID(uid), strings.TrimSpace(topic), patch.Version)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
res, err := a.db.ExecContext(ctx, `UPDATE im_conversation_settings SET `+strings.Join(sets, ",")+`
|
||||
WHERE user_id=? AND topic=? AND version=?`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
affected, _ := res.RowsAffected()
|
||||
if affected == 0 {
|
||||
return nil, t.ErrInvalidResponse
|
||||
}
|
||||
return a.conversationSettingsByKey(ctx, uid, strings.TrimSpace(topic))
|
||||
}
|
||||
|
||||
func (a *adapter) TenantTopicDraftGet(id t.TenantID, uid t.Uid, topic string) (*t.TopicDraft, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantTopicDraftGet(id, uid, topic)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || uid.IsZero() || strings.TrimSpace(topic) == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
draft, err := a.topicDraftByKey(ctx, uid, strings.TrimSpace(topic))
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
return draft, err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantTopicDraftPut(id t.TenantID, uid t.Uid, topic, content string, entities []byte, version int) (*t.TopicDraft, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantTopicDraftPut(id, uid, topic, content, entities, version)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || uid.IsZero() || strings.TrimSpace(topic) == "" || version < 0 {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
if version == 0 {
|
||||
draftID, err := nextSnowflakeID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := t.TimeNow()
|
||||
_, err = a.db.ExecContext(ctx, `INSERT INTO im_topic_drafts
|
||||
(id,user_id,topic,content,entities_json,version,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE content=VALUES(content),entities_json=VALUES(entities_json),
|
||||
version=version+1,updated_at=VALUES(updated_at)`,
|
||||
draftID, decodeUID(uid), strings.TrimSpace(topic), content, entities, 1, now, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.topicDraftByKey(ctx, uid, strings.TrimSpace(topic))
|
||||
}
|
||||
res, err := a.db.ExecContext(ctx, `UPDATE im_topic_drafts
|
||||
SET content=?,entities_json=?,version=version+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE user_id=? AND topic=? AND version=?`,
|
||||
content, entities, decodeUID(uid), strings.TrimSpace(topic), version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
affected, _ := res.RowsAffected()
|
||||
if affected == 0 {
|
||||
return nil, t.ErrInvalidResponse
|
||||
}
|
||||
return a.topicDraftByKey(ctx, uid, strings.TrimSpace(topic))
|
||||
}
|
||||
|
||||
func (a *adapter) conversationSettingsByKey(ctx context.Context, uid t.Uid, topic string) (*t.ConversationSettings, error) {
|
||||
row := struct {
|
||||
ID int64
|
||||
UserID int64 `db:"user_id"`
|
||||
Topic string `db:"topic"`
|
||||
Pinned bool
|
||||
Muted bool
|
||||
Remark string
|
||||
MessageRetention string `db:"message_retention"`
|
||||
Version int `db:"version"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}{}
|
||||
if err := sqlx.GetContext(ctx, a.db, &row, `SELECT id,user_id,topic,pinned,muted,remark,message_retention,version,created_at,updated_at
|
||||
FROM im_conversation_settings WHERE user_id=? AND topic=?`, decodeUID(uid), topic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &t.ConversationSettings{ID: row.ID, UserID: store.EncodeUid(row.UserID), UserIDRaw: row.UserID, Topic: row.Topic,
|
||||
Pinned: row.Pinned, Muted: row.Muted, Remark: row.Remark, MessageRetention: row.MessageRetention,
|
||||
Version: row.Version, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}, nil
|
||||
}
|
||||
|
||||
func (a *adapter) topicDraftByKey(ctx context.Context, uid t.Uid, topic string) (*t.TopicDraft, error) {
|
||||
row := struct {
|
||||
ID int64
|
||||
UserID int64 `db:"user_id"`
|
||||
Topic string `db:"topic"`
|
||||
Content string
|
||||
EntitiesJSON []byte `db:"entities_json"`
|
||||
Version int `db:"version"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}{}
|
||||
if err := sqlx.GetContext(ctx, a.db, &row, `SELECT id,user_id,topic,content,entities_json,version,created_at,updated_at
|
||||
FROM im_topic_drafts WHERE user_id=? AND topic=?`, decodeUID(uid), topic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &t.TopicDraft{ID: row.ID, UserID: store.EncodeUid(row.UserID), UserIDRaw: row.UserID, Topic: row.Topic,
|
||||
Content: row.Content, EntitiesJSON: row.EntitiesJSON, Version: row.Version, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFavoriteCreate(id t.TenantID, fav *t.FavoriteItem) (*t.FavoriteItem, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
@@ -5821,6 +6127,49 @@ func (a *adapter) TenantRealNameCreate(id t.TenantID, rv *t.RealNameVerification
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantRealNameAppealCreate(id t.TenantID, appeal *t.RealNameAppeal) (*t.RealNameAppeal, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantRealNameAppealCreate(id, appeal)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || appeal == nil || appeal.TenantID != id || appeal.UserID.IsZero() ||
|
||||
strings.TrimSpace(appeal.AppealID) == "" || strings.TrimSpace(appeal.Reason) == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
appealID, err := nextSnowflakeID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := t.TimeNow()
|
||||
if appeal.Status == "" {
|
||||
appeal.Status = "pending"
|
||||
}
|
||||
appeal.ID = appealID
|
||||
appeal.CreatedAt = now
|
||||
appeal.UpdatedAt = now
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err = a.db.ExecContext(ctx, `INSERT INTO im_realname_appeals
|
||||
(id,appeal_id,user_id,reason,attachments_json,status,review_comment,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?)`,
|
||||
appeal.ID, appeal.AppealID, decodeUID(appeal.UserID), appeal.Reason, appeal.AttachmentsJSON,
|
||||
appeal.Status, appeal.ReviewComment, appeal.CreatedAt, appeal.UpdatedAt)
|
||||
if isDupe(err) {
|
||||
return nil, t.ErrDuplicate
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return appeal, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantActionTokenCreate(id t.TenantID, token *t.ActionToken) error {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
|
||||
@@ -398,6 +398,97 @@ func (a *adapter) TenantGroupInvitationCreate(id t.TenantID, inv *t.GroupInvitat
|
||||
return inv, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupInvitationAccept(id t.TenantID, invitationID string, uid t.Uid, memberPrivate any, now time.Time) (*t.GroupInvitation, error) {
|
||||
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
||||
return nil, err
|
||||
} else if tenantAdp != a {
|
||||
return tenantAdp.TenantGroupInvitationAccept(id, invitationID, uid, memberPrivate, now)
|
||||
}
|
||||
if strings.TrimSpace(invitationID) == "" || uid.IsZero() || now.IsZero() {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContextForTx()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
tx, err := a.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
var inv t.GroupInvitation
|
||||
var handledAt sql.NullTime
|
||||
err = tx.QueryRowxContext(ctx, `SELECT id,invitation_id,topic,inviter_user_id,target_user_id,message,status,result,
|
||||
join_request_id,idempotency_key,expires_at,handled_at,created_at,updated_at
|
||||
FROM im_group_invitations WHERE invitation_id=? FOR UPDATE`, strings.TrimSpace(invitationID)).
|
||||
Scan(&inv.ID, &inv.InvitationID, &inv.Topic, &inv.InviterUserRaw, &inv.TargetUserRaw, &inv.Message, &inv.Status,
|
||||
&inv.Result, &inv.JoinRequestID, &inv.IdempotencyKey, &inv.ExpiresAt, &handledAt, &inv.CreatedAt, &inv.UpdatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inv.InviterUserID = store.EncodeUid(inv.InviterUserRaw)
|
||||
inv.TargetUserID = store.EncodeUid(inv.TargetUserRaw)
|
||||
if handledAt.Valid {
|
||||
inv.HandledAt = &handledAt.Time
|
||||
}
|
||||
if inv.TargetUserID != uid {
|
||||
return nil, t.ErrPermissionDenied
|
||||
}
|
||||
if inv.ExpiresAt.Before(now) {
|
||||
return nil, t.ErrExpired
|
||||
}
|
||||
if inv.Status == t.GroupInvitationStatusJoined || inv.Result == "joined" {
|
||||
return &inv, nil
|
||||
}
|
||||
var deletedAt sql.NullTime
|
||||
err = tx.QueryRowxContext(ctx, "SELECT DeletedAt FROM subscriptions WHERE Topic=? AND User=?", inv.Topic, uid.String()).Scan(&deletedAt)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
if err == sql.ErrNoRows || deletedAt.Valid {
|
||||
sub := &t.Subscription{TenantID: id, User: uid.String(), Topic: inv.Topic,
|
||||
ModeWant: t.ModeCPublic, ModeGiven: t.ModeCPublic, Private: memberPrivate}
|
||||
sub.InitTimes()
|
||||
if err = createSubscription(tx, sub, deletedAt.Valid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
member := &t.GroupMember{Topic: inv.Topic, UserID: uid, Role: t.GroupMemberRoleMember,
|
||||
MemberStatus: t.GroupMemberStatusActive, JoinSource: "invitation", JoinedBy: inv.InviterUserID, JoinedAt: now,
|
||||
Version: 1, CreatedAt: now, UpdatedAt: now}
|
||||
if err = upsertGroupMemberTx(tx, member); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = recountGroupMembersTx(tx, inv.Topic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = updateGroupTopicSubCountTx(tx, inv.Topic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, `UPDATE im_group_invitations
|
||||
SET status=?,result=?,handled_at=?,updated_at=? WHERE invitation_id=?`,
|
||||
t.GroupInvitationStatusJoined, "joined", now, now, inv.InvitationID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = insertGroupEventTx(tx, inv.Topic, uid, "group.member.joined", map[string]any{
|
||||
"user_id": uid.UserId(), "source": "invitation", "invitation_id": inv.InvitationID,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inv.Status = t.GroupInvitationStatusJoined
|
||||
inv.Result = "joined"
|
||||
inv.HandledAt = &now
|
||||
inv.UpdatedAt = now
|
||||
return &inv, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupJoinRequestCreate(id t.TenantID, req *t.GroupJoinRequest) (*t.GroupJoinRequest, error) {
|
||||
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
||||
return nil, err
|
||||
@@ -765,6 +856,130 @@ func (a *adapter) TenantGroupMuteDelete(id t.TenantID, topic string, uid, actor
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupFileList(id t.TenantID, topic, fileType, query, cursor string, limit int) ([]t.GroupFile, string, error) {
|
||||
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
||||
return nil, "", err
|
||||
} else if tenantAdp != a {
|
||||
return tenantAdp.TenantGroupFileList(id, topic, fileType, query, cursor, limit)
|
||||
}
|
||||
if strings.TrimSpace(topic) == "" {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
where := []string{"topic=?", "status='active'"}
|
||||
args := []any{strings.TrimSpace(topic)}
|
||||
if fileType != "" {
|
||||
where = append(where, "file_type=?")
|
||||
args = append(args, strings.TrimSpace(fileType))
|
||||
}
|
||||
if query != "" {
|
||||
where = append(where, "file_name LIKE ?")
|
||||
args = append(args, "%"+strings.TrimSpace(query)+"%")
|
||||
}
|
||||
if cursor != "" {
|
||||
parsed, err := strconv.ParseInt(cursor, 10, 64)
|
||||
if err != nil || parsed <= 0 {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
where = append(where, "id<?")
|
||||
args = append(args, parsed)
|
||||
}
|
||||
args = append(args, limit+1)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, `SELECT id,group_file_id,topic,file_id,source_topic,source_seq,uploader_id,
|
||||
file_name,mime_type,file_type,file_size,status,deleted_by,deleted_at,idempotency_key,created_at,updated_at
|
||||
FROM im_group_files WHERE `+strings.Join(where, " AND ")+` ORDER BY id DESC LIMIT ?`, args...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]t.GroupFile, 0, limit)
|
||||
next := ""
|
||||
for rows.Next() {
|
||||
file, err := scanGroupFile(rows)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if len(items) == limit {
|
||||
next = strconv.FormatInt(file.ID, 10)
|
||||
continue
|
||||
}
|
||||
items = append(items, *file)
|
||||
}
|
||||
return items, next, rows.Err()
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupFileCreate(id t.TenantID, file *t.GroupFile) (*t.GroupFile, error) {
|
||||
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
||||
return nil, err
|
||||
} else if tenantAdp != a {
|
||||
return tenantAdp.TenantGroupFileCreate(id, file)
|
||||
}
|
||||
if file == nil || strings.TrimSpace(file.Topic) == "" || strings.TrimSpace(file.FileID) == "" || file.UploaderID.IsZero() {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
if file.GroupFileID == "" {
|
||||
file.GroupFileID = "gfile_" + store.Store.GetUidString()
|
||||
}
|
||||
if file.FileType == "" {
|
||||
file.FileType = "file"
|
||||
}
|
||||
if file.Status == "" {
|
||||
file.Status = "active"
|
||||
}
|
||||
if file.IdempotencyKey == "" {
|
||||
file.IdempotencyKey = file.GroupFileID
|
||||
}
|
||||
now := t.TimeNow()
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err := a.db.ExecContext(ctx, `INSERT INTO im_group_files
|
||||
(group_file_id,topic,file_id,source_topic,source_seq,uploader_id,file_name,mime_type,file_type,file_size,status,idempotency_key,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE updated_at=updated_at`,
|
||||
file.GroupFileID, file.Topic, file.FileID, file.SourceTopic, file.SourceSeq, decodeUID(file.UploaderID),
|
||||
file.FileName, file.MIMEType, file.FileType, file.FileSize, file.Status, file.IdempotencyKey, now, now)
|
||||
if err != nil && !isDupe(err) {
|
||||
return nil, err
|
||||
}
|
||||
return a.groupFileByQuery(ctx, "SELECT * FROM im_group_files WHERE topic=? AND file_id=?", file.Topic, file.FileID)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupFileDelete(id t.TenantID, topic, groupFileID string, actor t.Uid, canManage bool, now time.Time) (*t.GroupFile, error) {
|
||||
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
||||
return nil, err
|
||||
} else if tenantAdp != a {
|
||||
return tenantAdp.TenantGroupFileDelete(id, topic, groupFileID, actor, canManage, now)
|
||||
}
|
||||
if strings.TrimSpace(topic) == "" || strings.TrimSpace(groupFileID) == "" || actor.IsZero() || now.IsZero() {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
query := `UPDATE im_group_files
|
||||
SET status='deleted',deleted_by=?,deleted_at=?,updated_at=?
|
||||
WHERE topic=? AND group_file_id=? AND status='active'`
|
||||
args := []any{decodeUID(actor), now, now, strings.TrimSpace(topic), strings.TrimSpace(groupFileID)}
|
||||
if !canManage {
|
||||
query += " AND uploader_id=?"
|
||||
args = append(args, decodeUID(actor))
|
||||
}
|
||||
res, err := a.db.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if affected, _ := res.RowsAffected(); affected == 0 {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
return a.groupFileByQuery(ctx, "SELECT * FROM im_group_files WHERE topic=? AND group_file_id=?", strings.TrimSpace(topic), strings.TrimSpace(groupFileID))
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupCreate(id t.TenantID, topic *t.Topic, owner t.Uid, ownerPrivate any,
|
||||
settings *t.GroupSettings, members []t.Uid, memberPrivate any, now time.Time) error {
|
||||
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
||||
@@ -1230,6 +1445,21 @@ func (a *adapter) groupMuteByQuery(ctx context.Context, query string, args ...an
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (a *adapter) groupFileByQuery(ctx context.Context, query string, args ...any) (*t.GroupFile, error) {
|
||||
rows, err := a.db.QueryxContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
if rows.Next() {
|
||||
return scanGroupFile(rows)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
|
||||
func (a *adapter) recountGroupMembers(ctx context.Context, topic string) error {
|
||||
_, err := a.db.ExecContext(ctx, "UPDATE im_group_settings SET member_count=(SELECT COUNT(*) FROM im_group_members WHERE topic=? AND member_status=?),updated_at=CURRENT_TIMESTAMP(3) WHERE topic=?", topic, t.GroupMemberStatusActive, topic)
|
||||
return err
|
||||
@@ -1270,3 +1500,15 @@ func scanGroupMute(item *t.GroupMute) {
|
||||
item.RevokedBy = store.EncodeUid(*item.RevokedRaw)
|
||||
}
|
||||
}
|
||||
|
||||
func scanGroupFile(rows *sqlx.Rows) (*t.GroupFile, error) {
|
||||
var item t.GroupFile
|
||||
if err := rows.StructScan(&item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.UploaderID = store.EncodeUid(item.UploaderRaw)
|
||||
if item.DeletedByRaw != nil {
|
||||
item.DeletedBy = store.EncodeUid(*item.DeletedByRaw)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user