1015 lines
33 KiB
Go
1015 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
|
|
}
|
|
if channel.Monoforum {
|
|
return domain.CreateChannelResult{}, domain.ErrChannelMonoforumUnsupported
|
|
}
|
|
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
|
|
}
|