feat: add im group mention flows
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user