separated from webmail mod

This commit is contained in:
NatureFreshMilk 2019-09-16 08:06:54 +02:00
commit ca88374fbd
19 changed files with 941 additions and 0 deletions

23
.luacheckrc Normal file
View File

@ -0,0 +1,23 @@
unused_args = false
allow_defined_top = true
globals = {
"mail",
}
read_globals = {
-- Stdlib
string = {fields = {"split"}},
table = {fields = {"copy", "getn"}},
-- Minetest
"minetest",
"vector", "ItemStack",
"dump",
-- Deps
"unified_inventory", "default",
-- optional mods
"xban"
}

13
LICENSE Normal file
View File

@ -0,0 +1,13 @@
The file textures/mail_button.png was created by bas080 and is licensed under the WTFPL.
Webmail component:
WTFPL
All other files:
Copyright (c) 2016 Carter Kolwey ("Cheapie Systems")
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and/or any associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

38
README.md Normal file
View File

@ -0,0 +1,38 @@
Mail mod for Minetest
======
This is a fork of cheapies mail mod
It adds a mail-system that allows players to send each other messages in-game and via webmail (optional)
# Screenshots
Ingame mail
![](pics/ingame.png?raw=true)
# Installation
## In-game mail mod
Install it like any other mod: copy the directory `mail_mod` to your "worldmods" folder
## Webmail
See: https://github.com/thomasrudin-mt/mail
# Commands/Howto
To access your mail click on the inventory mail button or use the "/mail" command
Mails can be deleted, marked as read or unread, replied to and forwarded to another player
# Dependencies
* None
# License
See the "LICENSE" file
# Old/Historic stuff
* Old forum topic: https://forum.minetest.net/viewtopic.php?t=14464
* Old mod: https://cheapiesystems.com/git/mail/

67
api.lua Normal file
View File

