From 20ab5b1bc67a6d80c0cb0144cff6a6335a0566cc Mon Sep 17 00:00:00 2001 From: John-Mason Shackelford Date: Wed, 8 Apr 2026 18:53:03 -0400 Subject: [PATCH] feat: Add bot API endpoint for adding reactions (boosts) to messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new endpoint that allows bots to add emoji reactions (boosts) to messages: POST /rooms/:room_id/:bot_key/messages/:message_id/boosts This enables bots to acknowledge messages with reactions like 👀 (eyes) when mentioned, providing immediate feedback to users before generating a full response. The endpoint: - Validates the bot is a member of the room - Validates the message exists in the room - Broadcasts the boost to connected clients via Turbo Streams - Returns 201 Created on success, 404 if room/message not found Co-authored-by: openhands --- .../messages/boosts/by_bots_controller.rb | 33 ++++++++ config/routes.rb | 1 + .../boosts/by_bots_controller_test.rb | 76 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 app/controllers/messages/boosts/by_bots_controller.rb create mode 100644 test/controllers/messages/boosts/by_bots_controller_test.rb diff --git a/app/controllers/messages/boosts/by_bots_controller.rb b/app/controllers/messages/boosts/by_bots_controller.rb new file mode 100644 index 0000000..85f93ae --- /dev/null +++ b/app/controllers/messages/boosts/by_bots_controller.rb @@ -0,0 +1,33 @@ +class Messages::Boosts::ByBotsController < ApplicationController + allow_bot_access only: :create + + def create + set_message + @boost = @message.boosts.create!(content: read_body) + + broadcast_create + head :created + rescue ActiveRecord::RecordNotFound + head :not_found + end + + private + def set_message + @room = Current.user.rooms.find(params[:room_id]) + @message = @room.messages.find(params[:message_id]) + end + + def read_body + request.body.rewind + request.body.read.force_encoding("UTF-8") + ensure + request.body.rewind + end + + def broadcast_create + @boost.broadcast_append_to @boost.message.room, :messages, + target: "boosts_message_#{@boost.message.client_message_id}", + partial: "messages/boosts/boost", + attributes: { maintain_scroll: true } + end +end diff --git a/config/routes.rb b/config/routes.rb index 2cc6676..26afbcb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -65,6 +65,7 @@ Rails.application.routes.draw do # Bot API endpoints - authenticated via bot_key in URL get ":bot_key/messages", to: "messages/by_bots#index", as: :bot_messages_index post ":bot_key/messages", to: "messages/by_bots#create", as: :bot_messages + post ":bot_key/messages/:message_id/boosts", to: "messages/boosts/by_bots#create", as: :bot_message_boosts scope module: "rooms" do resource :refresh, only: :show diff --git a/test/controllers/messages/boosts/by_bots_controller_test.rb b/test/controllers/messages/boosts/by_bots_controller_test.rb new file mode 100644 index 0000000..09dfd86 --- /dev/null +++ b/test/controllers/messages/boosts/by_bots_controller_test.rb @@ -0,0 +1,76 @@ +require "test_helper" + +class Messages::Boosts::ByBotsControllerTest < ActionDispatch::IntegrationTest + setup do + @room = rooms(:watercooler) + @message = messages(:fourth) # Message in watercooler room where bender bot is a member + @bot = users(:bender) + end + + test "create adds a boost to the message" do + assert_difference -> { @message.boosts.count }, +1 do + post room_bot_message_boosts_url(@room, @bot.bot_key, @message), params: +"👀" + assert_response :created + end + + assert_equal "👀", @message.boosts.last.content + end + + test "create with emoji reaction" do + assert_difference -> { Boost.count }, +1 do + post room_bot_message_boosts_url(@room, @bot.bot_key, @message), params: +"🎉" + assert_response :created + end + end + + test "create with text reaction" do + assert_difference -> { Boost.count }, +1 do + post room_bot_message_boosts_url(@room, @bot.bot_key, @message), params: +"Nice!" + assert_response :created + end + + assert_equal "Nice!", @message.boosts.last.content + end + + test "create broadcasts the boost" do + assert_turbo_stream_broadcasts [ @message.room, :messages ], count: 1 do + post room_bot_message_boosts_url(@room, @bot.bot_key, @message), params: +"👍" + end + end + + test "create requires valid bot key" do + assert_no_difference -> { Boost.count } do + post room_bot_message_boosts_url(@room, "invalid-bot-key", @message), params: +"👀" + end + assert_response :redirect # Redirects to login + end + + test "create returns not_found for room bot is not a member of" do + room_without_bot = rooms(:designers) + message_in_other_room = messages(:first) # Message in designers room + + assert_no_difference -> { Boost.count } do + post room_bot_message_boosts_url(room_without_bot, @bot.bot_key, message_in_other_room), params: +"👀" + end + assert_response :not_found + end + + test "create returns not_found for message not in the room" do + message_in_other_room = messages(:first) # Message in designers room, not watercooler + + assert_no_difference -> { Boost.count } do + post room_bot_message_boosts_url(@room, @bot.bot_key, message_in_other_room), params: +"👀" + end + assert_response :not_found + end + + test "create can't be abused to post boosts as regular user" do + user = users(:kevin) + bot_key = "#{user.id}-" + + assert_no_difference -> { Boost.count } do + post room_bot_message_boosts_url(@room, bot_key, @message), params: +"👀" + end + assert_response :redirect + end +end