owpengram-server/internal/rpc/convert_markup.go
2026-09-01 12:06:31 +03:00

625 lines
21 KiB
Go
Raw Permalink 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"
"strings"
"github.com/iamxvbaba/td/tg"
"github.com/iamxvbaba/td/tgerr"
"telesrv/internal/domain"
)
// validateReplyMarkupForPeer enforces the Bot API/TL boundary that reply keyboards control
// a chat input field and are not supported in broadcast channels. Inline keyboards remain
// valid in both megagroups and broadcasts.
func (r *Router) validateReplyMarkupForPeer(ctx context.Context, userID int64, peer domain.Peer, markup *domain.MessageReplyMarkup) error {
if err := r.prepareTelegramLoginMarkup(ctx, userID, markup); err != nil {
return replyMarkupErr(err)
}
if markup == nil || !markup.IsReplyKeyboardFamily() || peer.Type != domain.PeerTypeChannel {
return nil
}
if r == nil || r.deps.Channels == nil {
return channelInvalidErr(domain.ErrChannelInvalid)
}
view, err := r.deps.Channels.ResolveChannel(ctx, userID, peer.ID)
if err != nil {
return channelInvalidErr(err)
}
if view.Channel.Broadcast && !view.Channel.Megagroup {
return replyMarkupInvalidErr()
}
return nil
}
// prepareTelegramLoginMarkup resolves every login_url target and validates its
// linked web origin before persistence. It mutates only the freshly parsed
// request DTO and assigns a deterministic flattened button id, which is later
// re-read by messages.requestUrlAuth.
func (r *Router) prepareTelegramLoginMarkup(ctx context.Context, senderBotID int64, markup *domain.MessageReplyMarkup) error {
if markup == nil || markup.Kind() != domain.MessageReplyMarkupInline {
return nil
}
hasLoginButton := false
for rowIndex := range markup.Inline {
for buttonIndex := range markup.Inline[rowIndex] {
if markup.Inline[rowIndex][buttonIndex].Type == domain.MarkupButtonLoginURL {
hasLoginButton = true
break
}
}
if hasLoginButton {
break
}
}
if !hasLoginButton {
return nil
}
if r == nil || r.deps.TelegramLogin == nil || r.deps.Users == nil || senderBotID <= 0 {
return domain.ErrButtonTypeInvalid
}
sender, found, err := r.deps.Users.ByID(ctx, senderBotID, senderBotID)
if err != nil {
return err
}
if !found || !sender.Bot || sender.Deleted {
return domain.ErrButtonTypeInvalid
}
flatID := 0
for rowIndex := range markup.Inline {
for buttonIndex := range markup.Inline[rowIndex] {
button := &markup.Inline[rowIndex][buttonIndex]
if button.Type != domain.MarkupButtonLoginURL {
flatID++
continue
}
botID := button.LoginBotUserID
if button.LoginBotUsername != "" {
resolver, ok := r.deps.Users.(UserIdentityService)
if !ok {
return domain.ErrButtonInvalid
}
bot, found, err := resolver.ResolveUsername(ctx, senderBotID, strings.TrimPrefix(button.LoginBotUsername, "@"))
if err != nil {
return err
}
if !found || !bot.Bot || bot.Deleted {
return domain.ErrButtonInvalid
}
botID = bot.ID
}
if botID == 0 {
botID = senderBotID
}
bot, found, err := r.deps.Users.ByID(ctx, senderBotID, botID)
if err != nil {
return err
}
if !found || !bot.Bot || bot.Deleted {
return domain.ErrButtonInvalid
}
normalized, _, err := r.deps.TelegramLogin.ValidateMessageButton(ctx, botID, button.URL)
if err != nil {
if errors.Is(err, domain.ErrTelegramLoginURLInvalid) || errors.Is(err, domain.ErrTelegramLoginOriginNotAllowed) {
return domain.ErrButtonURLInvalid
}
if errors.Is(err, domain.ErrTelegramLoginClientDisabled) {
return domain.ErrButtonInvalid
}
return err
}
button.URL = normalized
button.LoginBotUserID = botID
button.LoginBotUsername = ""
button.ButtonID = flatID
flatID++
}
}
return domain.ValidateReplyMarkup(markup)
}
// P3 reply_markup 错误码(对齐官方)。
func buttonDataInvalidErr() error { return tgerr.New(400, "BUTTON_DATA_INVALID") }
func buttonInvalidErr() error { return tgerr.New(400, "BUTTON_INVALID") }
func buttonTypeInvalidErr() error { return tgerr.New(400, "BUTTON_TYPE_INVALID") }
func buttonURLInvalidErr() error { return tgerr.New(400, "BUTTON_URL_INVALID") }
// replyMarkupErr 把 domain 校验错误映射为客户端错误码。
func replyMarkupErr(err error) error {
switch {
case errors.Is(err, domain.ErrButtonDataInvalid):
return buttonDataInvalidErr()
case errors.Is(err, domain.ErrButtonURLInvalid):
return buttonURLInvalidErr()
case errors.Is(err, domain.ErrButtonTypeInvalid):
return buttonTypeInvalidErr()
case errors.Is(err, domain.ErrButtonInvalid):
return buttonInvalidErr()
default:
return replyMarkupInvalidErr()
}
}
// domainReplyMarkupForSender 解析只能携带 inline keyboard 的入站 reply_markupinline
// result / edit 路径)。普通消息发送使用 domainOutgoingReplyMarkupForSender。
// 语义:
// - 仅 bot 账号下发的 markup 被接受;非 bot 一律丢弃(返回 nil不报错——对齐
// 官方「普通用户 markup 无效」I1
// - 仅 ReplyInlineMarkup 被处理bot 的 reply keyboard 家族在这些上下文中显式拒绝。
// - inline 行内按钮仅 callback / url其它按钮类型webview/game/url_auth/
// request_* 等)→ ErrButtonTypeInvalid拒绝整条发送绝不半实现下发
// - data≤64B、行/按钮上限、url https 由 domain.ValidateReplyMarkup 校验。
func domainReplyMarkupForSender(markup tg.ReplyMarkupClass, senderIsBot bool) (*domain.MessageReplyMarkup, error) {
if markup == nil || !senderIsBot {
return nil, nil
}
inline, ok := markup.(*tg.ReplyInlineMarkup)
if !ok {
return nil, domain.ErrButtonTypeInvalid
}
parsed, err := domainInlineMarkup(inline)
if err != nil {
return nil, err
}
if parsed.IsZero() {
return nil, nil
}
if err := domain.ValidateReplyMarkup(parsed); err != nil {
return nil, err
}
return parsed, nil
}
// domainOutgoingReplyMarkupForSender 解析普通 sendMessage/sendMedia 的完整 reply markup。
// 非 bot 携带 markup 仍按官方权限边界静默丢弃bot 的未知/未实现按钮则拒绝整条消息,
// 避免客户端看到一个被服务端悄悄改形的键盘。
func domainOutgoingReplyMarkupForSender(markup tg.ReplyMarkupClass, senderIsBot bool) (*domain.MessageReplyMarkup, error) {
if markup == nil || !senderIsBot {
return nil, nil
}
switch v := markup.(type) {
case *tg.ReplyInlineMarkup:
return domainReplyMarkupForSender(v, true)
case *tg.ReplyKeyboardMarkup:
out := &domain.MessageReplyMarkup{
Type: domain.MessageReplyMarkupKeyboard,
Keyboard: make([][]domain.MarkupButton, 0, len(v.Rows)),
Resize: v.Resize,
SingleUse: v.SingleUse,
Selective: v.Selective,
Persistent: v.Persistent,
Placeholder: v.Placeholder,
}
for _, row := range v.Rows {
domainRow := make([]domain.MarkupButton, 0, len(row.Buttons))
for _, button := range row.Buttons {
parsed, err := domainReplyKeyboardButton(button)
if err != nil {
return nil, err
}
domainRow = append(domainRow, parsed)
}
out.Keyboard = append(out.Keyboard, domainRow)
}
if err := domain.ValidateReplyMarkup(out); err != nil {
return nil, err
}
return out, nil
case *tg.ReplyKeyboardHide:
out := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupHide, Selective: v.Selective}
if err := domain.ValidateReplyMarkup(out); err != nil {
return nil, err
}
return out, nil
case *tg.ReplyKeyboardForceReply:
out := &domain.MessageReplyMarkup{
Type: domain.MessageReplyMarkupForceReply,
SingleUse: v.SingleUse,
Selective: v.Selective,
Placeholder: v.Placeholder,
}
if err := domain.ValidateReplyMarkup(out); err != nil {
return nil, err
}
return out, nil
default:
return nil, domain.ErrButtonTypeInvalid
}
}
func domainReplyKeyboardButton(button tg.KeyboardButton) (domain.MarkupButton, error) {
style, icon, err := domainMarkupButtonStyle(&button)
if err != nil {
return domain.MarkupButton{}, err
}
base := domain.MarkupButton{Text: button.Text, Style: style, IconCustomEmojiID: icon}
switch b := button.Type.(type) {
case *tg.ButtonTypeDefault:
base.Type = domain.MarkupButtonText
case *tg.ButtonTypeRequestPhone:
base.Type = domain.MarkupButtonRequestPhone
case *tg.ButtonTypeRequestGeoLocation:
base.Type = domain.MarkupButtonRequestLocation
case *tg.ButtonTypeRequestPoll:
base.Type = domain.MarkupButtonRequestPoll
if quiz, ok := b.GetQuiz(); ok {
if quiz {
base.PollType = "quiz"
} else {
base.PollType = "regular"
}
}
case *tg.ButtonTypeRequestPeer:
base.Type = domain.MarkupButtonRequestPeer
base.ButtonID, base.MaxQuantity = b.ButtonID, b.MaxQuantity
base.RequestPeerType, base.RequestPeerFilter = domainRequestPeerFilter(b.PeerType)
case *tg.ButtonTypeSimpleWebView:
base.Type, base.URL = domain.MarkupButtonSimpleWebView, b.URL
default:
return domain.MarkupButton{}, domain.ErrButtonTypeInvalid
}
return base, nil
}
func domainInlineMarkup(inline *tg.ReplyInlineMarkup) (*domain.MessageReplyMarkup, error) {
out := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupInline, Inline: make([][]domain.MarkupButton, 0, len(inline.Rows))}
buttonID := 0
for _, row := range inline.Rows {
domainRow := make([]domain.MarkupButton, 0, len(row.Buttons))
for _, btn := range row.Buttons {
db, err := domainMarkupButton(btn, buttonID)
if err != nil {
return nil, err
}
domainRow = append(domainRow, db)
buttonID++
}
out.Inline = append(out.Inline, domainRow)
}
return out, nil
}
func domainMarkupButton(btn tg.KeyboardInlineButton, buttonID int) (domain.MarkupButton, error) {
style, icon, err := domainMarkupButtonStyle(&btn)
if err != nil {
return domain.MarkupButton{}, err
}
switch b := btn.Type.(type) {
case *tg.InlineButtonTypeCallback:
return domain.MarkupButton{
Type: domain.MarkupButtonCallback,
Text: btn.Text,
Style: style,
IconCustomEmojiID: icon,
Data: append([]byte(nil), b.Data...),
RequiresPassword: b.RequiresPassword,
}, nil
case *tg.InlineButtonTypeURL:
return domain.MarkupButton{
Type: domain.MarkupButtonURL, Text: btn.Text, URL: b.URL,
Style: style, IconCustomEmojiID: icon,
}, nil
case *tg.InputInlineButtonTypeURLAuth:
botUserID := int64(0)
switch bot := b.Bot.(type) {
case nil, *tg.InputUserEmpty, *tg.InputUserSelf:
case *tg.InputUser:
botUserID = bot.UserID
default:
return domain.MarkupButton{}, domain.ErrButtonInvalid
}
return domain.MarkupButton{
Type: domain.MarkupButtonLoginURL, Text: btn.Text, URL: b.URL,
ForwardText: b.FwdText, ButtonID: buttonID, LoginBotUserID: botUserID,
RequestWriteAccess: b.RequestWriteAccess, Style: style, IconCustomEmojiID: icon,
}, nil
case *tg.InlineButtonTypeURLAuth:
return domain.MarkupButton{
Type: domain.MarkupButtonLoginURL, Text: btn.Text, URL: b.URL,
ForwardText: b.FwdText, ButtonID: b.ButtonID,
Style: style, IconCustomEmojiID: icon,
}, nil
case *tg.InlineButtonTypeWebView:
return domain.MarkupButton{Type: domain.MarkupButtonWebView, Text: btn.Text, URL: b.URL, Style: style, IconCustomEmojiID: icon}, nil
case *tg.InlineButtonTypeSwitchInline:
peerTypes, err := preparedInlinePeerTypesFromTG(b.PeerTypes)
if err != nil {
return domain.MarkupButton{}, domain.ErrButtonInvalid
}
return domain.MarkupButton{Type: domain.MarkupButtonSwitchInline, Text: btn.Text, Query: b.Query, SamePeer: b.SamePeer, PeerTypes: peerTypes, Style: style, IconCustomEmojiID: icon}, nil
case *tg.InlineButtonTypeCopy:
return domain.MarkupButton{Type: domain.MarkupButtonCopy, Text: btn.Text, CopyText: b.CopyText, Style: style, IconCustomEmojiID: icon}, nil
default:
// webview/game/url_auth/request_*/switch_inline/buy 等 P3 未实现按钮类型。
return domain.MarkupButton{}, domain.ErrButtonTypeInvalid
}
}
func domainMarkupButtonStyle(btn interface {
GetStyle() (tg.KeyboardButtonStyle, bool)
}) (domain.MarkupButtonStyle, int64, error) {
style, ok := btn.GetStyle()
if !ok {
return "", 0, nil
}
colors := 0
var out domain.MarkupButtonStyle
if style.GetBgPrimary() {
colors++
out = domain.MarkupButtonStylePrimary
}
if style.GetBgDanger() {
colors++
out = domain.MarkupButtonStyleDanger
}
if style.GetBgSuccess() {
colors++
out = domain.MarkupButtonStyleSuccess
}
icon, hasIcon := style.GetIcon()
if colors > 1 || (hasIcon && icon <= 0) || (colors == 0 && !hasIcon) {
return "", 0, domain.ErrButtonInvalid
}
return out, icon, nil
}
func tgMarkupButtonStyle(btn domain.MarkupButton) (tg.KeyboardButtonStyle, bool) {
var out tg.KeyboardButtonStyle
switch btn.Style {
case domain.MarkupButtonStylePrimary:
out.SetBgPrimary(true)
case domain.MarkupButtonStyleDanger:
out.SetBgDanger(true)
case domain.MarkupButtonStyleSuccess:
out.SetBgSuccess(true)
}
if btn.IconCustomEmojiID > 0 {
out.SetIcon(btn.IconCustomEmojiID)
}
return out, btn.Style != "" || btn.IconCustomEmojiID > 0
}
// tgReplyMarkup 把存储的协议中立快照还原为对应 ReplyMarkup constructor。
func tgReplyMarkup(m *domain.MessageReplyMarkup) tg.ReplyMarkupClass {
if m.IsZero() {
return nil
}
switch m.Kind() {
case domain.MessageReplyMarkupKeyboard:
rows := make([]tg.KeyboardButtonRow, 0, len(m.Keyboard))
for _, row := range m.Keyboard {
buttons := make([]tg.KeyboardButton, 0, len(row))
for _, btn := range row {
buttons = append(buttons, tgReplyKeyboardButton(btn))
}
rows = append(rows, tg.KeyboardButtonRow{Buttons: buttons})
}
return &tg.ReplyKeyboardMarkup{
Resize: m.Resize,
SingleUse: m.SingleUse,
Selective: m.Selective,
Persistent: m.Persistent,
Rows: rows,
Placeholder: m.Placeholder,
}
case domain.MessageReplyMarkupHide:
return &tg.ReplyKeyboardHide{Selective: m.Selective}
case domain.MessageReplyMarkupForceReply:
return &tg.ReplyKeyboardForceReply{
SingleUse: m.SingleUse,
Selective: m.Selective,
Placeholder: m.Placeholder,
}
case domain.MessageReplyMarkupInline:
// Continue below.
default:
return nil
}
rows := make([]tg.KeyboardInlineButtonRow, 0, len(m.Inline))
for _, row := range m.Inline {
buttons := make([]tg.KeyboardInlineButton, 0, len(row))
for _, btn := range row {
buttons = append(buttons, tgMarkupButton(btn))
}
rows = append(rows, tg.KeyboardInlineButtonRow{Buttons: buttons})
}
return &tg.ReplyInlineMarkup{Rows: rows}
}
func tgMarkupButton(btn domain.MarkupButton) tg.KeyboardInlineButton {
out := tg.KeyboardInlineButton{Text: btn.Text}
switch btn.Type {
case domain.MarkupButtonURL:
out.Type = &tg.InlineButtonTypeURL{URL: btn.URL}
case domain.MarkupButtonLoginURL:
buttonType := &tg.InlineButtonTypeURLAuth{URL: btn.URL, ButtonID: btn.ButtonID}
if btn.ForwardText != "" {
buttonType.SetFwdText(btn.ForwardText)
}
out.Type = buttonType
case domain.MarkupButtonWebView:
out.Type = &tg.InlineButtonTypeWebView{URL: btn.URL}
case domain.MarkupButtonSwitchInline:
buttonType := &tg.InlineButtonTypeSwitchInline{Query: btn.Query, SamePeer: btn.SamePeer}
if len(btn.PeerTypes) > 0 {
buttonType.SetPeerTypes(tgPreparedInlinePeerTypes(btn.PeerTypes))
}
out.Type = buttonType
case domain.MarkupButtonCopy:
out.Type = &tg.InlineButtonTypeCopy{CopyText: btn.CopyText}
case domain.MarkupButtonBuy:
out.Type = &tg.InlineButtonTypeBuy{}
default: // callback
buttonType := &tg.InlineButtonTypeCallback{Data: btn.Data}
if btn.RequiresPassword {
buttonType.SetRequiresPassword(true)
}
out.Type = buttonType
}
if style, ok := tgMarkupButtonStyle(btn); ok {
out.SetStyle(style)
}
return out
}
func tgReplyKeyboardButton(btn domain.MarkupButton) tg.KeyboardButton {
out := tg.KeyboardButton{Text: btn.Text}
switch btn.Type {
case domain.MarkupButtonRequestPhone:
out.Type = &tg.ButtonTypeRequestPhone{}
case domain.MarkupButtonRequestLocation:
out.Type = &tg.ButtonTypeRequestGeoLocation{}
case domain.MarkupButtonRequestPoll:
button := &tg.ButtonTypeRequestPoll{}
if btn.PollType == "quiz" {
button.SetQuiz(true)
} else if btn.PollType == "regular" {
button.SetQuiz(false)
}
out.Type = button
case domain.MarkupButtonRequestPeer:
out.Type = &tg.ButtonTypeRequestPeer{ButtonID: btn.ButtonID, PeerType: tgRequestPeerTypeWithFilter(btn.RequestPeerType, btn.RequestPeerFilter), MaxQuantity: btn.MaxQuantity}
case domain.MarkupButtonSimpleWebView:
out.Type = &tg.ButtonTypeSimpleWebView{URL: btn.URL}
default:
out.Type = &tg.ButtonTypeDefault{}
}
if style, ok := tgMarkupButtonStyle(btn); ok {
out.SetStyle(style)
}
return out
}
func domainRequestPeerFilter(peerType tg.RequestPeerTypeClass) (string, *domain.BotRequestPeerFilter) {
filter := &domain.BotRequestPeerFilter{}
switch v := peerType.(type) {
case *tg.RequestPeerTypeUser:
if value, ok := v.GetBot(); ok {
filter.UserIsBotSet, filter.UserIsBot = true, value
}
if value, ok := v.GetPremium(); ok {
filter.UserIsPremiumSet, filter.UserIsPremium = true, value
}
if !filter.UserIsBotSet && !filter.UserIsPremiumSet {
return "user", nil
}
return "user", filter
case *tg.RequestPeerTypeChat:
filter.ChatIsCreated, filter.BotIsMember = v.Creator, v.BotParticipant
if value, ok := v.GetHasUsername(); ok {
filter.ChatHasUsernameSet, filter.ChatHasUsername = true, value
}
if value, ok := v.GetForum(); ok {
filter.ChatIsForumSet, filter.ChatIsForum = true, value
}
if rights, ok := v.GetUserAdminRights(); ok {
mapped := domainBotRequestAdminRights(rights)
filter.UserAdminRights = &mapped
}
if rights, ok := v.GetBotAdminRights(); ok {
mapped := domainBotRequestAdminRights(rights)
filter.BotAdminRights = &mapped
}
if botRequestPeerFilterZero(filter) {
return "chat", nil
}
return "chat", filter
case *tg.RequestPeerTypeBroadcast:
filter.ChatIsCreated = v.Creator
if value, ok := v.GetHasUsername(); ok {
filter.ChatHasUsernameSet, filter.ChatHasUsername = true, value
}
if rights, ok := v.GetUserAdminRights(); ok {
mapped := domainBotRequestAdminRights(rights)
filter.UserAdminRights = &mapped
}
if rights, ok := v.GetBotAdminRights(); ok {
mapped := domainBotRequestAdminRights(rights)
filter.BotAdminRights = &mapped
}
if botRequestPeerFilterZero(filter) {
return "broadcast", nil
}
return "broadcast", filter
default:
return "", nil
}
}
func botRequestPeerFilterZero(filter *domain.BotRequestPeerFilter) bool {
return filter == nil || (!filter.UserIsBotSet && !filter.UserIsPremiumSet && !filter.ChatHasUsernameSet &&
!filter.ChatIsForumSet && !filter.ChatIsCreated && !filter.BotIsMember && filter.UserAdminRights == nil && filter.BotAdminRights == nil)
}
func tgRequestPeerTypeWithFilter(kind string, filter *domain.BotRequestPeerFilter) tg.RequestPeerTypeClass {
switch kind {
case "chat":
out := &tg.RequestPeerTypeChat{}
if filter != nil {
out.Creator, out.BotParticipant = filter.ChatIsCreated, filter.BotIsMember
if filter.ChatHasUsernameSet {
out.SetHasUsername(filter.ChatHasUsername)
}
if filter.ChatIsForumSet {
out.SetForum(filter.ChatIsForum)
}
if filter.UserAdminRights != nil {
out.SetUserAdminRights(tgBotRequestAdminRights(*filter.UserAdminRights))
}
if filter.BotAdminRights != nil {
out.SetBotAdminRights(tgBotRequestAdminRights(*filter.BotAdminRights))
}
}
return out
case "broadcast":
out := &tg.RequestPeerTypeBroadcast{}
if filter != nil {
out.Creator = filter.ChatIsCreated
if filter.ChatHasUsernameSet {
out.SetHasUsername(filter.ChatHasUsername)
}
if filter.UserAdminRights != nil {
out.SetUserAdminRights(tgBotRequestAdminRights(*filter.UserAdminRights))
}
if filter.BotAdminRights != nil {
out.SetBotAdminRights(tgBotRequestAdminRights(*filter.BotAdminRights))
}
}
return out
default:
out := &tg.RequestPeerTypeUser{}
if filter != nil {
if filter.UserIsBotSet {
out.SetBot(filter.UserIsBot)
}
if filter.UserIsPremiumSet {
out.SetPremium(filter.UserIsPremium)
}
}
return out
}
}
func domainBotRequestAdminRights(rights tg.ChatAdminRights) domain.BotRequestAdminRights {
return domain.BotRequestAdminRights{
Anonymous: rights.Anonymous, ManageChat: rights.Other, DeleteMessages: rights.DeleteMessages,
ManageVideoChats: rights.ManageCall, RestrictMembers: rights.BanUsers, PromoteMembers: rights.AddAdmins,
ChangeInfo: rights.ChangeInfo, InviteUsers: rights.InviteUsers, PostStories: rights.PostStories,
EditStories: rights.EditStories, DeleteStories: rights.DeleteStories, PostMessages: rights.PostMessages,
EditMessages: rights.EditMessages, PinMessages: rights.PinMessages, ManageTopics: rights.ManageTopics,
ManageDirectMessages: rights.ManageDirectMessages,
}
}
func tgBotRequestAdminRights(rights domain.BotRequestAdminRights) tg.ChatAdminRights {
return tg.ChatAdminRights{
Anonymous: rights.Anonymous, Other: rights.ManageChat, DeleteMessages: rights.DeleteMessages,
ManageCall: rights.ManageVideoChats, BanUsers: rights.RestrictMembers, AddAdmins: rights.PromoteMembers,
ChangeInfo: rights.ChangeInfo, InviteUsers: rights.InviteUsers, PostStories: rights.PostStories,
EditStories: rights.EditStories, DeleteStories: rights.DeleteStories, PostMessages: rights.PostMessages,
EditMessages: rights.EditMessages, PinMessages: rights.PinMessages, ManageTopics: rights.ManageTopics,
ManageDirectMessages: rights.ManageDirectMessages,
}
}