@ -0,0 +1,67 @@
-- see: mail.md
mail.registered_on_receives = {}
function mail.register_on_receive(func)
mail.registered_on_receives[#mail.registered_on_receives + 1] = func
end
mail.receive_mail_message = "You have a new message from %s! Subject: %s\nTo view it, type /mail"
mail.read_later_message = "You can read your messages later by using the /mail command"
--[[
mail sending function, can be invoked with one object argument (new api) or
all 4 parameters (old compat version)
see: "Mail format" api.md
--]]
function mail.send(sender, receiver, subject, body)
local m
if receiver == nil and subject == nil and body == nil then
-- new format (one object param)
m = sender
else
-- old format
-- create mail from params
m = {}
m.sender = sender
m.receiver = receiver
m.subject = subject
m.body = body
end
m.unread = true
if not m.time then
-- add timestamp
m.time = os.time()
end
minetest.log("action", "[mail] '" .. m.sender .. "' sends mail to '" .. m.receiver ..
"' with subject '" .. m.subject .. "' and body: '" .. m.body .. "'")
local messages = mail.getMessages(m.receiver)
table.insert(messages, 1, m)
mail.setMessages(m.receiver, messages)
for _, player in ipairs(minetest.get_connected_players()) do
local name = player:get_player_name()
if name == m.receiver then
if m.subject == "" then m.subject = "(No subject)" end
if string.len(m.subject) > 30 then
m.subject = string.sub(m.subject,1,27) .. "..."
end
minetest.chat_send_player(m.receiver,
string.format(mail.receive_mail_message, m.sender, m.subject))
end
end
for i=1, #mail.registered_on_receives do
if mail.registered_on_receives[i](m) then
break
end
end
end

60
api.md Normal file
View File

@ -0,0 +1,60 @@
# Mail format
The mail format in the api hooks
```lua
mail = {
sender = "source name",
receiver = "destination name",
subject = "subject line",
body = "mail body",
-- 8 attachments max
attachments = {"default:stone 99", "default:gold_ingot 99"}
}
```
## Sending mail
Old variant (pre-1.1)
```lua
mail.send("source name", "destination name", "subject line", "mail body")
```
New variant (1.1+)
```lua
mail.send({
sender = "source name",
receiver = "destination name",
subject = "subject line",
body = "mail body"
})
```
# Hooks
On-receive mail hook:
```lua
mail.register_on_receive(function(m)
-- "m" is an object in the form: "Mail format"
end)
```
# internal mail format (on-disk)
The mail format on-disk
> (worldfolder)/mails/(playername).json
```json
[{
"unread": true,
"sender": "sender name",
"receiver": "receiver name",
"subject": "subject name",
"body": "main\nmultiline\nbody",
"time": 1551258349,
"attachments": [
"default:stone 99",
"default:gold_ingot 99"
]
}]
```

27
attachment.lua Normal file
View File

@ -0,0 +1,27 @@
local invmap = {}
mail.getAttachmentInventory = function(playername)
return invmap[playername]
end
mail.getAttachmentInventoryName = function(playername)
return "mail:" .. playername
end
minetest.register_on_joinplayer(function(player)
local name = player:get_player_name()
local inv = minetest.create_detached_inventory(mail.getAttachmentInventoryName(name), {})
invmap[name] = inv
end)
minetest.register_on_leaveplayer(function(player)
local name = player:get_player_name()
invmap[name] = nil
if minetest.remove_detached_inventory then
minetest.remove_detached_inventory(mail.getAttachmentInventoryName(name))
end
end)

6
chatcommands.lua Normal file
View File

@ -0,0 +1,6 @@
minetest.register_chatcommand("mail",{
description = "Open the mail interface",
func = function(name)
mail.show_inbox(name)
end
})

3
depends.txt Normal file
View File

@ -0,0 +1,3 @@
unified_inventory?
default?
xban2?

250
gui.lua Normal file
View File

@ -0,0 +1,250 @@
selected_message_idxs = {}
local theme
if minetest.get_modpath("default") then
theme = default.gui_bg .. default.gui_bg_img
else
theme = ""
end
mail.inbox_formspec = "size[8,9;]" .. theme .. [[
button_exit[7.5,0;0.5,0.5;quit;X]
button[6,1;2,0.5;new;New Message]
button[6,2;2,0.5;read;Read]
button[6,3;2,0.5;reply;Reply]
button[6,4;2,0.5;forward;Forward]
button[6,5;2,0.5;delete;Delete]
button[6,6;2,0.5;markread;Mark Read]
button[6,7;2,0.5;markunread;Mark Unread]
button[6,8;2,0.5;about;About]
tablecolumns[color;text;text]
table[0,0;5.75,9;messages;#999,From,Subject]]
function mail.show_about(name)
local formspec = [[
size[8,5;]
button[7.5,0;0.5,0.5;back;X]
label[0,0;Mail]
label[0,0.5;By cheapie]
label[0,1;http://github.com/cheapie/mail]
label[0,1.5;See LICENSE file for license information]
label[0,2.5;NOTE: Communication using this system]
label[0,3;is NOT guaranteed to be private!]
label[0,3.5;Admins are able to view the messages]
label[0,4;of any player.]
]] .. theme
minetest.show_formspec(name, "mail:about", formspec)
end
function mail.show_inbox(name)
local formspec = { mail.inbox_formspec }
local messages = mail.getMessages(name)
if messages[1] then
for idx, message in ipairs(messages) do
if message.unread then
formspec[#formspec + 1] = ",#FFD700"
else
formspec[#formspec + 1] = ","
end
formspec[#formspec + 1] = ","
formspec[#formspec + 1] = minetest.formspec_escape(message.sender)
formspec[#formspec + 1] = ","
if message.subject ~= "" then
if string.len(message.subject) > 30 then
formspec[#formspec + 1] =
minetest.formspec_escape(string.sub(message.subject, 1, 27))
formspec[#formspec + 1] = "..."
else
formspec[#formspec + 1] = minetest.formspec_escape(message.subject)
end
else
formspec[#formspec + 1] = "(No subject)"
end
end
if selected_message_idxs[name] then
formspec[#formspec + 1] = ";"
formspec[#formspec + 1] = tostring(selected_message_idxs[name] + 1)
end
formspec[#formspec + 1] = "]"
else
formspec[#formspec + 1] = "]label[2,4.5;No mail]"
end
minetest.show_formspec(name, "mail:inbox", table.concat(formspec, ""))
end
function mail.show_message(name, msgnumber)
local messages = mail.getMessages(name)
local message = messages[msgnumber]
local formspec = [[
size[8,7.2]
button[7,0;1,0.5;back;X]
label[0,0;From: %s]
label[0,0.5;Subject: %s]
textarea[0.25,1.25;8,6.25;body;;%s]
button[1,6.7;2,1;reply;Reply]
button[3,6.7;2,1;forward;Forward]
button[5,6.7;2,1;delete;Delete]
]] .. theme
local sender = minetest.formspec_escape(message.sender)
local subject = minetest.formspec_escape(message.subject)
local body = minetest.formspec_escape(message.body)
formspec = string.format(formspec, sender, subject, body)
minetest.show_formspec(name,"mail:message",formspec)
end
function mail.show_compose(name, defaulttgt, defaultsubj, defaultbody)
local formspec = [[
size[8,7.2]
field[0.25,0.5;4,1;to;To:;%s]
field[0.25,1.7;8,1;subject;Subject:;%s]
textarea[0.25,2.4;8,5;body;;%s]
button[0.5,6.7;3,1;cancel;Cancel]
button[7,0;1,0.5;cancel;X]
button[4.5,6.7;3,1;send;Send]
]] .. theme
formspec = string.format(formspec,
minetest.formspec_escape(defaulttgt),
minetest.formspec_escape(defaultsubj),
minetest.formspec_escape(defaultbody))
minetest.show_formspec(name, "mail:compose", formspec)
end
function mail.handle_receivefields(player, formname, fields)
if formname == "" and fields and fields.quit and minetest.get_modpath("unified_inventory") then
unified_inventory.set_inventory_formspec(player, "craft")
end
if formname == "mail:about" then
minetest.after(0.5, function()
mail.show_inbox(player:get_player_name())
end)
elseif formname == "mail:inbox" then
local name = player:get_player_name()
local messages = mail.getMessages(name)
if fields.messages then
local evt = minetest.explode_table_event(fields.messages)
selected_message_idxs[name] = evt.row - 1
if evt.type == "DCL" and messages[selected_message_idxs[name]] then
messages[selected_message_idxs[name]].unread = false
mail.show_message(name, selected_message_idxs[name])
end
mail.setMessages(name, messages)
return true
end
if fields.read then
if messages[selected_message_idxs[name]] then
messages[selected_message_idxs[name]].unread = false
mail.show_message(name, selected_message_idxs[name])
end
elseif fields.delete then
if messages[selected_message_idxs[name]] then
table.remove(messages, selected_message_idxs[name])
end
mail.show_inbox(name)
elseif fields.reply and messages[selected_message_idxs[name]] then
local message = messages[selected_message_idxs[name]]
local replyfooter = "Type your reply here.\n\n--Original message follows--\n" ..message.body
mail.show_compose(name, message.sender, "Re: "..message.subject,replyfooter)
elseif fields.forward and messages[selected_message_idxs[name]] then
local message = messages[selected_message_idxs[name]]
local fwfooter = "Type your message here.\n\n--Original message follows--\n" ..message.body
mail.show_compose(name, "", "Fw: "..message.subject, fwfooter)
elseif fields.markread then
if messages[selected_message_idxs[name]] then
messages[selected_message_idxs[name]].unread = false
end
mail.show_inbox(name)
elseif fields.markunread then
if messages[selected_message_idxs[name]] then
messages[selected_message_idxs[name]].unread = true
end
mail.show_inbox(name)
elseif fields.new then
mail.show_compose(name,"","","Type your message here.")
elseif fields.quit then
if minetest.get_modpath("unified_inventory") then
unified_inventory.set_inventory_formspec(player, "craft")
end
elseif fields.about then
mail.show_about(name)
end
mail.setMessages(name, messages)
return true
elseif formname == "mail:message" then
local name = player:get_player_name()
local messages = mail.getMessages(name)
if fields.back then
mail.show_inbox(name)
elseif fields.reply then
local message = messages[selected_message_idxs[name]]
local replyfooter = "Type your reply here.\n\n--Original message follows--\n" ..message.body
mail.show_compose(name, message.sender, "Re: "..message.subject, replyfooter)
elseif fields.forward then
local message = messages[selected_message_idxs[name]]
local fwfooter = "Type your message here.\n\n--Original message follows--\n" ..message.body
mail.show_compose(name, "", "Fw: "..message.subject, fwfooter)
elseif fields.delete then
if messages[selected_message_idxs[name]] then
table.remove(messages,selected_message_idxs[name])
end
mail.show_inbox(name)
end
mail.setMessages(name, messages)
return true
elseif formname == "mail:compose" then
if fields.send then
mail.send({
src = player:get_player_name(),
dst = fields.to,
subject = fields.subject,
body = fields.body
})
end
minetest.after(0.5, function()
mail.show_inbox(player:get_player_name())
end)
return true
elseif fields.mail then
mail.show_inbox(player:get_player_name())
else
return false
end
end
minetest.register_on_player_receive_fields(mail.handle_receivefields)
if minetest.get_modpath("unified_inventory") then
mail.receive_mail_message = mail.receive_mail_message ..
" or use the mail button in the inventory"
mail.read_later_message = mail.read_later_message ..
" or by using the mail button in the inventory"
unified_inventory.register_button("mail", {
type = "image",
image = "mail_button.png",
tooltip = "Mail"
})
end

59
hud.lua Normal file
View File

@ -0,0 +1,59 @@
local huddata = {}
minetest.register_on_joinplayer(function(player)
local name = player:get_player_name()
local data = {}
data.imageid = player:hud_add({
hud_elem_type = "image",
name = "MailIcon",
position = {x=0.52, y=0.52},
text="",
scale = {x=1,y=1},
alignment = {x=0.5, y=0.5},
})
data.textid = player:hud_add({
hud_elem_type = "text",
name = "MailText",
position = {x=0.55, y=0.52},
text= "",
scale = {x=1,y=1},
alignment = {x=0.5, y=0.5},
})
huddata[name] = data
end)
minetest.register_on_leaveplayer(function(player)
local name = player:get_player_name()
huddata[name] = nil
end)
mail.hud_update = function(playername, messages)
local data = huddata[playername]
local player = minetest.get_player_by_name(playername)
if not data or not player then
return
end
local unreadcount = 0
for _, message in ipairs(messages) do
if message.unread then
unreadcount = unreadcount + 1
end
end
if unreadcount == 0 then
player:hud_change(data.imageid, "text", "")
player:hud_change(data.textid, "text", "")
else
player:hud_change(data.imageid, "text", "email_mail.png")
player:hud_change(data.textid, "text", unreadcount .. " /mail")
end
end

55
init.lua Normal file
View File

@ -0,0 +1,55 @@
mail = {
-- mail directory
maildir = minetest.get_worldpath().."/mails",
-- allow item/node attachments
allow_attachments = minetest.settings:get("mail.allow_attachments") == "true",
webmail = {
-- disallow banned players in the webmail interface
disallow_banned_players = minetest.settings:get("webmail.disallow_banned_players") == "true",
-- url and key to the webmail server
url = minetest.settings:get("webmail.url"),
key = minetest.settings:get("webmail.key")
},
tan = {}
}
local MP = minetest.get_modpath(minetest.get_current_modname())
dofile(MP .. "/chatcommands.lua")
dofile(MP .. "/migrate.lua")
dofile(MP .. "/attachment.lua")
dofile(MP .. "/hud.lua")
dofile(MP .. "/storage.lua")
dofile(MP .. "/api.lua")
dofile(MP .. "/gui.lua")
dofile(MP .. "/onjoin.lua")
-- optional webmail stuff below
--[[ minetest.conf
secure.http_mods = mail
webmail.url = http://127.0.0.1:8080
webmail.key = myserverkey
--]]
local http = minetest.request_http_api()
if http then
local webmail_url = mail.webmail.url
local webmail_key = mail.webmail.key
if not webmail_url then error("webmail.url is not defined") end
if not webmail_key then error("webmail.key is not defined") end
print("[mail] loading webmail-component with endpoint: " .. webmail_url)
dofile(MP .. "/tan.lua")
dofile(MP .. "/webmail.lua")
mail.webmail_init(http, webmail_url, webmail_key)
end
-- migrate storage
mail.migrate()

24
migrate.lua Normal file
View File

@ -0,0 +1,24 @@
-- migrate from mail.db to player-file-based mailbox
mail.migrate = function()
local file = io.open(minetest.get_worldpath().."/mail.db", "r")
if file then
print("[mail] migrating to new per-player storage")
minetest.mkdir(mail.maildir)
local data = file:read("*a")
local oldmails = minetest.deserialize(data)
file:close()
for name, oldmessages in pairs(oldmails) do
mail.setMessages(name, oldmessages)
end
-- rename file
print("[mail] migration done, renaming old mail.db")
os.rename(minetest.get_worldpath().."/mail.db", minetest.get_worldpath().."/mail.db.old")
end
end

19
onjoin.lua Normal file
View File

@ -0,0 +1,19 @@
minetest.register_on_joinplayer(function(player)
minetest.after(2, function(name)
local messages = mail.getMessages(name)
local unreadcount = 0
for _, message in pairs(messages) do
if message.unread then
unreadcount = unreadcount + 1
end
end
if unreadcount > 0 then
minetest.chat_send_player(name,
"(" .. unreadcount .. ") You have mail! Type /mail to read")
end
end, player:get_player_name())
end)

32
storage.lua Normal file
View File

@ -0,0 +1,32 @@
-- TODO: maybe local cache?
function getMailFile(playername)
local saneplayername = string.gsub(playername, "[.|/]", "")
return mail.maildir .. "/" .. saneplayername .. ".json"
end
mail.getMessages = function(playername)
local file = io.open(getMailFile(playername), "r")
local messages = {}
if file then
local json = file:read("*a")
messages = minetest.parse_json(json or "[]") or {}
mail.hud_update(playername, messages)
file:close()
end
return messages
end
mail.setMessages = function(playername, messages)
local file = io.open(getMailFile(playername),"w")
local json = minetest.write_json(messages)
if file and file:write(json) and file:close() then
mail.hud_update(playername, messages)
return true
else
minetest.log("error","[mail] Save failed - messages may be lost!")
return false
end
end

16
tan.lua Normal file
View File

@ -0,0 +1,16 @@
minetest.register_chatcommand("webmail_tan", {
description = "generates a tan (temporary access number) for the webmail access",
func = function(name)
local tan = "" .. math.random(1000, 9999)
mail.tan[name] = tan
return true, "Your tan is " .. tan .. ", it will expire upon leaving the game"
end
})
minetest.register_on_leaveplayer(function(player)
local name = player:get_player_name()
mail.tan[name] = nil
end)

BIN
textures/email_mail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

BIN
textures/mail_button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

93
util/channel.lua Normal file
View File

@ -0,0 +1,93 @@
-- bi-directional http-channel
-- with long-poll GET and POST on the same URL
local debug = false
local function Channel(http, url, cfg)
cfg = cfg or {}
local extra_headers = cfg.extra_headers or {}
local timeout = cfg.timeout or 1
local long_poll_timeout = cfg.long_poll_timeout or 30
local error_retry = cfg.error_retry or 10
-- assemble post-header with json content
local post_headers = { "Content-Type: application/json" }
for _,header in pairs(cfg.extra_headers) do
table.insert(post_headers, header)
end
local recv_listeners = {}
local run = true
local recv_loop
recv_loop = function()
assert(run)
-- long-poll GET
http.fetch({
url = url,
extra_headers = extra_headers,
timeout = long_poll_timeout
}, function(res)
if res.succeeded and res.code == 200 then
local data = minetest.parse_json(res.data)
if debug then
minetest.log("action", "[webmail-rx] " .. dump(data))
end
if data then
for _,listener in pairs(recv_listeners) do
listener(data)
end
end
-- reschedule immediately
minetest.after(0, recv_loop)
else
-- error, retry after some time
minetest.after(error_retry, recv_loop)
end
end)
end
local send = function(data)
assert(run)
-- POST
if debug then
minetest.log("action", "[webmail-tx] " .. dump(data))
end
http.fetch({
url = url,
extra_headers = post_headers,
timeout = timeout,
post_data = minetest.write_json(data)
}, function(res)
-- TODO: error-handling
end)
end
local receive = function(listener)
table.insert(recv_listeners, listener)
end
local close = function()
run = false
end
recv_loop();
return {
send = send,
receive = receive,
close = close
}
end
return Channel

156
webmail.lua Normal file
View File

@ -0,0 +1,156 @@
-- false per default
local has_xban2_mod = minetest.get_modpath("xban2")
local MP = minetest.get_modpath(minetest.get_current_modname())
local Channel = dofile(MP .. "/util/channel.lua")
local channel
-- auth request from webmail
local function auth_handler(data)
local auth = data.params
local handler = minetest.get_auth_handler()
minetest.log("action", "[webmail] auth: " .. auth.playername)
local success = false
local banned = false
local message = ""
if mail.webmail.disallow_banned_players and has_xban2_mod then
-- check xban db
local xbanentry = xban.find_entry(auth.playername)
if xbanentry and xbanentry.banned then
banned = true
message = "Banned!"
end
end
if not banned then
-- check tan
local tan = mail.tan[auth.playername]
if tan ~= nil then
success = tan == auth.password
end
-- check auth
if not success then
local entry = handler.get_auth(auth.playername)
if entry and minetest.check_password_entry(auth.playername, entry.password, auth.password) then
success = true
end
end
end
channel.send({
method = data.method,
id = data.id,
result = {
success = success,
message = message
}
})
end
-- send request from webmail
local function send_handler(data)
-- send mail from webclient
if not data.params then
return
end
minetest.log("action", "[webmail] sending mail from webclient: " .. data.params.sender ..
" -> " .. data.params.receiver)
mail.send(data.params)
channel.send({
method = data.method,
id = data.id,
result = {
success = true
}
})
end
-- get player messages request from webmail
local function get_player_messages_handler(data)
local messages = mail.getMessages(data.params.playername)
channel.send({
method = data.method,
id = data.id,
result = messages
})
end
-- remove mail
local function delete_mail_handler(data)
local index = data.params.index
local playername = data.params.playername
local messages = mail.getMessages(playername)
if messages[index] then
table.remove(messages, index)
end
mail.setMessages(playername, messages)
-- TODO: check subject
channel.send({
method = data.method,
id = data.id,
result = { success = true }
})
end
-- mark mail as read
local function mark_mail_read_handler(data)
local index = data.params.index
local playername = data.params.playername
local read = data.params.read
local messages = mail.getMessages(playername)
if messages[index] then
messages[index].unread = not read
end
mail.setMessages(playername, messages)
-- TODO: check subject
channel.send({
method = data.method,
id = data.id,
result = { success = true }
})
end
function mail.webmail_send_hook(m)
channel.send({
type = "new-message",
data = m
})
end
mail.register_on_receive(mail.webmail_send_hook)
function mail.webmail_init(http, url, key)
channel = Channel(http, url .. "/api/minetest/channel", {
extra_headers = { "webmailkey: " .. key }
})
channel.receive(function(data)
if data.method == "auth" then
auth_handler(data)
elseif data.method == "get-mails" then
get_player_messages_handler(data)
elseif data.method == "mark-mail-read" then
mark_mail_read_handler(data)
elseif data.method == "delete-mail" then
delete_mail_handler(data)
elseif data.method == "send" then
send_handler(data)
end
end)
end