init.lua 18 KB

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