|
|
@@ -820,124 +820,39 @@ func (r *mutationResolver) DeleteTaskStatus(ctx context.Context, id string) (boo
|
|
|
return result.RowsAffected > 0, nil
|
|
|
}
|
|
|
|
|
|
-// CreateChannel is the resolver for the createChannel field.
|
|
|
-func (r *mutationResolver) CreateChannel(ctx context.Context, input model.NewChannel) (*model.Channel, error) {
|
|
|
- // Auth check
|
|
|
- if !auth.IsAuthenticated(ctx) {
|
|
|
- return nil, errors.New("unauthorized: authentication required")
|
|
|
- }
|
|
|
-
|
|
|
- channel := models.Channel{}
|
|
|
-
|
|
|
- for _, participantIDStr := range input.Participants {
|
|
|
- participantID, err := toID(participantIDStr)
|
|
|
- if err != nil {
|
|
|
- return nil, fmt.Errorf("invalid participant ID: %w", err)
|
|
|
- }
|
|
|
- var user models.User
|
|
|
- if err := r.DB.First(&user, participantID).Error; err != nil {
|
|
|
- return nil, fmt.Errorf("participant not found: %w", err)
|
|
|
- }
|
|
|
- channel.Participants = append(channel.Participants, user)
|
|
|
- }
|
|
|
-
|
|
|
- if err := r.DB.Create(&channel).Error; err != nil {
|
|
|
- return nil, fmt.Errorf("failed to create channel: %w", err)
|
|
|
- }
|
|
|
-
|
|
|
- // Reload with participants
|
|
|
- r.DB.Preload("Participants").First(&channel, channel.ID)
|
|
|
-
|
|
|
- logging.LogMutation(ctx, "CREATE", "CHANNEL", fmt.Sprintf("id=%d", channel.ID))
|
|
|
- return convertChannel(channel), nil
|
|
|
-}
|
|
|
-
|
|
|
-// UpdateChannel is the resolver for the updateChannel field.
|
|
|
-func (r *mutationResolver) UpdateChannel(ctx context.Context, id string, input model.UpdateChannelInput) (*model.Channel, error) {
|
|
|
+// CreateMessage is the resolver for the createMessage field.
|
|
|
+func (r *mutationResolver) CreateMessage(ctx context.Context, input model.NewMessage) (*model.Message, error) {
|
|
|
// Auth check
|
|
|
if !auth.IsAuthenticated(ctx) {
|
|
|
return nil, errors.New("unauthorized: authentication required")
|
|
|
}
|
|
|
- if !auth.HasPermission(ctx, "channel:update") {
|
|
|
- return nil, errors.New("unauthorized: missing channel:update permission")
|
|
|
- }
|
|
|
|
|
|
- channelID, err := toID(id)
|
|
|
+ // Get sender from authenticated user
|
|
|
+ currentUser, err := auth.CurrentUser(ctx)
|
|
|
if err != nil {
|
|
|
- return nil, fmt.Errorf("invalid channel ID: %w", err)
|
|
|
- }
|
|
|
-
|
|
|
- var existing models.Channel
|
|
|
- if err := r.DB.Preload("Participants").First(&existing, channelID).Error; err != nil {
|
|
|
- return nil, fmt.Errorf("channel not found: %w", err)
|
|
|
+ return nil, fmt.Errorf("failed to get current user: %w", err)
|
|
|
}
|
|
|
|
|
|
- participants := []models.User{}
|
|
|
- for _, participantIDStr := range input.Participants {
|
|
|
- participantID, err := toID(participantIDStr)
|
|
|
+ // Build receivers list
|
|
|
+ receivers := make([]models.User, 0, len(input.Receivers))
|
|
|
+ receiverIDs := make([]uint, 0, len(input.Receivers))
|
|
|
+ for _, receiverIDStr := range input.Receivers {
|
|
|
+ receiverID, err := toID(receiverIDStr)
|
|
|
if err != nil {
|
|
|
- return nil, fmt.Errorf("invalid participant ID: %w", err)
|
|
|
+ return nil, fmt.Errorf("invalid receiver ID: %w", err)
|
|
|
}
|
|
|
var user models.User
|
|
|
- if err := r.DB.First(&user, participantID).Error; err != nil {
|
|
|
- return nil, fmt.Errorf("participant not found: %w", err)
|
|
|
+ if err := r.DB.First(&user, receiverID).Error; err != nil {
|
|
|
+ return nil, fmt.Errorf("receiver not found: %w", err)
|
|
|
}
|
|
|
- participants = append(participants, user)
|
|
|
- }
|
|
|
- existing.Participants = participants
|
|
|
-
|
|
|
- if err := r.DB.Save(&existing).Error; err != nil {
|
|
|
- return nil, fmt.Errorf("failed to update channel: %w", err)
|
|
|
- }
|
|
|
-
|
|
|
- logging.LogMutation(ctx, "UPDATE", "CHANNEL", id)
|
|
|
- return convertChannel(existing), nil
|
|
|
-}
|
|
|
-
|
|
|
-// DeleteChannel is the resolver for the deleteChannel field.
|
|
|
-func (r *mutationResolver) DeleteChannel(ctx context.Context, id string) (bool, error) {
|
|
|
- // Auth check
|
|
|
- if !auth.IsAuthenticated(ctx) {
|
|
|
- return false, errors.New("unauthorized: authentication required")
|
|
|
- }
|
|
|
- if !auth.HasPermission(ctx, "channel:delete") {
|
|
|
- return false, errors.New("unauthorized: missing channel:delete permission")
|
|
|
- }
|
|
|
-
|
|
|
- channelID, err := toID(id)
|
|
|
- if err != nil {
|
|
|
- return false, fmt.Errorf("invalid channel ID: %w", err)
|
|
|
- }
|
|
|
-
|
|
|
- result := r.DB.Delete(&models.Channel{}, channelID)
|
|
|
- if result.Error != nil {
|
|
|
- return false, fmt.Errorf("failed to delete channel: %w", result.Error)
|
|
|
- }
|
|
|
-
|
|
|
- logging.LogMutation(ctx, "DELETE", "CHANNEL", id)
|
|
|
- return result.RowsAffected > 0, nil
|
|
|
-}
|
|
|
-
|
|
|
-// CreateMessage is the resolver for the createMessage field.
|
|
|
-func (r *mutationResolver) CreateMessage(ctx context.Context, input model.NewMessage) (*model.Message, error) {
|
|
|
- // Auth check
|
|
|
- if !auth.IsAuthenticated(ctx) {
|
|
|
- return nil, errors.New("unauthorized: authentication required")
|
|
|
- }
|
|
|
-
|
|
|
- conversationID, err := toID(input.ConversationID)
|
|
|
- if err != nil {
|
|
|
- return nil, fmt.Errorf("invalid conversation ID: %w", err)
|
|
|
- }
|
|
|
- senderID, err := toID(input.SenderID)
|
|
|
- if err != nil {
|
|
|
- return nil, fmt.Errorf("invalid sender ID: %w", err)
|
|
|
+ receivers = append(receivers, user)
|
|
|
+ receiverIDs = append(receiverIDs, receiverID)
|
|
|
}
|
|
|
|
|
|
message := models.Message{
|
|
|
- ConversationID: conversationID,
|
|
|
- SenderID: senderID,
|
|
|
- Content: input.Content,
|
|
|
+ SenderID: currentUser.ID,
|
|
|
+ Content: input.Content,
|
|
|
+ Receivers: receivers,
|
|
|
}
|
|
|
|
|
|
if err := r.DB.Create(&message).Error; err != nil {
|
|
|
@@ -945,26 +860,21 @@ func (r *mutationResolver) CreateMessage(ctx context.Context, input model.NewMes
|
|
|
}
|
|
|
|
|
|
// Reload with associations
|
|
|
- r.DB.Preload("Sender").First(&message, message.ID)
|
|
|
-
|
|
|
- // Get channel participants for publishing the message event
|
|
|
- var channel models.Channel
|
|
|
- if err := r.DB.Preload("Participants").First(&channel, conversationID).Error; err == nil {
|
|
|
- // Build list of participant IDs (excluding the sender to prevent notification loops)
|
|
|
- participantIDs := make([]uint, 0, len(channel.Participants))
|
|
|
- for _, participant := range channel.Participants {
|
|
|
- if participant.ID != senderID {
|
|
|
- participantIDs = append(participantIDs, participant.ID)
|
|
|
- }
|
|
|
- }
|
|
|
+ r.DB.Preload("Sender").Preload("Receivers").First(&message, message.ID)
|
|
|
|
|
|
- // Publish message event to channel participants
|
|
|
- graphqlMessage := convertMessage(message)
|
|
|
- r.PublishMessageEvent(graphqlMessage, conversationID, participantIDs)
|
|
|
+ // Publish message event to receivers (excluding the sender to prevent notification loops)
|
|
|
+ notifyReceiverIDs := make([]uint, 0, len(receiverIDs))
|
|
|
+ for _, receiverID := range receiverIDs {
|
|
|
+ if receiverID != currentUser.ID {
|
|
|
+ notifyReceiverIDs = append(notifyReceiverIDs, receiverID)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+ graphqlMessage := convertMessage(message)
|
|
|
+ r.PublishMessageEvent(graphqlMessage, notifyReceiverIDs)
|
|
|
+
|
|
|
logging.LogMutation(ctx, "CREATE", "MESSAGE", fmt.Sprintf("id=%d", message.ID))
|
|
|
- return convertMessage(message), nil
|
|
|
+ return graphqlMessage, nil
|
|
|
}
|
|
|
|
|
|
// UpdateMessage is the resolver for the updateMessage field.
|
|
|
@@ -983,32 +893,36 @@ func (r *mutationResolver) UpdateMessage(ctx context.Context, id string, input m
|
|
|
}
|
|
|
|
|
|
var existing models.Message
|
|
|
- if err := r.DB.Preload("Sender").First(&existing, messageID).Error; err != nil {
|
|
|
+ if err := r.DB.Preload("Sender").Preload("Receivers").First(&existing, messageID).Error; err != nil {
|
|
|
return nil, fmt.Errorf("message not found: %w", err)
|
|
|
}
|
|
|
|
|
|
- if input.ConversationID != nil {
|
|
|
- conversationID, err := toID(*input.ConversationID)
|
|
|
- if err != nil {
|
|
|
- return nil, fmt.Errorf("invalid conversation ID: %w", err)
|
|
|
- }
|
|
|
- existing.ConversationID = conversationID
|
|
|
- }
|
|
|
- if input.SenderID != nil {
|
|
|
- senderID, err := toID(*input.SenderID)
|
|
|
- if err != nil {
|
|
|
- return nil, fmt.Errorf("invalid sender ID: %w", err)
|
|
|
- }
|
|
|
- existing.SenderID = senderID
|
|
|
- }
|
|
|
if input.Content != nil {
|
|
|
existing.Content = *input.Content
|
|
|
}
|
|
|
+ if len(input.Receivers) > 0 {
|
|
|
+ receivers := make([]models.User, 0, len(input.Receivers))
|
|
|
+ for _, receiverIDStr := range input.Receivers {
|
|
|
+ receiverID, err := toID(receiverIDStr)
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("invalid receiver ID: %w", err)
|
|
|
+ }
|
|
|
+ var user models.User
|
|
|
+ if err := r.DB.First(&user, receiverID).Error; err != nil {
|
|
|
+ return nil, fmt.Errorf("receiver not found: %w", err)
|
|
|
+ }
|
|
|
+ receivers = append(receivers, user)
|
|
|
+ }
|
|
|
+ existing.Receivers = receivers
|
|
|
+ }
|
|
|
|
|
|
if err := r.DB.Save(&existing).Error; err != nil {
|
|
|
return nil, fmt.Errorf("failed to update message: %w", err)
|
|
|
}
|
|
|
|
|
|
+ // Reload with associations
|
|
|
+ r.DB.Preload("Sender").Preload("Receivers").First(&existing, existing.ID)
|
|
|
+
|
|
|
logging.LogMutation(ctx, "UPDATE", "MESSAGE", id)
|
|
|
return convertMessage(existing), nil
|
|
|
}
|
|
|
@@ -1289,42 +1203,6 @@ func (r *queryResolver) TaskStatus(ctx context.Context, id string) (*model.TaskS
|
|
|
return convertTaskStatus(status), nil
|
|
|
}
|
|
|
|
|
|
-// Channels is the resolver for the channels field.
|
|
|
-func (r *queryResolver) Channels(ctx context.Context) ([]*model.Channel, error) {
|
|
|
- // Auth check
|
|
|
- if !auth.IsAuthenticated(ctx) {
|
|
|
- return nil, errors.New("unauthorized: authentication required")
|
|
|
- }
|
|
|
-
|
|
|
- var channels []models.Channel
|
|
|
- if err := r.DB.Preload("Participants").Find(&channels).Error; err != nil {
|
|
|
- return nil, fmt.Errorf("failed to fetch channels: %w", err)
|
|
|
- }
|
|
|
- logging.LogQuery(ctx, "CHANNELS", "all")
|
|
|
- return convertChannels(channels), nil
|
|
|
-}
|
|
|
-
|
|
|
-// Channel is the resolver for the channel field.
|
|
|
-func (r *queryResolver) Channel(ctx context.Context, id string) (*model.Channel, error) {
|
|
|
- // Auth check
|
|
|
- if !auth.IsAuthenticated(ctx) {
|
|
|
- return nil, errors.New("unauthorized: authentication required")
|
|
|
- }
|
|
|
-
|
|
|
- channelID, err := toID(id)
|
|
|
- if err != nil {
|
|
|
- return nil, fmt.Errorf("invalid channel ID: %w", err)
|
|
|
- }
|
|
|
-
|
|
|
- var channel models.Channel
|
|
|
- if err := r.DB.Preload("Participants").First(&channel, channelID).Error; err != nil {
|
|
|
- return nil, fmt.Errorf("channel not found: %w", err)
|
|
|
- }
|
|
|
-
|
|
|
- logging.LogQuery(ctx, "CHANNEL", id)
|
|
|
- return convertChannel(channel), nil
|
|
|
-}
|
|
|
-
|
|
|
// Messages is the resolver for the messages field.
|
|
|
func (r *queryResolver) Messages(ctx context.Context) ([]*model.Message, error) {
|
|
|
// Auth check
|
|
|
@@ -1488,7 +1366,7 @@ func (r *subscriptionResolver) TaskDeleted(ctx context.Context) (<-chan *model.T
|
|
|
}
|
|
|
|
|
|
// MessageAdded is the resolver for the messageAdded field.
|
|
|
-// Users only receive events for messages in channels where they are participants.
|
|
|
+// Users only receive events for messages where they are in the receivers list.
|
|
|
func (r *subscriptionResolver) MessageAdded(ctx context.Context) (<-chan *model.Message, error) {
|
|
|
// Get current user
|
|
|
user, err := auth.CurrentUser(ctx)
|
|
|
@@ -1514,15 +1392,15 @@ func (r *subscriptionResolver) MessageAdded(ctx context.Context) (<-chan *model.
|
|
|
if !ok {
|
|
|
return
|
|
|
}
|
|
|
- // Check if user is in the participant list
|
|
|
- isParticipant := false
|
|
|
- for _, participantID := range event.ParticipantIDs {
|
|
|
- if participantID == user.ID {
|
|
|
- isParticipant = true
|
|
|
+ // Check if user is in the receiver list
|
|
|
+ isReceiver := false
|
|
|
+ for _, receiverID := range event.ReceiverIDs {
|
|
|
+ if receiverID == user.ID {
|
|
|
+ isReceiver = true
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
- if isParticipant && event.Message != nil {
|
|
|
+ if isReceiver && event.Message != nil {
|
|
|
select {
|
|
|
case outputChan <- event.Message:
|
|
|
default:
|