Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 51 additions & 4 deletions api/chat.lua
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,56 @@ local function make_not_enabled_cb()
end
end

-- How much of a message to quote when attributing a failed send to it (item
-- 2: with several sends in flight -- the fast-typing case that triggers rate
-- limiting -- the player needs to tell which one a notice is about). Long
-- enough to recognize at a glance, short enough to stay on one line.
local NOTICE_QUOTE_MAX = 30

local function quote_for_notice(text)
return '"' .. MPAPI.truncate(text, NOTICE_QUOTE_MAX) .. '"'
end

local function make_publish_fn(lobby)
return function(text)
if not MPAPI.config.chat_enabled then
MPAPI.chat.addMessage(localize('k_chat_client_disabled'), COLOUR_SYSTEM)
return
end
MPAPI._internal.send_chat_message(lobby.code, text, function(err, _)
-- The server rejects whitespace-only messages; don't echo them either.
if text:match('^%s*$') then
return
end
-- Optimistic echo: the sender sees their message the instant they hit
-- enter rather than after the round trip. The log is append-only on
-- both backends (DebugPlus owns its own log and hands back no handle),
-- so a line cannot be retracted or recoloured afterwards -- instead a
-- failure notice below quotes the message it refers to, which also
-- disambiguates several sends in flight.
local own_name = MPAPI.chat._own_name or localize('k_you')
MPAPI.chat.addMessage(own_name .. ': ' .. text, COLOUR_OWN)

MPAPI._internal.send_chat_message(lobby.code, text, function(err, data)
if err then
MPAPI.chat.addMessage('[!] ' .. tostring(err), COLOUR_SYSTEM)
-- Client-side failures (offline, transport, an unreadable
-- server response) don't carry player-facing copy -- only
-- ErrorKind.SERVER does -- so those get a generic reason
-- instead of leaking transport/proxy jargon.
local reason = tostring(err)
if err.kind ~= MPAPI.ErrorKind.SERVER then
reason = localize('k_chat_reason_unavailable')
end
MPAPI.chat.addMessage(
localize('k_chat_not_sent') .. ' ' .. quote_for_notice(text) .. ': ' .. reason,
COLOUR_SYSTEM
)
return
end

if data and type(data.publishText) == 'string' and data.publishText ~= text then
-- Moderation rewrote the message; the echo above showed the raw
-- form, so tell the sender what other players actually got.
MPAPI.chat.addMessage(localize('k_chat_sent_as') .. ' ' .. data.publishText, COLOUR_SYSTEM)
end
end)
end
Expand All @@ -74,9 +115,15 @@ local function subscribe_chat(lobby)
return
end

-- Own messages are rendered directly from the send result (see
-- make_publish_fn), never from this subscription; drop our own MQTT
-- echo so they don't double-render.
if sender_id == lobby.player_id then
return
end

local name = data.displayName or sender_id
local colour = (sender_id == lobby.player_id) and COLOUR_OWN or COLOUR_INCOMING
MPAPI.chat.addMessage(name .. ': ' .. data.message, colour)
MPAPI.chat.addMessage(name .. ': ' .. data.message, COLOUR_INCOMING)
end)
end

Expand Down
5 changes: 5 additions & 0 deletions compatibility/debugplus.lua
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ local function patch(dp_console, orig_render)
-- /say <message> → send as chat, don't let DP see it at all.
if M.send_fn then
M.send_fn(say_arg)
elseif MPAPI.connection_state.chat_enabled then
-- Chat is enabled server-side but we're not in a lobby
-- (send_fn only exists while lobby chat is active) --
-- mirrors chat.lua's make_not_enabled_cb branch.
M.addMessage(localize('k_chat_lobby_only'), COLOUR_SYSTEM)
else
M.addMessage(localize('k_chat_not_enabled'), COLOUR_SYSTEM)
end
Expand Down
7 changes: 7 additions & 0 deletions localization/en-us.lua
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ return {
k_match_status_completed = 'Completed',
k_match_status_abandoned = 'Abandoned',
k_match_status_terminated = 'Terminated',
-- Delivery outcomes for a message the player already saw echoed.
-- The frame makes no claim about why or for how long: the reason
-- appended after it is the server's own self-contained sentence,
-- or k_chat_reason_unavailable when the server gave none.
k_chat_not_sent = '[MultiplayerAPI] Not sent',
k_chat_sent_as = '[MultiplayerAPI] Other players saw:',
k_chat_reason_unavailable = 'Something went wrong. Try again.',
-- Chat section in account overlay
k_chat_section_title = 'Chat',
b_chat_on = 'On',
Expand Down
18 changes: 15 additions & 3 deletions networking/api_client/lobby.lua
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,25 @@ function api_client:send_chat_message(jwt_token, code, message, callback)

self:_enqueue(function(status, body)
if status < 200 or status >= 300 then
-- Only a decoded { error = "..." } body is genuine player-facing
-- copy (chat.lua's k_chat_not_sent guard shows it verbatim). A
-- non-JSON or bare-status response -- a proxy error page, an
-- unexpected 5xx with no body -- carries no usable reason, so it's
-- TRANSPORT rather than SERVER: the caller falls back to its own
-- clean message instead of leaking this raw text to the player.
local ok, data = pcall(api_client.json_decode, body)
local emsg = (ok and data and data.error) or ('Server returned status ' .. tostring(status))
callback(MPAPI.make_error(MPAPI.ErrorKind.SERVER, emsg), nil)
if ok and data and data.error then
callback(MPAPI.make_error(MPAPI.ErrorKind.SERVER, data.error), nil)
else
callback(MPAPI.make_error(MPAPI.ErrorKind.TRANSPORT, 'Server returned status ' .. tostring(status)), nil)
end
return
end

callback(nil, { ok = true })
-- Pass the response body through: on a moderation rewrite it carries
-- publishText (what other players actually received).
local ok, data = pcall(api_client.json_decode, body)
callback(nil, (ok and type(data) == 'table') and data or { ok = true })
end, function(msg)
callback(MPAPI.make_error(MPAPI.ErrorKind.TRANSPORT, 'HTTP request failed: ' .. tostring(msg)), nil)
end)
Expand Down