chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
364
internal/rpc/messages_todos.go
Normal file
364
internal/rpc/messages_todos.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue