722 lines
23 KiB
Go
722 lines
23 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"hash/fnv"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/store/postgres/sqlcgen"
|
|
)
|
|
|
|
// DialogStore 用 PostgreSQL 实现 store.DialogStore。
|
|
type DialogStore struct {
|
|
q *sqlcgen.Queries
|
|
}
|
|
|
|
// NewDialogStore 基于 pgx 连接池(或事务)创建 DialogStore。
|
|
func NewDialogStore(db sqlcgen.DBTX) *DialogStore {
|
|
return &DialogStore{q: sqlcgen.New(db)}
|
|
}
|
|
|
|
func (s *DialogStore) ListByUser(ctx context.Context, userID int64, filter domain.DialogFilter) (domain.DialogList, error) {
|
|
limit := filter.Limit
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
if limit > 500 {
|
|
limit = 500
|
|
}
|
|
offsetPeerID := int64(0)
|
|
if filter.HasOffsetPeer {
|
|
offsetPeerID = filter.OffsetPeer.ID
|
|
}
|
|
folderParams := dialogFolderQueryParams(filter.Folder)
|
|
summaryRows, err := s.q.ListDialogSummaryByUser(ctx, sqlcgen.ListDialogSummaryByUserParams{
|
|
UserID: userID,
|
|
HasFolderID: filter.HasFolderID,
|
|
FolderID: pgInt32NonNegative(filter.FolderID),
|
|
FolderExcludeArchived: folderParams.excludeArchived,
|
|
FolderExcludeRead: folderParams.excludeRead,
|
|
FolderExcludePeerTypes: folderParams.excludeTypes,
|
|
FolderExcludePeerIds: folderParams.excludeIDs,
|
|
FolderIncludePeerTypes: folderParams.includeTypes,
|
|
FolderIncludePeerIds: folderParams.includeIDs,
|
|
FolderPinnedPeerTypes: folderParams.pinnedTypes,
|
|
FolderPinnedPeerIds: folderParams.pinnedIDs,
|
|
FolderContacts: folderParams.contacts,
|
|
FolderNonContacts: folderParams.nonContacts,
|
|
PinnedOnly: filter.PinnedOnly,
|
|
ExcludePinned: filter.ExcludePinned,
|
|
})
|
|
if err != nil {
|
|
return domain.DialogList{}, fmt.Errorf("list dialog summary: %w", err)
|
|
}
|
|
summary := make([]domain.Dialog, 0, len(summaryRows))
|
|
for _, row := range summaryRows {
|
|
summary = append(summary, domain.Dialog{
|
|
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
|
|
FolderID: int(row.FolderID),
|
|
TopMessage: int(row.TopMessageID),
|
|
TopMessageDate: int(row.TopMessageDate),
|
|
ReadInboxMaxID: int(row.ReadInboxMaxID),
|
|
ReadOutboxMaxID: int(row.ReadOutboxMaxID),
|
|
UnreadCount: int(row.UnreadCount),
|
|
UnreadMentions: int(row.UnreadMentionsCount),
|
|
UnreadReactions: int(row.UnreadReactionsCount),
|
|
Pinned: row.Pinned,
|
|
PinnedOrder: int(row.PinnedOrder),
|
|
UnreadMark: row.UnreadMark,
|
|
PeerSettingsBarHidden: row.HiddenPeerSettingsBar,
|
|
})
|
|
}
|
|
out := domain.DialogList{
|
|
Dialogs: make([]domain.Dialog, 0, limit),
|
|
Count: len(summary),
|
|
Hash: dialogListHash(summary),
|
|
}
|
|
if len(summary) == 0 {
|
|
return out, nil
|
|
}
|
|
rows, err := s.q.ListDialogsByUser(ctx, sqlcgen.ListDialogsByUserParams{
|
|
UserID: userID,
|
|
LimitCount: int32(limit),
|
|
HasFolderID: filter.HasFolderID,
|
|
FolderID: pgInt32NonNegative(filter.FolderID),
|
|
FolderExcludeArchived: folderParams.excludeArchived,
|
|
FolderExcludeRead: folderParams.excludeRead,
|
|
FolderExcludePeerTypes: folderParams.excludeTypes,
|
|
FolderExcludePeerIds: folderParams.excludeIDs,
|
|
FolderIncludePeerTypes: folderParams.includeTypes,
|
|
FolderIncludePeerIds: folderParams.includeIDs,
|
|
FolderPinnedPeerTypes: folderParams.pinnedTypes,
|
|
FolderPinnedPeerIds: folderParams.pinnedIDs,
|
|
FolderContacts: folderParams.contacts,
|
|
FolderNonContacts: folderParams.nonContacts,
|
|
PinnedOnly: filter.PinnedOnly,
|
|
ExcludePinned: filter.ExcludePinned,
|
|
OffsetID: pgInt32NonNegative(filter.OffsetID),
|
|
OffsetDate: pgInt32NonNegative(filter.OffsetDate),
|
|
HasOffsetPeer: filter.HasOffsetPeer,
|
|
OffsetPeerID: offsetPeerID,
|
|
})
|
|
if err != nil {
|
|
return domain.DialogList{}, fmt.Errorf("list dialogs: %w", err)
|
|
}
|
|
out.Messages = make([]domain.Message, 0, len(rows))
|
|
out.Users = make([]domain.User, 0, len(rows))
|
|
seenUsers := map[int64]struct{}{}
|
|
for _, row := range rows {
|
|
dialog := domain.Dialog{
|
|
Peer: domain.Peer{
|
|
Type: domain.PeerType(row.PeerType),
|
|
ID: row.PeerID,
|
|
},
|
|
FolderID: int(row.FolderID),
|
|
TopMessage: int(row.TopMessageID),
|
|
TopMessageDate: int(row.TopMessageDate),
|
|
ReadInboxMaxID: int(row.ReadInboxMaxID),
|
|
ReadOutboxMaxID: int(row.ReadOutboxMaxID),
|
|
UnreadCount: int(row.UnreadCount),
|
|
UnreadMentions: int(row.UnreadMentionsCount),
|
|
UnreadReactions: int(row.UnreadReactionsCount),
|
|
Pinned: row.Pinned,
|
|
PinnedOrder: int(row.PinnedOrder),
|
|
UnreadMark: row.UnreadMark,
|
|
PeerSettingsBarHidden: row.HiddenPeerSettingsBar,
|
|
}
|
|
out.Dialogs = append(out.Dialogs, dialog)
|
|
if row.PeerUserID != 0 {
|
|
if _, ok := seenUsers[row.PeerUserID]; !ok {
|
|
seenUsers[row.PeerUserID] = struct{}{}
|
|
out.Users = append(out.Users, domain.User{
|
|
ID: row.PeerUserID,
|
|
AccessHash: row.PeerAccessHash,
|
|
Phone: row.PeerPhone,
|
|
FirstName: row.PeerFirstName,
|
|
LastName: row.PeerLastName,
|
|
Username: row.PeerUsername,
|
|
CountryCode: row.PeerCountryCode,
|
|
Verified: row.PeerVerified,
|
|
Support: row.PeerSupport,
|
|
LastSeenAt: int(row.PeerLastSeenAt),
|
|
Contact: row.PeerContact,
|
|
Mutual: row.PeerMutual,
|
|
})
|
|
}
|
|
}
|
|
if row.MessageID != 0 {
|
|
entities, err := decodeMessageEntities(row.MessageEntitiesJson)
|
|
if err != nil {
|
|
return domain.DialogList{}, fmt.Errorf("decode message entities: %w", err)
|
|
}
|
|
out.Messages = append(out.Messages, domain.Message{
|
|
ID: int(row.MessageID),
|
|
OwnerUserID: row.UserID,
|
|
Peer: dialog.Peer,
|
|
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.MessageFromUserID},
|
|
Date: int(row.MessageDate),
|
|
Out: row.MessageOutgoing,
|
|
Body: row.MessageBody,
|
|
Entities: entities,
|
|
})
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *DialogStore) ListByPeers(ctx context.Context, userID int64, peers []domain.Peer) (domain.DialogList, error) {
|
|
if len(peers) == 0 {
|
|
return domain.DialogList{}, nil
|
|
}
|
|
peerTypes := make([]string, 0, len(peers))
|
|
peerIDs := make([]int64, 0, len(peers))
|
|
for _, peer := range peers {
|
|
if peer.Type == "" || peer.ID == 0 {
|
|
continue
|
|
}
|
|
peerTypes = append(peerTypes, string(peer.Type))
|
|
peerIDs = append(peerIDs, peer.ID)
|
|
}
|
|
if len(peerTypes) == 0 {
|
|
return domain.DialogList{}, nil
|
|
}
|
|
rows, err := s.q.ListDialogsByPeers(ctx, sqlcgen.ListDialogsByPeersParams{
|
|
UserID: userID,
|
|
PeerTypes: peerTypes,
|
|
PeerIds: peerIDs,
|
|
})
|
|
if err != nil {
|
|
return domain.DialogList{}, fmt.Errorf("list dialogs by peers: %w", err)
|
|
}
|
|
out := domain.DialogList{
|
|
Dialogs: make([]domain.Dialog, 0, len(rows)),
|
|
Messages: make([]domain.Message, 0, len(rows)),
|
|
Users: make([]domain.User, 0, len(rows)),
|
|
}
|
|
seenUsers := map[int64]struct{}{}
|
|
for _, row := range rows {
|
|
dialog := domain.Dialog{
|
|
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
|
|
FolderID: int(row.FolderID),
|
|
TopMessage: int(row.TopMessageID),
|
|
TopMessageDate: int(row.TopMessageDate),
|
|
ReadInboxMaxID: int(row.ReadInboxMaxID),
|
|
ReadOutboxMaxID: int(row.ReadOutboxMaxID),
|
|
UnreadCount: int(row.UnreadCount),
|
|
UnreadMentions: int(row.UnreadMentionsCount),
|
|
UnreadReactions: int(row.UnreadReactionsCount),
|
|
Pinned: row.Pinned,
|
|
PinnedOrder: int(row.PinnedOrder),
|
|
UnreadMark: row.UnreadMark,
|
|
PeerSettingsBarHidden: row.HiddenPeerSettingsBar,
|
|
}
|
|
out.Dialogs = append(out.Dialogs, dialog)
|
|
if row.PeerUserID != 0 {
|
|
if _, ok := seenUsers[row.PeerUserID]; !ok {
|
|
seenUsers[row.PeerUserID] = struct{}{}
|
|
out.Users = append(out.Users, domain.User{
|
|
ID: row.PeerUserID,
|
|
AccessHash: row.PeerAccessHash,
|
|
Phone: row.PeerPhone,
|
|
FirstName: row.PeerFirstName,
|
|
LastName: row.PeerLastName,
|
|
Username: row.PeerUsername,
|
|
CountryCode: row.PeerCountryCode,
|
|
Verified: row.PeerVerified,
|
|
Support: row.PeerSupport,
|
|
LastSeenAt: int(row.PeerLastSeenAt),
|
|
Contact: row.PeerContact,
|
|
Mutual: row.PeerMutual,
|
|
})
|
|
}
|
|
}
|
|
if row.MessageID != 0 {
|
|
entities, err := decodeMessageEntities(row.MessageEntitiesJson)
|
|
if err != nil {
|
|
return domain.DialogList{}, fmt.Errorf("decode message entities: %w", err)
|
|
}
|
|
out.Messages = append(out.Messages, domain.Message{
|
|
ID: int(row.MessageID),
|
|
OwnerUserID: row.UserID,
|
|
Peer: dialog.Peer,
|
|
From: domain.Peer{Type: domain.PeerTypeUser, ID: row.MessageFromUserID},
|
|
Date: int(row.MessageDate),
|
|
Out: row.MessageOutgoing,
|
|
Body: row.MessageBody,
|
|
Entities: entities,
|
|
})
|
|
}
|
|
}
|
|
out.Count = len(out.Dialogs)
|
|
out.Hash = dialogListHash(out.Dialogs)
|
|
return out, nil
|
|
}
|
|
|
|
func (s *DialogStore) Upsert(ctx context.Context, userID int64, dialog domain.Dialog) error {
|
|
if err := s.q.UpsertDialog(ctx, sqlcgen.UpsertDialogParams{
|
|
UserID: userID,
|
|
PeerType: string(dialog.Peer.Type),
|
|
PeerID: dialog.Peer.ID,
|
|
TopMessageID: int32(dialog.TopMessage),
|
|
TopMessageDate: int32(dialog.TopMessageDate),
|
|
ReadInboxMaxID: int32(dialog.ReadInboxMaxID),
|
|
ReadOutboxMaxID: int32(dialog.ReadOutboxMaxID),
|
|
UnreadCount: int32(dialog.UnreadCount),
|
|
UnreadMentionsCount: int32(dialog.UnreadMentions),
|
|
UnreadReactionsCount: int32(dialog.UnreadReactions),
|
|
Pinned: dialog.Pinned,
|
|
UnreadMark: dialog.UnreadMark,
|
|
}); err != nil {
|
|
return fmt.Errorf("upsert dialog: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *DialogStore) SaveDraft(ctx context.Context, userID int64, draft domain.DialogDraft) error {
|
|
data, err := json.Marshal(draft)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal dialog draft: %w", err)
|
|
}
|
|
if err := s.q.UpsertDialogDraft(ctx, sqlcgen.UpsertDialogDraftParams{
|
|
UserID: userID,
|
|
PeerType: string(draft.Peer.Type),
|
|
PeerID: draft.Peer.ID,
|
|
TopMessageID: int32(draft.TopMessageID),
|
|
Date: int32(draft.Date),
|
|
DraftJson: data,
|
|
}); err != nil {
|
|
return fmt.Errorf("upsert dialog draft: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *DialogStore) DeleteDraft(ctx context.Context, userID int64, peer domain.Peer, topMessageID int) (bool, error) {
|
|
changed, err := s.q.DeleteDialogDraft(ctx, sqlcgen.DeleteDialogDraftParams{
|
|
UserID: userID,
|
|
PeerType: string(peer.Type),
|
|
PeerID: peer.ID,
|
|
TopMessageID: int32(topMessageID),
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return false, nil
|
|
}
|
|
return false, fmt.Errorf("delete dialog draft: %w", err)
|
|
}
|
|
return changed, nil
|
|
}
|
|
|
|
func (s *DialogStore) ListDrafts(ctx context.Context, userID int64, limit int) ([]domain.DialogDraft, error) {
|
|
rows, err := s.q.ListDialogDrafts(ctx, sqlcgen.ListDialogDraftsParams{
|
|
UserID: userID,
|
|
LimitCount: int32(clampDialogDraftLimit(limit)),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list dialog drafts: %w", err)
|
|
}
|
|
return decodeDialogDrafts(rows)
|
|
}
|
|
|
|
func (s *DialogStore) ClearDrafts(ctx context.Context, userID int64, limit int) ([]domain.DialogDraft, error) {
|
|
rows, err := s.q.ClearDialogDrafts(ctx, sqlcgen.ClearDialogDraftsParams{
|
|
UserID: userID,
|
|
LimitCount: int32(clampDialogDraftLimit(limit)),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("clear dialog drafts: %w", err)
|
|
}
|
|
return decodeDialogDrafts(rows)
|
|
}
|
|
|
|
func (s *DialogStore) MarkRead(ctx context.Context, userID int64, peer domain.Peer, maxID int) (domain.ReadHistoryResult, error) {
|
|
row, err := s.q.MarkDialogRead(ctx, sqlcgen.MarkDialogReadParams{
|
|
UserID: userID,
|
|
PeerType: string(peer.Type),
|
|
PeerID: peer.ID,
|
|
MaxID: pgInt32NonNegative(maxID),
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ReadHistoryResult{OwnerUserID: userID, Peer: peer, MaxID: maxID}, nil
|
|
}
|
|
return domain.ReadHistoryResult{}, fmt.Errorf("mark dialog read: %w", err)
|
|
}
|
|
return domain.ReadHistoryResult{
|
|
OwnerUserID: row.UserID,
|
|
Peer: domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID},
|
|
MaxID: int(row.ReadInboxMaxID),
|
|
StillUnreadCount: int(row.UnreadCount),
|
|
Changed: row.Changed,
|
|
}, nil
|
|
}
|
|
|
|
func (s *DialogStore) SetPinned(ctx context.Context, userID int64, peer domain.Peer, pinned bool) (bool, error) {
|
|
changed, err := s.q.SetDialogPinned(ctx, sqlcgen.SetDialogPinnedParams{
|
|
UserID: userID,
|
|
PeerType: string(peer.Type),
|
|
PeerID: peer.ID,
|
|
Pinned: pinned,
|
|
})
|
|
if err != nil {
|
|
return false, fmt.Errorf("set dialog pinned: %w", err)
|
|
}
|
|
return changed, nil
|
|
}
|
|
|
|
func (s *DialogStore) ReorderPinned(ctx context.Context, userID int64, order []domain.Peer, force bool) error {
|
|
peerTypes, peerIDs := peerArrays(order)
|
|
if force {
|
|
if err := s.q.ClearPinnedDialogsNotInOrder(ctx, sqlcgen.ClearPinnedDialogsNotInOrderParams{
|
|
UserID: userID,
|
|
PeerTypes: peerTypes,
|
|
PeerIds: peerIDs,
|
|
}); err != nil {
|
|
return fmt.Errorf("clear pinned dialogs not in order: %w", err)
|
|
}
|
|
}
|
|
if len(peerTypes) == 0 {
|
|
return nil
|
|
}
|
|
if err := s.q.ReorderPinnedDialogs(ctx, sqlcgen.ReorderPinnedDialogsParams{
|
|
UserID: userID,
|
|
PeerTypes: peerTypes,
|
|
PeerIds: peerIDs,
|
|
}); err != nil {
|
|
return fmt.Errorf("reorder pinned dialogs: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *DialogStore) SetUnreadMark(ctx context.Context, userID int64, peer domain.Peer, unread bool) (bool, error) {
|
|
changed, err := s.q.SetDialogUnreadMark(ctx, sqlcgen.SetDialogUnreadMarkParams{
|
|
UserID: userID,
|
|
PeerType: string(peer.Type),
|
|
PeerID: peer.ID,
|
|
Unread: unread,
|
|
})
|
|
if err != nil {
|
|
return false, fmt.Errorf("set dialog unread mark: %w", err)
|
|
}
|
|
return changed, nil
|
|
}
|
|
|
|
func (s *DialogStore) ListUnreadMarked(ctx context.Context, userID int64) ([]domain.Peer, error) {
|
|
rows, err := s.q.ListDialogUnreadMarks(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list dialog unread marks: %w", err)
|
|
}
|
|
out := make([]domain.Peer, 0, len(rows))
|
|
for _, row := range rows {
|
|
out = append(out, domain.Peer{Type: domain.PeerType(row.PeerType), ID: row.PeerID})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *DialogStore) SetPeerSettingsBarHidden(ctx context.Context, userID int64, peer domain.Peer) (bool, error) {
|
|
changed, err := s.q.SetPeerSettingsBarHidden(ctx, sqlcgen.SetPeerSettingsBarHiddenParams{
|
|
UserID: userID,
|
|
PeerType: string(peer.Type),
|
|
PeerID: peer.ID,
|
|
})
|
|
if err != nil {
|
|
return false, fmt.Errorf("set peer settings bar hidden: %w", err)
|
|
}
|
|
return changed, nil
|
|
}
|
|
|
|
func (s *DialogStore) PeerSettingsBarHidden(ctx context.Context, userID int64, peer domain.Peer) (bool, error) {
|
|
hidden, err := s.q.GetPeerSettingsBarHidden(ctx, sqlcgen.GetPeerSettingsBarHiddenParams{
|
|
UserID: userID,
|
|
PeerType: string(peer.Type),
|
|
PeerID: peer.ID,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return false, nil
|
|
}
|
|
return false, fmt.Errorf("get peer settings bar hidden: %w", err)
|
|
}
|
|
return hidden, nil
|
|
}
|
|
|
|
func (s *DialogStore) ListFolders(ctx context.Context, userID int64) (domain.DialogFolderList, error) {
|
|
rows, err := s.q.ListDialogFolders(ctx, userID)
|
|
if err != nil {
|
|
return domain.DialogFolderList{}, fmt.Errorf("list dialog folders: %w", err)
|
|
}
|
|
tagsEnabled := false
|
|
if enabled, err := s.q.GetDialogFolderTags(ctx, userID); err == nil {
|
|
tagsEnabled = enabled
|
|
} else if !errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.DialogFolderList{}, fmt.Errorf("get dialog folder tags: %w", err)
|
|
}
|
|
out := domain.DialogFolderList{
|
|
TagsEnabled: tagsEnabled,
|
|
Folders: make([]domain.DialogFolder, 0, len(rows)),
|
|
}
|
|
for _, row := range rows {
|
|
folder, err := decodeDialogFolder(row.FilterJson)
|
|
if err != nil {
|
|
return domain.DialogFolderList{}, fmt.Errorf("decode dialog folder %d: %w", row.FilterID, err)
|
|
}
|
|
folder.ID = int(row.FilterID)
|
|
folder.IsChatlist = row.IsChatlist
|
|
out.Folders = append(out.Folders, folder)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *DialogStore) GetFolder(ctx context.Context, userID int64, folderID int) (domain.DialogFolder, bool, error) {
|
|
row, err := s.q.GetDialogFolder(ctx, sqlcgen.GetDialogFolderParams{
|
|
UserID: userID,
|
|
FilterID: pgInt32NonNegative(folderID),
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.DialogFolder{}, false, nil
|
|
}
|
|
return domain.DialogFolder{}, false, fmt.Errorf("get dialog folder: %w", err)
|
|
}
|
|
folder, err := decodeDialogFolder(row.FilterJson)
|
|
if err != nil {
|
|
return domain.DialogFolder{}, false, fmt.Errorf("decode dialog folder: %w", err)
|
|
}
|
|
folder.ID = int(row.FilterID)
|
|
folder.IsChatlist = row.IsChatlist
|
|
return folder, true, nil
|
|
}
|
|
|
|
func (s *DialogStore) UpsertFolder(ctx context.Context, userID int64, folder domain.DialogFolder) error {
|
|
data, err := json.Marshal(folder)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal dialog folder: %w", err)
|
|
}
|
|
if err := s.q.UpsertDialogFolder(ctx, sqlcgen.UpsertDialogFolderParams{
|
|
UserID: userID,
|
|
FilterID: pgInt32NonNegative(folder.ID),
|
|
IsChatlist: folder.IsChatlist,
|
|
FilterJson: data,
|
|
}); err != nil {
|
|
return fmt.Errorf("upsert dialog folder: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *DialogStore) DeleteFolder(ctx context.Context, userID int64, folderID int) error {
|
|
if err := s.q.DeleteDialogFolder(ctx, sqlcgen.DeleteDialogFolderParams{
|
|
UserID: userID,
|
|
FilterID: pgInt32NonNegative(folderID),
|
|
}); err != nil {
|
|
return fmt.Errorf("delete dialog folder: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *DialogStore) ReorderFolders(ctx context.Context, userID int64, order []int) error {
|
|
if err := s.q.ReorderDialogFolders(ctx, sqlcgen.ReorderDialogFoldersParams{
|
|
UserID: userID,
|
|
FilterIds: int32s(order),
|
|
}); err != nil {
|
|
return fmt.Errorf("reorder dialog folders: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *DialogStore) SetFolderTagsEnabled(ctx context.Context, userID int64, enabled bool) error {
|
|
if err := s.q.SetDialogFolderTags(ctx, sqlcgen.SetDialogFolderTagsParams{
|
|
UserID: userID,
|
|
TagsEnabled: enabled,
|
|
}); err != nil {
|
|
return fmt.Errorf("set dialog folder tags: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *DialogStore) EditPeerFolders(ctx context.Context, userID int64, peers []domain.FolderPeerUpdate) error {
|
|
peerTypes := make([]string, 0, len(peers))
|
|
peerIDs := make([]int64, 0, len(peers))
|
|
folderIDs := make([]int32, 0, len(peers))
|
|
seen := make(map[domain.Peer]struct{}, len(peers))
|
|
for _, item := range peers {
|
|
if item.Peer.Type == "" || item.Peer.ID == 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[item.Peer]; ok {
|
|
continue
|
|
}
|
|
seen[item.Peer] = struct{}{}
|
|
peerTypes = append(peerTypes, string(item.Peer.Type))
|
|
peerIDs = append(peerIDs, item.Peer.ID)
|
|
folderIDs = append(folderIDs, pgInt32NonNegative(item.FolderID))
|
|
}
|
|
if len(peerTypes) == 0 {
|
|
return nil
|
|
}
|
|
if err := s.q.EditDialogPeerFolders(ctx, sqlcgen.EditDialogPeerFoldersParams{
|
|
UserID: userID,
|
|
PeerTypes: peerTypes,
|
|
PeerIds: peerIDs,
|
|
FolderIds: folderIDs,
|
|
}); err != nil {
|
|
return fmt.Errorf("edit dialog peer folders: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type dialogFolderParams struct {
|
|
contacts bool
|
|
nonContacts bool
|
|
excludeArchived bool
|
|
excludeRead bool
|
|
includeTypes []string
|
|
includeIDs []int64
|
|
pinnedTypes []string
|
|
pinnedIDs []int64
|
|
excludeTypes []string
|
|
excludeIDs []int64
|
|
}
|
|
|
|
func dialogFolderQueryParams(folder *domain.DialogFolder) dialogFolderParams {
|
|
if folder == nil {
|
|
return dialogFolderParams{}
|
|
}
|
|
includeTypes, includeIDs := folderPeerArrays(folder.IncludePeers)
|
|
pinnedTypes, pinnedIDs := folderPeerArrays(folder.PinnedPeers)
|
|
excludeTypes, excludeIDs := folderPeerArrays(folder.ExcludePeers)
|
|
return dialogFolderParams{
|
|
contacts: folder.Contacts,
|
|
nonContacts: folder.NonContacts,
|
|
excludeArchived: folder.ExcludeArchived,
|
|
excludeRead: folder.ExcludeRead,
|
|
includeTypes: includeTypes,
|
|
includeIDs: includeIDs,
|
|
pinnedTypes: pinnedTypes,
|
|
pinnedIDs: pinnedIDs,
|
|
excludeTypes: excludeTypes,
|
|
excludeIDs: excludeIDs,
|
|
}
|
|
}
|
|
|
|
func folderPeerArrays(peers []domain.DialogFolderPeer) ([]string, []int64) {
|
|
peerTypes := make([]string, 0, len(peers))
|
|
peerIDs := make([]int64, 0, len(peers))
|
|
seen := make(map[domain.Peer]struct{}, len(peers))
|
|
for _, item := range peers {
|
|
peer := item.Peer
|
|
if peer.Type == "" || peer.ID == 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[peer]; ok {
|
|
continue
|
|
}
|
|
seen[peer] = struct{}{}
|
|
peerTypes = append(peerTypes, string(peer.Type))
|
|
peerIDs = append(peerIDs, peer.ID)
|
|
}
|
|
return peerTypes, peerIDs
|
|
}
|
|
|
|
func decodeDialogFolder(data string) (domain.DialogFolder, error) {
|
|
if data == "" {
|
|
return domain.DialogFolder{}, nil
|
|
}
|
|
var folder domain.DialogFolder
|
|
if err := json.Unmarshal([]byte(data), &folder); err != nil {
|
|
return domain.DialogFolder{}, err
|
|
}
|
|
return folder, nil
|
|
}
|
|
|
|
func decodeDialogDrafts(rows []string) ([]domain.DialogDraft, error) {
|
|
out := make([]domain.DialogDraft, 0, len(rows))
|
|
for _, row := range rows {
|
|
draft, err := decodeDialogDraft(row)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, draft)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func decodeDialogDraft(data string) (domain.DialogDraft, error) {
|
|
if data == "" {
|
|
return domain.DialogDraft{}, nil
|
|
}
|
|
var draft domain.DialogDraft
|
|
if err := json.Unmarshal([]byte(data), &draft); err != nil {
|
|
return domain.DialogDraft{}, fmt.Errorf("decode dialog draft: %w", err)
|
|
}
|
|
if draft.Entities == nil {
|
|
draft.Entities = []domain.MessageEntity{}
|
|
}
|
|
return draft, nil
|
|
}
|
|
|
|
func clampDialogDraftLimit(limit int) int {
|
|
if limit <= 0 || limit > domain.MaxDialogDraftsPerUser {
|
|
return domain.MaxDialogDraftsPerUser
|
|
}
|
|
return limit
|
|
}
|
|
|
|
func peerArrays(peers []domain.Peer) ([]string, []int64) {
|
|
peerTypes := make([]string, 0, len(peers))
|
|
peerIDs := make([]int64, 0, len(peers))
|
|
seen := make(map[domain.Peer]struct{}, len(peers))
|
|
for _, peer := range peers {
|
|
if peer.Type == "" || peer.ID == 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[peer]; ok {
|
|
continue
|
|
}
|
|
seen[peer] = struct{}{}
|
|
peerTypes = append(peerTypes, string(peer.Type))
|
|
peerIDs = append(peerIDs, peer.ID)
|
|
}
|
|
return peerTypes, peerIDs
|
|
}
|
|
|
|
func dialogListHash(dialogs []domain.Dialog) int64 {
|
|
if len(dialogs) == 0 {
|
|
return 0
|
|
}
|
|
h := fnv.New64a()
|
|
var buf [47]byte
|
|
for _, d := range dialogs {
|
|
binary.LittleEndian.PutUint64(buf[:8], uint64(d.Peer.ID))
|
|
binary.LittleEndian.PutUint32(buf[8:12], uint32(d.FolderID))
|
|
binary.LittleEndian.PutUint32(buf[12:16], uint32(d.TopMessage))
|
|
binary.LittleEndian.PutUint32(buf[16:20], uint32(d.TopMessageDate))
|
|
binary.LittleEndian.PutUint32(buf[20:24], uint32(d.ReadInboxMaxID))
|
|
binary.LittleEndian.PutUint32(buf[24:28], uint32(d.ReadOutboxMaxID))
|
|
binary.LittleEndian.PutUint32(buf[28:32], uint32(d.UnreadCount))
|
|
binary.LittleEndian.PutUint32(buf[32:36], uint32(d.UnreadMentions))
|
|
binary.LittleEndian.PutUint32(buf[36:40], uint32(d.UnreadReactions))
|
|
if d.Pinned {
|
|
buf[40] = 1
|
|
} else {
|
|
buf[40] = 0
|
|
}
|
|
binary.LittleEndian.PutUint32(buf[41:45], uint32(d.PinnedOrder))
|
|
if d.UnreadMark {
|
|
buf[45] = 1
|
|
} else {
|
|
buf[45] = 0
|
|
}
|
|
if d.PeerSettingsBarHidden {
|
|
buf[46] = 1
|
|
} else {
|
|
buf[46] = 0
|
|
}
|
|
_, _ = h.Write(buf[:])
|
|
}
|
|
return int64(h.Sum64())
|
|
}
|