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 mail.registered_on_receives[#mail.registered_on_receives + 1] = func
end 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) function mail.send(m)
if type(m.from) ~= "string" then return false, "'from' is not a string" end 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 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 recipients = {}
local undeliverable = {} local undeliverable = {}
m.to = mail.concat_player_list(mail.extractMaillists(m.to, m.from)) 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 if m.cc then
m.cc = mail.concat_player_list(mail.extractMaillists(m.cc, m.from)) 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 end
if m.bcc then if m.bcc then
m.bcc = mail.concat_player_list(mail.extractMaillists(m.bcc, m.from)) 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 end
if next(undeliverable) then -- table is not empty if next(undeliverable) then -- table is not empty
local undeliverable_names = {} local undeliverable_reason = {S("The mail could not be sent:")}
for name in pairs(undeliverable) do for _, reason in pairs(undeliverable) do
undeliverable_names[#undeliverable_names + 1] = '"' .. name .. '"' table.insert(undeliverable_reason, reason)
end end
return false, f("recipients %s don't exist; cannot send mail.", table.concat(undeliverable_names, ", ")) return false, table.concat(undeliverable_reason, "\n")
end end
local extra = {} local extra = {}
@ -86,32 +96,8 @@ function mail.send(m)
msg.spam = #mail.check_spam(msg) >= 1 msg.spam = #mail.check_spam(msg) >= 1
-- add in every receivers inbox -- add in every receivers inbox
for recipient in pairs(recipients) do for _, deliver in pairs(recipients) do
entry = mail.get_storage_entry(recipient) deliver(msg)
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
end end
for i=1, #mail.registered_on_receives do for i=1, #mail.registered_on_receives do

25
api.md
View file

@ -34,7 +34,7 @@ local success, error = mail.send({
``` ```
# Hooks # Hooks
On-receive mail hook: Generic on-receive mail hook:
```lua ```lua
mail.register_on_receive(function(m) mail.register_on_receive(function(m)
@ -42,6 +42,29 @@ mail.register_on_receive(function(m)
end) 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 # Internals
mod-storage entry for a player (indexed by playername and serialized with json): 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) mtt.register("send mail", function(callback)
-- send a mail -- send a mail to a list
local success, err = mail.send({from = "player1", to = "player2", subject = "something", body = "blah"}) local success, err = mail.send({from = "player1", to = "list/test", subject = "something", body = "blah"})
assert(success) assert(success)
assert(not err) 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() callback()
end) end)

View file

@ -49,6 +49,7 @@ dofile(MP .. "/storage.lua")
dofile(MP .. "/api.lua") dofile(MP .. "/api.lua")
dofile(MP .. "/gui.lua") dofile(MP .. "/gui.lua")
dofile(MP .. "/onjoin.lua") dofile(MP .. "/onjoin.lua")
dofile(MP .. "/player_recipients.lua")
-- sub directories -- sub directories
dofile(MP .. "/ui/init.lua") 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) return the field normalized (comma separated, single space)
and add individual player names to recipient list 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) local order = mail.parse_player_list(field)
for _, recipient_name in ipairs(order) do recursive_expand_recipient_names(sender, order, true, recipients, undeliverable)
if not minetest.player_exists(recipient_name) then
undeliverable[recipient_name] = true
else
recipients[recipient_name] = true
end
end
return mail.concat_player_list(order) return mail.concat_player_list(order)
end end
@ -21,23 +46,14 @@ function mail.parse_player_list(field)
return {} return {}
end end
local separator = ", " local separator = ",%s"
local pattern = "([^" .. separator .. "]+)" local pattern = "([^" .. separator .. "]+)"
-- get individual players -- get individual players
local player_set = {}
local order = {} local order = {}
field:gsub(pattern, function(player_name) for name in field:gmatch(pattern) do
local lower = string.lower(player_name) table.insert(order, name)
if not player_set[lower] then end
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)
return order return order
end end

View file

@ -2,11 +2,11 @@
mtt.register("util/normalize_players_and_add_recipients", function(callback) mtt.register("util/normalize_players_and_add_recipients", function(callback)
local recipients = {} local recipients = {}
local undeliverable = {} 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(to == "player1, player2")
assert(not next(undeliverable)) assert(not next(undeliverable))
assert(recipients["player1"]) assert(recipients["player1"])
assert(recipients["player2"]) assert(recipients["player2"])
callback() callback()
end) end)