Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/comms/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,12 @@ func getNewBlasts(tx dbv1.DBTX, ctx context.Context, arg getNewBlastsParams) ([]
)
SELECT * FROM all_new
WHERE created_at > (select t from last_permission_change)
-- a blast is dropped once the sender changes their own inbox settings
AND created_at > (
SELECT COALESCE(MAX(cp.updated_at), to_timestamp(0))
FROM chat_permissions cp
WHERE cp.user_id = all_new.from_user_id
)
AND chat_allowed(from_user_id, @user_id)
ORDER BY created_at
;`
Expand Down
17 changes: 14 additions & 3 deletions api/comms/chat_inbox_closed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ func TestChatBlastThenCloseInbox(t *testing.T) {
assert.ErrorContains(t, err, "Not permitted to send messages to this user")
}
}
pendingBlastIds := func(userId int32) []string {
blasts, err := getNewBlasts(pool, ctx, getNewBlastsParams{UserID: userId})
require.NoError(t, err)
ids := []string{}
for _, b := range blasts {
ids = append(ids, b.BlastID)
}
return ids
}
upgrade := func(follower int32, ts time.Time) string {
chatId := trashid.ChatID(int(follower), 1)
err := chatCreate(pool, ctx, follower, ts, ChatCreateRPCParams{
Expand Down Expand Up @@ -95,11 +104,12 @@ func TestChatBlastThenCloseInbox(t *testing.T) {
// artist closes their inbox
require.NoError(t, chatSetPermissions(pool, ctx, 1, ChatPermissionAll, []ChatPermission{ChatPermissionNone}, boolPtr(true), t3))

// 201 can no longer start a thread off the old blast
// 201 can no longer start a thread off the old blast, and no longer sees it as pending
assertChatCreateAllowed(t, ctx, validator, 201, 1, false)
assert.False(t, chatAllowed(201, 1))
assert.Empty(t, pendingBlastIds(201))

// 202's thread holds nothing but the blast seed, so it grants no reply rights
// 202's thread only has the blast seed, so 202 can't reply
assertMessageAllowed(202, chatId_202, false)
assert.False(t, chatAllowed(202, 1))

Expand All @@ -123,9 +133,10 @@ func TestChatBlastThenCloseInbox(t *testing.T) {
require.NoError(t, err)

assertChatCreateAllowed(t, ctx, validator, 201, 1, true)
assert.Equal(t, []string{"b_closed"}, pendingBlastIds(201))
assert.True(t, chatAllowed(202, 1), "new blast fanned into 202's thread re-opens replies")

// closing the inbox once more after that blast shuts the door again
// closing the inbox again blocks replies to the second blast
require.NoError(t, chatSetPermissions(pool, ctx, 1, ChatPermissionAll, []ChatPermission{ChatPermissionNone}, boolPtr(true), t5))
assertChatCreateAllowed(t, ctx, validator, 201, 1, false)
assert.False(t, chatAllowed(202, 1))
Expand Down
6 changes: 6 additions & 0 deletions api/comms_blasts.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ func (app *ApiServer) getNewBlasts(c *fiber.Ctx) error {
)
SELECT * FROM all_new
WHERE created_at > (select t from last_permission_change)
-- a blast is dropped once the sender changes their own inbox settings
AND created_at > (
SELECT COALESCE(MAX(cp.updated_at), to_timestamp(0))
FROM chat_permissions cp
WHERE cp.user_id = all_new.from_user_id
)
AND chat_allowed(from_user_id, @user_id)
ORDER BY created_at
;`
Expand Down
31 changes: 31 additions & 0 deletions api/comms_blasts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"api.audius.co/database"
"api.audius.co/trashid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetNewBlasts(t *testing.T) {
Expand Down Expand Up @@ -540,6 +541,36 @@ func TestGetNewBlastsWithPermissions(t *testing.T) {
})
}

