TradeSkillMaster/LibTSM/Service/MailTracking.lua

399 lines
13 KiB
Lua

-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local MailTracking = TSM.Init("Service.MailTracking")
local Database = TSM.Include("Util.Database")
local Delay = TSM.Include("Util.Delay")
local Event = TSM.Include("Util.Event")
local TempTable = TSM.Include("Util.TempTable")
local Log = TSM.Include("Util.Log")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Settings = TSM.Include("Service.Settings")
local AuctionTracking = TSM.Include("Service.AuctionTracking")
local private = {
settings = nil,
mailDB = nil,
itemDB = nil,
quantityDB = nil,
isOpen = false,
tooltip = nil,
callbacks = {},
expiresCallbacks = {},
cancelAuctionQuery = nil,
}
local PLAYER_NAME = UnitName("player")
-- ============================================================================
-- Module Loading
-- ============================================================================
MailTracking:OnSettingsLoad(function()
private.settings = Settings.NewView()
:AddKey("factionrealm", "internalData", "pendingMail")
:AddKey("factionrealm", "internalData", "expiringMail")
:AddKey("sync", "internalData", "mailQuantity")
-- update the structure of TSM.db.factionrealm.internalData.pendingMail
local toUpdate = TempTable.Acquire()
for character, pendingMailData in pairs(private.settings.pendingMail) do
if pendingMailData.items then
Log.Info("Converting pending mail data for %s", character)
toUpdate[character] = pendingMailData.items
end
end
for character, items in pairs(toUpdate) do
private.settings.pendingMail[character] = items
end
TempTable.Release(toUpdate)
-- remove data for characters we don't own
local toRemove = TempTable.Acquire()
for character in pairs(private.settings.pendingMail) do
if Settings.GetCharacterSyncAccountKey(character) ~= Settings.GetCurrentSyncAccountKey() then
Log.Info("Removed pending mail data for %s", character)
tinsert(toRemove, character)
end
end
for _, character in ipairs(toRemove) do
private.settings.pendingMail[character] = nil
end
TempTable.Release(toRemove)
private.mailDB = Database.NewSchema("MAIL_TRACKING_INBOX_INFO")
:AddUniqueNumberField("index")
:AddStringField("icon")
:AddStringField("sender")
:AddStringField("subject")
:AddStringField("itemString")
:AddNumberField("itemCount")
:AddNumberField("money")
:AddNumberField("cod")
:AddNumberField("expires")
:AddIndex("index")
:Commit()
private.itemDB = Database.NewSchema("MAIL_TRACKING_INBOX_ITEMS")
:AddNumberField("index")
:AddNumberField("itemIndex")
:AddStringField("itemLink")
:AddNumberField("quantity")
:Commit()
private.quantityDB = Database.NewSchema("MAIL_TRACKING_QUANTITY")
:AddUniqueStringField("itemString")
:AddNumberField("quantity")
:Commit()
private.settings.pendingMail[PLAYER_NAME] = private.settings.pendingMail[PLAYER_NAME] or {}
Event.Register("MAIL_SHOW", private.MailShowHandler)
Event.Register("MAIL_CLOSED", private.MailClosedHandler)
Event.Register("MAIL_INBOX_UPDATE", private.MailInboxUpdateHandler)
if TSM.IsWowClassic() then
-- handle auction buying
hooksecurefunc("PlaceAuctionBid", function(listType, index, bidPlaced)
local itemString = ItemString.GetBase(GetAuctionItemLink(listType, index))
local _, _, stackSize, _, _, _, _, _, _, buyout = GetAuctionItemInfo(listType, index)
if not itemString or bidPlaced ~= buyout then
return
end
private.ChangePendingMailQuantity(itemString, stackSize)
end)
-- handle auction canceling
hooksecurefunc("CancelAuction", function(index)
local itemString = ItemString.GetBase(GetAuctionItemLink("owner", index))
local _, _, stackSize = GetAuctionItemInfo("owner", index)
-- for some reason, these APIs don't always work properly, so check the return values
if not itemString or not stackSize or stackSize == 0 then
return
end
private.ChangePendingMailQuantity(itemString, stackSize)
end)
else
private.cancelAuctionQuery = AuctionTracking.CreateQuery()
:Equal("auctionId", Database.BoundQueryParam())
:Select("itemString", "stackSize")
-- handle auction canceling
hooksecurefunc(C_AuctionHouse, "CancelAuction", function(auctionId)
private.cancelAuctionQuery:BindParams(auctionId)
for _, itemString, stackSize in private.cancelAuctionQuery:Iterator() do
private.ChangePendingMailQuantity(itemString, stackSize)
end
end)
end
-- handle sending mail to alts
hooksecurefunc("SendMail", function(target)
local character = private.ValidateCharacter(target)
if not character then
return
end
private.settings.pendingMail[character] = private.settings.pendingMail[character] or {}
local altPendingMail = private.settings.pendingMail[character]
for i = 1, ATTACHMENTS_MAX_SEND do
local itemString = ItemString.GetBase(GetSendMailItemLink(i))
local _, _, _, quantity = GetSendMailItem(i)
if itemString and quantity then
altPendingMail[itemString] = (altPendingMail[itemString] or 0) + quantity
end
end
end)
-- handle returning mail to alts
hooksecurefunc("ReturnInboxItem", function(index)
local character = private.ValidateCharacter(select(3, GetInboxHeaderInfo(index)))
if not character then
return
end
private.settings.pendingMail[character] = private.settings.pendingMail[character] or {}
local altPendingMail = private.settings.pendingMail[character]
for i = 1, ATTACHMENTS_MAX_SEND do
local _, _, _, quantity = GetInboxItem(index, i)
local itemLink = quantity and quantity > 0 and private.GetInboxItemLink(index, i) or nil
local itemString = itemLink and ItemString.GetBase(itemLink) or nil
if itemString then
altPendingMail[itemString] = (altPendingMail[itemString] or 0) + quantity
end
end
end)
private.quantityDB:BulkInsertStart()
for itemString, quantity in pairs(private.settings.pendingMail[PLAYER_NAME]) do
if quantity > 0 then
private.quantityDB:BulkInsertNewRow(itemString, quantity)
else
private.settings.pendingMail[PLAYER_NAME][itemString] = nil
end
end
private.quantityDB:BulkInsertEnd()
end)
-- ============================================================================
-- Module Functions
-- ============================================================================
function MailTracking.RegisterCallback(callback)
tinsert(private.callbacks, callback)
end
function MailTracking.RegisterExpiresCallback(callback)
tinsert(private.expiresCallbacks, callback)
end
function MailTracking.BaseItemIterator()
return private.quantityDB:NewQuery()
:Select("itemString")
:IteratorAndRelease()
end
function MailTracking.CreateMailInboxQuery()
return private.mailDB:NewQuery()
end
function MailTracking.CreateMailItemQuery()
return private.itemDB:NewQuery()
end
function MailTracking.GetInboxItemLink(index)
return private.GetInboxItemLink(index, 1)
end
function MailTracking.GetMailType(index)
return private.GetMailType(index)
end
function MailTracking.GetQuantityByBaseItemString(baseItemString)
return private.quantityDB:GetUniqueRowField("itemString", baseItemString, "quantity") or 0
end
function MailTracking.RecordAuctionBuyout(baseItemString, stackSize)
if TSM.IsWowClassic() then
-- on classic, we'll handle auction buys via a direct hook
return
end
private.ChangePendingMailQuantity(baseItemString, stackSize)
end
-- ============================================================================
-- Event Handlers
-- ============================================================================
function private.MailShowHandler()
private.isOpen = true
end
function private.MailClosedHandler()
private.isOpen = false
end
function private.MailInboxUpdateHandler()
if not private.isOpen then
return
end
Delay.AfterFrame("mailInboxScan", 1, private.MailInboxUpdateDelayed)
end
function private.MailInboxUpdateDelayed()
if not private.isOpen then
return
end
wipe(private.settings.mailQuantity)
wipe(private.settings.pendingMail[PLAYER_NAME])
private.mailDB:TruncateAndBulkInsertStart()
private.itemDB:TruncateAndBulkInsertStart()
local expiration = math.huge
for i = 1, GetInboxNumItems() do
local _, _, sender, subject, money, cod, daysLeft, itemCount = GetInboxHeaderInfo(i)
if itemCount and itemCount > 0 and money and money > 0 then
expiration = min(expiration, time() + (daysLeft * 24 * 60 * 60))
end
if money and money > 0 then
private.itemDB:BulkInsertNewRow(i, 0, tostring(money), 0)
end
local firstItemString = nil
for j = 1, ATTACHMENTS_MAX do
local _, _, _, quantity = GetInboxItem(i, j)
local itemLink = quantity and quantity > 0 and private.GetInboxItemLink(i, j) or nil
local itemString = itemLink and ItemString.Get(itemLink) or nil
if itemString then
firstItemString = firstItemString or itemString
local baseItemString = ItemString.GetBaseFast(itemString)
private.settings.mailQuantity[baseItemString] = (private.settings.mailQuantity[baseItemString] or 0) + quantity
private.itemDB:BulkInsertNewRow(i, j, itemLink, quantity)
end
end
local mailType = private.GetMailType(i, firstItemString) or ""
if mailType == "BUY" then
local _, _, _, bid = GetInboxInvoiceInfo(i)
cod = bid
end
private.mailDB:BulkInsertNewRow(i, mailType, sender or UNKNOWN, subject or "--", firstItemString or "", itemCount or 0, money or 0, cod or 0, daysLeft)
end
private.quantityDB:TruncateAndBulkInsertStart()
for itemString, quantity in pairs(private.settings.mailQuantity) do
private.quantityDB:BulkInsertNewRow(itemString, quantity)
end
private.quantityDB:BulkInsertEnd()
private.itemDB:BulkInsertEnd()
private.mailDB:BulkInsertEnd()
private.settings.expiringMail[PLAYER_NAME] = expiration ~= math.huge and expiration or nil
for _, callback in ipairs(private.expiresCallbacks) do
callback()
end
for _, callback in ipairs(private.callbacks) do
callback()
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.ChangePendingMailQuantity(itemString, quantity)
assert(quantity ~= 0)
private.settings.pendingMail[PLAYER_NAME][itemString] = (private.settings.pendingMail[PLAYER_NAME][itemString] or 0) + quantity
if not private.quantityDB:HasUniqueRow("itemString", itemString) then
-- create a new row
private.quantityDB:NewRow()
:SetField("itemString", itemString)
:SetField("quantity", quantity)
:Create()
else
local row = private.quantityDB:GetUniqueRow("itemString", itemString)
local newValue = row:GetField("quantity") + quantity
assert(newValue >= 0)
if newValue == 0 then
-- remove this row
private.quantityDB:DeleteRow(row)
else
-- update this row
row:SetField("quantity", newValue)
:Update()
end
row:Release()
end
for _, callback in ipairs(private.callbacks) do
callback()
end
end
function private.ValidateCharacter(character)
if not character then
return
end
local characterName, realm = strsplit("-", strlower(character))
-- we only care to track mails with characters on this realm
if realm and realm ~= strlower(GetRealmName()) then
return
end
-- we only care to track mails with characters on this account
local result = nil
for _, name in Settings.CharacterByAccountFactionrealmIterator() do
if strlower(name) == characterName then
result = name
end
end
return result
end
function private.GetInboxItemLink(index, num)
local link = GetInboxItemLink(index, num)
if ItemString.GetBase(link) ~= ItemString.GetPetCage() then
return link
end
-- need to do tooltip scanning to get battlepet links
private.tooltip = private.tooltip or CreateFrame("GameTooltip", "TSM4MailingInboxTooltip", UIParent, "GameTooltipTemplate")
private.tooltip:SetOwner(UIParent, "ANCHOR_NONE")
private.tooltip:ClearLines()
local _, speciesId, level, breedQuality = private.tooltip:SetInboxItem(index, num)
assert(speciesId and speciesId > 0)
private.tooltip:Hide()
return ItemInfo.GetLink(strjoin(":", "p", speciesId, level, breedQuality))
end
function private.GetMailType(index, firstItemString)
local _, _, _, subject, money, cod, _, numItems, _, _, _, _, isGM = GetInboxHeaderInfo(index)
if isGM or (cod and cod > 0) or (money == 0 and (not numItems or numItems == 0)) then
return nil
end
local info = GetInboxInvoiceInfo(index)
if money and money > 0 and info == "seller" then
return "SALE"
elseif numItems and numItems > 0 and info == "buyer" then
return "BUY"
elseif not info then
if strfind(subject, string.gsub("^"..AUCTION_REMOVED_MAIL_SUBJECT, "%%s", "")) then
return "CANCEL"
end
if strfind(subject, string.gsub("^"..AUCTION_EXPIRED_MAIL_SUBJECT, "%%s", "")) then
return "EXPIRE"
end
end
return "OTHER"
end