feat: complete im p2p relationship onboarding
This commit is contained in:
@@ -319,9 +319,12 @@ type TenantBusinessAdapter interface {
|
||||
TenantSystemNotificationMarkRead(t.TenantID, t.Uid, string, time.Time) error
|
||||
|
||||
TenantFriendRequestCreate(t.TenantID, *t.FriendRequest) (*t.FriendRequest, error)
|
||||
TenantFriendRequestList(t.TenantID, t.Uid, string, string, string, int) ([]t.FriendRequestListItem, string, error)
|
||||
TenantFriendRequestLatestBetween(t.TenantID, t.Uid, t.Uid) (*t.FriendRequest, error)
|
||||
TenantFriendRequestAccept(t.TenantID, string, t.Uid, time.Time) (*t.FriendRequest, error)
|
||||
TenantFriendRequestResolve(t.TenantID, string, t.Uid, string, time.Time) (*t.FriendRequest, error)
|
||||
TenantFriendshipUpsertPair(t.TenantID, t.Uid, t.Uid, string, time.Time) error
|
||||
TenantFriendshipList(t.TenantID, t.Uid, string, string, string, int) ([]t.FriendListItem, string, error)
|
||||
TenantFriendshipGet(t.TenantID, t.Uid, t.Uid) (*t.Friendship, error)
|
||||
TenantFriendshipDeletePair(t.TenantID, t.Uid, t.Uid, time.Time) error
|
||||
TenantUserBlockGet(t.TenantID, t.Uid, t.Uid) (*t.UserBlock, error)
|
||||
|
||||
@@ -87,6 +87,14 @@ func (a *adapter) TenantUserGetAll(id t.TenantID, uids ...t.Uid) ([]t.User, erro
|
||||
if err := validTenant(id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
return a.tenantUserGetAllTx(ctx, nil, id, uids...)
|
||||
}
|
||||
|
||||
func (a *adapter) tenantUserGetAllTx(ctx context.Context, tx *sqlx.Tx, id t.TenantID, uids ...t.Uid) ([]t.User, error) {
|
||||
if len(uids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -100,11 +108,14 @@ func (a *adapter) TenantUserGetAll(id t.TenantID, uids ...t.Uid) ([]t.User, erro
|
||||
return nil, nil
|
||||
}
|
||||
q, params, _ := sqlx.In("SELECT * FROM users WHERE tenant_id=? AND id IN (?) AND state!=?", id, args, t.StateDeleted)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
query := a.db.Rebind(q)
|
||||
var rows *sqlx.Rows
|
||||
var err error
|
||||
if tx != nil {
|
||||
rows, err = tx.QueryxContext(ctx, query, params...)
|
||||
} else {
|
||||
rows, err = a.db.QueryxContext(ctx, query, params...)
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, a.db.Rebind(q), params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2534,6 +2545,125 @@ func (a *adapter) TenantFriendRequestCreate(id t.TenantID, req *t.FriendRequest)
|
||||
return a.friendRequestByRequestID(ctx, id, req.RequestID)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFriendRequestList(id t.TenantID, uid t.Uid, direction, status, cursor string, limit int) ([]t.FriendRequestListItem, string, error) {
|
||||
if err := validTenant(id); err != nil || uid.IsZero() {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
where := []string{"fr.tenant_id=?"}
|
||||
args := []any{id}
|
||||
decodedUID := decodeUID(uid)
|
||||
switch direction {
|
||||
case "", "inbound":
|
||||
where = append(where, "fr.target_id=?")
|
||||
args = append(args, decodedUID)
|
||||
case "outbound":
|
||||
where = append(where, "fr.applicant_id=?")
|
||||
args = append(args, decodedUID)
|
||||
case "all":
|
||||
where = append(where, "(fr.applicant_id=? OR fr.target_id=?)")
|
||||
args = append(args, decodedUID, decodedUID)
|
||||
default:
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
if status != "" && status != "all" {
|
||||
where = append(where, "fr.status=?")
|
||||
args = append(args, status)
|
||||
}
|
||||
if cursor != "" {
|
||||
parsed, err := strconv.ParseInt(cursor, 10, 64)
|
||||
if err != nil || parsed <= 0 {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
where = append(where, "fr.id<?")
|
||||
args = append(args, parsed)
|
||||
}
|
||||
args = append(args, limit+1)
|
||||
query := `SELECT fr.*,u.public,u.trusted
|
||||
FROM im_friend_requests AS fr
|
||||
JOIN users AS u ON u.tenant_id=fr.tenant_id AND u.id=IF(fr.applicant_id=?,fr.target_id,fr.applicant_id)
|
||||
WHERE ` + strings.Join(where, " AND ") + `
|
||||
ORDER BY fr.id DESC LIMIT ?`
|
||||
args = append([]any{decodedUID}, args...)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]t.FriendRequestListItem, 0, limit)
|
||||
nextCursor := ""
|
||||
for rows.Next() {
|
||||
row := struct {
|
||||
ID int64
|
||||
TenantID t.TenantID `db:"tenant_id"`
|
||||
RequestID string `db:"request_id"`
|
||||
ApplicantID int64 `db:"applicant_id"`
|
||||
TargetID int64 `db:"target_id"`
|
||||
Message string
|
||||
Source string
|
||||
SourceTopic string `db:"source_topic"`
|
||||
Status string
|
||||
IdempotencyKey string `db:"idempotency_key"`
|
||||
ExpiresAt time.Time `db:"expires_at"`
|
||||
HandledAt *time.Time `db:"handled_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
Public any
|
||||
Trusted any
|
||||
}{}
|
||||
if err = rows.StructScan(&row); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if len(items) == limit {
|
||||
nextCursor = strconv.FormatInt(row.ID, 10)
|
||||
continue
|
||||
}
|
||||
applicant := store.EncodeUid(row.ApplicantID)
|
||||
target := store.EncodeUid(row.TargetID)
|
||||
peerID := target
|
||||
direction := "outbound"
|
||||
if target == uid {
|
||||
peerID = applicant
|
||||
direction = "inbound"
|
||||
}
|
||||
items = append(items, t.FriendRequestListItem{
|
||||
Request: t.FriendRequest{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
RequestID: row.RequestID,
|
||||
ApplicantID: applicant,
|
||||
TargetID: target,
|
||||
Message: row.Message,
|
||||
Source: row.Source,
|
||||
SourceTopic: row.SourceTopic,
|
||||
Status: row.Status,
|
||||
IdempotencyKey: row.IdempotencyKey,
|
||||
ExpiresAt: row.ExpiresAt,
|
||||
HandledAt: row.HandledAt,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
},
|
||||
Peer: &t.User{
|
||||
ObjHeader: t.ObjHeader{Id: peerID.String()},
|
||||
TenantID: row.TenantID,
|
||||
Public: common.FromJSON(row.Public),
|
||||
Trusted: common.FromJSON(row.Trusted),
|
||||
},
|
||||
Direction: direction,
|
||||
})
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return items, nextCursor, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFriendRequestLatestBetween(id t.TenantID, uid1, uid2 t.Uid) (*t.FriendRequest, error) {
|
||||
if err := validTenant(id); err != nil || uid1.IsZero() || uid2.IsZero() {
|
||||
return nil, t.ErrMalformed
|
||||
@@ -2547,6 +2677,151 @@ func (a *adapter) TenantFriendRequestLatestBetween(id t.TenantID, uid1, uid2 t.U
|
||||
id, decodeUID(uid1), decodeUID(uid2), decodeUID(uid2), decodeUID(uid1))
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFriendRequestAccept(id t.TenantID, requestID string, actor t.Uid, handledAt time.Time) (*t.FriendRequest, error) {
|
||||
if err := validTenant(id); err != nil || requestID == "" || actor.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 func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
row := struct {
|
||||
ID int64
|
||||
TenantID t.TenantID `db:"tenant_id"`
|
||||
RequestID string `db:"request_id"`
|
||||
ApplicantID int64 `db:"applicant_id"`
|
||||
TargetID int64 `db:"target_id"`
|
||||
Message string
|
||||
Source string
|
||||
SourceTopic string `db:"source_topic"`
|
||||
Status string
|
||||
IdempotencyKey string `db:"idempotency_key"`
|
||||
ExpiresAt time.Time `db:"expires_at"`
|
||||
HandledAt *time.Time `db:"handled_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}{}
|
||||
err = tx.GetContext(ctx, &row, "SELECT * FROM im_friend_requests WHERE tenant_id=? AND request_id=? FOR UPDATE", id, requestID)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if store.EncodeUid(row.TargetID) != actor {
|
||||
return nil, t.ErrPermissionDenied
|
||||
}
|
||||
if row.Status != t.FriendRequestPending && row.Status != t.FriendRequestAccepted {
|
||||
return &t.FriendRequest{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
RequestID: row.RequestID,
|
||||
ApplicantID: store.EncodeUid(row.ApplicantID),
|
||||
TargetID: store.EncodeUid(row.TargetID),
|
||||
Message: row.Message,
|
||||
Source: row.Source,
|
||||
SourceTopic: row.SourceTopic,
|
||||
Status: row.Status,
|
||||
IdempotencyKey: row.IdempotencyKey,
|
||||
ExpiresAt: row.ExpiresAt,
|
||||
HandledAt: row.HandledAt,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
applicant := store.EncodeUid(row.ApplicantID)
|
||||
target := store.EncodeUid(row.TargetID)
|
||||
topic := applicant.P2PName(target)
|
||||
if topic == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
if row.Status == t.FriendRequestPending {
|
||||
if _, err = tx.ExecContext(ctx,
|
||||
"UPDATE im_friend_requests SET status=?,handled_at=?,updated_at=? WHERE tenant_id=? AND request_id=? AND status=?",
|
||||
t.FriendRequestAccepted, handledAt, handledAt, id, requestID, t.FriendRequestPending); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row.Status = t.FriendRequestAccepted
|
||||
row.HandledAt = &handledAt
|
||||
row.UpdatedAt = handledAt
|
||||
}
|
||||
for _, pair := range [][2]t.Uid{{applicant, target}, {target, applicant}} {
|
||||
if _, err = tx.ExecContext(ctx,
|
||||
"INSERT INTO im_friendships(tenant_id,user_id,peer_id,status,topic,created_at,updated_at) VALUES(?,?,?,?,?,?,?) "+
|
||||
"ON DUPLICATE KEY UPDATE status=VALUES(status),topic=VALUES(topic),updated_at=VALUES(updated_at)",
|
||||
id, decodeUID(pair[0]), decodeUID(pair[1]), t.RelationshipFriend, topic, handledAt, handledAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
users, err := a.tenantUserGetAllTx(ctx, tx, id, applicant, target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(users) != 2 {
|
||||
return nil, t.ErrUserNotFound
|
||||
}
|
||||
userByID := map[t.Uid]t.User{}
|
||||
for _, user := range users {
|
||||
userByID[user.Uid()] = user
|
||||
}
|
||||
applicantUser, ok1 := userByID[applicant]
|
||||
targetUser, ok2 := userByID[target]
|
||||
if !ok1 || !ok2 {
|
||||
return nil, t.ErrUserNotFound
|
||||
}
|
||||
applicantSub := &t.Subscription{
|
||||
ObjHeader: t.ObjHeader{CreatedAt: handledAt, UpdatedAt: handledAt},
|
||||
TenantID: id,
|
||||
User: applicant.String(),
|
||||
Topic: topic,
|
||||
ModeWant: t.ModeCP2P,
|
||||
ModeGiven: t.ModeCP2P,
|
||||
}
|
||||
applicantSub.SetTouchedAt(handledAt)
|
||||
applicantSub.SetPublic(targetUser.Public)
|
||||
applicantSub.SetTrusted(targetUser.Trusted)
|
||||
targetSub := &t.Subscription{
|
||||
ObjHeader: t.ObjHeader{CreatedAt: handledAt, UpdatedAt: handledAt},
|
||||
TenantID: id,
|
||||
User: target.String(),
|
||||
Topic: topic,
|
||||
ModeWant: t.ModeCP2P,
|
||||
ModeGiven: t.ModeCP2P,
|
||||
}
|
||||
targetSub.SetTouchedAt(handledAt)
|
||||
targetSub.SetPublic(applicantUser.Public)
|
||||
targetSub.SetTrusted(applicantUser.Trusted)
|
||||
if err = createSubscription(tx, applicantSub, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = createSubscription(tx, targetSub, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
topicRow := &t.Topic{TenantID: id, ObjHeader: t.ObjHeader{Id: topic, CreatedAt: handledAt, UpdatedAt: handledAt}, TouchedAt: handledAt}
|
||||
if err = tenantTopicCreate(tx, topicRow); err != nil {
|
||||
if !isDupe(err) {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, "UPDATE topics SET updatedat=?,touchedat=?,state=? WHERE tenant_id=? AND name=?",
|
||||
handledAt, handledAt, t.StateOK, id, topic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.friendRequestByRequestID(ctx, id, requestID)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFriendRequestResolve(id t.TenantID, requestID string, actor t.Uid, status string, handledAt time.Time) (*t.FriendRequest, error) {
|
||||
if err := validTenant(id); err != nil || requestID == "" || actor.IsZero() {
|
||||
return nil, t.ErrMalformed
|
||||
@@ -2595,6 +2870,96 @@ func (a *adapter) TenantFriendshipUpsertPair(id t.TenantID, uid1, uid2 t.Uid, to
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFriendshipList(id t.TenantID, uid t.Uid, queryText, initial, cursor string, limit int) ([]t.FriendListItem, string, error) {
|
||||
if err := validTenant(id); err != nil || uid.IsZero() {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
where := []string{"fs.tenant_id=?", "fs.user_id=?", "fs.status=?"}
|
||||
args := []any{id, decodeUID(uid), t.RelationshipFriend}
|
||||
if cursor != "" {
|
||||
parsed, err := strconv.ParseInt(cursor, 10, 64)
|
||||
if err != nil || parsed <= 0 {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
where = append(where, "fs.id>?")
|
||||
args = append(args, parsed)
|
||||
}
|
||||
if q := strings.TrimSpace(queryText); q != "" {
|
||||
like := "%" + q + "%"
|
||||
where = append(where, "(fs.remark LIKE ? OR CAST(u.public AS CHAR) LIKE ? OR CAST(u.tags AS CHAR) LIKE ?)")
|
||||
args = append(args, like, like, like)
|
||||
}
|
||||
if initial = strings.TrimSpace(initial); initial != "" {
|
||||
like := initial + "%"
|
||||
where = append(where, "(fs.remark LIKE ? OR CAST(u.public AS CHAR) LIKE ?)")
|
||||
args = append(args, like, like)
|
||||
}
|
||||
args = append(args, limit+1)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, `SELECT fs.*,u.public,u.trusted
|
||||
FROM im_friendships AS fs
|
||||
JOIN users AS u ON u.tenant_id=fs.tenant_id AND u.id=fs.peer_id
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY fs.id ASC LIMIT ?`, args...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]t.FriendListItem, 0, limit)
|
||||
nextCursor := ""
|
||||
for rows.Next() {
|
||||
row := struct {
|
||||
ID int64
|
||||
TenantID t.TenantID `db:"tenant_id"`
|
||||
UserID int64 `db:"user_id"`
|
||||
PeerID int64 `db:"peer_id"`
|
||||
Status string
|
||||
Remark string
|
||||
Topic string
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
Public any
|
||||
Trusted any
|
||||
}{}
|
||||
if err = rows.StructScan(&row); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if len(items) == limit {
|
||||
nextCursor = strconv.FormatInt(row.ID, 10)
|
||||
continue
|
||||
}
|
||||
peerID := store.EncodeUid(row.PeerID)
|
||||
items = append(items, t.FriendListItem{
|
||||
Friendship: t.Friendship{
|
||||
TenantID: row.TenantID,
|
||||
UserID: store.EncodeUid(row.UserID),
|
||||
PeerID: peerID,
|
||||
Status: row.Status,
|
||||
Remark: row.Remark,
|
||||
Topic: row.Topic,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
},
|
||||
Peer: &t.User{
|
||||
ObjHeader: t.ObjHeader{Id: peerID.String()},
|
||||
TenantID: row.TenantID,
|
||||
Public: common.FromJSON(row.Public),
|
||||
Trusted: common.FromJSON(row.Trusted),
|
||||
},
|
||||
})
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return items, nextCursor, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFriendshipGet(id t.TenantID, userID, peerID t.Uid) (*t.Friendship, error) {
|
||||
if err := validTenant(id); err != nil || userID.IsZero() || peerID.IsZero() {
|
||||
return nil, t.ErrMalformed
|
||||
|
||||
+143
-2
@@ -1014,10 +1014,39 @@ func serveIMConversationContext(wrt http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
func serveIMFriendRequests(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
serveIMFriendRequestList(wrt, req)
|
||||
case http.MethodPost:
|
||||
serveIMFriendRequestCreate(wrt, req)
|
||||
default:
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func serveIMFriendRequestList(wrt http.ResponseWriter, req *http.Request) {
|
||||
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
limit := parseIMLimit(req.URL.Query().Get("limit"), 20, 100)
|
||||
items, nextCursor, err := store.Relations.ListFriendRequests(principal.TenantID, principal.Uid,
|
||||
strings.TrimSpace(req.URL.Query().Get("direction")),
|
||||
strings.TrimSpace(req.URL.Query().Get("status")),
|
||||
strings.TrimSpace(req.URL.Query().Get("cursor")),
|
||||
limit)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
payload := make([]map[string]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
payload = append(payload, friendRequestListItemPayload(item))
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{"items": payload, "next_cursor": nextCursor})
|
||||
}
|
||||
|
||||
func serveIMFriendRequestCreate(wrt http.ResponseWriter, req *http.Request) {
|
||||
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||
if !ok {
|
||||
return
|
||||
@@ -1060,6 +1089,32 @@ func serveIMFriendRequests(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMSuccess(wrt, req, friendRequestPayload(created))
|
||||
}
|
||||
|
||||
func serveIMFriends(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)
|
||||
items, nextCursor, err := store.Relations.ListFriends(principal.TenantID, principal.Uid,
|
||||
strings.TrimSpace(req.URL.Query().Get("query")),
|
||||
strings.TrimSpace(req.URL.Query().Get("initial")),
|
||||
strings.TrimSpace(req.URL.Query().Get("cursor")),
|
||||
limit)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
payload := make([]map[string]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
payload = append(payload, friendListItemPayload(item))
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{"items": payload, "next_cursor": nextCursor})
|
||||
}
|
||||
|
||||
func serveIMFriendRequestAction(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
@@ -1615,7 +1670,9 @@ func friendRequestPayload(req *types.FriendRequest) map[string]any {
|
||||
"status": req.Status,
|
||||
"created_at": req.CreatedAt.UTC().Format(time.RFC3339Nano),
|
||||
"expires_at": req.ExpiresAt.UTC().Format(time.RFC3339Nano),
|
||||
"topic": req.ApplicantID.P2PName(req.TargetID),
|
||||
}
|
||||
if topic := req.ApplicantID.P2PName(req.TargetID); topic != "" {
|
||||
data["topic"] = topic
|
||||
}
|
||||
if req.SourceTopic != "" {
|
||||
data["source_topic"] = req.SourceTopic
|
||||
@@ -1626,6 +1683,90 @@ func friendRequestPayload(req *types.FriendRequest) map[string]any {
|
||||
return data
|
||||
}
|
||||
|
||||
func friendRequestListItemPayload(item types.FriendRequestListItem) map[string]any {
|
||||
data := friendRequestPayload(&item.Request)
|
||||
data["direction"] = item.Direction
|
||||
if item.Peer != nil {
|
||||
data["peer"] = userSummaryPayload(item.Peer, "")
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func friendListItemPayload(item types.FriendListItem) map[string]any {
|
||||
data := map[string]any{
|
||||
"user_id": item.Friendship.PeerID.UserId(),
|
||||
"relationship": item.Friendship.Status,
|
||||
"remark": item.Friendship.Remark,
|
||||
"topic": item.Friendship.Topic,
|
||||
"online_visibility": "unknown",
|
||||
"created_at": item.Friendship.CreatedAt.UTC().Format(time.RFC3339Nano),
|
||||
"updated_at": item.Friendship.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
||||
"capabilities": map[string]any{"can_send": item.Friendship.Status == types.RelationshipFriend, "can_call": item.Friendship.Status == types.RelationshipFriend},
|
||||
"display_name_source": "profile",
|
||||
}
|
||||
if item.Friendship.Remark != "" {
|
||||
data["display_name"] = item.Friendship.Remark
|
||||
data["display_name_source"] = "remark"
|
||||
}
|
||||
if item.Peer != nil {
|
||||
for key, value := range userSummaryPayload(item.Peer, item.Friendship.Remark) {
|
||||
_, exists := data[key]
|
||||
if !exists || key == "avatar_file_id" || (key == "display_name" && item.Friendship.Remark == "") {
|
||||
data[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
data["initial"] = displayInitial(data["display_name"])
|
||||
return data
|
||||
}
|
||||
|
||||
func userSummaryPayload(user *types.User, remark string) map[string]any {
|
||||
uid := types.ParseUid(user.Id)
|
||||
userID := uid.UserId()
|
||||
if uid.IsZero() {
|
||||
userID = user.Id
|
||||
}
|
||||
data := map[string]any{"user_id": userID}
|
||||
if remark != "" {
|
||||
data["display_name"] = remark
|
||||
}
|
||||
if public, ok := user.Public.(map[string]any); ok {
|
||||
copyUserPublicSummary(data, public)
|
||||
} else if public, ok := user.Public.(types.KVMap); ok {
|
||||
copyUserPublicSummary(data, map[string]any(public))
|
||||
}
|
||||
if _, ok := data["display_name"]; !ok {
|
||||
data["display_name"] = userID
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func copyUserPublicSummary(data map[string]any, public map[string]any) {
|
||||
for _, key := range []string{"display_name", "fn", "name", "nickname"} {
|
||||
if value, ok := public[key]; ok && strings.TrimSpace(fmt.Sprint(value)) != "" {
|
||||
data["display_name"] = value
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"avatar_file_id", "photo", "avatar"} {
|
||||
if value, ok := public[key]; ok && strings.TrimSpace(fmt.Sprint(value)) != "" {
|
||||
data["avatar_file_id"] = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func displayInitial(value any) string {
|
||||
name := strings.TrimSpace(fmt.Sprint(value))
|
||||
if name == "" {
|
||||
return "#"
|
||||
}
|
||||
for _, r := range name {
|
||||
return strings.ToUpper(string(r))
|
||||
}
|
||||
return "#"
|
||||
}
|
||||
|
||||
func writeIMRelationError(wrt http.ResponseWriter, req *http.Request, err error) {
|
||||
switch {
|
||||
case errors.Is(err, types.ErrNotFound):
|
||||
|
||||
+96
-6
@@ -34,12 +34,14 @@ type testVerificationStore struct {
|
||||
}
|
||||
|
||||
type testRelationsStore struct {
|
||||
context *types.RelationshipContext
|
||||
created *types.FriendRequest
|
||||
blocked *types.UserBlock
|
||||
deleted bool
|
||||
canSend *bool
|
||||
reason string
|
||||
context *types.RelationshipContext
|
||||
created *types.FriendRequest
|
||||
blocked *types.UserBlock
|
||||
deleted bool
|
||||
canSend *bool
|
||||
reason string
|
||||
friends []types.FriendListItem
|
||||
friendRequests []types.FriendRequestListItem
|
||||
}
|
||||
|
||||
func (s *testRelationsStore) CreateFriendRequest(_ types.TenantID, req *types.FriendRequest) (*types.FriendRequest, error) {
|
||||
@@ -47,6 +49,10 @@ func (s *testRelationsStore) CreateFriendRequest(_ types.TenantID, req *types.Fr
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (s *testRelationsStore) ListFriendRequests(_ types.TenantID, _ types.Uid, _, _, _ string, _ int) ([]types.FriendRequestListItem, string, error) {
|
||||
return s.friendRequests, "", nil
|
||||
}
|
||||
|
||||
func (s *testRelationsStore) ResolveFriendRequest(_ types.TenantID, requestID string, actor types.Uid, status string, handledAt time.Time) (*types.FriendRequest, error) {
|
||||
return &types.FriendRequest{
|
||||
TenantID: 7,
|
||||
@@ -61,6 +67,10 @@ func (s *testRelationsStore) ResolveFriendRequest(_ types.TenantID, requestID st
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *testRelationsStore) ListFriends(_ types.TenantID, _ types.Uid, _, _, _ string, _ int) ([]types.FriendListItem, string, error) {
|
||||
return s.friends, "", nil
|
||||
}
|
||||
|
||||
func (s *testRelationsStore) GetRelationshipContext(_ types.TenantID, userID, peerID types.Uid, topic string) (*types.RelationshipContext, error) {
|
||||
if s.context != nil {
|
||||
return s.context, nil
|
||||
@@ -984,6 +994,86 @@ func TestServeIMFriendRequestCreate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMFriendRequestList(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 1)
|
||||
oldRelations := store.Relations
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
relations := &testRelationsStore{friendRequests: []types.FriendRequestListItem{{
|
||||
Request: types.FriendRequest{
|
||||
TenantID: 7,
|
||||
RequestID: "frq_1",
|
||||
ApplicantID: 2,
|
||||
TargetID: 1,
|
||||
Message: "hi",
|
||||
Source: "search",
|
||||
Status: types.FriendRequestPending,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
ExpiresAt: now.Add(time.Hour),
|
||||
},
|
||||
Peer: &types.User{ObjHeader: types.ObjHeader{Id: types.Uid(2).String()}, TenantID: 7, Public: map[string]any{"display_name": "Alice"}},
|
||||
Direction: "inbound",
|
||||
}}}
|
||||
store.Relations = relations
|
||||
defer func() { store.Relations = oldRelations }()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/friend-requests?direction=inbound&status=pending", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveIMFriendRequests(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
items := data["items"].([]any)
|
||||
if len(items) != 1 || items[0].(map[string]any)["direction"] != "inbound" {
|
||||
t.Fatalf("unexpected friend request list: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMFriendsList(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 1)
|
||||
oldRelations := store.Relations
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
peer := types.Uid(2)
|
||||
relations := &testRelationsStore{friends: []types.FriendListItem{{
|
||||
Friendship: types.Friendship{
|
||||
TenantID: 7,
|
||||
UserID: 1,
|
||||
PeerID: peer,
|
||||
Status: types.RelationshipFriend,
|
||||
Remark: "小明",
|
||||
Topic: types.Uid(1).P2PName(peer),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
Peer: &types.User{ObjHeader: types.ObjHeader{Id: peer.String()}, TenantID: 7, Public: map[string]any{"display_name": "Ming"}},
|
||||
}}}
|
||||
store.Relations = relations
|
||||
defer func() { store.Relations = oldRelations }()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/friends?query=ming", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveIMFriends(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
items := data["items"].([]any)
|
||||
if len(items) != 1 || items[0].(map[string]any)["display_name"] != "小明" {
|
||||
t.Fatalf("unexpected friends list: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func ptrTime(t time.Time) *time.Time {
|
||||
return &t
|
||||
}
|
||||
|
||||
@@ -797,6 +797,7 @@ func main() {
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/friend-requests", serveIMFriendRequests)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/friend-requests/", serveIMFriendRequestAction)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/blocks/", serveIMBlockUser)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/friends", serveIMFriends)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/friends/", serveIMFriendResource)
|
||||
// Handle long polling clients. Enable compression.
|
||||
mux.Handle(config.ApiPath+"v0/channels/lp", gh.CompressHandler(http.HandlerFunc(serveLongPoll)))
|
||||
|
||||
+43
-5
@@ -997,7 +997,9 @@ func (messagesMapper) Edit(tenantID types.TenantID, topic string, seq int, actor
|
||||
|
||||
type RelationsPersistenceInterface interface {
|
||||
CreateFriendRequest(tenantID types.TenantID, req *types.FriendRequest) (*types.FriendRequest, error)
|
||||
ListFriendRequests(tenantID types.TenantID, uid types.Uid, direction, status, cursor string, limit int) ([]types.FriendRequestListItem, string, error)
|
||||
ResolveFriendRequest(tenantID types.TenantID, requestID string, actor types.Uid, status string, handledAt time.Time) (*types.FriendRequest, error)
|
||||
ListFriends(tenantID types.TenantID, uid types.Uid, query, initial, cursor string, limit int) ([]types.FriendListItem, string, error)
|
||||
GetRelationshipContext(tenantID types.TenantID, userID, peerID types.Uid, topic string) (*types.RelationshipContext, error)
|
||||
CanSendP2P(tenantID types.TenantID, userID, peerID types.Uid) (bool, string, error)
|
||||
BlockUser(tenantID types.TenantID, blockerID, blockedID types.Uid, now time.Time) (*types.UserBlock, error)
|
||||
@@ -1020,6 +1022,30 @@ func (relationsMapper) CreateFriendRequest(tenantID types.TenantID, req *types.F
|
||||
return tenantAdp.TenantFriendRequestCreate(tenantID, req)
|
||||
}
|
||||
|
||||
func (relationsMapper) ListFriendRequests(tenantID types.TenantID, uid types.Uid, direction, status, cursor string, limit int) ([]types.FriendRequestListItem, string, error) {
|
||||
if uid.IsZero() {
|
||||
return nil, "", types.ErrMalformed
|
||||
}
|
||||
switch direction {
|
||||
case "", "inbound", "outbound", "all":
|
||||
default:
|
||||
return nil, "", types.ErrMalformed
|
||||
}
|
||||
switch status {
|
||||
case "", "all", types.FriendRequestPending, types.FriendRequestAccepted, types.FriendRequestRejected, types.FriendRequestExpired:
|
||||
default:
|
||||
return nil, "", types.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return tenantAdp.TenantFriendRequestList(tenantID, uid, direction, status, cursor, limit)
|
||||
}
|
||||
|
||||
func (relationsMapper) ResolveFriendRequest(tenantID types.TenantID, requestID string, actor types.Uid, status string, handledAt time.Time) (*types.FriendRequest, error) {
|
||||
if strings.TrimSpace(requestID) == "" || actor.IsZero() || (status != types.FriendRequestAccepted && status != types.FriendRequestRejected) {
|
||||
return nil, types.ErrMalformed
|
||||
@@ -1028,18 +1054,30 @@ func (relationsMapper) ResolveFriendRequest(tenantID types.TenantID, requestID s
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status == types.FriendRequestAccepted {
|
||||
return tenantAdp.TenantFriendRequestAccept(tenantID, requestID, actor, handledAt)
|
||||
}
|
||||
req, err := tenantAdp.TenantFriendRequestResolve(tenantID, requestID, actor, status, handledAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status == types.FriendRequestAccepted {
|
||||
if err := tenantAdp.TenantFriendshipUpsertPair(tenantID, req.ApplicantID, req.TargetID, "", handledAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (relationsMapper) ListFriends(tenantID types.TenantID, uid types.Uid, query, initial, cursor string, limit int) ([]types.FriendListItem, string, error) {
|
||||
if uid.IsZero() {
|
||||
return nil, "", types.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return tenantAdp.TenantFriendshipList(tenantID, uid, query, initial, cursor, limit)
|
||||
}
|
||||
|
||||
func (relationsMapper) GetRelationshipContext(tenantID types.TenantID, userID, peerID types.Uid, topic string) (*types.RelationshipContext, error) {
|
||||
if userID.IsZero() || peerID.IsZero() || userID == peerID {
|
||||
return nil, types.ErrMalformed
|
||||
|
||||
@@ -63,3 +63,14 @@ type RelationshipContext struct {
|
||||
PendingStatus string `json:"pending_status,omitempty"`
|
||||
Peer *User `json:"peer,omitempty"`
|
||||
}
|
||||
|
||||
type FriendListItem struct {
|
||||
Friendship Friendship
|
||||
Peer *User
|
||||
}
|
||||
|
||||
type FriendRequestListItem struct {
|
||||
Request FriendRequest
|
||||
Peer *User
|
||||
Direction string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user