init.lua 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. mailgui = mailgui or {}
  2. mailgui.modpath = minetest.get_modpath("mailgui")
  3. -- State management table. Used for GUIs.
  4. mailgui.players = mailgui.players or {}
  5. -- List of players who currently have open inbox formspecs.
  6. mailgui.open_inboxes = mailgui.open_inboxes or {}
  7. -- API function for alerting a player that they have mail.
  8. mailgui.alert_player = function(from, pname)
  9. -- If target player's inbox is open, alert them.
  10. minetest.after(5, function() -- The delay is useful if player sends mail to themselves.
  11. minetest.chat_send_player(pname, "# Server: <" .. rename.gpn(from) .. "> sent you mail! A Key is needed to view it.")
  12. if mailgui.open_inboxes[pname] then
  13. if mailgui.players[pname] then -- Sanity check.
  14. local intlive = mailgui.players[pname].live or 'true'
  15. if intlive == 'true' then
  16. mailgui.players[pname].infotext = "You received new mail! Press 'Get Mail'."
  17. mailgui.show_formspec(pname, true) -- Formspec should already be open.
  18. end
  19. end
  20. end
  21. end)
  22. end
  23. -- Called from the GUI to actually send a message.
  24. -- (This is done by interfacing with the email mod, which handles the database.)
  25. mailgui.send_mail_single = function(from, subject, message, mailto)
  26. local success, errstr = email.send_mail_single(from, mailto, subject, message)
  27. if success == true then
  28. -- This is so the player has a log of their message in chat.
  29. minetest.chat_send_player(from, "# Server: Email sent to <" .. rename.gpn(mailto) .. ">!")
  30. minetest.chat_send_player(from, "# Server: Subject: " .. subject)
  31. local tb = string.split(message, "\n")
  32. for k, v in ipairs(tb) do
  33. minetest.chat_send_player(from, "# Server: Message: " .. v)
  34. end
  35. mailgui.alert_player(from, mailto) -- GUI alert.
  36. else
  37. minetest.chat_send_player(from, "# Server: Failed to send mail to <" .. rename.gpn(mailto) .. ">!")
  38. easyvend.sound_error(from)
  39. end
  40. return success, errstr
  41. end
  42. mailgui.send_mail_multi = function(from, subject, message, mailto)
  43. local success, failure = email.send_mail_multi(from, mailto, subject, message)
  44. for k, v in ipairs(success) do
  45. minetest.chat_send_player(from, "# Server: Mail sent to <" .. rename.gpn(v.name) .. ">.")
  46. mailgui.alert_player(from, v.name) -- GUI alert.
  47. end
  48. for k, v in ipairs(failure) do
  49. local reason = "Reason unknown."
  50. if v.error == "boxfull" then
  51. reason = "Player's inbox is full."
  52. elseif v.error == "badplayer" then
  53. reason = "Player does not exist."
  54. elseif v.error == "toobig" then
  55. reason = "Email is too large."
  56. end
  57. minetest.chat_send_player(from, "# Server: Failed to send mail to <" .. rename.gpn(v.name) .. ">! " .. reason)
  58. easyvend.sound_error(from)
  59. end
  60. -- This is so the player has a log of their message in chat.
  61. minetest.chat_send_player(from, "# Server: Subject: " .. subject)
  62. local tb = string.split(message, "\n")
  63. for k, v in ipairs(tb) do
  64. minetest.chat_send_player(from, "# Server: Message: " .. v)
  65. end
  66. return #success, #failure -- Return number of successes and failures.
  67. end
  68. -- Called from the GUI to obtain a player's inbox for display.
  69. mailgui.get_inbox = function(pname)
  70. -- For purposes of the GUI, I need to make sure the table is ordered.
  71. local tb = email.get_inbox(pname)
  72. local out = {}
  73. for k, v in pairs(tb) do
  74. out[#out+1] = table.copy(v)
  75. end
  76. return out
  77. end
  78. -- Helper function to compose the display body of the currently selected mail.
  79. mailgui.compose_inbox_body = function(pname)
  80. local pstate = mailgui.players[pname]
  81. local inboxes = pstate.inboxes or {}
  82. local idx = pstate.selected
  83. -- Selection can be nil.
  84. if type(idx) ~= "number" then return "", "" end
  85. if idx >= 1 and idx <= #inboxes then
  86. local frm = inboxes[idx].from or ""
  87. local dat = inboxes[idx].date or ""
  88. local sub = inboxes[idx].sub or ""
  89. if sub == "" then sub = "No Subject" end
  90. local msg = inboxes[idx].msg or ""
  91. local body =
  92. "From: <" .. rename.gpn(frm) .. ">\n" ..
  93. "To: <" .. rename.gpn(pname) .. ">\n" ..
  94. "Date: " .. dat .. "\n" ..
  95. "Subject: " .. sub .. "\n" ..
  96. "------------------------------" ..
  97. "\n" .. msg
  98. return body, msg -- Display body, original body.
  99. end
  100. return "", ""
  101. end
  102. mailgui.compose_outbox_body = function(pname)
  103. local pstate = mailgui.players[pname]
  104. local frm = pname
  105. local to = pstate.mailto or ""
  106. local dat = os.date("%Y-%m-%d")
  107. local sub = pstate.subject or ""
  108. if sub == "" then sub = "No Subject" end
  109. local msg = pstate.message or ""
  110. local body =
  111. "From: <" .. rename.gpn(frm) .. ">\n" ..
  112. "To: <" .. rename.gpn(to) .. ">\n" ..
  113. "Date: " .. dat .. "\n" ..
  114. "Subject: " .. sub .. "\n" ..
  115. "------------------------------" ..
  116. "\n" .. msg
  117. return body
  118. end
  119. mailgui.compose_formspec = function(pname)
  120. -- Get state table for this player.
  121. local pstate = mailgui.players[pname]
  122. local inboxes = pstate.inboxes or {}
  123. local intlive = pstate.live or 'true'
  124. local formspec = "size[14,8]" ..
  125. default.gui_bg ..
  126. default.gui_bg_img ..
  127. default.gui_slots ..
  128. "item_image[0,0;1,1;passport:passport_adv]" ..
  129. "label[1,0;==| Send and Receive Mail |==]" ..
  130. -- Inbox panel.
  131. "label[1,0.5;Inbox (" .. #inboxes .. " / " .. email.maxsize .. ")]" ..
  132. "checkbox[3.8,0.32;enable_interupt;Enable Live Interrupt;" ..
  133. intlive .. "]" ..
  134. "textlist[0,1.1;6.31,1.8;headers;"
  135. -- Compose list of mail headers.
  136. local strs = ""
  137. for k, v in pairs(inboxes) do
  138. local frm = v.from or "N/A"
  139. local sub = v.sub or ""
  140. local dat = v.date or "N/A"
  141. if sub == "" then sub = "No Subject" end
  142. strs = strs .. minetest.formspec_escape(
  143. dat .. ": <" .. rename.gpn(frm) .. ">: " .. sub) .. ","
  144. end
  145. strs = string.gsub(strs, ",$", "") -- Remove trailing comma.
  146. formspec = formspec .. strs
  147. -- Finalize mail headers.
  148. local idx = pstate.selected -- May be nil.
  149. if type(idx) ~= "number" then idx = 1 end -- Must always be valid number.
  150. formspec = formspec .. ";" .. tostring(idx) .. ";false]"
  151. -- Compose display of currently selected message body.
  152. formspec = formspec .. "textarea[0.3,3.2;6.5,4;inbox;;"
  153. -- By composing the inbox body near the same code that composes the header
  154. -- list, using the same index value, I ensure the two GUI elements can never
  155. -- somehow get out of sync. This is important, because mail deletion is done
  156. -- by index, and I don't want players to be able to accidentally delete the
  157. -- wrong mail because the selected email and displayed email weren't the same.
  158. local body = mailgui.compose_inbox_body(pname)
  159. formspec = formspec .. minetest.formspec_escape(body)
  160. formspec = formspec .. "]" -- End textarea element.
  161. -- Compose message panel.
  162. formspec = formspec .. "label[7,0.5;Compose Message"
  163. if intlive == 'true' then
  164. formspec = formspec .. " (Interrupt Enabled)"
  165. end
  166. formspec = formspec .. "]" ..
  167. "label[7,1.0;Subject:]" ..
  168. "field[8.4,1.3;5.9,1;subject;;" ..
  169. minetest.formspec_escape(pstate.subject or "") .. "]" ..
  170. "label[7,2.0;Message:]" ..
  171. "textarea[8.4,2.0;5.9,4.3;message;;" ..
  172. minetest.formspec_escape(pstate.message or "") .. "]" ..
  173. "label[7,5.84;Send To:]" ..
  174. "field[8.4,6.14;5.9,1;mailto;;" ..
  175. minetest.formspec_escape(pstate.mailto or "") .. "]" ..
  176. "button[0,6.8;2,1;delall;Delete All]" ..
  177. "button[2,6.8;2,1;delone;Delete One]" ..
  178. "button[4,6.8;2,1;inboxcopy;Get Copy]" ..
  179. "label[0,7.7;Message: " ..
  180. minetest.formspec_escape(pstate.infotext or "") .. "]" ..
  181. "button[8,6.8;2,1;getmail;Get Mail]" ..
  182. "button[10,6.8;2,1;sendmail;Send Mail]" ..
  183. "button[12,6.8;2,1;outcopy;Get Copy]" ..
  184. -- Close button.
  185. "button[12,0;2,1;done;Close]"
  186. return formspec
  187. end
  188. -- API function.
  189. mailgui.show_formspec = function(pname, reshow)
  190. -- Make sure player has entry in state table.
  191. mailgui.players[pname] = mailgui.players[pname] or {
  192. -- State defaults.
  193. infotext = "", -- For operation messages.
  194. message = "", -- Current contents of draft area.
  195. subject = "", -- Current contents of subject line.
  196. mailto = "", -- Current conents of mailto line.
  197. selected = nil, -- Inbox selection index.
  198. inboxes = {}, -- Email records (badly named, I know).
  199. live = 'true', -- Whether interupting is allowed.
  200. }
  201. -- Do stuff if opening the GUI (not simply redrawing it).
  202. if not reshow then
  203. mailgui.players[pname].infotext = "Mail management opened!"
  204. mailgui.players[pname].inboxes = mailgui.get_inbox(pname)
  205. end
  206. mailgui.open_inboxes[pname] = true -- Player has open formspec.
  207. local formspec = mailgui.compose_formspec(pname)
  208. minetest.show_formspec(pname, "mailgui:mailgui", formspec)
  209. end
  210. -- GUI callback handler.
  211. mailgui.gui_handler = function(pref, fname, fields)
  212. if fname ~= "mailgui:mailgui" then return end -- Not valid for this callback.
  213. local pname = pref:get_player_name()
  214. if not mailgui.players[pname] then return true end -- No state, abort!
  215. if fields.quit then
  216. mailgui.open_inboxes[pname] = nil
  217. return true
  218. end
  219. -- Obtain state table for this player.
  220. local pstate = mailgui.players[pname]
  221. -- Don't update message composing state unless told to.
  222. if fields.message then pstate.message = fields.message end
  223. if fields.subject then pstate.subject = fields.subject end
  224. if fields.mailto then pstate.mailto = fields.mailto end
  225. if fields.done then
  226. mailgui.open_inboxes[pname] = nil
  227. passport.show_formspec(pname)
  228. return true
  229. end
  230. if fields.delall then
  231. email.clear_inbox(pname, pstate.inboxes)
  232. pstate.inboxes = {}
  233. pstate.infotext = "Inbox cleared!"
  234. pstate.selected = nil -- Unselect.
  235. local sz = email.get_inbox_size(pname)
  236. if sz > 0 then
  237. pstate.infotext = "Inbox cleared! (There are items remaining, press 'Get Mail'.)"
  238. end
  239. end
  240. if fields.delone then
  241. local idx = pstate.selected -- Can be nil.
  242. if type(idx) == "number" then
  243. if idx >= 1 and idx <= #pstate.inboxes then
  244. local frm = pstate.inboxes[idx].from or ""
  245. local sub = pstate.inboxes[idx].sub or ""
  246. if sub == "" then sub = "No Subject" end
  247. -- Delete email.
  248. local tb = {pstate.inboxes[idx]} -- Get table ref.
  249. email.clear_inbox(pname, tb)
  250. table.remove(pstate.inboxes, idx)
  251. pstate.infotext = "Message from <" .. rename.gpn(frm)
  252. .. "> (subject: " .. sub .. ") deleted!"
  253. else
  254. pstate.infotext = "Invalid selection index. Please select an email first."
  255. pstate.selected = nil
  256. end
  257. else
  258. pstate.infotext = "No message selected."
  259. end
  260. end
  261. if fields.enable_interupt then
  262. pstate.live = fields.enable_interupt
  263. if pstate.live == 'true' then
  264. pstate.infotext = "Live interrupt enabled. " ..
  265. "(Live mail updates will interfere with composing.)"
  266. elseif pstate.live == 'false' then
  267. pstate.infotext = "Live interrupt disabled. " ..
  268. "(You may compose mails without interruption.)"
  269. else
  270. pstate.infotext = "Invalid value!"
  271. end
  272. end
  273. if fields.sendmail then
  274. local subject = string.trim(pstate.subject or "")
  275. local message = string.trim(pstate.message or "")
  276. local mailto = pstate.mailto or ""
  277. -- Allow for no subject.
  278. if string.len(subject) <= email.max_subject_length then
  279. if message ~= "" then
  280. if mailto ~= "" then
  281. if not string.find(mailto, ",") then -- Singlename.
  282. mailto = string.trim(mailto)
  283. if mailto:lower() == "server" then
  284. if minetest.check_player_privs(pname, {server=true}) then
  285. -- Mail should be sent to all registered players.
  286. -- We use special keyword 'server' for this.
  287. -- No player can have this name.
  288. local nums, numf = mailall.send_mail(pname, subject, message)
  289. pstate.infotext = "Email sent to " .. nums ..
  290. " registered players(s). " .. numf .. " failure(s)."
  291. if numf == 0 then
  292. -- Clear fields on success.
  293. pstate.subject = ""
  294. pstate.message = ""
  295. pstate.mailto = ""
  296. end
  297. else
  298. pstate.infotext = "Cannot post server-wide mailing. Your privileges are insufficient!"
  299. end
  300. else
  301. local b, e = mailgui.send_mail_single(pname, subject, message, mailto)
  302. if b == true then
  303. pstate.infotext = "Mail sent to <" .. mailto .. ">!"
  304. -- Clear fields on success.
  305. pstate.subject = ""
  306. pstate.message = ""
  307. pstate.mailto = ""
  308. else
  309. if e == "boxfull" then
  310. pstate.infotext = "Recipient <" .. mailto .. ">'s inbox is full."
  311. elseif e == "badplayer" then
  312. pstate.infotext = "Recipient <" .. mailto .. "> does not exist!"
  313. elseif e == "toobig" then
  314. pstate.infotext = "Email is too big to send!"
  315. else
  316. pstate.infotext = "Unknown error!"
  317. end
  318. end
  319. end
  320. elseif string.find(mailto, ",") then -- Multiname.
  321. local strtab = string.split(mailto)
  322. -- Trim all names.
  323. for i=1, #strtab do
  324. strtab[i] = string.trim(strtab[i])
  325. end
  326. local nums, numf = mailgui.send_mail_multi(pname, subject, message, strtab)
  327. pstate.infotext = #strtab .. " recipient(s) listed. " ..
  328. nums .. " message(s) sent. " .. numf .. " failure(s)."
  329. if numf == 0 then
  330. -- Clear fields on all success.
  331. pstate.subject = ""
  332. pstate.message = ""
  333. pstate.mailto = ""
  334. end
  335. else
  336. pstate.infotext = "Unknown error!" -- Can we ever reach here?
  337. end
  338. else
  339. pstate.infotext = "Please enter a recipient!"
  340. end
  341. else
  342. pstate.infotext = "Please enter a message!"
  343. end
  344. else
  345. pstate.infotext = "Subject line is too long! Max is " .. email.max_subject_length .. " bytes."
  346. end
  347. end
  348. if fields.getmail then
  349. pstate.inboxes = mailgui.get_inbox(pname)
  350. pstate.infotext = "Inbox refreshed!"
  351. -- Not really useful to reset the selection?
  352. --pstate.selected = nil -- Unselect.
  353. end
  354. if fields.headers then
  355. assert(type(fields.headers) == "string")
  356. local event = minetest.explode_textlist_event(fields.headers)
  357. if event.type == "CHG" then
  358. local idx = event.index
  359. if idx >= 1 and idx <= #pstate.inboxes then
  360. pstate.selected = idx
  361. pstate.infotext = "Selected mail #" .. tostring(idx)
  362. else
  363. pstate.selected = nil
  364. pstate.infotext = "No mail currently selected."
  365. end
  366. elseif event.type == "DCL" then
  367. local idx = event.index
  368. if idx >= 1 and idx <= #pstate.inboxes then
  369. local mail = pstate.inboxes[idx]
  370. local sub = mail.sub or ""
  371. if sub == "" then sub = "No Subject" end
  372. pstate.subject = "RE: " .. sub
  373. pstate.mailto = rename.gpn(mail.from or "")
  374. pstate.selected = idx
  375. pstate.infotext = "Initialized response to mail #" .. tostring(idx)
  376. else
  377. pstate.selected = nil
  378. pstate.infotext = "No mail currently selected!"
  379. end
  380. end
  381. end
  382. if fields.inboxcopy then
  383. local idx = pstate.selected
  384. if type(idx) == "number" and idx >= 1 and idx <= #pstate.inboxes then
  385. local inv = pref:get_inventory()
  386. if inv then
  387. if inv:contains_item("main", "default:paper") then
  388. local fulltext = mailgui.compose_inbox_body(pname)
  389. local serialized = memorandum.compose_metadata({
  390. text = fulltext,
  391. signed = pname,
  392. })
  393. local itemstack = inv:add_item("main", {
  394. name="memorandum:letter",
  395. count=1, wear=0,
  396. metadata=serialized,
  397. })
  398. if itemstack:is_empty() then
  399. inv:remove_item("main", "default:paper")
  400. pstate.infotext = "Message printed! You should find it in your inventory."
  401. else
  402. pstate.infotext = "You must have space in your inventory for the printed mail."
  403. end
  404. else
  405. pstate.infotext = "You must have blank memorandum in your inventory in order to copy mail."
  406. end
  407. else
  408. pstate.infotext = "Cannot get inventory access!"
  409. end
  410. else
  411. pstate.infotext = "No mail currently selected!"
  412. end
  413. end
  414. if fields.outcopy then
  415. local subject = pstate.subject or ""
  416. local message = pstate.message or ""
  417. local mailto = pstate.mailto or ""
  418. if string.len(subject) <= email.max_subject_length then
  419. if message ~= "" then
  420. if mailto ~= "" then
  421. -- Attempt to copy outbox message to player's inventory.
  422. local inv = pref:get_inventory()
  423. if inv then
  424. if inv:contains_item("main", "default:paper") then
  425. local fulltext = mailgui.compose_outbox_body(pname)
  426. local serialized = memorandum.compose_metadata({
  427. text = fulltext,
  428. signed = pname,
  429. })
  430. local itemstack = inv:add_item("main", {
  431. name="memorandum:letter",
  432. count=1, wear=0,
  433. metadata=serialized,
  434. })
  435. if itemstack:is_empty() then
  436. inv:remove_item("main", "default:paper")
  437. pstate.infotext = "Message printed! You should find it in your inventory."
  438. else
  439. pstate.infotext = "You must have space in your inventory for the printed draft."
  440. end
  441. else
  442. pstate.infotext = "You must have blank memorandum in your inventory in order to copy a draft."
  443. end
  444. else
  445. pstate.infotext = "Cannot get inventory access!"
  446. end
  447. else
  448. pstate.infotext = "Please enter a recipient!"
  449. end
  450. else
  451. pstate.infotext = "Please enter a message!"
  452. end
  453. else
  454. pstate.infotext = "Subject line is too long! (Max " .. email.max_subject_length .. " bytes.)"
  455. end
  456. end
  457. -- Keep displaying formspec until explicitly told to quit.
  458. mailgui.show_formspec(pname, true)
  459. return true
  460. end
  461. if not mailgui.run_once then
  462. minetest.register_on_player_receive_fields(function(...)
  463. return mailgui.gui_handler(...)
  464. end)
  465. local c = "mailgui:core"
  466. local f = mailgui.modpath .. "/init.lua"
  467. reload.register_file(c, f, false)
  468. mailgui.run_once = true
  469. end