Files
coa-altoholic/Altoholic-Addon/DataStore_Mails/DataStore_Mails.lua
T

612 lines
18 KiB
Lua

--[[ *** DataStore_Mails ***
Written by : Thaoky, EU-Marécages de Zangar
July 16th, 2009
--]]
if not DataStore then return end
local addonName = "DataStore_Mails"
_G[addonName] = LibStub("AceAddon-3.0"):NewAddon(addonName, "AceConsole-3.0", "AceEvent-3.0", "AceComm-3.0", "AceSerializer-3.0", "AceTimer-3.0")
local addon = _G[addonName]
local THIS_ACCOUNT = "Default"
local commPrefix = "DS_Mails" -- let's keep it a bit shorter than the addon name, this goes on a comm channel, a byte is a byte ffs :p
local MAIL_EXPIRY = 30 -- Mails expire after 30 days
-- Func Call Spam Protection
local FCSP_timer_OnBagUpdate
-- Message types
local MSG_SENDMAIL_INIT = 1
local MSG_SENDMAIL_END = 2
local MSG_SENDMAIL_ATTACHMENT = 3
local MSG_SENDMAIL_BODY = 4
local ICON_COIN = "Interface\\Icons\\INV_Misc_Coin_01"
local ICON_NOTE = "Interface\\Icons\\INV_Misc_Note_01"
local AddonDB_Defaults = {
global = {
Options = {
ScanMailBody = 1, -- by default, scan the body of a mail (this action marks it as read)
CheckMailExpiry = 1, -- check mail expiry or not
MailWarningThreshold = 5,
CheckMailExpiryAllAccounts = 1,
CheckMailExpiryAllRealms = 1,
},
Characters = {
['*'] = { -- ["Account.Realm.Name"]
lastUpdate = nil, -- last time the mail was checked for this char
lastVisitDate = nil, -- in YYYY MM DD hh:mm, for external apps
Mails = {
['*'] = {
icon = nil,
link = nil,
count = nil,
money = nil,
lastCheck = 0, -- last time "THIS" mail was checked (can be different than that of the mailbox)
text = nil,
subject = nil,
sender = nil,
daysLeft = 0,
returned = nil,
}
},
MailCache = { -- same structure as "Mail", but serves as a cache for mails sent by a guildmate, until the mail actually arrives in the real mailbox (1h delay)
['*'] = {
icon = nil,
link = nil,
count = nil,
money = nil,
lastCheck = 0, -- last time "THIS" mail was checked (can be different than that of the mailbox)
text = nil,
subject = nil,
sender = nil,
daysLeft = 0,
}
},
}
}
}
}
-- *** Utility functions ***
local function GetIDFromLink(link)
return tonumber(link:match("item:(%d+)"))
end
local function GetOption(option)
return addon.db.global.Options[option]
end
local function GetMailTable(character, index)
-- depending on the index passed, returns the right mail entry either from the "Mails" table or the "MailCache" table
-- The assumption is that the MailCache entries come after the Mails entries
-- This function is pure utility, and is not made public to client addons
if index <= #character.Mails then
return character.Mails[index]
else
index = index - #character.Mails
return character.MailCache[index]
end
end
local function GuildWhisper(player, messageType, ...)
if DataStore:IsGuildMemberOnline(player) then
local serializedData = addon:Serialize(messageType, ...)
addon:SendCommMessage(commPrefix, serializedData, "WHISPER", player)
end
end
local function ReadMailAttachments(mailIndex)
-- reads the attachments of a mail that is about to be sent or returned
wipe(addon.Attachments)
local name, icon, count, link
for attachmentIndex = 1, 12 do
if mailIndex then -- returned mail
name, icon, count = GetInboxItem(mailIndex, attachmentIndex)
link = GetInboxItemLink(mailIndex, attachmentIndex)
else -- if there is no index, it's a sent mail
name, icon, count = GetSendMailItem(attachmentIndex)
link = GetSendMailItemLink(attachmentIndex)
end
if name then -- if attachment slot is not empty .. save it
table.insert(addon.Attachments, { ["icon"] = icon, ["link"] = link, ["count"] = count } )
end
end
end
local function SendGuildMail(recipient, subject, body)
local player = DataStore:GetNameOfMain(recipient)
if not player then return end
-- this mail is sent to "player", but is for alt "recipient"
GuildWhisper(player, MSG_SENDMAIL_INIT, recipient)
if type(addon.Attachments) == "table" then
for _, attachment in pairs(addon.Attachments) do
GuildWhisper(player, MSG_SENDMAIL_ATTACHMENT, attachment.icon, attachment.link, attachment.count)
end
end
-- .. then save the mail itself + gold if any
local money = GetSendMailMoney()
if (money > 0) or (strlen(body) > 0) then
GuildWhisper(player, MSG_SENDMAIL_BODY, subject, body, money)
end
GuildWhisper(player, MSG_SENDMAIL_END)
end
-- *** Scanning functions ***
local function SaveAttachments(character, index, mailSender, days, wasReturned)
-- saves attachments of a given mail index into a given character mailbox
for attachmentIndex = 1, 12 do -- mandatory, loop through all 12 slots, since attachments could be anywhere (ex: slot 4,5,8)
local item, mailIcon, itemCount = GetInboxItem(index, attachmentIndex)
if item then
table.insert(character.Mails, {
icon = mailIcon,
count = itemCount,
link = GetInboxItemLink(index, attachmentIndex),
sender = mailSender,
lastCheck = time(),
daysLeft = days,
returned = wasReturned,
} )
end
end
end
local function ScanMailbox()
local character = addon.ThisCharacter
wipe(character.Mails)
local numItems = GetInboxNumItems();
if numItems == 0 then
return
end
local cache = character.MailCache
-- check the cache, and clean entries that are about to be replaced in the scan
for i = #cache, 1, -1 do
if cache[i].lastCheck then
local age = time() - cache[i].lastCheck
if age > 3600 then -- if older than 1 hour, delete this entry
table.remove(cache, i)
end
end
end
for i = 1, numItems do
local _, stationaryIcon, mailSender, mailSubject, mailMoney, _, days, numAttachments, _, wasReturned = GetInboxHeaderInfo(i);
if numAttachments then -- treat attachments as separate entries
SaveAttachments(character, i, mailSender, days, wasReturned)
end
local inboxText
if GetOption("ScanMailBody") == 1 then
inboxText = GetInboxText(i) -- this marks the mail as read
end
if (mailMoney > 0) or inboxText then -- if there's money or text .. save the entry
local mailIcon
if mailMoney > 0 then
mailIcon = ICON_COIN
else
mailIcon = stationaryIcon
end
table.insert(character.Mails, {
icon = mailIcon,
money = mailMoney,
text = inboxText,
subject = mailSubject,
sender = mailSender,
lastCheck = time(),
daysLeft = days,
returned = wasReturned,
} )
end
end
-- show mails with the lowest expiry first
table.sort(character.Mails, function(a, b) return a.daysLeft < b.daysLeft end)
end
-- *** Event Handlers ***
local function OnBagUpdate(event, bag)
FCSP_timer_OnBagUpdate = nil -- manual reset (safety redundancy)
if addon.isOpen then -- if a bag is updated while the mailbox is opened, this means an attachment has been taken.
-- print("DataStore_Mails.lua -- OnBagUpdate(event, ",bag,")", DEBUG_CNT, format("%.3f",GetTime()%100)) -- DEBUG 2025 07 21 - 2
ScanMailbox() -- I could not hook TakeInboxItem because mailbox content is not updated yet
end
end
local function FCSP_OnBagUpdate(event, bag)
-- this function limits calls to "OnBagUpdate" to max 1 every second
if bag < 0 then
return
end
if addon.isOpen then -- if a bag is updated while the mailbox is opened, this means an attachment has been taken.
if FCSP_timer_OnBagUpdate then return end
FCSP_timer_OnBagUpdate = addon:ScheduleTimer(OnBagUpdate, 1, event, bag)
end
end
local function OnMailInboxUpdate()
-- process only one occurence of the event, right after MAIL_SHOW
addon:UnregisterEvent("MAIL_INBOX_UPDATE")
ScanMailbox()
end
local function OnMailSendInfoUpdate()
ReadMailAttachments()
end
local function OnMailClosed()
addon.isOpen = nil
addon:UnregisterEvent("MAIL_CLOSED")
ScanMailbox()
local character = addon.ThisCharacter
character.lastUpdate = time()
character.lastVisitDate = date("%Y/%m/%d %H:%M")
addon:UnregisterEvent("MAIL_SEND_INFO_UPDATE")
wipe(addon.Attachments)
end
local function OnMailShow()
-- the event may be triggered multiple times, exit if the mailbox is already open
if addon.isOpen then return end
CheckInbox()
addon:RegisterEvent("MAIL_CLOSED", OnMailClosed)
addon:RegisterEvent("MAIL_INBOX_UPDATE", OnMailInboxUpdate)
addon:RegisterEvent("MAIL_SEND_INFO_UPDATE", OnMailSendInfoUpdate)
-- create a temporary table to hold the attachments that will be sent, keep it local since the event is rare
addon.Attachments = addon.Attachments or {}
addon.isOpen = true
end
-- ** Mixins **
local function _GetMailboxLastVisit(character)
return character.lastUpdate or 0
end
local function _GetMailItemCount(character, searchedID)
local count = 0
for _, v in pairs (character.Mails) do
local link = v.link
if link and (GetIDFromLink(link) == searchedID) then
count = count + (v.count or 1)
end
end
for _, v in pairs (character.MailCache) do
local link = v.link
if link and (GetIDFromLink(link) == searchedID) then
count = count + (v.count or 1)
end
end
return count
end
local function _GetMailAttachments()
return addon.Attachments
end
local function _GetNumMails(character)
return #character.Mails + #character.MailCache
end
local function _GetMailInfo(character, index)
local data = GetMailTable(character, index)
return data.icon, data.count, data.link, data.money, data.text, data.returned
end
local function _GetMailSender(character, index)
local data = GetMailTable(character, index)
return data.sender
end
local function _GetMailExpiry(character, index)
local data = GetMailTable(character, index)
-- return the mail expiry, expressed in days and in seconds
local diff = time() - data.lastCheck
local days = data.daysLeft - (diff / 86400)
local seconds = (data.daysLeft * 86400) - diff
return days, seconds
end
local function _GetMailSubject(character, index)
local data = GetMailTable(character, index)
if data.subject then -- if there's a subject, use it
return data.subject
end
-- otherwise, return the name of the item
local name
local link = data.link
if link then
local id = GetIDFromLink(link)
name = GetItemInfo(id)
end
return name
end
local function _GetNumExpiredMails(character, threshold)
local count = 0
for i = 1, _GetNumMails(character) do
if _GetMailExpiry(character, i) < threshold then
count = count + 1
end
end
return count
end
local function _SaveMailToCache(character, mailMoney, mailBody, mailSubject, mailSender)
local mailIcon = (mailMoney > 0) and ICON_COIN or ICON_NOTE
table.insert(character.MailCache, {
money = mailMoney,
icon = mailIcon,
text = mailBody,
subject = mailSubject,
sender = mailSender,
lastCheck = time(),
daysLeft = MAIL_EXPIRY,
} )
end
local function _SaveMailAttachmentToCache(character, mailIcon, mailLink, mailCount, mailSender)
table.insert(character.MailCache, {
icon = mailIcon,
link = mailLink,
count = mailCount,
sender = mailSender,
lastCheck = time(),
daysLeft = MAIL_EXPIRY,
} )
end
local function _IsMailBoxOpen()
return addon.isOpen
end
local PublicMethods = {
GetMailboxLastVisit = _GetMailboxLastVisit,
GetMailItemCount = _GetMailItemCount,
GetMailAttachments = _GetMailAttachments,
GetNumMails = _GetNumMails,
GetMailInfo = _GetMailInfo,
GetMailSender = _GetMailSender,
GetMailExpiry = _GetMailExpiry,
GetMailSubject = _GetMailSubject,
GetNumExpiredMails = _GetNumExpiredMails,
SaveMailToCache = _SaveMailToCache,
SaveMailAttachmentToCache = _SaveMailAttachmentToCache,
IsMailBoxOpen = _IsMailBoxOpen,
}
-- *** Guild Comm ***
local guildMailRecipient -- name of the alt who receives a mail from a guildmate
local guildMailRecipientKey -- key of the alt who receives a mail from a guildmate
local GuildCommCallbacks = {
[MSG_SENDMAIL_INIT] = function(sender, recipient)
guildMailRecipient = recipient
guildMailRecipientKey = format("%s.%s.%s", THIS_ACCOUNT, GetRealmName(), recipient)
end,
[MSG_SENDMAIL_END] = function(sender)
if guildMailRecipient then
addon:SendMessage("DATASTORE_GUILD_MAIL_RECEIVED", sender, guildMailRecipient)
end
guildMailRecipient = nil
guildMailRecipientKey = nil
end,
[MSG_SENDMAIL_ATTACHMENT] = function(sender, icon, link, count)
local recipientTable = addon.db.global.Characters[guildMailRecipientKey]
if recipientTable then
_SaveMailAttachmentToCache(recipientTable, icon, link, count, sender)
end
end,
[MSG_SENDMAIL_BODY] = function(sender, subject, body, money)
local recipientTable = addon.db.global.Characters[guildMailRecipientKey]
if recipientTable then
_SaveMailToCache(recipientTable, money, body, subject, sender)
end
end,
}
local function CheckExpiries()
local allAccounts = GetOption("CheckMailExpiryAllAccounts")
local allRealms = GetOption("CheckMailExpiryAllRealms")
local threshold = GetOption("MailWarningThreshold")
local expiryFound
local account, realm
for key, character in pairs(addon.db.global.Characters) do
account, realm = strsplit(".", key)
if allAccounts == 1 or ((allAccounts == 0) and (account == THIS_ACCOUNT)) then -- all accounts, or only current and current was found
if allRealms == 1 or ((allRealms == 0) and (realm == GetRealmName())) then -- all realms, or only current and current was found
-- detect return vs delete
local numExpiredMails = _GetNumExpiredMails(character, threshold)
if numExpiredMails > 0 then
expiryFound = true
addon:SendMessage("DATASTORE_MAIL_EXPIRY", character, key, threshold, numExpiredMails)
end
end
end
end
if expiryFound then
-- global expiry message, register this one if your addon just wants to know that at least one mail has expired, and you don't care which.
addon:SendMessage("DATASTORE_GLOBAL_MAIL_EXPIRY", threshold)
end
end
function addon:OnInitialize()
addon.db = LibStub("AceDB-3.0"):New(addonName .. "DB", AddonDB_Defaults)
DataStore:RegisterModule(addonName, addon, PublicMethods)
DataStore:SetGuildCommCallbacks(commPrefix, GuildCommCallbacks)
DataStore:SetCharacterBasedMethod("GetMailboxLastVisit")
DataStore:SetCharacterBasedMethod("GetMailItemCount")
DataStore:SetCharacterBasedMethod("GetNumMails")
DataStore:SetCharacterBasedMethod("GetMailInfo")
DataStore:SetCharacterBasedMethod("GetMailSender")
DataStore:SetCharacterBasedMethod("GetMailExpiry")
DataStore:SetCharacterBasedMethod("GetMailSubject")
DataStore:SetCharacterBasedMethod("GetNumExpiredMails")
DataStore:SetCharacterBasedMethod("SaveMailToCache")
DataStore:SetCharacterBasedMethod("SaveMailAttachmentToCache")
addon:RegisterComm(commPrefix, DataStore:GetGuildCommHandler())
end
function addon:OnEnable()
addon:RegisterEvent("MAIL_SHOW", OnMailShow)
-- addon:RegisterEvent("BAG_UPDATE", OnBagUpdate)
addon:RegisterEvent("BAG_UPDATE", FCSP_OnBagUpdate)
addon:SetupOptions()
if GetOption("CheckMailExpiry") == 1 then
addon:ScheduleTimer(CheckExpiries, 5) -- check mail expiries 5 seconds later, to decrease the load at startup
end
end
function addon:OnDisable()
addon:UnregisterEvent("MAIL_SHOW")
addon:UnregisterEvent("BAG_UPDATE")
end
-- *** Hooks ***
local Orig_SendMail = SendMail
function SendMail(recipient, subject, body, ...)
-- this function takes care of saving mails sent to alts directly into their mailbox, so that client addons don't have to take care about it
local isRecipientAnAlt
for characterName, characterKey in pairs(DataStore:GetCharacters()) do -- browse alts on current realm
if strlower(characterName) == strlower(recipient) then -- if recipient is a known alt ..
local character = addon.db.global.Characters[characterKey]
for k, v in pairs(addon.Attachments) do -- .. save attachments into his mailbox
table.insert(character.Mails, { -- not in the mail cache, since they arrive directly in an alt's mailbox
icon = v.icon,
link = v.link,
count = v.count,
sender = UnitName("player"),
lastCheck = time(),
daysLeft = MAIL_EXPIRY,
} )
end
-- .. then save the mail itself + gold if any
local moneySent = GetSendMailMoney()
if (moneySent > 0) or (strlen(body) > 0) then
local mailIcon
if moneySent > 0 then
mailIcon = ICON_COIN
else
mailIcon = ICON_NOTE
end
table.insert(character.Mails, {
money = moneySent,
icon = mailIcon,
text = body,
subject = subject,
sender = UnitName("player"),
lastCheck = time(),
daysLeft = MAIL_EXPIRY,
} )
end
-- if the alt has never checked his mail before, this value won't be correct, so set it to make sure expiry returns proper results.
character.lastUpdate = time()
table.sort(character.Mails, function(a, b) -- show mails with the lowest expiry first
return a.daysLeft < b.daysLeft
end)
isRecipientAnAlt = true
break
end
end
if not isRecipientAnAlt then -- if recipient is not an alt, maybe it's a guildmate
SendGuildMail(recipient, subject, body)
end
Orig_SendMail(recipient, subject, body, ...)
end
local Orig_ReturnInboxItem = ReturnInboxItem
function ReturnInboxItem(index, ...)
local _, stationaryIcon, mailSender, mailSubject, mailMoney, _, _, numAttachments = GetInboxHeaderInfo(index)
local isRecipientAnAlt
local inboxText = ""
for characterName, characterKey in pairs(DataStore:GetCharacters()) do -- browse alts on current realm
if strlower(characterName) == strlower(mailSender) then -- if recipient is a known alt ..
local character = addon.db.global.Characters[characterKey]
if numAttachments then -- treat attachments as separate entries
SaveAttachments(character, index, UnitName("player"), MAIL_EXPIRY, true)
end
inboxText = GetInboxText(index) -- this marks the mail as read, no problem here since the mail is returned anyway
if (mailMoney > 0) or inboxText then -- if there's money or text .. save the entry
local mailIcon
if mailMoney > 0 then
mailIcon = ICON_COIN
else
mailIcon = stationaryIcon
end
table.insert(character.Mails, {
icon = mailIcon,
money = mailMoney,
text = inboxText,
subject = mailSubject,
sender = UnitName("player"),
lastCheck = time(),
daysLeft = MAIL_EXPIRY,
returned = true, -- this is the mail we're returning, so true
} )
end
isRecipientAnAlt = true
break
end
end
if not isRecipientAnAlt then -- if recipient is not an alt, maybe it's a guildmate
ReadMailAttachments(index)
SendGuildMail(mailSender, mailSubject, inboxText)
end
Orig_ReturnInboxItem(index, ...)
end