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

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"hash/fnv"
"sort"
"github.com/jackc/pgx/v5"
@ -68,6 +69,7 @@ func (s *ContactStore) GetMany(ctx context.Context, userID int64, contactUserIDs
SELECT
c.contact_user_id,
c.mutual,
c.close_friend,
c.contact_phone,
c.contact_first_name,
c.contact_last_name,
@ -82,6 +84,9 @@ SELECT
u.country_code,
u.verified,
u.support,
COALESCE(EXTRACT(EPOCH FROM u.premium_expires_at), 0)::bigint AS premium_until,
u.emoji_status_document_id,
u.emoji_status_until,
u.last_seen_at
FROM contacts c
JOIN users u ON u.id = c.contact_user_id
@ -114,6 +119,7 @@ func (s *ContactStore) GetReverseContacts(ctx context.Context, userID int64, own
SELECT
c.user_id AS owner_user_id,
c.mutual,
c.close_friend,
c.contact_phone,
c.contact_first_name,
c.contact_last_name,
@ -128,6 +134,9 @@ SELECT
u.country_code,
u.verified,
u.support,
COALESCE(EXTRACT(EPOCH FROM u.premium_expires_at), 0)::bigint AS premium_until,
u.emoji_status_document_id,
u.emoji_status_until,
u.last_seen_at
FROM contacts c
JOIN users u ON u.id = c.contact_user_id
@ -251,6 +260,7 @@ reverse_updated AS (
SELECT
c.contact_user_id,
c.mutual,
c.close_friend,
c.contact_phone,
c.contact_first_name,
c.contact_last_name,
@ -265,6 +275,9 @@ SELECT
u.country_code,
u.verified,
u.support,
COALESCE(EXTRACT(EPOCH FROM u.premium_expires_at), 0)::bigint AS premium_until,
u.emoji_status_document_id,
u.emoji_status_until,
u.last_seen_at,
EXISTS (SELECT 1 FROM reverse_updated ru WHERE ru.user_id = c.contact_user_id)::boolean AS reverse_mutual_changed
FROM upserted c
@ -311,6 +324,7 @@ func (s *ContactStore) UpsertMany(ctx context.Context, userID int64, inputs []do
var (
contactUserID int64
mutual bool
closeFriend bool
contactPhone string
contactFirstName string
contactLastName string
@ -325,12 +339,16 @@ func (s *ContactStore) UpsertMany(ctx context.Context, userID int64, inputs []do
countryCode string
verified bool
support bool
premiumUntil int64
emojiStatusDocID int64
emojiStatusUntil int64
lastSeenAt int64
reverseMutualChanged bool
)
if err := rows.Scan(
&contactUserID,
&mutual,
&closeFriend,
&contactPhone,
&contactFirstName,
&contactLastName,
@ -345,6 +363,9 @@ func (s *ContactStore) UpsertMany(ctx context.Context, userID int64, inputs []do
&countryCode,
&verified,
&support,
&premiumUntil,
&emojiStatusDocID,
&emojiStatusUntil,
&lastSeenAt,
&reverseMutualChanged,
); err != nil {
@ -355,7 +376,7 @@ func (s *ContactStore) UpsertMany(ctx context.Context, userID int64, inputs []do
if err != nil {
return nil, fmt.Errorf("decode contact note entities: %w", err)
}
out = append(out, contactFromFields(id, accessHash, phone, firstName, lastName, username, countryCode, verified, support, int(lastSeenAt), contactFirstName, contactLastName, contactPhone, note, entities, mutual))
out = append(out, contactFromFields(id, accessHash, phone, firstName, lastName, username, countryCode, verified, support, false, 0, int(premiumUntil), emojiStatusDocID, int(emojiStatusUntil), int(lastSeenAt), contactFirstName, contactLastName, contactPhone, note, entities, mutual, closeFriend))
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate upsert contacts many: %w", err)
@ -387,6 +408,70 @@ func (s *ContactStore) UpdateNote(ctx context.Context, userID, contactUserID int
return contact, true, nil
}
func (s *ContactStore) SetCloseFriends(ctx context.Context, userID int64, contactUserIDs []int64) (domain.CloseFriendsEditResult, error) {
if userID == 0 {
return domain.CloseFriendsEditResult{}, nil
}
if contactUserIDs == nil {
contactUserIDs = []int64{}
}
beforeList, err := s.ListByUser(ctx, userID)
if err != nil {
return domain.CloseFriendsEditResult{}, err
}
contactSet := make(map[int64]struct{}, len(beforeList.Contacts))
before := make(map[int64]struct{})
for _, contact := range beforeList.Contacts {
id := contact.User.ID
if id == 0 {
continue
}
contactSet[id] = struct{}{}
if contact.CloseFriend || contact.User.CloseFriend {
before[id] = struct{}{}
}
}
after := make(map[int64]struct{}, len(contactUserIDs))
for _, id := range contactUserIDs {
if id <= 0 || id == userID {
continue
}
if _, ok := contactSet[id]; ok {
after[id] = struct{}{}
}
}
_, err = s.db.Exec(ctx, `
UPDATE contacts
SET close_friend = contact_user_id = ANY($2::bigint[]),
updated_at = now()
WHERE user_id = $1
AND (
close_friend
OR contact_user_id = ANY($2::bigint[])
)`, userID, contactUserIDs)
if err != nil {
return domain.CloseFriendsEditResult{}, fmt.Errorf("set close friends: %w", err)
}
return closeFriendsEditDiff(before, after), nil
}
func closeFriendsEditDiff(before, after map[int64]struct{}) domain.CloseFriendsEditResult {
var result domain.CloseFriendsEditResult
for id := range after {
if _, ok := before[id]; !ok {
result.AddedUserIDs = append(result.AddedUserIDs, id)
}
}
for id := range before {
if _, ok := after[id]; !ok {
result.RemovedUserIDs = append(result.RemovedUserIDs, id)
}
}
sort.Slice(result.AddedUserIDs, func(i, j int) bool { return result.AddedUserIDs[i] < result.AddedUserIDs[j] })
sort.Slice(result.RemovedUserIDs, func(i, j int) bool { return result.RemovedUserIDs[i] < result.RemovedUserIDs[j] })
return result
}
func (s *ContactStore) SetPersonalPhoto(ctx context.Context, userID, contactUserID int64, photoID int64, date int) (domain.Contact, bool, error) {
tag, err := s.db.Exec(ctx, `
UPDATE contacts
@ -443,6 +528,7 @@ WHERE c.user_id = $1
DCID: int(dcID),
Stripped: domain.StrippedFromSizes(sizes),
Personal: true,
HasVideo: domain.PhotoHasVideo(sizes),
}
}
if err := rows.Err(); err != nil {
@ -470,7 +556,7 @@ func contactFromListRow(row sqlcgen.ListContactsByUserRow) (domain.Contact, erro
if err != nil {
return domain.Contact{}, fmt.Errorf("decode contact note entities: %w", err)
}
return contactFromFields(row.ID, row.AccessHash, row.Phone, row.FirstName, row.LastName, row.Username, row.CountryCode, row.Verified, row.Support, int(row.LastSeenAt), row.ContactFirstName, row.ContactLastName, row.ContactPhone, row.Note, entities, row.Mutual), nil
return contactFromFields(row.ID, row.AccessHash, row.Phone, row.FirstName, row.LastName, row.Username, row.CountryCode, row.Verified, row.Support, row.IsBot, int(row.BotInfoVersion), premiumUntilFromModel(row.PremiumExpiresAt), row.EmojiStatusDocumentID, int(row.EmojiStatusUntil), int(row.LastSeenAt), row.ContactFirstName, row.ContactLastName, row.ContactPhone, row.Note, entities, row.Mutual, row.CloseFriend), nil
}
func contactFromGetRow(row sqlcgen.GetContactRow) (domain.Contact, error) {
@ -478,7 +564,7 @@ func contactFromGetRow(row sqlcgen.GetContactRow) (domain.Contact, error) {
if err != nil {
return domain.Contact{}, fmt.Errorf("decode contact note entities: %w", err)
}
return contactFromFields(row.ID, row.AccessHash, row.Phone, row.FirstName, row.LastName, row.Username, row.CountryCode, row.Verified, row.Support, int(row.LastSeenAt), row.ContactFirstName, row.ContactLastName, row.ContactPhone, row.Note, entities, row.Mutual), nil
return contactFromFields(row.ID, row.AccessHash, row.Phone, row.FirstName, row.LastName, row.Username, row.CountryCode, row.Verified, row.Support, row.IsBot, int(row.BotInfoVersion), premiumUntilFromModel(row.PremiumExpiresAt), row.EmojiStatusDocumentID, int(row.EmojiStatusUntil), int(row.LastSeenAt), row.ContactFirstName, row.ContactLastName, row.ContactPhone, row.Note, entities, row.Mutual, row.CloseFriend), nil
}
func contactFromUpsertRow(row sqlcgen.UpsertContactRow) (domain.Contact, error) {
@ -486,7 +572,7 @@ func contactFromUpsertRow(row sqlcgen.UpsertContactRow) (domain.Contact, error)
if err != nil {
return domain.Contact{}, fmt.Errorf("decode contact note entities: %w", err)
}
return contactFromFields(row.ID, row.AccessHash, row.Phone, row.FirstName, row.LastName, row.Username, row.CountryCode, row.Verified, row.Support, int(row.LastSeenAt), row.ContactFirstName, row.ContactLastName, row.ContactPhone, row.Note, entities, row.Mutual), nil
return contactFromFields(row.ID, row.AccessHash, row.Phone, row.FirstName, row.LastName, row.Username, row.CountryCode, row.Verified, row.Support, row.IsBot, int(row.BotInfoVersion), premiumUntilFromModel(row.PremiumExpiresAt), row.EmojiStatusDocumentID, int(row.EmojiStatusUntil), int(row.LastSeenAt), row.ContactFirstName, row.ContactLastName, row.ContactPhone, row.Note, entities, row.Mutual, row.CloseFriend), nil
}
func contactFromUpdateNoteRow(row sqlcgen.UpdateContactNoteRow) (domain.Contact, error) {
@ -494,24 +580,35 @@ func contactFromUpdateNoteRow(row sqlcgen.UpdateContactNoteRow) (domain.Contact,
if err != nil {
return domain.Contact{}, fmt.Errorf("decode contact note entities: %w", err)
}
return contactFromFields(row.ID, row.AccessHash, row.Phone, row.FirstName, row.LastName, row.Username, row.CountryCode, row.Verified, row.Support, int(row.LastSeenAt), row.ContactFirstName, row.ContactLastName, row.ContactPhone, row.Note, entities, row.Mutual), nil
return contactFromFields(row.ID, row.AccessHash, row.Phone, row.FirstName, row.LastName, row.Username, row.CountryCode, row.Verified, row.Support, row.IsBot, int(row.BotInfoVersion), premiumUntilFromModel(row.PremiumExpiresAt), row.EmojiStatusDocumentID, int(row.EmojiStatusUntil), int(row.LastSeenAt), row.ContactFirstName, row.ContactLastName, row.ContactPhone, row.Note, entities, row.Mutual, row.CloseFriend), nil
}
func contactFromFields(id, accessHash int64, phone, firstName, lastName, username, countryCode string, verified, support bool, lastSeenAt int, contactFirstName, contactLastName, contactPhone, note string, noteEntities []domain.MessageEntity, mutual bool) domain.Contact {
// contactFromFields 组装 domain.Contact。getContacts 主路径(List/Get/Upsert/UpdateNote
// 四个 sqlc 查询)传真实 is_bot/bot_info_version;ImportContacts 等手机号批量导入的
// raw-scan 调用传 false/0——bot 无 phone 不经手机号导入,bot 加联系人走 username
// 的单条 UpsertContact 路径(已带真实 bot 列)。premium/emoji status 列所有路径必须
// 传真实值:TDesktop 对任何缺 emoji_status 字段的 user TL 一律清空本地状态。
func contactFromFields(id, accessHash int64, phone, firstName, lastName, username, countryCode string, verified, support, isBot bool, botInfoVersion, premiumUntil int, emojiStatusDocumentID int64, emojiStatusUntil, lastSeenAt int, contactFirstName, contactLastName, contactPhone, note string, noteEntities []domain.MessageEntity, mutual, closeFriend bool) domain.Contact {
return domain.Contact{
User: domain.User{
ID: id,
AccessHash: accessHash,
Phone: phone,
FirstName: firstName,
LastName: lastName,
Username: username,
CountryCode: countryCode,
Verified: verified,
Support: support,
LastSeenAt: lastSeenAt,
Contact: true,
Mutual: mutual,
ID: id,
AccessHash: accessHash,
Phone: phone,
FirstName: firstName,
LastName: lastName,
Username: username,
CountryCode: countryCode,
Verified: verified,
Support: support,
Bot: isBot,
BotInfoVersion: botInfoVersion,
PremiumUntil: premiumUntil,
EmojiStatusDocumentID: emojiStatusDocumentID,
EmojiStatusUntil: emojiStatusUntil,
LastSeenAt: lastSeenAt,
Contact: true,
Mutual: mutual,
CloseFriend: closeFriend,
},
FirstName: contactFirstName,
LastName: contactLastName,
@ -519,6 +616,7 @@ func contactFromFields(id, accessHash int64, phone, firstName, lastName, usernam
Note: note,
NoteEntities: noteEntities,
Mutual: mutual,
CloseFriend: closeFriend,
}
}
@ -530,6 +628,7 @@ func scanContactRows(row contactScanner) (domain.Contact, error) {
var (
contactUserID int64
mutual bool
closeFriend bool
contactPhone string
contactFirstName string
contactLastName string
@ -544,11 +643,15 @@ func scanContactRows(row contactScanner) (domain.Contact, error) {
countryCode string
verified bool
support bool
premiumUntil int64
emojiStatusDocID int64
emojiStatusUntil int64
lastSeenAt int32
)
if err := row.Scan(
&contactUserID,
&mutual,
&closeFriend,
&contactPhone,
&contactFirstName,
&contactLastName,
@ -563,6 +666,9 @@ func scanContactRows(row contactScanner) (domain.Contact, error) {
&countryCode,
&verified,
&support,
&premiumUntil,
&emojiStatusDocID,
&emojiStatusUntil,
&lastSeenAt,
); err != nil {
return domain.Contact{}, err
@ -571,13 +677,14 @@ func scanContactRows(row contactScanner) (domain.Contact, error) {
if err != nil {
return domain.Contact{}, err
}
return contactFromFields(id, accessHash, phone, firstName, lastName, username, countryCode, verified, support, int(lastSeenAt), contactFirstName, contactLastName, contactPhone, note, entities, mutual), nil
return contactFromFields(id, accessHash, phone, firstName, lastName, username, countryCode, verified, support, false, 0, int(premiumUntil), emojiStatusDocID, int(emojiStatusUntil), int(lastSeenAt), contactFirstName, contactLastName, contactPhone, note, entities, mutual, closeFriend), nil
}
func scanReverseContactRows(row contactScanner) (int64, domain.Contact, error) {
var (
ownerUserID int64
mutual bool
closeFriend bool
contactPhone string
contactFirstName string
contactLastName string
@ -592,11 +699,15 @@ func scanReverseContactRows(row contactScanner) (int64, domain.Contact, error) {
countryCode string
verified bool
support bool
premiumUntil int64
emojiStatusDocID int64
emojiStatusUntil int64
lastSeenAt int32
)
if err := row.Scan(
&ownerUserID,
&mutual,
&closeFriend,
&contactPhone,
&contactFirstName,
&contactLastName,
@ -611,6 +722,9 @@ func scanReverseContactRows(row contactScanner) (int64, domain.Contact, error) {
&countryCode,
&verified,
&support,
&premiumUntil,
&emojiStatusDocID,
&emojiStatusUntil,
&lastSeenAt,
); err != nil {
return 0, domain.Contact{}, err
@ -619,7 +733,7 @@ func scanReverseContactRows(row contactScanner) (int64, domain.Contact, error) {
if err != nil {
return 0, domain.Contact{}, err
}
contact := contactFromFields(id, accessHash, phone, firstName, lastName, username, countryCode, verified, support, int(lastSeenAt), contactFirstName, contactLastName, contactPhone, note, entities, mutual)
contact := contactFromFields(id, accessHash, phone, firstName, lastName, username, countryCode, verified, support, false, 0, int(premiumUntil), emojiStatusDocID, int(emojiStatusUntil), int(lastSeenAt), contactFirstName, contactLastName, contactPhone, note, entities, mutual, closeFriend)
return ownerUserID, contact, nil
}
@ -748,7 +862,12 @@ func contactListHash(contacts []domain.Contact) int64 {
} else {
buf[8] = 0
}
_, _ = h.Write(buf[:9])
if c.CloseFriend || c.User.CloseFriend {
buf[9] = 1
} else {
buf[9] = 0
}
_, _ = h.Write(buf[:10])
_, _ = h.Write([]byte(c.FirstName))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(c.LastName))