internal/store/postgres/channel_member_admin.go
Rama main · réplica de lectura del repositorio original.
package postgres
import (
"context"
"errors"
"fmt"
"telesrv/internal/domain"
)
func (s *ChannelStore) EditChannelAdmin(ctx context.Context, req domain.EditChannelAdminRequest) (domain.EditChannelAdminResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.MemberID == 0 {
return domain.EditChannelAdminResult{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.EditChannelAdminResult{}, fmt.Errorf("edit channel admin: db does not support transactions")
}
if req.Date == 0 {
req.Date = nowUnix()
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.EditChannelAdminResult{}, fmt.Errorf("begin edit channel admin: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, actor, err := s.getChannelForMember(ctx, tx, req.UserID, req.ChannelID)
if err != nil {
return domain.EditChannelAdminResult{}, err
}
if !canAddChannelAdmins(actor) {
return domain.EditChannelAdminResult{}, domain.ErrChannelAdminRequired
}
previous, err := s.getChannelMember(ctx, tx, req.ChannelID, req.MemberID)
membershipActivated := err != nil && errors.Is(err, domain.ErrChannelPrivate)
if err != nil {
if !errors.Is(err, domain.ErrChannelPrivate) {
return domain.EditChannelAdminResult{}, err
}
previous = domain.ChannelMember{
ChannelID: req.ChannelID,
UserID: req.MemberID,
InviterUserID: req.UserID,
Role: domain.ChannelRoleMember,
Status: domain.ChannelMemberActive,
JoinedAt: req.Date,
AvailableMinID: channelInitialAvailableMinID(channel),
AvailableMinPts: channelInitialAvailableMinPts(channel),
ReadInboxMaxID: channel.TopMessageID,
}
}
if previous.Status != domain.ChannelMemberActive {
membershipActivated = true
}
if previous.Role == domain.ChannelRoleCreator {
if req.MemberID != req.UserID || channel.CreatorUserID != req.UserID || actor.Role != domain.ChannelRoleCreator {
return domain.EditChannelAdminResult{}, domain.ErrChannelUserCreator
}
member := previous
member.Status = domain.ChannelMemberActive
member.LeftAt = 0
member.AdminRights = domain.CreatorProjectionAdminRights(req.AdminRights)
if req.HasRank() {
member.Rank = req.Rank
}
if err := upsertChannelMemberTx(ctx, tx, channel, member); err != nil {
return domain.EditChannelAdminResult{}, err
}
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: req.Date,
Type: domain.ChannelAdminLogParticipantPromote,
PrevParticipant: &previous,
NewParticipant: &member,
}); err != nil {
return domain.EditChannelAdminResult{}, err
}
channel, err = refreshChannelCountsTx(ctx, tx, channel)
if err != nil {
return domain.EditChannelAdminResult{}, err
}
event := transientChannelParticipantEvent(channel.ID, req.UserID, previous, member, req.Date)
if err := tx.Commit(ctx); err != nil {
return domain.EditChannelAdminResult{}, fmt.Errorf("commit edit channel creator admin: %w", err)
}
committed = true
recipients, _ := s.ListActiveChannelMemberIDs(ctx, req.UserID, req.ChannelID, 0)
recipients = append(recipients, req.MemberID)
return domain.EditChannelAdminResult{Channel: channel, Previous: previous, Participant: member, Event: event, Recipients: recipients, Date: req.Date}, nil
}
if actor.Role != domain.ChannelRoleCreator && !adminRightsSubset(req.AdminRights, actor.AdminRights) {
return domain.EditChannelAdminResult{}, domain.ErrChannelRightForbidden
}
member := previous
member.InviterUserID = req.UserID
member.Status = domain.ChannelMemberActive
member.LeftAt = 0
if req.HasRank() {
member.Rank = req.Rank
}
if previous.Status != domain.ChannelMemberActive {
member.JoinedAt = req.Date
if minPts := channelInitialAvailableMinPts(channel); minPts > member.AvailableMinPts {
member.AvailableMinPts = minPts
}
}
member.AdminRights = domain.NormalizeFullMegagroupAdminRights(channel, req.AdminRights)
if zeroChannelAdminRights(req.AdminRights) {
member.Role = domain.ChannelRoleMember
member.Rank = ""
} else {
member.Role = domain.ChannelRoleAdmin
}
if err := upsertChannelMemberTx(ctx, tx, channel, member); err != nil {
return domain.EditChannelAdminResult{}, err
}
// A reactivated member (previously left/kicked) likewise has its prehistory read boundary returned, otherwise they permanently see an empty channel.
if membershipActivated {
if err := resetPrehistoryBoundaryTx(ctx, tx, channel, member.UserID); err != nil {
return domain.EditChannelAdminResult{}, err
}
}
logType := domain.ChannelAdminLogParticipantPromote
if member.Role != domain.ChannelRoleAdmin {
logType = domain.ChannelAdminLogParticipantDemote
}
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: req.Date,
Type: logType,
PrevParticipant: &previous,
NewParticipant: &member,
}); err != nil {
return domain.EditChannelAdminResult{}, err
}
channel, err = refreshChannelCountsTx(ctx, tx, channel)
if err != nil {
return domain.EditChannelAdminResult{}, err
}
event := transientChannelParticipantEvent(channel.ID, req.UserID, previous, member, req.Date)
msg, _ := s.getChannelMessage(ctx, tx, req.ChannelID, channel.TopMessageID)
if err := upsertChannelDialogTx(ctx, tx, member.UserID, channel, msg, member.ReadInboxMaxID, member.ReadOutboxMaxID); err != nil {
return domain.EditChannelAdminResult{}, err
}
if membershipActivated {
if err := enqueueWelcomeMessageDeliveriesTx(ctx, tx, channel.ID, []domain.ChannelMember{member}); err != nil {
return domain.EditChannelAdminResult{}, err
}
}
if err := tx.Commit(ctx); err != nil {
return domain.EditChannelAdminResult{}, fmt.Errorf("commit edit channel admin: %w", err)
}
committed = true
recipients, _ := s.ListActiveChannelMemberIDs(ctx, req.UserID, req.ChannelID, 0)
recipients = append(recipients, req.MemberID)
return domain.EditChannelAdminResult{Channel: channel, Previous: previous, Participant: member, Event: event, Recipients: recipients, Date: req.Date}, nil
}
func (s *ChannelStore) TransferChannelOwnership(ctx context.Context, req domain.TransferChannelOwnershipRequest) (domain.TransferChannelOwnershipResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.NewOwnerID == 0 || req.NewOwnerID == req.UserID {
return domain.TransferChannelOwnershipResult{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.TransferChannelOwnershipResult{}, fmt.Errorf("transfer channel ownership: db does not support transactions")
}
if req.Date == 0 {
req.Date = nowUnix()
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.TransferChannelOwnershipResult{}, fmt.Errorf("begin transfer channel ownership: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, previousOwner, err := s.getChannelForMember(ctx, tx, req.UserID, req.ChannelID)
if err != nil {
return domain.TransferChannelOwnershipResult{}, err
}
if channel.CreatorUserID != req.UserID || previousOwner.Role != domain.ChannelRoleCreator {
return domain.TransferChannelOwnershipResult{}, domain.ErrChannelAdminRequired
}
previousNewOwner, err := s.getChannelMember(ctx, tx, req.ChannelID, req.NewOwnerID)
if err != nil {
if errors.Is(err, domain.ErrChannelPrivate) {
return domain.TransferChannelOwnershipResult{}, domain.ErrUserNotParticipant
}
return domain.TransferChannelOwnershipResult{}, err
}
if previousNewOwner.Status != domain.ChannelMemberActive || previousNewOwner.BannedRights.ViewMessages {
return domain.TransferChannelOwnershipResult{}, domain.ErrUserNotParticipant
}
if previousNewOwner.Role == domain.ChannelRoleCreator {
return domain.TransferChannelOwnershipResult{}, domain.ErrChannelNotModified
}
oldOwner := previousOwner
oldOwner.Role = domain.ChannelRoleAdmin
oldOwner.AdminRights = creatorChannelMember(req.ChannelID, req.UserID, req.Date).AdminRights
oldOwner.Rank = ""
oldOwner.Status = domain.ChannelMemberActive
oldOwner.LeftAt = 0
if oldOwner.InviterUserID == 0 {
oldOwner.InviterUserID = req.UserID
}
newOwner := previousNewOwner
newOwner.Role = domain.ChannelRoleCreator
newOwner.AdminRights = creatorChannelMember(req.ChannelID, req.NewOwnerID, req.Date).AdminRights
newOwner.Rank = ""
newOwner.Status = domain.ChannelMemberActive
newOwner.LeftAt = 0
if newOwner.JoinedAt == 0 {
newOwner.JoinedAt = req.Date
}
channel.CreatorUserID = req.NewOwnerID
if _, err := tx.Exec(ctx, `
UPDATE channels
SET creator_user_id = $2,
updated_at = now()
WHERE id = $1 AND NOT deleted`, req.ChannelID, req.NewOwnerID); err != nil {
return domain.TransferChannelOwnershipResult{}, fmt.Errorf("update channel creator: %w", err)
}
if err := upsertChannelMemberTx(ctx, tx, channel, oldOwner); err != nil {
return domain.TransferChannelOwnershipResult{}, err
}
if err := upsertChannelMemberTx(ctx, tx, channel, newOwner); err != nil {
return domain.TransferChannelOwnershipResult{}, err
}
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: req.Date,
Type: domain.ChannelAdminLogParticipantPromote,
PrevParticipant: &previousOwner,
NewParticipant: &oldOwner,
}); err != nil {
return domain.TransferChannelOwnershipResult{}, err
}
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: req.Date,
Type: domain.ChannelAdminLogParticipantPromote,
PrevParticipant: &previousNewOwner,
NewParticipant: &newOwner,
}); err != nil {
return domain.TransferChannelOwnershipResult{}, err
}
channel, err = refreshChannelCountsTx(ctx, tx, channel)
if err != nil {
return domain.TransferChannelOwnershipResult{}, err
}
msg, _ := s.getChannelMessage(ctx, tx, req.ChannelID, channel.TopMessageID)
if err := upsertChannelDialogTx(ctx, tx, oldOwner.UserID, channel, msg, oldOwner.ReadInboxMaxID, oldOwner.ReadOutboxMaxID); err != nil {
return domain.TransferChannelOwnershipResult{}, err
}
if err := upsertChannelDialogTx(ctx, tx, newOwner.UserID, channel, msg, newOwner.ReadInboxMaxID, newOwner.ReadOutboxMaxID); err != nil {
return domain.TransferChannelOwnershipResult{}, err
}
recipients, err := s.listActiveChannelMemberIDs(ctx, tx, req.ChannelID, 0)
if err != nil {
return domain.TransferChannelOwnershipResult{}, err
}
if err := tx.Commit(ctx); err != nil {
return domain.TransferChannelOwnershipResult{}, fmt.Errorf("commit transfer channel ownership: %w", err)
}
committed = true
if s.rowCache != nil {
s.rowCache.delete(req.ChannelID)
}
if s.memberCache != nil {
s.memberCache.delete(req.ChannelID, req.UserID)
s.memberCache.delete(req.ChannelID, req.NewOwnerID)
}
if s.dialogCache != nil {
s.dialogCache.delete(oldOwner.UserID, req.ChannelID)
s.dialogCache.delete(newOwner.UserID, req.ChannelID)
}
events := []domain.ChannelUpdateEvent{
transientChannelParticipantEvent(channel.ID, req.UserID, previousOwner, oldOwner, req.Date),
transientChannelParticipantEvent(channel.ID, req.UserID, previousNewOwner, newOwner, req.Date),
}
recipients = append(recipients, req.UserID, req.NewOwnerID)
return domain.TransferChannelOwnershipResult{
Channel: channel,
PreviousOwner: previousOwner,
OldOwner: oldOwner,
PreviousNewOwner: previousNewOwner,
NewOwner: newOwner,
Events: events,
Recipients: recipients,
Date: req.Date,
}, nil
}
func (s *ChannelStore) EditChannelMemberRank(ctx context.Context, req domain.EditChannelMemberRankRequest) (domain.EditChannelAdminResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.MemberID == 0 {
return domain.EditChannelAdminResult{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.EditChannelAdminResult{}, fmt.Errorf("edit channel member rank: db does not support transactions")
}
if req.Date == 0 {
req.Date = nowUnix()
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.EditChannelAdminResult{}, fmt.Errorf("begin edit channel member rank: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, actor, err := s.getChannelForMember(ctx, tx, req.UserID, req.ChannelID)
if err != nil {
return domain.EditChannelAdminResult{}, err
}
previous, err := s.getChannelMember(ctx, tx, req.ChannelID, req.MemberID)
if err != nil {
if errors.Is(err, domain.ErrChannelPrivate) {
return domain.EditChannelAdminResult{}, domain.ErrUserNotParticipant
}
return domain.EditChannelAdminResult{}, err
}
if previous.Status != domain.ChannelMemberActive {
return domain.EditChannelAdminResult{}, domain.ErrUserNotParticipant
}
if err := checkEditMemberRank(channel, actor, previous); err != nil {
return domain.EditChannelAdminResult{}, err
}
member := previous
member.Rank = req.Rank
if err := upsertChannelMemberTx(ctx, tx, channel, member); err != nil {
return domain.EditChannelAdminResult{}, err
}
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: req.Date,
Type: domain.ChannelAdminLogParticipantEditRank,
PrevString: previous.Rank,
NewString: member.Rank,
Participant: &member,
}); err != nil {
return domain.EditChannelAdminResult{}, err
}
if err := tx.Commit(ctx); err != nil {
return domain.EditChannelAdminResult{}, fmt.Errorf("commit edit channel member rank: %w", err)
}
committed = true
event := transientChannelParticipantEvent(channel.ID, req.UserID, previous, member, req.Date)
recipients, _ := s.ListActiveChannelMemberIDs(ctx, req.UserID, req.ChannelID, 0)
recipients = append(recipients, req.MemberID)
return domain.EditChannelAdminResult{Channel: channel, Previous: previous, Participant: member, Event: event, Recipients: recipients, Date: req.Date}, nil
}
func (s *ChannelStore) EditChannelBanned(ctx context.Context, req domain.EditChannelBannedRequest) (domain.EditChannelBannedResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.Participant.Type != domain.PeerTypeUser || req.Participant.ID == 0 {
return domain.EditChannelBannedResult{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.EditChannelBannedResult{}, fmt.Errorf("edit channel banned: db does not support transactions")
}
if req.Date == 0 {
req.Date = nowUnix()
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.EditChannelBannedResult{}, fmt.Errorf("begin edit channel banned: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, actor, err := s.getChannelForMember(ctx, tx, req.UserID, req.ChannelID)
if err != nil {
return domain.EditChannelBannedResult{}, err
}
if !canBanChannelUsers(actor) {
return domain.EditChannelBannedResult{}, domain.ErrChannelAdminRequired
}
previous, err := s.getChannelMember(ctx, tx, req.ChannelID, req.Participant.ID)
if err != nil {
if !errors.Is(err, domain.ErrChannelPrivate) {
return domain.EditChannelBannedResult{}, err
}
previous = domain.ChannelMember{
ChannelID: req.ChannelID,
UserID: req.Participant.ID,
InviterUserID: req.UserID,
Role: domain.ChannelRoleMember,
Status: domain.ChannelMemberLeft,
}
}
if previous.Role == domain.ChannelRoleCreator {
return domain.EditChannelBannedResult{}, domain.ErrChannelUserCreator
}
member := previous
member.BannedRights = req.BannedRights
member.Role = domain.ChannelRoleMember
switch {
case req.BannedRights.ViewMessages:
member.InviterUserID = req.UserID
member.Status = domain.ChannelMemberKicked
member.LeftAt = req.Date
case zeroChannelBannedRights(req.BannedRights):
if previous.Status == domain.ChannelMemberActive {
member.Status = domain.ChannelMemberActive
} else {
member.Status = domain.ChannelMemberLeft
}
member.LeftAt = 0
default:
member.InviterUserID = req.UserID
if previous.Status == domain.ChannelMemberActive {
member.Status = domain.ChannelMemberActive
} else {
member.Status = domain.ChannelMemberBanned
}
}
if member.JoinedAt == 0 && member.Status == domain.ChannelMemberActive {
member.JoinedAt = req.Date
}
if err := upsertChannelMemberTx(ctx, tx, channel, member); err != nil {
return domain.EditChannelBannedResult{}, err
}
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: req.Date,
Type: adminLogBanType(previous, member),
PrevParticipant: &previous,
NewParticipant: &member,
}); err != nil {
return domain.EditChannelBannedResult{}, err
}
channel, err = refreshChannelCountsTx(ctx, tx, channel)
if err != nil {
return domain.EditChannelBannedResult{}, err
}
event := transientChannelParticipantEvent(channel.ID, req.UserID, previous, member, req.Date)
if member.Status == domain.ChannelMemberActive {
msg, _ := s.getChannelMessage(ctx, tx, req.ChannelID, channel.TopMessageID)
if err := upsertChannelDialogTx(ctx, tx, member.UserID, channel, msg, member.ReadInboxMaxID, member.ReadOutboxMaxID); err != nil {
return domain.EditChannelBannedResult{}, err
}
}
if member.Status == domain.ChannelMemberKicked && previous.Status == domain.ChannelMemberActive {
if err := clearChannelMentionsForUserTx(ctx, tx, req.ChannelID, req.Participant.ID); err != nil {
return domain.EditChannelBannedResult{}, err
}
}
if previous.Status == domain.ChannelMemberActive && member.Status != domain.ChannelMemberActive {
if err := deleteWelcomeMessageDeliveriesTx(ctx, tx, req.ChannelID, []int64{req.Participant.ID}); err != nil {
return domain.EditChannelBannedResult{}, err
}
}
var serviceMsg domain.ChannelMessage
var serviceEvent domain.ChannelUpdateEvent
if channel.Megagroup && previous.Status == domain.ChannelMemberActive && member.Status == domain.ChannelMemberKicked {
// Kicking in a megagroup produces a visible "X removed Y" service message and consumes channel pts;
// other members' member panel/count and offline diffs converge via it.
serviceMsg, serviceEvent, err = s.insertServiceMessage(ctx, tx, channel, req.UserID, req.Date, domain.ChannelMessageAction{
Type: domain.ChannelActionChatDelete,
UserIDs: []int64{req.Participant.ID},
})
if err != nil {
return domain.EditChannelBannedResult{}, err
}
channel.TopMessageID = serviceMsg.ID
channel.Pts = serviceEvent.Pts
}
if err := tx.Commit(ctx); err != nil {
return domain.EditChannelBannedResult{}, fmt.Errorf("commit edit channel banned: %w", err)
}
committed = true
recipients, _ := s.ListActiveChannelMemberIDs(ctx, req.UserID, req.ChannelID, 0)
recipients = append(recipients, req.Participant.ID)
return domain.EditChannelBannedResult{Channel: channel, Previous: previous, Participant: member, Event: event, Recipients: recipients, Date: req.Date, Message: serviceMsg, ServiceEvent: serviceEvent}, nil
}
func (s *ChannelStore) EditChannelDefaultBannedRights(ctx context.Context, req domain.EditChannelDefaultBannedRightsRequest) (domain.Channel, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
channel, actor, err := s.getChannelForMember(ctx, s.db, req.UserID, req.ChannelID)
if err != nil {
return domain.Channel{}, err
}
if !canBanChannelUsers(actor) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
if channel.DefaultBannedRights == req.BannedRights {
return domain.Channel{}, domain.ErrChannelNotModified
}
rights, err := marshalJSON(req.BannedRights, "{}")
if err != nil {
return domain.Channel{}, err
}
if _, err := s.db.Exec(ctx, `
UPDATE channels
SET default_banned_rights = $2::jsonb, updated_at = now()
WHERE id = $1 AND NOT deleted`, req.ChannelID, rights); err != nil {
return domain.Channel{}, fmt.Errorf("edit channel default banned rights: %w", err)
}
channel.DefaultBannedRights = req.BannedRights
return channel, nil
}
func (s *ChannelStore) ListAdminedPublicChannels(ctx context.Context, userID int64) ([]domain.Channel, error) {
if userID == 0 {
return nil, nil
}
rows, err := s.db.Query(ctx, `
SELECT channel_id
FROM user_channel_member_index
WHERE user_id = $1
AND status = 'active'
AND role IN ('creator','admin')
AND public_username
AND NOT deleted
ORDER BY channel_id DESC
LIMIT $2`, userID, domain.MaxAdminedPublicChannels)
if err != nil {
return nil, fmt.Errorf("list admined public channels: %w", err)
}
defer rows.Close()
ids := make([]int64, 0, domain.MaxAdminedPublicChannels)
for rows.Next() {
var channelID int64
if err := rows.Scan(&channelID); err != nil {
return nil, err
}
ids = append(ids, channelID)
}
if err := rows.Err(); err != nil {
return nil, err
}
channels, err := listChannelsByIDsInOrder(ctx, s.db, ids)
if err != nil {
return nil, fmt.Errorf("list admined public channel details: %w", err)
}
out := make([]domain.Channel, 0, len(channels))
for _, channel := range channels {
if channel.Username == "" || channel.Deleted {
continue
}
out = append(out, channel)
}
return out, nil
}
func (s *ChannelStore) ListCommunityLinkableChannels(ctx context.Context, userID int64) ([]domain.Channel, error) {
if userID == 0 {
return nil, nil
}
rows, err := s.db.Query(ctx, `
SELECT i.channel_id
FROM user_channel_member_index i
JOIN channels c ON c.id=i.channel_id
WHERE i.user_id=$1
AND i.status='active'
AND i.role IN ('creator','admin')
AND NOT i.deleted
AND NOT c.monoforum
AND c.linked_community_id=0
ORDER BY i.channel_id DESC
LIMIT $2`, userID, domain.MaxCommunityPeers)
if err != nil {
return nil, fmt.Errorf("list community-linkable channels: %w", err)
}
defer rows.Close()
ids := make([]int64, 0, domain.MaxCommunityPeers)
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
ids = append(ids, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return listChannelsByIDsInOrder(ctx, s.db, ids)
}
// ListSendAsChannels lists the broadcast channels a user may post messages AS in groups
// (channels.getSendAs candidates): channels where the user is the creator, or an admin holding
// PostMessages rights. Mirrors ListStoryPostableChannels but is restricted to broadcast channels
// and the PostMessages right (sending as an owned megagroup is not a real Telegram capability).
func (s *ChannelStore) ListSendAsChannels(ctx context.Context, userID int64) ([]domain.Channel, error) {
if userID == 0 {
return nil, nil
}
rows, err := s.db.Query(ctx, `
SELECT i.channel_id
FROM user_channel_member_index i
JOIN channel_members m ON m.channel_id = i.channel_id AND m.user_id = i.user_id
WHERE i.user_id = $1
AND i.status = 'active'
AND i.role IN ('creator', 'admin')
AND i.broadcast
AND NOT i.deleted
AND (
i.role = 'creator'
OR COALESCE((m.admin_rights->>'PostMessages')::boolean, false)
)
ORDER BY i.channel_id DESC
LIMIT $2`, userID, domain.MaxSendAsChannels)
if err != nil {
return nil, fmt.Errorf("list send-as channels: %w", err)
}
defer rows.Close()
ids := make([]int64, 0, domain.MaxSendAsChannels)
for rows.Next() {
var channelID int64
if err := rows.Scan(&channelID); err != nil {
return nil, err
}
ids = append(ids, channelID)
}
if err := rows.Err(); err != nil {
return nil, err
}
channels, err := listChannelsByIDsInOrder(ctx, s.db, ids)
if err != nil {
return nil, fmt.Errorf("list send-as channel details: %w", err)
}
out := make([]domain.Channel, 0, len(channels))
for _, channel := range channels {
if channel.Deleted || !channel.Broadcast {
continue
}
out = append(out, channel)
}
return out, nil
}
func (s *ChannelStore) ListStoryPostableChannels(ctx context.Context, userID int64) ([]domain.Channel, error) {
if userID == 0 {
return nil, nil
}
rows, err := s.db.Query(ctx, `
SELECT i.channel_id
FROM user_channel_member_index i
JOIN channel_members m ON m.channel_id = i.channel_id AND m.user_id = i.user_id
WHERE i.user_id = $1
AND i.status = 'active'
AND i.role IN ('creator', 'admin')
AND (i.megagroup OR i.broadcast)
AND NOT i.deleted
AND (
i.role = 'creator'
OR COALESCE((m.admin_rights->>'PostStories')::boolean, false)
)
ORDER BY i.channel_id DESC
LIMIT $2`, userID, domain.MaxStorySendAsChannels)
if err != nil {
return nil, fmt.Errorf("list story postable channels: %w", err)
}
defer rows.Close()
ids := make([]int64, 0, domain.MaxStorySendAsChannels)
for rows.Next() {
var channelID int64
if err := rows.Scan(&channelID); err != nil {
return nil, err
}
ids = append(ids, channelID)
}
if err := rows.Err(); err != nil {
return nil, err
}
channels, err := listChannelsByIDsInOrder(ctx, s.db, ids)
if err != nil {
return nil, fmt.Errorf("list story postable channel details: %w", err)
}
out := make([]domain.Channel, 0, len(channels))
for _, channel := range channels {
if channel.Deleted || (!channel.Broadcast && !channel.Megagroup) {
continue
}
out = append(out, channel)
}
return out, nil
}