owpengram-server/internal/rpc/convert_users.go

308 lines
9 KiB
Go
Raw 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 (
"time"
"github.com/gotd/td/tg"
"telesrv/internal/domain"
)
// tgSelfUser 把 domain.User 转为 self 标记的 tg.User(optional 字段由 Encode 自动 SetFlags)。
func tgSelfUser(u domain.User) *tg.User {
out := &tg.User{
ID: u.ID,
AccessHash: u.AccessHash,
FirstName: u.FirstName,
LastName: u.LastName,
Username: u.Username,
Phone: u.Phone,
Self: true,
Verified: u.Verified,
Support: u.Support,
Contact: u.Contact,
MutualContact: u.Mutual,
CloseFriend: u.CloseFriend,
Usernames: tgUsernames(u.Username),
}
applyTgUserBotFields(out, u)
applyTgUserPremiumFields(out, u)
applyTgUserColorFields(out, u)
if photo := tgUserProfilePhoto(u); photo != nil {
out.Photo = photo
}
return out
}
func tgUser(u domain.User) *tg.User {
out := &tg.User{
ID: u.ID,
AccessHash: u.AccessHash,
FirstName: u.FirstName,
LastName: u.LastName,
Username: u.Username,
Phone: u.Phone,
Verified: u.Verified,
Support: u.Support,
Contact: u.Contact,
MutualContact: u.Mutual,
CloseFriend: u.CloseFriend,
Usernames: tgUsernames(u.Username),
}
applyTgUserBotFields(out, u)
applyTgUserPremiumFields(out, u)
applyTgUserColorFields(out, u)
if photo := tgUserProfilePhoto(u); photo != nil {
out.Photo = photo
}
return out
}
// applyTgUserPremiumFields 由到期时间即时派生 premium flag(bit28,独立位)与
// emoji status。判断用真实时钟:premium 的权威来源是 premium_expires_at 本身,
// 到期即停发,正确性不依赖后台 sweeper(它只负责清理与 updateUser 通知);
// bot 永不带 premium(PremiumActiveAt 内排除)。
func applyTgUserPremiumFields(out *tg.User, u domain.User) {
now := time.Now().Unix()
if !u.PremiumActiveAt(now) {
return
}
out.Premium = true
if u.EmojiStatusActiveAt(now) {
out.SetEmojiStatus(tgUserEmojiStatus(u, now))
}
}
// tgUserEmojiStatus 把用户 emoji status 转为 TL:未设置/已失效返回
// emojiStatusEmpty(updateUserEmojiStatus 清除语义用)。user TL 内联输出仍由
// applyTgUserPremiumFields 控制(无状态时直接省略字段,对齐官方 user 编码)。
func tgUserEmojiStatus(u domain.User, now int64) tg.EmojiStatusClass {
if !u.EmojiStatusActiveAt(now) {
return &tg.EmojiStatusEmpty{}
}
status := &tg.EmojiStatus{DocumentID: u.EmojiStatusDocumentID}
if u.EmojiStatusUntil > 0 {
status.SetUntil(u.EmojiStatusUntil)
}
return status
}
func applyTgUserColorFields(out *tg.User, u domain.User) {
if color := tgUserPeerColor(u.Color); color != nil {
out.SetColor(color)
}
if color := tgUserPeerColor(u.ProfileColor); color != nil {
out.SetProfileColor(color)
}
}
func tgUserPeerColor(color domain.PeerColor) tg.PeerColorClass {
if color.Empty() {
return nil
}
out := &tg.PeerColor{}
if color.HasColor {
out.SetColor(color.Color)
}
if color.BackgroundEmojiID != 0 {
out.SetBackgroundEmojiID(color.BackgroundEmojiID)
}
return out
}
// applyTgUserBotFields 应用 bot/普通用户的互斥输出:
// - bot:必须同时置 bot flag 与 bot_info_version(Layer 225 两者共用 flags
// bit14,TDesktop 只认 bot_info_version 字段是否存在);无 phone、无 status
// (客户端显示 "bot" 而非 last seen)。
// - 普通用户:照常输出 presence status。
func applyTgUserBotFields(out *tg.User, u domain.User) {
if !u.Bot {
out.Status = tgUserStatus(u.Status)
return
}
out.Bot = true
version := u.BotInfoVersion
if version < 1 {
version = 1
}
out.SetBotInfoVersion(version)
if u.ID != domain.BotFatherUserID {
out.SetBotBusiness(true)
}
out.Phone = ""
}
// isSystemUserID 报告 id 是否为内置系统账号(777000 / BotFather)。
func isSystemUserID(id int64) bool {
_, ok := domain.SystemUserByID(id)
return ok
}
// tgUserProfilePhoto 由 domain.User 反范式头像字段构造 UserProfilePhoto;无头像返回 nil(Encode 时为 empty)。
func tgUserProfilePhoto(u domain.User) tg.UserProfilePhotoClass {
if u.PhotoID == 0 {
return nil
}
photo := &tg.UserProfilePhoto{PhotoID: u.PhotoID, DCID: u.PhotoDCID, Personal: u.PhotoPersonal}
if u.PhotoHasVideo {
photo.SetHasVideo(true)
}
if len(u.PhotoStripped) > 0 {
photo.SetStrippedThumb(u.PhotoStripped)
}
return photo
}
func tgUserStatus(status domain.UserStatus) tg.UserStatusClass {
switch status.Kind {
case domain.UserStatusOnline:
if status.Expires > 0 {
return &tg.UserStatusOnline{Expires: status.Expires}
}
case domain.UserStatusOffline:
if status.WasOnline > 0 {
return &tg.UserStatusOffline{WasOnline: status.WasOnline}
}
case domain.UserStatusLastWeek:
return &tg.UserStatusLastWeek{}
case domain.UserStatusLastMonth:
return &tg.UserStatusLastMonth{}
case domain.UserStatusEmpty:
return &tg.UserStatusEmpty{}
case domain.UserStatusRecently, domain.UserStatusUnknown:
return &tg.UserStatusRecently{}
}
return &tg.UserStatusRecently{}
}
func tgUsernames(username string) []tg.Username {
if username == "" {
return nil
}
return []tg.Username{{Editable: true, Active: true, Username: username}}
}
func tgContacts(list domain.ContactList) tg.ContactsContactsClass {
out := &tg.ContactsContacts{
Contacts: make([]tg.Contact, 0, len(list.Contacts)),
Users: make([]tg.UserClass, 0, len(list.Contacts)),
SavedCount: len(list.Contacts),
}
for _, c := range list.Contacts {
out.Contacts = append(out.Contacts, tg.Contact{UserID: c.User.ID, Mutual: c.Mutual})
out.Users = append(out.Users, tgUser(c.User))
}
return out
}
func tgContactsFound(viewerUserID int64, res domain.UserSearchResult) *tg.ContactsFound {
out := &tg.ContactsFound{
MyResults: make([]tg.PeerClass, 0, len(res.MyResults)+len(res.MyChannelResults)),
Results: make([]tg.PeerClass, 0, len(res.Results)+len(res.ChannelResults)),
Chats: make([]tg.ChatClass, 0, len(res.MyChannelResults)+len(res.ChannelResults)),
Users: make([]tg.UserClass, 0, len(res.MyResults)+len(res.Results)),
}
seen := make(map[int64]struct{}, len(res.MyResults)+len(res.Results))
appendUser := func(u domain.User) {
if u.ID == 0 {
return
}
if _, ok := seen[u.ID]; ok {
return
}
seen[u.ID] = struct{}{}
out.Users = append(out.Users, tgUser(u))
}
seenChannels := make(map[int64]struct{}, len(res.MyChannelResults)+len(res.ChannelResults))
appendChannel := func(ch domain.Channel, self *domain.ChannelMember) {
if ch.ID == 0 {
return
}
if _, ok := seenChannels[ch.ID]; ok {
return
}
seenChannels[ch.ID] = struct{}{}
out.Chats = append(out.Chats, tgChannelChat(viewerUserID, ch, self))
}
for _, u := range res.MyResults {
out.MyResults = append(out.MyResults, &tg.PeerUser{UserID: u.ID})
appendUser(u)
}
for _, ch := range res.MyChannelResults {
out.MyResults = append(out.MyResults, &tg.PeerChannel{ChannelID: ch.ID})
appendChannel(ch, &domain.ChannelMember{ChannelID: ch.ID, UserID: viewerUserID, Status: domain.ChannelMemberActive})
}
for _, u := range res.Results {
out.Results = append(out.Results, &tg.PeerUser{UserID: u.ID})
appendUser(u)
}
for _, ch := range res.ChannelResults {
out.Results = append(out.Results, &tg.PeerChannel{ChannelID: ch.ID})
appendChannel(ch, &domain.ChannelMember{ChannelID: ch.ID, UserID: viewerUserID, Status: domain.ChannelMemberLeft})
}
return out
}
func tgUsers(users []domain.User) []tg.UserClass {
out := make([]tg.UserClass, 0, len(users))
for _, u := range users {
out = append(out, tgUser(u))
}
return out
}
func tgUsersForViewer(viewerUserID int64, users []domain.User) []tg.UserClass {
out := make([]tg.UserClass, 0, len(users))
for _, u := range users {
if viewerUserID != 0 && u.ID == viewerUserID {
out = append(out, tgSelfUser(u))
continue
}
out = append(out, tgUser(u))
}
return out
}
func addUsers(out *tg.UpdatesDifference, seen map[int64]struct{}, viewerUserID int64, users []domain.User) {
for _, u := range users {
if u.ID == 0 {
continue
}
if _, ok := seen[u.ID]; ok {
continue
}
seen[u.ID] = struct{}{}
// difference 里自己的 user 必须带 self 标志:客户端会用该对象
// 持久覆盖当前账号资料,self=false 会破坏 Saved Messages 语义。
if viewerUserID != 0 && u.ID == viewerUserID {
out.Users = append(out.Users, tgSelfUser(u))
continue
}
out.Users = append(out.Users, tgUser(u))
}
}
func addMessageUsers(out *tg.UpdatesDifference, seen map[int64]struct{}, msg domain.Message) {
for _, peer := range []domain.Peer{msg.From, msg.Peer, domain.Peer{Type: domain.PeerTypeUser, ID: msg.ViaBotID}} {
if peer.Type != domain.PeerTypeUser {
continue
}
if _, ok := seen[peer.ID]; ok {
continue
}
seen[peer.ID] = struct{}{}
if u, ok := domain.SystemUserByID(peer.ID); ok {
out.Users = append(out.Users, tgUser(u))
}
}
}
func tgChannelEmojiStatus(status domain.ChannelEmojiStatus) tg.EmojiStatusClass {
if status.Empty() {
return nil
}
out := &tg.EmojiStatus{DocumentID: status.DocumentID}
if status.Until > 0 {
out.SetUntil(status.Until)
}
return out
}