feat: allow viewing user profile QR codes

This commit is contained in:
richjohnrambo
2026-08-20 17:54:12 +08:00
parent 6db2938672
commit 3e1ce5df26
2 changed files with 280 additions and 10 deletions
+138 -10
View File
@@ -4527,15 +4527,22 @@ func serveIMUserSearch(wrt http.ResponseWriter, req *http.Request) {
}
func serveIMUserResource(wrt http.ResponseWriter, req *http.Request) {
userID, tail, ok := userResourcePath(req.URL.Path)
if !ok || tail != "profile" {
identifier, tail, ok := userResourcePath(req.URL.Path)
if !ok {
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "user route not found", nil)
return
}
serveIMUserProfile(wrt, req, userID)
switch tail {
case "profile":
serveIMUserProfile(wrt, req, identifier)
case "qrcodes":
serveIMUserQRCodes(wrt, req, identifier)
default:
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "user route not found", nil)
}
}
func serveIMUserProfile(wrt http.ResponseWriter, req *http.Request, userID types.Uid) {
func serveIMUserProfile(wrt http.ResponseWriter, req *http.Request, identifier string) {
if req.Method != http.MethodGet {
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
return
@@ -4544,6 +4551,11 @@ func serveIMUserProfile(wrt http.ResponseWriter, req *http.Request, userID types
if !ok {
return
}
userID, err := resolveIMUserIdentifier(principal, identifier)
if err != nil {
writeIMResolveUserError(wrt, req, err)
return
}
if userID.IsZero() || userID == principal.Uid {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "user_id"})
return
@@ -4574,6 +4586,121 @@ func serveIMUserProfile(wrt http.ResponseWriter, req *http.Request, userID types
writeIMSuccess(wrt, req, userProfilePayload(user, ctx))
}
func serveIMUserQRCodes(wrt http.ResponseWriter, req *http.Request, identifier 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
}
userID, err := resolveIMUserIdentifier(principal, identifier)
if err != nil {
writeIMResolveUserError(wrt, req, err)
return
}
if userID.IsZero() {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "user_id"})
return
}
var body imQRCodeCreateRequest
if !decodeIMJSONBody(wrt, req, &body) {
return
}
purpose := strings.TrimSpace(body.Purpose)
if purpose == "" {
purpose = "add_friend"
}
if purpose != "add_friend" {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "purpose"})
return
}
user, err := store.Users.Get(principal.TenantID, userID)
if err != nil {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
return
}
if user == nil {
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "user not found", nil)
return
}
if userID != principal.Uid {
ctx, err := store.Relations.GetRelationshipContext(principal.TenantID, principal.Uid, userID, principal.Uid.P2PName(userID))
if err != nil {
writeIMRelationError(wrt, req, err)
return
}
privacy, err := store.Profiles.GetPrivacy(principal.TenantID, userID)
if err != nil {
writeIMRelationError(wrt, req, err)
return
}
if !imPrivacyAllowsProfile(privacy, ctx.Relationship) || !imPrivacyAllowsAdd(privacy, "qr") {
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
return
}
}
expiresIn := body.ExpiresInSeconds
if expiresIn <= 0 {
expiresIn = imQRCodeDefaultExpiresInSeconds
}
if expiresIn > imQRCodeMaxExpiresInSeconds {
expiresIn = imQRCodeMaxExpiresInSeconds
}
expiresAt := time.Now().UTC().Add(time.Duration(expiresIn) * time.Second).Round(time.Millisecond)
token, err := generateIMQRCodeToken(principal.TenantID, userID, purpose, expiresAt)
if err != nil {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
return
}
writeIMSuccess(wrt, req, map[string]any{
"qr_token": token,
"qr_payload": "im://qr?token=" + url.QueryEscape(token) + "&purpose=" + url.QueryEscape(purpose),
"expires_at": expiresAt.Format(time.RFC3339Nano),
"display_profile": userSummaryPayload(user, ""),
})
}
func resolveIMUserIdentifier(principal *imPrincipal, identifier string) (types.Uid, error) {
identifier = strings.TrimSpace(identifier)
if identifier == "" {
return types.ZeroUid, types.ErrMalformed
}
if userID := types.ParseUserId(identifier); !userID.IsZero() {
return userID, nil
}
if store.Relations == nil {
return types.ZeroUid, types.ErrUnsupported
}
items, _, err := store.Relations.SearchUsers(principal.TenantID, principal.Uid, identifier, "exact_account", "", 2)
if err != nil {
return types.ZeroUid, err
}
for _, item := range items {
if item.User == nil {
continue
}
if userID := item.User.Uid(); !userID.IsZero() {
return userID, nil
}
}
return types.ZeroUid, types.ErrNotFound
}
func writeIMResolveUserError(wrt http.ResponseWriter, req *http.Request, err error) {
switch {
case errors.Is(err, types.ErrMalformed):
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "user_id"})
case errors.Is(err, types.ErrNotFound):
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "user not found", nil)
case errors.Is(err, types.ErrPermissionDenied):
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
default:
writeIMRelationError(wrt, req, err)
}
}
func serveIMFriendRequestAction(wrt http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
@@ -7492,18 +7619,19 @@ func groupResourcePath(path string) (string, string, bool) {
return topic, strings.Trim(tail, "/"), true
}
func userResourcePath(path string) (types.Uid, string, bool) {
func userResourcePath(path string) (string, string, bool) {
const prefix = "/v1/im/users/"
idx := strings.Index(path, prefix)
if idx < 0 {
return types.ZeroUid, "", false
return "", "", false
}
rest := strings.TrimPrefix(path[idx:], prefix)
userID, tail, ok := strings.Cut(rest, "/")
if !ok || userID == "" || tail == "" {
return types.ZeroUid, "", false
identifier, tail, ok := strings.Cut(rest, "/")
identifier = strings.TrimSpace(identifier)
if !ok || identifier == "" || tail == "" {
return "", "", false
}
return types.ParseUserId(strings.TrimSpace(userID)), strings.Trim(tail, "/"), true
return identifier, strings.Trim(tail, "/"), true
}
func organizationDepartmentResourcePath(path string) (string, string, bool) {