Skip to content

Commit f53ba3a

Browse files
Merge pull request #90 from FlameInTheDark/feature/bot-api
fix: application commands and stability fixes
2 parents b5fc31b + 3d74cfb commit f53ba3a

15 files changed

Lines changed: 223 additions & 87 deletions

File tree

‎clients/bot/goclient/endpoints.go‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@ var (
2222
EndpointGuilds = EndpointAPIPath + "/guild"
2323
EndpointGuildChannels = func(guildID string) string { return EndpointGuilds + "/" + guildID + "/channels" }
2424

25-
EndpointChannelMessages = func(channelID string) string { return EndpointAPIPath + "/message/channel/" + channelID }
26-
EndpointChannelMessage = func(channelID, messageID string) string { return EndpointChannelMessages(channelID) + "/" + messageID }
27-
EndpointChannelMessageAck = func(channelID, messageID string) string { return EndpointChannelMessage(channelID, messageID) + "/ack" }
28-
EndpointChannelTyping = func(channelID string) string { return EndpointChannelMessages(channelID) + "/typing" }
29-
EndpointMessageReactions = func(channelID, messageID, reaction string) string {
25+
EndpointChannelMessages = func(channelID string) string { return EndpointAPIPath + "/message/channel/" + channelID }
26+
EndpointChannelMessage = func(channelID, messageID string) string { return EndpointChannelMessages(channelID) + "/" + messageID }
27+
EndpointChannelTyping = func(channelID string) string { return EndpointChannelMessages(channelID) + "/typing" }
28+
EndpointMessageReactions = func(channelID, messageID, reaction string) string {
3029
return EndpointChannelMessage(channelID, messageID) + "/reactions/" + url.PathEscape(reaction)
3130
}
3231
EndpointMessageReactionAll = func(channelID, messageID string) string {

‎clients/bot/goclient/restapi.go‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,6 @@ func (s *Session) ChannelMessageDelete(ctx context.Context, channelID, messageID
259259
return s.Request(ctx, http.MethodDelete, path, nil, nil, nil, options...)
260260
}
261261

262-
// ChannelMessageAck marks a channel read as the bot.
263-
func (s *Session) ChannelMessageAck(ctx context.Context, channelID, messageID int64, options ...RequestOption) error {
264-
path := EndpointChannelMessageAck(formatID(channelID), formatID(messageID))
265-
return s.Request(ctx, http.MethodPost, path, nil, nil, nil, options...)
266-
}
267-
268262
// ChannelTyping sends a typing indicator as the bot.
269263
func (s *Session) ChannelTyping(ctx context.Context, channelID int64, options ...RequestOption) error {
270264
return s.Request(ctx, http.MethodPost, EndpointChannelTyping(formatID(channelID)), nil, nil, nil, options...)

‎clients/bot/goclient/restapi_test.go‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ func TestBotRESTMethodsUseCurrentRuntimeRoutes(t *testing.T) {
3333
{method: http.MethodPost, path: "/bot/api/v1/message/channel/2230469276416868353", body: `{"content":"hello"}`, reply: `{"id":2230469276416868355,"channel_id":2230469276416868353,"author":{"id":1,"name":"bot","discriminator":"0001","is_bot":true},"content":"hello","type":0}`},
3434
{method: http.MethodPatch, path: "/bot/api/v1/message/channel/2230469276416868353/2230469276416868355", body: `{"content":"edited"}`, reply: `{"id":2230469276416868355,"channel_id":2230469276416868353,"author":{"id":1,"name":"bot","discriminator":"0001","is_bot":true},"content":"edited","type":0}`},
3535
{method: http.MethodDelete, path: "/bot/api/v1/message/channel/2230469276416868353/2230469276416868355", status: http.StatusNoContent},
36-
{method: http.MethodPost, path: "/bot/api/v1/message/channel/2230469276416868353/2230469276416868355/ack", status: http.StatusNoContent},
3736
{method: http.MethodPost, path: "/bot/api/v1/message/channel/2230469276416868353/typing", status: http.StatusNoContent},
3837
{method: http.MethodPut, path: "/bot/api/v1/message/channel/2230469276416868353/2230469276416868355/reactions/smile:123", reply: `{"count":1,"me":true,"emoji":{"id":123,"name":"smile"}}`},
3938
{method: http.MethodDelete, path: "/bot/api/v1/message/channel/2230469276416868353/2230469276416868355/reactions/smile:123", status: http.StatusNoContent},
@@ -102,9 +101,6 @@ func TestBotRESTMethodsUseCurrentRuntimeRoutes(t *testing.T) {
102101
if err := s.ChannelMessageDelete(ctx, 2230469276416868353, 2230469276416868355); err != nil {
103102
t.Fatal(err)
104103
}
105-
if err := s.ChannelMessageAck(ctx, 2230469276416868353, 2230469276416868355); err != nil {
106-
t.Fatal(err)
107-
}
108104
if err := s.ChannelTyping(ctx, 2230469276416868353); err != nil {
109105
t.Fatal(err)
110106
}

‎cmd/botapi/endpoints/applicationcommand/handlers.go‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
package applicationcommand
22

33
import (
4-
"time"
5-
64
appcmd "github.com/FlameInTheDark/gochat/internal/applicationcommands"
75
"github.com/FlameInTheDark/gochat/internal/botauth"
6+
"github.com/FlameInTheDark/gochat/internal/dto"
87
"github.com/FlameInTheDark/gochat/internal/idgen"
98
"github.com/gofiber/fiber/v2"
109
)
@@ -190,9 +189,6 @@ func (e *Entity) RespondInteraction(c *fiber.Ctx) error {
190189
if err := requireInteractionID(c, record); err != nil {
191190
return err
192191
}
193-
if time.Now().After(record.CreatedAt.Add(appcmd.InteractionDeadline)) {
194-
return fiber.NewError(fiber.StatusNotFound, "unknown interaction")
195-
}
196192
var response appcmd.InteractionResponse
197193
if err := c.BodyParser(&response); err != nil {
198194
return fiber.NewError(fiber.StatusBadRequest, "invalid interaction response")
@@ -202,32 +198,43 @@ func (e *Entity) RespondInteraction(c *fiber.Ctx) error {
202198
if err := e.appcmd.AckInteraction(c.UserContext(), record.ID, appcmd.AckStateResponded, nil); err != nil {
203199
return duplicateAckError()
204200
}
201+
e.notifyInteractionStatus(c.UserContext(), record, "responded", response.Data, nil)
205202
return c.SendStatus(fiber.StatusNoContent)
206203
case appcmd.ResponseTypeDeferredChannelMessageSource:
204+
var deferredData *appcmd.InteractionResponseData
207205
if response.Data != nil && appcmd.HasResponseFlag(response.Data, appcmd.MessageFlagEphemeral) {
208-
_ = e.setEphemeralResponse(c.UserContext(), record.ID, &appcmd.InteractionResponseData{Flags: response.Data.Flags | appcmd.MessageFlagLoading})
206+
deferredData = &appcmd.InteractionResponseData{Flags: response.Data.Flags | appcmd.MessageFlagLoading}
207+
_ = e.setEphemeralResponse(c.UserContext(), record.ID, deferredData)
208+
} else {
209+
deferredData = response.Data
209210
}
210211
if err := e.appcmd.AckInteraction(c.UserContext(), record.ID, appcmd.AckStateDeferred, nil); err != nil {
211212
return duplicateAckError()
212213
}
214+
e.notifyInteractionStatus(c.UserContext(), record, "deferred", deferredData, nil)
213215
return c.SendStatus(fiber.StatusNoContent)
214216
case appcmd.ResponseTypeChannelMessageWithSource:
215217
var messageID *int64
218+
var message *dto.Message
219+
state := "responded"
216220
if appcmd.HasResponseFlag(response.Data, appcmd.MessageFlagEphemeral) {
217221
if err := e.setEphemeralResponse(c.UserContext(), record.ID, response.Data); err != nil {
218222
return fiber.NewError(fiber.StatusInternalServerError, "unable to store ephemeral response")
219223
}
224+
state = "ephemeral"
220225
} else {
221226
msg, err := e.createPublicInteractionMessage(c, record, response.Data)
222227
if err != nil {
223228
return err
224229
}
225230
id := msg.Id
226231
messageID = &id
232+
message = &msg
227233
}
228234
if err := e.appcmd.AckInteraction(c.UserContext(), record.ID, appcmd.AckStateResponded, messageID); err != nil {
229235
return duplicateAckError()
230236
}
237+
e.notifyInteractionStatus(c.UserContext(), record, state, response.Data, message)
231238
return c.SendStatus(fiber.StatusNoContent)
232239
case appcmd.ResponseTypeAutocompleteResult:
233240
if response.Data == nil {
@@ -247,6 +254,7 @@ func (e *Entity) RespondInteraction(c *fiber.Ctx) error {
247254
if err := e.appcmd.AckInteraction(c.UserContext(), record.ID, appcmd.AckStateResponded, nil); err != nil {
248255
return duplicateAckError()
249256
}
257+
e.notifyInteractionStatus(c.UserContext(), record, "modal", response.Data, nil)
250258
return c.JSON(response)
251259
default:
252260
return fiber.NewError(fiber.StatusBadRequest, "unsupported interaction response type")
@@ -294,6 +302,7 @@ func (e *Entity) EditOriginalInteractionResponse(c *fiber.Ctx) error {
294302
if err := e.setEphemeralResponse(c.UserContext(), record.ID, &data); err != nil {
295303
return fiber.NewError(fiber.StatusInternalServerError, "unable to store ephemeral response")
296304
}
305+
e.notifyInteractionStatus(c.UserContext(), record, "ephemeral", &data, nil)
297306
return c.JSON(data)
298307
}
299308
if record.InitialResponseID == nil {
@@ -303,6 +312,7 @@ func (e *Entity) EditOriginalInteractionResponse(c *fiber.Ctx) error {
303312
if err := e.setEphemeralResponse(c.UserContext(), record.ID, &data); err != nil {
304313
return fiber.NewError(fiber.StatusInternalServerError, "unable to store ephemeral response")
305314
}
315+
e.notifyInteractionStatus(c.UserContext(), record, "ephemeral", &data, nil)
306316
return c.JSON(data)
307317
}
308318
out, err := e.createPublicInteractionMessage(c, record, &data)
@@ -313,12 +323,14 @@ func (e *Entity) EditOriginalInteractionResponse(c *fiber.Ctx) error {
313323
_ = e.deleteMessageResponse(c, record, out.Id)
314324
return duplicateAckError()
315325
}
326+
e.notifyInteractionStatus(c.UserContext(), record, "responded", &data, &out)
316327
return c.JSON(out)
317328
}
318329
out, err := e.editMessageResponse(c, record, *record.InitialResponseID, &data)
319330
if err != nil {
320331
return err
321332
}
333+
e.notifyInteractionStatus(c.UserContext(), record, "responded", &data, &out)
322334
return c.JSON(out)
323335
}
324336

@@ -357,12 +369,14 @@ func (e *Entity) CreateFollowupMessage(c *fiber.Ctx) error {
357369
if err := e.setEphemeralResponse(c.UserContext(), idgen.Next(), &data); err != nil {
358370
return fiber.NewError(fiber.StatusInternalServerError, "unable to store ephemeral followup")
359371
}
372+
e.notifyInteractionStatus(c.UserContext(), record, "ephemeral_followup", &data, nil)
360373
return c.Status(fiber.StatusCreated).JSON(data)
361374
}
362375
out, err := e.createPublicInteractionMessage(c, record, &data)
363376
if err != nil {
364377
return err
365378
}
379+
e.notifyInteractionStatus(c.UserContext(), record, "responded", &data, &out)
366380
return c.Status(fiber.StatusCreated).JSON(out)
367381
}
368382

‎cmd/botapi/endpoints/applicationcommand/helpers.go‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,27 @@ func (e *Entity) deleteEphemeralResponse(ctx context.Context, interactionID int6
239239
}
240240
}
241241

242+
func (e *Entity) notifyInteractionStatus(ctx context.Context, record appcmd.InteractionRecord, state string, data *appcmd.InteractionResponseData, message *dto.Message) {
243+
commandName := ""
244+
if payload, err := e.payload.GetInteractionPayload(ctx, record.ID); err == nil {
245+
if parsed := appcmd.ParseCommandData(payload.DataJSON); parsed != nil {
246+
commandName = parsed.Name
247+
}
248+
}
249+
_ = mq.SendUserUpdate(ctx, e.mqt, record.InvokerUserID, &mqmsg.ApplicationCommandInteractionStatus{
250+
InteractionID: record.ID,
251+
ApplicationID: record.ApplicationID,
252+
CommandID: record.CommandID,
253+
CommandName: commandName,
254+
ChannelID: record.ChannelID,
255+
GuildID: record.GuildID,
256+
UserID: record.InvokerUserID,
257+
State: state,
258+
Response: data,
259+
Message: message,
260+
})
261+
}
262+
242263
func (e *Entity) setAutocompleteResponse(ctx context.Context, interactionID int64, choices []appcmd.ApplicationCommandChoice) error {
243264
if e.cache == nil {
244265
return nil

‎cmd/botapi/endpoints/message/entity.go‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ func (e *Entity) Init(router fiber.Router) {
6363
router.Get("/channel/:channel_id<int>", e.List)
6464
router.Patch("/channel/:channel_id<int>/:message_id<int>", e.Update)
6565
router.Delete("/channel/:channel_id<int>/:message_id<int>", e.Delete)
66-
router.Post("/channel/:channel_id<int>/:message_id<int>/ack", e.Ack)
6766
router.Post("/channel/:channel_id<int>/typing", e.Typing)
6867
router.Put("/channel/:channel_id<int>/:message_id<int>/reactions/:reaction_name", e.AddReaction)
6968
router.Delete("/channel/:channel_id<int>/:message_id<int>/reactions/:reaction_name", e.RemoveReaction)

‎cmd/botapi/endpoints/message/handlers.go‎

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -276,36 +276,6 @@ func (e *Entity) Delete(c *fiber.Ctx) error {
276276
return c.SendStatus(fiber.StatusNoContent)
277277
}
278278

279-
// Ack
280-
//
281-
// @Summary Mark a channel read as the bot
282-
// @Produce json
283-
// @Tags Bot Message
284-
// @Security BotToken
285-
// @Param channel_id path int64 true "Channel id"
286-
// @Param message_id path int64 true "Message id"
287-
// @Success 204
288-
// @Failure 400 {string} string "Bad request"
289-
// @Failure 401 {string} string "Unauthorized"
290-
// @Failure 403 {string} string "Forbidden"
291-
// @Failure 500 {string} string "Internal server error"
292-
// @Router /bot/api/v1/message/channel/{channel_id}/{message_id}/ack [post]
293-
func (e *Entity) Ack(c *fiber.Ctx) error {
294-
principal, channel, _, err := e.requireChannel(c, permissions.PermServerViewChannels, permissions.PermTextReadMessageHistory)
295-
if err != nil {
296-
return err
297-
}
298-
msgID, err := parseParamInt64(c, "message_id")
299-
if err != nil {
300-
return err
301-
}
302-
if err := e.rs.SetReadState(c.UserContext(), principal.BotUserID, channel.Id, msgID); err != nil {
303-
return fiber.NewError(fiber.StatusInternalServerError, "unable to update read state")
304-
}
305-
_ = mq.SendUserUpdate(c.UserContext(), e.mqt, principal.BotUserID, &mqmsg.UpdateReadState{ChannelId: channel.Id, MessageId: msgID})
306-
return c.SendStatus(fiber.StatusNoContent)
307-
}
308-
309279
// Typing
310280
//
311281
// @Summary Send a typing indicator as the bot

‎cmd/ws/app.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ func (a *App) presenceTTLSeconds() int64 {
181181
if a == nil || a.cfg == nil {
182182
return 60
183183
}
184-
ttl := a.cfg.HearthBeatTimeout * 2 / 1000
184+
ttl := int64((wsHeartbeatDeadline(a.cfg.HearthBeatTimeout) * 2) / time.Second)
185185
if ttl < 1 {
186186
return 1
187187
}

‎cmd/ws/handler.go‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,7 @@ func (a *App) wsHandler(c *websocket.Conn) {
276276
pingInterval = half
277277
}
278278
}
279-
readDeadline := time.Duration(a.cfg.HearthBeatTimeout+15000) * time.Millisecond
280-
if readDeadline <= 0 {
281-
readDeadline = 60 * time.Second
282-
}
279+
readDeadline := wsReadDeadline(a.cfg.HearthBeatTimeout)
283280
_ = c.SetReadDeadline(time.Now().Add(readDeadline))
284281
c.SetPongHandler(func(string) error {
285282
return c.SetReadDeadline(time.Now().Add(readDeadline))
@@ -317,6 +314,7 @@ func (a *App) wsHandler(c *websocket.Conn) {
317314
connLog.Error("Read WS message error", "error", err)
318315
return
319316
}
317+
_ = c.SetReadDeadline(time.Now().Add(readDeadline))
320318

321319
switch mt {
322320
case websocket.TextMessage:
@@ -356,3 +354,22 @@ func isExpectedWSReadError(err error) bool {
356354

357355
return errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, net.ErrClosed)
358356
}
357+
358+
func wsReadDeadline(heartbeatTimeoutMs int64) time.Duration {
359+
return wsHeartbeatDeadline(heartbeatTimeoutMs) + 15*time.Second
360+
}
361+
362+
func wsHeartbeatDeadline(heartbeatTimeoutMs int64) time.Duration {
363+
base := time.Duration(heartbeatTimeoutMs) * time.Millisecond
364+
if base <= 0 {
365+
return 60 * time.Second
366+
}
367+
deadline := base + 10*time.Second
368+
if minimum := base * 3; deadline < minimum {
369+
deadline = minimum
370+
}
371+
if deadline < 60*time.Second {
372+
return 60 * time.Second
373+
}
374+
return deadline
375+
}

0 commit comments

Comments
 (0)