feat: add im group mention flows

This commit is contained in:
richjohnrambo
2026-08-12 15:33:56 +08:00
parent 943ebe1397
commit 774a448c12
5 changed files with 345 additions and 0 deletions
+161
View File
@@ -1115,6 +1115,93 @@ func serveIMFriends(wrt http.ResponseWriter, req *http.Request) {
writeIMSuccess(wrt, req, map[string]any{"items": payload, "next_cursor": nextCursor})
}
func serveIMGroupResource(wrt http.ResponseWriter, req *http.Request) {
topic, tail, ok := groupResourcePath(req.URL.Path)
if !ok {
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "group route not found", nil)
return
}
if tail == "members" {
serveIMGroupMembers(wrt, req, topic)
return
}
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "group route not found", nil)
}
func serveIMGroupMembers(wrt http.ResponseWriter, req *http.Request, topic string) {
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
}
if types.GetTopicCat(topic) != types.TopicCatGrp {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
return
}
selfSub, err := store.Subs.Get(principal.TenantID, topic, principal.Uid, false)
if err != nil {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
return
}
selfAccess := types.ModeNone
if selfSub != nil {
selfAccess = selfSub.ModeGiven & selfSub.ModeWant
}
if selfSub == nil || !selfAccess.IsReader() {
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "topic read permission required", nil)
return
}
limit := parseIMLimit(req.URL.Query().Get("limit"), 30, 100)
cursor, err := parseIMNonNegativeCursor(req.URL.Query().Get("cursor"))
if err != nil {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "cursor"})
return
}
query := strings.TrimSpace(req.URL.Query().Get("query"))
mentionableOnly := parseIMBool(req.URL.Query().Get("mentionable"))
members, err := store.Topics.GetUsers(principal.TenantID, topic, &types.QueryOpt{Limit: 1000})
if err != nil {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
return
}
items := make([]map[string]any, 0, limit)
seen := 0
nextCursor := ""
for _, member := range members {
access := member.ModeGiven & member.ModeWant
mentionable := access.IsJoiner() && access.IsReader()
if mentionableOnly && !mentionable {
continue
}
item := groupMemberPayload(member, mentionable)
if query != "" && !groupMemberMatches(item, query) {
continue
}
if seen < cursor {
seen++
continue
}
if len(items) == limit {
nextCursor = strconv.Itoa(seen)
break
}
items = append(items, item)
seen++
}
permissions := map[string]any{
"can_mention_all": selfAccess.IsAdmin(),
}
writeIMSuccess(wrt, req, map[string]any{
"topic": topic,
"items": items,
"next_cursor": nextCursor,
"permissions": permissions,
})
}
func serveIMFriendRequestAction(wrt http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
@@ -1525,6 +1612,27 @@ func parseIMLimit(raw string, fallback, max int) int {
return limit
}
func parseIMNonNegativeCursor(raw string) (int, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return 0, nil
}
cursor, err := strconv.Atoi(raw)
if err != nil || cursor < 0 {
return 0, errors.New("invalid cursor")
}
return cursor, nil
}
func parseIMBool(raw string) bool {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "1", "true", "yes", "y":
return true
default:
return false
}
}
func conversationMatches(sub types.Subscription, query string) bool {
if strings.Contains(strings.ToLower(sub.Topic), query) {
return true
@@ -1560,6 +1668,45 @@ func receiptMemberPayload(sub types.Subscription) map[string]any {
}
}
func groupMemberPayload(sub types.Subscription, mentionable bool) map[string]any {
access := sub.ModeGiven & sub.ModeWant
userID := types.ParseUserId(sub.User)
user := &types.User{ObjHeader: types.ObjHeader{Id: userID.String()}, Public: sub.GetPublic(), Trusted: sub.GetTrusted()}
data := userSummaryPayload(user, "")
data["role"] = groupMemberRole(access)
data["mentionable"] = mentionable
data["acs"] = map[string]any{
"mode": access.String(),
"want": sub.ModeWant.String(),
"given": sub.ModeGiven.String(),
}
return data
}
func groupMemberRole(access types.AccessMode) string {
switch {
case access.IsOwner():
return "owner"
case access.IsAdmin():
return "admin"
default:
return "member"
}
}
func groupMemberMatches(item map[string]any, query string) bool {
query = strings.ToLower(strings.TrimSpace(query))
if query == "" {
return true
}
for _, key := range []string{"display_name", "user_id"} {
if value, ok := item[key]; ok && strings.Contains(strings.ToLower(fmt.Sprint(value)), query) {
return true
}
}
return false
}
func authorizeIMMessageMutation(principal *imPrincipal, topic string, seq int, allowAdmin bool) (*types.Message, bool, error) {
if principal == nil {
return nil, false, types.ErrPermissionDenied
@@ -2068,6 +2215,20 @@ func conversationContextTopicFromPath(path string) string {
return strings.TrimSuffix(strings.TrimPrefix(path[idx:], prefix), suffix)
}
func groupResourcePath(path string) (string, string, bool) {
const prefix = "/v1/im/groups/"
idx := strings.Index(path, prefix)
if idx < 0 {
return "", "", false
}
rest := strings.TrimPrefix(path[idx:], prefix)
topic, tail, ok := strings.Cut(rest, "/")
if !ok || topic == "" || tail == "" {
return "", "", false
}
return topic, strings.Trim(tail, "/"), true
}
func friendRequestActionFromPath(path string) (string, string) {
const prefix = "/v1/im/friend-requests/"
idx := strings.Index(path, prefix)