owpengram-server/internal/rpc/channels_settings.go

579 lines
20 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"errors"
"github.com/iamxvbaba/td/tg"
"strings"
"telesrv/internal/domain"
"unicode/utf8"
)
func (r *Router) onChannelsCheckUsername(ctx context.Context, req *tg.ChannelsCheckUsernameRequest) (bool, error) {
if r.deps.Channels == nil {
return false, notImplementedErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
if err != nil {
return false, err
}
okUsername, err := r.deps.Channels.CheckUsername(ctx, userID, channelID, req.Username)
if err != nil {
return false, channelUsernameErr(err)
}
return okUsername, nil
}
func (r *Router) onChannelsUpdateUsername(ctx context.Context, req *tg.ChannelsUpdateUsernameRequest) (bool, error) {
if r.deps.Channels == nil {
return false, notImplementedErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
if err != nil {
return false, err
}
channel, err := r.deps.Channels.UpdateUsername(ctx, userID, domain.UpdateChannelUsernameRequest{
UserID: userID,
ChannelID: channelID,
Username: req.Username,
})
if err != nil {
return false, channelUsernameErr(err)
}
r.invalidateRPCProjectionForChannel(channel.ID)
r.pushChannelStateToMembers(ctx, userID, channel)
return true, nil
}
func (r *Router) onChannelsToggleSignatures(ctx context.Context, req *tg.ChannelsToggleSignaturesRequest) (tg.UpdatesClass, error) {
return r.applyChannelAdminStateMutation(ctx, req.Channel, func(ctx context.Context, userID, channelID int64) (domain.Channel, error) {
return r.deps.Channels.SetSignatures(ctx, userID, channelID, req.SignaturesEnabled)
})
}
func (r *Router) onChannelsTogglePreHistoryHidden(ctx context.Context, req *tg.ChannelsTogglePreHistoryHiddenRequest) (tg.UpdatesClass, error) {
return r.applyChannelAdminStateMutation(ctx, req.Channel, func(ctx context.Context, userID, channelID int64) (domain.Channel, error) {
return r.deps.Channels.SetPreHistoryHidden(ctx, userID, channelID, req.Enabled)
})
}
func (r *Router) onChannelsToggleSlowMode(ctx context.Context, req *tg.ChannelsToggleSlowModeRequest) (tg.UpdatesClass, error) {
if !domain.ValidChannelSlowModeSeconds(req.Seconds) {
return nil, secondsInvalidErr()
}
return r.applyChannelAdminStateMutation(ctx, req.Channel, func(ctx context.Context, userID, channelID int64) (domain.Channel, error) {
return r.deps.Channels.SetSlowMode(ctx, userID, channelID, req.Seconds)
})
}
func (r *Router) onChannelsSetStickers(ctx context.Context, req *tg.ChannelsSetStickersRequest) (bool, error) {
_, view, err := r.channelChangeInfoView(ctx, req.Channel)
if err != nil {
return false, err
}
if !view.Channel.Megagroup || view.Channel.Broadcast {
return false, channelInvalidErr(domain.ErrChannelInvalid)
}
if err := validateEmptyChannelStickerSet(req.Stickerset); err != nil {
return false, err
}
return true, nil
}
func (r *Router) onChannelsSetEmojiStickers(ctx context.Context, req *tg.ChannelsSetEmojiStickersRequest) (bool, error) {
_, view, err := r.channelChangeInfoView(ctx, req.Channel)
if err != nil {
return false, err
}
if !view.Channel.Megagroup || view.Channel.Broadcast {
return false, channelInvalidErr(domain.ErrChannelInvalid)
}
if err := validateEmptyChannelStickerSet(req.Stickerset); err != nil {
return false, err
}
return true, nil
}
// onChannelsReorderUsernames rewrites the channel's collectible username order.
// Permissions are the ordinary change_info gate every other channels.* setting
// uses (channelChangeInfoView).
//
// With no username registry wired the handler keeps its historical accept-and-
// ignore answer: the channel then owns exactly one editable username, whose order
// is not expressible, so reporting success is both true and what shipped clients
// already saw.
func (r *Router) onChannelsReorderUsernames(ctx context.Context, req *tg.ChannelsReorderUsernamesRequest) (bool, error) {
if req == nil {
return false, channelInvalidErr(domain.ErrChannelInvalid)
}
if len(req.Order) > maxChannelUsernameOrder {
return false, limitInvalidErr()
}
_, view, err := r.channelChangeInfoView(ctx, req.Channel)
if err != nil {
return false, err
}
if r.deps.Usernames == nil {
return true, nil
}
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: view.Channel.ID}
if err := r.reorderRegistryUsernames(ctx, peer, req.Order); err != nil {
return false, err
}
return true, nil
}
func (r *Router) onChannelsToggleUsername(ctx context.Context, req *tg.ChannelsToggleUsernameRequest) (bool, error) {
if req == nil {
return false, channelInvalidErr(domain.ErrChannelInvalid)
}
if req.Username != "" && !validChannelManagementUsername(req.Username) {
return false, usernameInvalidErr()
}
_, view, err := r.channelChangeInfoView(ctx, req.Channel)
if err != nil {
return false, err
}
if r.deps.Usernames == nil {
return true, nil
}
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: view.Channel.ID}
if err := r.toggleRegistryUsername(ctx, peer, req.Username, req.Active); err != nil {
return false, err
}
return true, nil
}
func (r *Router) onChannelsDeactivateAllUsernames(ctx context.Context, input tg.InputChannelClass) (bool, error) {
_, view, err := r.channelChangeInfoView(ctx, input)
if err != nil {
return false, err
}
if r.deps.Usernames == nil {
return true, nil
}
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: view.Channel.ID}
if err := r.deactivateAllRegistryUsernames(ctx, peer); err != nil {
return false, err
}
return true, nil
}
func (r *Router) onChannelsUpdateColor(ctx context.Context, req *tg.ChannelsUpdateColorRequest) (tg.UpdatesClass, error) {
return r.applyChannelChangeInfoMutation(ctx, req.Channel, func(ctx context.Context, userID int64, view domain.ChannelView) (domain.Channel, error) {
return r.deps.Channels.SetColor(ctx, userID, view.Channel.ID, req.ForProfile, domainPeerColorFromChannelUpdate(req))
})
}
func (r *Router) onChannelsUpdateEmojiStatus(ctx context.Context, req *tg.ChannelsUpdateEmojiStatusRequest) (tg.UpdatesClass, error) {
viewerUserID, view, err := r.channelChangeInfoView(ctx, req.Channel)
if err != nil {
return nil, err
}
status, err := domainChannelEmojiStatus(req.EmojiStatus)
if err != nil {
return nil, err
}
channel, err := r.deps.Channels.SetEmojiStatus(ctx, viewerUserID, view.Channel.ID, status)
if err != nil {
return nil, channelAdminErr(err)
}
return r.channelStateMutationUpdates(ctx, viewerUserID, channel), nil
}
func (r *Router) onChannelsSetDiscussionGroup(ctx context.Context, req *tg.ChannelsSetDiscussionGroupRequest) (bool, error) {
if r.deps.Channels == nil {
return false, notImplementedErr()
}
if req == nil {
return false, channelInvalidErr(domain.ErrChannelInvalid)
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
broadcastID, err := r.optionalChannelIDFromInput(ctx, userID, req.Broadcast)
if err != nil {
return false, channelDiscussionErr(err)
}
groupID, err := r.optionalChannelIDFromInput(ctx, userID, req.Group)
if err != nil {
return false, channelDiscussionErr(err)
}
res, err := r.deps.Channels.SetDiscussionGroup(ctx, userID, broadcastID, groupID)
if err != nil {
return false, channelDiscussionErr(err)
}
for _, channel := range res.Channels {
r.invalidateRPCProjectionForChannel(channel.ID)
r.pushChannelStateToMembers(ctx, userID, channel)
}
return true, nil
}
func (r *Router) onChannelsEditLocation(ctx context.Context, req *tg.ChannelsEditLocationRequest) (bool, error) {
if _, _, err := r.channelChangeInfoView(ctx, req.Channel); err != nil {
return false, err
}
return true, nil
}
func (r *Router) onChannelsConvertToGigagroup(ctx context.Context, input tg.InputChannelClass) (tg.UpdatesClass, error) {
userID, view, err := r.channelChangeInfoView(ctx, input)
if err != nil {
return nil, err
}
return r.channelStateUpdates(userID, view.Channel), nil
}
func (r *Router) onChannelsToggleAntiSpam(ctx context.Context, req *tg.ChannelsToggleAntiSpamRequest) (tg.UpdatesClass, error) {
return r.applyChannelAdminStateMutation(ctx, req.Channel, func(ctx context.Context, userID, channelID int64) (domain.Channel, error) {
return r.deps.Channels.SetAntiSpam(ctx, userID, channelID, req.Enabled)
})
}
func (r *Router) onChannelsReportAntiSpamFalsePositive(ctx context.Context, req *tg.ChannelsReportAntiSpamFalsePositiveRequest) (bool, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return false, messageIDInvalidErr()
}
userID, view, err := r.channelChangeInfoView(ctx, req.Channel)
if err != nil {
return false, err
}
if !view.Channel.Megagroup || view.Channel.Broadcast {
return false, channelInvalidErr(domain.ErrChannelInvalid)
}
if r.deps.Moderation == nil {
return false, internalErr()
}
if _, _, err := r.deps.Moderation.ReportAntiSpamFalsePositive(
ctx, userID, view.Channel.ID, req.MsgID, r.clock.Now(),
); err != nil {
return false, moderationReportError(err)
}
return true, nil
}
func (r *Router) onChannelsGetChannelRecommendations(ctx context.Context, req *tg.ChannelsGetChannelRecommendationsRequest) (tg.MessagesChatsClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if r.deps.Channels == nil {
return &tg.MessagesChats{Chats: []tg.ChatClass{}}, nil
}
sourceChannelID := int64(0)
if req != nil {
if input, ok := req.GetChannel(); ok {
source, err := r.publicRecommendationSourceChannel(ctx, userID, input)
if err != nil {
return nil, err
}
sourceChannelID = source
}
}
res, err := r.deps.Channels.ChannelRecommendations(ctx, userID, domain.ChannelRecommendationsRequest{
UserID: userID,
SourceChannelID: sourceChannelID,
Limit: domain.DefaultChannelRecommendationsLimit,
})
if err != nil {
return nil, channelInvalidErr(err)
}
chats := tgChannels(userID, res.Channels)
r.applyUsernamesToPeerObjects(ctx, nil, chats)
if res.Count > len(chats) {
return &tg.MessagesChatsSlice{Count: res.Count, Chats: chats}, nil
}
return &tg.MessagesChats{Chats: chats}, nil
}
func (r *Router) onChannelsSetBoostsToUnblockRestrictions(ctx context.Context, req *tg.ChannelsSetBoostsToUnblockRestrictionsRequest) (tg.UpdatesClass, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
if req.Boosts < 0 || req.Boosts > maxChannelBoostsToUnblockRestrictions {
return nil, limitInvalidErr()
}
return r.applyChannelChangeInfoMutation(ctx, req.Channel, func(ctx context.Context, userID int64, view domain.ChannelView) (domain.Channel, error) {
return r.deps.Channels.SetBoostsToUnblockRestrictions(ctx, userID, view.Channel.ID, req.Boosts)
})
}
func (r *Router) onChannelsRestrictSponsoredMessages(ctx context.Context, req *tg.ChannelsRestrictSponsoredMessagesRequest) (tg.UpdatesClass, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
return r.applyChannelChangeInfoMutation(ctx, req.Channel, func(ctx context.Context, userID int64, view domain.ChannelView) (domain.Channel, error) {
return r.deps.Channels.SetRestrictedSponsored(ctx, userID, view.Channel.ID, req.Restricted)
})
}
func (r *Router) onChannelsUpdatePaidMessagesPrice(ctx context.Context, req *tg.ChannelsUpdatePaidMessagesPriceRequest) (tg.UpdatesClass, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
userID, view, err := r.channelChangeInfoView(ctx, req.Channel)
if err != nil {
return nil, err
}
if err := validateChannelPaidMessagesPriceRequest(req, view.Channel); err != nil {
return nil, err
}
stars := req.SendPaidMessagesStars
if stars < 0 {
stars = 0
}
res, err := r.deps.Channels.SetPaidMessagesPrice(ctx, userID, view.Channel.ID, stars, req.BroadcastMessagesAllowed)
if err != nil {
return nil, channelAdminErr(err)
}
return r.channelPaidMessagesPriceUpdates(ctx, userID, res), nil
}
func validateChannelPaidMessagesPriceRequest(req *tg.ChannelsUpdatePaidMessagesPriceRequest, channel domain.Channel) error {
stars := req.SendPaidMessagesStars
if stars == -1 && channel.Broadcast && !req.BroadcastMessagesAllowed {
return nil
}
if stars < 0 || stars > maxChannelPaidMessageStars {
return starsAmountInvalidErr()
}
return nil
}
func (r *Router) onChannelsToggleAutotranslation(ctx context.Context, req *tg.ChannelsToggleAutotranslationRequest) (tg.UpdatesClass, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
return r.applyChannelChangeInfoMutation(ctx, req.Channel, func(ctx context.Context, userID int64, view domain.ChannelView) (domain.Channel, error) {
return r.deps.Channels.SetAutotranslation(ctx, userID, view.Channel.ID, req.Enabled)
})
}
func (r *Router) onChannelsEditTitle(ctx context.Context, req *tg.ChannelsEditTitleRequest) (tg.UpdatesClass, error) {
if r.deps.Channels == nil && r.deps.Communities == nil {
return nil, notImplementedErr()
}
if !validChannelTitle(req.Title) {
return nil, channelInvalidErr(domain.ErrChannelTitleInvalid)
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if community, ok, err := r.maybeCommunityFromInput(ctx, userID, req.Channel); ok {
if err != nil {
return nil, err
}
view, changed, err := r.deps.Communities.EditTitle(ctx, userID, community.Community.ID, req.Title)
if err != nil {
return nil, communityErr(err)
}
return r.communityMutationUpdates(ctx, userID, view, changed), nil
}
if r.deps.Channels == nil {
return nil, channelInvalidErr(domain.ErrChannelInvalid)
}
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
if err != nil {
return nil, err
}
res, err := r.deps.Channels.EditTitle(ctx, userID, domain.EditChannelTitleRequest{
UserID: userID,
ChannelID: channelID,
Title: req.Title,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, channelAdminErr(err)
}
r.invalidateRPCProjectionForChannel(res.Channel.ID)
updates := r.channelTitleUpdates(ctx, userID, res)
// 混合容器拆分(设计 fan-out epicchannelTitleUpdates 同时含①无 pts UpdateChannel频道元数据
// 刷新,无 channel difference 恢复面)②带 pts 改名服务消息broadcast+megagroup 均产 pts
// difference 恢复面)。①必须同步发(不能进可丢弃队列,否则永久漏人数/元数据刷新),但它无 Users
// 投影、廉价;②走异步 fan-out含 owner 预热 + >cap nudge丢弃由 getChannelDifference 兜底),把
// per-viewer 服务消息投影移出改名者 RPC 路径。操作者本设备仍由上面的 RPC result 即时回显完整容器。
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
return r.channelStateUpdates(viewerUserID, res.Channel)
})
if res.Event.Pts != 0 {
r.enqueueChannelMessageFanout(ctx, userID, domain.SendChannelMessageResult{
Channel: res.Channel,
Message: res.Message,
Event: res.Event,
Recipients: res.Recipients,
}, nil)
}
return updates, nil
}
func (r *Router) onChannelsEditPhoto(ctx context.Context, req *tg.ChannelsEditPhotoRequest) (tg.UpdatesClass, error) {
if r.deps.Channels == nil && r.deps.Communities == nil {
return nil, notImplementedErr()
}
if req.Photo == nil {
return nil, photoInvalidErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
photo, err := r.resolveInputChatPhoto(ctx, userID, req.Photo)
if err != nil {
return nil, err
}
if community, ok, err := r.maybeCommunityFromInput(ctx, userID, req.Channel); ok {
if err != nil {
return nil, err
}
view, changed, err := r.deps.Communities.SetPhoto(ctx, userID, community.Community.ID, photo, int(r.clock.Now().Unix()))
if err != nil {
return nil, communityErr(err)
}
return r.communityMutationUpdates(ctx, userID, view, changed), nil
}
if r.deps.Channels == nil {
return nil, channelInvalidErr(domain.ErrChannelInvalid)
}
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
if err != nil {
return nil, err
}
res, err := r.deps.Channels.SetPhoto(ctx, userID, channelID, photo, int(r.clock.Now().Unix()))
if err != nil {
return nil, channelAdminErr(err)
}
r.invalidateRPCProjectionForChannel(res.Channel.ID)
updates := r.channelPhotoUpdates(ctx, userID, res)
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
return r.channelStateUpdates(viewerUserID, res.Channel)
})
if res.Event.Pts != 0 {
r.enqueueChannelMessageFanout(ctx, userID, domain.SendChannelMessageResult{
Channel: res.Channel,
Message: res.Message,
Event: res.Event,
Recipients: res.Recipients,
}, nil)
}
return updates, nil
}
func (r *Router) channelTitleUpdates(ctx context.Context, viewerUserID int64, res domain.EditChannelTitleResult) *tg.Updates {
updates := []tg.UpdateClass{&tg.UpdateChannel{ChannelID: res.Channel.ID}}
if res.Event.Pts != 0 {
if update := tgChannelUpdate(viewerUserID, res.Event); update != nil {
updates = append(updates, update)
}
}
return &tg.Updates{
Updates: updates,
Users: r.tgUsersForIDs(ctx, viewerUserID, []int64{res.Message.SenderUserID}),
Chats: []tg.ChatClass{tgChannelChatMin(viewerUserID, res.Channel)},
Date: int(r.clock.Now().Unix()),
Seq: 0,
}
}
func (r *Router) channelPhotoUpdates(ctx context.Context, viewerUserID int64, res domain.SetChannelPhotoResult) *tg.Updates {
updates := []tg.UpdateClass{&tg.UpdateChannel{ChannelID: res.Channel.ID}}
if res.Event.Pts != 0 {
if update := tgChannelUpdate(viewerUserID, res.Event); update != nil {
updates = append(updates, update)
}
}
return &tg.Updates{
Updates: updates,
Users: r.tgUsersForIDs(ctx, viewerUserID, []int64{res.Message.SenderUserID}),
Chats: []tg.ChatClass{tgChannelChatMin(viewerUserID, res.Channel)},
Date: int(r.clock.Now().Unix()),
Seq: 0,
}
}
func validChannelTitle(title string) bool {
n := utf8.RuneCountInString(title)
return n > 0 && n <= maxChannelTitleLength
}
func validChannelManagementUsername(username string) bool {
username = strings.TrimSpace(strings.TrimPrefix(username, "@"))
if len(username) < 5 || len(username) > 32 {
return false
}
for i := 0; i < len(username); i++ {
c := username[i]
switch {
case i == 0 && ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')):
case i > 0 && ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'):
default:
return false
}
}
return true
}
func domainPeerColorFromChannelUpdate(req *tg.ChannelsUpdateColorRequest) domain.ChannelPeerColor {
if req == nil {
return domain.ChannelPeerColor{}
}
color, hasColor := req.GetColor()
backgroundEmojiID, hasBackground := req.GetBackgroundEmojiID()
out := domain.ChannelPeerColor{HasColor: hasColor, Color: color}
if hasBackground {
out.BackgroundEmojiID = backgroundEmojiID
}
return out
}
func domainChannelEmojiStatus(status tg.EmojiStatusClass) (domain.ChannelEmojiStatus, error) {
switch s := status.(type) {
case *tg.EmojiStatusEmpty:
return domain.ChannelEmojiStatus{}, nil
case *tg.EmojiStatus:
if s.DocumentID <= 0 {
return domain.ChannelEmojiStatus{}, tgerr400("EMOJI_STATUS_INVALID")
}
until, _ := s.GetUntil()
if until < 0 {
return domain.ChannelEmojiStatus{}, tgerr400("EMOJI_STATUS_INVALID")
}
return domain.ChannelEmojiStatus{DocumentID: s.DocumentID, Until: until}, nil
case *tg.EmojiStatusCollectible:
if s.DocumentID <= 0 {
return domain.ChannelEmojiStatus{}, tgerr400("EMOJI_STATUS_INVALID")
}
until, _ := s.GetUntil()
if until < 0 {
return domain.ChannelEmojiStatus{}, tgerr400("EMOJI_STATUS_INVALID")
}
return domain.ChannelEmojiStatus{DocumentID: s.DocumentID, Until: until}, nil
case *tg.InputEmojiStatusCollectible:
return domain.ChannelEmojiStatus{}, tgerr400("EMOJI_STATUS_INVALID")
default:
return domain.ChannelEmojiStatus{}, tgerr400("EMOJI_STATUS_INVALID")
}
}
func channelUsernameErr(err error) error {
switch {
case errors.Is(err, domain.ErrUsernameInvalid):
return usernameInvalidErr()
case errors.Is(err, domain.ErrUsernameOccupied):
return usernameOccupiedErr()
case errors.Is(err, domain.ErrChannelNotModified):
return usernameNotModifiedErr()
default:
return channelAdminErr(err)
}
}