chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,278 @@
package rpc
import (
"context"
"errors"
"github.com/gotd/td/tg"
"telesrv/internal/domain"
"unicode/utf8"
)
func (r *Router) onMessagesEditMessage(ctx context.Context, req *tg.MessagesEditMessageRequest) (tg.UpdatesClass, error) {
scheduleDate, hasScheduleDate := req.GetScheduleDate()
scheduleRepeatPeriod, hasScheduleRepeatPeriod := req.GetScheduleRepeatPeriod()
if hasScheduleRepeatPeriod && scheduleRepeatPeriod != 0 {
return nil, scheduleDateInvalidErr()
}
if hasScheduleRepeatPeriod && !hasScheduleDate {
return nil, scheduleDateInvalidErr()
}
if _, ok := req.GetQuickReplyShortcutID(); ok {
return nil, messageIDInvalidErr()
}
message, hasMessage := req.GetMessage()
if hasMessage && utf8.RuneCountInString(message) > maxSendMessageTextLength {
return nil, messageTooLongErr()
}
entities, _ := req.GetEntities()
if hasMessage {
if len(entities) > maxMessageEntityCount {
return nil, entitiesTooLongErr()
}
// 编辑后的文本同样补服务端自动实体(url/@mention/#hashtag/bot command),与发送一致;
// 覆盖频道/私聊编辑与各自的定时编辑分支(editScheduledMessage 仅由本处调用)。
entities = augmentAutoEntities(message, entities)
} else {
entities = nil
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if userID == 0 {
return nil, peerIDInvalidErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if hasScheduleDate {
if media, ok := req.GetMedia(); ok && !editMessageMediaCanDegradeToText(media) {
return nil, mediaInvalidErr()
}
return r.editScheduledMessage(ctx, userID, peer, req.ID, message, hasMessage, entities, scheduleDate)
}
if media, ok := req.GetMedia(); ok {
// 关闭 poll 走 editMessage + InputMediaPoll(closed)(TDesktop "Stop poll" 路径)。
if pollMedia, isPoll := media.(*tg.InputMediaPoll); isPoll {
return r.onEditMessageClosePoll(ctx, req, pollMedia)
}
// live location 续报/停止走 editMessage + InputMediaGeoLive(DrKLO Android 路径)。
if liveMedia, isLive := media.(*tg.InputMediaGeoLive); isLive {
return r.onEditMessageLiveLocation(ctx, req, liveMedia)
}
if !editMessageMediaCanDegradeToText(media) {
return nil, mediaInvalidErr()
}
}
if !hasMessage {
return nil, messageEmptyErr()
}
if message == "" {
// 编辑媒体消息时 message="" 是合法的清空 caption;当前文本-only
// 编辑模型由 store 层校验目标消息(无媒体的纯文本消息清空仍会
// 落 MESSAGE_EMPTY),RPC 层不再一刀切拒绝。
if _, hasMedia := req.GetMedia(); !hasMedia {
return nil, messageEmptyErr()
}
}
// reply_markup(bot inline keyboard):仅 bot 编辑自己消息时替换/清空(非 bot 丢弃,
// 不报错)。P3 仅私聊;markup-only 编辑(无文本)暂不支持——须随文本一起编辑。
// 携带 inline markup(即便空 rows)即视为「设置」:空 inline 表示清空键盘,
// 否则替换;非 inline(reply keyboard 等)或非 bot 一律不设置(保留原 markup)。
var replyMarkup *domain.MessageReplyMarkup
var setReplyMarkup bool
if req.ReplyMarkup != nil {
isBot := r.userIsBot(ctx, userID)
replyMarkup, err = domainReplyMarkupForSender(req.ReplyMarkup, isBot)
if err != nil {
return nil, replyMarkupErr(err)
}
if _, isInline := req.ReplyMarkup.(*tg.ReplyInlineMarkup); isInline && isBot {
setReplyMarkup = true
}
}
if peer.Type == domain.PeerTypeChannel {
if r.deps.Channels == nil {
return nil, peerIDInvalidErr()
}
mentionUserIDs, err := r.mentionedUserIDsFromMessage(ctx, userID, message, entities)
if err != nil {
return nil, internalErr()
}
res, err := r.deps.Channels.EditMessage(ctx, userID, domain.EditChannelMessageRequest{
UserID: userID,
ChannelID: peer.ID,
ID: req.ID,
Message: message,
Entities: domainMessageEntitiesForViewer(userID, entities),
MentionUserIDs: mentionUserIDs,
EditDate: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, channelEditErr(err)
}
updates := r.channelEditMessageUpdates(ctx, userID, res)
// 编辑 fan-out 异步化 + 跨 viewer 投影预热(设计 Phase 0/Phase 1;P1-x bot 高频 editMessage
// 是稳态放大源)。同步 echo 仍走上面单 viewer 的 channelEditMessageUpdates。
r.enqueueChannelEditMessageFanout(ctx, userID, res)
return updates, nil
}
if peer.Type != domain.PeerTypeUser || r.deps.Messages == nil {
return nil, peerIDInvalidErr()
}
blocked, err := r.peerBlocksUser(ctx, userID, peer.ID)
if err != nil {
return nil, err
}
if blocked {
return nil, messageEditForbiddenErr()
}
sessionID, _ := SessionIDFrom(ctx)
authKeyID, _ := AuthKeyIDFrom(ctx)
res, err := r.deps.Messages.EditMessage(ctx, userID, domain.EditMessageRequest{
OwnerUserID: userID,
Peer: peer,
ID: req.ID,
Message: message,
Entities: domainMessageEntitiesForViewer(userID, entities),
EditDate: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
SetReplyMarkup: setReplyMarkup,
ReplyMarkup: replyMarkup,
})
if err != nil {
return nil, messageEditErr(err)
}
self := res.Self()
if self.Event.Pts == 0 || self.Message.ID == 0 {
return nil, messageIDInvalidErr()
}
users := r.usersForMessageUpdate(ctx, userID, self.Message)
chats := r.chatsForMessageUpdate(ctx, userID, self.Message)
return tgEditMessageUpdates(self.Event, self.Message, users, chats), nil
}
func editMessageMediaCanDegradeToText(media tg.InputMediaClass) bool {
switch media.(type) {
case *tg.InputMediaEmpty, *tg.InputMediaWebPage:
return true
default:
return false
}
}
func (r *Router) onMessagesGetMessageEditData(ctx context.Context, req *tg.MessagesGetMessageEditDataRequest) (*tg.MessagesMessageEditData, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.ID <= 0 || req.ID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
switch peer.Type {
case domain.PeerTypeChannel:
if r.deps.Channels == nil {
return nil, peerIDInvalidErr()
}
history, err := r.deps.Channels.GetHistory(ctx, userID, domain.ChannelHistoryFilter{
ChannelID: peer.ID,
Limit: 1,
MaxID: req.ID,
MinID: req.ID - 1,
})
if err != nil {
return nil, channelInvalidErr(err)
}
if len(history.Messages) != 1 || history.Messages[0].ID != req.ID {
return nil, messageIDInvalidErr()
}
msg := history.Messages[0]
if msg.Deleted || msg.Action != nil {
return nil, messageIDInvalidErr()
}
view, err := r.deps.Channels.GetChannel(ctx, userID, peer.ID)
if err != nil {
return nil, channelInvalidErr(err)
}
if msg.SenderUserID != userID && !canEditChannelMessageForRPC(view.Self) {
return nil, messageAuthorRequiredErr()
}
case domain.PeerTypeUser:
if r.deps.Messages == nil {
return nil, messageIDInvalidErr()
}
msg, ok, err := r.lookupOwnerMessage(ctx, userID, req.ID)
if err != nil {
return nil, internalErr()
}
if !ok || msg.Peer != peer {
return nil, messageIDInvalidErr()
}
if !msg.Out || msg.From.ID != userID {
return nil, messageAuthorRequiredErr()
}
default:
return nil, peerIDInvalidErr()
}
return &tg.MessagesMessageEditData{}, nil
}
func canEditChannelMessageForRPC(member domain.ChannelMember) bool {
return member.Role == domain.ChannelRoleCreator ||
(member.Role == domain.ChannelRoleAdmin && member.AdminRights.EditMessages)
}
func messageEditErr(err error) error {
switch {
case errors.Is(err, domain.ErrMessageIDInvalid):
return messageIDInvalidErr()
case errors.Is(err, domain.ErrMessageEmpty):
return messageEmptyErr()
case errors.Is(err, domain.ErrMessageAuthorRequired):
return messageAuthorRequiredErr()
case errors.Is(err, domain.ErrMessageNotModified):
return messageNotModifiedErr()
default:
return internalErr()
}
}
func channelEditErr(err error) error {
switch {
case errors.Is(err, domain.ErrMessageIDInvalid):
return messageIDInvalidErr()
case errors.Is(err, domain.ErrMessageEmpty):
return messageEmptyErr()
case errors.Is(err, domain.ErrMessageAuthorRequired):
return messageAuthorRequiredErr()
case errors.Is(err, domain.ErrMessageNotModified):
return messageNotModifiedErr()
default:
return channelInvalidErr(err)
}
}
func tgEditMessageUpdates(event domain.UpdateEvent, msg domain.Message, users []tg.UserClass, chats []tg.ChatClass) *tg.Updates {
update := tgOtherUpdateFromEvent(domain.UpdateEvent{
Type: domain.UpdateEventEditMessage,
Pts: event.Pts,
PtsCount: event.PtsCount,
Message: msg,
})
if update == nil {
return &tg.Updates{Date: event.Date}
}
return &tg.Updates{
Updates: []tg.UpdateClass{update},
Users: users,
Chats: chats,
Date: event.Date,
Seq: 0,
}
}