owpengram-server/internal/rpc/convert_users.go
A edb7057757 feat: sync account deletion lifecycle
Sync telesrv 73f5c91 (feat(account): implement unified account deletion lifecycle).

Skipped telesrv docs changes per public sync rules.
2026-07-19 20:35:58 +08:00

314 lines
9.1 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/iamxvbaba/td/tg"
"telesrv/internal/domain"
)
// tgSelfUser 把 domain.User 转为 self 标记的 tg.Useroptional 字段由 Encode 自动 SetFlags
func tgSelfUser(u domain.User) *tg.User {
if u.Deleted {
return &tg.User{ID: u.ID, Deleted: true}
}
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 {
if u.Deleted {
return &tg.User{ID: u.ID, Deleted: true}
}
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 flagbit28独立位
// emoji status。判断用真实时钟premium 的权威来源是 premium_expires_at 本身,
// 到期即停发,正确性不依赖后台 sweeper它只负责清理与 updateUser 通知);
// bot 永不带 premiumPremiumActiveAt 内排除)。
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未设置/已失效返回
// emojiStatusEmptyupdateUserEmojiStatus 清除语义用。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_versionLayer 225 两者共用 flags
// bit14TDesktop 只认 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 !domain.IsSystemUserID(u.ID) {
out.SetBotBusiness(true)
}
out.Phone = ""
}
// isSystemUserID 报告 id 是否为内置系统账号777000 / BotFather
func isSystemUserID(id int64) bool {
_, ok := domain.SystemUserByID(id)
return ok
}
// tgUserProfilePhoto 由 domain.User 反范式头像字段构造 UserProfilePhoto无头像返回 nilEncode 时为 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
}