Skip to content

Commit

Permalink
feat: notifications (#109)
Browse files Browse the repository at this point in the history
  • Loading branch information
crlssn authored Nov 27, 2024
1 parent fd95786 commit 34556a0
Show file tree
Hide file tree
Showing 25 changed files with 2,553 additions and 168 deletions.
13 changes: 13 additions & 0 deletions database/migrations/015_notifications.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE TYPE getstronger.notification_type AS ENUM ('Follow', 'WorkoutComment');

CREATE TABLE getstronger.notifications
(
id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES getstronger.users (id),
"type" getstronger.notification_type NOT NULL,
payload JSONB NOT NULL,
read_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC')
);

CREATE INDEX idx_notifications_user_id_created_at ON getstronger.notifications (user_id, read_at);
29 changes: 29 additions & 0 deletions protobufs/api/v1/users.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ package api.v1;

import "api/v1/options.proto";
import "api/v1/shared.proto";
import "api/v1/workouts.proto";

import "google/protobuf/timestamp.proto";

import "buf/validate/validate.proto";

Expand All @@ -26,6 +29,9 @@ service UserService {
rpc Search(SearchRequest) returns (SearchResponse) {
option (auth) = true;
}
rpc ListNotifications(ListNotificationsRequest) returns (ListNotificationsResponse) {
option (auth) = true;
}
}

message GetUserRequest {
Expand Down Expand Up @@ -67,3 +73,26 @@ message SearchResponse {
repeated User users = 1;
PaginationResponse pagination = 2;
}

message ListNotificationsRequest {
bool only_unread = 1;
PaginationRequest pagination = 2 [(buf.validate.field).required = true];
}
message ListNotificationsResponse {
repeated Notification notifications = 1;
PaginationResponse pagination = 2;
}

message Notification {
message WorkoutComment {
User actor = 1;
Workout workout = 2;
}

string id = 1;
// DEBT: This should be a timestamp but the client is not able to parse it.
int64 notified_at_unix = 2;
oneof type {
WorkoutComment workout_comment = 3;
}
}
5 changes: 4 additions & 1 deletion server/bus/events/events.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
package events

const RequestTraced = "request:traced"
const (
RequestTraced = "request:traced"
WorkoutCommentPosted = "workout_comment:posted"
)
45 changes: 44 additions & 1 deletion server/bus/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import (
"go.uber.org/zap"

"github.com/crlssn/getstronger/server/bus/payloads"
"github.com/crlssn/getstronger/server/pkg/orm"
"github.com/crlssn/getstronger/server/pkg/repo"
)

type Handler interface {
HandlePayload(payload any)
}

var _ Handler = (*RequestTraced)(nil)
var (
_ Handler = (*RequestTraced)(nil)
_ Handler = (*WorkoutCommentPosted)(nil)
)

type RequestTraced struct {
log *zap.Logger
Expand Down Expand Up @@ -44,3 +48,42 @@ func (h *RequestTraced) HandlePayload(payload any) {
h.log.Error("unexpected event type", zap.Any("event", payload))
}
}

type WorkoutCommentPosted struct {
log *zap.Logger
repo *repo.Repo
}

func NewWorkoutCommentPosted(log *zap.Logger, repo *repo.Repo) *WorkoutCommentPosted {
return &WorkoutCommentPosted{log, repo}
}

func (w *WorkoutCommentPosted) HandlePayload(payload any) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

switch t := payload.(type) {
case *payloads.WorkoutCommentPosted:
comment, err := w.repo.GetWorkoutComment(ctx,
repo.GetWorkoutCommentWithID(t.CommentID),
repo.GetWorkoutCommentWithWorkout(),
)
if err != nil {
w.log.Error("get workout comment", zap.Error(err))
return
}

if err = w.repo.CreateNotification(ctx, repo.CreateNotificationParams{
Type: orm.NotificationTypeWorkoutComment,
UserID: comment.R.Workout.UserID,
Payload: repo.NotificationPayload{
ActorID: comment.UserID,
WorkoutID: comment.WorkoutID,
},
}); err != nil {
w.log.Error("create notification", zap.Error(err))
}
default:
w.log.Error("unexpected event type", zap.Any("event", payload))
}
}
6 changes: 4 additions & 2 deletions server/bus/handlers/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ type Registry struct {
type RegistryParams struct {
fx.In

RequestTraced *RequestTraced
RequestTraced *RequestTraced
WorkoutCommentPosted *WorkoutCommentPosted
}

func NewRegistry(p RegistryParams) *Registry {
return &Registry{
handlers: map[string]Handler{
events.RequestTraced: p.RequestTraced,
events.RequestTraced: p.RequestTraced,
events.WorkoutCommentPosted: p.WorkoutCommentPosted,
},
}
}
Expand Down
1 change: 1 addition & 0 deletions server/bus/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func Module() fx.Option {
New,
handlers.NewRegistry,
handlers.NewRequestTraced,
handlers.NewWorkoutCommentPosted,
),
fx.Invoke(
func(lc fx.Lifecycle, bus *Bus, registry *handlers.Registry) {
Expand Down
4 changes: 4 additions & 0 deletions server/bus/payloads/payloads.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ type RequestTraced struct {
DurationMS int
StatusCode int
}

type WorkoutCommentPosted struct {
CommentID string
}
2 changes: 2 additions & 0 deletions server/pkg/orm/boil_table_names.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions server/pkg/orm/boil_types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 34556a0

Please sign in to comment.