Boost Azure Demo
This commit is contained in:
708
Backend/internal/handlers/api/messages/handler.go
Normal file
708
Backend/internal/handlers/api/messages/handler.go
Normal file
@@ -0,0 +1,708 @@
|
||||
package messages
|
||||
|
||||
import (
|
||||
"boostai-backend/internal/database"
|
||||
"boostai-backend/internal/handlers/api/shared"
|
||||
"boostai-backend/internal/http/params"
|
||||
"boostai-backend/internal/http/respond"
|
||||
authmw "boostai-backend/internal/middleware"
|
||||
"boostai-backend/internal/sqlc"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
db *database.DB
|
||||
queries *sqlc.Queries
|
||||
}
|
||||
|
||||
type recipientResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
FullName string `json:"full_name"`
|
||||
PreferredName *string `json:"preferred_name"`
|
||||
ProfileIconURL *string `json:"profile_icon_url"`
|
||||
Headline *string `json:"headline"`
|
||||
}
|
||||
|
||||
type threadParticipantResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
FullName string `json:"full_name"`
|
||||
PreferredName *string `json:"preferred_name"`
|
||||
ProfileIconURL *string `json:"profile_icon_url"`
|
||||
Headline *string `json:"headline"`
|
||||
JoinedAt *time.Time `json:"joined_at,omitempty"`
|
||||
LastReadAt *time.Time `json:"last_read_at,omitempty"`
|
||||
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
||||
}
|
||||
|
||||
type messageSenderResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
FullName string `json:"full_name"`
|
||||
PreferredName *string `json:"preferred_name"`
|
||||
ProfileIconURL *string `json:"profile_icon_url"`
|
||||
Headline *string `json:"headline"`
|
||||
}
|
||||
|
||||
type messageResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
ThreadID int64 `json:"thread_id"`
|
||||
Body string `json:"body"`
|
||||
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
Mine bool `json:"mine"`
|
||||
Sender messageSenderResponse `json:"sender"`
|
||||
}
|
||||
|
||||
type messageThreadSummaryResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
Subject string `json:"subject"`
|
||||
CreatedByUserID int64 `json:"created_by_user_id"`
|
||||
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
UnreadCount int64 `json:"unread_count"`
|
||||
LastMessageID int64 `json:"last_message_id"`
|
||||
LastMessageBody *string `json:"last_message_body"`
|
||||
LastMessageCreatedAt *time.Time `json:"last_message_created_at,omitempty"`
|
||||
LastMessageSender *messageSenderResponse `json:"last_message_sender,omitempty"`
|
||||
Participants []threadParticipantResponse `json:"participants"`
|
||||
}
|
||||
|
||||
type messageThreadDetailResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
Subject string `json:"subject"`
|
||||
CreatedByUserID int64 `json:"created_by_user_id"`
|
||||
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
UnreadCount int64 `json:"unread_count"`
|
||||
LastReadAt *time.Time `json:"last_read_at,omitempty"`
|
||||
Participants []threadParticipantResponse `json:"participants"`
|
||||
Messages []messageResponse `json:"messages"`
|
||||
}
|
||||
|
||||
type createThreadRequest struct {
|
||||
Subject string `json:"subject"`
|
||||
RecipientIDs []int64 `json:"recipient_ids"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
type createThreadResponse struct {
|
||||
ThreadID int64 `json:"thread_id"`
|
||||
}
|
||||
|
||||
type createThreadMessageRequest struct {
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
type updateThreadRequest struct {
|
||||
Subject string `json:"subject"`
|
||||
}
|
||||
|
||||
type updateThreadMessageRequest struct {
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
func NewHandler(db *database.DB) *Handler {
|
||||
return &Handler{db: db, queries: sqlc.New(db.Pool)}
|
||||
}
|
||||
|
||||
func (h *Handler) ListRecipients(c *fiber.Ctx) error {
|
||||
currentUserID := authmw.CurrentUserID(c)
|
||||
ctx, cancel := shared.WithTimeout()
|
||||
defer cancel()
|
||||
|
||||
recipients, err := h.queries.ListMessageRecipientsForUser(ctx, currentUserID)
|
||||
if err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
items := make([]recipientResponse, 0, len(recipients))
|
||||
for _, recipient := range recipients {
|
||||
items = append(items, mapRecipient(recipient))
|
||||
}
|
||||
|
||||
return c.JSON(shared.ListResponse[recipientResponse]{Data: items})
|
||||
}
|
||||
|
||||
func (h *Handler) ListThreads(c *fiber.Ctx) error {
|
||||
currentUserID := authmw.CurrentUserID(c)
|
||||
ctx, cancel := shared.WithTimeout()
|
||||
defer cancel()
|
||||
|
||||
threads, err := h.queries.ListMessageThreadsForUser(ctx, currentUserID)
|
||||
if err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
participants, err := h.queries.ListMessageThreadParticipantsForUser(ctx, currentUserID)
|
||||
if err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
participantsByThread := make(map[int64][]threadParticipantResponse)
|
||||
for _, participant := range participants {
|
||||
participantsByThread[participant.ThreadID] = append(participantsByThread[participant.ThreadID], mapThreadParticipant(participant))
|
||||
}
|
||||
|
||||
items := make([]messageThreadSummaryResponse, 0, len(threads))
|
||||
for _, thread := range threads {
|
||||
items = append(items, mapThreadSummary(thread, participantsByThread[thread.ThreadID]))
|
||||
}
|
||||
|
||||
return c.JSON(shared.ListResponse[messageThreadSummaryResponse]{Data: items})
|
||||
}
|
||||
|
||||
func (h *Handler) GetThread(c *fiber.Ctx) error {
|
||||
threadID, err := params.Int64PathParam(c, "threadId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
thread, err := h.loadThread(threadID, authmw.CurrentUserID(c))
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return respond.Error(c, fiber.StatusNotFound, "not_found", "Message thread not found")
|
||||
}
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
return c.JSON(thread)
|
||||
}
|
||||
|
||||
func (h *Handler) CreateThread(c *fiber.Ctx) error {
|
||||
currentUserID := authmw.CurrentUserID(c)
|
||||
|
||||
var req createThreadRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Unable to parse request body")
|
||||
}
|
||||
|
||||
subject := strings.TrimSpace(req.Subject)
|
||||
body := strings.TrimSpace(req.Body)
|
||||
if subject == "" {
|
||||
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "subject is required")
|
||||
}
|
||||
|
||||
recipientIDs := normalizeRecipientIDs(currentUserID, req.RecipientIDs)
|
||||
if len(recipientIDs) == 0 {
|
||||
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "At least one valid recipient is required")
|
||||
}
|
||||
|
||||
ctx, cancel := shared.WithTimeout()
|
||||
defer cancel()
|
||||
|
||||
tx, err := h.db.Pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback(ctx)
|
||||
}()
|
||||
|
||||
queries := h.queries.WithTx(tx)
|
||||
for _, recipientID := range recipientIDs {
|
||||
if _, err := queries.GetMessageRecipientByIDForUser(ctx, sqlc.GetMessageRecipientByIDForUserParams{ID: currentUserID, ID_2: recipientID}); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "One or more recipients are not available for messaging")
|
||||
}
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
}
|
||||
|
||||
thread, err := queries.CreateMessageThread(ctx, sqlc.CreateMessageThreadParams{
|
||||
CreatedByUserID: currentUserID,
|
||||
Subject: subject,
|
||||
})
|
||||
if err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
creatorReadAt := pgtype.Timestamptz{}
|
||||
if body != "" {
|
||||
message, err := queries.CreateThreadMessage(ctx, sqlc.CreateThreadMessageParams{
|
||||
ThreadID: thread.ID,
|
||||
SenderUserID: currentUserID,
|
||||
Body: body,
|
||||
})
|
||||
if err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
if message.CreatedAt.Valid {
|
||||
creatorReadAt = pgtype.Timestamptz{Time: message.CreatedAt.Time.UTC(), Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
if err := queries.AddMessageThreadParticipant(ctx, sqlc.AddMessageThreadParticipantParams{
|
||||
ThreadID: thread.ID,
|
||||
UserID: currentUserID,
|
||||
LastReadAt: creatorReadAt,
|
||||
}); err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
for _, recipientID := range recipientIDs {
|
||||
if err := queries.AddMessageThreadParticipant(ctx, sqlc.AddMessageThreadParticipantParams{
|
||||
ThreadID: thread.ID,
|
||||
UserID: recipientID,
|
||||
}); err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := queries.TouchMessageThread(ctx, thread.ID); err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(createThreadResponse{ThreadID: thread.ID})
|
||||
}
|
||||
|
||||
func (h *Handler) CreateThreadMessage(c *fiber.Ctx) error {
|
||||
threadID, err := params.Int64PathParam(c, "threadId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentUserID := authmw.CurrentUserID(c)
|
||||
var req createThreadMessageRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Unable to parse request body")
|
||||
}
|
||||
|
||||
body := strings.TrimSpace(req.Body)
|
||||
if body == "" {
|
||||
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "body is required")
|
||||
}
|
||||
|
||||
ctx, cancel := shared.WithTimeout()
|
||||
defer cancel()
|
||||
|
||||
tx, err := h.db.Pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback(ctx)
|
||||
}()
|
||||
|
||||
queries := h.queries.WithTx(tx)
|
||||
if _, err := queries.GetMessageThreadForUser(ctx, sqlc.GetMessageThreadForUserParams{ID: threadID, SenderUserID: currentUserID}); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return respond.Error(c, fiber.StatusNotFound, "not_found", "Message thread not found")
|
||||
}
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
if _, err := queries.CreateThreadMessage(ctx, sqlc.CreateThreadMessageParams{
|
||||
ThreadID: threadID,
|
||||
SenderUserID: currentUserID,
|
||||
Body: body,
|
||||
}); err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
if err := queries.TouchMessageThread(ctx, threadID); err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
if _, err := queries.MarkMessageThreadRead(ctx, sqlc.MarkMessageThreadReadParams{ThreadID: threadID, UserID: currentUserID}); err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateThread(c *fiber.Ctx) error {
|
||||
threadID, err := params.Int64PathParam(c, "threadId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentUserID := authmw.CurrentUserID(c)
|
||||
var req updateThreadRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Unable to parse request body")
|
||||
}
|
||||
|
||||
subject := strings.TrimSpace(req.Subject)
|
||||
if subject == "" {
|
||||
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "subject is required")
|
||||
}
|
||||
|
||||
ctx, cancel := shared.WithTimeout()
|
||||
defer cancel()
|
||||
|
||||
thread, err := h.queries.GetMessageThreadForUser(ctx, sqlc.GetMessageThreadForUserParams{ID: threadID, SenderUserID: currentUserID})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return respond.Error(c, fiber.StatusNotFound, "not_found", "Message thread not found")
|
||||
}
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
if thread.CreatedByUserID != currentUserID {
|
||||
return respond.Error(c, fiber.StatusForbidden, "forbidden", "Only the conversation starter can edit the thread title")
|
||||
}
|
||||
|
||||
if _, err := h.queries.UpdateMessageThreadSubject(ctx, sqlc.UpdateMessageThreadSubjectParams{
|
||||
ThreadID: threadID,
|
||||
Subject: subject,
|
||||
}); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return respond.Error(c, fiber.StatusNotFound, "not_found", "Message thread not found")
|
||||
}
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteThread(c *fiber.Ctx) error {
|
||||
threadID, err := params.Int64PathParam(c, "threadId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentUserID := authmw.CurrentUserID(c)
|
||||
ctx, cancel := shared.WithTimeout()
|
||||
defer cancel()
|
||||
|
||||
thread, err := h.queries.GetMessageThreadForUser(ctx, sqlc.GetMessageThreadForUserParams{ID: threadID, SenderUserID: currentUserID})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return respond.Error(c, fiber.StatusNotFound, "not_found", "Message thread not found")
|
||||
}
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
if thread.CreatedByUserID != currentUserID {
|
||||
return respond.Error(c, fiber.StatusForbidden, "forbidden", "Only the conversation starter can delete this conversation")
|
||||
}
|
||||
|
||||
if _, err := h.queries.DeleteMessageThread(ctx, threadID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return respond.Error(c, fiber.StatusNotFound, "not_found", "Message thread not found")
|
||||
}
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateThreadMessage(c *fiber.Ctx) error {
|
||||
threadID, err := params.Int64PathParam(c, "threadId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
messageID, err := params.Int64PathParam(c, "messageId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentUserID := authmw.CurrentUserID(c)
|
||||
var req updateThreadMessageRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Unable to parse request body")
|
||||
}
|
||||
|
||||
body := strings.TrimSpace(req.Body)
|
||||
if body == "" {
|
||||
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "body is required")
|
||||
}
|
||||
|
||||
ctx, cancel := shared.WithTimeout()
|
||||
defer cancel()
|
||||
|
||||
tx, err := h.db.Pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback(ctx)
|
||||
}()
|
||||
|
||||
queries := h.queries.WithTx(tx)
|
||||
if _, err := queries.UpdateThreadMessageBody(ctx, sqlc.UpdateThreadMessageBodyParams{
|
||||
Body: body,
|
||||
MessageID: messageID,
|
||||
ThreadID: threadID,
|
||||
UserID: currentUserID,
|
||||
}); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return respond.Error(c, fiber.StatusNotFound, "not_found", "Message not found")
|
||||
}
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
if err := queries.TouchMessageThread(ctx, threadID); err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteThreadMessage(c *fiber.Ctx) error {
|
||||
threadID, err := params.Int64PathParam(c, "threadId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
messageID, err := params.Int64PathParam(c, "messageId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentUserID := authmw.CurrentUserID(c)
|
||||
ctx, cancel := shared.WithTimeout()
|
||||
defer cancel()
|
||||
|
||||
tx, err := h.db.Pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback(ctx)
|
||||
}()
|
||||
|
||||
queries := h.queries.WithTx(tx)
|
||||
if _, err := queries.DeleteThreadMessage(ctx, sqlc.DeleteThreadMessageParams{
|
||||
MessageID: messageID,
|
||||
ThreadID: threadID,
|
||||
UserID: currentUserID,
|
||||
}); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return respond.Error(c, fiber.StatusNotFound, "not_found", "Message not found")
|
||||
}
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
if err := queries.TouchMessageThread(ctx, threadID); err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *Handler) MarkThreadRead(c *fiber.Ctx) error {
|
||||
threadID, err := params.Int64PathParam(c, "threadId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := shared.WithTimeout()
|
||||
defer cancel()
|
||||
|
||||
if _, err := h.queries.MarkMessageThreadRead(ctx, sqlc.MarkMessageThreadReadParams{ThreadID: threadID, UserID: authmw.CurrentUserID(c)}); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return respond.Error(c, fiber.StatusNotFound, "not_found", "Message thread not found")
|
||||
}
|
||||
return respond.DatabaseError(c, err)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *Handler) loadThread(threadID, currentUserID int64) (messageThreadDetailResponse, error) {
|
||||
queryCtx, cancel := shared.WithTimeout()
|
||||
defer cancel()
|
||||
|
||||
thread, err := h.queries.GetMessageThreadForUser(queryCtx, sqlc.GetMessageThreadForUserParams{ID: threadID, SenderUserID: currentUserID})
|
||||
if err != nil {
|
||||
return messageThreadDetailResponse{}, err
|
||||
}
|
||||
|
||||
participants, err := h.queries.ListParticipantsForThreadForUser(queryCtx, sqlc.ListParticipantsForThreadForUserParams{ThreadID: threadID, UserID: currentUserID})
|
||||
if err != nil {
|
||||
return messageThreadDetailResponse{}, err
|
||||
}
|
||||
|
||||
messages, err := h.queries.ListMessagesForThreadForUser(queryCtx, sqlc.ListMessagesForThreadForUserParams{ThreadID: threadID, UserID: currentUserID})
|
||||
if err != nil {
|
||||
return messageThreadDetailResponse{}, err
|
||||
}
|
||||
|
||||
participantItems := make([]threadParticipantResponse, 0, len(participants))
|
||||
for _, participant := range participants {
|
||||
participantItems = append(participantItems, mapThreadParticipantByThread(participant))
|
||||
}
|
||||
|
||||
messageItems := make([]messageResponse, 0, len(messages))
|
||||
for _, message := range messages {
|
||||
messageItems = append(messageItems, mapThreadMessage(message, currentUserID))
|
||||
}
|
||||
|
||||
return messageThreadDetailResponse{
|
||||
ID: thread.ID,
|
||||
Subject: thread.Subject,
|
||||
CreatedByUserID: thread.CreatedByUserID,
|
||||
CreatedAt: shared.TimePointer(thread.CreatedAt),
|
||||
UpdatedAt: shared.TimePointer(thread.UpdatedAt),
|
||||
UnreadCount: thread.UnreadCount,
|
||||
LastReadAt: shared.TimePointer(thread.LastReadAt),
|
||||
Participants: participantItems,
|
||||
Messages: messageItems,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapRecipient(row sqlc.ListMessageRecipientsForUserRow) recipientResponse {
|
||||
return recipientResponse{
|
||||
ID: row.UserID,
|
||||
Email: row.UserEmail,
|
||||
Role: string(row.UserRole),
|
||||
FullName: row.UserFullName,
|
||||
PreferredName: shared.TextPointer(row.PreferredName),
|
||||
ProfileIconURL: shared.TextPointer(row.ProfileIconUrl),
|
||||
Headline: shared.TextPointer(row.Headline),
|
||||
}
|
||||
}
|
||||
|
||||
func mapRecipientByID(row sqlc.GetMessageRecipientByIDForUserRow) recipientResponse {
|
||||
return recipientResponse{
|
||||
ID: row.UserID,
|
||||
Email: row.UserEmail,
|
||||
Role: string(row.UserRole),
|
||||
FullName: row.UserFullName,
|
||||
PreferredName: shared.TextPointer(row.PreferredName),
|
||||
ProfileIconURL: shared.TextPointer(row.ProfileIconUrl),
|
||||
Headline: shared.TextPointer(row.Headline),
|
||||
}
|
||||
}
|
||||
|
||||
func mapThreadParticipant(row sqlc.ListMessageThreadParticipantsForUserRow) threadParticipantResponse {
|
||||
return threadParticipantResponse{
|
||||
ID: row.UserID,
|
||||
Email: row.UserEmail,
|
||||
Role: string(row.UserRole),
|
||||
FullName: row.UserFullName,
|
||||
PreferredName: shared.TextPointer(row.PreferredName),
|
||||
ProfileIconURL: shared.TextPointer(row.ProfileIconUrl),
|
||||
Headline: shared.TextPointer(row.Headline),
|
||||
JoinedAt: shared.TimePointer(row.JoinedAt),
|
||||
LastReadAt: shared.TimePointer(row.LastReadAt),
|
||||
ArchivedAt: shared.TimePointer(row.ArchivedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func mapThreadParticipantByThread(row sqlc.ListParticipantsForThreadForUserRow) threadParticipantResponse {
|
||||
return threadParticipantResponse{
|
||||
ID: row.UserID,
|
||||
Email: row.UserEmail,
|
||||
Role: string(row.UserRole),
|
||||
FullName: row.UserFullName,
|
||||
PreferredName: shared.TextPointer(row.PreferredName),
|
||||
ProfileIconURL: shared.TextPointer(row.ProfileIconUrl),
|
||||
Headline: shared.TextPointer(row.Headline),
|
||||
JoinedAt: shared.TimePointer(row.JoinedAt),
|
||||
LastReadAt: shared.TimePointer(row.LastReadAt),
|
||||
ArchivedAt: shared.TimePointer(row.ArchivedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func mapThreadSummary(row sqlc.ListMessageThreadsForUserRow, participants []threadParticipantResponse) messageThreadSummaryResponse {
|
||||
response := messageThreadSummaryResponse{
|
||||
ID: row.ThreadID,
|
||||
Subject: row.Subject,
|
||||
CreatedByUserID: row.CreatedByUserID,
|
||||
CreatedAt: shared.TimePointer(row.ThreadCreatedAt),
|
||||
UpdatedAt: shared.TimePointer(row.ThreadUpdatedAt),
|
||||
UnreadCount: row.UnreadCount,
|
||||
LastMessageID: row.LastMessageID,
|
||||
LastMessageBody: stringPointerOrNil(row.LastMessageBody),
|
||||
LastMessageCreatedAt: shared.TimePointer(row.LastMessageCreatedAt),
|
||||
Participants: participants,
|
||||
}
|
||||
|
||||
if row.LastMessageID > 0 {
|
||||
response.LastMessageSender = &messageSenderResponse{
|
||||
ID: row.LastMessageSenderUserID,
|
||||
Email: "",
|
||||
Role: "",
|
||||
FullName: valueOrEmpty(row.LastMessageSenderFullName),
|
||||
PreferredName: shared.TextPointer(row.LastMessageSenderPreferredName),
|
||||
ProfileIconURL: shared.TextPointer(row.LastMessageSenderProfileIconUrl),
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func mapThreadMessage(row sqlc.ListMessagesForThreadForUserRow, currentUserID int64) messageResponse {
|
||||
return messageResponse{
|
||||
ID: row.ID,
|
||||
ThreadID: row.ThreadID,
|
||||
Body: row.Body,
|
||||
CreatedAt: shared.TimePointer(row.CreatedAt),
|
||||
UpdatedAt: shared.TimePointer(row.UpdatedAt),
|
||||
Mine: row.SenderUserID == currentUserID,
|
||||
Sender: messageSenderResponse{
|
||||
ID: row.SenderUserID,
|
||||
Email: row.SenderEmail,
|
||||
Role: string(row.SenderRole),
|
||||
FullName: row.SenderFullName,
|
||||
PreferredName: shared.TextPointer(row.SenderPreferredName),
|
||||
ProfileIconURL: shared.TextPointer(row.SenderProfileIconUrl),
|
||||
Headline: shared.TextPointer(row.SenderHeadline),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeRecipientIDs(currentUserID int64, values []int64) []int64 {
|
||||
seen := make(map[int64]struct{}, len(values))
|
||||
normalized := make([]int64, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value <= 0 || value == currentUserID {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[value]; ok {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
normalized = append(normalized, value)
|
||||
}
|
||||
|
||||
sort.Slice(normalized, func(i, j int) bool { return normalized[i] < normalized[j] })
|
||||
return normalized
|
||||
}
|
||||
|
||||
func valueOrEmpty(value pgtype.Text) string {
|
||||
if !value.Valid {
|
||||
return ""
|
||||
}
|
||||
return value.String
|
||||
}
|
||||
|
||||
func stringPointerOrNil(value string) *string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil
|
||||
}
|
||||
copy := value
|
||||
return ©
|
||||
}
|
||||
Reference in New Issue
Block a user