Initial open source release
This commit is contained in:
commit
74992e893f
377 changed files with 118084 additions and 0 deletions
722
internal/store/postgres/dialog.go
Normal file
722
internal/store/postgres/dialog.go
Normal file
|
|
@ -0,0 +1,722 @@
|
|||
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())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue