diff --git a/api.lua b/api.lua index a8d7e30..c480f7c 100644 --- a/api.lua +++ b/api.lua @@ -14,6 +14,7 @@ all 4 parameters (old compat version) see: "Mail format" api.md --]] function mail.send(src, dst, subject, body) + -- figure out format local m if dst == nil and subject == nil and body == nil then -- new format (one object param) @@ -21,36 +22,78 @@ function mail.send(src, dst, subject, body) else -- old format m = {} - m.src = src - m.dst = dst + m.from = src + m.to = dst m.subject = subject m.body = body end + local cc + local bcc + local extra + -- log mail send action + if m.cc or m.bcc then + if m.cc then + cc = "CC: " .. m.cc + if m.bcc then + cc = cc .. " - " + end + else + cc = "" + end + if m.bcc then + bcc = "BCC: " .. m.bcc + else + bcc = "" + end + extra = " (" .. cc .. bcc .. ")" + else + extra = "" + end + minetest.log("action", "[mail] '" .. m.from .. "' sends mail to '" .. m.to .. "'" .. + extra .. "' with subject '" .. m.subject .. "' and body: '" .. m.body .. "'") - minetest.log("action", "[mail] '" .. m.src .. "' sends mail to '" .. m.dst .. - "' with subject '" .. m.subject .. "' and body: '" .. m.body .. "'") - local messages = mail.getMessages(m.dst) + -- normalize to, cc and bcc while compiling a list of all recipients + local recipients = {} + m.to = mail.normalize_players_and_add_recipients(m.to, recipients) + if m.cc then + m.cc = mail.normalize_players_and_add_recipients(m.cc, recipients) + end + if m.bcc then + m.bcc = mail.normalize_players_and_add_recipients(m.bcc, recipients) + end - table.insert(messages, 1, { + -- form the actual mail + local msg = { unread = true, - sender = m.src, + sender = m.from, + to = m.to, subject = m.subject, body = m.body, time = os.time(), - }) - mail.setMessages(m.dst, messages) + } + if m.cc then + msg.cc = m.cc + end + -- send the mail to all recipients + for _, recipient in pairs(recipients) do + local messages = mail.getMessages(recipient) + table.insert(messages, 1, msg) + mail.setMessages(recipient, messages) + end + + -- notify recipients that happen to be online for _, player in ipairs(minetest.get_connected_players()) do local name = player:get_player_name() - if name == m.dst then + if recipients[string.lower(name)] ~= nil 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.dst, - string.format(mail.receive_mail_message, m.src, m.subject)) + minetest.chat_send_player(name, + string.format(mail.receive_mail_message, m.from, m.subject)) end end diff --git a/api.md b/api.md index 5d059c2..984cd28 100644 --- a/api.md +++ b/api.md @@ -2,6 +2,25 @@ # Mail format The mail format in the api hooks +```lua +mail = { + from = "sender name", + to = "players, which, are, addressed", + cc = "carbon copy", + bcc = "players, which, get, a, copy, but, are, not, visible, to, others", + subject = "subject line", + body = "mail body", + -- 8 attachments max + attachments = {"default:stone 99", "default:gold_ingot 99"} +} +``` + +The fields `to`, `cc` and `bcc` can contain a player, multiple player names separated by commas, or be empty. Players in `to` are the recipiants, who are addressed directly. `cc` specifies players that get the mail to get notified, but are not immediate part of the conversation. There is no technical difference between `to` and `cc`, it just implies meaning for the players. Players can see all fields making up the mail except `bcc`, which is the only difference to `cc`. + +Attachments need to be provided for each player getting the mail. Until this is implemented, trying to send a mail to multiple players will fail. + +The `from` and `to` fields were renamed from the previous format: + ```lua mail = { src = "source name", @@ -22,8 +41,10 @@ mail.send("source name", "destination name", "subject line", "mail body") New variant (1.1+) ```lua mail.send({ - src = "source name", - dst = "destination name", + from = "sender name", + to = "destination name", + cc = "carbon copy", + bcc = "blind carbon copy", subject = "subject line", body = "mail body" }) diff --git a/gui.lua b/gui.lua index f608b85..576a1d5 100644 --- a/gui.lua +++ b/gui.lua @@ -1,4 +1,12 @@ -selected_message_idxs = {} +-- refactor these to some proper management thing +selected_idxs = { + messages = {}, + contacts = {}, + to = {}, + cc = {}, + bcc = {}, +} +message_drafts = {} local theme if minetest.get_modpath("default") then @@ -8,23 +16,48 @@ else 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] + button[6,0.10;2,0.5;new;New] + button[6,0.95;2,0.5;read;Read] + button[6,1.70;2,0.5;reply;Reply] + button[6,2.45;2,0.5;replyall;Reply All] + button[6,3.20;2,0.5;forward;Forward] + button[6,3.95;2,0.5;delete;Delete] + button[6,4.82;2,0.5;markread;Mark Read] + button[6,5.55;2,0.5;markunread;Mark Unread] + button[6,6.55;2,0.5;contacts;Contacts] + button[6,7.40;2,0.5;about;About] + button_exit[6,8.45;2,0.5;quit;Close] + tablecolumns[color;text;text] table[0,0;5.75,9;messages;#999,From,Subject]] +mail.contacts_formspec = "size[8,9;]" .. theme .. [[ + button[6,0.10;2,0.5;new;New] + button[6,0.85;2,0.5;edit;Edit] + button[6,1.60;2,0.5;delete;Delete] + button[6,8.25;2,0.5;back;Back] + tablecolumns[color;text;text] + table[0,0;5.75,9;contacts;#999,Name,Note]] + +mail.select_contact_formspec = "size[8,9;]" .. theme .. [[ + tablecolumns[color;text;text] + table[0,0;3.5,9;contacts;#999,Name,Note%s] + button[3.55,2.00;1.75,0.5;toadd;→ Add] + button[3.55,2.75;1.75,0.5;toremove;← Remove] + button[3.55,6.00;1.75,0.5;ccadd;→ Add] + button[3.55,6.75;1.75,0.5;ccremove;← Remove] + tablecolumns[color;text;text] + table[5.15,0.0;2.75,4.5;to;#999,TO:,Note%s] + tablecolumns[color;text;text] + table[5.15,4.6;2.75,4.5;cc;#999,CC:,Note%s] + button[3.55,8.25;1.75,0.5;back;Back] + ]] + function mail.show_about(name) local formspec = [[ size[8,5;] - button[7.5,0;0.5,0.5;back;X] + button[7.25,0;0.75,0.5;back;X] label[0,0;Mail] label[0,0.5;By cheapie] label[0,1;http://github.com/cheapie/mail] @@ -42,12 +75,23 @@ function mail.show_inbox(name) local formspec = { mail.inbox_formspec } local messages = mail.getMessages(name) + message_drafts[name] = nil + if messages[1] then for _, message in ipairs(messages) do + mail.ensure_new_format(message, name) if message.unread then - formspec[#formspec + 1] = ",#FFD700" + if not mail.player_in_list(name, message.to) then + formspec[#formspec + 1] = ",#FFD788" + else + formspec[#formspec + 1] = ",#FFD700" + end else - formspec[#formspec + 1] = "," + if not mail.player_in_list(name, message.to) then + formspec[#formspec + 1] = ",#CCCCDD" + else + formspec[#formspec + 1] = "," + end end formspec[#formspec + 1] = "," formspec[#formspec + 1] = minetest.formspec_escape(message.sender) @@ -64,58 +108,251 @@ function mail.show_inbox(name) formspec[#formspec + 1] = "(No subject)" end end - if selected_message_idxs[name] then + if selected_idxs.messages[name] then formspec[#formspec + 1] = ";" - formspec[#formspec + 1] = tostring(selected_message_idxs[name] + 1) + formspec[#formspec + 1] = tostring(selected_idxs.messages[name] + 1) end formspec[#formspec + 1] = "]" else - formspec[#formspec + 1] = "]label[2,4.5;No mail]" + formspec[#formspec + 1] = "]label[2.25,4.5;No mail]" end minetest.show_formspec(name, "mail:inbox", table.concat(formspec, "")) end +function mail.show_contacts(name) + local formspec = mail.contacts_formspec .. mail.compile_contact_list(name, selected_idxs.contacts[name]) + minetest.show_formspec(name, "mail:contacts", formspec) +end + +function mail.show_edit_contact(name, contact_name, note, illegal_name_hint) + local formspec = [[ + size[6,7] + button[4,6.25;2,0.5;back;Back] + field[0.25,0.5;4,1;name;Player name:;%s] + textarea[0.25,1.6;4,6.25;note;Note:;%s] + button[4,0.10;2,1;save;Save] + ]] + if illegal_name_hint == "collision" then + formspec = formspec .. [[ + label[4,1;That name] + label[4,1.5;is already in] + label[4,2;your contacts.] + ]] + elseif illegal_name_hint == "empty" then + formspec = formspec .. [[ + label[4,1;The contact] + label[4,1.5;name cannot] + label[4,2;be empty.] + ]] + end + formspec = formspec .. theme + formspec = string.format(formspec, + minetest.formspec_escape(contact_name or ""), + minetest.formspec_escape(note or "")) + minetest.show_formspec(name, "mail:editcontact", formspec) +end + +function mail.show_select_contact(name, to, cc, bcc) + local formspec = mail.select_contact_formspec + local contacts = mail.compile_contact_list(name, selected_idxs.contacts[name]) + + -- compile lists + if to then + to = mail.compile_contact_list(name, selected_idxs.to[name], to) + else + to = "" + end + if cc then + cc = mail.compile_contact_list(name, selected_idxs.cc[name], cc) + else + cc = "" + end + --[[if bcc then + bcc = table.concat(mail.compile_contact_list(name, selected_idxs.bcc[name], bcc) + else + bcc = "" + end]]-- + formspec = string.format(formspec, contacts, to, cc)--, bcc() + minetest.show_formspec(name, "mail:selectcontact", formspec) +end + +function mail.compile_contact_list(name, selected, playernames) + -- TODO: refactor this - not just compiles *a* list, but *the* list for the contacts screen (too inflexible) + local formspec = {} + local contacts = mail.getContacts(name) + + if playernames == nil then + local length = 0 + for k, contact, i, l in pairsByKeys(contacts) do + if i == 1 then length = l end + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = minetest.formspec_escape(contact.name) + formspec[#formspec + 1] = "," + local note = contact.note + -- display an ellipsis if the note spans multiple lines + local idx = string.find(note, '\n') + if idx ~= nil then + note = string.sub(note, 1, idx-1) .. ' ...' + end + formspec[#formspec + 1] = minetest.formspec_escape(note) + if type(selected) == "string" then + if string.lower(selected) == k then + selected = i + end + end + end + if length > 0 then + if selected and type(selected) == "number" then + formspec[#formspec + 1] = ";" + formspec[#formspec + 1] = tostring(selected + 1) + end + formspec[#formspec + 1] = "]" + else + formspec[#formspec + 1] = "]label[2,4.5;No contacts]" + end + else + if type(playernames) == "string" then + playernames = mail.parse_player_list(playernames) + end + for i,c in ipairs(playernames) do + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = minetest.formspec_escape(c) + formspec[#formspec + 1] = "," + if contacts[string.lower(c)] == nil then + formspec[#formspec + 1] = "" + else + local note = contacts[string.lower(c)].note + -- display an ellipsis if the note spans multiple lines + local idx = string.find(note, '\n') + if idx ~= nil then + note = string.sub(note, 1, idx-1) .. ' ...' + end + formspec[#formspec + 1] = minetest.formspec_escape(note) + end + if not selected then + if type(selected) == "string" then + if string.lower(selected) == string.lower(c) then + selected = i + end + end + end + end + if #playernames > 0 and selected and type(selected) == "number" then + formspec[#formspec + 1] = ";" + formspec[#formspec + 1] = tostring(selected + 1) + end + formspec[#formspec + 1] = "]" + end + return 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] + size[8,9] + button[7.25,0;0.75,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] + label[0,0.4;To: %s] + label[0,0.8;CC: %s] + label[0,1.3;Subject: %s] + textarea[0.25,1.8;8,7.8;body;;%s] + button[0,8.5;2,1;reply;Reply] + button[2,8.5;2,1;replyall;Reply All] + button[4,8.5;2,1;forward;Forward] + button[6,8.5;2,1;delete;Delete] ]] .. theme - local sender = minetest.formspec_escape(message.sender) + local from = minetest.formspec_escape(message.sender) + local to = minetest.formspec_escape(message.to) + local cc = minetest.formspec_escape(message.cc) local subject = minetest.formspec_escape(message.subject) local body = minetest.formspec_escape(message.body) - formspec = string.format(formspec, sender, subject, body) + formspec = string.format(formspec, from, to, cc, subject, body) + + if message.unread then + message.unread = false + mail.setMessages(name, messages) + end minetest.show_formspec(name,"mail:message",formspec) end -function mail.show_compose(name, defaulttgt, defaultsubj, defaultbody) +function mail.show_compose(name, defaultto, defaultsubj, defaultbody, defaultcc, defaultbcc) 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] + size[8,9] + button[0,0;1,1;tocontacts;To:] + field[1.1,0.3;3.2,1;to;;%s] + button[4,0;1,1;cccontacts;CC:] + field[5.1,0.3;3.1,1;cc;;%s] + button[4,0.75;1,1;bcccontacts;BCC:] + field[5.1,1.05;3.1,1;bcc;;%s] + field[0.25,2;8,1;subject;Subject:;%s] + textarea[0.25,2.5;8,6;body;;%s] + button[0.5,8.5;3,1;cancel;Cancel] + button[4.5,8.5;3,1;send;Send] ]] .. theme + defaultto = defaultto or "" + defaultsubj = defaultsubj or "" + defaultbody = defaultbody or "" + defaultcc = defaultcc or "" + defaultbcc = defaultbcc or "" + formspec = string.format(formspec, - minetest.formspec_escape(defaulttgt), + minetest.formspec_escape(defaultto), + minetest.formspec_escape(defaultcc), + minetest.formspec_escape(defaultbcc), minetest.formspec_escape(defaultsubj), minetest.formspec_escape(defaultbody)) minetest.show_formspec(name, "mail:compose", formspec) end +function mail.reply(name, message) + mail.ensure_new_format(message) + local replyfooter = "Type your reply here.\n\n--Original message follows--\n" ..message.body + mail.show_compose(name, message.sender, "Re: "..message.subject, replyfooter) +end + +function mail.replyall(name, message) + mail.ensure_new_format(message) + local replyfooter = "Type your reply here.\n\n--Original message follows--\n" ..message.body + + -- new recipients are the sender plus the original recipients, minus ourselves + local recipients = message.to or "" + if message.sender ~= nil then + recipients = message.sender .. ", " .. recipients + end + recipients = mail.parse_player_list(recipients) + for k,v in pairs(recipients) do + if v == name then + table.remove(recipients, k) + break + end + end + recipients = mail.concat_player_list(recipients) + + -- new CC is old CC minus ourselves + local cc = mail.parse_player_list(message.cc) + for k,v in pairs(cc) do + if v == name then + table.remove(cc, k) + break + end + end + cc = mail.concat_player_list(cc) + + mail.show_compose(name, recipients, "Re: "..message.subject, replyfooter, cc) +end + +function mail.forward(name, message) + local fwfooter = "Type your message here.\n\n--Original message follows--\n" ..message.body + mail.show_compose(name, "", "Fw: "..message.subject, fwfooter) +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") @@ -132,50 +369,59 @@ function mail.handle_receivefields(player, formname, fields) 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]) + selected_idxs.messages[name] = evt.row - 1 + if evt.type == "DCL" and messages[selected_idxs.messages[name]] then + mail.show_message(name, selected_idxs.messages[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]) + if messages[selected_idxs.messages[name]] then + mail.show_message(name, selected_idxs.messages[name]) end elseif fields.delete then - if messages[selected_message_idxs[name]] then - table.remove(messages, selected_message_idxs[name]) + if messages[selected_idxs.messages[name]] then + table.remove(messages, selected_idxs.messages[name]) + mail.setMessages(name, messages) 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.reply and messages[selected_idxs.messages[name]] then + local message = messages[selected_idxs.messages[name]] + mail.reply(name, message) + + elseif fields.replyall and messages[selected_idxs.messages[name]] then + local message = messages[selected_idxs.messages[name]] + mail.replyall(name, message) - 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.forward and messages[selected_idxs.messages[name]] then + local message = messages[selected_idxs.messages[name]] + mail.forward(name, message) elseif fields.markread then - if messages[selected_message_idxs[name]] then - messages[selected_message_idxs[name]].unread = false + if messages[selected_idxs.messages[name]] then + messages[selected_idxs.messages[name]].unread = false + -- set messages immediately, so it shows up already when updating the inbox + mail.setMessages(name, messages) end mail.show_inbox(name) + return true elseif fields.markunread then - if messages[selected_message_idxs[name]] then - messages[selected_message_idxs[name]].unread = true + if messages[selected_idxs.messages[name]] then + messages[selected_idxs.messages[name]].unread = true + -- set messages immediately, so it shows up already when updating the inbox + mail.setMessages(name, messages) end mail.show_inbox(name) + return true elseif fields.new then - mail.show_compose(name,"","","Type your message here.") + mail.show_compose(name) + + elseif fields.contacts then + mail.show_contacts(name) elseif fields.quit then if minetest.get_modpath("unified_inventory") then @@ -187,7 +433,6 @@ function mail.handle_receivefields(player, formname, fields) end - mail.setMessages(name, messages) return true elseif formname == "mail:message" then local name = player:get_player_name() @@ -195,37 +440,231 @@ function mail.handle_receivefields(player, formname, fields) if fields.back then mail.show_inbox(name) + return true -- don't uselessly set messages 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) + local message = messages[selected_idxs.messages[name]] + mail.reply(name, message) + elseif fields.replyall then + local message = messages[selected_idxs.messages[name]] + mail.replyall(name, message) 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) + local message = messages[selected_idxs.messages[name]] + mail.forward(name, message.subject) elseif fields.delete then - if messages[selected_message_idxs[name]] then - table.remove(messages,selected_message_idxs[name]) + if messages[selected_idxs.messages[name]] then + table.remove(messages,selected_idxs.messages[name]) + mail.setMessages(name, messages) end mail.show_inbox(name) end - - mail.setMessages(name, messages) return true + elseif formname == "mail:compose" then + local name = player:get_player_name() if fields.send then mail.send({ - src = player:get_player_name(), - dst = fields.to, + from = name, + to = fields.to, + cc = fields.cc, + bcc = fields.bcc, subject = fields.subject, - body = fields.body + body = fields.body, }) + local contacts = mail.getContacts(name) + local recipients = mail.parse_player_list(fields.to) + local changed = false + for _,v in pairs(recipients) do + if contacts[string.lower(v)] == nil then + contacts[string.lower(v)] = { + name = v, + note = "", + } + changed = true + end + end + if changed then + mail.setContacts(name, contacts) + end + + minetest.after(0.5, function() + mail.show_inbox(name) + end) + + elseif fields.tocontacts or fields.cccontacts or fields.bcccontacts then + message_drafts[name] = { + to = fields.to, + cc = fields.cc, + bcc = fields.bcc, + subject = fields.subject, + body = fields.body, + } + mail.show_select_contact(name, fields.to, fields.cc, fields.bcc) + elseif fields.cancel then + message_drafts[name] = nil + mail.show_inbox(name) end - minetest.after(0.5, function() - mail.show_inbox(player:get_player_name()) - end) return true + elseif formname == "mail:selectcontact" then + local name = player:get_player_name() + local contacts = mail.getContacts(name) + local draft = message_drafts[name] + + -- get indexes for fields with selected rows + -- execute their default button's actions if double clicked + for k,action in pairs({ + contacts = "toadd", + to = "toremove", + cc = "ccremove", + bcc = "bccremove" + }) do + if fields[k] then + local evt = minetest.explode_table_event(fields[k]) + selected_idxs[k][name] = evt.row - 1 + if evt.type == "DCL" and selected_idxs[k][name] then + fields[action] = true + end + return true + end + end + + local update = false + -- add + for _,v in pairs({"to","cc","bcc"}) do + if fields[v.."add"] then + update = true + if selected_idxs.contacts[name] then + for k, contact, i in pairsByKeys(contacts) do + if k == selected_idxs.contacts[name] or i == selected_idxs.contacts[name] then + local list = mail.parse_player_list(draft[v]) + list[#list+1] = contact.name + selected_idxs[v][name] = #list + draft[v] = mail.concat_player_list(list) + break + end + end + end + end + end + -- remove + for _,v in pairs({"to","cc","bcc"}) do + if fields[v.."remove"] then + update = true + if selected_idxs[v][name] then + local list = mail.parse_player_list(draft[v]) + table.remove(list, selected_idxs[v][name]) + if #list < selected_idxs[v][name] then + selected_idxs[v][name] = #list + end + draft[v] = mail.concat_player_list(list) + end + end + end + if update then + mail.show_select_contact(name, draft.to, draft.cc, draft.bcc) + return true + end + + if fields.back then + -- just do the default stuff below, as ESC will + end + + -- delete old idxs + for _,v in ipairs({"contacts","to","cc","bcc"}) do + selected_idxs[v][name] = nil + end + mail.show_compose(name, draft.to, draft.subject, draft.body, draft.cc, draft.bcc) + return true + + elseif formname == "mail:contacts" then + local name = player:get_player_name() + local contacts = mail.getContacts(name) + + if fields.contacts then + local evt = minetest.explode_table_event(fields.contacts) + for k,c,i in pairsByKeys(contacts) do + if i == evt.row - 1 then + selected_idxs.contacts[name] = k + break + end + end + if evt.type == "DCL" and contacts[selected_idxs.contacts[name]] then + mail.show_edit_contact(name, contacts[selected_idxs.contacts[name]].name, contacts[selected_idxs.contacts[name]].note) + end + return true + elseif fields.new then + selected_idxs.contacts[name] = "#NEW#" + mail.show_edit_contact(name, "", "") + elseif fields.edit then + mail.show_edit_contact(name, contacts[selected_idxs.contacts[name]].name, contacts[selected_idxs.contacts[name]].note) + elseif fields.delete then + if contacts[selected_idxs.contacts[name]] then + -- delete the contact and set the selected to the next in the list, except if it was the last. Then determine the new last + local found = false + local last = nil + for k,v,i in pairsByKeys(contacts) do + if found then + selected_idxs.contacts[name] = k + break + elseif k == selected_idxs.contacts[name] then + contacts[selected_idxs.contacts[name]] = nil + selected_idxs.contacts[name] = nil + found = true + else + last = k + end + end + if found and not selected_idxs.contacts[name] then + -- was the last in the list, so take the previous (new last) + selected_idxs.contacts[name] = last + end + + mail.setContacts(name, contacts) + end + + mail.show_contacts(name) + + elseif fields.back then + mail.show_inbox(name) + + end + elseif formname == "mail:editcontact" then + local name = player:get_player_name() + local contacts = mail.getContacts(name) + + if fields.save then + if selected_idxs.contacts[name] and selected_idxs.contacts[name] ~= "#NEW#" then + local contact = contacts[selected_idxs.contacts[name]] + if selected_idxs.contacts[name] ~= string.lower(fields.name) then + -- name changed! + if #fields.name == 0 then + mail.show_edit_contact(name, contact.name, fields.note, "empty") + return true + elseif contacts[string.lower(fields.name)] ~= nil then + mail.show_edit_contact(name, contact.name, fields.note, "collision") + return true + else + contacts[string.lower(fields.name)] = contact + contacts[selected_idxs.contacts[name]] = nil + end + end + contact.name = fields.name + contact.note = fields.note + else + local contact = { + name = fields.name, + note = fields.note, + } + contacts[string.lower(contact.name)] = contact + end + mail.setContacts(name, contacts) + mail.show_contacts(name) + + elseif fields.back then + mail.show_contacts(name) + + end + elseif fields.mail then mail.show_inbox(player:get_player_name()) else diff --git a/init.lua b/init.lua index e35875b..4423265 100644 --- a/init.lua +++ b/init.lua @@ -8,6 +8,7 @@ mail = { -- mail directory maildir = minetest.get_worldpath().."/mails", + contactsdir = minetest.get_worldpath().."/mails/contacts", -- allow item/node attachments allow_attachments = minetest.settings:get("mail.allow_attachments") == "true", @@ -26,6 +27,7 @@ mail = { local MP = minetest.get_modpath(minetest.get_current_modname()) +dofile(MP .. "/util/normalize.lua") dofile(MP .. "/chatcommands.lua") dofile(MP .. "/migrate.lua") dofile(MP .. "/attachment.lua") diff --git a/migrate.lua b/migrate.lua index 6ce5dde..66dc6b2 100644 --- a/migrate.lua +++ b/migrate.lua @@ -4,6 +4,7 @@ mail.migrate = function() -- create directory, just in case minetest.mkdir(mail.maildir) + minetest.mkdir(mail.contactsdir) local file = io.open(minetest.get_worldpath().."/mail.db", "r") if file then @@ -23,3 +24,27 @@ mail.migrate = function() end end + + +mail.migrate_contacts = function(playername) + local file = io.open(mail.getContactsFile(playername), 'r') + if not file then + -- file doesn't exist! This is a case for Migrate Man! + local messages = mail.getMessages(playername) + local contacts = {} + + if messages and not contacts then + for k,message in pairs(messages) do + mail.ensure_new_format(message) + if contacts[string.lower(message.from)] == nil then + contacts[string.lower(message.from)] = { + name = message.from, + note = "", + } + end + end + end + else + file:close() -- uh, um, nope, let's leave those alone, shall we? + end +end diff --git a/onjoin.lua b/onjoin.lua index 79ad287..2433540 100644 --- a/onjoin.lua +++ b/onjoin.lua @@ -16,4 +16,6 @@ minetest.register_on_joinplayer(function(player) end end, player:get_player_name()) + + mail.migrate_contacts(player:get_player_name()) end) diff --git a/storage.lua b/storage.lua index 8f50a74..0aaff28 100644 --- a/storage.lua +++ b/storage.lua @@ -1,32 +1,87 @@ -- TODO: maybe local cache? -function getMailFile(playername) +function mail.getMailFile(playername) local saneplayername = string.gsub(playername, "[.|/]", "") return mail.maildir .. "/" .. saneplayername .. ".json" end +function mail.getContactsFile(playername) + local saneplayername = string.gsub(playername, "[.|/]", "") + return mail.maildir .. "/contacts/" .. 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 {} + local messages = mail.read_json_file(mail.getMailFile(playername)) + if messages then 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 + if mail.write_json_file(mail.getMailFile(playername), messages) then mail.hud_update(playername, messages) return true else - minetest.log("error","[mail] Save failed - messages may be lost!") + minetest.log("error","[mail] Save failed - messages may be lost! ("..playername..")") + return false + end +end + + +mail.getContacts = function(playername) + return mail.read_json_file(mail.getContactsFile(playername)) +end + +function pairsByKeys(t, f) + -- http://www.lua.org/pil/19.3.html + local a = {} + for n in pairs(t) do table.insert(a, n) end + table.sort(a, f) + local i = 0 -- iterator variable + local iter = function() -- iterator function + i = i + 1 + if a[i] == nil then + return nil + else + --return a[i], t[a[i]] + -- add the current position and the length for convenience + return a[i], t[a[i]], i, #a + end + end + return iter +end + +mail.setContacts = function(playername, contacts) + if mail.write_json_file(mail.getContactsFile(playername), contacts) then + return true + else + minetest.log("error","[mail] Save failed - contacts may be lost! ("..playername..")") + return false + end +end + + +function mail.read_json_file(path) + local file = io.open(path, "r") + local content = {} + if file then + local json = file:read("*a") + content = minetest.parse_json(json or "[]") or {} + file:close() + end + return content +end + +function mail.write_json_file(path, content) + local file = io.open(path,"w") + local json = minetest.write_json(content) + if file and file:write(json) and file:close() then + return true + else return false end end diff --git a/test/auth.sqlite b/test/auth.sqlite index eaf402d..e3f8829 100644 Binary files a/test/auth.sqlite and b/test/auth.sqlite differ diff --git a/util/normalize.lua b/util/normalize.lua new file mode 100644 index 0000000..961c1bc --- /dev/null +++ b/util/normalize.lua @@ -0,0 +1,56 @@ +--[[ +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) + local order = mail.parse_player_list(field) + for i,c in ipairs(order) do + if recipients[string.lower(c)] == nil then + recipients[string.lower(c)] = c + end + end + return mail.concat_player_list(order) +end + + +function mail.parse_player_list(field) + local separator = ", " + local pattern = "([^" .. separator .. "]+)" + + -- get individual players + local player_set = {} + local order = {} + field:gsub(pattern, function(c) + if player_set[string.lower(c)] == nil then + player_set[string.lower(c)] = c + order[#order+1] = c + end + end) + + return order +end + +function mail.concat_player_list(order) + -- turn list of players back into normalized string + return table.concat(order, ", ") +end + +function mail.player_in_list(name, list) + list = list or {} + if type(list) == "string" then + list = mail.parse_player_list(list) + end + for k,c in pairs(list) do + if name == c then + return true + end + end + return false +end + + +function mail.ensure_new_format(message, name) + if message.to == nil then + message.to = name + end +end