feat: add im relationship flows
This commit is contained in:
@@ -317,4 +317,14 @@ type TenantBusinessAdapter interface {
|
|||||||
TenantSystemNotificationCreate(t.TenantID, *t.SystemNotification) error
|
TenantSystemNotificationCreate(t.TenantID, *t.SystemNotification) error
|
||||||
TenantSystemNotificationList(t.TenantID, t.Uid, string, int) ([]t.SystemNotification, string, error)
|
TenantSystemNotificationList(t.TenantID, t.Uid, string, int) ([]t.SystemNotification, string, error)
|
||||||
TenantSystemNotificationMarkRead(t.TenantID, t.Uid, string, time.Time) error
|
TenantSystemNotificationMarkRead(t.TenantID, t.Uid, string, time.Time) error
|
||||||
|
|
||||||
|
TenantFriendRequestCreate(t.TenantID, *t.FriendRequest) (*t.FriendRequest, error)
|
||||||
|
TenantFriendRequestLatestBetween(t.TenantID, t.Uid, t.Uid) (*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
|
||||||
|
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)
|
||||||
|
TenantUserBlockUpsert(t.TenantID, t.Uid, t.Uid, time.Time) (*t.UserBlock, error)
|
||||||
|
TenantUserBlockDelete(t.TenantID, t.Uid, t.Uid) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ type adapter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
adpVersion = 122
|
adpVersion = 123
|
||||||
adapterName = "mysql"
|
adapterName = "mysql"
|
||||||
|
|
||||||
defaultDSN = "root:@tcp(localhost:3306)/tinode?parseTime=true"
|
defaultDSN = "root:@tcp(localhost:3306)/tinode?parseTime=true"
|
||||||
@@ -637,6 +637,10 @@ func (a *adapter) CreateDb(reset bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = createIMRelationTables(tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Deletion log
|
// Deletion log
|
||||||
if _, err = tx.Exec(
|
if _, err = tx.Exec(
|
||||||
`CREATE TABLE dellog(
|
`CREATE TABLE dellog(
|
||||||
@@ -1149,6 +1153,15 @@ func (a *adapter) UpgradeDb() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.version == 122 {
|
||||||
|
if err := createIMRelationTables(a.db.DB); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := bumpVersion(a, 123); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if a.version != adpVersion {
|
if a.version != adpVersion {
|
||||||
return errors.New("Failed to perform database upgrade to version " + strconv.Itoa(adpVersion) +
|
return errors.New("Failed to perform database upgrade to version " + strconv.Itoa(adpVersion) +
|
||||||
". DB is still at " + strconv.Itoa(a.version))
|
". DB is still at " + strconv.Itoa(a.version))
|
||||||
@@ -1208,6 +1221,66 @@ func upgradeIMMessageActions(a *adapter) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type sqlExecutor interface {
|
||||||
|
Exec(query string, args ...any) (sql.Result, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createIMRelationTables(exec sqlExecutor) error {
|
||||||
|
statements := []string{
|
||||||
|
`CREATE TABLE im_friend_requests(
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
tenant_id BIGINT NOT NULL,
|
||||||
|
request_id VARCHAR(64) NOT NULL,
|
||||||
|
applicant_id BIGINT NOT NULL,
|
||||||
|
target_id BIGINT NOT NULL,
|
||||||
|
message VARCHAR(200) NOT NULL DEFAULT '',
|
||||||
|
source VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
source_topic VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
status VARCHAR(16) NOT NULL,
|
||||||
|
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
expires_at DATETIME(3) NOT NULL,
|
||||||
|
handled_at DATETIME(3),
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_friend_requests_request(tenant_id, request_id),
|
||||||
|
UNIQUE INDEX uk_im_friend_requests_idempotency(tenant_id, applicant_id, idempotency_key),
|
||||||
|
INDEX idx_im_friend_requests_pair(tenant_id, applicant_id, target_id, status, created_at),
|
||||||
|
INDEX idx_im_friend_requests_inbox(tenant_id, target_id, status, created_at),
|
||||||
|
CONSTRAINT fk_im_friend_requests_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE im_friendships(
|
||||||
|
tenant_id BIGINT NOT NULL,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
peer_id BIGINT NOT NULL,
|
||||||
|
status VARCHAR(16) NOT NULL,
|
||||||
|
remark VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
topic CHAR(25) NOT NULL DEFAULT '',
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY(tenant_id, user_id, peer_id),
|
||||||
|
INDEX idx_im_friendships_peer(tenant_id, peer_id, status),
|
||||||
|
CONSTRAINT fk_im_friendships_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE im_user_blocks(
|
||||||
|
tenant_id BIGINT NOT NULL,
|
||||||
|
blocker_id BIGINT NOT NULL,
|
||||||
|
blocked_id BIGINT NOT NULL,
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY(tenant_id, blocker_id, blocked_id),
|
||||||
|
INDEX idx_im_user_blocks_blocked(tenant_id, blocked_id),
|
||||||
|
CONSTRAINT fk_im_user_blocks_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||||
|
)`,
|
||||||
|
}
|
||||||
|
for _, stmt := range statements {
|
||||||
|
if _, err := exec.Exec(stmt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func createIMSecurityTables(tx *sql.Tx) error {
|
func createIMSecurityTables(tx *sql.Tx) error {
|
||||||
if _, err := tx.Exec(
|
if _, err := tx.Exec(
|
||||||
`CREATE TABLE im_realname_verifications(
|
`CREATE TABLE im_realname_verifications(
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ CREATE TABLE kvmeta(
|
|||||||
INDEX kvmeta_createdat_key(createdat, `key`)
|
INDEX kvmeta_createdat_key(createdat, `key`)
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO kvmeta(`key`, `value`) VALUES("version", "122");
|
INSERT INTO kvmeta(`key`, `value`) VALUES("version", "123");
|
||||||
|
|
||||||
CREATE TABLE users(
|
CREATE TABLE users(
|
||||||
id BIGINT NOT NULL,
|
id BIGINT NOT NULL,
|
||||||
@@ -195,6 +195,57 @@ CREATE TABLE im_system_notifications(
|
|||||||
CONSTRAINT fk_im_system_notifications_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
CONSTRAINT fk_im_system_notifications_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE im_friend_requests(
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
tenant_id BIGINT NOT NULL,
|
||||||
|
request_id VARCHAR(64) NOT NULL,
|
||||||
|
applicant_id BIGINT NOT NULL,
|
||||||
|
target_id BIGINT NOT NULL,
|
||||||
|
message VARCHAR(200) NOT NULL DEFAULT '',
|
||||||
|
source VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
source_topic VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
status VARCHAR(16) NOT NULL,
|
||||||
|
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
expires_at DATETIME(3) NOT NULL,
|
||||||
|
handled_at DATETIME(3),
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
|
||||||
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_friend_requests_request(tenant_id, request_id),
|
||||||
|
UNIQUE INDEX uk_im_friend_requests_idempotency(tenant_id, applicant_id, idempotency_key),
|
||||||
|
INDEX idx_im_friend_requests_pair(tenant_id, applicant_id, target_id, status, created_at),
|
||||||
|
INDEX idx_im_friend_requests_inbox(tenant_id, target_id, status, created_at),
|
||||||
|
CONSTRAINT fk_im_friend_requests_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE im_friendships(
|
||||||
|
tenant_id BIGINT NOT NULL,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
peer_id BIGINT NOT NULL,
|
||||||
|
status VARCHAR(16) NOT NULL,
|
||||||
|
remark VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
topic CHAR(25) NOT NULL DEFAULT '',
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
|
||||||
|
PRIMARY KEY(tenant_id, user_id, peer_id),
|
||||||
|
INDEX idx_im_friendships_peer(tenant_id, peer_id, status),
|
||||||
|
CONSTRAINT fk_im_friendships_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE im_user_blocks(
|
||||||
|
tenant_id BIGINT NOT NULL,
|
||||||
|
blocker_id BIGINT NOT NULL,
|
||||||
|
blocked_id BIGINT NOT NULL,
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
|
||||||
|
PRIMARY KEY(tenant_id, blocker_id, blocked_id),
|
||||||
|
INDEX idx_im_user_blocks_blocked(tenant_id, blocked_id),
|
||||||
|
CONSTRAINT fk_im_user_blocks_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
# Indexed user tags.
|
# Indexed user tags.
|
||||||
CREATE TABLE usertags(
|
CREATE TABLE usertags(
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
|
|||||||
@@ -2514,6 +2514,252 @@ func (a *adapter) TenantSystemNotificationMarkRead(id t.TenantID, uid t.Uid, eve
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *adapter) TenantFriendRequestCreate(id t.TenantID, req *t.FriendRequest) (*t.FriendRequest, error) {
|
||||||
|
if err := validTenant(id); err != nil || req == nil || req.TenantID != id || req.ApplicantID.IsZero() || req.TargetID.IsZero() {
|
||||||
|
return nil, t.ErrMalformed
|
||||||
|
}
|
||||||
|
ctx, cancel := a.getContext()
|
||||||
|
if cancel != nil {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
_, err := a.db.ExecContext(ctx,
|
||||||
|
"INSERT INTO im_friend_requests(tenant_id,request_id,applicant_id,target_id,message,source,source_topic,status,idempotency_key,expires_at,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
|
id, req.RequestID, decodeUID(req.ApplicantID), decodeUID(req.TargetID), req.Message, req.Source, req.SourceTopic, req.Status, req.IdempotencyKey, req.ExpiresAt, req.CreatedAt, req.UpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
if isDupe(err) && req.IdempotencyKey != "" {
|
||||||
|
return a.friendRequestByIdempotency(ctx, id, req.ApplicantID, req.IdempotencyKey)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return a.friendRequestByRequestID(ctx, id, req.RequestID)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
ctx, cancel := a.getContext()
|
||||||
|
if cancel != nil {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
return a.friendRequestByQuery(ctx,
|
||||||
|
"SELECT * FROM im_friend_requests WHERE tenant_id=? AND ((applicant_id=? AND target_id=?) OR (applicant_id=? AND target_id=?)) ORDER BY created_at DESC,id DESC LIMIT 1",
|
||||||
|
id, decodeUID(uid1), decodeUID(uid2), decodeUID(uid2), decodeUID(uid1))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
ctx, cancel := a.getContext()
|
||||||
|
if cancel != nil {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
req, err := a.friendRequestByRequestID(ctx, id, requestID)
|
||||||
|
if err != nil || req == nil {
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
if req.TargetID != actor {
|
||||||
|
return nil, t.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
if req.Status != t.FriendRequestPending {
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
if _, err = a.db.ExecContext(ctx,
|
||||||
|
"UPDATE im_friend_requests SET status=?,handled_at=?,updated_at=? WHERE tenant_id=? AND request_id=? AND status=?",
|
||||||
|
status, handledAt, handledAt, id, requestID, t.FriendRequestPending); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return a.friendRequestByRequestID(ctx, id, requestID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) TenantFriendshipUpsertPair(id t.TenantID, uid1, uid2 t.Uid, topic string, now time.Time) error {
|
||||||
|
if err := validTenant(id); err != nil || uid1.IsZero() || uid2.IsZero() || uid1 == uid2 {
|
||||||
|
return t.ErrMalformed
|
||||||
|
}
|
||||||
|
if topic == "" {
|
||||||
|
topic = uid1.P2PName(uid2)
|
||||||
|
}
|
||||||
|
ctx, cancel := a.getContext()
|
||||||
|
if cancel != nil {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
for _, pair := range [][2]t.Uid{{uid1, uid2}, {uid2, uid1}} {
|
||||||
|
if _, err := a.db.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, now, now); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 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
|
||||||
|
}
|
||||||
|
ctx, cancel := a.getContext()
|
||||||
|
if cancel != nil {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
row := struct {
|
||||||
|
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"`
|
||||||
|
}{}
|
||||||
|
err := a.db.GetContext(ctx, &row, "SELECT * FROM im_friendships WHERE tenant_id=? AND user_id=? AND peer_id=?",
|
||||||
|
id, decodeUID(userID), decodeUID(peerID))
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &t.Friendship{
|
||||||
|
TenantID: row.TenantID,
|
||||||
|
UserID: store.EncodeUid(row.UserID),
|
||||||
|
PeerID: store.EncodeUid(row.PeerID),
|
||||||
|
Status: row.Status,
|
||||||
|
Remark: row.Remark,
|
||||||
|
Topic: row.Topic,
|
||||||
|
CreatedAt: row.CreatedAt,
|
||||||
|
UpdatedAt: row.UpdatedAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) TenantFriendshipDeletePair(id t.TenantID, uid1, uid2 t.Uid, now time.Time) error {
|
||||||
|
if err := validTenant(id); err != nil || uid1.IsZero() || uid2.IsZero() {
|
||||||
|
return t.ErrMalformed
|
||||||
|
}
|
||||||
|
ctx, cancel := a.getContext()
|
||||||
|
if cancel != nil {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
_, err := a.db.ExecContext(ctx,
|
||||||
|
"UPDATE im_friendships SET status=?,updated_at=? WHERE tenant_id=? AND ((user_id=? AND peer_id=?) OR (user_id=? AND peer_id=?))",
|
||||||
|
t.RelationshipNone, now, id, decodeUID(uid1), decodeUID(uid2), decodeUID(uid2), decodeUID(uid1))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) TenantUserBlockGet(id t.TenantID, blockerID, blockedID t.Uid) (*t.UserBlock, error) {
|
||||||
|
if err := validTenant(id); err != nil || blockerID.IsZero() || blockedID.IsZero() {
|
||||||
|
return nil, t.ErrMalformed
|
||||||
|
}
|
||||||
|
ctx, cancel := a.getContext()
|
||||||
|
if cancel != nil {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
row := struct {
|
||||||
|
TenantID t.TenantID `db:"tenant_id"`
|
||||||
|
BlockerID int64 `db:"blocker_id"`
|
||||||
|
BlockedID int64 `db:"blocked_id"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
}{}
|
||||||
|
err := a.db.GetContext(ctx, &row, "SELECT * FROM im_user_blocks WHERE tenant_id=? AND blocker_id=? AND blocked_id=?",
|
||||||
|
id, decodeUID(blockerID), decodeUID(blockedID))
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &t.UserBlock{
|
||||||
|
TenantID: row.TenantID,
|
||||||
|
BlockerID: store.EncodeUid(row.BlockerID),
|
||||||
|
BlockedID: store.EncodeUid(row.BlockedID),
|
||||||
|
CreatedAt: row.CreatedAt,
|
||||||
|
UpdatedAt: row.UpdatedAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) TenantUserBlockUpsert(id t.TenantID, blockerID, blockedID t.Uid, now time.Time) (*t.UserBlock, error) {
|
||||||
|
if err := validTenant(id); err != nil || blockerID.IsZero() || blockedID.IsZero() {
|
||||||
|
return nil, t.ErrMalformed
|
||||||
|
}
|
||||||
|
ctx, cancel := a.getContext()
|
||||||
|
if cancel != nil {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
_, err := a.db.ExecContext(ctx,
|
||||||
|
"INSERT INTO im_user_blocks(tenant_id,blocker_id,blocked_id,created_at,updated_at) VALUES(?,?,?,?,?) "+
|
||||||
|
"ON DUPLICATE KEY UPDATE updated_at=VALUES(updated_at)",
|
||||||
|
id, decodeUID(blockerID), decodeUID(blockedID), now, now)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return a.TenantUserBlockGet(id, blockerID, blockedID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) TenantUserBlockDelete(id t.TenantID, blockerID, blockedID t.Uid) error {
|
||||||
|
if err := validTenant(id); err != nil || blockerID.IsZero() || blockedID.IsZero() {
|
||||||
|
return t.ErrMalformed
|
||||||
|
}
|
||||||
|
ctx, cancel := a.getContext()
|
||||||
|
if cancel != nil {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
_, err := a.db.ExecContext(ctx, "DELETE FROM im_user_blocks WHERE tenant_id=? AND blocker_id=? AND blocked_id=?",
|
||||||
|
id, decodeUID(blockerID), decodeUID(blockedID))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) friendRequestByRequestID(ctx context.Context, tenantID t.TenantID, requestID string) (*t.FriendRequest, error) {
|
||||||
|
return a.friendRequestByQuery(ctx, "SELECT * FROM im_friend_requests WHERE tenant_id=? AND request_id=?", tenantID, requestID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) friendRequestByIdempotency(ctx context.Context, tenantID t.TenantID, applicantID t.Uid, key string) (*t.FriendRequest, error) {
|
||||||
|
return a.friendRequestByQuery(ctx, "SELECT * FROM im_friend_requests WHERE tenant_id=? AND applicant_id=? AND idempotency_key=?", tenantID, decodeUID(applicantID), key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) friendRequestByQuery(ctx context.Context, query string, args ...any) (*t.FriendRequest, error) {
|
||||||
|
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 := a.db.GetContext(ctx, &row, query, args...)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
func nullableUID(uid t.Uid) any {
|
func nullableUID(uid t.Uid) any {
|
||||||
if uid.IsZero() {
|
if uid.IsZero() {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -127,6 +127,13 @@ type imMessageRevisionRequest struct {
|
|||||||
Reason string `json:"reason,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type imFriendRequestCreateRequest struct {
|
||||||
|
TargetUserID string `json:"target_user_id"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
SourceTopic string `json:"source_topic,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func serveIMTenantResolve(wrt http.ResponseWriter, req *http.Request) {
|
func serveIMTenantResolve(wrt http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != http.MethodPost {
|
if req.Method != http.MethodPost {
|
||||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||||
@@ -983,6 +990,163 @@ func serveIMMessageRevision(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
writeIMSuccess(wrt, req, messageMutationPayload(updated, "edited"))
|
writeIMSuccess(wrt, req, messageMutationPayload(updated, "edited"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func serveIMConversationContext(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
|
||||||
|
}
|
||||||
|
topic := conversationContextTopicFromPath(req.URL.Path)
|
||||||
|
peer := peerFromP2PTopic(topic, principal.Uid)
|
||||||
|
if topic == "" || peer.IsZero() {
|
||||||
|
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, err := store.Relations.GetRelationshipContext(principal.TenantID, principal.Uid, peer, topic)
|
||||||
|
if err != nil {
|
||||||
|
writeIMRelationError(wrt, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeIMSuccess(wrt, req, relationshipContextPayload(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveIMFriendRequests(wrt http.ResponseWriter, req *http.Request) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
var body imFriendRequestCreateRequest
|
||||||
|
if !decodeIMJSONBody(wrt, req, &body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
target := types.ParseUserId(body.TargetUserID)
|
||||||
|
message := strings.TrimSpace(body.Message)
|
||||||
|
if target.IsZero() || target == principal.Uid || len([]rune(message)) > 50 {
|
||||||
|
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
idempotencyKey := strings.TrimSpace(req.Header.Get("Idempotency-Key"))
|
||||||
|
if idempotencyKey == "" {
|
||||||
|
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "Idempotency-Key is required", map[string]any{"field": "Idempotency-Key"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now().UTC().Round(time.Millisecond)
|
||||||
|
requestID := "frq_" + store.Store.GetUidString()
|
||||||
|
created, err := store.Relations.CreateFriendRequest(principal.TenantID, &types.FriendRequest{
|
||||||
|
TenantID: principal.TenantID,
|
||||||
|
RequestID: requestID,
|
||||||
|
ApplicantID: principal.Uid,
|
||||||
|
TargetID: target,
|
||||||
|
Message: message,
|
||||||
|
Source: strings.TrimSpace(body.Source),
|
||||||
|
SourceTopic: strings.TrimSpace(body.SourceTopic),
|
||||||
|
Status: types.FriendRequestPending,
|
||||||
|
IdempotencyKey: idempotencyKey,
|
||||||
|
ExpiresAt: now.Add(7 * 24 * time.Hour),
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeIMRelationError(wrt, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeIMSuccess(wrt, req, friendRequestPayload(created))
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveIMFriendRequestAction(wrt http.ResponseWriter, req *http.Request) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
requestID, action := friendRequestActionFromPath(req.URL.Path)
|
||||||
|
status := ""
|
||||||
|
switch action {
|
||||||
|
case "accept":
|
||||||
|
status = types.FriendRequestAccepted
|
||||||
|
case "reject":
|
||||||
|
status = types.FriendRequestRejected
|
||||||
|
default:
|
||||||
|
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "friend request action not found", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updated, err := store.Relations.ResolveFriendRequest(principal.TenantID, requestID, principal.Uid, status, time.Now().UTC().Round(time.Millisecond))
|
||||||
|
if err != nil {
|
||||||
|
writeIMRelationError(wrt, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeIMSuccess(wrt, req, friendRequestPayload(updated))
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveIMBlockUser(wrt http.ResponseWriter, req *http.Request) {
|
||||||
|
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
peer := userIDFromTail(req.URL.Path, "/v1/im/blocks/")
|
||||||
|
if peer.IsZero() || peer == principal.Uid {
|
||||||
|
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch req.Method {
|
||||||
|
case http.MethodPut:
|
||||||
|
if _, err := store.Relations.BlockUser(principal.TenantID, principal.Uid, peer, time.Now().UTC().Round(time.Millisecond)); err != nil {
|
||||||
|
writeIMRelationError(wrt, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case http.MethodDelete:
|
||||||
|
if err := store.Relations.UnblockUser(principal.TenantID, principal.Uid, peer); err != nil {
|
||||||
|
writeIMRelationError(wrt, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, err := store.Relations.GetRelationshipContext(principal.TenantID, principal.Uid, peer, principal.Uid.P2PName(peer))
|
||||||
|
if err != nil {
|
||||||
|
writeIMRelationError(wrt, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeIMSuccess(wrt, req, relationshipContextPayload(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveIMFriendResource(wrt http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodDelete {
|
||||||
|
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
peer := userIDFromTail(req.URL.Path, "/v1/im/friends/")
|
||||||
|
if peer.IsZero() || peer == principal.Uid {
|
||||||
|
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := store.Relations.DeleteFriend(principal.TenantID, principal.Uid, peer, time.Now().UTC().Round(time.Millisecond)); err != nil {
|
||||||
|
writeIMRelationError(wrt, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, err := store.Relations.GetRelationshipContext(principal.TenantID, principal.Uid, peer, principal.Uid.P2PName(peer))
|
||||||
|
if err != nil {
|
||||||
|
writeIMRelationError(wrt, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeIMSuccess(wrt, req, relationshipContextPayload(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
func serveIMIndependentPassword(wrt http.ResponseWriter, req *http.Request) {
|
func serveIMIndependentPassword(wrt http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != http.MethodPost {
|
if req.Method != http.MethodPost {
|
||||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||||
@@ -1413,6 +1577,68 @@ func messageMutationPayload(msg *types.Message, status string) map[string]any {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func relationshipContextPayload(ctx *types.RelationshipContext) map[string]any {
|
||||||
|
data := map[string]any{
|
||||||
|
"topic": ctx.Topic,
|
||||||
|
"peer_user_id": ctx.PeerID.UserId(),
|
||||||
|
"relationship": ctx.Relationship,
|
||||||
|
"can_send": ctx.CanSend,
|
||||||
|
"capabilities": map[string]any{
|
||||||
|
"can_send_text": ctx.CanSend,
|
||||||
|
"can_send_media": ctx.CanSend,
|
||||||
|
"can_send_card": ctx.CanSend,
|
||||||
|
"can_call": ctx.CanSend,
|
||||||
|
"can_add_friend": ctx.Relationship == types.RelationshipNone,
|
||||||
|
"can_handle_request": ctx.Relationship == types.RelationshipPendingInbound,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if ctx.RequestID != "" {
|
||||||
|
data["request_id"] = ctx.RequestID
|
||||||
|
}
|
||||||
|
if ctx.Peer != nil {
|
||||||
|
data["peer"] = map[string]any{
|
||||||
|
"user_id": ctx.PeerID.UserId(),
|
||||||
|
"public": ctx.Peer.Public,
|
||||||
|
"trusted": ctx.Peer.Trusted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func friendRequestPayload(req *types.FriendRequest) map[string]any {
|
||||||
|
data := map[string]any{
|
||||||
|
"request_id": req.RequestID,
|
||||||
|
"applicant_id": req.ApplicantID.UserId(),
|
||||||
|
"target_user_id": req.TargetID.UserId(),
|
||||||
|
"message": req.Message,
|
||||||
|
"source": req.Source,
|
||||||
|
"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 req.SourceTopic != "" {
|
||||||
|
data["source_topic"] = req.SourceTopic
|
||||||
|
}
|
||||||
|
if req.HandledAt != nil {
|
||||||
|
data["handled_at"] = req.HandledAt.UTC().Format(time.RFC3339Nano)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeIMRelationError(wrt http.ResponseWriter, req *http.Request, err error) {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, types.ErrNotFound):
|
||||||
|
writeIMError(wrt, req, http.StatusNotFound, 20004, imReasonMalformedRequest, "resource not found", nil)
|
||||||
|
case errors.Is(err, types.ErrPermissionDenied):
|
||||||
|
writeIMError(wrt, req, http.StatusForbidden, 20005, imReasonPermissionDenied, "permission denied", nil)
|
||||||
|
case errors.Is(err, types.ErrMalformed):
|
||||||
|
writeIMError(wrt, req, http.StatusBadRequest, 20000, imReasonMalformedRequest, "malformed request", nil)
|
||||||
|
default:
|
||||||
|
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func validIMRealName(name, idType, idNumber string) bool {
|
func validIMRealName(name, idType, idNumber string) bool {
|
||||||
if len([]rune(name)) < 2 || len([]rune(name)) > 32 || idType != "cn_id" {
|
if len([]rune(name)) < 2 || len([]rune(name)) > 32 || idType != "cn_id" {
|
||||||
return false
|
return false
|
||||||
@@ -1690,3 +1916,56 @@ func messageActionPath(path, action string) (string, int, bool) {
|
|||||||
}
|
}
|
||||||
return topic, seq, true
|
return topic, seq, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func conversationContextTopicFromPath(path string) string {
|
||||||
|
const prefix = "/v1/im/conversations/"
|
||||||
|
const suffix = "/context"
|
||||||
|
if !strings.Contains(path, prefix) || !strings.HasSuffix(path, suffix) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
idx := strings.Index(path, prefix)
|
||||||
|
return strings.TrimSuffix(strings.TrimPrefix(path[idx:], prefix), suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func friendRequestActionFromPath(path string) (string, string) {
|
||||||
|
const prefix = "/v1/im/friend-requests/"
|
||||||
|
idx := strings.Index(path, prefix)
|
||||||
|
if idx < 0 {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
rest := strings.TrimPrefix(path[idx:], prefix)
|
||||||
|
requestID, action, ok := strings.Cut(rest, "/")
|
||||||
|
if !ok {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(requestID), strings.TrimSpace(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userIDFromTail(path, prefix string) types.Uid {
|
||||||
|
idx := strings.Index(path, prefix)
|
||||||
|
if idx < 0 {
|
||||||
|
return types.ZeroUid
|
||||||
|
}
|
||||||
|
return types.ParseUserId(strings.TrimSpace(strings.TrimPrefix(path[idx:], prefix)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func peerFromP2PTopic(topic string, uid types.Uid) types.Uid {
|
||||||
|
if types.GetTopicCat(topic) != types.TopicCatP2P || uid.IsZero() {
|
||||||
|
return types.ZeroUid
|
||||||
|
}
|
||||||
|
prefix := "p2p"
|
||||||
|
body := strings.TrimPrefix(topic, prefix)
|
||||||
|
uidLen := len(uid.String())
|
||||||
|
if len(body) < 2*uidLen {
|
||||||
|
return types.ZeroUid
|
||||||
|
}
|
||||||
|
left := types.ParseUid(body[:uidLen])
|
||||||
|
right := types.ParseUid(body[uidLen:])
|
||||||
|
if left == uid {
|
||||||
|
return right
|
||||||
|
}
|
||||||
|
if right == uid {
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
return types.ZeroUid
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,70 @@ type testVerificationStore struct {
|
|||||||
tokenHash string
|
tokenHash string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type testRelationsStore struct {
|
||||||
|
context *types.RelationshipContext
|
||||||
|
created *types.FriendRequest
|
||||||
|
blocked *types.UserBlock
|
||||||
|
deleted bool
|
||||||
|
canSend *bool
|
||||||
|
reason string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testRelationsStore) CreateFriendRequest(_ types.TenantID, req *types.FriendRequest) (*types.FriendRequest, error) {
|
||||||
|
s.created = req
|
||||||
|
return req, 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,
|
||||||
|
RequestID: requestID,
|
||||||
|
ApplicantID: 1,
|
||||||
|
TargetID: actor,
|
||||||
|
Status: status,
|
||||||
|
HandledAt: &handledAt,
|
||||||
|
CreatedAt: handledAt,
|
||||||
|
UpdatedAt: handledAt,
|
||||||
|
ExpiresAt: handledAt.Add(time.Hour),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testRelationsStore) GetRelationshipContext(_ types.TenantID, userID, peerID types.Uid, topic string) (*types.RelationshipContext, error) {
|
||||||
|
if s.context != nil {
|
||||||
|
return s.context, nil
|
||||||
|
}
|
||||||
|
return &types.RelationshipContext{
|
||||||
|
TenantID: 7,
|
||||||
|
UserID: userID,
|
||||||
|
PeerID: peerID,
|
||||||
|
Topic: topic,
|
||||||
|
Relationship: types.RelationshipFriend,
|
||||||
|
CanSend: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testRelationsStore) CanSendP2P(_ types.TenantID, _, _ types.Uid) (bool, string, error) {
|
||||||
|
if s.canSend != nil {
|
||||||
|
return *s.canSend, s.reason, nil
|
||||||
|
}
|
||||||
|
return true, types.RelationshipFriend, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testRelationsStore) BlockUser(tenantID types.TenantID, blockerID, blockedID types.Uid, now time.Time) (*types.UserBlock, error) {
|
||||||
|
s.blocked = &types.UserBlock{TenantID: tenantID, BlockerID: blockerID, BlockedID: blockedID, CreatedAt: now, UpdatedAt: now}
|
||||||
|
return s.blocked, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testRelationsStore) UnblockUser(_ types.TenantID, _, _ types.Uid) error {
|
||||||
|
s.blocked = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testRelationsStore) DeleteFriend(_ types.TenantID, _, _ types.Uid, _ time.Time) error {
|
||||||
|
s.deleted = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *testVerificationStore) Create(_ types.TenantID, vc *types.VerificationCode) error {
|
func (s *testVerificationStore) Create(_ types.TenantID, vc *types.VerificationCode) error {
|
||||||
s.created = vc
|
s.created = vc
|
||||||
return s.err
|
return s.err
|
||||||
@@ -861,6 +925,65 @@ func TestServeIMMessageRevision(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServeIMConversationContext(t *testing.T) {
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
expectIMBearer(t, ctrl, "token-1", 7, 1)
|
||||||
|
oldRelations := store.Relations
|
||||||
|
peer := types.Uid(2)
|
||||||
|
topic := types.Uid(1).P2PName(peer)
|
||||||
|
store.Relations = &testRelationsStore{context: &types.RelationshipContext{
|
||||||
|
TenantID: 7,
|
||||||
|
UserID: 1,
|
||||||
|
PeerID: peer,
|
||||||
|
Topic: topic,
|
||||||
|
Relationship: types.RelationshipFriend,
|
||||||
|
CanSend: true,
|
||||||
|
}}
|
||||||
|
defer func() { store.Relations = oldRelations }()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/v1/im/conversations/"+topic+"/context", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token-1")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
serveIMConversationContext(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)
|
||||||
|
if data["relationship"] != types.RelationshipFriend || data["can_send"] != true {
|
||||||
|
t.Fatalf("unexpected context response: %#v", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServeIMFriendRequestCreate(t *testing.T) {
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
mockStore := expectIMBearer(t, ctrl, "token-1", 7, 1)
|
||||||
|
mockStore.EXPECT().GetUidString().Return("request-1")
|
||||||
|
oldRelations := store.Relations
|
||||||
|
relations := &testRelationsStore{}
|
||||||
|
store.Relations = relations
|
||||||
|
defer func() { store.Relations = oldRelations }()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/im/friend-requests",
|
||||||
|
bytes.NewBufferString(`{"target_user_id":"`+types.Uid(2).UserId()+`","message":"你好","source":"search"}`))
|
||||||
|
req.Header.Set("Authorization", "Bearer token-1")
|
||||||
|
req.Header.Set("Idempotency-Key", "fr-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)
|
||||||
|
if data["status"] != types.FriendRequestPending || relations.created == nil || relations.created.IdempotencyKey != "fr-1" {
|
||||||
|
t.Fatalf("unexpected friend request response: %#v created=%#v", data, relations.created)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ptrTime(t time.Time) *time.Time {
|
func ptrTime(t time.Time) *time.Time {
|
||||||
return &t
|
return &t
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -793,6 +793,11 @@ func main() {
|
|||||||
mux.HandleFunc(config.ApiPath+"v1/im/system-notifications", serveIMSystemNotifications)
|
mux.HandleFunc(config.ApiPath+"v1/im/system-notifications", serveIMSystemNotifications)
|
||||||
mux.HandleFunc(config.ApiPath+"v1/im/system-notifications/", serveIMSystemNotificationRead)
|
mux.HandleFunc(config.ApiPath+"v1/im/system-notifications/", serveIMSystemNotificationRead)
|
||||||
mux.HandleFunc(config.ApiPath+"v1/im/topics/", serveIMTopicMessageAction)
|
mux.HandleFunc(config.ApiPath+"v1/im/topics/", serveIMTopicMessageAction)
|
||||||
|
mux.HandleFunc(config.ApiPath+"v1/im/conversations/", serveIMConversationContext)
|
||||||
|
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/", serveIMFriendResource)
|
||||||
// Handle long polling clients. Enable compression.
|
// Handle long polling clients. Enable compression.
|
||||||
mux.Handle(config.ApiPath+"v0/channels/lp", gh.CompressHandler(http.HandlerFunc(serveLongPoll)))
|
mux.Handle(config.ApiPath+"v0/channels/lp", gh.CompressHandler(http.HandlerFunc(serveLongPoll)))
|
||||||
if config.Media != nil {
|
if config.Media != nil {
|
||||||
|
|||||||
@@ -995,6 +995,157 @@ func (messagesMapper) Edit(tenantID types.TenantID, topic string, seq int, actor
|
|||||||
return tenantAdp.TenantMessageEdit(tenantID, topic, seq, actor, expectedVersion, head, content, reason, editedAt)
|
return tenantAdp.TenantMessageEdit(tenantID, topic, seq, actor, expectedVersion, head, content, reason, editedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RelationsPersistenceInterface interface {
|
||||||
|
CreateFriendRequest(tenantID types.TenantID, req *types.FriendRequest) (*types.FriendRequest, error)
|
||||||
|
ResolveFriendRequest(tenantID types.TenantID, requestID string, actor types.Uid, status string, handledAt time.Time) (*types.FriendRequest, 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)
|
||||||
|
UnblockUser(tenantID types.TenantID, blockerID, blockedID types.Uid) error
|
||||||
|
DeleteFriend(tenantID types.TenantID, userID, peerID types.Uid, now time.Time) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type relationsMapper struct{}
|
||||||
|
|
||||||
|
var Relations RelationsPersistenceInterface
|
||||||
|
|
||||||
|
func (relationsMapper) CreateFriendRequest(tenantID types.TenantID, req *types.FriendRequest) (*types.FriendRequest, error) {
|
||||||
|
if req == nil || req.TenantID != tenantID || req.ApplicantID.IsZero() || req.TargetID.IsZero() || req.ApplicantID == req.TargetID {
|
||||||
|
return nil, types.ErrMalformed
|
||||||
|
}
|
||||||
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tenantAdp.TenantFriendRequestCreate(tenantID, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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) GetRelationshipContext(tenantID types.TenantID, userID, peerID types.Uid, topic string) (*types.RelationshipContext, error) {
|
||||||
|
if userID.IsZero() || peerID.IsZero() || userID == peerID {
|
||||||
|
return nil, types.ErrMalformed
|
||||||
|
}
|
||||||
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return relationshipContext(tenantAdp, tenantID, userID, peerID, topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (relationsMapper) CanSendP2P(tenantID types.TenantID, userID, peerID types.Uid) (bool, string, error) {
|
||||||
|
ctx, err := (relationsMapper{}).GetRelationshipContext(tenantID, userID, peerID, "")
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
if ctx.Relationship == types.RelationshipFriend {
|
||||||
|
return true, ctx.Relationship, nil
|
||||||
|
}
|
||||||
|
return false, ctx.Relationship, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (relationsMapper) BlockUser(tenantID types.TenantID, blockerID, blockedID types.Uid, now time.Time) (*types.UserBlock, error) {
|
||||||
|
if blockerID.IsZero() || blockedID.IsZero() || blockerID == blockedID {
|
||||||
|
return nil, types.ErrMalformed
|
||||||
|
}
|
||||||
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tenantAdp.TenantUserBlockUpsert(tenantID, blockerID, blockedID, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (relationsMapper) UnblockUser(tenantID types.TenantID, blockerID, blockedID types.Uid) error {
|
||||||
|
if blockerID.IsZero() || blockedID.IsZero() || blockerID == blockedID {
|
||||||
|
return types.ErrMalformed
|
||||||
|
}
|
||||||
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tenantAdp.TenantUserBlockDelete(tenantID, blockerID, blockedID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (relationsMapper) DeleteFriend(tenantID types.TenantID, userID, peerID types.Uid, now time.Time) error {
|
||||||
|
if userID.IsZero() || peerID.IsZero() || userID == peerID {
|
||||||
|
return types.ErrMalformed
|
||||||
|
}
|
||||||
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tenantAdp.TenantFriendshipDeletePair(tenantID, userID, peerID, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
func relationshipContext(tenantAdp adapter.TenantBusinessAdapter, tenantID types.TenantID, userID, peerID types.Uid, topic string) (*types.RelationshipContext, error) {
|
||||||
|
ctx := &types.RelationshipContext{
|
||||||
|
TenantID: tenantID,
|
||||||
|
UserID: userID,
|
||||||
|
PeerID: peerID,
|
||||||
|
Topic: topic,
|
||||||
|
Relationship: types.RelationshipNone,
|
||||||
|
}
|
||||||
|
peer, err := tenantAdp.TenantUserGet(tenantID, peerID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ctx.Peer = peer
|
||||||
|
if block, err := tenantAdp.TenantUserBlockGet(tenantID, userID, peerID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if block != nil {
|
||||||
|
ctx.Relationship = types.RelationshipBlockedByMe
|
||||||
|
return ctx, nil
|
||||||
|
}
|
||||||
|
if block, err := tenantAdp.TenantUserBlockGet(tenantID, peerID, userID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if block != nil {
|
||||||
|
ctx.Relationship = types.RelationshipBlockedByPeer
|
||||||
|
return ctx, nil
|
||||||
|
}
|
||||||
|
if friendship, err := tenantAdp.TenantFriendshipGet(tenantID, userID, peerID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if friendship != nil && friendship.Status == types.RelationshipFriend {
|
||||||
|
ctx.Relationship = types.RelationshipFriend
|
||||||
|
ctx.CanSend = true
|
||||||
|
if ctx.Topic == "" {
|
||||||
|
ctx.Topic = friendship.Topic
|
||||||
|
}
|
||||||
|
return ctx, nil
|
||||||
|
}
|
||||||
|
req, err := tenantAdp.TenantFriendRequestLatestBetween(tenantID, userID, peerID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req != nil && req.Status == types.FriendRequestPending && req.ExpiresAt.After(time.Now()) {
|
||||||
|
ctx.RequestID = req.RequestID
|
||||||
|
ctx.PendingStatus = req.Status
|
||||||
|
if req.ApplicantID == userID {
|
||||||
|
ctx.Relationship = types.RelationshipPendingOutbound
|
||||||
|
} else {
|
||||||
|
ctx.Relationship = types.RelationshipPendingInbound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Save message
|
// Save message
|
||||||
func (messagesMapper) Save(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error) {
|
func (messagesMapper) Save(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error) {
|
||||||
if msg == nil || msg.TenantID != tenantID {
|
if msg == nil || msg.TenantID != tenantID {
|
||||||
@@ -1814,4 +1965,5 @@ func init() {
|
|||||||
PCache = pcacheMapper{}
|
PCache = pcacheMapper{}
|
||||||
Verifications = verificationMapper{}
|
Verifications = verificationMapper{}
|
||||||
Security = userSecurityMapper{}
|
Security = userSecurityMapper{}
|
||||||
|
Relations = relationsMapper{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const (
|
||||||
|
FriendRequestPending = "pending"
|
||||||
|
FriendRequestAccepted = "accepted"
|
||||||
|
FriendRequestRejected = "rejected"
|
||||||
|
FriendRequestExpired = "expired"
|
||||||
|
|
||||||
|
RelationshipNone = "none"
|
||||||
|
RelationshipFriend = "friend"
|
||||||
|
RelationshipPendingInbound = "pending_inbound"
|
||||||
|
RelationshipPendingOutbound = "pending_outbound"
|
||||||
|
RelationshipBlockedByMe = "blocked_by_me"
|
||||||
|
RelationshipBlockedByPeer = "blocked_by_peer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FriendRequest struct {
|
||||||
|
ID int64
|
||||||
|
TenantID TenantID `db:"tenant_id" json:"-" bson:"tenant_id"`
|
||||||
|
RequestID string `db:"request_id"`
|
||||||
|
ApplicantID Uid `db:"applicant_id"`
|
||||||
|
TargetID Uid `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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Friendship struct {
|
||||||
|
TenantID TenantID `db:"tenant_id" json:"-" bson:"tenant_id"`
|
||||||
|
UserID Uid `db:"user_id"`
|
||||||
|
PeerID Uid `db:"peer_id"`
|
||||||
|
Status string
|
||||||
|
Remark string
|
||||||
|
Topic string
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserBlock struct {
|
||||||
|
TenantID TenantID `db:"tenant_id" json:"-" bson:"tenant_id"`
|
||||||
|
BlockerID Uid `db:"blocker_id"`
|
||||||
|
BlockedID Uid `db:"blocked_id"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RelationshipContext struct {
|
||||||
|
TenantID TenantID `json:"-"`
|
||||||
|
UserID Uid `json:"-"`
|
||||||
|
PeerID Uid `json:"-"`
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
Relationship string `json:"relationship"`
|
||||||
|
CanSend bool `json:"can_send"`
|
||||||
|
RequestID string `json:"request_id,omitempty"`
|
||||||
|
PendingStatus string `json:"pending_status,omitempty"`
|
||||||
|
Peer *User `json:"peer,omitempty"`
|
||||||
|
}
|
||||||
@@ -1013,6 +1013,16 @@ func (t *Topic) sendSubNotifications(asUid types.Uid, sid, userAgent string) {
|
|||||||
// in response to a client request (msg, asUid) and broadcasts it to the attached sessions.
|
// in response to a client request (msg, asUid) and broadcasts it to the attached sessions.
|
||||||
func (t *Topic) saveAndBroadcastMessage(msg *ClientComMessage, asUid types.Uid, noEcho bool, attachments []string, head map[string]any, content any) error {
|
func (t *Topic) saveAndBroadcastMessage(msg *ClientComMessage, asUid types.Uid, noEcho bool, attachments []string, head map[string]any, content any) error {
|
||||||
pud, userFound := t.perUser[asUid]
|
pud, userFound := t.perUser[asUid]
|
||||||
|
if !userFound && msg.sess != nil {
|
||||||
|
asUid = msg.sess.uid
|
||||||
|
pud, userFound = t.perUser[asUid]
|
||||||
|
}
|
||||||
|
if !userFound && msg.sess != nil {
|
||||||
|
if psd, ok := t.sessions[msg.sess]; ok {
|
||||||
|
asUid = psd.uid
|
||||||
|
pud, userFound = t.perUser[asUid]
|
||||||
|
}
|
||||||
|
}
|
||||||
if t.cat == types.TopicCatSys {
|
if t.cat == types.TopicCatSys {
|
||||||
if msg.sess != nil && msg.sess.authLvl != auth.LevelRoot {
|
if msg.sess != nil && msg.sess.authLvl != auth.LevelRoot {
|
||||||
msg.sess.queueOut(ErrPermissionDenied(msg.Id, t.original(asUid), msg.Timestamp))
|
msg.sess.queueOut(ErrPermissionDenied(msg.Id, t.original(asUid), msg.Timestamp))
|
||||||
@@ -1042,6 +1052,22 @@ func (t *Topic) saveAndBroadcastMessage(msg *ClientComMessage, asUid types.Uid,
|
|||||||
return types.ErrPermissionDenied
|
return types.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if t.cat == types.TopicCatP2P && msg.sess != nil && msg.sess.authLvl != auth.LevelRoot &&
|
||||||
|
store.Relations != nil && store.Store != nil && store.Store.IsOpen() {
|
||||||
|
peer := t.p2pOtherUser(asUid)
|
||||||
|
if !peer.IsZero() {
|
||||||
|
canSend, _, err := store.Relations.CanSendP2P(t.tenantID, asUid, peer)
|
||||||
|
if err != nil {
|
||||||
|
logs.Warn.Printf("topic[%s]: failed to check p2p relationship: %v", t.name, err)
|
||||||
|
msg.sess.queueOut(ErrUnknown(msg.Id, t.original(asUid), msg.Timestamp))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !canSend {
|
||||||
|
msg.sess.queueOut(ErrPermissionDenied(msg.Id, t.original(asUid), msg.Timestamp))
|
||||||
|
return types.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveResult, err := store.Messages.Save(t.tenantID,
|
saveResult, err := store.Messages.Save(t.tenantID,
|
||||||
&types.Message{
|
&types.Message{
|
||||||
@@ -1124,6 +1150,9 @@ func (t *Topic) saveAndBroadcastMessage(msg *ClientComMessage, asUid types.Uid,
|
|||||||
// This is a NON-proxy broadcast.
|
// This is a NON-proxy broadcast.
|
||||||
func (t *Topic) handlePubBroadcast(msg *ClientComMessage) {
|
func (t *Topic) handlePubBroadcast(msg *ClientComMessage) {
|
||||||
asUid := types.ParseUserId(msg.AsUser)
|
asUid := types.ParseUserId(msg.AsUser)
|
||||||
|
if asUid.IsZero() && msg.sess != nil {
|
||||||
|
asUid = msg.sess.uid
|
||||||
|
}
|
||||||
if t.isInactive() {
|
if t.isInactive() {
|
||||||
// Ignore broadcast - topic is paused or being deleted.
|
// Ignore broadcast - topic is paused or being deleted.
|
||||||
msg.sess.queueOut(ErrLocked(msg.Id, t.original(asUid), msg.Timestamp))
|
msg.sess.queueOut(ErrLocked(msg.Id, t.original(asUid), msg.Timestamp))
|
||||||
|
|||||||
+65
-10
@@ -49,6 +49,13 @@ type TopicTestHelper struct {
|
|||||||
uu *mock_store.MockUsersPersistenceInterface
|
uu *mock_store.MockUsersPersistenceInterface
|
||||||
tt *mock_store.MockTopicsPersistenceInterface
|
tt *mock_store.MockTopicsPersistenceInterface
|
||||||
ss *mock_store.MockSubsPersistenceInterface
|
ss *mock_store.MockSubsPersistenceInterface
|
||||||
|
|
||||||
|
oldMessages store.MessagesPersistenceInterface
|
||||||
|
oldUsers store.UsersPersistenceInterface
|
||||||
|
oldTopics store.TopicsPersistenceInterface
|
||||||
|
oldSubs store.SubsPersistenceInterface
|
||||||
|
oldRelations store.RelationsPersistenceInterface
|
||||||
|
oldModeCP2P types.AccessMode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *TopicTestHelper) finish() {
|
func (b *TopicTestHelper) finish() {
|
||||||
@@ -67,12 +74,14 @@ func (b *TopicTestHelper) finish() {
|
|||||||
|
|
||||||
func (b *TopicTestHelper) newSession(sid string, uid types.Uid) (*Session, *responses) {
|
func (b *TopicTestHelper) newSession(sid string, uid types.Uid) (*Session, *responses) {
|
||||||
s := &Session{
|
s := &Session{
|
||||||
tenantID: 1,
|
tenantID: 1,
|
||||||
sid: sid,
|
tenantCode: "test",
|
||||||
uid: uid,
|
sid: sid,
|
||||||
subs: make(map[string]*Subscription),
|
uid: uid,
|
||||||
send: make(chan any, 10),
|
authLvl: auth.LevelAuth,
|
||||||
detach: make(chan string, 10),
|
subs: make(map[string]*Subscription),
|
||||||
|
send: make(chan any, 10),
|
||||||
|
detach: make(chan string, 10),
|
||||||
}
|
}
|
||||||
r := &responses{}
|
r := &responses{}
|
||||||
b.sessWg.Add(1)
|
b.sessWg.Add(1)
|
||||||
@@ -95,10 +104,18 @@ func (b *TopicTestHelper) setUp(t *testing.T, numUsers int, cat types.TopicCat,
|
|||||||
b.uu = mock_store.NewMockUsersPersistenceInterface(b.ctrl)
|
b.uu = mock_store.NewMockUsersPersistenceInterface(b.ctrl)
|
||||||
b.tt = mock_store.NewMockTopicsPersistenceInterface(b.ctrl)
|
b.tt = mock_store.NewMockTopicsPersistenceInterface(b.ctrl)
|
||||||
b.ss = mock_store.NewMockSubsPersistenceInterface(b.ctrl)
|
b.ss = mock_store.NewMockSubsPersistenceInterface(b.ctrl)
|
||||||
|
b.oldMessages = store.Messages
|
||||||
|
b.oldUsers = store.Users
|
||||||
|
b.oldTopics = store.Topics
|
||||||
|
b.oldSubs = store.Subs
|
||||||
store.Messages = b.mm
|
store.Messages = b.mm
|
||||||
store.Users = b.uu
|
store.Users = b.uu
|
||||||
store.Topics = b.tt
|
store.Topics = b.tt
|
||||||
store.Subs = b.ss
|
store.Subs = b.ss
|
||||||
|
b.oldRelations = store.Relations
|
||||||
|
store.Relations = nil
|
||||||
|
b.oldModeCP2P = globals.typesModeCP2P
|
||||||
|
globals.typesModeCP2P = types.ModeCP2P
|
||||||
// Sessions.
|
// Sessions.
|
||||||
b.sessions = make([]*Session, b.numUsers)
|
b.sessions = make([]*Session, b.numUsers)
|
||||||
b.results = make([]*responses, b.numUsers)
|
b.results = make([]*responses, b.numUsers)
|
||||||
@@ -162,10 +179,12 @@ func (b *TopicTestHelper) setUp(t *testing.T, numUsers int, cat types.TopicCat,
|
|||||||
|
|
||||||
func (b *TopicTestHelper) tearDown() {
|
func (b *TopicTestHelper) tearDown() {
|
||||||
globals.hub = nil
|
globals.hub = nil
|
||||||
store.Messages = nil
|
store.Messages = b.oldMessages
|
||||||
store.Users = nil
|
store.Users = b.oldUsers
|
||||||
store.Topics = nil
|
store.Topics = b.oldTopics
|
||||||
store.Subs = nil
|
store.Subs = b.oldSubs
|
||||||
|
store.Relations = b.oldRelations
|
||||||
|
globals.typesModeCP2P = b.oldModeCP2P
|
||||||
b.ctrl.Finish()
|
b.ctrl.Finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,6 +298,42 @@ func TestHandleBroadcastDataP2P(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleBroadcastDataP2PRequiresRelationship(t *testing.T) {
|
||||||
|
numUsers := 2
|
||||||
|
helper := TopicTestHelper{}
|
||||||
|
helper.setUp(t, numUsers, types.TopicCatP2P, "p2p-test" /*attach=*/, true)
|
||||||
|
defer helper.tearDown()
|
||||||
|
|
||||||
|
oldStore := store.Store
|
||||||
|
mockStore := mock_store.NewMockPersistentStorageInterface(helper.ctrl)
|
||||||
|
mockStore.EXPECT().IsOpen().Return(true)
|
||||||
|
store.Store = mockStore
|
||||||
|
canSend := false
|
||||||
|
store.Relations = &testRelationsStore{canSend: &canSend, reason: types.RelationshipNone}
|
||||||
|
defer func() {
|
||||||
|
store.Store = oldStore
|
||||||
|
}()
|
||||||
|
|
||||||
|
from := helper.uids[0].UserId()
|
||||||
|
msg := &ClientComMessage{
|
||||||
|
AsUser: from,
|
||||||
|
Original: from,
|
||||||
|
Pub: &MsgClientPub{
|
||||||
|
Topic: "p2p",
|
||||||
|
Content: "test",
|
||||||
|
NoEcho: true,
|
||||||
|
},
|
||||||
|
sess: helper.sessions[0],
|
||||||
|
}
|
||||||
|
helper.topic.handleClientMsg(msg)
|
||||||
|
helper.finish()
|
||||||
|
|
||||||
|
verifyResponseCodes(helper.results[0], []int{http.StatusForbidden}, t)
|
||||||
|
if len(helper.results[1].messages) != 0 {
|
||||||
|
t.Fatalf("peer: expected 0 messages, got %d", len(helper.results[1].messages))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleBroadcastCall(t *testing.T) {
|
func TestHandleBroadcastCall(t *testing.T) {
|
||||||
numUsers := 2
|
numUsers := 2
|
||||||
helper := TopicTestHelper{}
|
helper := TopicTestHelper{}
|
||||||
|
|||||||
Reference in New Issue
Block a user