Initial open source release
This commit is contained in:
commit
74992e893f
377 changed files with 118084 additions and 0 deletions
539
internal/rpc/send_media.go
Normal file
539
internal/rpc/send_media.go
Normal file
|
|
@ -0,0 +1,539 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// 本文件实现 messages.uploadMedia / sendMedia / sendMultiMedia 的 photo/document/sticker 主路径,
|
||||
// 并抽取 sendOutgoing 作为「已校验的一条出站消息(文本或媒体)落地」的共享实现,私聊与频道共用。
|
||||
|
||||
// outgoingSend 是 sendOutgoing 的入参:一条已校验的出站消息。
|
||||
type outgoingSend struct {
|
||||
randomID int64
|
||||
message string
|
||||
entities []tg.MessageEntityClass
|
||||
media *domain.MessageMedia
|
||||
silent bool
|
||||
noforwards bool
|
||||
replyToInput tg.InputReplyToClass
|
||||
sendAsInput tg.InputPeerClass
|
||||
clearDraft bool
|
||||
}
|
||||
|
||||
// sendOutgoing 把一条出站消息落地到私聊或频道,返回 *tg.Updates、是否重复、错误。
|
||||
// media 为空即纯文本。校验(长度/random_id/限流)由调用方完成。
|
||||
func (r *Router) sendOutgoing(ctx context.Context, userID int64, peer domain.Peer, p outgoingSend) (tg.UpdatesClass, bool, error) {
|
||||
sendAs, err := r.resolveSendAsPeer(ctx, userID, peer, p.sendAsInput)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if peer.Type == domain.PeerTypeChannel {
|
||||
if r.deps.Channels == nil {
|
||||
return nil, false, peerIDInvalidErr()
|
||||
}
|
||||
replyTo, err := r.messageReplyFromInput(ctx, userID, peer, p.replyToInput)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
mentionUserIDs, err := r.mentionedUserIDsFromMessage(ctx, userID, p.message, p.entities)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
res, err := r.deps.Channels.SendMessage(ctx, userID, domain.SendChannelMessageRequest{
|
||||
UserID: userID,
|
||||
ChannelID: peer.ID,
|
||||
RandomID: p.randomID,
|
||||
Message: p.message,
|
||||
Entities: domainMessageEntities(p.entities),
|
||||
Media: p.media,
|
||||
MentionUserIDs: mentionUserIDs,
|
||||
Silent: p.silent,
|
||||
NoForwards: p.noforwards,
|
||||
ReplyTo: replyTo,
|
||||
SendAs: sendAs,
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, channelInvalidErr(err)
|
||||
}
|
||||
updates := r.channelMessageUpdates(ctx, userID, res, p.randomID)
|
||||
if !res.Duplicate {
|
||||
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
||||
return r.channelMessageUpdates(ctx, viewerUserID, res, 0)
|
||||
})
|
||||
r.pushChannelDiscussionUpdate(ctx, userID, res.Discussion)
|
||||
}
|
||||
if p.clearDraft {
|
||||
r.clearDraftAfterSend(ctx, userID, peer, replyTo)
|
||||
}
|
||||
return updates, res.Duplicate, nil
|
||||
}
|
||||
if peer.Type != domain.PeerTypeUser {
|
||||
return nil, false, peerIDInvalidErr()
|
||||
}
|
||||
if r.deps.Messages == nil {
|
||||
return nil, false, peerIDInvalidErr()
|
||||
}
|
||||
if r.deps.Users != nil && peer.ID != userID {
|
||||
if _, found, err := r.deps.Users.ByID(ctx, userID, peer.ID); err != nil {
|
||||
return nil, false, internalErr()
|
||||
} else if !found {
|
||||
return nil, false, peerIDInvalidErr()
|
||||
}
|
||||
}
|
||||
replyTo, err := r.messageReplyFromInput(ctx, userID, peer, p.replyToInput)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
sessionID, _ := SessionIDFrom(ctx)
|
||||
authKeyID, _ := AuthKeyIDFrom(ctx)
|
||||
res, err := r.deps.Messages.SendPrivateText(ctx, userID, domain.SendPrivateTextRequest{
|
||||
SenderUserID: userID,
|
||||
RecipientUserID: peer.ID,
|
||||
RandomID: p.randomID,
|
||||
Message: p.message,
|
||||
Entities: domainMessageEntities(p.entities),
|
||||
Media: p.media,
|
||||
Silent: p.silent,
|
||||
NoForwards: p.noforwards,
|
||||
ReplyTo: replyTo,
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
OriginAuthKeyID: authKeyID,
|
||||
OriginSessionID: sessionID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, messageSendErr(err)
|
||||
}
|
||||
users := r.usersForMessageUpdate(ctx, userID, res.SenderMessage)
|
||||
chats := r.chatsForMessageUpdate(ctx, userID, res.SenderMessage)
|
||||
if p.clearDraft {
|
||||
r.clearDraftAfterSend(ctx, userID, peer, replyTo)
|
||||
}
|
||||
return tgPrivateMessageUpdates(res.SenderEvent, res.SenderMessage, p.randomID, true, users, chats), res.Duplicate, nil
|
||||
}
|
||||
|
||||
// onMessagesUploadMedia 解析 InputMedia(上传或引用),返回可复用的 tg.MessageMedia。
|
||||
func (r *Router) onMessagesUploadMedia(ctx context.Context, req *tg.MessagesUploadMediaRequest) (tg.MessageMediaClass, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if userID == 0 {
|
||||
return nil, mediaInvalidErr()
|
||||
}
|
||||
if len(req.BusinessConnectionID) > maxBusinessConnIDLength {
|
||||
return nil, limitInvalidErr()
|
||||
}
|
||||
if _, ok := req.Peer.(*tg.InputPeerEmpty); !ok {
|
||||
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if _, ok := req.Media.(*tg.InputMediaEmpty); ok {
|
||||
return &tg.MessageMediaEmpty{}, nil
|
||||
}
|
||||
media, err := r.resolveInputMedia(ctx, userID, req.Media)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if media == nil {
|
||||
return nil, mediaInvalidErr()
|
||||
}
|
||||
return tgMessageMedia(media), nil
|
||||
}
|
||||
|
||||
// onMessagesSendMedia 发送一条带媒体的消息(photo/document/sticker),私聊与频道均支持。
|
||||
func (r *Router) onMessagesSendMedia(ctx context.Context, req *tg.MessagesSendMediaRequest) (tg.UpdatesClass, error) {
|
||||
if req.RandomID == 0 {
|
||||
return nil, randomIDEmptyErr()
|
||||
}
|
||||
if utf8.RuneCountInString(req.Message) > maxSendMessageTextLength {
|
||||
return nil, mediaCaptionTooLongErr()
|
||||
}
|
||||
if len(req.Entities) > maxMessageEntityCount {
|
||||
return nil, limitInvalidErr()
|
||||
}
|
||||
if req.ScheduleDate != 0 || req.ScheduleRepeatPeriod != 0 {
|
||||
return nil, scheduleDateInvalidErr()
|
||||
}
|
||||
if req.Media == nil {
|
||||
return nil, mediaInvalidErr()
|
||||
}
|
||||
// InputMediaEmpty / WebPage:退化为纯文本发送(复用 sendMessage 校验与流程)。
|
||||
switch req.Media.(type) {
|
||||
case *tg.InputMediaEmpty, *tg.InputMediaWebPage:
|
||||
return r.onMessagesSendMessage(ctx, sendMessageRequestFromSendMedia(req))
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if userID == 0 {
|
||||
return nil, peerIDInvalidErr()
|
||||
}
|
||||
if r.deps.Limiter != nil {
|
||||
allowed, retryAfter, err := r.deps.Limiter.Allow(ctx, "messages:send:"+strconv.FormatInt(userID, 10), sendMessageRateLimit, sendMessageRateWindow)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if !allowed {
|
||||
r.metrics().MessageRateLimited(retryAfter)
|
||||
return nil, floodWaitErr(retryAfter)
|
||||
}
|
||||
}
|
||||
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
media, err := r.resolveInputMedia(ctx, userID, req.Media)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if media == nil {
|
||||
return nil, mediaInvalidErr()
|
||||
}
|
||||
updates, _, err := r.sendOutgoing(ctx, userID, peer, outgoingSend{
|
||||
randomID: req.RandomID,
|
||||
message: req.Message,
|
||||
entities: req.Entities,
|
||||
media: media,
|
||||
silent: req.Silent,
|
||||
noforwards: req.Noforwards,
|
||||
replyToInput: req.ReplyTo,
|
||||
sendAsInput: req.SendAs,
|
||||
clearDraft: req.ClearDraft,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return updates, nil
|
||||
}
|
||||
|
||||
// onMessagesSendMultiMedia 发送相册(多条媒体)。本阶段不绑定 grouped_id(各条作为独立消息呈现)。
|
||||
func (r *Router) onMessagesSendMultiMedia(ctx context.Context, req *tg.MessagesSendMultiMediaRequest) (tg.UpdatesClass, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if userID == 0 {
|
||||
return nil, peerIDInvalidErr()
|
||||
}
|
||||
if len(req.MultiMedia) == 0 || len(req.MultiMedia) > maxSendMultiMediaItems {
|
||||
return nil, limitInvalidErr()
|
||||
}
|
||||
if req.ScheduleDate != 0 {
|
||||
return nil, scheduleDateInvalidErr()
|
||||
}
|
||||
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range req.MultiMedia {
|
||||
if item.RandomID == 0 {
|
||||
return nil, randomIDEmptyErr()
|
||||
}
|
||||
if utf8.RuneCountInString(item.Message) > maxSendMessageTextLength {
|
||||
return nil, mediaCaptionTooLongErr()
|
||||
}
|
||||
if len(item.Entities) > maxMessageEntityCount {
|
||||
return nil, limitInvalidErr()
|
||||
}
|
||||
if item.Media == nil {
|
||||
return nil, mediaInvalidErr()
|
||||
}
|
||||
}
|
||||
|
||||
combined := make([]tg.UpdateClass, 0, len(req.MultiMedia)*2)
|
||||
usersByID := map[int64]tg.UserClass{}
|
||||
chatsByID := map[int64]tg.ChatClass{}
|
||||
date := 0
|
||||
for _, item := range req.MultiMedia {
|
||||
media, err := r.resolveInputMedia(ctx, userID, item.Media)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if media == nil {
|
||||
return nil, mediaInvalidErr()
|
||||
}
|
||||
result, _, err := r.sendOutgoing(ctx, userID, peer, outgoingSend{
|
||||
randomID: item.RandomID,
|
||||
message: item.Message,
|
||||
entities: item.Entities,
|
||||
media: media,
|
||||
silent: req.Silent,
|
||||
noforwards: req.Noforwards,
|
||||
replyToInput: req.ReplyTo,
|
||||
sendAsInput: req.SendAs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if upd, ok := result.(*tg.Updates); ok {
|
||||
combined = append(combined, upd.Updates...)
|
||||
for _, u := range upd.Users {
|
||||
if id := userClassID(u); id != 0 {
|
||||
usersByID[id] = u
|
||||
}
|
||||
}
|
||||
for _, c := range upd.Chats {
|
||||
if id := chatClassID(c); id != 0 {
|
||||
chatsByID[id] = c
|
||||
}
|
||||
}
|
||||
if upd.Date != 0 {
|
||||
date = upd.Date
|
||||
}
|
||||
}
|
||||
}
|
||||
return &tg.Updates{
|
||||
Updates: combined,
|
||||
Users: mapValuesUsers(usersByID),
|
||||
Chats: mapValuesChats(chatsByID),
|
||||
Date: date,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resolveInputMedia 把 tg.InputMedia 解析为 domain.MessageMedia(上传则落库,引用则加载)。
|
||||
// 返回 nil 表示 InputMediaEmpty(调用方退化为纯文本)。
|
||||
func (r *Router) resolveInputMedia(ctx context.Context, userID int64, input tg.InputMediaClass) (*domain.MessageMedia, error) {
|
||||
if r.deps.Files == nil {
|
||||
return nil, mediaInvalidErr()
|
||||
}
|
||||
switch in := input.(type) {
|
||||
case *tg.InputMediaEmpty:
|
||||
return nil, nil
|
||||
case *tg.InputMediaUploadedPhoto:
|
||||
if in.File == nil {
|
||||
return nil, mediaInvalidErr()
|
||||
}
|
||||
ref, ok := uploadedFileRef(userID, in.File)
|
||||
if !ok {
|
||||
return nil, fileReferenceInvalidErr()
|
||||
}
|
||||
photo, err := r.deps.Files.CreatePhotoFromUpload(ctx, ref)
|
||||
if err != nil {
|
||||
return nil, mediaUploadErr(err)
|
||||
}
|
||||
return &domain.MessageMedia{Kind: domain.MessageMediaKindPhoto, Photo: &photo, Spoiler: in.Spoiler, TTLSeconds: in.TTLSeconds}, nil
|
||||
case *tg.InputMediaUploadedDocument:
|
||||
if in.File == nil {
|
||||
return nil, mediaInvalidErr()
|
||||
}
|
||||
ref, ok := uploadedFileRef(userID, in.File)
|
||||
if !ok {
|
||||
return nil, fileReferenceInvalidErr()
|
||||
}
|
||||
spec := domain.DocumentSpec{
|
||||
MimeType: in.MimeType,
|
||||
Attributes: domainDocumentAttributes(in.Attributes),
|
||||
ForceFile: in.ForceFile,
|
||||
}
|
||||
if thumb, ok := in.GetThumb(); ok {
|
||||
if tref, ok := uploadedFileRef(userID, thumb); ok {
|
||||
spec.Thumb = &tref
|
||||
}
|
||||
}
|
||||
doc, err := r.deps.Files.CreateDocumentFromUpload(ctx, ref, spec)
|
||||
if err != nil {
|
||||
return nil, mediaUploadErr(err)
|
||||
}
|
||||
return messageMediaFromDocument(doc, in.Spoiler, in.TTLSeconds), nil
|
||||
case *tg.InputMediaPhoto:
|
||||
photoID, ok := inputPhotoID(in.ID)
|
||||
if !ok {
|
||||
return nil, photoInvalidErr()
|
||||
}
|
||||
photo, found, err := r.deps.Files.GetPhoto(ctx, photoID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if !found {
|
||||
return nil, photoInvalidErr()
|
||||
}
|
||||
return &domain.MessageMedia{Kind: domain.MessageMediaKindPhoto, Photo: &photo, Spoiler: in.Spoiler, TTLSeconds: in.TTLSeconds}, nil
|
||||
case *tg.InputMediaDocument:
|
||||
docIDs, ok := inputDocumentCandidateIDs(in.ID)
|
||||
if !ok {
|
||||
return nil, mediaInvalidErr()
|
||||
}
|
||||
var doc domain.Document
|
||||
found := false
|
||||
for _, docID := range docIDs {
|
||||
var err error
|
||||
doc, found, err = r.deps.Files.GetDocument(ctx, docID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, mediaInvalidErr()
|
||||
}
|
||||
return messageMediaFromDocument(doc, in.Spoiler, in.TTLSeconds), nil
|
||||
default:
|
||||
// geo / contact / poll / venue / dice / story / 等本阶段不支持。
|
||||
return nil, mediaInvalidErr()
|
||||
}
|
||||
}
|
||||
|
||||
// messageMediaFromDocument 由 Document 构造 MessageMedia,并从属性推导 Video/Round/Voice 标志。
|
||||
func messageMediaFromDocument(doc domain.Document, spoiler bool, ttl int) *domain.MessageMedia {
|
||||
media := &domain.MessageMedia{Kind: domain.MessageMediaKindDocument, Document: &doc, Spoiler: spoiler, TTLSeconds: ttl}
|
||||
for _, attr := range doc.Attributes {
|
||||
switch attr.Kind {
|
||||
case domain.DocAttrVideo:
|
||||
media.Video = true
|
||||
if attr.RoundMessage {
|
||||
media.Round = true
|
||||
}
|
||||
case domain.DocAttrAudio:
|
||||
if attr.Voice {
|
||||
media.Voice = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return media
|
||||
}
|
||||
|
||||
func inputPhotoID(input tg.InputPhotoClass) (int64, bool) {
|
||||
if p, ok := input.(*tg.InputPhoto); ok && p.ID != 0 {
|
||||
return p.ID, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func inputDocumentID(input tg.InputDocumentClass) (int64, bool) {
|
||||
if d, ok := input.(*tg.InputDocument); ok && d.ID != 0 {
|
||||
return d.ID, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func inputDocumentCandidateIDs(input tg.InputDocumentClass) ([]int64, bool) {
|
||||
if d, ok := input.(*tg.InputDocument); ok && d.ID != 0 {
|
||||
return []int64{d.ID}, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// domainDocumentAttributes 把 tg.DocumentAttribute 反向转为 domain(InputMediaUploadedDocument 用)。
|
||||
func domainDocumentAttributes(attrs []tg.DocumentAttributeClass) []domain.DocumentAttribute {
|
||||
out := make([]domain.DocumentAttribute, 0, len(attrs))
|
||||
for _, a := range attrs {
|
||||
switch v := a.(type) {
|
||||
case *tg.DocumentAttributeImageSize:
|
||||
out = append(out, domain.DocumentAttribute{Kind: domain.DocAttrImageSize, W: v.W, H: v.H})
|
||||
case *tg.DocumentAttributeAnimated:
|
||||
out = append(out, domain.DocumentAttribute{Kind: domain.DocAttrAnimated})
|
||||
case *tg.DocumentAttributeSticker:
|
||||
attr := domain.DocumentAttribute{Kind: domain.DocAttrSticker, Alt: v.Alt, Mask: v.Mask}
|
||||
if id, hash, ok := inputStickerSetIDs(v.Stickerset); ok {
|
||||
attr.StickerSetID = id
|
||||
attr.StickerSetAccessHash = hash
|
||||
}
|
||||
out = append(out, attr)
|
||||
case *tg.DocumentAttributeVideo:
|
||||
out = append(out, domain.DocumentAttribute{Kind: domain.DocAttrVideo, W: v.W, H: v.H, Duration: v.Duration, RoundMessage: v.RoundMessage, SupportsStreaming: v.SupportsStreaming})
|
||||
case *tg.DocumentAttributeAudio:
|
||||
out = append(out, domain.DocumentAttribute{Kind: domain.DocAttrAudio, AudioDuration: v.Duration, Voice: v.Voice, Title: v.Title, Performer: v.Performer, Waveform: v.Waveform})
|
||||
case *tg.DocumentAttributeFilename:
|
||||
out = append(out, domain.DocumentAttribute{Kind: domain.DocAttrFilename, FileName: v.FileName})
|
||||
case *tg.DocumentAttributeCustomEmoji:
|
||||
attr := domain.DocumentAttribute{Kind: domain.DocAttrCustomEmoji, Alt: v.Alt, Free: v.Free, TextColor: v.TextColor}
|
||||
if id, hash, ok := inputStickerSetIDs(v.Stickerset); ok {
|
||||
attr.StickerSetID = id
|
||||
attr.StickerSetAccessHash = hash
|
||||
}
|
||||
out = append(out, attr)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func inputStickerSetIDs(input tg.InputStickerSetClass) (int64, int64, bool) {
|
||||
if s, ok := input.(*tg.InputStickerSetID); ok {
|
||||
return s.ID, s.AccessHash, true
|
||||
}
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
// sendMessageRequestFromSendMedia 把 sendMedia(空媒体)的字段映射到 sendMessage 请求。
|
||||
func sendMessageRequestFromSendMedia(req *tg.MessagesSendMediaRequest) *tg.MessagesSendMessageRequest {
|
||||
return &tg.MessagesSendMessageRequest{
|
||||
Silent: req.Silent,
|
||||
Background: req.Background,
|
||||
ClearDraft: req.ClearDraft,
|
||||
Noforwards: req.Noforwards,
|
||||
UpdateStickersetsOrder: req.UpdateStickersetsOrder,
|
||||
InvertMedia: req.InvertMedia,
|
||||
AllowPaidFloodskip: req.AllowPaidFloodskip,
|
||||
Peer: req.Peer,
|
||||
ReplyTo: req.ReplyTo,
|
||||
Message: req.Message,
|
||||
RandomID: req.RandomID,
|
||||
ReplyMarkup: req.ReplyMarkup,
|
||||
Entities: req.Entities,
|
||||
ScheduleDate: req.ScheduleDate,
|
||||
ScheduleRepeatPeriod: req.ScheduleRepeatPeriod,
|
||||
SendAs: req.SendAs,
|
||||
QuickReplyShortcut: req.QuickReplyShortcut,
|
||||
Effect: req.Effect,
|
||||
AllowPaidStars: req.AllowPaidStars,
|
||||
SuggestedPost: req.SuggestedPost,
|
||||
}
|
||||
}
|
||||
|
||||
func mediaUploadErr(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrFilePartsInvalid):
|
||||
return filePartsInvalidErr()
|
||||
case errors.Is(err, domain.ErrPhotoInvalid):
|
||||
return photoInvalidErr()
|
||||
case errors.Is(err, domain.ErrDocumentInvalid):
|
||||
return mediaInvalidErr()
|
||||
default:
|
||||
return internalErr()
|
||||
}
|
||||
}
|
||||
|
||||
func userClassID(u tg.UserClass) int64 {
|
||||
if v, ok := u.(*tg.User); ok {
|
||||
return v.ID
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func chatClassID(c tg.ChatClass) int64 {
|
||||
switch v := c.(type) {
|
||||
case *tg.Channel:
|
||||
return v.ID
|
||||
case *tg.Chat:
|
||||
return v.ID
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func mapValuesUsers(m map[int64]tg.UserClass) []tg.UserClass {
|
||||
out := make([]tg.UserClass, 0, len(m))
|
||||
for _, v := range m {
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mapValuesChats(m map[int64]tg.ChatClass) []tg.ChatClass {
|
||||
out := make([]tg.ChatClass, 0, len(m))
|
||||
for _, v := range m {
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue