Files
OwnCord/Server/ws/handlers_chat.go
T
J3vb db0275a290 fix: batch of 29 correctness fixes across server and client (#1369)
* fix(ws): 2 defect(s) (OC-0013, OC-0140)

* fix(voice): 1 defect(s) (OC-0044)

* fix(ws): 1 defect(s) (OC-0024)

* fix(server): 1 defect(s) (OC-0027)

* fix(ws): 1 defect(s) (OC-0028)

* fix(server): 7 defect(s) (OC-0033, OC-0066, OC-0067, OC-0068, OC-0074, OC-0077, OC-0106)

* fix(ws): 1 defect(s) (OC-0051)

* fix(client): 1 defect(s) (OC-0053)

* fix(client): 1 defect(s) (OC-0055)

* fix(service): 1 defect(s) (OC-0069)

* fix(voice): 1 defect(s) (OC-0072)

* fix(service): 1 defect(s) (OC-0082)

* fix(client): 1 defect(s) (OC-0083)

* fix(plugin): 1 defect(s) (OC-0088)

* fix(plugin): 4 defect(s) (OC-0104, OC-0126, OC-0127, OC-0133)

* fix(admin): 1 defect(s) (OC-0110)

* fix(client): 1 defect(s) (OC-0114)

* fix(api): 1 defect(s) (OC-0139)

* fix(client): 1 defect(s) (OC-0149)

* test(server): adapt existing tests to updated OpenDM and IncrementMentionCounts signatures

* style(plugin): modernize loops and goroutine spawns in race test

* fix(ws): mirror the focus admission gate in the post-subscribe revalidation

* fix(service): detach DM post-commit side effects from the request ctx, fail delete closed, add empty-fan-out fallback

* fix(plugin): preserve enabled intent when upgrade reactivation hits a runtime-less build

* chore(skills): harden bughunt-fix workflow and fold review lessons into bughunt-run/db-change

* Add comprehensive documentation for task-observer skill

- Introduced environments.md to outline activation setup, compaction behavior, and handoff-doc mode.
- Created skill-authoring.md detailing taxonomy, licensing, confidentiality, and editing rules for skill creation.
- Added weekly-review.md for a structured review process of OPEN observations, including scheduled and in-session fallback modes.

* chore(go): pin toolchain go1.26.6 (stdlib CVE fixes flagged by govulncheck)
2026-08-14 10:05:40 +02:00

188 lines
6.6 KiB
Go

package ws
import (
"context"
"errors"
"github.com/owncord/server/db"
"github.com/owncord/server/service"
)
// registerChatHandlers registers all chat-related V2 message handlers.
func registerChatHandlers(r *HandlerRegistry, deps ChatDeps) {
r.RegisterV2(MsgTypeChatSend, handleChatSendV2, deps)
r.RegisterV2(MsgTypeChatEdit, handleChatEditV2, deps)
r.RegisterV2(MsgTypeChatDelete, handleChatDeleteV2, deps)
}
// handleChatSendV2 processes a chat_send command via the MessageService.
func handleChatSendV2(ctx context.Context, cmd Command, info ClientInfo, deps any) Result {
d := deps.(ChatDeps)
sendCmd := cmd.(ChatSendCmd)
result, err := d.MessageSvc.SendMessage(ctx, service.SendMessageParams{
ChannelID: sendCmd.ChannelID(),
UserID: info.UserID,
Username: info.Username,
Avatar: info.Avatar,
RoleName: info.RoleName,
Content: sendCmd.Content(),
ReplyTo: sendCmd.ReplyTo(),
AttachmentIDs: sendCmd.Attachments(),
})
if err != nil {
return serviceErrorToResult(err)
}
// Build attachment data for broadcast.
var attData []map[string]any
for _, ai := range result.Attachments {
attData = append(attData, map[string]any{
"id": ai.ID,
"filename": ai.Filename,
"size": ai.Size,
"mime": ai.Mime,
"url": ai.URL,
})
}
reply := buildChatSendOK(info.ReqID, result.MessageID, result.Timestamp)
broadcast := buildChatMessage(chatMessageArgs{
MsgID: result.MessageID,
ChannelID: sendCmd.ChannelID(),
UserID: info.UserID,
Username: info.Username,
Avatar: info.Avatar,
DisplayName: info.DisplayName,
RoleName: info.RoleName,
Content: result.Content,
Timestamp: result.Timestamp,
ReplyTo: sendCmd.ReplyTo(),
Attachments: attData,
Mentions: result.Mentions,
MentionsEveryone: result.MentionsEveryone,
})
if !result.IsDM {
return Result{
Reply: reply,
Events: []Event{MessageSentChannelEvent{channelID: sendCmd.ChannelID(), payload: broadcast}},
}
}
// DM path: build dm_channel_open events + sequenced message.
var events []Event
if len(result.OpenedDMFor) > 0 {
// One payload per recipient, not one for all of them: `recipient` and
// `recipients` are defined relative to who is reading, so a shared
// payload would list a group member as their own DM partner.
chName := ""
if result.Channel != nil {
chName = result.Channel.Name
}
for _, pid := range result.OpenedDMFor {
var openPayload []byte
if len(result.DMParticipants) > 0 {
openPayload = buildDMChannelOpen(
db.NewDMChannelInfo(sendCmd.ChannelID(), chName, result.DMIsGroup, result.DMParticipants, pid))
} else {
openPayload = buildDMChannelOpenFor(sendCmd.ChannelID(), result.SenderUser, pid)
}
if openPayload == nil {
continue
}
events = append(events, DMChannelOpenEvent{
targetUserID: pid,
payload: openPayload,
})
}
}
events = append(events, dmEventOrFallback(
MessageSentDMEvent{channelID: sendCmd.ChannelID(), participantIDs: result.ParticipantIDs, payload: broadcast},
MessageSentChannelEvent{channelID: sendCmd.ChannelID(), payload: broadcast},
result.ParticipantIDs))
return Result{Reply: reply, Events: events}
}
// handleChatEditV2 processes a chat_edit command via the MessageService.
func handleChatEditV2(ctx context.Context, cmd Command, info ClientInfo, deps any) Result {
d := deps.(ChatDeps)
editCmd := cmd.(ChatEditCmd)
result, err := d.MessageSvc.EditMessage(ctx, info.UserID, editCmd.MessageID(), editCmd.Content())
if err != nil {
return serviceErrorToResult(err)
}
editedPayload := buildChatEdited(result.MessageID, result.ChannelID, result.Content, result.EditedAt,
result.Mentions, result.MentionsEveryone)
if result.IsDM {
return Result{Events: []Event{dmEventOrFallback(
MessageEditedDMEvent{channelID: result.ChannelID, participantIDs: result.ParticipantIDs, payload: editedPayload},
MessageEditedChannelEvent{channelID: result.ChannelID, payload: editedPayload},
result.ParticipantIDs)}}
}
return Result{Events: []Event{MessageEditedChannelEvent{
channelID: result.ChannelID,
payload: editedPayload,
}}}
}
// handleChatDeleteV2 processes a chat_delete command via the MessageService.
func handleChatDeleteV2(ctx context.Context, cmd Command, info ClientInfo, deps any) Result {
d := deps.(ChatDeps)
deleteCmd := cmd.(ChatDeleteCmd)
result, err := d.MessageSvc.DeleteMessage(ctx, info.UserID, deleteCmd.MessageID())
if err != nil {
return serviceErrorToResult(err)
}
deletedPayload := buildChatDeleted(result.MessageID, result.ChannelID)
if result.IsDM {
return Result{Events: []Event{dmEventOrFallback(
MessageDeletedDMEvent{channelID: result.ChannelID, participantIDs: result.ParticipantIDs, payload: deletedPayload},
MessageDeletedChannelEvent{channelID: result.ChannelID, payload: deletedPayload},
result.ParticipantIDs)}}
}
return Result{Events: []Event{MessageDeletedChannelEvent{
channelID: result.ChannelID,
payload: deletedPayload,
}}}
}
// dmEventOrFallback returns the participant-targeted DM event, falling back
// to the channel-topic broadcast when the participant list is empty — the
// degraded shape a failed post-commit GetDMParticipantIDs leaves behind. A
// sequenced frame addressed to nobody would consume a seq and reach no one;
// the topic fallback still reaches whoever has the DM focused.
func dmEventOrFallback(dmEvent, fallback Event, participantIDs []int64) Event {
if len(participantIDs) == 0 {
return fallback
}
return dmEvent
}
// serviceErrorToResult converts a service-layer error to a WS Result.
func serviceErrorToResult(err error) Result {
switch {
case errors.Is(err, service.ErrRateLimited):
return Result{Error: ClientError{Code: ErrCodeRateLimited, Message: err.Error()}}
case errors.Is(err, service.ErrSlowMode):
return Result{Error: ClientError{Code: ErrCodeSlowMode, Message: err.Error()}}
case errors.Is(err, service.ErrBadRequest):
return Result{Error: ClientError{Code: ErrCodeBadRequest, Message: err.Error()}}
case errors.Is(err, service.ErrNotFound):
return Result{Error: ClientError{Code: ErrCodeNotFound, Message: err.Error()}}
case errors.Is(err, service.ErrForbidden), errors.Is(err, service.ErrBlocked),
errors.Is(err, service.ErrDeletedMessage):
return Result{Error: ClientError{Code: ErrCodeForbidden, Message: err.Error()}}
case errors.Is(err, service.ErrConflict):
return Result{Error: ClientError{Code: ErrCodeConflict, Message: err.Error()}}
default:
return Result{Error: ClientError{Code: ErrCodeInternal, Message: err.Error()}}
}
}