Notify player of old but unread messages.
[insidethebox.git] / mods / telex / init.lua
blob697896e8e98872be4fe362115d065c37b6b82aac
2 --[[
4 telex mod for minetest
6 Copyright (C) 2019 Auke Kok <sofar@foo-projects.org>
8 Permission to use, copy, modify, and/or distribute this software for
9 any purpose with or without fee is hereby granted, provided that the
10 above copyright notice and this permission notice appear in all copies.
12 THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
13 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
14 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY
15 SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
16 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
17 AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
18 OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20 ]]--
22 telex = {}
24 --[[
26 - messages are sent from player to another player.
27 - messages have "from", "to", "subject", and "content", and "unread" properties
28 - When a player sends a message, it is put in the global spool
29 - the global spool is in mod_storage and persistent
30 - periodically, the spool will attempt to deliver all messages to receivers
31 - if the receiver does not exist or is not online, the message stays in the spool
32 - if the message stays in the spool longer than <config>, the message will be
33 deleted. A new spool message, containing the old message in quoted form, will be
34 returned to the sender. These messages will not expire from the spool.
35 - if a player comes online, the spool attempts to deliver queued messages to the player
36 - if the player is online, the spool immediately delivers the message
37 - the player has their own mailbox, in player attributes/storage
39 user actions:
40 - list messages
41 - read messages
42 - reply to a message
43 - delete a message
45 msg = {
46 "from" = <string>,
47 "to" = <string>,
48 "subject" = <string>,
49 "content" = <array of strings>,
50 "read" = nil or 1,
51 "age" = <int>
54 mbox = {
55 [1] = msgid,
56 [2] = msgid,
57 ...
60 spool = {
61 [1] = msgid,
62 [2] = msgid,
63 ...
66 Spool/mbox format: array of messages
68 ]]--
70 local S = minetest.get_mod_storage()
71 assert(S)
73 -- helper functions for handling msgid
75 -- allocate a new msgid str
76 function telex.msgid()
77 local msgid = S:get_int("msgid") + 1
78 S:set_int("msgid", msgid)
79 return "m" .. tostring(msgid)
80 end
82 -- get
83 function telex.get_msg(msgid)
84 if not msgid then
85 minetest.log("action", "telex: get_msg() called with nil")
86 return nil
87 end
88 local msg = S:get_string(msgid)
89 if msg then
90 return telex.decode(msg)
91 else
92 minetest.log("action", "telex: get_msg() unable to find " .. msgid)
93 return nil
94 end
95 end
97 -- set
98 function telex.save_msg(msgid, msg)
99 S:set_string(msgid, telex.encode(msg))
102 -- remove
103 function telex.remove_msg(msgid)
104 S:set_string(msgid, "")
107 -- returns an array of strings
108 function telex.list(player)
109 local pmeta = player:get_meta()
110 local mbox = telex.decode(pmeta:get_string("telex_mbox"))
111 local list = {}
112 for n, msgid in pairs(mbox) do
113 local msg = telex.get_msg(msgid)
114 local unread = "!"
115 if msg.read == 1 then
116 unread = " "
118 list[#list + 1] = unread .. n .. ": <" .. msg.from .. "> \"" .. msg.subject .. "\""
120 return list
123 -- returns a table
124 function telex.get(player, no)
125 local pmeta = player:get_meta()
126 local mbox = telex.decode(pmeta:get_string("telex_mbox"))
127 if not mbox then
128 return { "You have no messages." }
131 local msgid = mbox[no]
132 local msg = telex.get_msg(msgid)
134 if not msg then
135 minetest.log("action", "telex: get() unable to find msg no " .. no)
136 return { "No such message exists." }
139 return msg
142 -- returns an array of strings
143 function telex.read(player, no)
144 local pmeta = player:get_meta()
145 local mbox = telex.decode(pmeta:get_string("telex_mbox"))
146 if not mbox then
147 return { "You have no messages." }
149 local msgid = mbox[no]
150 local msg = telex.get_msg(msgid)
151 if not msg then
152 return { "No such message exists." }
155 local list = {
156 "From: <" .. msg.from .. ">",
157 "Subject: " .. msg.subject,
160 for _, v in pairs(msg.content) do
161 table.insert(list, v)
164 -- mark as read and store
165 msg.read = 1
166 telex.save_msg(msgid, msg)
167 mbox[no] = msgid
168 pmeta:set_string("telex_mbox", telex.encode(mbox))
170 return list
173 -- returns array of strings
174 function telex.delete(player, no)
175 local pmeta = player:get_meta()
176 local mbox = telex.decode(pmeta:get_string("telex_mbox"))
177 if not mbox then
178 return { "You have no messages." }
180 local msgid = mbox[no]
181 local msg = telex.get_msg(msgid)
182 if not msg then
183 return { "No such message exists." }
186 table.remove(mbox, no)
187 telex.remove_msg(msgid)
188 pmeta:set_string("telex_mbox", telex.encode(mbox))
190 return { "Message deleted." }
193 -- external should use `send`, internal uses `deliver`
194 function telex.send(msg)
195 assert(msg.from)
196 assert(msg.to)
197 assert(msg.subject)
198 assert(msg.content)
200 local msgid = telex.msgid()
201 telex.save_msg(msgid, msg)
202 telex.deliver(msgid, msg)
205 -- returns nothing
206 function telex.deliver(msgid, msg)
207 -- msg is optional
208 if not msg then
209 msg = telex.get_msg(msgid)
211 local player = minetest.get_player_by_name(msg.to)
212 if player then
213 -- remove age, no longer needed
214 msg.age = nil
215 telex.save_msg(msgid, msg)
216 -- retrieve inbox
217 local pmeta = player:get_meta()
218 local mbox = telex.decode(pmeta:get_string("telex_mbox"))
219 table.insert(mbox, msgid)
220 pmeta:set_string("telex_mbox", telex.encode(mbox))
221 minetest.chat_send_player(msg.to, "You have a new message from <" .. msg.from .. ">")
222 minetest.log("action", "telex: delivered " .. msgid .. " from <" .. msg.from .. "> to <" .. msg.to .. ">")
223 else
224 -- append to spool
225 msg.age = 7 * 86400 -- 7 days max in spool
226 telex.save_msg(msgid, msg)
227 local spool = telex.decode(S:get_string("telex_spool"))
228 table.insert(spool, msgid)
229 S:set_string("telex_spool", telex.encode(spool))
230 minetest.log("action", "telex: spooled " .. msgid .. " from <" .. msg.from .. "> to <" .. msg.to .. ">")
231 announce.admins("spooled " .. msgid .. " from <" .. msg.from .. "> to <" .. msg.to .. ">")
235 -- returns nothing
236 function telex.retour(msgid)
237 local msg = telex.get_msg(msgid)
238 if msg.from == "MAILER-DEAMON" then
239 -- discard, we tried hard enough!
240 minetest.log("action", "telex: discarded " .. msgid .. " from <" .. msg.from .. "> to <" .. msg.to .. ">")
241 announce.admins("discarded " .. msgid .. "from <" .. msg.from .. "> to <" .. msg.to .. ">")
242 return
245 local to = msg.to
246 local from = msg.from
247 msg.to = from
248 msg.from = "MAILER-DAEMON"
249 msg.subject = "UNDELIVERABLE: " .. msg.subject
250 msg.age = 86400 * 30 -- return mail for 30 days max, then discard.
251 table.insert(msg.content, 1, "Your message to <" .. to .. "> was unable to be delivered.")
252 table.insert(msg.content, 2, "================== ORIGINAL MESSAGE BELOW ================")
254 -- remove the old msg, it's no longer in spool
255 telex.remove_msg(msgid)
257 -- allocate a new ID for the return msg
258 local msgid2 = telex.msgid()
259 telex.save_msg(msgid2, msg)
260 telex.deliver(msgid2, msg)
262 minetest.log("action", "telex: returned " .. msgid .. " -> " .. msgid2 .. " from <" .. from .. "> back to <" .. to .. ">")
263 announce.admins("returned " .. msgid .. " -> " .. msgid2 .. " from <" .. from .. "> back to <" .. to .. ">")
266 -- returns message/mbox --FIXME compress/decompress
267 function telex.decode(digest)
268 if not digest or digest == "" then
269 return {}
272 local tbl = minetest.parse_json(digest)
273 if not tbl then
274 return {}
276 return tbl
279 -- returns digest
280 function telex.encode(message_or_mbox)
281 return minetest.write_json(message_or_mbox)
284 minetest.register_on_joinplayer(function(player)
285 -- convert old mbox
286 local pmeta = player:get_meta()
287 local mbox = telex.decode(pmeta:get_string("telex_mbox"))
288 local save = false
289 for k, v in pairs(mbox) do
290 if type(v) == "table" then
291 local msgid = telex.msgid()
292 telex.save_msg(msgid, v)
293 mbox[k] = msgid
294 save = true
297 if save then
298 pmeta:set_string("telex_mbox", telex.encode(mbox))
301 -- check the spool for messages for `player`
302 local spool = telex.decode(S:get_string("telex_spool"))
303 local name = player:get_player_name()
305 -- deliver items for this player
306 local old = {}
307 local del = {}
308 for k, msgid in pairs(spool) do
309 local msg = telex.get_msg(msgid)
310 if msg.to == name then
311 old[#old + 1] = k
312 del[#del + 1] = msgid
316 -- remove items from spool in reverse order
317 for i = #old, 1, -1 do
318 table.remove(spool, old[i])
320 S:set_string("telex_spool", telex.encode(spool))
322 -- now deliver them to player
323 for _, msgid in pairs(del) do
324 telex.deliver(msgid)
327 -- notify if there wasn't anything new but the player still had some unread
328 if #old == 0 then
329 -- reuse `mbox`
330 for _, msgid in pairs(mbox) do
331 local msg = telex.get_msg(msgid)
332 if not msg.read then
333 minetest.chat_send_player(name, "You have unread messages")
334 break
338 end)
340 function telex.process_spool()
341 local spool = telex.decode(S:get_string("telex_spool"))
343 minetest.log("action", "telex: processing spool: " .. #spool .. " messages" )
344 local old = {}
345 local del = {}
346 for k, msgid in pairs(spool) do
347 local msg = telex.get_msg(msgid)
348 msg.age = msg.age - 3600
349 if msg.age < 0 then
350 old[#old + 1] = k
351 del[#del + 1] = msgid
353 telex.save_msg(msgid, msg)
356 -- remove old msgs from spool
357 for i = #old, 1, -1 do
358 table.remove(spool, old[i])
360 S:set_string("telex_spool", telex.encode(spool))
362 -- return the actual messages
363 for _, msgid in pairs(del) do
364 telex.retour(msgid)
367 minetest.after(3600, telex.process_spool)
370 minetest.after(5, telex.process_spool)