chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,308 @@
package rpc
import (
"time"
"github.com/gotd/td/tg"
"telesrv/internal/domain"
)
// tgSelfUser 把 domain.User 转为 self 标记的 tg.Useroptional 字段由 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 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 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无头像返回 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, nil)
}
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
}