feat: add im tenant db routing and chapter seven flows
This commit is contained in:
@@ -1549,6 +1549,69 @@ func (a *adapter) TenantMessageGetAll(id t.TenantID, topic string, uid t.Uid, op
|
||||
return msgs, rows.Err()
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageSearch(id t.TenantID, topic string, uid t.Uid, queryText, messageType, cursor string, limit int) ([]t.Message, string, error) {
|
||||
if err := validTenant(id); err != nil || topic == "" || uid.IsZero() {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
where := []string{"m.tenant_id=?", "m.topic=?", "m.delid=0"}
|
||||
args := []any{id, topic}
|
||||
if cursor != "" {
|
||||
parsed, err := strconv.Atoi(cursor)
|
||||
if err != nil || parsed <= 0 {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
where = append(where, "m.seqid<?")
|
||||
args = append(args, parsed)
|
||||
}
|
||||
queryText = strings.TrimSpace(queryText)
|
||||
if queryText != "" {
|
||||
where = append(where, "CAST(m.content AS CHAR) LIKE ?")
|
||||
args = append(args, "%"+queryText+"%")
|
||||
}
|
||||
if messageType != "" && messageType != "all" {
|
||||
where = append(where, "(JSON_UNQUOTE(JSON_EXTRACT(m.head,'$.type'))=? OR JSON_UNQUOTE(JSON_EXTRACT(m.head,'$.x-im-type'))=?)")
|
||||
args = append(args, messageType, messageType)
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
queryArgs := append([]any{decodeUID(uid)}, args...)
|
||||
queryArgs = append(queryArgs, limit+1)
|
||||
rows, err := a.db.QueryxContext(ctx,
|
||||
"SELECT m.tenant_id,m.createdat,m.updatedat,m.deletedat,m.delid,m.seqid,m.topic,m.`from`,m.client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content "+
|
||||
"FROM messages AS m LEFT JOIN dellog AS d ON d.tenant_id=m.tenant_id AND d.topic=m.topic "+
|
||||
"AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? WHERE "+strings.Join(where, " AND ")+" AND d.deletedfor IS NULL ORDER BY m.seqid DESC LIMIT ?",
|
||||
queryArgs...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer rows.Close()
|
||||
msgs := make([]t.Message, 0, limit)
|
||||
nextCursor := ""
|
||||
for rows.Next() {
|
||||
var msg t.Message
|
||||
if err = rows.StructScan(&msg); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
msg.From = common.EncodeUidString(msg.From).String()
|
||||
msg.Head = kvMapFromJSON(msg.Head)
|
||||
msg.Content = common.FromJSON(msg.Content)
|
||||
if len(msgs) == limit {
|
||||
nextCursor = strconv.Itoa(msg.SeqId)
|
||||
continue
|
||||
}
|
||||
msgs = append(msgs, msg)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return msgs, nextCursor, nil
|
||||
}
|
||||
|
||||
func tenantMessageGetOne(q sqlx.QueryerContext, ctx context.Context, tenantID t.TenantID, topic string, seq int, suffix string) (*t.Message, error) {
|
||||
row := struct {
|
||||
t.Message
|
||||
@@ -1577,6 +1640,146 @@ func messageVersion(version int) int {
|
||||
return version
|
||||
}
|
||||
|
||||
func kvMapFromJSON(value any) t.KVMap {
|
||||
switch parsed := common.FromJSON(value).(type) {
|
||||
case map[string]any:
|
||||
return t.KVMap(parsed)
|
||||
case t.KVMap:
|
||||
return parsed
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFavoriteCreate(id t.TenantID, fav *t.FavoriteItem) (*t.FavoriteItem, error) {
|
||||
if err := validTenant(id); err != nil || fav == nil || fav.TenantID != id || fav.UserID.IsZero() || fav.FavoriteID == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err := a.db.ExecContext(ctx,
|
||||
`INSERT INTO im_favorites(tenant_id,favorite_id,user_id,item_type,source_topic,source_seq,snapshot,attachment_refs,availability,idempotency_key,collected_at,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE updated_at=updated_at`,
|
||||
id, fav.FavoriteID, decodeUID(fav.UserID), fav.ItemType, fav.SourceTopic, fav.SourceSeq, common.ToJSON(fav.Snapshot), common.ToJSON(fav.AttachmentRefs), fav.Availability, fav.IdempotencyKey, fav.CollectedAt, fav.CreatedAt, fav.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.TenantFavoriteGet(id, fav.UserID, fav.FavoriteID)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFavoriteList(id t.TenantID, uid t.Uid, itemType, queryText, cursor string, limit int) ([]t.FavoriteItem, string, error) {
|
||||
if err := validTenant(id); err != nil || uid.IsZero() {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
where := []string{"tenant_id=?", "user_id=?"}
|
||||
args := []any{id, decodeUID(uid)}
|
||||
if cursor != "" {
|
||||
parsed, err := strconv.ParseInt(cursor, 10, 64)
|
||||
if err != nil || parsed <= 0 {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
where = append(where, "id<?")
|
||||
args = append(args, parsed)
|
||||
}
|
||||
if itemType != "" && itemType != "all" {
|
||||
where = append(where, "item_type=?")
|
||||
args = append(args, itemType)
|
||||
}
|
||||
if queryText = strings.TrimSpace(queryText); queryText != "" {
|
||||
where = append(where, "CAST(snapshot AS CHAR) LIKE ?")
|
||||
args = append(args, "%"+queryText+"%")
|
||||
}
|
||||
args = append(args, limit+1)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, "SELECT * FROM im_favorites WHERE "+strings.Join(where, " AND ")+" ORDER BY id DESC LIMIT ?", args...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]t.FavoriteItem, 0, limit)
|
||||
nextCursor := ""
|
||||
for rows.Next() {
|
||||
item, err := scanFavorite(rows)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if len(items) == limit {
|
||||
nextCursor = strconv.FormatInt(item.ID, 10)
|
||||
continue
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return items, nextCursor, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFavoriteGet(id t.TenantID, uid t.Uid, favoriteID string) (*t.FavoriteItem, error) {
|
||||
if err := validTenant(id); err != nil || uid.IsZero() || favoriteID == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, "SELECT * FROM im_favorites WHERE tenant_id=? AND user_id=? AND favorite_id=?", id, decodeUID(uid), favoriteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
item, err := scanFavorite(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFavoriteDelete(id t.TenantID, uid t.Uid, favoriteID string) error {
|
||||
if err := validTenant(id); err != nil || uid.IsZero() || favoriteID == "" {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
res, err := a.db.ExecContext(ctx, "DELETE FROM im_favorites WHERE tenant_id=? AND user_id=? AND favorite_id=?", id, decodeUID(uid), favoriteID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected, err := res.RowsAffected(); err == nil && affected == 0 {
|
||||
return t.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanFavorite(rows *sqlx.Rows) (t.FavoriteItem, error) {
|
||||
row := struct {
|
||||
t.FavoriteItem
|
||||
UserID int64 `db:"user_id"`
|
||||
}{}
|
||||
if err := rows.StructScan(&row); err != nil {
|
||||
return t.FavoriteItem{}, err
|
||||
}
|
||||
item := row.FavoriteItem
|
||||
item.UserID = store.EncodeUid(row.UserID)
|
||||
item.Snapshot = common.FromJSON(item.Snapshot)
|
||||
item.AttachmentRefs = common.FromJSON(item.AttachmentRefs)
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageDeleteList(id t.TenantID, topic string, del *t.DelMessage) error {
|
||||
if err := validTenant(id); err != nil || topic == "" || (del != nil && del.TenantID != id) {
|
||||
return t.ErrMalformed
|
||||
@@ -1954,6 +2157,57 @@ func (a *adapter) TenantFileLinkAttachments(id t.TenantID, topic string, userID,
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFileUserHasAccess(id t.TenantID, fid string, uid t.Uid) (bool, error) {
|
||||
if err := validTenant(id); err != nil || uid.IsZero() {
|
||||
return false, t.ErrMalformed
|
||||
}
|
||||
fileID := t.ParseUid(fid)
|
||||
if fileID.IsZero() {
|
||||
return false, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
var allowed int
|
||||
err := a.db.GetContext(ctx, &allowed, `
|
||||
SELECT COUNT(*) FROM fileuploads AS fu
|
||||
WHERE fu.tenant_id=? AND fu.id=? AND fu.status=? AND (
|
||||
fu.userid=?
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM filemsglinks AS fml
|
||||
WHERE fml.tenant_id=fu.tenant_id AND fml.fileid=fu.id AND fml.userid=?
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM filemsglinks AS fml
|
||||
INNER JOIN subscriptions AS s
|
||||
ON s.tenant_id=fml.tenant_id AND s.topic=fml.topic AND s.userid=?
|
||||
WHERE fml.tenant_id=fu.tenant_id AND fml.fileid=fu.id AND fml.topic IS NOT NULL
|
||||
AND s.deletedat IS NULL AND INSTR(s.modewant,'R')>0 AND INSTR(s.modegiven,'R')>0
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM filemsglinks AS fml
|
||||
INNER JOIN messages AS m
|
||||
ON m.tenant_id=fml.tenant_id AND m.id=fml.msgid
|
||||
INNER JOIN subscriptions AS s
|
||||
ON s.tenant_id=m.tenant_id AND s.topic=m.topic AND s.userid=?
|
||||
WHERE fml.tenant_id=fu.tenant_id AND fml.fileid=fu.id AND fml.msgid IS NOT NULL
|
||||
AND m.deletedat IS NULL AND m.recalled_at IS NULL
|
||||
AND s.deletedat IS NULL AND INSTR(s.modewant,'R')>0 AND INSTR(s.modegiven,'R')>0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM dellog AS d
|
||||
WHERE d.tenant_id=m.tenant_id AND d.topic=m.topic AND d.deletedfor=?
|
||||
AND d.low<=m.seqid AND (d.hi=0 OR d.hi>m.seqid)
|
||||
)
|
||||
)
|
||||
) LIMIT 1`,
|
||||
id, decodeUID(fileID), t.UploadCompleted, decodeUID(uid), decodeUID(uid), decodeUID(uid), decodeUID(uid), decodeUID(uid))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return allowed > 0, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantPCacheGet(id t.TenantID, key string) (string, error) {
|
||||
if err := validTenant(id); err != nil {
|
||||
return "", err
|
||||
@@ -2998,6 +3252,26 @@ func (a *adapter) TenantFriendshipGet(id t.TenantID, userID, peerID t.Uid) (*t.F
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFriendshipUpdateRemark(id t.TenantID, userID, peerID t.Uid, remark string, now time.Time) (*t.Friendship, error) {
|
||||
if err := validTenant(id); err != nil || userID.IsZero() || peerID.IsZero() || len([]rune(remark)) > 64 {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
res, err := a.db.ExecContext(ctx,
|
||||
"UPDATE im_friendships SET remark=?,updated_at=? WHERE tenant_id=? AND user_id=? AND peer_id=? AND status=?",
|
||||
remark, now, id, decodeUID(userID), decodeUID(peerID), t.RelationshipFriend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if affected, err := res.RowsAffected(); err == nil && affected == 0 {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
return a.TenantFriendshipGet(id, userID, peerID)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -3044,6 +3318,79 @@ func (a *adapter) TenantUserBlockGet(id t.TenantID, blockerID, blockedID t.Uid)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantUserBlockList(id t.TenantID, blockerID t.Uid, cursor string, limit int) ([]t.BlockListItem, string, error) {
|
||||
if err := validTenant(id); err != nil || blockerID.IsZero() {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
where := []string{"b.tenant_id=?", "b.blocker_id=?"}
|
||||
args := []any{id, decodeUID(blockerID)}
|
||||
if cursor != "" {
|
||||
parsed, err := strconv.ParseInt(cursor, 10, 64)
|
||||
if err != nil || parsed <= 0 {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
where = append(where, "b.blocked_id<?")
|
||||
args = append(args, parsed)
|
||||
}
|
||||
args = append(args, limit+1)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, `SELECT b.*,u.public,u.trusted
|
||||
FROM im_user_blocks AS b
|
||||
LEFT JOIN users AS u ON u.tenant_id=b.tenant_id AND u.id=b.blocked_id
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY b.blocked_id DESC LIMIT ?`, args...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]t.BlockListItem, 0, limit)
|
||||
nextCursor := ""
|
||||
for rows.Next() {
|
||||
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"`
|
||||
Public any
|
||||
Trusted any
|
||||
}{}
|
||||
if err = rows.StructScan(&row); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if len(items) == limit {
|
||||
nextCursor = strconv.FormatInt(row.BlockedID, 10)
|
||||
continue
|
||||
}
|
||||
blockedID := store.EncodeUid(row.BlockedID)
|
||||
items = append(items, t.BlockListItem{
|
||||
Block: t.UserBlock{
|
||||
TenantID: row.TenantID,
|
||||
BlockerID: store.EncodeUid(row.BlockerID),
|
||||
BlockedID: blockedID,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
},
|
||||
User: &t.User{
|
||||
ObjHeader: t.ObjHeader{Id: blockedID.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) 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
|
||||
@@ -3075,6 +3422,85 @@ func (a *adapter) TenantUserBlockDelete(id t.TenantID, blockerID, blockedID t.Ui
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantUserSearch(id t.TenantID, viewer t.Uid, queryText, searchType, cursor string, limit int) ([]t.User, string, error) {
|
||||
if err := validTenant(id); err != nil || viewer.IsZero() {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
queryText = strings.TrimSpace(queryText)
|
||||
if queryText == "" {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 50 {
|
||||
limit = 20
|
||||
}
|
||||
where := []string{"u.tenant_id=?", "u.id<>?", "u.state=?"}
|
||||
args := []any{id, decodeUID(viewer), t.StateOK}
|
||||
if cursor != "" {
|
||||
parsed, err := strconv.ParseInt(cursor, 10, 64)
|
||||
if err != nil || parsed <= 0 {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
where = append(where, "u.id>?")
|
||||
args = append(args, parsed)
|
||||
}
|
||||
join := ""
|
||||
switch searchType {
|
||||
case "", "account":
|
||||
like := "%" + queryText + "%"
|
||||
join = "LEFT JOIN usertags AS ut ON ut.tenant_id=u.tenant_id AND ut.userid=u.id"
|
||||
where = append(where, "(ut.tag LIKE ? OR CAST(u.public AS CHAR) LIKE ? OR u.id=?)")
|
||||
args = append(args, like, like, userIDNumericArg(queryText))
|
||||
case "exact_account":
|
||||
join = "LEFT JOIN usertags AS ut ON ut.tenant_id=u.tenant_id AND ut.userid=u.id"
|
||||
where = append(where, "(ut.tag=? OR u.id=?)")
|
||||
args = append(args, queryText, userIDNumericArg(queryText))
|
||||
case "phone":
|
||||
where = append(where, `EXISTS (
|
||||
SELECT 1 FROM credentials AS c
|
||||
WHERE c.tenant_id=u.tenant_id AND c.userid=u.id AND c.method='tel' AND c.value=? AND c.done=true
|
||||
)`)
|
||||
args = append(args, queryText)
|
||||
default:
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
args = append(args, limit+1)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, `SELECT DISTINCT u.* FROM users AS u `+join+`
|
||||
WHERE `+strings.Join(where, " AND ")+` ORDER BY u.id ASC LIMIT ?`, args...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer rows.Close()
|
||||
users := make([]t.User, 0, limit)
|
||||
nextCursor := ""
|
||||
for rows.Next() {
|
||||
var user t.User
|
||||
if err = rows.StructScan(&user); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
scanTenantUser(&user)
|
||||
if len(users) == limit {
|
||||
nextCursor = strconv.FormatInt(decodeUID(user.Uid()), 10)
|
||||
continue
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return users, nextCursor, nil
|
||||
}
|
||||
|
||||
func userIDNumericArg(value string) int64 {
|
||||
if uid := t.ParseUserId(value); !uid.IsZero() {
|
||||
return decodeUID(uid)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user