func TestGetNewBlastsAfterSenderClosesInbox(t *testing.T) {
app := emptyTestApp(t)

// chat_permissions.updated_at is timestamp without time zone, written as UTC
now := time.Now().UTC()
fixtures := database.FixtureMap{
"users": {
{"user_id": 1, "handle": "artist1", "wallet": "0x7d273271690538cf855e5b3002a0dd8c154bb060", "created_at": now.Add(-time.Hour), "updated_at": now.Add(-time.Hour), "is_current": true},
{"user_id": 2, "handle": "fan1", "wallet": "0xc3d1d41e6872ffbd15c473d14fc3a9250be5b5e0", "created_at": now.Add(-time.Hour), "updated_at": now.Add(-time.Hour), "is_current": true},
},
"follows": {
{"follower_user_id": 2, "followee_user_id": 1, "created_at": now.Add(-time.Hour), "is_current": true, "is_delete": false},
},
"chat_blast": {
{"blast_id": "blast_before_close", "from_user_id": 1, "audience": "follower_audience", "plaintext": "before close", "created_at": now.Add(-10 * time.Minute)},
{"blast_id": "blast_after_close", "from_user_id": 1, "audience": "follower_audience", "plaintext": "after close", "created_at": now.Add(-2 * time.Minute)},
},
// the artist (sender) closed their inbox between the two blasts
"chat_permissions": {
{"user_id": 1, "permits": "none", "allowed": true, "updated_at": now.Add(-5 * time.Minute)},
},
}
database.Seed(app.pool.Replicas[0], fixtures)

status, body := testGetWithWallet(t, app, "/comms/blasts", "0xc3d1d41e6872ffbd15c473d14fc3a9250be5b5e0")
require.Equal(t, 200, status)
assert.Contains(t, string(body), "blast_after_close")
assert.NotContains(t, string(body), "blast_before_close")
}

func TestGetNewBlastsWithExistingChats(t *testing.T) {
app := emptyTestApp(t)

Expand Down
4 changes: 1 addition & 3 deletions api/v1_challenges_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ func getWeeklyPoolWindowStart() time.Time {
// weeklyPoolWindowStartAt returns the most recent Monday 16:00 UTC at or
// before `now`: the instant the current weekly reward pool opened.
//
// Go numbers Sunday as 0, so the naive "subtract Weekday, add one" walk to
// Monday landed on the *following* Monday all day Sunday, putting the window
// start in the future and reporting the full pool as remaining every Sunday.
// Go's Weekday has Sunday=0, so it is remapped to Monday=0..Sunday=6.
func weeklyPoolWindowStartAt(now time.Time) time.Time {
now = now.UTC()
daysSinceMonday := (int(now.Weekday()) + 6) % 7 // Monday=0 ... Sunday=6
Expand Down
2 changes: 1 addition & 1 deletion api/v1_challenges_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func TestWeeklyPoolWindowStartAt(t *testing.T) {
{"Monday at 16:00 opens the week", thisMonday, thisMonday},
{"Wednesday", utc(2026, time.September, 9, 12), thisMonday},
{"Saturday", utc(2026, time.September, 12, 23), thisMonday},
{"Sunday (Go weekday 0) is the end of the week, not the start of the next", utc(2026, time.September, 13, 21), thisMonday},
{"Sunday", utc(2026, time.September, 13, 21), thisMonday},
{"Sunday just before the next Monday", utc(2026, time.September, 13, 23), thisMonday},
}
for _, tc := range cases {
Expand Down
6 changes: 2 additions & 4 deletions ddl/functions/chat_allowed.sql
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,8 @@ BEGIN
RETURN TRUE;
END IF;

-- existing chat takes priority over permissions.
-- A blast message only counts if the blast is newer than to_user's most
-- recent inbox settings change: otherwise an artist who blasts and then
-- closes their inbox would still be reachable by every blast recipient.
-- existing chat takes priority over permissions; blast messages only count
-- if newer than to_user's last inbox settings change
SELECT COUNT(*) > 0 INTO can_message
FROM chat_member member_a
JOIN chat_member member_b USING (chat_id)
Expand Down
6 changes: 2 additions & 4 deletions sql/01_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1203,10 +1203,8 @@ BEGIN
RETURN TRUE;
END IF;

-- existing chat takes priority over permissions.
-- A blast message only counts if the blast is newer than to_user's most
-- recent inbox settings change: otherwise an artist who blasts and then
-- closes their inbox would still be reachable by every blast recipient.
-- existing chat takes priority over permissions; blast messages only count
-- if newer than to_user's last inbox settings change
SELECT COUNT(*) > 0 INTO can_message
FROM chat_member member_a
JOIN chat_member member_b USING (chat_id)
Expand Down
9 changes: 3 additions & 6 deletions weeklyrotation/period.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,9 @@ import (
"time"
)

// RolloverOffsetDays is how far the period boundary sits after the ISO
// week's Monday. The mix rolls over on Wednesday 00:00 UTC, not at the ISO
// week boundary: Monday already belongs to the other weekly surfaces and
// Friday is Spotify's day. Expressed as an offset so the period is still
// identified by an (iso_year, iso_week) pair everywhere -- cache keys, the
// deterministic seed, share links, notification group ids.
// RolloverOffsetDays shifts the period start from the ISO Monday to Wednesday
// 00:00 UTC. Periods are still keyed by (iso_year, iso_week) in cache keys,
// the seed, share links and notification group ids.
const RolloverOffsetDays = 2

// Period returns the (ISO year, ISO week) pair that identifies the period
Expand Down
Loading