feat: implement first batch im enhancements
This commit is contained in:
+513
-20
@@ -270,6 +270,11 @@ type imRealNameSubmissionRequest struct {
|
||||
IDNumber string `json:"id_number"`
|
||||
}
|
||||
|
||||
type imRealNameAppealRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
Attachments []string `json:"attachments,omitempty"`
|
||||
}
|
||||
|
||||
type imIndependentPasswordRequest struct {
|
||||
Password string `json:"password"`
|
||||
Purpose string `json:"purpose"`
|
||||
@@ -294,6 +299,25 @@ type imDevicePushTokenRequest struct {
|
||||
Platform string `json:"platform,omitempty"`
|
||||
}
|
||||
|
||||
type imP2PConversationRequest struct {
|
||||
PeerUserID string `json:"peer_user_id"`
|
||||
}
|
||||
|
||||
type imConversationSettingsRequest struct {
|
||||
Pinned *bool `json:"pinned,omitempty"`
|
||||
Muted *bool `json:"muted,omitempty"`
|
||||
MuteNotifications *bool `json:"mute_notifications,omitempty"`
|
||||
Remark *string `json:"remark,omitempty"`
|
||||
MessageRetention *string `json:"message_retention,omitempty"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
type imTopicDraftRequest struct {
|
||||
Content string `json:"content"`
|
||||
Entities any `json:"entities,omitempty"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
type imRiskChallengeConfirmRequest struct {
|
||||
Decision string `json:"decision"`
|
||||
}
|
||||
@@ -426,6 +450,16 @@ type imGroupJoinRequestActionRequest struct {
|
||||
Version int `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type imGroupFileCreateRequest struct {
|
||||
FileID string `json:"file_id"`
|
||||
FileName string `json:"file_name,omitempty"`
|
||||
MIMEType string `json:"mime_type,omitempty"`
|
||||
FileType string `json:"file_type,omitempty"`
|
||||
FileSize int64 `json:"file_size,omitempty"`
|
||||
SourceTopic string `json:"source_topic,omitempty"`
|
||||
SourceSeq int `json:"source_seq,omitempty"`
|
||||
}
|
||||
|
||||
type imGroupAnnouncementPutRequest struct {
|
||||
Content string `json:"content"`
|
||||
Version int `json:"version,omitempty"`
|
||||
@@ -1306,6 +1340,58 @@ func serveIMRealNameSubmissions(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMSuccess(wrt, req, map[string]any{"submission_id": submissionID, "status": "pending"})
|
||||
}
|
||||
|
||||
func serveIMRealNameAppeals(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
return
|
||||
}
|
||||
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var body imRealNameAppealRequest
|
||||
if !decodeIMJSONBody(wrt, req, &body) {
|
||||
return
|
||||
}
|
||||
reason := strings.TrimSpace(body.Reason)
|
||||
if reason == "" || len([]rune(reason)) > 500 || len(body.Attachments) > 9 {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "reason"})
|
||||
return
|
||||
}
|
||||
attachments := make([]string, 0, len(body.Attachments))
|
||||
for _, raw := range body.Attachments {
|
||||
fileID := strings.TrimSpace(raw)
|
||||
if fileID == "" {
|
||||
continue
|
||||
}
|
||||
attachments = append(attachments, fileID)
|
||||
}
|
||||
encoded, err := json.Marshal(attachments)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
token, err := randomIMToken(12)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
appeal, err := store.Security.CreateRealNameAppeal(principal.TenantID, &types.RealNameAppeal{
|
||||
TenantID: principal.TenantID,
|
||||
UserID: principal.Uid,
|
||||
AppealID: "rna_" + token,
|
||||
Reason: reason,
|
||||
Attachments: attachments,
|
||||
AttachmentsJSON: encoded,
|
||||
Status: "pending",
|
||||
})
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, realNameAppealPayload(appeal))
|
||||
}
|
||||
|
||||
func serveIMBootstrap(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
@@ -1438,6 +1524,7 @@ func serveIMOrganizationDepartments(wrt http.ResponseWriter, req *http.Request)
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"items": organizationDepartmentPayloads(items),
|
||||
"next_cursor": nextCursor,
|
||||
"has_more": nextCursor != "",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1465,6 +1552,35 @@ func serveIMOrganizationDepartmentResource(wrt http.ResponseWriter, req *http.Re
|
||||
"department_id": departmentID,
|
||||
"items": organizationMemberPayloads(items),
|
||||
"next_cursor": nextCursor,
|
||||
"has_more": nextCursor != "",
|
||||
})
|
||||
}
|
||||
|
||||
func serveIMOrganizationMembers(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
return
|
||||
}
|
||||
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
limit := parseIMLimit(req.URL.Query().Get("limit"), 50, 100)
|
||||
query := strings.TrimSpace(req.URL.Query().Get("q"))
|
||||
if query == "" {
|
||||
query = strings.TrimSpace(req.URL.Query().Get("keyword"))
|
||||
}
|
||||
departmentID := strings.TrimSpace(req.URL.Query().Get("department_id"))
|
||||
items, nextCursor, err := store.Organization.ListAllMembers(principal.TenantID, principal.Uid, departmentID, query, strings.TrimSpace(req.URL.Query().Get("cursor")), limit)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"department_id": departmentID,
|
||||
"items": organizationMemberPayloads(items),
|
||||
"next_cursor": nextCursor,
|
||||
"has_more": nextCursor != "",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1491,6 +1607,7 @@ func serveIMOrganizationSearch(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"items": organizationSearchPayloads(items),
|
||||
"next_cursor": nextCursor,
|
||||
"has_more": nextCursor != "",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1981,7 +2098,7 @@ func serveIMMonthlyReports(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{"items": monthlyReportListPayloads(reports), "next_cursor": nextCursor})
|
||||
writeIMSuccess(wrt, req, map[string]any{"items": monthlyReportListPayloads(reports), "next_cursor": nextCursor, "has_more": nextCursor != ""})
|
||||
case http.MethodPost:
|
||||
var body struct {
|
||||
Month string `json:"month"`
|
||||
@@ -2031,6 +2148,9 @@ func serveIMMonthlyReportResource(wrt http.ResponseWriter, req *http.Request) {
|
||||
case "submit":
|
||||
serveIMMonthlyReportSubmit(wrt, req, reportID)
|
||||
return
|
||||
case "reopen":
|
||||
serveIMMonthlyReportReopen(wrt, req, reportID)
|
||||
return
|
||||
}
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
}
|
||||
@@ -2097,6 +2217,29 @@ func serveIMMonthlyReportSubmit(wrt http.ResponseWriter, req *http.Request, repo
|
||||
writeIMSuccess(wrt, req, monthlyReportDetailPayload(report, principal.Uid))
|
||||
}
|
||||
|
||||
func serveIMMonthlyReportReopen(wrt http.ResponseWriter, req *http.Request, reportID string) {
|
||||
if req.Method != http.MethodPost {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
return
|
||||
}
|
||||
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Version int `json:"version"`
|
||||
}
|
||||
if !decodeIMJSONBody(wrt, req, &body) {
|
||||
return
|
||||
}
|
||||
report, err := store.MonthlyReports.Reopen(principal.TenantID, principal.Uid, reportID, body.Version)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, monthlyReportDetailPayload(report, principal.Uid))
|
||||
}
|
||||
|
||||
func serveIMMonthlyReportStatistics(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
@@ -2488,6 +2631,37 @@ func serveIMConversationSearch(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMSuccess(wrt, req, map[string]any{"items": items, "next_cursor": nextCursor})
|
||||
}
|
||||
|
||||
func serveIMP2PConversation(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
return
|
||||
}
|
||||
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var body imP2PConversationRequest
|
||||
if !decodeIMJSONBody(wrt, req, &body) {
|
||||
return
|
||||
}
|
||||
peer := types.ParseUserId(strings.TrimSpace(body.PeerUserID))
|
||||
if peer.IsZero() || peer == principal.Uid {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "peer_user_id"})
|
||||
return
|
||||
}
|
||||
topic, err := store.Conversations.EnsureP2P(principal.TenantID, principal.Uid, peer)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
ctx, err := store.Relations.GetRelationshipContext(principal.TenantID, principal.Uid, peer, topic)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, relationshipContextPayload(ctx))
|
||||
}
|
||||
|
||||
func serveIMForwardTargets(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
@@ -2828,6 +3002,8 @@ func serveIMFavoriteForward(wrt http.ResponseWriter, req *http.Request) {
|
||||
|
||||
func serveIMTopicMessageAction(wrt http.ResponseWriter, req *http.Request) {
|
||||
switch {
|
||||
case strings.HasSuffix(req.URL.Path, "/draft"):
|
||||
serveIMTopicDraft(wrt, req)
|
||||
case strings.HasSuffix(req.URL.Path, "/calls"):
|
||||
serveIMTopicCalls(wrt, req)
|
||||
case strings.HasSuffix(req.URL.Path, "/messages/search"):
|
||||
@@ -2847,6 +3023,53 @@ func serveIMTopicMessageAction(wrt http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func serveIMTopicDraft(wrt http.ResponseWriter, req *http.Request) {
|
||||
topic := topicFromDraftPath(req.URL.Path)
|
||||
if topic == "" {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
|
||||
return
|
||||
}
|
||||
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
draft, err := store.Conversations.GetDraft(principal.TenantID, principal.Uid, topic)
|
||||
if err != nil {
|
||||
if errors.Is(err, types.ErrNotFound) {
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "content": "", "entities": []any{}, "version": 0})
|
||||
return
|
||||
}
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, topicDraftPayload(draft))
|
||||
case http.MethodPut:
|
||||
var body imTopicDraftRequest
|
||||
if !decodeIMJSONBody(wrt, req, &body) {
|
||||
return
|
||||
}
|
||||
encoded, err := json.Marshal(body.Entities)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "entities"})
|
||||
return
|
||||
}
|
||||
if int64(len([]byte(body.Content))) > globals.maxMessageSize {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "content"})
|
||||
return
|
||||
}
|
||||
draft, err := store.Conversations.PutDraft(principal.TenantID, principal.Uid, topic, body.Content, encoded, body.Version)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, topicDraftPayload(draft))
|
||||
default:
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func serveIMCalls(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
@@ -3149,44 +3372,35 @@ func serveIMMessageRevision(wrt http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
func serveIMConversationContext(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
return
|
||||
}
|
||||
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
topic, tail := conversationResourceFromPath(req.URL.Path)
|
||||
if tail == "settings" {
|
||||
if topic == "" {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"topic": topic,
|
||||
"pinned": false,
|
||||
"mute_notifications": false,
|
||||
"message_retention": "default",
|
||||
"blocked": false,
|
||||
"remark": "",
|
||||
"version": 1,
|
||||
"permissions": map[string]any{"can_update": false},
|
||||
})
|
||||
serveIMConversationSettings(wrt, req, principal, topic)
|
||||
return
|
||||
}
|
||||
if tail == "messages" {
|
||||
if req.Method != http.MethodGet {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
return
|
||||
}
|
||||
if topic == "" {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": []any{}, "next_cursor": ""})
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": []any{}, "next_cursor": "", "has_more": false})
|
||||
return
|
||||
}
|
||||
if tail != "context" {
|
||||
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "conversation route not found", nil)
|
||||
return
|
||||
}
|
||||
if req.Method != http.MethodGet {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
return
|
||||
}
|
||||
peer := peerFromP2PTopic(topic, principal.Uid)
|
||||
if topic == "" || peer.IsZero() {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
|
||||
@@ -3200,6 +3414,46 @@ func serveIMConversationContext(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMSuccess(wrt, req, relationshipContextPayload(ctx))
|
||||
}
|
||||
|
||||
func serveIMConversationSettings(wrt http.ResponseWriter, req *http.Request, principal *imPrincipal, topic string) {
|
||||
if topic == "" {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
|
||||
return
|
||||
}
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
settings, err := store.Conversations.GetSettings(principal.TenantID, principal.Uid, topic)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, conversationSettingsPayload(settings))
|
||||
case http.MethodPatch:
|
||||
var body imConversationSettingsRequest
|
||||
if !decodeIMJSONBody(wrt, req, &body) {
|
||||
return
|
||||
}
|
||||
muted := body.Muted
|
||||
if muted == nil {
|
||||
muted = body.MuteNotifications
|
||||
}
|
||||
patch := types.ConversationSettingsPatch{
|
||||
Pinned: body.Pinned,
|
||||
Muted: muted,
|
||||
Remark: body.Remark,
|
||||
MessageRetention: body.MessageRetention,
|
||||
Version: body.Version,
|
||||
}
|
||||
settings, err := store.Conversations.UpdateSettings(principal.TenantID, principal.Uid, topic, patch)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, conversationSettingsPayload(settings))
|
||||
default:
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func serveIMFriendRequests(wrt http.ResponseWriter, req *http.Request) {
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
@@ -3510,6 +3764,10 @@ func serveIMGroupResource(wrt http.ResponseWriter, req *http.Request) {
|
||||
serveIMGroupAnnouncements(wrt, req, topic, tail)
|
||||
return
|
||||
}
|
||||
if tail == "files" || strings.HasPrefix(tail, "files/") {
|
||||
serveIMGroupFiles(wrt, req, topic, tail)
|
||||
return
|
||||
}
|
||||
if tail == "governance" {
|
||||
serveIMGroupGovernance(wrt, req, topic)
|
||||
return
|
||||
@@ -3967,6 +4225,104 @@ func serveIMGroupInvitations(wrt http.ResponseWriter, req *http.Request, topic s
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": results})
|
||||
}
|
||||
|
||||
func serveIMGroupFiles(wrt http.ResponseWriter, req *http.Request, topic, tail string) {
|
||||
principal, selfSub, ok := requireIMGroupMember(wrt, req, topic, true)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
access := selfSub.ModeGiven & selfSub.ModeWant
|
||||
switch {
|
||||
case tail == "files" && req.Method == http.MethodGet:
|
||||
limit := parseIMLimit(req.URL.Query().Get("limit"), 50, 100)
|
||||
items, nextCursor, err := store.GroupBusiness.ListFiles(principal.TenantID, topic,
|
||||
strings.TrimSpace(req.URL.Query().Get("type")),
|
||||
strings.TrimSpace(req.URL.Query().Get("q")),
|
||||
strings.TrimSpace(req.URL.Query().Get("cursor")),
|
||||
limit)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"topic": topic,
|
||||
"items": groupFilePayloads(items),
|
||||
"next_cursor": nextCursor,
|
||||
"has_more": nextCursor != "",
|
||||
})
|
||||
case tail == "files" && req.Method == http.MethodPost:
|
||||
idempotencyKey, ok := requireIMIdempotencyKey(wrt, req)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var body imGroupFileCreateRequest
|
||||
if !decodeIMJSONBody(wrt, req, &body) {
|
||||
return
|
||||
}
|
||||
fileID := strings.TrimSpace(body.FileID)
|
||||
if fileID == "" {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "file_id"})
|
||||
return
|
||||
}
|
||||
file, err := store.GroupBusiness.CreateFile(principal.TenantID, &types.GroupFile{
|
||||
Topic: topic,
|
||||
FileID: fileID,
|
||||
SourceTopic: strings.TrimSpace(body.SourceTopic),
|
||||
SourceSeq: body.SourceSeq,
|
||||
UploaderID: principal.Uid,
|
||||
FileName: strings.TrimSpace(body.FileName),
|
||||
MIMEType: strings.TrimSpace(body.MIMEType),
|
||||
FileType: normalizeIMFileType(body.FileType),
|
||||
FileSize: body.FileSize,
|
||||
Status: "active",
|
||||
IdempotencyKey: idempotencyKey,
|
||||
})
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, groupFilePayload(file))
|
||||
case strings.HasPrefix(tail, "files/") && req.Method == http.MethodDelete:
|
||||
groupFileID := strings.TrimSpace(strings.TrimPrefix(tail, "files/"))
|
||||
if groupFileID == "" || strings.Contains(groupFileID, "/") {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "file_id"})
|
||||
return
|
||||
}
|
||||
canManage := access.IsAdmin() || access.IsOwner()
|
||||
file, err := store.GroupBusiness.DeleteFile(principal.TenantID, topic, groupFileID, principal.Uid, canManage, time.Now().UTC().Round(time.Millisecond))
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, groupFilePayload(file))
|
||||
default:
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func serveIMGroupInvitationResource(wrt http.ResponseWriter, req *http.Request) {
|
||||
invitationID, tail, ok := groupInvitationResourcePath(req.URL.Path)
|
||||
if !ok || tail != "accept" {
|
||||
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "group invitation route not found", nil)
|
||||
return
|
||||
}
|
||||
if req.Method != http.MethodPost {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
return
|
||||
}
|
||||
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
inv, err := store.GroupBusiness.AcceptInvitation(principal.TenantID, invitationID, principal.Uid, groupMemberPrivate("", false, false, 1, now), now)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
writeIMSuccess(wrt, req, groupInvitationPayload(inv))
|
||||
}
|
||||
|
||||
func serveIMGroupJoinRequests(wrt http.ResponseWriter, req *http.Request, topic string) {
|
||||
if req.Method == http.MethodPost {
|
||||
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||
@@ -5332,6 +5688,89 @@ func realNamePayload(rv *types.RealNameVerification) map[string]any {
|
||||
return data
|
||||
}
|
||||
|
||||
func realNameAppealPayload(appeal *types.RealNameAppeal) map[string]any {
|
||||
if appeal == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
attachments := appeal.Attachments
|
||||
if len(attachments) == 0 && len(appeal.AttachmentsJSON) > 0 {
|
||||
_ = json.Unmarshal(appeal.AttachmentsJSON, &attachments)
|
||||
}
|
||||
return map[string]any{
|
||||
"appeal_id": appeal.AppealID,
|
||||
"reason": appeal.Reason,
|
||||
"attachments": attachments,
|
||||
"status": appeal.Status,
|
||||
"created_at": appeal.CreatedAt.Format(time.RFC3339Nano),
|
||||
}
|
||||
}
|
||||
|
||||
func groupInvitationPayload(inv *types.GroupInvitation) map[string]any {
|
||||
if inv == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
data := map[string]any{
|
||||
"invitation_id": inv.InvitationID,
|
||||
"topic": inv.Topic,
|
||||
"inviter_id": inv.InviterUserID.UserId(),
|
||||
"target_user_id": inv.TargetUserID.UserId(),
|
||||
"status": inv.Status,
|
||||
"result": inv.Result,
|
||||
"expires_at": inv.ExpiresAt.Format(time.RFC3339Nano),
|
||||
}
|
||||
if inv.HandledAt != nil {
|
||||
data["handled_at"] = inv.HandledAt.Format(time.RFC3339Nano)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func groupFilePayloads(files []types.GroupFile) []map[string]any {
|
||||
items := make([]map[string]any, 0, len(files))
|
||||
for i := range files {
|
||||
items = append(items, groupFilePayload(&files[i]))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func groupFilePayload(file *types.GroupFile) map[string]any {
|
||||
if file == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
data := map[string]any{
|
||||
"group_file_id": file.GroupFileID,
|
||||
"topic": file.Topic,
|
||||
"file_id": file.FileID,
|
||||
"file_name": file.FileName,
|
||||
"mime_type": file.MIMEType,
|
||||
"file_type": file.FileType,
|
||||
"file_size": file.FileSize,
|
||||
"uploader_id": file.UploaderID.UserId(),
|
||||
"status": file.Status,
|
||||
"created_at": file.CreatedAt.UTC().Format(time.RFC3339Nano),
|
||||
"updated_at": file.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
||||
}
|
||||
if file.SourceTopic != "" {
|
||||
data["source_topic"] = file.SourceTopic
|
||||
}
|
||||
if file.SourceSeq > 0 {
|
||||
data["source_seq"] = file.SourceSeq
|
||||
}
|
||||
if file.DeletedAt != nil {
|
||||
data["deleted_at"] = file.DeletedAt.UTC().Format(time.RFC3339Nano)
|
||||
data["deleted_by"] = file.DeletedBy.UserId()
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func normalizeIMFileType(raw string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case "image", "video", "audio", "document", "file":
|
||||
return strings.ToLower(strings.TrimSpace(raw))
|
||||
default:
|
||||
return "file"
|
||||
}
|
||||
}
|
||||
|
||||
func parseIMLimit(raw string, fallback, max int) int {
|
||||
limit := fallback
|
||||
if raw != "" {
|
||||
@@ -5442,6 +5881,40 @@ func conversationPayload(sub types.Subscription) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func conversationSettingsPayload(settings *types.ConversationSettings) map[string]any {
|
||||
if settings == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
return map[string]any{
|
||||
"topic": settings.Topic,
|
||||
"pinned": settings.Pinned,
|
||||
"muted": settings.Muted,
|
||||
"mute_notifications": settings.Muted,
|
||||
"remark": settings.Remark,
|
||||
"message_retention": settings.MessageRetention,
|
||||
"version": settings.Version,
|
||||
"updated_at": settings.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
||||
"permissions": map[string]any{"can_update": true},
|
||||
}
|
||||
}
|
||||
|
||||
func topicDraftPayload(draft *types.TopicDraft) map[string]any {
|
||||
if draft == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
var entities any = []any{}
|
||||
if len(draft.EntitiesJSON) > 0 {
|
||||
_ = json.Unmarshal(draft.EntitiesJSON, &entities)
|
||||
}
|
||||
return map[string]any{
|
||||
"topic": draft.Topic,
|
||||
"content": draft.Content,
|
||||
"entities": entities,
|
||||
"version": draft.Version,
|
||||
"updated_at": draft.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
||||
}
|
||||
}
|
||||
|
||||
func forwardTargetPayload(sub types.Subscription) map[string]any {
|
||||
topicType := ""
|
||||
switch types.GetTopicCat(sub.Topic) {
|
||||
@@ -7691,6 +8164,20 @@ func systemNotificationIDFromPath(path string) string {
|
||||
return strings.TrimSpace(eventID)
|
||||
}
|
||||
|
||||
func groupInvitationResourcePath(path string) (string, string, bool) {
|
||||
const marker = "/v1/im/group-invitations/"
|
||||
idx := strings.Index(path, marker)
|
||||
if idx < 0 {
|
||||
return "", "", false
|
||||
}
|
||||
rest := strings.TrimPrefix(path[idx:], marker)
|
||||
invitationID, tail, ok := strings.Cut(rest, "/")
|
||||
if !ok || strings.TrimSpace(invitationID) == "" {
|
||||
return "", "", false
|
||||
}
|
||||
return strings.TrimSpace(invitationID), strings.Trim(tail, "/"), true
|
||||
}
|
||||
|
||||
func messageReceiptsPath(path string) (string, int, bool) {
|
||||
const marker = "/v1/im/topics/"
|
||||
idx := strings.Index(path, marker)
|
||||
@@ -7731,6 +8218,12 @@ func topicFromCallsPath(path string) string {
|
||||
return topicFromPathWithSuffix(path, prefix, suffix)
|
||||
}
|
||||
|
||||
func topicFromDraftPath(path string) string {
|
||||
const prefix = "/v1/im/topics/"
|
||||
const suffix = "/draft"
|
||||
return topicFromPathWithSuffix(path, prefix, suffix)
|
||||
}
|
||||
|
||||
func topicFromPathWithSuffix(path, prefix, suffix string) string {
|
||||
idx := strings.Index(path, prefix)
|
||||
if idx < 0 || !strings.HasSuffix(path, suffix) {
|
||||
|
||||
Reference in New Issue
Block a user