Ctrl K
ComunidadGuías…

internal/store/postgres/channel_invite_members.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package postgres

import (
	"context"
	"fmt"
	"sort"
	"telesrv/internal/domain"
)

func (s *ChannelStore) InviteToChannel(ctx context.Context, channelID, inviterUserID int64, userIDs []int64, date int) (domain.CreateChannelResult, error) {
	if channelID == 0 || inviterUserID == 0 || len(userIDs) == 0 {
		return domain.CreateChannelResult{}, domain.ErrChannelInvalid
	}
	beginner, ok := s.db.(txBeginner)
	if !ok {
		return domain.CreateChannelResult{}, fmt.Errorf("invite channel: db does not support transactions")
	}
	tx, err := beginner.Begin(ctx)
	if err != nil {
		return domain.CreateChannelResult{}, fmt.Errorf("begin invite channel: %w", err)
	}
	committed := false
	defer func() {
		if !committed {
			_ = tx.Rollback(ctx)
		}
	}()
	channel, inviter, err := s.getChannelForMember(ctx, tx, inviterUserID, channelID)
	if err != nil {
		return domain.CreateChannelResult{}, err
	}
	if channel.Monoforum {
		return domain.CreateChannelResult{}, domain.ErrChannelMonoforumUnsupported
	}
	if !canInviteToChannel(channel, inviter) {
		return domain.CreateChannelResult{}, domain.ErrChannelAdminRequired
	}
	if date == 0 {
		date = nowUnix()
	}
	requested := uniqueChannelUserIDs(userIDs, 0)
	sort.Slice(requested, func(i, j int) bool { return requested[i] < requested[j] })
	inviteOne := len(requested) == 1
	canRestoreKicked := canBanChannelUsers(inviter)
	invitedIDs := make([]int64, 0, len(requested))
	members := make([]domain.ChannelMember, 0, len(requested))
	restoredKicked := 0
	existingMembers, err := channelMembersForUpdateBatchTx(ctx, tx, channelID, requested)
	if err != nil {
		return domain.CreateChannelResult{}, err
	}
	for _, userID := range requested {
		if existing, ok := existingMembers[userID]; ok {
			if existing.Status == domain.ChannelMemberActive {
				if inviteOne {
					return domain.CreateChannelResult{}, domain.ErrUserAlreadyParticipant
				}
				continue
			}
			if existing.Status == domain.ChannelMemberBanned || existing.Status == domain.ChannelMemberKicked || existing.BannedRights.ViewMessages {
				if !canRestoreKicked {
					if inviteOne {
						return domain.CreateChannelResult{}, domain.ErrUserKicked
					}
					continue
				}
				if existing.Status == domain.ChannelMemberKicked {
					restoredKicked++
				}
			}
		}
		member := domain.ChannelMember{
			ChannelID:       channelID,
			UserID:          userID,
			InviterUserID:   inviterUserID,
			Role:            domain.ChannelRoleMember,
			Status:          domain.ChannelMemberActive,
			JoinedAt:        date,
			AvailableMinID:  channelInitialAvailableMinID(channel),
			AvailableMinPts: channelInitialAvailableMinPts(channel),
			ReadInboxMaxID:  channel.TopMessageID,
		}
		members = append(members, member)
		invitedIDs = append(invitedIDs, userID)
	}
	if len(members) > 0 {
		if err := enableChannelMembershipBatchTx(ctx, tx); err != nil {
			return domain.CreateChannelResult{}, err
		}
		if err := upsertChannelMembersBatchTx(ctx, tx, channel, members); err != nil {
			return domain.CreateChannelResult{}, err
		}
		// Bulk invites may likewise include members "re-joining": return the purely prehistory-sourced read boundary for each
		// (the upsert is GREATEST and will not lower it), otherwise they permanently see an empty channel.
		if !channel.PreHistoryHidden {
			for _, m := range members {
				if err := resetPrehistoryBoundaryTx(ctx, tx, channel, m.UserID); err != nil {
					return domain.CreateChannelResult{}, err
				}
			}
		}
		if err := insertChannelInviteAdminLogsBatchTx(ctx, tx, channelID, inviterUserID, date, members); err != nil {
			return domain.CreateChannelResult{}, err
		}
		if _, err := tx.Exec(ctx, `UPDATE channels SET participants_count = participants_count + $2, kicked_count = GREATEST(kicked_count - $3, 0), updated_at = now() WHERE id = $1`, channelID, len(members), restoredKicked); err != nil {
			return domain.CreateChannelResult{}, fmt.Errorf("update channel participants: %w", err)
		}
		channel.ParticipantsCount += len(members)
		channel.KickedCount = maxInt(channel.KickedCount-restoredKicked, 0)
	}
	var msg domain.ChannelMessage
	var event domain.ChannelUpdateEvent
	if len(members) > 0 && channel.Megagroup {
		msg, event, err = s.insertServiceMessage(ctx, tx, channel, inviterUserID, date, domain.ChannelMessageAction{
			Type:    domain.ChannelActionChatAddUser,
			UserIDs: invitedIDs,
		})
		if err != nil {
			return domain.CreateChannelResult{}, err
		}
		channel.TopMessageID = msg.ID
		channel.Pts = event.Pts
	}
	if err := upsertChannelDialogsBatchTx(ctx, tx, channel, msg, members); err != nil {
		return domain.CreateChannelResult{}, err
	}
	// Being re-invited into the group is also a re-entry: recompute the unread reaction count by the new available_min_id set to clear the ghost badge.
	if err := refreshChannelUnreadReactionsCountsBatchTx(ctx, tx, channel.ID, invitedIDs); err != nil {
		return domain.CreateChannelResult{}, err
	}
	if err := enqueueWelcomeMessageDeliveriesTx(ctx, tx, channel.ID, members); err != nil {
		return domain.CreateChannelResult{}, err
	}
	if err := bumpChannelMembershipReadModelsBatchTx(ctx, tx, channel.ID, invitedIDs); err != nil {
		return domain.CreateChannelResult{}, err
	}
	if err := tx.Commit(ctx); err != nil {
		return domain.CreateChannelResult{}, fmt.Errorf("commit invite channel: %w", err)
	}
	committed = true
	return domain.CreateChannelResult{Channel: channel, Members: members, Message: msg, Event: event}, nil
}

func canInviteToChannel(channel domain.Channel, member domain.ChannelMember) bool {
	if member.Role == domain.ChannelRoleCreator ||
		(member.Role == domain.ChannelRoleAdmin && (member.AdminRights.InviteUsers || member.AdminRights.ChangeInfo)) {
		return true
	}
	return channel.Megagroup && !channel.DefaultBannedRights.InviteUsers && !member.BannedRights.InviteUsers
}