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
|
||||
|
||||
Reference in New Issue
Block a user