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 } if actor.Role != domain.ChannelRoleCreator && !adminRightsSubset(req.AdminRights, actor.AdminRights) { return domain.EditChannelAdminResult{}, domain.ErrChannelRightForbidden } previous, err := s.getChannelMember(ctx, tx, req.ChannelID, req.MemberID) 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.Role == domain.ChannelRoleCreator { return domain.EditChannelAdminResult{}, domain.ErrChannelUserCreator } member := previous member.InviterUserID = req.UserID member.Status = domain.ChannelMemberActive member.LeftAt = 0 member.Rank = req.Rank if previous.Status != domain.ChannelMemberActive { if minPts := channelInitialAvailableMinPts(channel); minPts > member.AvailableMinPts { member.AvailableMinPts = minPts } } member.AdminRights = 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 } 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 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) 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 } } var serviceMsg domain.ChannelMessage var serviceEvent domain.ChannelUpdateEvent if channel.Megagroup && previous.Status == domain.ChannelMemberActive && member.Status == domain.ChannelMemberKicked { // megagroup 踢人产生可见 "X removed Y" 服务消息并占 channel pts, // 其它成员的成员面板/人数与离线差量都靠它收敛。 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 } // 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 }