From 570cf788ecca9f4174f923b07e46b9bb84c419db Mon Sep 17 00:00:00 2001 From: y5nw <37980625+y5nw@users.noreply.github.com> Date: Thu, 1 Feb 2024 20:46:26 +0100 Subject: [PATCH] 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 --- api.lua | 52 +++++++++++++---------------------- api.md | 25 ++++++++++++++++- api.spec.lua | 56 +++++++++++++++++++++++++++++++++----- init.lua | 1 + player_recipients.lua | 51 +++++++++++++++++++++++++++++++++++ util/normalize.lua | 60 ++++++++++++++++++++++++++--------------- util/normalize.spec.lua | 4 +-- 7 files changed, 185 insertions(+), 64 deletions(-) create mode 100644 player_recipients.lua diff --git a/api.lua b/api.lua index c1ff515..ff4625b 100644 --- a/api.lua +++ b/api.lua @@ -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 diff --git a/api.md b/api.md index 7ef2ea9..bfb5ed2 100644 --- a/api.md +++ b/api.md @@ -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): diff --git a/api.spec.lua b/api.spec.lua index a9f9d2f..5c82898 100644 --- a/api.spec.lua +++ b/api.spec.lua @@ -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) diff --git a/init.lua b/init.lua index 889031c..b921ac4 100644 --- a/init.lua +++ b/init.lua @@ -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") diff --git a/player_recipients.lua b/player_recipients.lua new file mode 100644 index 0000000..7586b98 --- /dev/null +++ b/player_recipients.lua @@ -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 diff --git a/util/normalize.lua b/util/normalize.lua index b817068..4d16530 100644 --- a/util/normalize.lua +++ b/util/normalize.lua @@ -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 diff --git a/util/normalize.spec.lua b/util/normalize.spec.lua index 88628ad..b9caa0f 100644 --- a/util/normalize.spec.lua +++ b/util/normalize.spec.lua @@ -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) \ No newline at end of file +end)