Move the report commands into exp_reports

The create-report, get-reports and clear-reports commands now live in the
plugin's own module next to the functions they call, with their locale.
The permissions keep their exp_scenario.command names because the command
authority derives them from the command name.

The Discord report alert in exp_scenario is gone since exp_reports posts to
its own webhook. The shared add_player stub takes an options table with
connected and admin defaulting to true and false, replacing the copy in
the exp_reports test environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Opus 5.5 committed 2026-09-26 23:22:18 +00:00
1 parent e47f41d440
commit 9ddb7b362b
20 files changed
+95 -155

No files matched your search

@@ -4,7 +4,6 @@
return {
show_playtime = true,
entity_protection = true,
player_reports = true,
player_bans = true,
player_mutes = true,
player_kicks = true,
@@ -1,5 +1,5 @@
--[[-- Commands - Reports
Adds a commands that allow players to report other players
--[[-- ExpReports - Commands
Commands which let players report each other and admins manage the reports
]]
local Commands = require("modules/exp_commands")
@@ -19,19 +19,19 @@ local function reportable_player(input, player)
if not success then return status, result end
--- @cast result LuaPlayer
if player_has_permission(input, "exp_scenario.bypass.reports") then
return Commands.status.invalid_input{ "exp-commands_reports.player-immune" }
elseif player == input then
return Commands.status.invalid_input{ "exp-commands_reports.self-report" }
if player_has_permission(result, "exp_scenario.bypass.reports") then
return Commands.status.invalid_input{ "exp-reports_commands.player-immune" }
elseif player == result then
return Commands.status.invalid_input{ "exp-reports_commands.self-report" }
else
return Commands.status.success(result)
end
end
--- Reports a player and notifies admins, the outcome is printed once the controller answers
Commands.new("create-report", { "exp-commands_reports.description-create" })
:argument("player", { "exp-commands_reports.arg-player-create" }, reportable_player)
:argument("reason", { "exp-commands_reports.arg-reason" }, Commands.types.string)
Commands.new("create-report", { "exp-reports_commands.description-create" })
:argument("player", { "exp-reports_commands.arg-player-create" }, reportable_player)
:argument("reason", { "exp-reports_commands.arg-reason" }, Commands.types.string)
:enable_auto_concatenation()
:add_aliases{ "report" }
:register(function(player, other_player, reason)
@@ -41,8 +41,8 @@ Commands.new("create-report", { "exp-commands_reports.description-create" })
end)
--- Lists the reports against a player, or the number against every player, printed once the controller answers
Commands.new("get-reports", { "exp-commands_reports.description-get" })
:optional("player", { "exp-commands_reports.arg-player-get" }, Commands.types.player)
Commands.new("get-reports", { "exp-reports_commands.description-get" })
:optional("player", { "exp-reports_commands.arg-player-get" }, Commands.types.player)
:add_aliases{ "reports" }
:add_flags{ "admin_only" }
:register(function(player, other_player)
@@ -51,9 +51,9 @@ Commands.new("get-reports", { "exp-commands_reports.description-get" })
end)
--- Clears all reports from a player or just the report from one player, printed once the controller answers
Commands.new("clear-reports", { "exp-commands_reports.description-clear" })
:argument("player", { "exp-commands_reports.arg-player-clear" }, Commands.types.player)
:optional("from-player", { "exp-commands_reports.arg-from-player" }, Commands.types.player)
Commands.new("clear-reports", { "exp-reports_commands.description-clear" })
:argument("player", { "exp-reports_commands.arg-player-clear" }, Commands.types.player)
:optional("from-player", { "exp-reports_commands.arg-from-player" }, Commands.types.player)
:add_flags{ "admin_only" }
:register(function(player, other_player, from_player)
--- @cast other_player LuaPlayer
+12
View File
@@ -8,3 +8,15 @@ list-all-entry=__1__: __2__ __plural_for_parameter__2__{1=report|rest=reports}__
list-all-none=No players have reports against them.
deleted=__1__ had __2__ __plural_for_parameter__2__{1=report|rest=reports}__ removed by __3__.
deleted-none=__1__ has no reports against them.
[exp-reports_commands]
description-create=Reports a player and notifies moderators
description-get=Gets a list of all reports that a player has on them. If no player then lists all players and the number of reports on them.
description-clear=Clears all reports from a player or just the report from one player.
arg-player-create=Player you want to report.
arg-player-get=Player to get the reports of.
arg-player-clear=Player to clear the reports of.
arg-reason=Reason you want to report this player.
arg-from-player=Only remove the report from this player.
player-immune=This player can not be reported.
self-report=You cannot report yourself.
+12
View File
@@ -8,3 +8,15 @@ list-all-entry=__1__: __2__
list-all-none=沒有用戶被舉報。
deleted=__1__ 被舉報的 __2__ 個紀錄已被 __3__ 清除。
deleted-none=__1__ 沒有被舉報的紀錄。
[exp-reports_commands]
description-create=檢舉用戶並通知管理員。
description-get=取得用戶的報告清單。如果沒有設定用戶,則列出所有用戶以及有關他們的報告數量。
description-clear=清除一名用戶的所有報告或僅清除一名用戶的報告。
arg-player-create=用戶
arg-player-get=用戶
arg-player-clear=用戶
arg-reason=原因
arg-from-player=用戶
player-immune=用戶不能被舉報
self-report=你不能舉報自己。
+12
View File
@@ -8,3 +8,15 @@ list-all-entry=__1__: __2__
list-all-none=沒有用戶被舉報。
deleted=__1__ 被舉報的 __2__ 個紀錄已被 __3__ 清除。
deleted-none=__1__ 沒有被舉報的紀錄。
[exp-reports_commands]
description-create=檢舉用戶並通知管理員。
description-get=取得用戶的報告清單。如果沒有設定用戶,則列出所有用戶以及有關他們的報告數量。
description-clear=清除一名用戶的所有報告或僅清除一名用戶的報告。
arg-player-create=用戶
arg-player-get=用戶
arg-player-clear=用戶
arg-reason=原因
arg-from-player=用戶
player-immune=用戶不能被舉報
self-report=你不能舉報自己。
+4 -1
View File
@@ -1,12 +1,15 @@
{
"name": "exp_reports",
"load": [
"commands.lua"
],
"require": [
"globals.lua"
],
"dependencies": {
"clusterio": "*",
"exp_util": "*"
"exp_util": "*",
"exp_commands": "*",
"exp_roles": "*"
}
}
-13
View File
@@ -13,15 +13,10 @@ local plugin_root = assert(source:match("^@(.*)/test/module/env%.lua$"))
local Framework = assert(loadfile(shared_root .. "/framework.lua"))() --- @type Framework
--- A stubbed player which also carries the admin flag the module reads
--- @class ExpReports.TestPlayer : Stubs.Player
--- @field admin boolean
--- The environment given to each test: the stubs extended with a fresh copy
--- of the reports module and the fixture helpers
--- @class ExpReports.TestEnv : Stubs
--- @field Reports ExpReports A fresh copy of the reports module
--- @field add_player fun(name: string, is_connected: boolean?, is_admin: boolean?): ExpReports.TestPlayer
--- @field report fun(player_name: string, by_player_name: string, reason: string?): ExpReports.Report A report as the controller sends it
return Framework.suite(function(env)
@@ -34,14 +29,6 @@ return Framework.suite(function(env)
}
env.Reports = assert(loadfile(plugin_root .. "/module/control.lua"))() --- @type ExpReports
-- The module reads player.admin, which the strict stub raises on unless it is set
local add_player = env.add_player
function env.add_player(name, is_connected, is_admin)
local player = add_player(name, is_connected) --[[@as ExpReports.TestPlayer]]
player.admin = is_admin == true
return player
end
local next_id = 0
function env.report(player_name, by_player_name, reason)
next_id = next_id + 1
+15 -15
View File
@@ -1,9 +1,9 @@
local Suite = ... --- @type Suite<ExpReports.TestEnv>
Suite.test("receive_created() tells everyone and raises on_player_reported", function(env)
env.add_player("alice", true, true)
local bob = env.add_player("bob")
env.add_player("carol")
env.add_player{ name = "alice", admin = true }
local bob = env.add_player{ name = "bob" }
env.add_player{ name = "carol" }
local report = env.report("bob", "alice")
env.Reports.receive_created{ report = report, reports = { report, env.report("bob", "carol") } }
@@ -26,8 +26,8 @@ Suite.test("receive_created() tells everyone and raises on_player_reported", fun
end)
Suite.test("receive_created() raises nothing when the reported player has left", function(env)
env.add_player("alice")
env.add_player("bob", false)
env.add_player{ name = "alice" }
env.add_player{ name = "bob", connected = false }
local report = env.report("bob", "alice")
env.Reports.receive_created{ report = report, reports = { report } }
Suite.eq(env.printed, { { to = "alice", { "exp-reports.created", "bob", "griefing" } } }, "the report is still announced")
@@ -35,7 +35,7 @@ Suite.test("receive_created() raises nothing when the reported player has left",
end)
Suite.test("receive_list() prints the reports against a player to the caller", function(env)
env.add_player("alice")
env.add_player{ name = "alice" }
env.Reports.receive_list{
caller = "alice",
player_name = "bob",
@@ -49,7 +49,7 @@ Suite.test("receive_list() prints the reports against a player to the caller", f
end)
Suite.test("receive_list() prints how many reports each player has", function(env)
env.add_player("alice")
env.add_player{ name = "alice" }
env.Reports.receive_list{
caller = "alice",
reports = { env.report("dave", "alice"), env.report("bob", "carol"), env.report("dave", "carol") },
@@ -62,20 +62,20 @@ Suite.test("receive_list() prints how many reports each player has", function(en
end)
Suite.test("receive_list() says when nobody is reported", function(env)
env.add_player("alice")
env.add_player{ name = "alice" }
env.Reports.receive_list{ caller = "alice", reports = {} }
Suite.eq(env.printed, { { to = "alice", { "exp-reports.list-all-none" } } }, "the caller is told there are none")
end)
Suite.test("receive_list() prints nothing once the caller has left", function(env)
env.add_player("alice", false)
env.add_player{ name = "alice", connected = false }
env.Reports.receive_list{ caller = "alice", reports = { env.report("bob", "carol") } }
Suite.empty(env.printed, "nothing is printed")
end)
Suite.test("receive_deleted() announces the deletion and raises on_reports_deleted", function(env)
env.add_player("alice")
local bob = env.add_player("bob")
env.add_player{ name = "alice" }
local bob = env.add_player{ name = "bob" }
env.Reports.receive_deleted{ caller = "alice", player_name = "bob", count = 2 }
Suite.eq(env.printed, { { "exp-reports.deleted", "bob", 2, "alice" } }, "everyone is told")
Suite.eq(env.events, {
@@ -84,16 +84,16 @@ Suite.test("receive_deleted() announces the deletion and raises on_reports_delet
end)
Suite.test("receive_deleted() tells the caller when there was nothing to delete", function(env)
env.add_player("alice")
env.add_player("bob")
env.add_player{ name = "alice" }
env.add_player{ name = "bob" }
env.Reports.receive_deleted{ caller = "alice", player_name = "bob", count = 0 }
Suite.eq(env.printed, { { to = "alice", { "exp-reports.deleted-none", "bob" } } }, "only the caller is told")
Suite.empty(env.events, "nothing was deleted")
end)
Suite.test("receive_error() prints the refusal to the caller while they are online", function(env)
env.add_player("alice")
env.add_player("bob", false)
env.add_player{ name = "alice" }
env.add_player{ name = "bob", connected = false }
env.Reports.receive_error{ caller = "alice", message = "alice has already reported bob" }
env.Reports.receive_error{ caller = "bob", message = "gone" }
Suite.eq(env.printed, { { to = "alice", "alice has already reported bob" } }, "the offline player is skipped")
+6 -6
View File
@@ -1,8 +1,8 @@
local Suite = ... --- @type Suite<ExpReports.TestEnv>
Suite.test("create_report() sends the report to the controller", function(env)
local alice = env.add_player("alice")
local bob = env.add_player("bob")
local alice = env.add_player{ name = "alice" }
local bob = env.add_player{ name = "bob" }
env.Reports.create_report(alice, bob, "griefing")
Suite.eq(env.sent, {
{ channel = "exp_reports:create", data = { player_name = "bob", by_player_name = "alice", reason = "griefing" } },
@@ -10,8 +10,8 @@ Suite.test("create_report() sends the report to the controller", function(env)
end)
Suite.test("list_reports() sends the caller and the player", function(env)
local alice = env.add_player("alice")
local bob = env.add_player("bob")
local alice = env.add_player{ name = "alice" }
local bob = env.add_player{ name = "bob" }
env.Reports.list_reports(alice, bob)
env.Reports.list_reports(alice)
Suite.eq(env.sent, {
@@ -21,8 +21,8 @@ Suite.test("list_reports() sends the caller and the player", function(env)
end)
Suite.test("delete_reports() sends the caller, the player and the reporter", function(env)
local alice = env.add_player("alice")
local bob = env.add_player("bob")
local alice = env.add_player{ name = "alice" }
local bob = env.add_player{ name = "bob" }
env.Reports.delete_reports(alice, bob, "carol")
env.Reports.delete_reports(alice, bob)
Suite.eq(env.sent, {
File renamed without changes.
+2 -2
View File
@@ -5,8 +5,8 @@ local test, check, eq, empty = Suite.test, Suite.check, Suite.eq, Suite.empty
--- alice is a moderator and bob a regular, with changes sent to the controller
local function setup(env)
local players = {
alice = env.add_player("alice"),
bob = env.add_player("bob"),
alice = env.add_player{ name = "alice" },
bob = env.add_player{ name = "bob" },
}
env.initialise{
env.assignment("alice", { "Moderator" }),
+3 -3
View File
@@ -5,9 +5,9 @@ local test, check, eq = Suite.test, Suite.check, Suite.eq
--- alice and dave are moderators with dave offline, zed has never joined
local function setup(env)
local players = {
alice = env.add_player("alice"),
bob = env.add_player("bob"),
dave = env.add_player("dave", false),
alice = env.add_player{ name = "alice" },
bob = env.add_player{ name = "bob" },
dave = env.add_player{ name = "dave", connected = false },
}
env.initialise{
env.assignment("alice", { "Moderator" }),
+3 -3
View File
@@ -5,9 +5,9 @@ local test, check, eq = Suite.test, Suite.check, Suite.eq
--- alice is a moderator, bob is a regular, and carol has only the default role
local function setup(env)
local players = {
alice = env.add_player("alice"),
bob = env.add_player("bob"),
carol = env.add_player("carol"),
alice = env.add_player{ name = "alice" },
bob = env.add_player{ name = "bob" },
carol = env.add_player{ name = "carol" },
}
env.initialise{
env.assignment("alice", { "Moderator" }),
+2 -2
View File
@@ -8,8 +8,8 @@ local admin_state = {} --- @type table<string, boolean>
--- alice is a moderator and carol has only the default role, both connected
local function setup(env)
local players = {
alice = env.add_player("alice"),
carol = env.add_player("carol"),
alice = env.add_player{ name = "alice" },
carol = env.add_player{ name = "carol" },
}
admin_state = {}
env.Roles.define_permission_trigger("exp_scenario.player.admin", function(player, state)
-1
View File
@@ -30,7 +30,6 @@ require("modules/exp_scenario/commands/me")
require("modules/exp_scenario/commands/rainbow")
require("modules/exp_scenario/commands/ratio")
require("modules/exp_scenario/commands/repair")
require("modules/exp_scenario/commands/reports")
require("modules/exp_scenario/commands/roles")
require("modules/exp_scenario/commands/search")
require("modules/exp_scenario/commands/spectate")
@@ -97,40 +97,6 @@ if config.entity_protection then
end
end
--- Reports added and removed
if config.player_reports then
local Reports = require("modules/exp_reports")
--- @param event EventData.ExpReports.on_player_reported
events[Reports.on_player_reported] = function(event)
local player_name, by_player_name = get_player_name(event)
emit_event{
title = "Report",
description = "A player was reported",
color = Colors.yellow,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "By", inline = true, value = append_playtime(by_player_name) },
{ name = "Report Count", inline = true, value = tostring(event.report_count) },
{ name = "Reason", value = event.reason },
},
}
end
--- @param event EventData.ExpReports.on_reports_deleted
events[Reports.on_reports_deleted] = function(event)
local player_name, by_player_name = get_player_name(event)
emit_event{
title = "Reports Removed",
description = "A player has reports removed",
color = Colors.green,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "By", inline = true, value = append_playtime(by_player_name) },
{ name = "Report Count", inline = true, value = tostring(event.count) },
},
}
end
end
--- When a player is jailed or unjailed
if config.player_jail then
local Jail = require("modules/exp_scenario/control/jail")
-12
View File
@@ -161,18 +161,6 @@ description=Repairs entities on your force around you.
response-revive=__1__ entities were revived.
response-heal=__1__ entities were healed.
[exp-commands_reports]
description-create=Reports a player and notifies moderators
description-get=Gets a list of all reports that a player has on them. If no player then lists all players and the number of reports on them.
description-clear=Clears all reports from a player or just the report from one player.
arg-player-create=Player you want to report.
arg-player-get=Player to get the reports of.
arg-player-clear=Player to clear the reports of.
arg-reason=Reason you want to report this player.
arg-from-player=Only remove the report from this player.
player-immune=This player can not be reported.
self-report=You cannot report yourself.
[exp-commands_roles]
description-assign=Assigns a role to a player.
description-unassign=Unassigns a role from a player.
-22
View File
@@ -165,28 +165,6 @@ arg-range=半徑
response-revive=__1__ 個建築恢復。
response-heal=回滿 __2__ 個建築耐久。
[exp-commands_reports]
description-create=檢舉用戶並通知管理員。
description-get=取得用戶的報告清單。如果沒有設定用戶,則列出所有用戶以及有關他們的報告數量。
description-clear=清除一名用戶的所有報告或僅清除一名用戶的報告。
arg-player-create=用戶
arg-player-get=用戶
arg-player-clear=用戶
arg-reason=原因
arg-from-player=用戶
player-immune=用戶不能被舉報
self-report=你不能舉報自己。
response=__1__ 因 __2__ 被舉報了。
response-admin=__1__ 因 __3__ 被 __2__ 舉報了。
already-reported=你已經舉報過該用戶了,你可以讓管理員清除報告來重寫。
not-reported=該用戶沒有被舉報的紀錄。
not-reported-by=該用戶沒有被 __1__ 舉報的紀錄。
reports-title==該用戶現在的被舉報紀錄:
player-title=__1__ 有 __2__ 項被舉報的紀錄:
list-element=__1__: __2__
removed-all=__1__ 被舉報的所有紀錄已被 __2__ 清除。
removed=__1__ 被舉報的一個紀錄已被 __2__ 清除。
[exp-commands_roles]
description-assign=為用戶指配用戶組。
description-unassign=為用戶取消指配用戶組。
-22
View File
@@ -165,28 +165,6 @@ arg-range=半徑
response-revive=__1__ 個建築恢復。
response-heal=回滿 __2__ 個建築耐久。
[exp-commands_reports]
description-create=檢舉用戶並通知管理員。
description-get=取得用戶的報告清單。如果沒有設定用戶,則列出所有用戶以及有關他們的報告數量。
description-clear=清除一名用戶的所有報告或僅清除一名用戶的報告。
arg-player-create=用戶
arg-player-get=用戶
arg-player-clear=用戶
arg-reason=原因
arg-from-player=用戶
player-immune=用戶不能被舉報
self-report=你不能舉報自己。
response=__1__ 因 __2__ 被舉報了。
response-admin=__1__ 因 __3__ 被 __2__ 舉報了。
already-reported=你已經舉報過該用戶了,你可以讓管理員清除報告來重寫。
not-reported=該用戶沒有被舉報的紀錄。
not-reported-by=該用戶沒有被 __1__ 舉報的紀錄。
reports-title==該用戶現在的被舉報紀錄:
player-title=__1__ 有 __2__ 項被舉報的紀錄:
list-element=__1__: __2__
removed-all=__1__ 被舉報的所有紀錄已被 __2__ 清除。
removed=__1__ 被舉報的一個紀錄已被 __2__ 清除。
[exp-commands_roles]
description-assign=為用戶指配用戶組。
description-unassign=為用戶取消指配用戶組。
+10 -4
View File
@@ -100,17 +100,23 @@ function Stubs.new()
get_player = function(key) return players[key] end,
}, { "player" })
--- @class Stubs.PlayerParams
--- @field name string
--- @field connected boolean? Defaults to true
--- @field admin boolean? Defaults to false
--- Add a player to the stubbed game, indexes are assigned in join order
--- @param name string
--- @param is_connected boolean? Defaults to connected
--- @param params Stubs.PlayerParams
--- @return Stubs.Player
function stubs.add_player(name, is_connected)
function stubs.add_player(params)
local name = params.name
local index = #players_by_index + 1
--- @class Stubs.Player
local player = Stubs.strict("LuaPlayer " .. name, {
name = name,
index = index,
connected = is_connected ~= false,
connected = params.connected ~= false,
admin = params.admin == true,
valid = true,
play_sound = function(opts)
stubs.sounds[#stubs.sounds + 1] = name .. ":" .. opts.path