feat: sync chatlist sharing support
This commit is contained in:
parent
ec6e8fd13d
commit
2a9c12263f
62 changed files with 3408 additions and 212 deletions
25
internal/store/chatlist.go
Normal file
25
internal/store/chatlist.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// ChatlistStore persists exported shared-folder links and imported memberships.
|
||||
type ChatlistStore interface {
|
||||
CountInvites(ctx context.Context, ownerUserID int64, filterID int) (int, error)
|
||||
CountActiveInvites(ctx context.Context, ownerUserID int64, filterID int) (int, error)
|
||||
SaveInvite(ctx context.Context, invite domain.ChatlistInvite) (domain.ChatlistInvite, error)
|
||||
GetInvite(ctx context.Context, ownerUserID int64, filterID int, slug string) (domain.ChatlistInvite, bool, error)
|
||||
GetInviteBySlug(ctx context.Context, slug string) (domain.ChatlistInvite, bool, error)
|
||||
ListInvites(ctx context.Context, ownerUserID int64, filterID int) ([]domain.ChatlistInvite, error)
|
||||
DeleteInvite(ctx context.Context, ownerUserID int64, filterID int, slug string) (bool, error)
|
||||
|
||||
CountMemberships(ctx context.Context, userID int64) (int, error)
|
||||
SaveMembership(ctx context.Context, membership domain.ChatlistMembership) error
|
||||
GetMembershipBySlug(ctx context.Context, userID int64, slug string) (domain.ChatlistMembership, bool, error)
|
||||
GetMembershipByLocalFilter(ctx context.Context, userID int64, localFilterID int) (domain.ChatlistMembership, bool, error)
|
||||
DeleteMembershipByLocalFilter(ctx context.Context, userID int64, localFilterID int) (bool, error)
|
||||
SetMembershipHidden(ctx context.Context, userID int64, localFilterID int, hidden bool) (bool, error)
|
||||
}
|
||||
212
internal/store/memory/chatlists.go
Normal file
212
internal/store/memory/chatlists.go
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
type ChatlistStore struct {
|
||||
mu sync.RWMutex
|
||||
nextInvite int64
|
||||
invites map[string]domain.ChatlistInvite
|
||||
memberships map[chatlistMembershipKey]domain.ChatlistMembership
|
||||
}
|
||||
|
||||
type chatlistMembershipKey struct {
|
||||
userID int64
|
||||
localFilterID int
|
||||
}
|
||||
|
||||
// NewChatlistStore creates an in-memory ChatlistStore.
|
||||
func NewChatlistStore() *ChatlistStore {
|
||||
return &ChatlistStore{
|
||||
nextInvite: 1,
|
||||
invites: make(map[string]domain.ChatlistInvite),
|
||||
memberships: make(map[chatlistMembershipKey]domain.ChatlistMembership),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) CountInvites(_ context.Context, ownerUserID int64, filterID int) (int, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
count := 0
|
||||
for _, invite := range s.invites {
|
||||
if invite.OwnerUserID == ownerUserID && invite.FilterID == filterID && !invite.Deleted {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) CountActiveInvites(_ context.Context, ownerUserID int64, filterID int) (int, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
count := 0
|
||||
for _, invite := range s.invites {
|
||||
if invite.OwnerUserID == ownerUserID && invite.FilterID == filterID && !invite.Deleted && !invite.Revoked {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) SaveInvite(_ context.Context, invite domain.ChatlistInvite) (domain.ChatlistInvite, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if invite.Slug == "" {
|
||||
return domain.ChatlistInvite{}, domain.ErrChatlistInviteInvalid
|
||||
}
|
||||
if existing, ok := s.invites[invite.Slug]; ok && (existing.OwnerUserID != invite.OwnerUserID || existing.FilterID != invite.FilterID) {
|
||||
return domain.ChatlistInvite{}, domain.ErrChatlistSlugOccupied
|
||||
}
|
||||
if invite.ID == 0 {
|
||||
if existing, ok := s.invites[invite.Slug]; ok {
|
||||
invite.ID = existing.ID
|
||||
if invite.Date == 0 {
|
||||
invite.Date = existing.Date
|
||||
}
|
||||
} else {
|
||||
invite.ID = s.nextInvite
|
||||
s.nextInvite++
|
||||
}
|
||||
}
|
||||
invite.Deleted = false
|
||||
s.invites[invite.Slug] = cloneChatlistInvite(invite)
|
||||
return cloneChatlistInvite(invite), nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) GetInvite(_ context.Context, ownerUserID int64, filterID int, slug string) (domain.ChatlistInvite, bool, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
invite, ok := s.invites[slug]
|
||||
if !ok || invite.Deleted || invite.OwnerUserID != ownerUserID || invite.FilterID != filterID {
|
||||
return domain.ChatlistInvite{}, false, nil
|
||||
}
|
||||
return cloneChatlistInvite(invite), true, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) GetInviteBySlug(_ context.Context, slug string) (domain.ChatlistInvite, bool, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
invite, ok := s.invites[slug]
|
||||
if !ok || invite.Deleted || invite.Revoked {
|
||||
return domain.ChatlistInvite{}, false, nil
|
||||
}
|
||||
return cloneChatlistInvite(invite), true, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) ListInvites(_ context.Context, ownerUserID int64, filterID int) ([]domain.ChatlistInvite, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
out := make([]domain.ChatlistInvite, 0)
|
||||
for _, invite := range s.invites {
|
||||
if invite.OwnerUserID == ownerUserID && invite.FilterID == filterID && !invite.Deleted {
|
||||
out = append(out, cloneChatlistInvite(invite))
|
||||
}
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
if out[i].Date != out[j].Date {
|
||||
return out[i].Date < out[j].Date
|
||||
}
|
||||
return out[i].Slug < out[j].Slug
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) DeleteInvite(_ context.Context, ownerUserID int64, filterID int, slug string) (bool, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
invite, ok := s.invites[slug]
|
||||
if !ok || invite.Deleted || invite.OwnerUserID != ownerUserID || invite.FilterID != filterID {
|
||||
return false, nil
|
||||
}
|
||||
invite.Deleted = true
|
||||
s.invites[slug] = invite
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) CountMemberships(_ context.Context, userID int64) (int, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
count := 0
|
||||
for key := range s.memberships {
|
||||
if key.userID == userID {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) SaveMembership(_ context.Context, membership domain.ChatlistMembership) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if membership.UserID == 0 || membership.LocalFilterID == 0 || membership.Slug == "" {
|
||||
return domain.ErrChatlistInvalid
|
||||
}
|
||||
key := chatlistMembershipKey{userID: membership.UserID, localFilterID: membership.LocalFilterID}
|
||||
for existingKey, existing := range s.memberships {
|
||||
if existingKey != key && existing.UserID == membership.UserID && existing.Slug == membership.Slug {
|
||||
delete(s.memberships, existingKey)
|
||||
}
|
||||
}
|
||||
s.memberships[key] = cloneChatlistMembership(membership)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) GetMembershipBySlug(_ context.Context, userID int64, slug string) (domain.ChatlistMembership, bool, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, membership := range s.memberships {
|
||||
if membership.UserID == userID && membership.Slug == slug {
|
||||
return cloneChatlistMembership(membership), true, nil
|
||||
}
|
||||
}
|
||||
return domain.ChatlistMembership{}, false, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) GetMembershipByLocalFilter(_ context.Context, userID int64, localFilterID int) (domain.ChatlistMembership, bool, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
membership, ok := s.memberships[chatlistMembershipKey{userID: userID, localFilterID: localFilterID}]
|
||||
if !ok {
|
||||
return domain.ChatlistMembership{}, false, nil
|
||||
}
|
||||
return cloneChatlistMembership(membership), true, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) DeleteMembershipByLocalFilter(_ context.Context, userID int64, localFilterID int) (bool, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
key := chatlistMembershipKey{userID: userID, localFilterID: localFilterID}
|
||||
if _, ok := s.memberships[key]; !ok {
|
||||
return false, nil
|
||||
}
|
||||
delete(s.memberships, key)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) SetMembershipHidden(_ context.Context, userID int64, localFilterID int, hidden bool) (bool, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
key := chatlistMembershipKey{userID: userID, localFilterID: localFilterID}
|
||||
membership, ok := s.memberships[key]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
changed := membership.HiddenUpdates != hidden
|
||||
membership.HiddenUpdates = hidden
|
||||
s.memberships[key] = membership
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
func cloneChatlistInvite(invite domain.ChatlistInvite) domain.ChatlistInvite {
|
||||
invite.Peers = append([]domain.DialogFolderPeer(nil), invite.Peers...)
|
||||
return invite
|
||||
}
|
||||
|
||||
func cloneChatlistMembership(membership domain.ChatlistMembership) domain.ChatlistMembership {
|
||||
return membership
|
||||
}
|
||||
279
internal/store/postgres/chatlist.go
Normal file
279
internal/store/postgres/chatlist.go
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/postgres/sqlcgen"
|
||||
)
|
||||
|
||||
type ChatlistStore struct {
|
||||
db sqlcgen.DBTX
|
||||
}
|
||||
|
||||
func NewChatlistStore(db sqlcgen.DBTX) *ChatlistStore {
|
||||
return &ChatlistStore{db: db}
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) CountInvites(ctx context.Context, ownerUserID int64, filterID int) (int, error) {
|
||||
var count int
|
||||
if err := s.db.QueryRow(ctx, `
|
||||
SELECT count(*)::int
|
||||
FROM chatlist_invites
|
||||
WHERE owner_user_id = $1 AND filter_id = $2 AND NOT deleted`, ownerUserID, filterID).Scan(&count); err != nil {
|
||||
return 0, fmt.Errorf("count chatlist invites: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) CountActiveInvites(ctx context.Context, ownerUserID int64, filterID int) (int, error) {
|
||||
var count int
|
||||
if err := s.db.QueryRow(ctx, `
|
||||
SELECT count(*)::int
|
||||
FROM chatlist_invites
|
||||
WHERE owner_user_id = $1 AND filter_id = $2 AND NOT deleted AND NOT revoked`, ownerUserID, filterID).Scan(&count); err != nil {
|
||||
return 0, fmt.Errorf("count active chatlist invites: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) SaveInvite(ctx context.Context, invite domain.ChatlistInvite) (domain.ChatlistInvite, error) {
|
||||
peers, err := json.Marshal(invite.Peers)
|
||||
if err != nil {
|
||||
return domain.ChatlistInvite{}, fmt.Errorf("marshal chatlist invite peers: %w", err)
|
||||
}
|
||||
row := s.db.QueryRow(ctx, `
|
||||
INSERT INTO chatlist_invites (owner_user_id, filter_id, slug, title, peers, revoked, deleted, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5::jsonb, $6, false,
|
||||
CASE WHEN $7::int > 0 THEN to_timestamp($7::int) ELSE now() END, now())
|
||||
ON CONFLICT (slug) DO UPDATE SET
|
||||
title = EXCLUDED.title,
|
||||
peers = EXCLUDED.peers,
|
||||
revoked = EXCLUDED.revoked,
|
||||
deleted = false,
|
||||
updated_at = now()
|
||||
WHERE chatlist_invites.owner_user_id = EXCLUDED.owner_user_id
|
||||
AND chatlist_invites.filter_id = EXCLUDED.filter_id
|
||||
RETURNING id, owner_user_id, filter_id, slug, title, peers::text, revoked, deleted,
|
||||
EXTRACT(EPOCH FROM created_at)::int`, invite.OwnerUserID, invite.FilterID, invite.Slug, invite.Title, peers, invite.Revoked, invite.Date)
|
||||
out, err := scanChatlistInvite(row)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.ChatlistInvite{}, domain.ErrChatlistSlugOccupied
|
||||
}
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
||||
return domain.ChatlistInvite{}, domain.ErrChatlistSlugOccupied
|
||||
}
|
||||
return domain.ChatlistInvite{}, fmt.Errorf("save chatlist invite: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) GetInvite(ctx context.Context, ownerUserID int64, filterID int, slug string) (domain.ChatlistInvite, bool, error) {
|
||||
invite, err := scanChatlistInvite(s.db.QueryRow(ctx, `
|
||||
SELECT id, owner_user_id, filter_id, slug, title, peers::text, revoked, deleted,
|
||||
EXTRACT(EPOCH FROM created_at)::int
|
||||
FROM chatlist_invites
|
||||
WHERE owner_user_id = $1 AND filter_id = $2 AND slug = $3 AND NOT deleted`, ownerUserID, filterID, slug))
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.ChatlistInvite{}, false, nil
|
||||
}
|
||||
return domain.ChatlistInvite{}, false, fmt.Errorf("get chatlist invite: %w", err)
|
||||
}
|
||||
return invite, true, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) GetInviteBySlug(ctx context.Context, slug string) (domain.ChatlistInvite, bool, error) {
|
||||
invite, err := scanChatlistInvite(s.db.QueryRow(ctx, `
|
||||
SELECT id, owner_user_id, filter_id, slug, title, peers::text, revoked, deleted,
|
||||
EXTRACT(EPOCH FROM created_at)::int
|
||||
FROM chatlist_invites
|
||||
WHERE slug = $1 AND NOT deleted AND NOT revoked`, slug))
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.ChatlistInvite{}, false, nil
|
||||
}
|
||||
return domain.ChatlistInvite{}, false, fmt.Errorf("get chatlist invite by slug: %w", err)
|
||||
}
|
||||
return invite, true, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) ListInvites(ctx context.Context, ownerUserID int64, filterID int) ([]domain.ChatlistInvite, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, owner_user_id, filter_id, slug, title, peers::text, revoked, deleted,
|
||||
EXTRACT(EPOCH FROM created_at)::int
|
||||
FROM chatlist_invites
|
||||
WHERE owner_user_id = $1 AND filter_id = $2 AND NOT deleted
|
||||
ORDER BY created_at ASC, slug ASC`, ownerUserID, filterID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list chatlist invites: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]domain.ChatlistInvite, 0)
|
||||
for rows.Next() {
|
||||
invite, err := scanChatlistInvite(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, invite)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("list chatlist invites rows: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) DeleteInvite(ctx context.Context, ownerUserID int64, filterID int, slug string) (bool, error) {
|
||||
tag, err := s.db.Exec(ctx, `
|
||||
UPDATE chatlist_invites
|
||||
SET deleted = true, updated_at = now()
|
||||
WHERE owner_user_id = $1 AND filter_id = $2 AND slug = $3 AND NOT deleted`, ownerUserID, filterID, slug)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("delete chatlist invite: %w", err)
|
||||
}
|
||||
return tag.RowsAffected() > 0, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) CountMemberships(ctx context.Context, userID int64) (int, error) {
|
||||
var count int
|
||||
if err := s.db.QueryRow(ctx, `
|
||||
SELECT count(*)::int
|
||||
FROM chatlist_memberships
|
||||
WHERE user_id = $1`, userID).Scan(&count); err != nil {
|
||||
return 0, fmt.Errorf("count chatlist memberships: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) SaveMembership(ctx context.Context, membership domain.ChatlistMembership) error {
|
||||
if membership.Date == 0 {
|
||||
membership.Date = nowUnix()
|
||||
}
|
||||
exec := func(ctx context.Context, db sqlcgen.DBTX) error {
|
||||
if _, err := db.Exec(ctx, `
|
||||
DELETE FROM chatlist_memberships
|
||||
WHERE user_id = $1 AND slug = $2 AND local_filter_id <> $3`, membership.UserID, membership.Slug, membership.LocalFilterID); err != nil {
|
||||
return fmt.Errorf("dedupe chatlist membership: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(ctx, `
|
||||
INSERT INTO chatlist_memberships (
|
||||
user_id, local_filter_id, owner_user_id, owner_filter_id, slug, hidden_updates, joined_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
CASE WHEN $7::int > 0 THEN to_timestamp($7::int) ELSE now() END,
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (user_id, local_filter_id) DO UPDATE SET
|
||||
owner_user_id = EXCLUDED.owner_user_id,
|
||||
owner_filter_id = EXCLUDED.owner_filter_id,
|
||||
slug = EXCLUDED.slug,
|
||||
hidden_updates = EXCLUDED.hidden_updates,
|
||||
updated_at = now()`, membership.UserID, membership.LocalFilterID, membership.OwnerUserID, membership.OwnerFilterID, membership.Slug, membership.HiddenUpdates, membership.Date); err != nil {
|
||||
return fmt.Errorf("save chatlist membership: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
beginner, ok := s.db.(txBeginner)
|
||||
if !ok {
|
||||
return exec(ctx, s.db)
|
||||
}
|
||||
tx, err := beginner.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin save chatlist membership: %w", err)
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = tx.Rollback(ctx)
|
||||
}
|
||||
}()
|
||||
if err := exec(ctx, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return fmt.Errorf("commit save chatlist membership: %w", err)
|
||||
}
|
||||
committed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) GetMembershipBySlug(ctx context.Context, userID int64, slug string) (domain.ChatlistMembership, bool, error) {
|
||||
membership, err := scanChatlistMembership(s.db.QueryRow(ctx, `
|
||||
SELECT user_id, local_filter_id, owner_user_id, owner_filter_id, slug, hidden_updates,
|
||||
EXTRACT(EPOCH FROM joined_at)::int
|
||||
FROM chatlist_memberships
|
||||
WHERE user_id = $1 AND slug = $2`, userID, slug))
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.ChatlistMembership{}, false, nil
|
||||
}
|
||||
return domain.ChatlistMembership{}, false, fmt.Errorf("get chatlist membership by slug: %w", err)
|
||||
}
|
||||
return membership, true, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) GetMembershipByLocalFilter(ctx context.Context, userID int64, localFilterID int) (domain.ChatlistMembership, bool, error) {
|
||||
membership, err := scanChatlistMembership(s.db.QueryRow(ctx, `
|
||||
SELECT user_id, local_filter_id, owner_user_id, owner_filter_id, slug, hidden_updates,
|
||||
EXTRACT(EPOCH FROM joined_at)::int
|
||||
FROM chatlist_memberships
|
||||
WHERE user_id = $1 AND local_filter_id = $2`, userID, localFilterID))
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.ChatlistMembership{}, false, nil
|
||||
}
|
||||
return domain.ChatlistMembership{}, false, fmt.Errorf("get chatlist membership by local filter: %w", err)
|
||||
}
|
||||
return membership, true, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) DeleteMembershipByLocalFilter(ctx context.Context, userID int64, localFilterID int) (bool, error) {
|
||||
tag, err := s.db.Exec(ctx, `
|
||||
DELETE FROM chatlist_memberships
|
||||
WHERE user_id = $1 AND local_filter_id = $2`, userID, localFilterID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("delete chatlist membership: %w", err)
|
||||
}
|
||||
return tag.RowsAffected() > 0, nil
|
||||
}
|
||||
|
||||
func (s *ChatlistStore) SetMembershipHidden(ctx context.Context, userID int64, localFilterID int, hidden bool) (bool, error) {
|
||||
tag, err := s.db.Exec(ctx, `
|
||||
UPDATE chatlist_memberships
|
||||
SET hidden_updates = $3, updated_at = now()
|
||||
WHERE user_id = $1 AND local_filter_id = $2 AND hidden_updates IS DISTINCT FROM $3`, userID, localFilterID, hidden)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("set chatlist membership hidden: %w", err)
|
||||
}
|
||||
return tag.RowsAffected() > 0, nil
|
||||
}
|
||||
|
||||
func scanChatlistInvite(row rowScanner) (domain.ChatlistInvite, error) {
|
||||
var invite domain.ChatlistInvite
|
||||
var peersJSON string
|
||||
if err := row.Scan(&invite.ID, &invite.OwnerUserID, &invite.FilterID, &invite.Slug, &invite.Title, &peersJSON, &invite.Revoked, &invite.Deleted, &invite.Date); err != nil {
|
||||
return domain.ChatlistInvite{}, err
|
||||
}
|
||||
if peersJSON != "" {
|
||||
if err := json.Unmarshal([]byte(peersJSON), &invite.Peers); err != nil {
|
||||
return domain.ChatlistInvite{}, fmt.Errorf("decode chatlist invite peers: %w", err)
|
||||
}
|
||||
}
|
||||
return invite, nil
|
||||
}
|
||||
|
||||
func scanChatlistMembership(row rowScanner) (domain.ChatlistMembership, error) {
|
||||
var membership domain.ChatlistMembership
|
||||
if err := row.Scan(&membership.UserID, &membership.LocalFilterID, &membership.OwnerUserID, &membership.OwnerFilterID, &membership.Slug, &membership.HiddenUpdates, &membership.Date); err != nil {
|
||||
return domain.ChatlistMembership{}, err
|
||||
}
|
||||
return membership, nil
|
||||
}
|
||||
|
|
@ -7,11 +7,13 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/links"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
|
|
@ -22,6 +24,10 @@ func baseNow() int {
|
|||
return int(time.Now().Unix())
|
||||
}
|
||||
|
||||
func conferenceInviteLink(slug string) string {
|
||||
return links.Build(links.DefaultPublicBaseURL, "call/"+slug, url.Values{"slug": []string{slug}})
|
||||
}
|
||||
|
||||
// GroupCallStoreFactory 为每个用例提供干净的 store 与不冲突的 channel id。
|
||||
type GroupCallStoreFactory func(t *testing.T) (st store.GroupCallStore, channelID int64)
|
||||
|
||||
|
|
@ -340,7 +346,7 @@ func contractConferenceChainBlocks(t *testing.T, factory GroupCallStoreFactory)
|
|||
slug := fmt.Sprintf("contract-chain-%d", channelID)
|
||||
call, err := st.CreateConferenceCall(ctx, domain.GroupCall{
|
||||
ID: channelID*100 + 51, AccessHash: channelID*100 + 58, CreatorUserID: 1,
|
||||
InviteSlug: slug, InviteLink: "https://telesrv.net/call/" + slug + "?slug=" + slug,
|
||||
InviteSlug: slug, InviteLink: conferenceInviteLink(slug),
|
||||
RandomID: channelID*100 + 51, CreatedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -388,7 +394,7 @@ func contractConferenceRecipientsTerminalAccess(t *testing.T, factory GroupCallS
|
|||
slug := fmt.Sprintf("contract-recipient-%d", channelID)
|
||||
call, err := st.CreateConferenceCall(ctx, domain.GroupCall{
|
||||
ID: channelID*100 + 61, AccessHash: channelID*100 + 68, CreatorUserID: 1,
|
||||
InviteSlug: slug, InviteLink: "https://telesrv.net/call/" + slug + "?slug=" + slug,
|
||||
InviteSlug: slug, InviteLink: conferenceInviteLink(slug),
|
||||
RandomID: channelID*100 + 61, CreatedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -434,10 +440,11 @@ func contractConferenceEmptyDiscards(t *testing.T, factory GroupCallStoreFactory
|
|||
st, channelID := factory(t)
|
||||
ctx := context.Background()
|
||||
now := baseNow()
|
||||
emptySlug := fmt.Sprintf("contract-empty-%d", channelID)
|
||||
call, err := st.CreateConferenceCall(ctx, domain.GroupCall{
|
||||
ID: channelID*100 + 71, AccessHash: channelID*100 + 78, CreatorUserID: 1,
|
||||
InviteSlug: fmt.Sprintf("contract-empty-%d", channelID),
|
||||
InviteLink: fmt.Sprintf("https://telesrv.net/call/contract-empty-%d?slug=contract-empty-%d", channelID, channelID),
|
||||
InviteSlug: emptySlug,
|
||||
InviteLink: conferenceInviteLink(emptySlug),
|
||||
RandomID: channelID*100 + 71,
|
||||
CreatedAt: now,
|
||||
})
|
||||
|
|
@ -464,10 +471,11 @@ func contractConferenceEmptyDiscards(t *testing.T, factory GroupCallStoreFactory
|
|||
t.Fatalf("join empty discarded conference err = %v, want ErrGroupCallDiscarded", err)
|
||||
}
|
||||
|
||||
resetSlug := fmt.Sprintf("contract-reset-empty-%d", channelID)
|
||||
resetCall, err := st.CreateConferenceCall(ctx, domain.GroupCall{
|
||||
ID: channelID*100 + 81, AccessHash: channelID*100 + 88, CreatorUserID: 1,
|
||||
InviteSlug: fmt.Sprintf("contract-reset-empty-%d", channelID),
|
||||
InviteLink: fmt.Sprintf("https://telesrv.net/call/contract-reset-empty-%d?slug=contract-reset-empty-%d", channelID, channelID),
|
||||
InviteSlug: resetSlug,
|
||||
InviteLink: conferenceInviteLink(resetSlug),
|
||||
RandomID: channelID*100 + 81,
|
||||
CreatedAt: now + 10,
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue