-- ------------------------------------------------------------------------------ -- -- 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