diff --git a/api/dbv1/full_comments.go b/api/dbv1/full_comments.go index 9649df55..fcd59e56 100644 --- a/api/dbv1/full_comments.go +++ b/api/dbv1/full_comments.go @@ -105,7 +105,7 @@ func (q *Queries) FullCommentsKeyed(ctx context.Context, arg GetCommentsParams) SELECT 1 FROM comment_reactions WHERE comment_id = comments.comment_id - AND user_id = COALESCE(tracks.owner_id, events.user_id, comments.entity_id) + AND user_id = COALESCE(tracks.owner_id, comments.entity_id) AND is_delete = false ) AS is_artist_reacted, @@ -128,13 +128,11 @@ func (q *Queries) FullCommentsKeyed(ctx context.Context, arg GetCommentsParams) FROM comments LEFT JOIN tracks ON comments.entity_type = 'Track' AND comments.entity_id = tracks.track_id - LEFT JOIN events ON comments.entity_type = 'Event' AND comments.entity_id = events.event_id LEFT JOIN comment_threads USING (comment_id) WHERE comments.comment_id = ANY(@ids::int[]) AND ( (comments.entity_type = 'Track' AND (@include_unlisted = true OR COALESCE(tracks.is_unlisted, false) = false)) OR comments.entity_type = 'FanClub' - OR (comments.entity_type = 'Event' AND COALESCE(events.is_deleted, false) = false) ) ORDER BY comments.created_at DESC ` diff --git a/api/dbv1/models.go b/api/dbv1/models.go index 25cce869..6f5333f3 100644 --- a/api/dbv1/models.go +++ b/api/dbv1/models.go @@ -2244,8 +2244,6 @@ type Subscription struct { IsDelete bool `json:"is_delete"` CreatedAt time.Time `json:"created_at"` Txhash string `json:"txhash"` - EntityType string `json:"entity_type"` - EntityID pgtype.Int4 `json:"entity_id"` } type SupporterRankUp struct { diff --git a/api/server.go b/api/server.go index ea9fdecf..9eaccff5 100644 --- a/api/server.go +++ b/api/server.go @@ -511,12 +511,6 @@ func NewApiServer(config config.Config) *ApiServer { g.Get("/fan_club/feed", app.v1FanClubFeed) g.Get("/fan-club/feed", app.v1FanClubFeed) - g.Get("/events/:eventId/comments", app.v1EventComments) - g.Get("/events/:eventId/follow_state", app.v1EventFollowState) - g.Get("/events/:eventId/follow-state", app.v1EventFollowState) - g.Post("/events/:eventId/follow", app.requireAuthMiddleware, app.requireWriteScope, app.postV1EventFollow) - g.Delete("/events/:eventId/follow", app.requireAuthMiddleware, app.requireWriteScope, app.deleteV1EventFollow) - g.Get("/tracks/:trackId/comments", app.v1TrackComments) g.Get("/tracks/:trackId/comment_count", app.v1TrackCommentCount) g.Get("/tracks/:trackId/comment-count", app.v1TrackCommentCount) @@ -614,7 +608,6 @@ func NewApiServer(config config.Config) *ApiServer { // Events g.Get("/events/unclaimed_id", app.v1EventsUnclaimedId) g.Get("/events/unclaimed-id", app.v1EventsUnclaimedId) - g.Get("/events/remix-contests", app.v1EventsRemixContests) g.Get("/events", app.v1Events) g.Get("/events/all", app.v1Events) g.Get("/events/entity", app.v1Events) diff --git a/api/swagger/swagger-v1.yaml b/api/swagger/swagger-v1.yaml index 3cf503ca..de72057f 100644 --- a/api/swagger/swagger-v1.yaml +++ b/api/swagger/swagger-v1.yaml @@ -1194,57 +1194,6 @@ paths: "500": description: Server error content: {} - /events/remix-contests: - get: - tags: - - events - summary: Get all remix contests - description: - Get remix contest events ordered with currently-active contests first - (by soonest-ending), followed by ended contests (most-recently-ended - first). Active contests are those whose end_date is null or in the - future. - operationId: Get Remix Contests - security: - - {} - - OAuth2: - - read - parameters: - - name: offset - in: query - description: - The number of items to skip. Useful for pagination (page number - * limit) - schema: - type: integer - - name: limit - in: query - description: The number of items to fetch - schema: - type: integer - - name: status - in: query - description: Filter contests by status - schema: - type: string - default: all - enum: - - active - - ended - - all - responses: - "200": - description: Success - content: - application/json: - schema: - $ref: "#/components/schemas/events_response" - "400": - description: Bad request - content: {} - "500": - description: Server error - content: {} /events/unclaimed_id: get: tags: diff --git a/api/v1_comments.go b/api/v1_comments.go index 856ee1a1..c0425b32 100644 --- a/api/v1_comments.go +++ b/api/v1_comments.go @@ -23,7 +23,7 @@ type GetCommentsParams struct { } type CreateCommentRequest struct { - EntityType string `json:"entityType" validate:"required,oneof=Track FanClub Event"` + EntityType string `json:"entityType" validate:"required,oneof=Track"` EntityId int `json:"entityId" validate:"required,min=1"` Body string `json:"body" validate:"required,max=500"` CommentId *int `json:"commentId,omitempty" validate:"omitempty,min=1"` @@ -33,19 +33,19 @@ type CreateCommentRequest struct { } type UpdateCommentRequest struct { - EntityType string `json:"entityType" validate:"required,oneof=Track FanClub Event"` + EntityType string `json:"entityType" validate:"required,oneof=Track"` EntityId int `json:"entityId" validate:"required,min=1"` Body string `json:"body" validate:"required,max=500"` Mentions []int `json:"mentions,omitempty" validate:"omitempty,dive,min=1"` } type ReactCommentRequest struct { - EntityType string `json:"entityType" validate:"required,oneof=Track FanClub Event"` + EntityType string `json:"entityType" validate:"required,oneof=Track"` EntityId int `json:"entityId" validate:"required,min=1"` } type PinCommentRequest struct { - EntityType string `json:"entityType" validate:"required,oneof=Track FanClub Event"` + EntityType string `json:"entityType" validate:"required,oneof=Track"` EntityId int `json:"entityId" validate:"required,min=1"` } diff --git a/api/v1_event_comments.go b/api/v1_event_comments.go deleted file mode 100644 index 7d4dff2d..00000000 --- a/api/v1_event_comments.go +++ /dev/null @@ -1,128 +0,0 @@ -package api - -import ( - "errors" - - "api.audius.co/api/dbv1" - "api.audius.co/trashid" - "github.com/gofiber/fiber/v2" - "github.com/jackc/pgx/v5" -) - -// v1EventComments returns the top-level comment stream for a remix-contest event. -// Comments are authored by any signed-in user; a comment is considered a "post -// update" when its user_id matches the event's owner user_id (resolved client-side). -// Replies are not returned in this list — they come back nested inside the -// FullComment result just like track comments. -func (app *ApiServer) v1EventComments(c *fiber.Ctx) error { - encodedEventID := c.Params("eventId") - eventID, err := trashid.DecodeHashId(encodedEventID) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "invalid event id") - } - - var eventRow struct { - UserID int32 - IsDeleted bool - } - err = app.pool.QueryRow(c.Context(), ` - SELECT user_id, COALESCE(is_deleted, false) - FROM events - WHERE event_id = $1 - LIMIT 1 - `, eventID).Scan(&eventRow.UserID, &eventRow.IsDeleted) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return fiber.NewError(fiber.StatusNotFound, "event not found") - } - return err - } - if eventRow.IsDeleted { - return fiber.NewError(fiber.StatusNotFound, "event not found") - } - - var params GetCommentsParams - if err := app.ParseAndValidateQueryParams(c, ¶ms); err != nil { - return err - } - - myID := app.getMyId(c) - - // Pull top-level comment ids for this event, sorted and paginated the same - // way track comments are. Threads are materialised below by FullComments. - orderBy := `comments.created_at DESC` - switch params.SortMethod { - case "timestamp": - orderBy = `comments.created_at ASC` - case "top": - orderBy = `(SELECT COUNT(*) FROM comment_reactions cr WHERE cr.comment_id = comments.comment_id) DESC, comments.created_at DESC` - } - - sql := ` - SELECT comments.comment_id - FROM comments - LEFT JOIN comment_threads ct ON ct.comment_id = comments.comment_id - WHERE comments.entity_type = 'Event' - AND comments.entity_id = @eventId - AND comments.is_delete = false - AND ct.parent_comment_id IS NULL - ORDER BY ` + orderBy + ` - LIMIT @limit - OFFSET @offset - ` - - args := pgx.NamedArgs{ - "eventId": eventID, - "limit": params.Limit, - "offset": params.Offset, - } - - rows, err := app.pool.Query(c.Context(), sql, args) - if err != nil { - return err - } - commentIDs, err := pgx.CollectRows(rows, pgx.RowTo[int32]) - if err != nil { - return err - } - - comments, err := app.queries.FullComments(c.Context(), dbv1.GetCommentsParams{ - Ids: commentIDs, - MyID: myID, - IncludeUnlisted: true, - }) - if err != nil { - return err - } - - // Collect related user ids so the UI can render avatars/handles in one shot. - userIDs := []int32{eventRow.UserID} - for _, co := range comments { - userIDs = append(userIDs, int32(co.UserId)) - for _, m := range co.Mentions { - userIDs = append(userIDs, int32(m.UserId)) - } - for _, r := range co.Replies { - userIDs = append(userIDs, int32(r.UserId)) - } - } - - related, err := app.queries.Parallel(c.Context(), dbv1.ParallelParams{ - UserIds: userIDs, - TrackIds: nil, - MyID: myID, - AuthedWallet: app.tryGetAuthedWallet(c), - }) - if err != nil { - return err - } - - return c.JSON(fiber.Map{ - "data": comments, - "related": fiber.Map{ - "users": related.UserList(), - "tracks": related.TrackList(), - }, - "event_user_id": trashid.MustEncodeHashID(int(eventRow.UserID)), - }) -} diff --git a/api/v1_event_comments_test.go b/api/v1_event_comments_test.go deleted file mode 100644 index 55e8dc4a..00000000 --- a/api/v1_event_comments_test.go +++ /dev/null @@ -1,346 +0,0 @@ -package api - -import ( - "testing" - - "api.audius.co/database" - "api.audius.co/trashid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// The event_comments endpoint returns the top-level comment stream attached to -// a remix-contest event. These tests verify the endpoint's shape, filtering, -// and the artist-reacted resolution we added for events. - -func testEventCommentsBaseUsers() []map[string]any { - return []map[string]any{ - { - "user_id": 1, - "handle": "eventartist", - "handle_lc": "eventartist", - "name": "Event Artist", - "wallet": "0xe0f1230000000000000000000000000000000001", - }, - { - "user_id": 2, - "handle": "eventfan", - "handle_lc": "eventfan", - "name": "Event Fan", - "wallet": "0xe0f1230000000000000000000000000000000002", - }, - { - "user_id": 3, - "handle": "eventfan2", - "handle_lc": "eventfan2", - "name": "Event Fan 2", - "wallet": "0xe0f1230000000000000000000000000000000003", - }, - } -} - -func TestEventComments_UnknownEvent(t *testing.T) { - app := emptyTestApp(t) - enc, err := trashid.EncodeHashId(999999) - require.NoError(t, err) - status, _ := testGet(t, app, "/v1/events/"+enc+"/comments") - assert.Equal(t, 404, status) -} - -func TestEventComments_ReturnsTopLevelOnlyNewestFirst(t *testing.T) { - app := emptyTestApp(t) - - // Event 100 is owned by user 1 and points at track 1. - // Comments 600 and 601 are both top-level; 602 is a reply to 600 and should - // NOT appear in the top-level listing (it comes back nested instead). - database.Seed(app.pool.Replicas[0], database.FixtureMap{ - "users": testEventCommentsBaseUsers(), - "tracks": { - { - "track_id": 1, - "owner_id": 1, - "title": "Original", - "created_at": "2020-01-01 00:00:00", - }, - }, - "events": { - { - "event_id": 100, - "event_type": "remix_contest", - "user_id": 1, - "entity_type": "track", - "entity_id": 1, - "event_data": map[string]any{"description": "remix me"}, - "created_at": "2020-05-01 00:00:00", - "updated_at": "2020-05-01 00:00:00", - }, - }, - "comments": []map[string]any{ - { - "comment_id": 600, - "user_id": 2, - "entity_id": 100, - "entity_type": "Event", - "text": "first!", - "created_at": "2020-06-01 00:00:00", - }, - { - "comment_id": 601, - "user_id": 1, - "entity_id": 100, - "entity_type": "Event", - "text": "thanks for joining", - "created_at": "2020-06-02 00:00:00", - }, - { - "comment_id": 602, - "user_id": 1, - "entity_id": 100, - "entity_type": "Event", - "text": "and welcome", - "created_at": "2020-06-03 00:00:00", - }, - }, - // Comment 602 is threaded under 600 - "comment_threads": []map[string]any{ - {"parent_comment_id": 600, "comment_id": 602}, - }, - }) - - encEvent, err := trashid.EncodeHashId(100) - require.NoError(t, err) - enc600, err := trashid.EncodeHashId(600) - require.NoError(t, err) - enc601, err := trashid.EncodeHashId(601) - require.NoError(t, err) - encUser1, err := trashid.EncodeHashId(1) - require.NoError(t, err) - - status, body := testGet(t, app, "/v1/events/"+encEvent+"/comments?sort_method=newest") - require.Equal(t, 200, status, string(body)) - - // Default sort is newest-first, so 601 (Jun 2) should come before 600 (Jun 1). - // The reply (602) must NOT appear as its own top-level item. - jsonAssert(t, body, map[string]any{ - "data.#": 2, - "data.0.id": enc601, - "data.0.message": "thanks for joining", - "data.1.id": enc600, - "data.1.message": "first!", - "event_user_id": encUser1, - "data.1.replies.#": 1, - }) -} - -func TestEventComments_IsArtistReactedResolvesViaEventOwner(t *testing.T) { - app := emptyTestApp(t) - - // Event 200 is owned by user 1. User 2 writes a comment. User 1 (the event - // artist) reacts to it. The is_artist_reacted field should be true even - // though the comment's entity_id points at an event, not a track — the - // COALESCE(tracks.owner_id, events.user_id, comments.entity_id) resolution - // should pick up events.user_id. - database.Seed(app.pool.Replicas[0], database.FixtureMap{ - "users": testEventCommentsBaseUsers(), - "tracks": { - { - "track_id": 1, - "owner_id": 1, - "title": "Original", - "created_at": "2020-01-01 00:00:00", - }, - }, - "events": { - { - "event_id": 200, - "event_type": "remix_contest", - "user_id": 1, - "entity_type": "track", - "entity_id": 1, - "event_data": map[string]any{"description": "remix me"}, - "created_at": "2020-05-01 00:00:00", - "updated_at": "2020-05-01 00:00:00", - }, - }, - "comments": []map[string]any{ - { - "comment_id": 700, - "user_id": 2, - "entity_id": 200, - "entity_type": "Event", - "text": "here's my remix", - "created_at": "2020-06-01 00:00:00", - }, - }, - "comment_reactions": []map[string]any{ - { - "comment_id": 700, - "user_id": 1, // event owner reacted - "is_delete": false, - "created_at": "2020-06-01 01:00:00", - "updated_at": "2020-06-01 01:00:00", - }, - }, - }) - - encEvent, err := trashid.EncodeHashId(200) - require.NoError(t, err) - - status, body := testGet(t, app, "/v1/events/"+encEvent+"/comments") - require.Equal(t, 200, status, string(body)) - - jsonAssert(t, body, map[string]any{ - "data.#": 1, - "data.0.message": "here's my remix", - "data.0.react_count": 1, - "data.0.is_artist_reacted": true, - }) -} - -func TestEventComments_DeletedEventReturns404(t *testing.T) { - app := emptyTestApp(t) - database.Seed(app.pool.Replicas[0], database.FixtureMap{ - "users": testEventCommentsBaseUsers(), - "tracks": { - { - "track_id": 1, - "owner_id": 1, - "title": "Original", - "created_at": "2020-01-01 00:00:00", - }, - }, - "events": { - { - "event_id": 300, - "event_type": "remix_contest", - "user_id": 1, - "entity_type": "track", - "entity_id": 1, - "event_data": map[string]any{"description": "remix me"}, - "is_deleted": true, - "created_at": "2020-05-01 00:00:00", - "updated_at": "2020-05-01 00:00:00", - }, - }, - }) - - encEvent, err := trashid.EncodeHashId(300) - require.NoError(t, err) - status, _ := testGet(t, app, "/v1/events/"+encEvent+"/comments") - assert.Equal(t, 404, status) -} - -func TestEventComments_PaginationAndTimestampSort(t *testing.T) { - app := emptyTestApp(t) - - // Three top-level comments; with sort_method=timestamp we expect oldest - // first. offset=1&limit=1 returns the middle one. - database.Seed(app.pool.Replicas[0], database.FixtureMap{ - "users": testEventCommentsBaseUsers(), - "tracks": { - { - "track_id": 1, - "owner_id": 1, - "title": "Original", - "created_at": "2020-01-01 00:00:00", - }, - }, - "events": { - { - "event_id": 400, - "event_type": "remix_contest", - "user_id": 1, - "entity_type": "track", - "entity_id": 1, - "event_data": map[string]any{"description": "remix me"}, - "created_at": "2020-05-01 00:00:00", - "updated_at": "2020-05-01 00:00:00", - }, - }, - "comments": []map[string]any{ - { - "comment_id": 800, - "user_id": 2, - "entity_id": 400, - "entity_type": "Event", - "text": "oldest", - "created_at": "2020-06-01 00:00:00", - }, - { - "comment_id": 801, - "user_id": 3, - "entity_id": 400, - "entity_type": "Event", - "text": "middle", - "created_at": "2020-06-02 00:00:00", - }, - { - "comment_id": 802, - "user_id": 2, - "entity_id": 400, - "entity_type": "Event", - "text": "newest", - "created_at": "2020-06-03 00:00:00", - }, - }, - }) - - encEvent, err := trashid.EncodeHashId(400) - require.NoError(t, err) - - status, body := testGet(t, app, "/v1/events/"+encEvent+"/comments?sort_method=timestamp&offset=1&limit=1") - require.Equal(t, 200, status, string(body)) - jsonAssert(t, body, map[string]any{ - "data.#": 1, - "data.0.message": "middle", - }) -} - -func TestEventComments_DoesNotLeakTrackComments(t *testing.T) { - app := emptyTestApp(t) - - // Track 1 has a comment, event 500 is attached to track 1. The event's - // endpoint should return nothing (only event-scoped comments), even though - // there's a track comment with entity_id=1 in the same table. - database.Seed(app.pool.Replicas[0], database.FixtureMap{ - "users": testEventCommentsBaseUsers(), - "tracks": { - { - "track_id": 1, - "owner_id": 1, - "title": "Original", - "created_at": "2020-01-01 00:00:00", - }, - }, - "events": { - { - "event_id": 500, - "event_type": "remix_contest", - "user_id": 1, - "entity_type": "track", - "entity_id": 1, - "event_data": map[string]any{"description": "remix me"}, - "created_at": "2020-05-01 00:00:00", - "updated_at": "2020-05-01 00:00:00", - }, - }, - "comments": []map[string]any{ - { - "comment_id": 900, - "user_id": 2, - "entity_id": 1, // track comment - "entity_type": "Track", - "text": "great track", - "created_at": "2020-06-01 00:00:00", - }, - }, - }) - - encEvent, err := trashid.EncodeHashId(500) - require.NoError(t, err) - status, body := testGet(t, app, "/v1/events/"+encEvent+"/comments") - require.Equal(t, 200, status, string(body)) - jsonAssert(t, body, map[string]any{ - "data.#": 0, - }) -} diff --git a/api/v1_events_followers.go b/api/v1_events_followers.go deleted file mode 100644 index 0605cfa1..00000000 --- a/api/v1_events_followers.go +++ /dev/null @@ -1,158 +0,0 @@ -package api - -import ( - "errors" - "strconv" - "time" - - "api.audius.co/indexer" - "api.audius.co/trashid" - corev1 "github.com/OpenAudio/go-openaudio/pkg/api/core/v1" - "github.com/ethereum/go-ethereum/common" - "github.com/gofiber/fiber/v2" - "github.com/jackc/pgx/v5" - "go.uber.org/zap" -) - -// postV1EventFollow subscribes the authenticated user to a remix-contest event -// so they'll be notified when the event's artist posts an update. It emits a -// Subscribe/Event ManageEntity transaction and relies on the indexer to write -// the subscriptions row. -func (app *ApiServer) postV1EventFollow(c *fiber.Ctx) error { - userID := app.getMyId(c) - eventID, err := trashid.DecodeHashId(c.Params("eventId")) - if err != nil { - return err - } - - // Sanity-check that the event actually exists before we burn a chain tx. - var exists bool - if err := app.pool.QueryRow(c.Context(), ` - SELECT EXISTS ( - SELECT 1 FROM events - WHERE event_id = $1 AND COALESCE(is_deleted, false) = false - ) - `, eventID).Scan(&exists); err != nil { - return err - } - if !exists { - return fiber.NewError(fiber.StatusNotFound, "event not found") - } - - signer, err := app.getApiSigner(c) - if err != nil { - return err - } - - nonce := time.Now().UnixNano() - - manageEntityTx := &corev1.ManageEntityLegacy{ - Signer: common.HexToAddress(signer.Address).String(), - UserId: int64(userID), - EntityId: int64(eventID), - Action: indexer.Action_Subscribe, - EntityType: indexer.Entity_Event, - Nonce: strconv.FormatInt(nonce, 10), - Metadata: "", - } - - response, err := app.sendTransactionWithSigner(manageEntityTx, signer.PrivateKey) - if err != nil { - app.logger.Error("Failed to send event follow transaction", zap.Error(err)) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Failed to follow event", - }) - } - - return c.JSON(fiber.Map{ - "transaction_hash": response.Msg.GetTransaction().GetHash(), - "block_hash": response.Msg.GetTransaction().GetBlockHash(), - "block_number": response.Msg.GetTransaction().GetHeight(), - }) -} - -func (app *ApiServer) deleteV1EventFollow(c *fiber.Ctx) error { - userID := app.getMyId(c) - eventID, err := trashid.DecodeHashId(c.Params("eventId")) - if err != nil { - return err - } - - signer, err := app.getApiSigner(c) - if err != nil { - return err - } - - nonce := time.Now().UnixNano() - - manageEntityTx := &corev1.ManageEntityLegacy{ - Signer: common.HexToAddress(signer.Address).String(), - UserId: int64(userID), - EntityId: int64(eventID), - Action: indexer.Action_Unsubscribe, - EntityType: indexer.Entity_Event, - Nonce: strconv.FormatInt(nonce, 10), - Metadata: "", - } - - response, err := app.sendTransactionWithSigner(manageEntityTx, signer.PrivateKey) - if err != nil { - app.logger.Error("Failed to send event unfollow transaction", zap.Error(err)) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Failed to unfollow event", - }) - } - - return c.JSON(fiber.Map{ - "transaction_hash": response.Msg.GetTransaction().GetHash(), - "block_hash": response.Msg.GetTransaction().GetBlockHash(), - "block_number": response.Msg.GetTransaction().GetHeight(), - }) -} - -// v1EventFollowState returns { is_followed, follower_count } for an event. -// Used by the contest page to render the follow button in the right state. -func (app *ApiServer) v1EventFollowState(c *fiber.Ctx) error { - eventID, err := trashid.DecodeHashId(c.Params("eventId")) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "invalid event id") - } - myID := app.getMyId(c) - - var followerCount int - if err := app.pool.QueryRow(c.Context(), ` - SELECT COUNT(*) - FROM subscriptions - WHERE entity_type = 'Event' - AND user_id = $1 - AND is_current = true - AND is_delete = false - `, eventID).Scan(&followerCount); err != nil { - if !errors.Is(err, pgx.ErrNoRows) { - return err - } - } - - var isFollowed bool - if myID > 0 { - if err := app.pool.QueryRow(c.Context(), ` - SELECT EXISTS ( - SELECT 1 FROM subscriptions - WHERE entity_type = 'Event' - AND user_id = $1 - AND subscriber_id = $2 - AND is_current = true - AND is_delete = false - ) - `, eventID, myID).Scan(&isFollowed); err != nil { - return err - } - } - - return c.JSON(fiber.Map{ - "data": fiber.Map{ - "is_followed": isFollowed, - "follower_count": followerCount, - }, - }) -} diff --git a/api/v1_events_followers_test.go b/api/v1_events_followers_test.go deleted file mode 100644 index 313637cc..00000000 --- a/api/v1_events_followers_test.go +++ /dev/null @@ -1,493 +0,0 @@ -package api - -import ( - "encoding/base64" - "fmt" - "strings" - "testing" - "time" - - "api.audius.co/api/testdata" - "api.audius.co/database" - "api.audius.co/trashid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// Tests for /v1/events/:eventId/follow* — the follow-contest endpoints added -// in Phase 2. The follow_state endpoint is the one with real read logic that -// can be validated end-to-end here; the POST / DELETE endpoints emit on-chain -// transactions via sendTransactionWithSigner, which is exercised upstream by -// the discovery-provider indexer tests. Here we only verify they're routed, -// authed, and parse params correctly. - -// Pre-registered test wallets with signature data in api/testdata/signatures.go. -// testGetWithWallet resolves the caller's my_id by looking up the wallet against -// the users table, so each test user must be seeded with one of these addresses. -const ( - testEventArtistWallet = "0x7d273271690538cf855e5b3002a0dd8c154bb060" - testEventFanWallet = "0xc3d1d41e6872ffbd15c473d14fc3a9250be5b5e0" - testEventFan2Wallet = "0x4954d18926ba0ed9378938444731be4e622537b2" -) - -func testEventFollowersBaseUsers() []map[string]any { - return []map[string]any{ - { - "user_id": 1, - "handle": "eventartist", - "handle_lc": "eventartist", - "name": "Event Artist", - "wallet": testEventArtistWallet, - }, - { - "user_id": 2, - "handle": "eventfan", - "handle_lc": "eventfan", - "name": "Event Fan", - "wallet": testEventFanWallet, - }, - { - "user_id": 3, - "handle": "eventfan2", - "handle_lc": "eventfan2", - "name": "Event Fan 2", - "wallet": testEventFan2Wallet, - }, - } -} - -func TestEventFollowState_UnknownEventReturnsZeroNotError(t *testing.T) { - // The endpoint is tolerant: a request for a non-existent event returns - // 200 with zero counts rather than a 404. This makes the UI simpler - // (the button just renders "Follow" for non-existent events) and avoids - // a round-trip cascade when the contest was deleted mid-session. - app := emptyTestApp(t) - encEvent, err := trashid.EncodeHashId(999999) - require.NoError(t, err) - status, body := testGet(t, app, "/v1/events/"+encEvent+"/follow_state") - require.Equal(t, 200, status, string(body)) - jsonAssert(t, body, map[string]any{ - "data.is_followed": false, - "data.follower_count": 0, - }) -} - -func TestEventFollowState_InvalidEventIdReturns400(t *testing.T) { - app := emptyTestApp(t) - status, _ := testGet(t, app, "/v1/events/not-a-hashid/follow_state") - assert.Equal(t, 400, status) -} - -func TestEventFollowState_ZeroFollowers(t *testing.T) { - app := emptyTestApp(t) - database.Seed(app.pool.Replicas[0], database.FixtureMap{ - "users": testEventFollowersBaseUsers(), - "tracks": { - { - "track_id": 1, - "owner_id": 1, - "title": "Original", - "created_at": "2020-01-01 00:00:00", - }, - }, - "events": { - { - "event_id": 100, - "event_type": "remix_contest", - "user_id": 1, - "entity_type": "track", - "entity_id": 1, - "event_data": map[string]any{"description": "remix"}, - "created_at": "2020-05-01 00:00:00", - "updated_at": "2020-05-01 00:00:00", - }, - }, - }) - - encEvent, err := trashid.EncodeHashId(100) - require.NoError(t, err) - status, body := testGet(t, app, "/v1/events/"+encEvent+"/follow_state") - require.Equal(t, 200, status, string(body)) - jsonAssert(t, body, map[string]any{ - "data.is_followed": false, - "data.follower_count": 0, - }) -} - -func TestEventFollowState_CountsOnlyLiveEventSubscriptions(t *testing.T) { - // follower_count MUST: - // - count rows with entity_type='Event' and matching user_id=event_id - // - only include current & non-deleted rows - // - NOT count rows with entity_type='User' (legacy user-follows) even if - // they happen to target the same numeric id - // - NOT count stale (is_current=false) rows - app := emptyTestApp(t) - - database.Seed(app.pool.Replicas[0], database.FixtureMap{ - "users": testEventFollowersBaseUsers(), - "tracks": { - { - "track_id": 1, - "owner_id": 1, - "title": "Original", - "created_at": "2020-01-01 00:00:00", - }, - }, - "events": { - { - "event_id": 200, - "event_type": "remix_contest", - "user_id": 1, - "entity_type": "track", - "entity_id": 1, - "event_data": map[string]any{"description": "remix"}, - "created_at": "2020-05-01 00:00:00", - "updated_at": "2020-05-01 00:00:00", - }, - }, - "subscriptions": []map[string]any{ - // Two live event subscribers - { - "subscriber_id": 2, - "user_id": 200, - "entity_type": "Event", - "entity_id": 200, - "is_current": true, - "is_delete": false, - "blockhash": "bh1", - "blocknumber": 101, - "txhash": "tx1", - }, - { - "subscriber_id": 3, - "user_id": 200, - "entity_type": "Event", - "entity_id": 200, - "is_current": true, - "is_delete": false, - "blockhash": "bh2", - "blocknumber": 101, - "txhash": "tx2", - }, - // A legacy user-type subscription with matching numeric id — - // must NOT be counted. - { - "subscriber_id": 2, - "user_id": 200, - "entity_type": "User", - "entity_id": nil, - "is_current": true, - "is_delete": false, - "blockhash": "bh3", - "blocknumber": 101, - "txhash": "tx3", - }, - // A deleted event subscription — must NOT be counted. - { - "subscriber_id": 1, - "user_id": 200, - "entity_type": "Event", - "entity_id": 200, - "is_current": true, - "is_delete": true, - "blockhash": "bh4", - "blocknumber": 101, - "txhash": "tx4", - }, - }, - }) - - encEvent, err := trashid.EncodeHashId(200) - require.NoError(t, err) - status, body := testGet(t, app, "/v1/events/"+encEvent+"/follow_state") - require.Equal(t, 200, status, string(body)) - jsonAssert(t, body, map[string]any{ - "data.follower_count": 2, - // No viewer auth was passed, so the is_followed flag is false. - "data.is_followed": false, - }) -} - -func TestEventFollowState_IsFollowedFromAuthedViewer(t *testing.T) { - // When the request carries a signed wallet header that resolves to a user - // with a current, non-deleted event subscription, is_followed is true. - app := emptyTestApp(t) - - database.Seed(app.pool.Replicas[0], database.FixtureMap{ - "users": testEventFollowersBaseUsers(), - "tracks": { - { - "track_id": 1, - "owner_id": 1, - "title": "Original", - "created_at": "2020-01-01 00:00:00", - }, - }, - "events": { - { - "event_id": 300, - "event_type": "remix_contest", - "user_id": 1, - "entity_type": "track", - "entity_id": 1, - "event_data": map[string]any{"description": "remix"}, - "created_at": "2020-05-01 00:00:00", - "updated_at": "2020-05-01 00:00:00", - }, - }, - "subscriptions": []map[string]any{ - { - "subscriber_id": 2, // user 2 is subscribed - "user_id": 300, - "entity_type": "Event", - "entity_id": 300, - "is_current": true, - "is_delete": false, - "blockhash": "bh1", - "blocknumber": 101, - "txhash": "tx1", - }, - }, - }) - - encEvent, err := trashid.EncodeHashId(300) - require.NoError(t, err) - user2HashID := trashid.MustEncodeHashID(2) - user3HashID := trashid.MustEncodeHashID(3) - - // Viewer = user 2 — subscribed. Must supply BOTH user_id (so the middleware - // sets myId=2) AND the matching wallet signature (so the auth check - // accepts the request). - status, body := testGetWithWallet( - t, - app, - "/v1/events/"+encEvent+"/follow_state?user_id="+user2HashID, - testEventFanWallet, - ) - require.Equal(t, 200, status, string(body)) - jsonAssert(t, body, map[string]any{ - "data.is_followed": true, - "data.follower_count": 1, - }) - - // Viewer = user 3 — NOT subscribed. Still sees the follower_count but - // their own follow state is false. - status, body = testGetWithWallet( - t, - app, - "/v1/events/"+encEvent+"/follow_state?user_id="+user3HashID, - testEventFan2Wallet, - ) - require.Equal(t, 200, status, string(body)) - jsonAssert(t, body, map[string]any{ - "data.is_followed": false, - "data.follower_count": 1, - }) -} - -func TestEventFollowState_DoesNotTreatUserSubscriptionAsEventFollow(t *testing.T) { - // A user who has a legacy User-type subscription whose user_id column - // happens to equal an event's event_id must NOT appear as an event - // follower. This is the regression guard for the entity_type discriminator. - app := emptyTestApp(t) - - database.Seed(app.pool.Replicas[0], database.FixtureMap{ - "users": testEventFollowersBaseUsers(), - "tracks": { - { - "track_id": 1, - "owner_id": 1, - "title": "Original", - "created_at": "2020-01-01 00:00:00", - }, - }, - "events": { - { - "event_id": 400, - "event_type": "remix_contest", - "user_id": 1, - "entity_type": "track", - "entity_id": 1, - "event_data": map[string]any{"description": "remix"}, - "created_at": "2020-05-01 00:00:00", - "updated_at": "2020-05-01 00:00:00", - }, - }, - "subscriptions": []map[string]any{ - // Legacy user-type subscription: user 2 is subscribed to user 400 - // (fake user_id chosen to match the event_id numerically). - { - "subscriber_id": 2, - "user_id": 400, - "entity_type": "User", - "entity_id": nil, - "is_current": true, - "is_delete": false, - "blockhash": "bh1", - "blocknumber": 101, - "txhash": "tx1", - }, - }, - }) - - encEvent, err := trashid.EncodeHashId(400) - require.NoError(t, err) - user2HashID := trashid.MustEncodeHashID(2) - status, body := testGetWithWallet( - t, - app, - "/v1/events/"+encEvent+"/follow_state?user_id="+user2HashID, - testEventFanWallet, - ) - require.Equal(t, 200, status, string(body)) - jsonAssert(t, body, map[string]any{ - "data.is_followed": false, - "data.follower_count": 0, - }) -} - -// --------------------------------------------------------------------------- -// POST/DELETE follow — shallow coverage -// --------------------------------------------------------------------------- -// -// These endpoints build a ManageEntity transaction and hand it off to -// sendTransactionWithSigner, which requires a running ganache / chain to -// actually roundtrip. The real behavioral coverage lives in -// integration_tests/tasks/entity_manager/test_event_follow.py. Here we only -// verify that: -// - missing auth is 401 -// - a non-existent event is 404 (pre-flight DB check, before any tx) -// - a valid request gets past auth/parse/pre-flight, which means the -// handler reaches the SDK layer (whose failure we tolerate) - -func TestPostEventFollow_MissingAuthRejected(t *testing.T) { - app := emptyTestApp(t) - enc, err := trashid.EncodeHashId(100) - require.NoError(t, err) - status, _ := testPost(t, app, "/v1/events/"+enc+"/follow", nil, nil) - assert.Equal(t, 401, status) -} - -func TestPostEventFollow_UnknownEventIs404BeforeSendingTx(t *testing.T) { - // We hit the pre-flight EXISTS check inside postV1EventFollow before any - // chain transaction is built, so this must return 404 deterministically - // even without a running chain. - testPrivateKey := "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - testWallet := testdata.CreateTestWallet(t, testPrivateKey) - - app := emptyTestApp(t) - - now := time.Now() - database.Seed(app.writePool, database.FixtureMap{ - "users": { - { - "user_id": 500, - "handle": "eventfollower", - "handle_lc": "eventfollower", - "wallet": strings.ToLower(testWallet.Address), - "is_current": true, - "created_at": now, - "updated_at": now, - }, - }, - "grants": { - { - "user_id": 500, - "grantee_address": strings.ToLower(testWallet.Address), - "is_approved": true, - "is_revoked": false, - "is_current": true, - "created_at": now, - "updated_at": now, - }, - }, - }) - - encEvent, err := trashid.EncodeHashId(999999) - require.NoError(t, err) - userHashID := trashid.MustEncodeHashID(500) - - authString := fmt.Sprintf("user:%s", testPrivateKey) - headers := map[string]string{ - "Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte(authString)), - } - status, _ := testPost( - t, - app, - fmt.Sprintf("/v1/events/%s/follow?user_id=%s", encEvent, userHashID), - nil, - headers, - ) - assert.Equal(t, 404, status) -} - -func TestPostEventFollow_DeletedEventIs404(t *testing.T) { - testPrivateKey := "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - testWallet := testdata.CreateTestWallet(t, testPrivateKey) - - app := emptyTestApp(t) - - now := time.Now() - database.Seed(app.writePool, database.FixtureMap{ - "users": { - { - "user_id": 500, - "handle": "eventfollower", - "handle_lc": "eventfollower", - "wallet": strings.ToLower(testWallet.Address), - "is_current": true, - "created_at": now, - "updated_at": now, - }, - }, - "tracks": { - { - "track_id": 1, - "owner_id": 1, - "title": "Original", - "created_at": now, - }, - }, - "events": { - { - "event_id": 700, - "event_type": "remix_contest", - "user_id": 1, - "entity_type": "track", - "entity_id": 1, - "event_data": map[string]any{"description": "deleted contest"}, - "is_deleted": true, - "created_at": now, - "updated_at": now, - }, - }, - "grants": { - { - "user_id": 500, - "grantee_address": strings.ToLower(testWallet.Address), - "is_approved": true, - "is_revoked": false, - "is_current": true, - "created_at": now, - "updated_at": now, - }, - }, - }) - - encEvent, err := trashid.EncodeHashId(700) - require.NoError(t, err) - userHashID := trashid.MustEncodeHashID(500) - - authString := fmt.Sprintf("user:%s", testPrivateKey) - headers := map[string]string{ - "Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte(authString)), - } - status, _ := testPost( - t, - app, - fmt.Sprintf("/v1/events/%s/follow?user_id=%s", encEvent, userHashID), - nil, - headers, - ) - assert.Equal(t, 404, status) -} diff --git a/api/v1_events_remix_contests.go b/api/v1_events_remix_contests.go deleted file mode 100644 index b816d575..00000000 --- a/api/v1_events_remix_contests.go +++ /dev/null @@ -1,106 +0,0 @@ -package api - -import ( - "strings" - - "api.audius.co/api/dbv1" - "github.com/gofiber/fiber/v2" - "github.com/jackc/pgx/v5" -) - -type GetRemixContestsParams struct { - Limit int `query:"limit" default:"25" validate:"min=1,max=100"` - Offset int `query:"offset" default:"0" validate:"min=0"` - Status string `query:"status" default:"all" validate:"oneof=active ended all"` -} - -// v1EventsRemixContests returns remix-contest events from the events table, -// ordered with currently-active contests first (by soonest-ending end_date), -// followed by ended contests (most-recently-ended first). Supports pagination -// and an optional `status` filter (active | ended | all). -func (app *ApiServer) v1EventsRemixContests(c *fiber.Ctx) error { - params := GetRemixContestsParams{} - if err := app.ParseAndValidateQueryParams(c, ¶ms); err != nil { - return err - } - - filters := []string{ - "e.event_type = 'remix_contest'", - "e.is_deleted = false", - "(e.entity_type != 'track' OR (t.track_id IS NOT NULL AND t.is_delete = false))", - } - - switch params.Status { - case "active": - filters = append(filters, "(e.end_date IS NULL OR e.end_date > NOW())") - case "ended": - filters = append(filters, "(e.end_date IS NOT NULL AND e.end_date <= NOW())") - } - - sql := ` - SELECT - e.event_id, - e.entity_type::event_entity_type AS entity_type, - e.user_id, - e.entity_id, - e.event_type::event_type AS event_type, - e.end_date, - e.is_deleted, - e.created_at, - e.updated_at, - e.event_data - FROM events e - LEFT JOIN tracks t ON t.track_id = e.entity_id - AND t.is_current = true - AND e.entity_type = 'track' - AND t.access_authorities IS NULL - WHERE ` + strings.Join(filters, " AND ") + ` - ORDER BY - CASE WHEN e.end_date IS NULL OR e.end_date > NOW() THEN 0 ELSE 1 END ASC, - CASE WHEN e.end_date IS NULL OR e.end_date > NOW() THEN e.end_date END ASC NULLS LAST, - CASE WHEN e.end_date IS NOT NULL AND e.end_date <= NOW() THEN e.end_date END DESC, - e.event_id ASC - LIMIT @limit OFFSET @offset; - ` - - rows, err := app.pool.Query(c.Context(), sql, pgx.NamedArgs{ - "limit": params.Limit, - "offset": params.Offset, - }) - if err != nil { - return err - } - defer rows.Close() - - var items []dbv1.GetEventsRow - for rows.Next() { - var row dbv1.GetEventsRow - if err := rows.Scan( - &row.EventID, - &row.EntityType, - &row.UserID, - &row.EntityID, - &row.EventType, - &row.EndDate, - &row.IsDeleted, - &row.CreatedAt, - &row.UpdatedAt, - &row.EventData, - ); err != nil { - return err - } - items = append(items, row) - } - if err := rows.Err(); err != nil { - return err - } - - data := make([]dbv1.FullEvent, 0, len(items)) - for _, event := range items { - data = append(data, app.queries.ToFullEvent(event)) - } - - return c.JSON(fiber.Map{ - "data": data, - }) -} diff --git a/api/v1_track_remixes_test.go b/api/v1_track_remixes_test.go index d57107a6..23825e59 100644 --- a/api/v1_track_remixes_test.go +++ b/api/v1_track_remixes_test.go @@ -1,14 +1,11 @@ package api import ( - "context" "testing" - "time" "api.audius.co/database" "api.audius.co/trashid" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestTrackRemixes(t *testing.T) { @@ -233,136 +230,6 @@ func TestTrackRemixes(t *testing.T) { }) } -// When an artist creates a remix contest while their track is still unlisted, -// the on_event trigger skips the fan_remix_contest_started notifications because -// the track is not yet public. When the artist later flips is_unlisted to false, -// the on_track trigger must pick up the active contest and create the missing -// notifications for the contest creator's followers and the track's savers. -func TestFanRemixContestStartedOnUnlistedToPublic(t *testing.T) { - app := emptyTestApp(t) - ctx := context.Background() - require.NotNil(t, app.writePool, "test requires write pool") - - ownerId := 9001 - followerId := 9002 - saverId := 9003 - unrelatedUserId := 9004 - trackId := 9101 - eventId := 9201 - - now := time.Now().UTC() - fixtures := database.FixtureMap{ - "users": []map[string]any{ - {"user_id": ownerId, "handle": "fan_contest_owner"}, - {"user_id": followerId, "handle": "fan_contest_follower"}, - {"user_id": saverId, "handle": "fan_contest_saver"}, - {"user_id": unrelatedUserId, "handle": "fan_contest_unrelated"}, - }, - "tracks": []map[string]any{ - { - "track_id": trackId, - "owner_id": ownerId, - "title": "Unlisted Contest Track", - "is_unlisted": true, - "created_at": now, - "updated_at": now, - }, - }, - "follows": []map[string]any{ - { - "follower_user_id": followerId, - "followee_user_id": ownerId, - "created_at": now.Add(-time.Hour), - }, - }, - "saves": []map[string]any{ - { - "user_id": saverId, - "save_item_id": trackId, - "save_type": "track", - "created_at": now.Add(-time.Hour), - }, - }, - "events": []map[string]any{ - { - "event_id": eventId, - "event_type": "remix_contest", - "entity_id": trackId, - "user_id": ownerId, - "created_at": now, - "end_date": now.Add(7 * 24 * time.Hour), - }, - }, - } - - database.Seed(app.pool.Replicas[0], fixtures) - - // Sanity check: the on_event trigger should NOT have created fan notifications - // while the track was still unlisted. - var preFlipCount int - err := app.writePool.QueryRow(ctx, ` - SELECT count(*) FROM notification - WHERE type = 'fan_remix_contest_started' - AND group_id = 'fan_remix_contest_started:' || $1::int || ':user:' || $2::int - `, trackId, ownerId).Scan(&preFlipCount) - require.NoError(t, err) - assert.Equal(t, 0, preFlipCount, "no fan_remix_contest_started notifications should exist while track is unlisted") - - // Flip the track to public. This should fire handle_track's new unlisted->public - // branch and create fan_remix_contest_started notifications. - _, err = app.writePool.Exec(ctx, - `UPDATE tracks SET is_unlisted = false WHERE track_id = $1 AND is_current = true`, - trackId, - ) - require.NoError(t, err) - - // Both the follower and the saver should have received a notification. - type notifRow struct { - Specifier string - GroupId string - UserIds []int32 - EntityId int - OwnerId int - } - rows, err := app.writePool.Query(ctx, ` - SELECT specifier, group_id, user_ids, - (data->>'entity_id')::int, - (data->>'entity_user_id')::int - FROM notification - WHERE type = 'fan_remix_contest_started' - AND group_id = 'fan_remix_contest_started:' || $1::int || ':user:' || $2::int - ORDER BY specifier ASC - `, trackId, ownerId) - require.NoError(t, err) - defer rows.Close() - - var got []notifRow - for rows.Next() { - var r notifRow - require.NoError(t, rows.Scan(&r.Specifier, &r.GroupId, &r.UserIds, &r.EntityId, &r.OwnerId)) - got = append(got, r) - } - require.NoError(t, rows.Err()) - - require.Len(t, got, 2, "expected exactly one notification per notified user (follower, saver)") - - notifiedUserIds := map[int32]bool{} - for _, r := range got { - assert.Equal(t, - "fan_remix_contest_started:9101:user:9001", - r.GroupId, - "group_id must match handle_event's format", - ) - assert.Equal(t, trackId, r.EntityId) - assert.Equal(t, ownerId, r.OwnerId) - require.Len(t, r.UserIds, 1) - notifiedUserIds[r.UserIds[0]] = true - } - assert.True(t, notifiedUserIds[int32(followerId)], "follower should have been notified") - assert.True(t, notifiedUserIds[int32(saverId)], "saver should have been notified") - assert.False(t, notifiedUserIds[int32(unrelatedUserId)], "unrelated user should not have been notified") -} - func TestTrackRemixesInvalidParams(t *testing.T) { app := emptyTestApp(t) diff --git a/database/seed.go b/database/seed.go index ca2aebbc..d8a965f1 100644 --- a/database/seed.go +++ b/database/seed.go @@ -245,28 +245,6 @@ var ( "txhash": "0x1", "blockhash": "0x2", }, - "comment_reactions": { - "comment_id": nil, - "user_id": nil, - "is_delete": false, - "created_at": time.Now(), - "updated_at": time.Now(), - "txhash": "0x1", - "blockhash": "0x2", - "blocknumber": 101, - }, - "subscriptions": { - "subscriber_id": nil, - "user_id": nil, - "is_current": true, - "is_delete": false, - "created_at": time.Now(), - "blockhash": "block_abc123", - "blocknumber": 101, - "txhash": "0x1", - "entity_type": "User", - "entity_id": nil, - }, "events": { "txhash": "0x1", "blockhash": "0x2", diff --git a/ddl/functions/handle_track.sql b/ddl/functions/handle_track.sql index 803db001..d39da7e3 100644 --- a/ddl/functions/handle_track.sql +++ b/ddl/functions/handle_track.sql @@ -163,71 +163,6 @@ begin raise warning 'An error occurred in %: %', tg_name, sqlerrm; end; - -- If a track with an active remix contest transitions from unlisted to public, - -- create fan_remix_contest_started notifications for the contest creator's - -- followers and the track's savers. Mirrors handle_event.sql for the case - -- where the contest was created while the track was still unlisted. - begin - if TG_OP = 'UPDATE' and OLD.is_unlisted = true and new.is_unlisted = false THEN - declare - contest_event_id int; - contest_creator_id int; - notified_user_id int; - begin - select event_id, user_id - into contest_event_id, contest_creator_id - from events - where event_type = 'remix_contest' - and is_deleted = false - and end_date > now() - and entity_id = new.track_id - limit 1; - - if contest_event_id is not null then - for notified_user_id in - select distinct user_id - from ( - -- Get followers of the contest creator - select f.follower_user_id as user_id - from follows f - where f.followee_user_id = contest_creator_id - and f.is_current = true - and f.is_delete = false - union - -- Get users who favorited the track - select s.user_id - from saves s - where s.save_item_id = new.track_id - and s.save_type = 'track' - and s.is_current = true - and s.is_delete = false - ) as users_to_notify - loop - insert into notification - (blocknumber, user_ids, timestamp, type, specifier, group_id, data) - values - ( - new.blocknumber, - ARRAY [notified_user_id], - new.updated_at, - 'fan_remix_contest_started', - notified_user_id, - 'fan_remix_contest_started:' || new.track_id || ':user:' || contest_creator_id, - json_build_object( - 'entity_user_id', new.owner_id, - 'entity_id', new.track_id - ) - ) - on conflict do nothing; - end loop; - end if; - end; - end if; - exception - when others then - raise warning 'An error occurred in %: %', tg_name, sqlerrm; - end; - return null; exception diff --git a/ddl/migrations/0195_subscriptions_generic_entity.sql b/ddl/migrations/0195_subscriptions_generic_entity.sql deleted file mode 100644 index 4e28abc0..00000000 --- a/ddl/migrations/0195_subscriptions_generic_entity.sql +++ /dev/null @@ -1,25 +0,0 @@ -begin; - --- Generalise the subscriptions table to support following entities other --- than users. Existing rows always represent User→User subscriptions; the --- new entity_type column defaults to 'User' so they're unchanged. New rows --- (e.g. "user follows a remix-contest event") set entity_type='Event' and --- mirror the target's id into entity_id while keeping the legacy user_id --- column populated so the existing (subscriber_id, user_id) uniqueness --- constraint remains collision-free across subscription kinds. - -alter table subscriptions - add column if not exists entity_type text not null default 'User'; - -alter table subscriptions - add column if not exists entity_id integer; - -update subscriptions - set entity_type = 'User' - where entity_type is null or entity_type = ''; - -create index if not exists subscriptions_entity_type_entity_id_idx - on subscriptions (entity_type, entity_id) - where is_current = true and is_delete = false; - -commit; diff --git a/indexer/constants.go b/indexer/constants.go index 0fe8dd4f..aed32b98 100644 --- a/indexer/constants.go +++ b/indexer/constants.go @@ -37,5 +37,4 @@ const ( Entity_AssociatedWallet = "AssociatedWallet" Entity_Grant = "Grant" Entity_DeveloperApp = "DeveloperApp" - Entity_Event = "Event" ) diff --git a/sql/01_schema.sql b/sql/01_schema.sql index 5567cab3..f30086e9 100644 --- a/sql/01_schema.sql +++ b/sql/01_schema.sql @@ -3935,71 +3935,6 @@ begin raise warning 'An error occurred in %: %', tg_name, sqlerrm; end; - -- If a track with an active remix contest transitions from unlisted to public, - -- create fan_remix_contest_started notifications for the contest creator's - -- followers and the track's savers. Mirrors handle_event.sql for the case - -- where the contest was created while the track was still unlisted. - begin - if TG_OP = 'UPDATE' and OLD.is_unlisted = true and new.is_unlisted = false THEN - declare - contest_event_id int; - contest_creator_id int; - notified_user_id int; - begin - select event_id, user_id - into contest_event_id, contest_creator_id - from events - where event_type = 'remix_contest' - and is_deleted = false - and end_date > now() - and entity_id = new.track_id - limit 1; - - if contest_event_id is not null then - for notified_user_id in - select distinct user_id - from ( - -- Get followers of the contest creator - select f.follower_user_id as user_id - from follows f - where f.followee_user_id = contest_creator_id - and f.is_current = true - and f.is_delete = false - union - -- Get users who favorited the track - select s.user_id - from saves s - where s.save_item_id = new.track_id - and s.save_type = 'track' - and s.is_current = true - and s.is_delete = false - ) as users_to_notify - loop - insert into notification - (blocknumber, user_ids, timestamp, type, specifier, group_id, data) - values - ( - new.blocknumber, - ARRAY [notified_user_id], - new.updated_at, - 'fan_remix_contest_started', - notified_user_id, - 'fan_remix_contest_started:' || new.track_id || ':user:' || contest_creator_id, - json_build_object( - 'entity_user_id', new.owner_id, - 'entity_id', new.track_id - ) - ) - on conflict do nothing; - end loop; - end if; - end; - end if; - exception - when others then - raise warning 'An error occurred in %: %', tg_name, sqlerrm; - end; - return null; exception @@ -8677,9 +8612,7 @@ CREATE TABLE public.subscriptions ( is_current boolean NOT NULL, is_delete boolean NOT NULL, created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - txhash character varying DEFAULT ''::character varying NOT NULL, - entity_type text DEFAULT 'User'::text NOT NULL, - entity_id integer + txhash character varying DEFAULT ''::character varying NOT NULL );