fix: sync StarGift private message references
This commit is contained in:
parent
1a2d03f529
commit
30774f8c39
12 changed files with 580 additions and 15 deletions
|
|
@ -0,0 +1,4 @@
|
||||||
|
-- This migration emits durable per-user edit_message events. Reverting the
|
||||||
|
-- repaired ids or rewinding pts would reintroduce cross-account references and
|
||||||
|
-- create holes in updates.getDifference, so rollback intentionally preserves
|
||||||
|
-- both the corrected snapshots and their update facts.
|
||||||
186
deploy/migrations/0124_star_gift_private_box_local_refs.up.sql
Normal file
186
deploy/migrations/0124_star_gift_private_box_local_refs.up.sql
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
-- Private message box ids are account-local. Repair user-owned Star Gift
|
||||||
|
-- service actions that copied the owner's msg_id into both participants'
|
||||||
|
-- message boxes, and publish durable edit_message facts for already-visible
|
||||||
|
-- incorrect projections.
|
||||||
|
|
||||||
|
CREATE TEMP TABLE star_gift_box_media_repairs (
|
||||||
|
owner_user_id bigint NOT NULL,
|
||||||
|
box_id integer NOT NULL,
|
||||||
|
peer_type text NOT NULL,
|
||||||
|
peer_id bigint NOT NULL,
|
||||||
|
repaired_media jsonb NOT NULL,
|
||||||
|
PRIMARY KEY (owner_user_id, box_id)
|
||||||
|
) ON COMMIT DROP;
|
||||||
|
|
||||||
|
-- An upgrade action points back to the original ordinary gift. user saved_id
|
||||||
|
-- is a management identity, not a conversation message link: only the current
|
||||||
|
-- gift owner's box may carry it. The other participant must omit the field.
|
||||||
|
INSERT INTO star_gift_box_media_repairs (
|
||||||
|
owner_user_id, box_id, peer_type, peer_id, repaired_media
|
||||||
|
)
|
||||||
|
SELECT unique_box.owner_user_id,
|
||||||
|
unique_box.box_id,
|
||||||
|
unique_box.peer_type,
|
||||||
|
unique_box.peer_id,
|
||||||
|
CASE
|
||||||
|
WHEN unique_box.owner_user_id = gift.owner_peer_id THEN jsonb_set(
|
||||||
|
unique_box.media,
|
||||||
|
'{service_action,star_gift_unique,saved_id}',
|
||||||
|
to_jsonb(gift.msg_id::bigint),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
ELSE unique_box.media #- '{service_action,star_gift_unique,saved_id}'
|
||||||
|
END
|
||||||
|
FROM peer_star_gifts gift
|
||||||
|
JOIN message_boxes upgrade_owner
|
||||||
|
ON upgrade_owner.owner_user_id = gift.owner_peer_id
|
||||||
|
AND upgrade_owner.box_id = gift.upgrade_msg_id
|
||||||
|
JOIN message_boxes unique_box
|
||||||
|
ON unique_box.message_sender_id = upgrade_owner.message_sender_id
|
||||||
|
AND unique_box.private_message_id = upgrade_owner.private_message_id
|
||||||
|
WHERE gift.owner_peer_type = 'user'
|
||||||
|
AND gift.unique_gift_id IS NOT NULL
|
||||||
|
AND gift.msg_id > 0
|
||||||
|
AND gift.upgrade_msg_id > 0
|
||||||
|
AND NOT unique_box.deleted
|
||||||
|
AND unique_box.media #>> '{service_action,kind}' = 'star_gift_unique'
|
||||||
|
AND unique_box.media #>> '{service_action,star_gift_unique,upgrade}' = 'true'
|
||||||
|
AND unique_box.media IS DISTINCT FROM CASE
|
||||||
|
WHEN unique_box.owner_user_id = gift.owner_peer_id THEN jsonb_set(
|
||||||
|
unique_box.media,
|
||||||
|
'{service_action,star_gift_unique,saved_id}',
|
||||||
|
to_jsonb(gift.msg_id::bigint),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
ELSE unique_box.media #- '{service_action,star_gift_unique,saved_id}'
|
||||||
|
END
|
||||||
|
ON CONFLICT (owner_user_id, box_id) DO UPDATE
|
||||||
|
SET repaired_media = EXCLUDED.repaired_media;
|
||||||
|
|
||||||
|
-- For every other user-target unique action (transfer, resale, offer accept,
|
||||||
|
-- craft), the action message itself is the new user saved-gift identity.
|
||||||
|
-- saved_id is a channel-only field there and must be absent from every box.
|
||||||
|
INSERT INTO star_gift_box_media_repairs (
|
||||||
|
owner_user_id, box_id, peer_type, peer_id, repaired_media
|
||||||
|
)
|
||||||
|
SELECT box.owner_user_id,
|
||||||
|
box.box_id,
|
||||||
|
box.peer_type,
|
||||||
|
box.peer_id,
|
||||||
|
box.media #- '{service_action,star_gift_unique,saved_id}'
|
||||||
|
FROM message_boxes box
|
||||||
|
WHERE NOT box.deleted
|
||||||
|
AND box.media #>> '{service_action,kind}' = 'star_gift_unique'
|
||||||
|
AND box.media #>> '{service_action,star_gift_unique,peer,Type}' = 'user'
|
||||||
|
AND COALESCE((box.media #>> '{service_action,star_gift_unique,upgrade}')::boolean, false) = false
|
||||||
|
AND box.media #> '{service_action,star_gift_unique,saved_id}' IS NOT NULL
|
||||||
|
ON CONFLICT (owner_user_id, box_id) DO UPDATE
|
||||||
|
SET repaired_media = EXCLUDED.repaired_media;
|
||||||
|
|
||||||
|
-- A separate prepaid-upgrade action points to the same ordinary gift.
|
||||||
|
-- Telegram defines gift_msg_id as receiver-only, so retain it only in the
|
||||||
|
-- owner's service-message box and remove it from the payer's outgoing copy.
|
||||||
|
INSERT INTO star_gift_box_media_repairs (
|
||||||
|
owner_user_id, box_id, peer_type, peer_id, repaired_media
|
||||||
|
)
|
||||||
|
SELECT prepay_box.owner_user_id,
|
||||||
|
prepay_box.box_id,
|
||||||
|
prepay_box.peer_type,
|
||||||
|
prepay_box.peer_id,
|
||||||
|
CASE
|
||||||
|
WHEN prepay_box.owner_user_id = gift.owner_peer_id THEN jsonb_set(
|
||||||
|
prepay_box.media,
|
||||||
|
'{service_action,star_gift,gift_msg_id}',
|
||||||
|
to_jsonb(gift.msg_id::bigint),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
ELSE prepay_box.media #- '{service_action,star_gift,gift_msg_id}'
|
||||||
|
END
|
||||||
|
FROM peer_star_gifts gift
|
||||||
|
JOIN message_boxes prepay_owner
|
||||||
|
ON prepay_owner.owner_user_id = gift.owner_peer_id
|
||||||
|
AND prepay_owner.media #>> '{service_action,kind}' = 'star_gift'
|
||||||
|
AND prepay_owner.media #>> '{service_action,star_gift,prepaid_upgrade}' = 'true'
|
||||||
|
AND prepay_owner.media #>> '{service_action,star_gift,upgrade_separate}' = 'true'
|
||||||
|
AND (prepay_owner.media #>> '{service_action,star_gift,gift_msg_id}')::integer = gift.msg_id
|
||||||
|
AND (prepay_owner.media #>> '{service_action,star_gift,gift_id}')::bigint = gift.gift_id
|
||||||
|
JOIN message_boxes prepay_box
|
||||||
|
ON prepay_box.message_sender_id = prepay_owner.message_sender_id
|
||||||
|
AND prepay_box.private_message_id = prepay_owner.private_message_id
|
||||||
|
WHERE gift.owner_peer_type = 'user'
|
||||||
|
AND gift.msg_id > 0
|
||||||
|
AND NOT prepay_box.deleted
|
||||||
|
AND prepay_box.media IS DISTINCT FROM CASE
|
||||||
|
WHEN prepay_box.owner_user_id = gift.owner_peer_id THEN jsonb_set(
|
||||||
|
prepay_box.media,
|
||||||
|
'{service_action,star_gift,gift_msg_id}',
|
||||||
|
to_jsonb(gift.msg_id::bigint),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
ELSE prepay_box.media #- '{service_action,star_gift,gift_msg_id}'
|
||||||
|
END
|
||||||
|
ON CONFLICT (owner_user_id, box_id) DO UPDATE
|
||||||
|
SET repaired_media = EXCLUDED.repaired_media;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
repair record;
|
||||||
|
next_pts integer;
|
||||||
|
event_date integer := EXTRACT(EPOCH FROM clock_timestamp())::integer;
|
||||||
|
BEGIN
|
||||||
|
FOR repair IN
|
||||||
|
SELECT owner_user_id, box_id, peer_type, peer_id, repaired_media
|
||||||
|
FROM star_gift_box_media_repairs
|
||||||
|
ORDER BY owner_user_id, box_id
|
||||||
|
LOOP
|
||||||
|
INSERT INTO user_update_watermarks (user_id, contiguous_pts)
|
||||||
|
VALUES (repair.owner_user_id, 0)
|
||||||
|
ON CONFLICT (user_id) DO NOTHING;
|
||||||
|
|
||||||
|
UPDATE user_update_watermarks
|
||||||
|
SET contiguous_pts = contiguous_pts + 1,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE user_id = repair.owner_user_id
|
||||||
|
RETURNING contiguous_pts INTO next_pts;
|
||||||
|
|
||||||
|
UPDATE message_boxes
|
||||||
|
SET media = repair.repaired_media,
|
||||||
|
pts = next_pts
|
||||||
|
WHERE owner_user_id = repair.owner_user_id
|
||||||
|
AND box_id = repair.box_id
|
||||||
|
AND NOT deleted;
|
||||||
|
|
||||||
|
INSERT INTO user_update_events (
|
||||||
|
user_id, pts, pts_count, date, event_type,
|
||||||
|
message_box_id, peer_type, peer_id
|
||||||
|
) VALUES (
|
||||||
|
repair.owner_user_id, next_pts, 1, event_date, 'edit_message',
|
||||||
|
repair.box_id, repair.peer_type, repair.peer_id
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO dispatch_outbox (
|
||||||
|
target_user_id, pts, event_type,
|
||||||
|
exclude_auth_key_id, exclude_session_id
|
||||||
|
) VALUES (repair.owner_user_id, next_pts, 'edit_message', 0, 0);
|
||||||
|
END LOOP;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- private_messages is the logical shared envelope and cannot contain either
|
||||||
|
-- participant's local message id. User-visible history/difference always reads
|
||||||
|
-- the per-owner message_boxes snapshots repaired above.
|
||||||
|
UPDATE private_messages
|
||||||
|
SET media = media
|
||||||
|
#- '{service_action,star_gift,saved_id}'
|
||||||
|
#- '{service_action,star_gift,gift_msg_id}'
|
||||||
|
#- '{service_action,star_gift,upgrade_msg_id}'
|
||||||
|
WHERE media #>> '{service_action,kind}' = 'star_gift'
|
||||||
|
AND (
|
||||||
|
media #> '{service_action,star_gift,peer_user_id}' IS NOT NULL
|
||||||
|
OR media #>> '{service_action,star_gift,to,Type}' = 'user'
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE private_messages
|
||||||
|
SET media = media #- '{service_action,star_gift_unique,saved_id}'
|
||||||
|
WHERE media #>> '{service_action,kind}' = 'star_gift_unique'
|
||||||
|
AND media #>> '{service_action,star_gift_unique,peer,Type}' = 'user';
|
||||||
|
|
@ -117,8 +117,19 @@ func (s *MessageStore) SendPrivateText(ctx context.Context, req domain.SendPriva
|
||||||
}
|
}
|
||||||
|
|
||||||
type privateSendTxHooks struct {
|
type privateSendTxHooks struct {
|
||||||
before func(context.Context, pgx.Tx, *domain.SendPrivateTextRequest) error
|
before func(context.Context, pgx.Tx, *domain.SendPrivateTextRequest) error
|
||||||
after func(context.Context, pgx.Tx, domain.SendPrivateTextResult) error
|
projectMedia func(context.Context, pgx.Tx, *domain.SendPrivateTextRequest) (privateSendMediaProjection, error)
|
||||||
|
after func(context.Context, pgx.Tx, domain.SendPrivateTextResult) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// privateSendMediaProjection separates the logical private-message payload
|
||||||
|
// from the two account-local message-box projections. Most messages use the
|
||||||
|
// same media for all three fields. Service actions that carry message ids must
|
||||||
|
// project those ids per account because box ids are not shared by both users.
|
||||||
|
type privateSendMediaProjection struct {
|
||||||
|
Shared *domain.MessageMedia
|
||||||
|
Sender *domain.MessageMedia
|
||||||
|
Recipient *domain.MessageMedia
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MessageStore) sendPrivateTextWithHooks(ctx context.Context, req domain.SendPrivateTextRequest, hooks privateSendTxHooks) (res domain.SendPrivateTextResult, err error) {
|
func (s *MessageStore) sendPrivateTextWithHooks(ctx context.Context, req domain.SendPrivateTextRequest, hooks privateSendTxHooks) (res domain.SendPrivateTextResult, err error) {
|
||||||
|
|
@ -228,7 +239,22 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP
|
||||||
return domain.SendPrivateTextResult{}, err
|
return domain.SendPrivateTextResult{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mediaJSON, err := encodeMessageMedia(req.Media)
|
media := privateSendMediaProjection{Shared: req.Media, Sender: req.Media, Recipient: req.Media}
|
||||||
|
if hooks.projectMedia != nil {
|
||||||
|
media, err = hooks.projectMedia(ctx, tx, &req)
|
||||||
|
if err != nil {
|
||||||
|
return domain.SendPrivateTextResult{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sharedMediaJSON, err := encodeMessageMedia(media.Shared)
|
||||||
|
if err != nil {
|
||||||
|
return domain.SendPrivateTextResult{}, err
|
||||||
|
}
|
||||||
|
senderMediaJSON, err := encodeMessageMedia(media.Sender)
|
||||||
|
if err != nil {
|
||||||
|
return domain.SendPrivateTextResult{}, err
|
||||||
|
}
|
||||||
|
recipientMediaJSON, err := encodeMessageMedia(media.Recipient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.SendPrivateTextResult{}, err
|
return domain.SendPrivateTextResult{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -255,7 +281,7 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP
|
||||||
TtlPeriod: int32(ttlPeriod),
|
TtlPeriod: int32(ttlPeriod),
|
||||||
ExpiresAt: int32(expiresAt),
|
ExpiresAt: int32(expiresAt),
|
||||||
EntitiesJson: entities,
|
EntitiesJson: entities,
|
||||||
MediaJson: mediaJSON,
|
MediaJson: sharedMediaJSON,
|
||||||
ReplyMarkupJson: replyMarkupJSON,
|
ReplyMarkupJson: replyMarkupJSON,
|
||||||
RichMessageJson: richMessageJSON,
|
RichMessageJson: richMessageJSON,
|
||||||
ViaBotID: req.ViaBotID,
|
ViaBotID: req.ViaBotID,
|
||||||
|
|
@ -315,7 +341,7 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP
|
||||||
ExpiresAt: int32(expiresAt),
|
ExpiresAt: int32(expiresAt),
|
||||||
EntitiesJson: entities,
|
EntitiesJson: entities,
|
||||||
Pts: int32(senderPts),
|
Pts: int32(senderPts),
|
||||||
MediaJson: mediaJSON,
|
MediaJson: senderMediaJSON,
|
||||||
ReplyMarkupJson: replyMarkupJSON,
|
ReplyMarkupJson: replyMarkupJSON,
|
||||||
RichMessageJson: richMessageJSON,
|
RichMessageJson: richMessageJSON,
|
||||||
ViaBotID: req.ViaBotID,
|
ViaBotID: req.ViaBotID,
|
||||||
|
|
@ -323,7 +349,7 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP
|
||||||
Effect: req.Effect,
|
Effect: req.Effect,
|
||||||
// voice/round 在发送者自己的副本上也保持"未听",直到对端
|
// voice/round 在发送者自己的副本上也保持"未听",直到对端
|
||||||
// readMessageContents 触发 sender 侧清除;发给自己无人可听,恒已读。
|
// readMessageContents 触发 sender 侧清除;发给自己无人可听,恒已读。
|
||||||
MediaUnread: req.Media.HasUnreadPayload() && !selfMessage,
|
MediaUnread: media.Sender.HasUnreadPayload() && !selfMessage,
|
||||||
ReactionUnread: false,
|
ReactionUnread: false,
|
||||||
}
|
}
|
||||||
applyCreateMessageBoxMetadata(&senderArg, senderMeta)
|
applyCreateMessageBoxMetadata(&senderArg, senderMeta)
|
||||||
|
|
@ -334,7 +360,7 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP
|
||||||
sender := messageFromBoxRow(senderRow)
|
sender := messageFromBoxRow(senderRow)
|
||||||
sender.RandomID = req.RandomID
|
sender.RandomID = req.RandomID
|
||||||
// 共享媒体索引(0118):发送者侧 box 按媒体类别建索引(peer=收件人)。
|
// 共享媒体索引(0118):发送者侧 box 按媒体类别建索引(peer=收件人)。
|
||||||
if err := insertMessageBoxMediaIndexTx(ctx, tx, req.SenderUserID, req.RecipientUserID, int(senderBoxID), req.Date, req.Media, req.Entities); err != nil {
|
if err := insertMessageBoxMediaIndexTx(ctx, tx, req.SenderUserID, req.RecipientUserID, int(senderBoxID), req.Date, media.Sender, req.Entities); err != nil {
|
||||||
return domain.SendPrivateTextResult{}, err
|
return domain.SendPrivateTextResult{}, err
|
||||||
}
|
}
|
||||||
if err := qtx.UpsertOutboxDialog(ctx, sqlcgen.UpsertOutboxDialogParams{
|
if err := qtx.UpsertOutboxDialog(ctx, sqlcgen.UpsertOutboxDialogParams{
|
||||||
|
|
@ -388,13 +414,13 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP
|
||||||
ExpiresAt: int32(expiresAt),
|
ExpiresAt: int32(expiresAt),
|
||||||
EntitiesJson: entities,
|
EntitiesJson: entities,
|
||||||
Pts: int32(recipientPts),
|
Pts: int32(recipientPts),
|
||||||
MediaJson: mediaJSON,
|
MediaJson: recipientMediaJSON,
|
||||||
ReplyMarkupJson: replyMarkupJSON,
|
ReplyMarkupJson: replyMarkupJSON,
|
||||||
RichMessageJson: richMessageJSON,
|
RichMessageJson: richMessageJSON,
|
||||||
ViaBotID: req.ViaBotID,
|
ViaBotID: req.ViaBotID,
|
||||||
GroupedID: req.GroupedID,
|
GroupedID: req.GroupedID,
|
||||||
Effect: req.Effect,
|
Effect: req.Effect,
|
||||||
MediaUnread: req.Media.HasUnreadPayload(),
|
MediaUnread: media.Recipient.HasUnreadPayload(),
|
||||||
ReactionUnread: false,
|
ReactionUnread: false,
|
||||||
}
|
}
|
||||||
applyCreateMessageBoxMetadata(&recipientArg, recipientMeta)
|
applyCreateMessageBoxMetadata(&recipientArg, recipientMeta)
|
||||||
|
|
@ -405,7 +431,7 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP
|
||||||
recipient = messageFromBoxRow(recipientRow)
|
recipient = messageFromBoxRow(recipientRow)
|
||||||
recipient.RandomID = req.RandomID
|
recipient.RandomID = req.RandomID
|
||||||
// 共享媒体索引(0118):收件人侧 box 按媒体类别建索引(peer=发送者)。
|
// 共享媒体索引(0118):收件人侧 box 按媒体类别建索引(peer=发送者)。
|
||||||
if err := insertMessageBoxMediaIndexTx(ctx, tx, req.RecipientUserID, req.SenderUserID, int(recipientBoxID), req.Date, req.Media, req.Entities); err != nil {
|
if err := insertMessageBoxMediaIndexTx(ctx, tx, req.RecipientUserID, req.SenderUserID, int(recipientBoxID), req.Date, media.Recipient, req.Entities); err != nil {
|
||||||
return domain.SendPrivateTextResult{}, err
|
return domain.SendPrivateTextResult{}, err
|
||||||
}
|
}
|
||||||
if err := qtx.UpsertInboxDialog(ctx, sqlcgen.UpsertInboxDialogParams{
|
if err := qtx.UpsertInboxDialog(ctx, sqlcgen.UpsertInboxDialogParams{
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,10 @@ func TestStarGiftCollectibleUpgradeAggregatePostgres(t *testing.T) {
|
||||||
if uniqueAction.SavedID != int64(saved.MsgID) {
|
if uniqueAction.SavedID != int64(saved.MsgID) {
|
||||||
t.Fatalf("unique action saved_id = %d, want stable source msg id %d", uniqueAction.SavedID, saved.MsgID)
|
t.Fatalf("unique action saved_id = %d, want stable source msg id %d", uniqueAction.SavedID, saved.MsgID)
|
||||||
}
|
}
|
||||||
|
senderUniqueAction := upgraded.Send.SenderMessage.Media.ServiceAction.StarGiftUnique
|
||||||
|
if senderUniqueAction == nil || senderUniqueAction.SavedID != 0 {
|
||||||
|
t.Fatalf("sender unique action leaked owner-only saved_id: %+v", senderUniqueAction)
|
||||||
|
}
|
||||||
ownerSourceEdit := upgradedSourceEditForUser(upgraded, owner.ID)
|
ownerSourceEdit := upgradedSourceEditForUser(upgraded, owner.ID)
|
||||||
if ownerSourceEdit.Event.Pts <= ownerMessage.Pts || ownerSourceEdit.Message.Media == nil ||
|
if ownerSourceEdit.Event.Pts <= ownerMessage.Pts || ownerSourceEdit.Message.Media == nil ||
|
||||||
ownerSourceEdit.Message.Media.ServiceAction == nil || ownerSourceEdit.Message.Media.ServiceAction.StarGift == nil ||
|
ownerSourceEdit.Message.Media.ServiceAction == nil || ownerSourceEdit.Message.Media.ServiceAction.StarGift == nil ||
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,10 @@ WHERE owner_user_id=$1 AND box_id=$2 AND NOT deleted`, box.OwnerUserID, box.BoxI
|
||||||
return nil, 0, fmt.Errorf("enqueue craft input edit: %w", err)
|
return nil, 0, fmt.Errorf("enqueue craft input edit: %w", err)
|
||||||
}
|
}
|
||||||
if box.OwnerUserID == box.MessageSenderID || len(privateMediaJSON) == 0 {
|
if box.OwnerUserID == box.MessageSenderID || len(privateMediaJSON) == 0 {
|
||||||
privateMediaJSON = mediaJSON
|
privateMediaJSON, err = encodeSharedPrivateStarGiftMedia(media)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
edits = append(edits, domain.EditedMessageForUser{UserID: msg.OwnerUserID, Message: msg, Event: event})
|
edits = append(edits, domain.EditedMessageForUser{UserID: msg.OwnerUserID, Message: msg, Event: event})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,11 @@ VALUES($1,$2,$3,$4,$5,$6,$7)`, req.PayerUserID, req.CommandKey, locked.ID, req.F
|
||||||
locked.PrepaidUpgradeStars, locked.PrepaidUpgradeHash = req.ChargeStars, ""
|
locked.PrepaidUpgradeStars, locked.PrepaidUpgradeHash = req.ChargeStars, ""
|
||||||
result.Saved, result.Balance = locked, balance
|
result.Saved, result.Balance = locked, balance
|
||||||
return nil
|
return nil
|
||||||
|
}, projectMedia: func(ctx context.Context, tx pgx.Tx, messageReq *domain.SendPrivateTextRequest) (privateSendMediaProjection, error) {
|
||||||
|
if result.Saved.Owner.Type != domain.PeerTypeUser {
|
||||||
|
return privateSendMediaProjection{Shared: messageReq.Media, Sender: messageReq.Media, Recipient: messageReq.Media}, nil
|
||||||
|
}
|
||||||
|
return projectPrivateStarGiftSourceRef(ctx, tx, messageReq, result.Saved.Owner.ID, result.Saved.MsgID)
|
||||||
}, after: func(ctx context.Context, tx pgx.Tx, sent domain.SendPrivateTextResult) error {
|
}, after: func(ctx context.Context, tx pgx.Tx, sent domain.SendPrivateTextResult) error {
|
||||||
if req.Owner.Type != domain.PeerTypeChannel {
|
if req.Owner.Type != domain.PeerTypeChannel {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -1148,8 +1148,15 @@ func ensureNoStarGiftMarketConflict(ctx context.Context, tx pgx.Tx, uniqueID int
|
||||||
}
|
}
|
||||||
|
|
||||||
func transferUniqueAction(unique domain.UniqueStarGift, fromUserID int64, to domain.Peer, saved domain.SavedStarGift) *domain.MessageStarGiftUniqueAction {
|
func transferUniqueAction(unique domain.UniqueStarGift, fromUserID int64, to domain.Peer, saved domain.SavedStarGift) *domain.MessageStarGiftUniqueAction {
|
||||||
|
savedID := saved.SavedID
|
||||||
|
if to.Type == domain.PeerTypeUser {
|
||||||
|
// For a user-owned transferred gift the action message itself becomes
|
||||||
|
// inputSavedStarGiftUser.msg_id. A channel saved_id belongs to a different
|
||||||
|
// identity namespace and must never leak into the recipient's user view.
|
||||||
|
savedID = 0
|
||||||
|
}
|
||||||
return &domain.MessageStarGiftUniqueAction{Gift: unique, FromUserID: fromUserID, Peer: to,
|
return &domain.MessageStarGiftUniqueAction{Gift: unique, FromUserID: fromUserID, Peer: to,
|
||||||
SavedID: saved.SavedID, Transferred: true, Saved: true, CanExportAt: saved.CanExportAt,
|
SavedID: savedID, Transferred: true, Saved: true, CanExportAt: saved.CanExportAt,
|
||||||
TransferStars: saved.TransferStars, CanTransferAt: saved.CanTransferAt, CanResellAt: saved.CanResellAt,
|
TransferStars: saved.TransferStars, CanTransferAt: saved.CanTransferAt, CanResellAt: saved.CanResellAt,
|
||||||
DropOriginalDetailsStars: saved.DropOriginalDetailsStars, CanCraftAt: saved.CanCraftAt}
|
DropOriginalDetailsStars: saved.DropOriginalDetailsStars, CanCraftAt: saved.CanCraftAt}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,39 @@ func TestStarGiftLifecycleAggregatePostgres(t *testing.T) {
|
||||||
if err != nil || prepaid.Saved.PrepaidUpgradeStars != 100 || prepaid.Saved.PrepaidUpgradeHash != "" || prepaid.Balance.Balance != 9850 {
|
if err != nil || prepaid.Saved.PrepaidUpgradeStars != 100 || prepaid.Saved.PrepaidUpgradeHash != "" || prepaid.Balance.Balance != 9850 {
|
||||||
t.Fatalf("prepay upgrade = %+v err %v", prepaid, err)
|
t.Fatalf("prepay upgrade = %+v err %v", prepaid, err)
|
||||||
}
|
}
|
||||||
|
prepaySenderAction := prepaid.Send.SenderMessage.Media.ServiceAction.StarGift
|
||||||
|
prepayOwnerAction := prepaid.Send.RecipientMessage.Media.ServiceAction.StarGift
|
||||||
|
if prepaySenderAction == nil || prepayOwnerAction == nil ||
|
||||||
|
prepaySenderAction.GiftMsgID != 0 ||
|
||||||
|
prepayOwnerAction.GiftMsgID != purchased.Send.RecipientMessage.ID {
|
||||||
|
t.Fatalf("prepay gift_msg_id is not owner-only: sender=%+v owner=%+v purchase=%+v",
|
||||||
|
prepaySenderAction, prepayOwnerAction, purchased.Send)
|
||||||
|
}
|
||||||
|
prepaySenderDifference, err := NewUpdateEventStore(pool).ListAfter(ctx, buyer.ID, prepaid.Send.SenderMessage.Pts-1, 1)
|
||||||
|
if err != nil || len(prepaySenderDifference) != 1 || prepaySenderDifference[0].Message.Media == nil ||
|
||||||
|
prepaySenderDifference[0].Message.Media.ServiceAction == nil ||
|
||||||
|
prepaySenderDifference[0].Message.Media.ServiceAction.StarGift == nil ||
|
||||||
|
prepaySenderDifference[0].Message.Media.ServiceAction.StarGift.GiftMsgID != 0 {
|
||||||
|
t.Fatalf("payer prepay difference leaked owner-only gift_msg_id: events=%+v err=%v", prepaySenderDifference, err)
|
||||||
|
}
|
||||||
|
prepayOwnerDifference, err := NewUpdateEventStore(pool).ListAfter(ctx, owner.ID, prepaid.Send.RecipientMessage.Pts-1, 1)
|
||||||
|
if err != nil || len(prepayOwnerDifference) != 1 || prepayOwnerDifference[0].Message.Media == nil ||
|
||||||
|
prepayOwnerDifference[0].Message.Media.ServiceAction == nil ||
|
||||||
|
prepayOwnerDifference[0].Message.Media.ServiceAction.StarGift == nil ||
|
||||||
|
prepayOwnerDifference[0].Message.Media.ServiceAction.StarGift.GiftMsgID != purchased.Send.RecipientMessage.ID {
|
||||||
|
t.Fatalf("owner prepay difference lost box-local gift_msg_id: events=%+v err=%v", prepayOwnerDifference, err)
|
||||||
|
}
|
||||||
|
var sharedPrepayMediaJSON string
|
||||||
|
if err := pool.QueryRow(ctx, `SELECT p.media::text FROM private_messages p
|
||||||
|
JOIN message_boxes b ON b.message_sender_id=p.sender_user_id AND b.private_message_id=p.id
|
||||||
|
WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, prepaid.Send.RecipientMessage.ID).Scan(&sharedPrepayMediaJSON); err != nil {
|
||||||
|
t.Fatalf("load shared prepay media: %v", err)
|
||||||
|
}
|
||||||
|
sharedPrepayMedia, err := decodeMessageMedia(sharedPrepayMediaJSON)
|
||||||
|
if err != nil || sharedPrepayMedia == nil || sharedPrepayMedia.ServiceAction == nil ||
|
||||||
|
sharedPrepayMedia.ServiceAction.StarGift == nil || sharedPrepayMedia.ServiceAction.StarGift.GiftMsgID != 0 {
|
||||||
|
t.Fatalf("shared prepay media retained account-local gift_msg_id: media=%+v err=%v", sharedPrepayMedia, err)
|
||||||
|
}
|
||||||
upgraded, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{
|
upgraded, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{
|
||||||
UserID: owner.ID, Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: purchased.Saved.MsgID},
|
UserID: owner.ID, Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: purchased.Saved.MsgID},
|
||||||
RequirePrepaid: true, KeepOriginalDetails: true, CommandKey: "upgrade-" + suffix, Date: now + 2,
|
RequirePrepaid: true, KeepOriginalDetails: true, CommandKey: "upgrade-" + suffix, Date: now + 2,
|
||||||
|
|
@ -111,14 +144,40 @@ func TestStarGiftLifecycleAggregatePostgres(t *testing.T) {
|
||||||
t.Fatalf("issued lifecycle snapshot = saved %+v unique %+v", upgraded.Saved, upgraded.Unique)
|
t.Fatalf("issued lifecycle snapshot = saved %+v unique %+v", upgraded.Saved, upgraded.Unique)
|
||||||
}
|
}
|
||||||
upgradeAction := upgraded.Send.RecipientMessage.Media.ServiceAction.StarGiftUnique
|
upgradeAction := upgraded.Send.RecipientMessage.Media.ServiceAction.StarGiftUnique
|
||||||
|
senderUpgradeAction := upgraded.Send.SenderMessage.Media.ServiceAction.StarGiftUnique
|
||||||
ownerSourceEdit := upgradedSourceEditForUser(upgraded, owner.ID)
|
ownerSourceEdit := upgradedSourceEditForUser(upgraded, owner.ID)
|
||||||
if upgradeAction == nil || upgradeAction.SavedID != int64(purchased.Saved.MsgID) ||
|
if upgradeAction == nil || upgradeAction.SavedID != int64(purchased.Saved.MsgID) ||
|
||||||
|
senderUpgradeAction == nil || senderUpgradeAction.SavedID != 0 ||
|
||||||
ownerSourceEdit.Message.Media == nil || ownerSourceEdit.Message.Media.ServiceAction == nil ||
|
ownerSourceEdit.Message.Media == nil || ownerSourceEdit.Message.Media.ServiceAction == nil ||
|
||||||
ownerSourceEdit.Message.Media.ServiceAction.StarGift == nil ||
|
ownerSourceEdit.Message.Media.ServiceAction.StarGift == nil ||
|
||||||
ownerSourceEdit.Message.Media.ServiceAction.StarGift.UpgradeMsgID != upgraded.Saved.UpgradeMsgID ||
|
ownerSourceEdit.Message.Media.ServiceAction.StarGift.UpgradeMsgID != upgraded.Saved.UpgradeMsgID ||
|
||||||
ownerSourceEdit.Message.Media.ServiceAction.StarGift.CanUpgrade {
|
ownerSourceEdit.Message.Media.ServiceAction.StarGift.CanUpgrade {
|
||||||
t.Fatalf("upgrade message linkage = action %+v source edit %+v", upgradeAction, ownerSourceEdit)
|
t.Fatalf("upgrade message linkage = action %+v source edit %+v", upgradeAction, ownerSourceEdit)
|
||||||
}
|
}
|
||||||
|
var sharedUpgradeSourceMediaJSON string
|
||||||
|
if err := pool.QueryRow(ctx, `SELECT p.media::text FROM private_messages p
|
||||||
|
JOIN message_boxes b ON b.message_sender_id=p.sender_user_id AND b.private_message_id=p.id
|
||||||
|
WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, purchased.Saved.MsgID).Scan(&sharedUpgradeSourceMediaJSON); err != nil {
|
||||||
|
t.Fatalf("load shared upgraded source media: %v", err)
|
||||||
|
}
|
||||||
|
sharedUpgradeSourceMedia, err := decodeMessageMedia(sharedUpgradeSourceMediaJSON)
|
||||||
|
if err != nil || sharedUpgradeSourceMedia == nil || sharedUpgradeSourceMedia.ServiceAction == nil ||
|
||||||
|
sharedUpgradeSourceMedia.ServiceAction.StarGift == nil ||
|
||||||
|
sharedUpgradeSourceMedia.ServiceAction.StarGift.UpgradeMsgID != 0 ||
|
||||||
|
sharedUpgradeSourceMedia.ServiceAction.StarGift.GiftMsgID != 0 {
|
||||||
|
t.Fatalf("shared upgraded source media retained account-local message id: media=%+v err=%v", sharedUpgradeSourceMedia, err)
|
||||||
|
}
|
||||||
|
var sharedUpgradeMediaJSON string
|
||||||
|
if err := pool.QueryRow(ctx, `SELECT p.media::text FROM private_messages p
|
||||||
|
JOIN message_boxes b ON b.message_sender_id=p.sender_user_id AND b.private_message_id=p.id
|
||||||
|
WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, upgraded.Send.RecipientMessage.ID).Scan(&sharedUpgradeMediaJSON); err != nil {
|
||||||
|
t.Fatalf("load shared upgrade media: %v", err)
|
||||||
|
}
|
||||||
|
sharedUpgradeMedia, err := decodeMessageMedia(sharedUpgradeMediaJSON)
|
||||||
|
if err != nil || sharedUpgradeMedia == nil || sharedUpgradeMedia.ServiceAction == nil ||
|
||||||
|
sharedUpgradeMedia.ServiceAction.StarGiftUnique == nil || sharedUpgradeMedia.ServiceAction.StarGiftUnique.SavedID != 0 {
|
||||||
|
t.Fatalf("shared upgrade media retained account-local saved_id: media=%+v err=%v", sharedUpgradeMedia, err)
|
||||||
|
}
|
||||||
dropped, err := lifecycle.DropStarGiftOriginalDetails(ctx, domain.StarGiftDropOriginalDetailsRequest{
|
dropped, err := lifecycle.DropStarGiftOriginalDetails(ctx, domain.StarGiftDropOriginalDetailsRequest{
|
||||||
UserID: owner.ID, Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: purchased.Saved.MsgID},
|
UserID: owner.ID, Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: purchased.Saved.MsgID},
|
||||||
ChargeStars: 25, FormID: 11003, CommandKey: "drop-" + suffix, Date: now + 3,
|
ChargeStars: 25, FormID: 11003, CommandKey: "drop-" + suffix, Date: now + 3,
|
||||||
|
|
@ -359,6 +418,21 @@ WHERE target_user_id=$1 AND pts=$2 AND event_type='user_emoji_status'`, resaleBu
|
||||||
burnedInputAction.Saved || burnedInputAction.CanCraftAt != 0 {
|
burnedInputAction.Saved || burnedInputAction.CanCraftAt != 0 {
|
||||||
t.Fatalf("burned input message projection = %+v", burnedInputAction)
|
t.Fatalf("burned input message projection = %+v", burnedInputAction)
|
||||||
}
|
}
|
||||||
|
for _, edit := range []domain.EditedMessageForUser{craftedInputEdit, burnedInputEdit} {
|
||||||
|
var sharedCraftInputMediaJSON string
|
||||||
|
if err := pool.QueryRow(ctx, `SELECT p.media::text FROM private_messages p
|
||||||
|
JOIN message_boxes b ON b.message_sender_id=p.sender_user_id AND b.private_message_id=p.id
|
||||||
|
WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, edit.Message.ID).Scan(&sharedCraftInputMediaJSON); err != nil {
|
||||||
|
t.Fatalf("load shared craft input media for box %d: %v", edit.Message.ID, err)
|
||||||
|
}
|
||||||
|
sharedCraftInputMedia, err := decodeMessageMedia(sharedCraftInputMediaJSON)
|
||||||
|
if err != nil || sharedCraftInputMedia == nil || sharedCraftInputMedia.ServiceAction == nil ||
|
||||||
|
sharedCraftInputMedia.ServiceAction.StarGiftUnique == nil ||
|
||||||
|
sharedCraftInputMedia.ServiceAction.StarGiftUnique.SavedID != 0 {
|
||||||
|
t.Fatalf("shared craft input retained account-local saved_id for box %d: media=%+v err=%v",
|
||||||
|
edit.Message.ID, sharedCraftInputMedia, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
craftReq := domain.StarGiftCraftRequest{UserID: owner.ID,
|
craftReq := domain.StarGiftCraftRequest{UserID: owner.ID,
|
||||||
Refs: []domain.SavedStarGiftRef{
|
Refs: []domain.SavedStarGiftRef{
|
||||||
{Owner: ownerPeer, MsgID: transferred.Saved.MsgID},
|
{Owner: ownerPeer, MsgID: transferred.Saved.MsgID},
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ func TestStarGiftLifecycleMigrationsApply(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("migrate star gift lifecycle schema: %v", err)
|
t.Fatalf("migrate star gift lifecycle schema: %v", err)
|
||||||
}
|
}
|
||||||
if status.Dirty || status.Empty || status.Version != 121 {
|
if status.Dirty || status.Empty || status.Version != 124 {
|
||||||
t.Fatalf("migration status = %+v, want clean version 121", status)
|
t.Fatalf("migration status = %+v, want clean version 124", status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
96
internal/store/postgres/star_gift_lifecycle_test.go
Normal file
96
internal/store/postgres/star_gift_lifecycle_test.go
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"telesrv/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTransferUniqueActionSavedIDNamespace(t *testing.T) {
|
||||||
|
saved := domain.SavedStarGift{SavedID: 42}
|
||||||
|
unique := domain.UniqueStarGift{ID: 7}
|
||||||
|
user := domain.Peer{Type: domain.PeerTypeUser, ID: 100}
|
||||||
|
channel := domain.Peer{Type: domain.PeerTypeChannel, ID: 200}
|
||||||
|
|
||||||
|
if action := transferUniqueAction(unique, 1, user, saved); action.SavedID != 0 {
|
||||||
|
t.Fatalf("user transfer action leaked channel saved_id: %+v", action)
|
||||||
|
}
|
||||||
|
if action := transferUniqueAction(unique, 1, channel, saved); action.SavedID != saved.SavedID {
|
||||||
|
t.Fatalf("channel transfer action lost channel saved_id: %+v", action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeSharedPrivateStarGiftMediaOmitsUserBoxLocalRefs(t *testing.T) {
|
||||||
|
ordinary := &domain.MessageMedia{
|
||||||
|
Kind: domain.MessageMediaKindService,
|
||||||
|
ServiceAction: &domain.MessageServiceAction{
|
||||||
|
Kind: domain.MessageServiceActionStarGift,
|
||||||
|
StarGift: &domain.MessageStarGiftAction{
|
||||||
|
PeerUserID: 9,
|
||||||
|
SavedID: 10, GiftMsgID: 11, UpgradeMsgID: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
encoded, err := encodeSharedPrivateStarGiftMedia(ordinary)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encode ordinary shared projection: %v", err)
|
||||||
|
}
|
||||||
|
sharedOrdinary, err := decodeMessageMedia(string(encoded))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode ordinary shared projection: %v", err)
|
||||||
|
}
|
||||||
|
ordinaryAction := sharedOrdinary.ServiceAction.StarGift
|
||||||
|
if ordinaryAction.SavedID != 0 || ordinaryAction.GiftMsgID != 0 || ordinaryAction.UpgradeMsgID != 0 {
|
||||||
|
t.Fatalf("ordinary shared projection retained box-local refs: %+v", ordinaryAction)
|
||||||
|
}
|
||||||
|
if original := ordinary.ServiceAction.StarGift; original.SavedID != 10 || original.GiftMsgID != 11 || original.UpgradeMsgID != 12 {
|
||||||
|
t.Fatalf("ordinary source projection was mutated: %+v", original)
|
||||||
|
}
|
||||||
|
|
||||||
|
unique := &domain.MessageMedia{
|
||||||
|
Kind: domain.MessageMediaKindService,
|
||||||
|
ServiceAction: &domain.MessageServiceAction{
|
||||||
|
Kind: domain.MessageServiceActionStarGiftUnique,
|
||||||
|
StarGiftUnique: &domain.MessageStarGiftUniqueAction{
|
||||||
|
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 9}, SavedID: 13,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
encoded, err = encodeSharedPrivateStarGiftMedia(unique)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encode unique shared projection: %v", err)
|
||||||
|
}
|
||||||
|
sharedUnique, err := decodeMessageMedia(string(encoded))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode unique shared projection: %v", err)
|
||||||
|
}
|
||||||
|
if action := sharedUnique.ServiceAction.StarGiftUnique; action.SavedID != 0 {
|
||||||
|
t.Fatalf("unique shared projection retained user saved_id: %+v", action)
|
||||||
|
}
|
||||||
|
if unique.ServiceAction.StarGiftUnique.SavedID != 13 {
|
||||||
|
t.Fatalf("unique source projection was mutated: %+v", unique.ServiceAction.StarGiftUnique)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeSharedPrivateStarGiftMediaPreservesChannelSavedID(t *testing.T) {
|
||||||
|
media := &domain.MessageMedia{
|
||||||
|
Kind: domain.MessageMediaKindService,
|
||||||
|
ServiceAction: &domain.MessageServiceAction{
|
||||||
|
Kind: domain.MessageServiceActionStarGiftUnique,
|
||||||
|
StarGiftUnique: &domain.MessageStarGiftUniqueAction{
|
||||||
|
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 9}, SavedID: 14,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
encoded, err := encodeSharedPrivateStarGiftMedia(media)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encode channel shared projection: %v", err)
|
||||||
|
}
|
||||||
|
shared, err := decodeMessageMedia(string(encoded))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode channel shared projection: %v", err)
|
||||||
|
}
|
||||||
|
if action := shared.ServiceAction.StarGiftUnique; action.SavedID != 14 {
|
||||||
|
t.Fatalf("channel shared projection lost saved_id: %+v", action)
|
||||||
|
}
|
||||||
|
}
|
||||||
151
internal/store/postgres/star_gift_private_projection.go
Normal file
151
internal/store/postgres/star_gift_private_projection.go
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
|
||||||
|
"telesrv/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// projectPrivateStarGiftSourceRef exposes a user-owned gift's stable source
|
||||||
|
// message identity only in the gift owner's message-box projection. Telegram
|
||||||
|
// defines gift_msg_id as receiver-only, and TDesktop also treats user unique
|
||||||
|
// saved_id as an inputSavedStarGiftUser identity. A non-owner counterpart box
|
||||||
|
// id is therefore not a valid substitute: it could resolve to an unrelated
|
||||||
|
// gift owned by that viewer. The shared private_messages row omits the local
|
||||||
|
// reference for the same reason.
|
||||||
|
func projectPrivateStarGiftSourceRef(
|
||||||
|
_ context.Context,
|
||||||
|
_ pgx.Tx,
|
||||||
|
req *domain.SendPrivateTextRequest,
|
||||||
|
sourceOwnerUserID int64,
|
||||||
|
sourceOwnerBoxID int,
|
||||||
|
) (privateSendMediaProjection, error) {
|
||||||
|
if req == nil || req.Media == nil || sourceOwnerUserID <= 0 || sourceOwnerBoxID <= 0 ||
|
||||||
|
(sourceOwnerUserID != req.SenderUserID && sourceOwnerUserID != req.RecipientUserID) {
|
||||||
|
return privateSendMediaProjection{}, fmt.Errorf("project private star gift source: invalid scope")
|
||||||
|
}
|
||||||
|
|
||||||
|
shared, err := cloneMessageMedia(req.Media)
|
||||||
|
if err != nil {
|
||||||
|
return privateSendMediaProjection{}, err
|
||||||
|
}
|
||||||
|
sender, err := cloneMessageMedia(req.Media)
|
||||||
|
if err != nil {
|
||||||
|
return privateSendMediaProjection{}, err
|
||||||
|
}
|
||||||
|
recipient, err := cloneMessageMedia(req.Media)
|
||||||
|
if err != nil {
|
||||||
|
return privateSendMediaProjection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case privateStarGiftAction(shared) != nil:
|
||||||
|
sharedAction := privateStarGiftAction(shared)
|
||||||
|
senderAction := privateStarGiftAction(sender)
|
||||||
|
recipientAction := privateStarGiftAction(recipient)
|
||||||
|
if sharedAction.GiftMsgID != sourceOwnerBoxID {
|
||||||
|
return privateSendMediaProjection{}, fmt.Errorf(
|
||||||
|
"project private star gift source: gift_msg_id %d does not match owner box %d",
|
||||||
|
sharedAction.GiftMsgID, sourceOwnerBoxID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sharedAction.GiftMsgID = 0
|
||||||
|
senderAction.GiftMsgID = 0
|
||||||
|
recipientAction.GiftMsgID = 0
|
||||||
|
if req.SenderUserID == sourceOwnerUserID {
|
||||||
|
senderAction.GiftMsgID = sourceOwnerBoxID
|
||||||
|
} else {
|
||||||
|
recipientAction.GiftMsgID = sourceOwnerBoxID
|
||||||
|
}
|
||||||
|
case privateStarGiftUniqueAction(shared) != nil:
|
||||||
|
sharedAction := privateStarGiftUniqueAction(shared)
|
||||||
|
senderAction := privateStarGiftUniqueAction(sender)
|
||||||
|
recipientAction := privateStarGiftUniqueAction(recipient)
|
||||||
|
if sharedAction.Peer.Type != domain.PeerTypeUser || sharedAction.Peer.ID != sourceOwnerUserID ||
|
||||||
|
sharedAction.SavedID != int64(sourceOwnerBoxID) {
|
||||||
|
return privateSendMediaProjection{}, fmt.Errorf(
|
||||||
|
"project private unique star gift source: saved_id %d does not match owner box %d",
|
||||||
|
sharedAction.SavedID, sourceOwnerBoxID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sharedAction.SavedID = 0
|
||||||
|
senderAction.SavedID = 0
|
||||||
|
recipientAction.SavedID = 0
|
||||||
|
if req.SenderUserID == sourceOwnerUserID {
|
||||||
|
senderAction.SavedID = int64(sourceOwnerBoxID)
|
||||||
|
} else {
|
||||||
|
recipientAction.SavedID = int64(sourceOwnerBoxID)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return privateSendMediaProjection{}, fmt.Errorf("project private star gift source: unsupported media")
|
||||||
|
}
|
||||||
|
|
||||||
|
return privateSendMediaProjection{Shared: shared, Sender: sender, Recipient: recipient}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneMessageMedia(media *domain.MessageMedia) (*domain.MessageMedia, error) {
|
||||||
|
encoded, err := encodeMessageMedia(media)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("clone private message media: %w", err)
|
||||||
|
}
|
||||||
|
cloned, err := decodeMessageMedia(string(encoded))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("clone private message media: %w", err)
|
||||||
|
}
|
||||||
|
return cloned, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeSharedPrivateStarGiftMedia returns the logical private-message
|
||||||
|
// envelope for an already viewpoint-projected Star Gift service message.
|
||||||
|
// Conversation message ids belong to a single owner's message_boxes
|
||||||
|
// namespace, so the shared row must never retain them. saved_id is likewise
|
||||||
|
// box-local for user gifts, while channel saved ids remain globally meaningful
|
||||||
|
// inside the channel gift namespace.
|
||||||
|
func encodeSharedPrivateStarGiftMedia(media *domain.MessageMedia) ([]byte, error) {
|
||||||
|
shared, err := cloneMessageMedia(media)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case privateStarGiftAction(shared) != nil:
|
||||||
|
action := privateStarGiftAction(shared)
|
||||||
|
action.GiftMsgID = 0
|
||||||
|
action.UpgradeMsgID = 0
|
||||||
|
if action.PeerUserID > 0 || action.To.Type == domain.PeerTypeUser {
|
||||||
|
action.SavedID = 0
|
||||||
|
}
|
||||||
|
case privateStarGiftUniqueAction(shared) != nil:
|
||||||
|
action := privateStarGiftUniqueAction(shared)
|
||||||
|
if action.Peer.Type == domain.PeerTypeUser {
|
||||||
|
action.SavedID = 0
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("encode shared private star gift media: unsupported media")
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := encodeMessageMedia(shared)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encode shared private star gift media: %w", err)
|
||||||
|
}
|
||||||
|
return encoded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func privateStarGiftAction(media *domain.MessageMedia) *domain.MessageStarGiftAction {
|
||||||
|
if media == nil || media.Kind != domain.MessageMediaKindService || media.ServiceAction == nil ||
|
||||||
|
media.ServiceAction.Kind != domain.MessageServiceActionStarGift {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return media.ServiceAction.StarGift
|
||||||
|
}
|
||||||
|
|
||||||
|
func privateStarGiftUniqueAction(media *domain.MessageMedia) *domain.MessageStarGiftUniqueAction {
|
||||||
|
if media == nil || media.Kind != domain.MessageMediaKindService || media.ServiceAction == nil ||
|
||||||
|
media.ServiceAction.Kind != domain.MessageServiceActionStarGiftUnique {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return media.ServiceAction.StarGiftUnique
|
||||||
|
}
|
||||||
|
|
@ -230,6 +230,12 @@ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`, req.UserID, commandKey, locked.ID, req.For
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
projectMedia: func(ctx context.Context, tx pgx.Tx, messageReq *domain.SendPrivateTextRequest) (privateSendMediaProjection, error) {
|
||||||
|
if result.Saved.Owner.Type != domain.PeerTypeUser {
|
||||||
|
return privateSendMediaProjection{Shared: messageReq.Media, Sender: messageReq.Media, Recipient: messageReq.Media}, nil
|
||||||
|
}
|
||||||
|
return projectPrivateStarGiftSourceRef(ctx, tx, messageReq, result.Saved.Owner.ID, result.Saved.MsgID)
|
||||||
|
},
|
||||||
after: func(ctx context.Context, tx pgx.Tx, sent domain.SendPrivateTextResult) error {
|
after: func(ctx context.Context, tx pgx.Tx, sent domain.SendPrivateTextResult) error {
|
||||||
ownerMessageID := sent.RecipientMessage.ID
|
ownerMessageID := sent.RecipientMessage.ID
|
||||||
if saved.FromUserID == req.UserID {
|
if saved.FromUserID == req.UserID {
|
||||||
|
|
@ -434,7 +440,10 @@ WHERE owner_user_id=$1 AND box_id=$2 AND NOT deleted`, box.OwnerUserID, box.BoxI
|
||||||
return nil, fmt.Errorf("enqueue star gift source edit: %w", err)
|
return nil, fmt.Errorf("enqueue star gift source edit: %w", err)
|
||||||
}
|
}
|
||||||
if box.OwnerUserID == box.MessageSenderID || len(privateMediaJSON) == 0 {
|
if box.OwnerUserID == box.MessageSenderID || len(privateMediaJSON) == 0 {
|
||||||
privateMediaJSON = mediaJSON
|
privateMediaJSON, err = encodeSharedPrivateStarGiftMedia(media)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
edits = append(edits, domain.EditedMessageForUser{UserID: msg.OwnerUserID, Message: msg, Event: event})
|
edits = append(edits, domain.EditedMessageForUser{UserID: msg.OwnerUserID, Message: msg, Event: event})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue