feat: complete im p2p relationship onboarding

This commit is contained in:
richjohnrambo
2026-08-12 15:20:15 +08:00
parent d02041659e
commit 943ebe1397
7 changed files with 666 additions and 17 deletions
+3
View File
@@ -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)
+369 -4
View File
@@ -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