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,364 @@
package rpc
import (
"context"
"strings"
"unicode/utf8"
"github.com/gotd/td/tg"
"github.com/gotd/td/tgerr"
"go.uber.org/zap"
"telesrv/internal/domain"
)
// todo 清单:发送/渲染/转发全链路真实现;appendTodoList / toggleTodoCompleted
// 在私聊和超级群均支持 others_can_* 协作。超级群成功协作会同时更新原
// checklist media 快照,并生成 reply 到原消息的 todo 服务消息,二者各占
// 一个 channel pts。premium 创建入口依赖 premium 业务模型,记矩阵 todo。
// domainTodoFromInput 校验 InputMediaTodo 的清单定义并转为 domain 快照。
func domainTodoFromInput(in tg.TodoList) (*domain.MessageTodo, error) {
title := in.Title.Text
if strings.TrimSpace(title) == "" {
return nil, mediaEmptyErr()
}
if utf8.RuneCountInString(title) > maxTodoTitleLength {
return nil, limitInvalidErr()
}
if len(in.Title.Entities) > maxMessageEntityCount {
return nil, limitInvalidErr()
}
if err := validateTodoItems(in.List); err != nil {
return nil, err
}
out := &domain.MessageTodo{
OthersCanAppend: in.OthersCanAppend,
OthersCanComplete: in.OthersCanComplete,
Title: title,
TitleEntities: domainMessageEntities(in.Title.Entities),
}
for _, item := range in.List {
if item.ID <= 0 {
return nil, messageIDInvalidErr()
}
out.Items = append(out.Items, domain.MessageTodoItem{
ID: item.ID,
Title: item.Title.Text,
Entities: domainMessageEntities(item.Title.Entities),
})
}
return out, nil
}
// tgTodoMedia 把 domain 快照转回 tg.MessageMediaToDo。
func tgTodoMedia(todo domain.MessageTodo) *tg.MessageMediaToDo {
list := tg.TodoList{
OthersCanAppend: todo.OthersCanAppend,
OthersCanComplete: todo.OthersCanComplete,
Title: tgTextWithEntities(todo.Title, todo.TitleEntities),
}
for _, item := range todo.Items {
list.List = append(list.List, tg.TodoItem{
ID: item.ID,
Title: tgTextWithEntities(item.Title, item.Entities),
})
}
out := &tg.MessageMediaToDo{Todo: list}
if len(todo.Completions) > 0 {
completions := make([]tg.TodoCompletion, 0, len(todo.Completions))
for _, c := range todo.Completions {
completions = append(completions, tg.TodoCompletion{
ID: c.ID,
CompletedBy: &tg.PeerUser{UserID: c.CompletedBy},
Date: c.Date,
})
}
out.SetCompletions(completions)
}
return out
}
func tgTodoItems(items []domain.MessageTodoItem) []tg.TodoItem {
if len(items) == 0 {
return nil
}
out := make([]tg.TodoItem, 0, len(items))
for _, item := range items {
out = append(out, tg.TodoItem{
ID: item.ID,
Title: tgTextWithEntities(item.Title, item.Entities),
})
}
return out
}
func (r *Router) onMessagesAppendTodoList(ctx context.Context, req *tg.MessagesAppendTodoListRequest) (tg.UpdatesClass, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if len(req.List) == 0 {
return nil, todoNotModifiedErr()
}
if err := validateTodoItems(req.List); err != nil {
return nil, err
}
return r.mutateTodoMedia(ctx, req.Peer, req.MsgID, func(todo *domain.MessageTodo) bool {
return todo.OthersCanAppend
}, func(userID int64, todo *domain.MessageTodo, _ int) (*domain.ChannelMessageAction, error) {
existing := make(map[int]struct{}, len(todo.Items))
for _, item := range todo.Items {
existing[item.ID] = struct{}{}
}
if len(todo.Items)+len(req.List) > maxTodoItems {
return nil, limitInvalidErr()
}
appended := make([]domain.MessageTodoItem, 0, len(req.List))
for _, item := range req.List {
if item.ID <= 0 {
return nil, messageIDInvalidErr()
}
if _, dup := existing[item.ID]; dup {
return nil, tgerr.New(400, "TODO_ITEM_DUPLICATE")
}
existing[item.ID] = struct{}{}
appended = append(appended, domain.MessageTodoItem{
ID: item.ID,
Title: item.Title.Text,
Entities: domainMessageEntities(item.Title.Entities),
})
}
todo.Items = append(todo.Items, appended...)
return &domain.ChannelMessageAction{
Type: domain.ChannelActionTodoAppendTasks,
TodoItems: appended,
}, nil
})
}
func (r *Router) onMessagesToggleTodoCompleted(ctx context.Context, req *tg.MessagesToggleTodoCompletedRequest) (tg.UpdatesClass, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if len(req.Completed) == 0 && len(req.Incompleted) == 0 {
return nil, todoNotModifiedErr()
}
if err := validateTodoIDVector(req.Completed, req.Incompleted); err != nil {
return nil, err
}
return r.mutateTodoMedia(ctx, req.Peer, req.MsgID, func(todo *domain.MessageTodo) bool {
return todo.OthersCanComplete
}, func(userID int64, todo *domain.MessageTodo, now int) (*domain.ChannelMessageAction, error) {
valid := make(map[int]struct{}, len(todo.Items))
for _, item := range todo.Items {
valid[item.ID] = struct{}{}
}
done := make(map[int]domain.MessageTodoCompletion, len(todo.Completions))
for _, c := range todo.Completions {
done[c.ID] = c
}
changed := false
for _, id := range req.Completed {
if _, ok := valid[id]; !ok {
return nil, messageIDInvalidErr()
}
if _, already := done[id]; !already {
done[id] = domain.MessageTodoCompletion{ID: id, CompletedBy: userID, Date: now}
changed = true
}
}
for _, id := range req.Incompleted {
if _, ok := valid[id]; !ok {
return nil, messageIDInvalidErr()
}
if _, exists := done[id]; exists {
delete(done, id)
changed = true
}
}
if !changed {
return nil, todoNotModifiedErr()
}
todo.Completions = todo.Completions[:0]
for _, item := range todo.Items {
if c, ok := done[item.ID]; ok {
todo.Completions = append(todo.Completions, c)
}
}
return &domain.ChannelMessageAction{
Type: domain.ChannelActionTodoCompletions,
Completed: append([]int(nil), req.Completed...),
Incompleted: append([]int(nil), req.Incompleted...),
}, nil
})
}
// mutateTodoMedia 加载目标 todo 消息、应用变更并经 editMessage 媒体替换链路落库推送。
func (r *Router) mutateTodoMedia(ctx context.Context, inputPeer tg.InputPeerClass, msgID int, participantAllowed func(todo *domain.MessageTodo) bool, mutate func(userID int64, todo *domain.MessageTodo, now int) (*domain.ChannelMessageAction, error)) (tg.UpdatesClass, error) {
userID, peer, err := r.reactionPeer(ctx, inputPeer, nil)
if err != nil {
return nil, err
}
now := int(r.clock.Now().Unix())
current, found := r.loadTodoMessage(ctx, userID, peer, msgID)
if !found {
return nil, messageIDInvalidErr()
}
todo := *current.media.Todo
todo.Items = append([]domain.MessageTodoItem(nil), current.media.Todo.Items...)
todo.Completions = append([]domain.MessageTodoCompletion(nil), current.media.Todo.Completions...)
participantEdit := peer.Type == domain.PeerTypeUser && !current.out
if participantEdit && (participantAllowed == nil || !participantAllowed(&todo)) {
return nil, messageAuthorRequiredErr()
}
serviceAction, err := mutate(userID, &todo, now)
if err != nil {
return nil, err
}
newMedia := &domain.MessageMedia{Kind: domain.MessageMediaKindTodo, Todo: &todo}
if peer.Type == domain.PeerTypeChannel {
if r.deps.Channels == nil {
return nil, peerIDInvalidErr()
}
res, err := r.deps.Channels.EditMessage(ctx, userID, domain.EditChannelMessageRequest{
UserID: userID,
ChannelID: peer.ID,
ID: msgID,
Message: current.body,
Entities: current.entities,
Media: newMedia,
AllowTodoParticipantMutation: participantAllowed != nil && participantAllowed(&todo),
TodoServiceAction: serviceAction,
EditDate: now,
})
if err != nil {
return nil, channelEditErr(err)
}
updates := r.channelEditMessageUpdates(ctx, userID, res)
r.enqueueChannelEditMessageFanout(ctx, userID, res)
return updates, nil
}
if peer.Type != domain.PeerTypeUser || r.deps.Messages == nil {
return nil, peerIDInvalidErr()
}
sessionID, _ := SessionIDFrom(ctx)
authKeyID, _ := AuthKeyIDFrom(ctx)
res, err := r.deps.Messages.EditMessage(ctx, userID, domain.EditMessageRequest{
OwnerUserID: userID,
Peer: peer,
ID: msgID,
Message: current.body,
Entities: current.entities,
Media: newMedia,
EditDate: now,
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
AllowTodoParticipantMutation: participantEdit,
})
if err != nil {
r.log.Warn("todo edit message failed",
zap.Int64("user_id", userID),
zap.String("peer_type", string(peer.Type)),
zap.Int64("peer_id", peer.ID),
zap.Int("msg_id", msgID),
zap.Bool("participant_edit", participantEdit),
zap.Error(err))
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
}
type todoMessageTarget struct {
media *domain.MessageMedia
body string
entities []domain.MessageEntity
out bool
}
func (r *Router) loadTodoMessage(ctx context.Context, userID int64, peer domain.Peer, msgID int) (todoMessageTarget, bool) {
switch peer.Type {
case domain.PeerTypeChannel:
if r.deps.Channels == nil {
return todoMessageTarget{}, false
}
history, err := r.deps.Channels.GetMessages(ctx, userID, peer.ID, []int{msgID})
if err != nil {
return todoMessageTarget{}, false
}
for _, msg := range history.Messages {
if msg.ID == msgID && msg.Media != nil && msg.Media.Kind == domain.MessageMediaKindTodo && msg.Media.Todo != nil {
return todoMessageTarget{media: msg.Media, body: msg.Body, entities: msg.Entities}, true
}
}
case domain.PeerTypeUser:
if r.deps.Messages == nil {
return todoMessageTarget{}, false
}
list, err := r.deps.Messages.GetMessages(ctx, userID, []int{msgID})
if err != nil {
return todoMessageTarget{}, false
}
for _, msg := range list.Messages {
if msg.ID == msgID && msg.Peer == peer && msg.Media != nil && msg.Media.Kind == domain.MessageMediaKindTodo && msg.Media.Todo != nil {
return todoMessageTarget{media: msg.Media, body: msg.Body, entities: msg.Entities, out: msg.Out}, true
}
}
}
return todoMessageTarget{}, false
}
func validateTodoItems(items []tg.TodoItem) error {
if len(items) == 0 {
return todoItemsEmptyErr()
}
if len(items) > maxTodoItems {
return limitInvalidErr()
}
seen := make(map[int]struct{}, len(items))
for _, item := range items {
if item.ID < 0 || item.ID > maxTodoItemID {
return messageIDInvalidErr()
}
if item.ID != 0 {
if _, ok := seen[item.ID]; ok {
return tgerr.New(400, "TODO_ITEM_DUPLICATE")
}
seen[item.ID] = struct{}{}
}
if strings.TrimSpace(item.Title.Text) == "" || utf8.RuneCountInString(item.Title.Text) > maxTodoTitleLength {
return limitInvalidErr()
}
if len(item.Title.Entities) > maxMessageEntityCount {
return limitInvalidErr()
}
}
return nil
}
func validateTodoIDVector(vectors ...[]int) error {
total := 0
seen := map[int]struct{}{}
for _, ids := range vectors {
total += len(ids)
if total > maxTodoItems {
return limitInvalidErr()
}
for _, id := range ids {
if id <= 0 || id > maxTodoItemID {
return messageIDInvalidErr()
}
if _, ok := seen[id]; ok {
return todoNotModifiedErr()
}
seen[id] = struct{}{}
}
}
return nil
}