owpengram-server/internal/store/memory/channel_invites.go
2026-07-13 10:11:29 +03:00

1012 lines
33 KiB
Go

package memory
import (
"context"
"crypto/rand"
"encoding/base64"
"sort"
"strings"
"telesrv/internal/domain"
"time"
)
func (s *ChannelStore) InviteToChannel(_ context.Context, channelID, inviterUserID int64, userIDs []int64, date int) (domain.CreateChannelResult, error) {
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(inviterUserID, channelID)
if err != nil {
return domain.CreateChannelResult{}, err
}
inviter := s.members[channelID][inviterUserID]
if !canInviteToChannel(channel, inviter) {
return domain.CreateChannelResult{}, domain.ErrChannelAdminRequired
}
requested := uniqueNonZero(userIDs, 0)
inviteOne := len(requested) == 1
canRestoreKicked := canBanChannelUsers(inviter)
added := make([]int64, 0, len(requested))
members := make([]domain.ChannelMember, 0, len(requested))
restoredKicked := 0
for _, userID := range requested {
if existing, ok := s.members[channelID][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,
}
s.members[channelID][userID] = member
members = append(members, member)
added = append(added, userID)
channel.ParticipantsCount++
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: inviterUserID,
Date: date,
Type: domain.ChannelAdminLogParticipantInvite,
Participant: ptrChannelMember(member),
})
}
if restoredKicked > 0 {
channel.KickedCount = maxInt(channel.KickedCount-restoredKicked, 0)
}
s.channels[channelID] = channel
var msg domain.ChannelMessage
var event domain.ChannelUpdateEvent
if len(added) > 0 && channel.Megagroup {
msg, event = s.appendChannelServiceMessageLocked(channelID, inviterUserID, date, domain.ChannelMessageAction{
Type: domain.ChannelActionChatAddUser,
UserIDs: append([]int64(nil), added...),
})
channel.TopMessageID = msg.ID
channel.Pts = event.Pts
s.channels[channelID] = channel
}
for _, member := range members {
s.upsertChannelDialogLocked(member.UserID, channel, msg, false)
}
return domain.CreateChannelResult{
Channel: channel,
Members: cloneChannelMembers(members),
Message: cloneChannelMessage(msg),
Event: cloneChannelEvent(event),
Recipients: s.activeMemberIDsLocked(channelID, 0, 0),
}, nil
}
func (s *ChannelStore) ListAdminLog(_ context.Context, req domain.ChannelAdminLogRequest) (domain.ChannelAdminLogResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.MaxID < 0 || req.MinID < 0 {
return domain.ChannelAdminLogResult{}, domain.ErrChannelInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelAdminLogResult{}, err
}
if !isChannelAdmin(s.members[req.ChannelID][req.UserID]) {
return domain.ChannelAdminLogResult{}, domain.ErrChannelAdminRequired
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelAdminLogLimit {
limit = domain.MaxChannelAdminLogLimit
}
admins := int64Set(req.AdminUserIDs)
query := strings.ToLower(strings.TrimSpace(req.Query))
out := make([]domain.ChannelAdminLogEvent, 0, limit)
events := s.adminLogs[req.ChannelID]
for i := len(events) - 1; i >= 0 && len(out) < limit; i-- {
event := events[i]
if req.MaxID > 0 && event.ID >= req.MaxID {
continue
}
if req.MinID > 0 && event.ID <= req.MinID {
continue
}
if len(admins) > 0 {
if _, ok := admins[event.UserID]; !ok {
continue
}
}
if !adminLogEventMatchesFilter(event.Type, req.Filter) {
continue
}
if query != "" && !adminLogEventMatchesQuery(event, query) {
continue
}
out = append(out, cloneChannelAdminLogEvent(event))
}
return domain.ChannelAdminLogResult{Channel: channel, Events: out}, nil
}
func (s *ChannelStore) ExportInvite(_ context.Context, req domain.ExportChannelInviteRequest) (domain.ExportChannelInviteResult, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.ExportChannelInviteResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ExportChannelInviteResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canExportChannelInvite(member) {
return domain.ExportChannelInviteResult{}, domain.ErrChannelAdminRequired
}
if req.LegacyRevokePermanent {
for hash, invite := range s.invites {
if invite.ChannelID == req.ChannelID && invite.AdminUserID == req.UserID && invite.Permanent {
invite.Revoked = true
s.invites[hash] = invite
}
}
}
inviteID, err := randomMemoryPositiveInt64()
if err != nil {
return domain.ExportChannelInviteResult{}, err
}
hash, err := randomMemoryInviteHash()
if err != nil {
return domain.ExportChannelInviteResult{}, err
}
invite := domain.ChannelInvite{
ChannelID: req.ChannelID,
InviteID: inviteID,
Hash: hash,
AdminUserID: req.UserID,
Title: req.Title,
Permanent: req.ExpireDate == 0 && req.UsageLimit == 0 && !req.RequestNeeded && req.Title == "",
RequestNeeded: req.RequestNeeded,
ExpireDate: req.ExpireDate,
UsageLimit: req.UsageLimit,
Date: req.Date,
}
s.invites[hash] = invite
channel = s.refreshChannelHasLinkLocked(req.ChannelID)
return domain.ExportChannelInviteResult{Channel: channel, Invite: invite}, nil
}
// EnsurePermanentInvite 幂等返回 (channel, admin) 当前未撤销的永久邀请;缺失则创建。
// 与 postgres 实现语义对齐(官方语义:邀请权限管理员必有主链接)。
func (s *ChannelStore) EnsurePermanentInvite(_ context.Context, channelID, adminUserID int64, date int) (domain.ChannelInvite, error) {
if channelID == 0 || adminUserID == 0 {
return domain.ChannelInvite{}, domain.ErrChannelInvalid
}
if date == 0 {
date = int(time.Now().Unix())
}
s.mu.Lock()
defer s.mu.Unlock()
if _, err := s.channelForMemberLocked(adminUserID, channelID); err != nil {
return domain.ChannelInvite{}, err
}
member := s.members[channelID][adminUserID]
if !canExportChannelInvite(member) {
return domain.ChannelInvite{}, domain.ErrChannelAdminRequired
}
var existing *domain.ChannelInvite
for hash := range s.invites {
invite := s.invites[hash]
if invite.ChannelID != channelID || invite.AdminUserID != adminUserID || !invite.Permanent || invite.Revoked {
continue
}
// map 遍历无序:取最早创建的一条,对齐 postgres ORDER BY created_at ASC。
if existing == nil || invite.Date < existing.Date || (invite.Date == existing.Date && invite.Hash < existing.Hash) {
copied := invite
existing = &copied
}
}
if existing != nil {
s.setChannelHasLinkLocked(channelID, true)
return *existing, nil
}
inviteID, err := randomMemoryPositiveInt64()
if err != nil {
return domain.ChannelInvite{}, err
}
hash, err := randomMemoryInviteHash()
if err != nil {
return domain.ChannelInvite{}, err
}
invite := domain.ChannelInvite{
ChannelID: channelID,
InviteID: inviteID,
Hash: hash,
AdminUserID: adminUserID,
Permanent: true,
Date: date,
}
s.invites[hash] = invite
s.setChannelHasLinkLocked(channelID, true)
return invite, nil
}
func (s *ChannelStore) CheckInvite(_ context.Context, userID int64, hash string, date int) (domain.CheckChannelInviteResult, error) {
if userID == 0 || strings.TrimSpace(hash) == "" {
return domain.CheckChannelInviteResult{}, domain.ErrInviteHashEmpty
}
s.mu.RLock()
defer s.mu.RUnlock()
invite, ok := s.invites[strings.TrimSpace(hash)]
if !ok || invite.Revoked {
return domain.CheckChannelInviteResult{}, domain.ErrInviteHashInvalid
}
if invite.ExpireDate > 0 && invite.ExpireDate < date {
return domain.CheckChannelInviteResult{}, domain.ErrInviteHashExpired
}
channel, ok := s.channels[invite.ChannelID]
if !ok || channel.Deleted {
return domain.CheckChannelInviteResult{}, domain.ErrInviteHashInvalid
}
member := s.members[invite.ChannelID][userID]
if member.Status == domain.ChannelMemberKicked || member.Status == domain.ChannelMemberBanned || member.BannedRights.ViewMessages {
return domain.CheckChannelInviteResult{}, domain.ErrInviteHashInvalid
}
return domain.CheckChannelInviteResult{
Channel: channel,
Invite: invite,
Already: member.Status == domain.ChannelMemberActive,
Self: member,
}, nil
}
// ResolvePublicChannelInvite is the anonymous, unauthenticated counterpart of
// CheckInvite: no userID/membership lookup, just "is this hash currently
// joinable" plus the public channel projection for the invite preview page.
func (s *ChannelStore) ResolvePublicChannelInvite(_ context.Context, hash string) (domain.Channel, domain.ChannelInvite, bool, error) {
hash = strings.TrimSpace(hash)
if hash == "" {
return domain.Channel{}, domain.ChannelInvite{}, false, nil
}
s.mu.RLock()
defer s.mu.RUnlock()
invite, ok := s.invites[hash]
if !ok || invite.Revoked {
return domain.Channel{}, domain.ChannelInvite{}, false, nil
}
if invite.ExpireDate > 0 && invite.ExpireDate < int(time.Now().Unix()) {
return domain.Channel{}, domain.ChannelInvite{}, false, nil
}
channel, ok := s.channels[invite.ChannelID]
if !ok || channel.Deleted {
return domain.Channel{}, domain.ChannelInvite{}, false, nil
}
return channel, invite, true, nil
}
func (s *ChannelStore) ImportInvite(_ context.Context, req domain.ImportChannelInviteRequest) (domain.CreateChannelResult, error) {
if req.UserID == 0 || strings.TrimSpace(req.Hash) == "" {
return domain.CreateChannelResult{}, domain.ErrInviteHashEmpty
}
s.mu.Lock()
defer s.mu.Unlock()
invite, ok := s.invites[strings.TrimSpace(req.Hash)]
if !ok || invite.Revoked {
return domain.CreateChannelResult{}, domain.ErrInviteHashInvalid
}
if invite.ExpireDate > 0 && invite.ExpireDate < req.Date {
return domain.CreateChannelResult{}, domain.ErrInviteHashExpired
}
channel, ok := s.channels[invite.ChannelID]
if !ok || channel.Deleted {
return domain.CreateChannelResult{}, domain.ErrInviteHashInvalid
}
if invite.RequestNeeded {
if err := s.recordPendingInviteRequestLocked(invite, req.UserID, req.Date); err != nil {
return domain.CreateChannelResult{}, err
}
return domain.CreateChannelResult{Channel: channel}, domain.ErrInviteRequestSent
}
return s.approveInviteImporterLocked(channel, invite, req.UserID, 0, req.Date)
}
func (s *ChannelStore) ListExportedInvites(_ context.Context, req domain.ChannelInviteListRequest) (domain.ChannelInviteList, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.AdminUserID == 0 {
return domain.ChannelInviteList{}, domain.ErrChannelInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
if _, err := s.channelForMemberLocked(req.UserID, req.ChannelID); err != nil {
return domain.ChannelInviteList{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canExportChannelInvite(member) {
return domain.ChannelInviteList{}, domain.ErrChannelAdminRequired
}
all := make([]domain.ChannelInvite, 0)
for _, invite := range s.invites {
if invite.ChannelID == req.ChannelID && invite.AdminUserID == req.AdminUserID && invite.Revoked == req.Revoked {
all = append(all, invite)
}
}
sort.Slice(all, func(i, j int) bool {
if all[i].Date != all[j].Date {
return all[i].Date > all[j].Date
}
return all[i].Hash > all[j].Hash
})
total := len(all)
start := 0
if req.OffsetDate > 0 || req.OffsetHash != "" {
start = len(all)
for i, invite := range all {
if invite.Date == req.OffsetDate && invite.Hash == req.OffsetHash {
start = i + 1
break
}
}
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelInviteListLimit {
limit = domain.MaxChannelInviteListLimit
}
if start > len(all) {
start = len(all)
}
end := start + limit
if end > len(all) {
end = len(all)
}
return domain.ChannelInviteList{Count: total, Invites: cloneChannelInvites(all[start:end])}, nil
}
func (s *ChannelStore) permanentInviteForAdminLocked(channelID, adminUserID int64) (domain.ChannelInvite, bool) {
var (
best domain.ChannelInvite
ok bool
)
for _, invite := range s.invites {
if invite.ChannelID != channelID || invite.AdminUserID != adminUserID || !invite.Permanent || invite.Revoked {
continue
}
if !ok || invite.Date < best.Date || (invite.Date == best.Date && invite.Hash < best.Hash) {
best = invite
ok = true
}
}
return best, ok
}
func (s *ChannelStore) GetExportedInvite(_ context.Context, req domain.GetChannelInviteRequest) (domain.ChannelInvite, error) {
if req.UserID == 0 || req.ChannelID == 0 || strings.TrimSpace(req.Hash) == "" {
return domain.ChannelInvite{}, domain.ErrInviteHashEmpty
}
s.mu.RLock()
defer s.mu.RUnlock()
if _, err := s.channelForMemberLocked(req.UserID, req.ChannelID); err != nil {
return domain.ChannelInvite{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canExportChannelInvite(member) {
return domain.ChannelInvite{}, domain.ErrChannelAdminRequired
}
return s.inviteByChannelHashLocked(req.ChannelID, req.Hash)
}
func (s *ChannelStore) EditExportedInvite(_ context.Context, req domain.EditChannelInviteRequest) (domain.EditChannelInviteResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || strings.TrimSpace(req.Hash) == "" {
return domain.EditChannelInviteResult{}, domain.ErrInviteHashEmpty
}
s.mu.Lock()
defer s.mu.Unlock()
if _, err := s.channelForMemberLocked(req.UserID, req.ChannelID); err != nil {
return domain.EditChannelInviteResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canExportChannelInvite(member) {
return domain.EditChannelInviteResult{}, domain.ErrChannelAdminRequired
}
invite, err := s.inviteByChannelHashLocked(req.ChannelID, req.Hash)
if err != nil {
return domain.EditChannelInviteResult{}, err
}
if req.Revoked {
if invite.Revoked {
return domain.EditChannelInviteResult{}, domain.ErrInviteRevokedMissing
}
invite.Revoked = true
s.invites[invite.Hash] = invite
if !invite.Permanent {
s.refreshChannelHasLinkLocked(req.ChannelID)
return domain.EditChannelInviteResult{Invite: invite}, nil
}
newInvite, err := s.newReplacementInviteLocked(invite, req.Date)
if err != nil {
return domain.EditChannelInviteResult{}, err
}
s.invites[newInvite.Hash] = newInvite
s.refreshChannelHasLinkLocked(req.ChannelID)
return domain.EditChannelInviteResult{Invite: invite, NewInvite: &newInvite}, nil
}
if invite.Permanent && ((req.HasExpireDate && req.ExpireDate > 0) || (req.HasUsageLimit && req.UsageLimit > 0) || (req.HasRequestNeeded && req.RequestNeeded)) {
return domain.EditChannelInviteResult{}, domain.ErrInvitePermanent
}
if req.HasExpireDate {
invite.ExpireDate = req.ExpireDate
}
if req.HasUsageLimit {
invite.UsageLimit = req.UsageLimit
}
if req.HasRequestNeeded {
invite.RequestNeeded = req.RequestNeeded
}
if req.HasTitle {
invite.Title = req.Title
}
invite.Permanent = invite.ExpireDate == 0 && invite.UsageLimit == 0 && !invite.RequestNeeded && invite.Title == ""
s.invites[invite.Hash] = invite
s.refreshChannelHasLinkLocked(req.ChannelID)
return domain.EditChannelInviteResult{Invite: invite}, nil
}
func (s *ChannelStore) DeleteExportedInvite(_ context.Context, req domain.DeleteChannelInviteRequest) error {
if req.UserID == 0 || req.ChannelID == 0 || strings.TrimSpace(req.Hash) == "" {
return domain.ErrInviteHashEmpty
}
s.mu.Lock()
defer s.mu.Unlock()
if _, err := s.channelForMemberLocked(req.UserID, req.ChannelID); err != nil {
return err
}
member := s.members[req.ChannelID][req.UserID]
if !canExportChannelInvite(member) {
return domain.ErrChannelAdminRequired
}
invite, err := s.inviteByChannelHashLocked(req.ChannelID, req.Hash)
if err != nil {
return err
}
delete(s.invites, invite.Hash)
s.refreshChannelHasLinkLocked(req.ChannelID)
return nil
}
func (s *ChannelStore) DeleteRevokedExportedInvites(_ context.Context, req domain.DeleteRevokedChannelInvitesRequest) error {
if req.UserID == 0 || req.ChannelID == 0 || req.AdminUserID == 0 {
return domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
if _, err := s.channelForMemberLocked(req.UserID, req.ChannelID); err != nil {
return err
}
member := s.members[req.ChannelID][req.UserID]
if !canExportChannelInvite(member) {
return domain.ErrChannelAdminRequired
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelHideJoinRequests {
limit = domain.MaxChannelHideJoinRequests
}
deleted := 0
for hash, invite := range s.invites {
if invite.ChannelID == req.ChannelID && invite.AdminUserID == req.AdminUserID && invite.Revoked {
delete(s.invites, hash)
deleted++
if deleted >= limit {
break
}
}
}
return nil
}
func (s *ChannelStore) ListAdminsWithInvites(_ context.Context, userID, channelID int64) ([]domain.ChannelAdminInviteCount, error) {
if userID == 0 || channelID == 0 {
return nil, domain.ErrChannelInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
if _, err := s.channelForMemberLocked(userID, channelID); err != nil {
return nil, err
}
member := s.members[channelID][userID]
if !canExportChannelInvite(member) {
return nil, domain.ErrChannelAdminRequired
}
byAdmin := map[int64]*domain.ChannelAdminInviteCount{}
for _, invite := range s.invites {
if invite.ChannelID != channelID {
continue
}
count := byAdmin[invite.AdminUserID]
if count == nil {
count = &domain.ChannelAdminInviteCount{AdminUserID: invite.AdminUserID}
byAdmin[invite.AdminUserID] = count
}
if invite.Revoked {
count.RevokedInvitesCount++
} else {
count.InvitesCount++
}
}
out := make([]domain.ChannelAdminInviteCount, 0, len(byAdmin))
for _, count := range byAdmin {
out = append(out, *count)
}
sort.Slice(out, func(i, j int) bool { return out[i].AdminUserID < out[j].AdminUserID })
return out, nil
}
func (s *ChannelStore) ListInviteImporters(_ context.Context, req domain.ChannelInviteImportersRequest) (domain.ChannelInviteImporterList, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.ChannelInviteImporterList{}, domain.ErrChannelInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
if _, err := s.channelForMemberLocked(req.UserID, req.ChannelID); err != nil {
return domain.ChannelInviteImporterList{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canExportChannelInvite(member) {
return domain.ChannelInviteImporterList{}, domain.ErrChannelAdminRequired
}
var inviteID int64
if req.Hash != "" {
invite, err := s.inviteByChannelHashLocked(req.ChannelID, req.Hash)
if err != nil {
return domain.ChannelInviteImporterList{}, err
}
inviteID = invite.InviteID
}
if req.Query != "" {
return domain.ChannelInviteImporterList{}, nil
}
all := make([]domain.ChannelInviteImporter, 0)
for _, importer := range s.importers[req.ChannelID] {
if importer.Requested != req.Requested {
continue
}
if inviteID != 0 && importer.InviteID != inviteID {
continue
}
all = append(all, importer)
}
sort.Slice(all, func(i, j int) bool {
if all[i].Date != all[j].Date {
return all[i].Date > all[j].Date
}
return all[i].UserID > all[j].UserID
})
total := len(all)
start := 0
if req.OffsetDate > 0 || req.OffsetUserID != 0 {
start = len(all)
for i, importer := range all {
if importer.Date == req.OffsetDate && importer.UserID == req.OffsetUserID {
start = i + 1
break
}
}
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelInviteListLimit {
limit = domain.MaxChannelInviteListLimit
}
if start > len(all) {
start = len(all)
}
end := start + limit
if end > len(all) {
end = len(all)
}
return domain.ChannelInviteImporterList{Count: total, Importers: cloneChannelInviteImporters(all[start:end])}, nil
}
func (s *ChannelStore) PendingJoinRequests(_ context.Context, channelID int64, limit int) (domain.ChannelPendingJoinRequests, error) {
if channelID == 0 {
return domain.ChannelPendingJoinRequests{}, domain.ErrChannelInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
return domain.ChannelPendingJoinRequests{}, domain.ErrChannelInvalid
}
all := make([]domain.ChannelInviteImporter, 0)
for _, importer := range s.importers[channelID] {
if importer.Requested {
all = append(all, importer)
}
}
sort.Slice(all, func(i, j int) bool {
if all[i].Date != all[j].Date {
return all[i].Date > all[j].Date
}
return all[i].UserID > all[j].UserID
})
if limit <= 0 || limit > domain.MaxChannelPendingJoinRecentRequesters {
limit = domain.MaxChannelPendingJoinRecentRequesters
}
if len(all) < limit {
limit = len(all)
}
recent := make([]int64, 0, limit)
for _, importer := range all[:limit] {
recent = append(recent, importer.UserID)
}
return domain.ChannelPendingJoinRequests{
ChannelID: channelID,
Count: len(all),
RecentRequesters: recent,
}, nil
}
func (s *ChannelStore) HideAllChatJoinRequests(_ context.Context, req domain.HideChannelJoinRequestsRequest) (domain.CreateChannelResult, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.CreateChannelResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.CreateChannelResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canExportChannelInvite(member) {
return domain.CreateChannelResult{}, domain.ErrChannelAdminRequired
}
var inviteID int64
if req.Hash != "" {
invite, err := s.inviteByChannelHashLocked(req.ChannelID, req.Hash)
if err != nil {
return domain.CreateChannelResult{}, err
}
inviteID = invite.InviteID
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelHideJoinRequests {
limit = domain.MaxChannelHideJoinRequests
}
targets := make([]domain.ChannelInviteImporter, 0, limit)
for _, importer := range s.importers[req.ChannelID] {
if !importer.Requested {
continue
}
if inviteID != 0 && importer.InviteID != inviteID {
continue
}
targets = append(targets, importer)
if len(targets) >= limit {
break
}
}
var result domain.CreateChannelResult
for _, importer := range targets {
invite := domain.ChannelInvite{ChannelID: req.ChannelID, AdminUserID: req.UserID}
if importer.InviteID != 0 {
var err error
invite, err = s.inviteByIDLocked(req.ChannelID, importer.InviteID)
if err != nil {
return domain.CreateChannelResult{}, err
}
}
if !req.Approved {
s.deletePendingInviteImporterLocked(invite, importer.UserID)
result = domain.CreateChannelResult{Channel: channel, Recipients: s.activeMemberIDsLocked(req.ChannelID, importer.UserID, 0)}
continue
}
result, err = s.approveInviteImporterLocked(channel, invite, importer.UserID, req.UserID, req.Date)
if err != nil {
return domain.CreateChannelResult{}, err
}
channel = result.Channel
}
if result.Channel.ID == 0 {
result = domain.CreateChannelResult{Channel: channel, Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0)}
}
return result, nil
}
func (s *ChannelStore) approveInviteImporterLocked(channel domain.Channel, invite domain.ChannelInvite, userID, approvedBy int64, date int) (domain.CreateChannelResult, error) {
if invite.InviteID != 0 && invite.UsageLimit > 0 && invite.UsageCount >= invite.UsageLimit {
return domain.CreateChannelResult{}, domain.ErrUsersTooMuch
}
channelID := channel.ID
if channelID == 0 {
channelID = invite.ChannelID
}
if existing, ok := s.members[channelID][userID]; ok {
if existing.Status == domain.ChannelMemberActive {
return domain.CreateChannelResult{}, domain.ErrUserAlreadyParticipant
}
if existing.Status == domain.ChannelMemberKicked || existing.Status == domain.ChannelMemberBanned || existing.BannedRights.ViewMessages {
return domain.CreateChannelResult{}, domain.ErrInviteHashInvalid
}
}
preJoinTopID := channel.TopMessageID
minID := channelInitialAvailableMinID(channel)
inviterID := invite.AdminUserID
if inviterID == 0 {
inviterID = approvedBy
}
member := domain.ChannelMember{
ChannelID: channelID,
UserID: userID,
InviterUserID: inviterID,
Role: domain.ChannelRoleMember,
Status: domain.ChannelMemberActive,
JoinedAt: date,
AvailableMinID: minID,
AvailableMinPts: channelInitialAvailableMinPts(channel),
ReadInboxMaxID: maxInt(minID, preJoinTopID),
}
if s.members[channelID] == nil {
s.members[channelID] = make(map[int64]domain.ChannelMember)
}
s.members[channelID][userID] = member
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: userID,
Date: date,
Type: domain.ChannelAdminLogParticipantJoin,
})
if importer, ok := s.importers[channelID][userID]; ok && importer.Requested {
if importer.InviteID == invite.InviteID {
if invite.InviteID != 0 && invite.RequestedCount > 0 {
invite.RequestedCount--
}
} else if importer.InviteID != 0 {
if pendingInvite, err := s.inviteByIDLocked(channelID, importer.InviteID); err == nil && pendingInvite.RequestedCount > 0 {
pendingInvite.RequestedCount--
s.invites[pendingInvite.Hash] = pendingInvite
}
}
}
if invite.InviteID != 0 && invite.Hash != "" {
invite.UsageCount++
s.invites[invite.Hash] = invite
}
s.refreshChannelCountsLocked(channelID)
channel = s.channels[channelID]
var msg domain.ChannelMessage
var event domain.ChannelUpdateEvent
if channel.Megagroup {
msg, event = s.appendChannelServiceMessageLocked(channelID, userID, date, domain.ChannelMessageAction{
Type: domain.ChannelActionChatJoinedByLink,
InviterUserID: invite.AdminUserID,
UserIDs: []int64{userID},
})
channel.TopMessageID = msg.ID
channel.Pts = event.Pts
s.channels[channelID] = channel
}
member.ReadInboxMaxID = maxInt(member.ReadInboxMaxID, channel.TopMessageID)
if msg.ID != 0 {
member.ReadOutboxMaxID = maxInt(member.ReadOutboxMaxID, msg.ID)
}
s.members[channelID][userID] = member
s.upsertChannelDialogLocked(userID, channel, msg, true)
if s.importers[channelID] == nil {
s.importers[channelID] = make(map[int64]domain.ChannelInviteImporter)
}
s.importers[channelID][userID] = domain.ChannelInviteImporter{
ChannelID: channelID,
InviteID: invite.InviteID,
UserID: userID,
Date: date,
ApprovedBy: approvedBy,
}
return domain.CreateChannelResult{
Channel: channel,
Members: []domain.ChannelMember{member},
Message: cloneChannelMessage(msg),
Event: cloneChannelEvent(event),
Recipients: s.activeMemberIDsLocked(channelID, 0, 0),
}, nil
}
func (s *ChannelStore) recordPendingInviteRequestLocked(invite domain.ChannelInvite, userID int64, date int) error {
if existing, ok := s.members[invite.ChannelID][userID]; ok {
if existing.Status == domain.ChannelMemberActive {
return domain.ErrUserAlreadyParticipant
}
if existing.Status == domain.ChannelMemberKicked || existing.Status == domain.ChannelMemberBanned || existing.BannedRights.ViewMessages {
return domain.ErrInviteHashInvalid
}
}
if s.importers[invite.ChannelID] == nil {
s.importers[invite.ChannelID] = make(map[int64]domain.ChannelInviteImporter)
}
if existing, ok := s.importers[invite.ChannelID][userID]; ok && existing.Requested {
return domain.ErrInviteRequestSent
}
s.importers[invite.ChannelID][userID] = domain.ChannelInviteImporter{
ChannelID: invite.ChannelID,
InviteID: invite.InviteID,
UserID: userID,
Date: date,
Requested: true,
}
invite.RequestedCount++
s.invites[invite.Hash] = invite
return nil
}
func (s *ChannelStore) deletePendingInviteImporterLocked(invite domain.ChannelInvite, userID int64) {
if existing, ok := s.importers[invite.ChannelID][userID]; ok && existing.Requested {
delete(s.importers[invite.ChannelID], userID)
if invite.InviteID != 0 && invite.Hash != "" && invite.RequestedCount > 0 {
invite.RequestedCount--
s.invites[invite.Hash] = invite
}
}
}
func (s *ChannelStore) newReplacementInviteLocked(old domain.ChannelInvite, date int) (domain.ChannelInvite, error) {
inviteID, err := randomMemoryPositiveInt64()
if err != nil {
return domain.ChannelInvite{}, err
}
hash, err := randomMemoryInviteHash()
if err != nil {
return domain.ChannelInvite{}, err
}
if date == 0 {
date = int(time.Now().Unix())
}
return domain.ChannelInvite{
ChannelID: old.ChannelID,
InviteID: inviteID,
Hash: hash,
AdminUserID: old.AdminUserID,
Permanent: old.Permanent,
Date: date,
}, nil
}
func (s *ChannelStore) setChannelHasLinkLocked(channelID int64, hasLink bool) {
channel, ok := s.channels[channelID]
if !ok {
return
}
channel.HasLink = hasLink
s.channels[channelID] = channel
}
func (s *ChannelStore) refreshChannelHasLinkLocked(channelID int64) domain.Channel {
channel, ok := s.channels[channelID]
if !ok {
return domain.Channel{}
}
channel.HasLink = s.channelHasNonRevokedInviteLocked(channelID)
s.channels[channelID] = channel
return channel
}
func (s *ChannelStore) channelHasNonRevokedInviteLocked(channelID int64) bool {
for _, invite := range s.invites {
if invite.ChannelID == channelID && !invite.Revoked {
return true
}
}
return false
}
func cloneChannelInvites(in []domain.ChannelInvite) []domain.ChannelInvite {
if len(in) == 0 {
return nil
}
out := make([]domain.ChannelInvite, len(in))
copy(out, in)
return out
}
func cloneChannelInviteImporters(in []domain.ChannelInviteImporter) []domain.ChannelInviteImporter {
if len(in) == 0 {
return nil
}
out := make([]domain.ChannelInviteImporter, len(in))
copy(out, in)
return out
}
func (s *ChannelStore) ListChannelInviteAdminMemberIDs(_ context.Context, channelID int64, limit int) ([]int64, error) {
s.mu.RLock()
defer s.mu.RUnlock()
channel, ok := s.channels[channelID]
if channelID == 0 || !ok || channel.Deleted {
return nil, domain.ErrChannelInvalid
}
if limit <= 0 || limit > domain.MaxChannelRealtimeFanout {
limit = domain.MaxChannelRealtimeFanout
}
members := s.members[channelID]
out := make([]int64, 0, minInt(len(members), limit))
for _, member := range members {
if member.Status != domain.ChannelMemberActive {
continue
}
if member.Role == domain.ChannelRoleCreator {
out = append(out, member.UserID)
if len(out) >= limit {
break
}
continue
}
if member.Role == domain.ChannelRoleAdmin && (member.AdminRights.InviteUsers || member.AdminRights.ChangeInfo) {
out = append(out, member.UserID)
if len(out) >= limit {
break
}
}
}
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
return out, nil
}
func (s *ChannelStore) appendChannelAdminLogLocked(event domain.ChannelAdminLogEvent) {
if event.ChannelID == 0 || event.UserID == 0 || event.Type == "" {
return
}
s.logSeq[event.ChannelID]++
event.ID = s.logSeq[event.ChannelID]
event.Query = adminLogSearchText(event)
s.adminLogs[event.ChannelID] = append(s.adminLogs[event.ChannelID], cloneChannelAdminLogEvent(event))
}
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
}
func canExportChannelInvite(member domain.ChannelMember) bool {
return member.Role == domain.ChannelRoleCreator ||
(member.Role == domain.ChannelRoleAdmin && (member.AdminRights.InviteUsers || member.AdminRights.ChangeInfo))
}
func randomMemoryInviteHash() (string, error) {
var b [18]byte
if _, err := rand.Read(b[:]); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b[:]), nil
}
func cloneChannelAdminLogEvent(in domain.ChannelAdminLogEvent) domain.ChannelAdminLogEvent {
if in.PrevParticipant != nil {
in.PrevParticipant = ptrChannelMember(*in.PrevParticipant)
}
if in.NewParticipant != nil {
in.NewParticipant = ptrChannelMember(*in.NewParticipant)
}
if in.Participant != nil {
in.Participant = ptrChannelMember(*in.Participant)
}
if in.Message != nil {
in.Message = ptrChannelMessage(*in.Message)
}
if in.PrevMessage != nil {
in.PrevMessage = ptrChannelMessage(*in.PrevMessage)
}
if in.NewMessage != nil {
in.NewMessage = ptrChannelMessage(*in.NewMessage)
}
return in
}