Implement non-player recipients (#131)

* Implement non-player recipients

* Add API callback specifically for players receiving mail

* Exclude sender from (mailing list) recipients

* Complement test

* Fixup typos in complemented test

* Expand aliases at toplevel if the current expansion is at toplevel

This should allow players to send mail to their own aliases

* Also test on_(player_)receive callbacks

* Fix oversight in test case
This commit is contained in:
y5nw 2024-02-01 20:46:26 +01:00 committed by GitHub
parent fcca0b7511
commit 570cf788ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 185 additions and 64 deletions

52
api.lua
View File

@ -10,6 +10,16 @@ function mail.register_on_receive(func)
mail.registered_on_receives[#mail.registered_on_receives + 1] = func
end
mail.registered_on_player_receives = {}
function mail.register_on_player_receive(func)
table.insert(mail.registered_on_player_receives, func)
end
mail.registered_recipient_handlers = {}
function mail.register_recipient_handler(func)
table.insert(mail.registered_recipient_handlers, func)
end
function mail.send(m)
if type(m.from) ~= "string" then return false, "'from' is not a string" end
if type(m.to or "") ~= "string" then return false, "'to' is not a string" end
@ -25,22 +35,22 @@ function mail.send(m)
local recipients = {}
local undeliverable = {}
m.to = mail.concat_player_list(mail.extractMaillists(m.to, m.from))
m.to = mail.normalize_players_and_add_recipients(m.to, recipients, undeliverable)
m.to = mail.normalize_players_and_add_recipients(m.from, m.to, recipients, undeliverable)
if m.cc then
m.cc = mail.concat_player_list(mail.extractMaillists(m.cc, m.from))
m.cc = mail.normalize_players_and_add_recipients(m.cc, recipients, undeliverable)
m.cc = mail.normalize_players_and_add_recipients(mail.from, m.cc, recipients, undeliverable)
end
if m.bcc then
m.bcc = mail.concat_player_list(mail.extractMaillists(m.bcc, m.from))
m.bcc = mail.normalize_players_and_add_recipients(m.bcc, recipients, undeliverable)
m.bcc = mail.normalize_players_and_add_recipients(m.from, m.bcc, recipients, undeliverable)
end
if next(undeliverable) then -- table is not empty
local undeliverable_names = {}
for name in pairs(undeliverable) do
undeliverable_names[#undeliverable_names + 1] = '"' .. name .. '"'
local undeliverable_reason = {S("The mail could not be sent:")}
for _, reason in pairs(undeliverable) do
table.insert(undeliverable_reason, reason)
end
return false, f("recipients %s don't exist; cannot send mail.", table.concat(undeliverable_names, ", "))
return false, table.concat(undeliverable_reason, "\n")
end
local extra = {}
@ -86,32 +96,8 @@ function mail.send(m)
msg.spam = #mail.check_spam(msg) >= 1
-- add in every receivers inbox
for recipient in pairs(recipients) do
entry = mail.get_storage_entry(recipient)
table.insert(entry.inbox, msg)
mail.set_storage_entry(recipient, entry)
end
-- notify recipients that happen to be online
local mail_alert = S("You have a new message from @1! Subject: @2", m.from, m.subject) ..
"\n" .. S("To view it, type /mail")
local inventory_alert = S("You could also use the button in your inventory.")
for _, player in ipairs(minetest.get_connected_players()) do
local name = player:get_player_name()
if recipients[name] then
if mail.get_setting(name, "chat_notifications") == true then
minetest.chat_send_player(name, mail_alert)
if minetest.get_modpath("unified_inventory") or minetest.get_modpath("sfinv_buttons") then
minetest.chat_send_player(name, inventory_alert)
end
end
if mail.get_setting(name, "sound_notifications") == true then
minetest.sound_play("mail_notif", {to_player=name})
end
local receiver_entry = mail.get_storage_entry(name)
local receiver_messages = receiver_entry.inbox
mail.hud_update(name, receiver_messages)
end
for _, deliver in pairs(recipients) do
deliver(msg)
end
for i=1, #mail.registered_on_receives do

25
api.md
View File

@ -34,7 +34,7 @@ local success, error = mail.send({
```
# Hooks
On-receive mail hook:
Generic on-receive mail hook:
```lua
mail.register_on_receive(function(m)
@ -42,6 +42,29 @@ mail.register_on_receive(function(m)
end)
```
Player-specific on-receive mail hook:
```lua
mail.register_on_player_receive(function(player, msg)
-- "player" is the name of a recipient; "msg" is a mail object (see "Mail format")
end)
```
# Recipient handler
Recipient handlers are registered using
```lua
mail.register_recipient_handler(function(sender, name)
end)
```
where `name` is the name of a single recipient.
The recipient handler should return
* `nil` if the handler does not handle messages sent to the particular recipient,
* `true, player` (where `player` is a string or a list of strings) if the mail should be redirected to `player`,
* `true, deliver` if the mail should be delivered by calling `deliver` with the message, or
* `false, reason` (where `reason` is optional or, if provided, a string) if the recipient explicitly rejects the mail.
# Internals
mod-storage entry for a player (indexed by playername and serialized with json):

View File

@ -1,12 +1,56 @@
mail.register_recipient_handler(function(_, name)
if name:sub(1, 6) == "alias/" then
return true, name:sub(7)
elseif name == "list/test" then
return true, {"alias/player1", "alias/player2"}
elseif name == "list/reject" then
return false, "It works (?)"
end
end)
local received_count = {}
mail.register_on_player_receive(function(player)
received_count[player] = (received_count[player] or 0) + 1
end)
local sent_count = 0
mail.register_on_receive(function()
sent_count = sent_count+1
end)
local function assert_inbox_count(player_name, count)
local entry = mail.get_storage_entry(player_name)
assert(entry, player_name .. " has no mail entry")
local actual_count = #entry.inbox
assert(actual_count == count, ("incorrect mail count: %d expected, got %d"):format(count, actual_count))
local player_received = received_count[player_name] or 0
assert(player_received == count, ("incorrect receive count: %d expected, got %d"):format(count, player_received))
end
mtt.register("send mail", function(callback)
-- send a mail
local success, err = mail.send({from = "player1", to = "player2", subject = "something", body = "blah"})
-- send a mail to a list
local success, err = mail.send({from = "player1", to = "list/test", subject = "something", body = "blah"})
assert(success)
assert(not err)
assert_inbox_count("player2", 1)
assert_inbox_count("player1", 0)
assert(sent_count == 1)
-- send a second mail to the list and also the sender
success, err = mail.send({from = "player1", to = "list/test, alias/player1", subject = "something", body = "blah"})
assert(success)
assert(not err)
assert_inbox_count("player2", 2)
assert_inbox_count("player1", 1)
assert(sent_count == 2)
-- send a mail to list/reject - the mail should be rejected
success, err = mail.send({from = "player1", to = "list/reject", subject = "something", body = "NO"})
assert(not success)
assert(type(err) == "string")
assert_inbox_count("player2", 2)
assert_inbox_count("player1", 1)
assert(sent_count == 2)
-- check the receivers inbox
local entry = mail.get_storage_entry("player2")
assert(entry)
assert(#entry.inbox > 0)
callback()
end)

View File

@ -49,6 +49,7 @@ dofile(MP .. "/storage.lua")
dofile(MP .. "/api.lua")
dofile(MP .. "/gui.lua")
dofile(MP .. "/onjoin.lua")
dofile(MP .. "/player_recipients.lua")
-- sub directories
dofile(MP .. "/ui/init.lua")

51
player_recipients.lua Normal file
View File

@ -0,0 +1,51 @@
local S = minetest.get_translator("mail")
local has_canonical_name = minetest.get_modpath("canonical_name")
mail.register_on_player_receive(function(name, msg)
-- add to inbox
local entry = mail.get_storage_entry(name)
table.insert(entry.inbox, msg)
mail.set_storage_entry(name, entry)
-- notify recipients that happen to be online
local mail_alert = S("You have a new message from @1! Subject: @2", msg.from, msg.subject) ..
"\n" .. S("To view it, type /mail")
local inventory_alert = S("You could also use the button in your inventory.")
local player = minetest.get_player_by_name(name)
if player then
if mail.get_setting(name, "chat_notifications") == true then
minetest.chat_send_player(name, mail_alert)
if minetest.get_modpath("unified_inventory") or minetest.get_modpath("sfinv_buttons") then
minetest.chat_send_player(name, inventory_alert)
end
end
if mail.get_setting(name, "sound_notifications") == true then
minetest.sound_play("mail_notif", {to_player=name})
end
local receiver_entry = mail.get_storage_entry(name)
local receiver_messages = receiver_entry.inbox
mail.hud_update(name, receiver_messages)
end
end)
mail.register_recipient_handler(function(_, pname)
if not minetest.player_exists(pname) then
return nil
end
return true, function(msg)
for _, on_player_receive in ipairs(mail.registered_on_player_receives) do
if on_player_receive(pname, msg) then
break
end
end
end
end)
if has_canonical_name then
mail.register_recipient_handler(function(_, name)
local realname = canonical_name.get(name)
if realname then
return true, realname
end
end)
end

View File

@ -1,18 +1,43 @@
local has_canonical_name = minetest.get_modpath("canonical_name")
local S = minetest.get_translator("mail")
local function recursive_expand_recipient_names(sender, list, is_toplevel, recipients, undeliverable)
for _, name in ipairs(list) do
if not (recipients[name] or undeliverable[name] or (name == sender and not is_toplevel)) then
local succ, value
for _, handler in ipairs(mail.registered_recipient_handlers) do
succ, value = handler(sender, name)
if succ ~= nil then
break
end
end
local vtp = type(value)
if succ then
if vtp == "string" then
recursive_expand_recipient_names(sender, {value}, is_toplevel, recipients, undeliverable)
elseif vtp == "table" then
recursive_expand_recipient_names(sender, value, false, recipients, undeliverable)
elseif vtp == "function" then
recipients[name] = value
else
undeliverable[name] = S("The method of delivery to @1 is invalid.", name)
end
elseif succ == nil then
undeliverable[name] = S("The recipient @1 could not be identified.", name)
else
local reason = tostring(value) or S("@1 rejected your mail.", name)
undeliverable[name] = reason
end
end
end
end
--[[
return the field normalized (comma separated, single space)
and add individual player names to recipient list
--]]
function mail.normalize_players_and_add_recipients(field, recipients, undeliverable)
function mail.normalize_players_and_add_recipients(sender, field, recipients, undeliverable)
local order = mail.parse_player_list(field)
for _, recipient_name in ipairs(order) do
if not minetest.player_exists(recipient_name) then
undeliverable[recipient_name] = true
else
recipients[recipient_name] = true
end
end
recursive_expand_recipient_names(sender, order, true, recipients, undeliverable)
return mail.concat_player_list(order)
end
@ -21,23 +46,14 @@ function mail.parse_player_list(field)
return {}
end
local separator = ", "
local separator = ",%s"
local pattern = "([^" .. separator .. "]+)"
-- get individual players
local player_set = {}
local order = {}
field:gsub(pattern, function(player_name)
local lower = string.lower(player_name)
if not player_set[lower] then
if has_canonical_name then
player_name = canonical_name.get(player_name) or player_name
end
player_set[lower] = player_name
order[#order+1] = player_name
end
end)
for name in field:gmatch(pattern) do
table.insert(order, name)
end
return order
end

View File

@ -2,11 +2,11 @@
mtt.register("util/normalize_players_and_add_recipients", function(callback)
local recipients = {}
local undeliverable = {}
local to = mail.normalize_players_and_add_recipients("player1,player2", recipients, undeliverable)
local to = mail.normalize_players_and_add_recipients("sender", "player1,player2", recipients, undeliverable)
assert(to == "player1, player2")
assert(not next(undeliverable))
assert(recipients["player1"])
assert(recipients["player2"])
callback()
end)
end)