--[[ Postman: Building a Better Mailbox Bima, aka Tale, aka David C Lawrence ]]-- Postman = AceLibrary("AceAddon-2.0"):new("AceConsole-2.0", "AceEvent-2.0", "AceDB-2.0", "AceHook-2.1", "AceDebug-2.0") local L = AceLibrary("AceLocale-2.2"):new("Postman") local abacus = AceLibrary("Abacus-2.0") Postman.version = "2.0." .. string.sub("$Revision: 25931 $", 12, -3) Postman.date = string.sub("$Date: 2007-01-23 18:43:23 -0500 (Tue, 23 Jan 2007) $", 8, 17) local options = { handler = Postman, type = "group", args = { track = { order = 1.1, name = L["Track"], type = "execute", desc = L["Show time until mailed items are delivered."], func = "ReportTransit" }, autosend = { order = 1.2, type = "toggle", name = L["Autosend"], desc = L["Instantly send item when attached with Alt-LeftClick."], get = function() return Postman.db.profile.autosend end, set = function(v) Postman.db.profile.autosend = v end } } } -- PlayerMenu uses /pm as well. Postman:RegisterChatCommand({'/postman', '/pman', '/pm'}, options) -- Convenience variables. local recips local me = UnitName("player") local server = GetRealmName():trim() -- Initialize from MailTo, if MailTo_List exists. local initlist = MailToList and MailTo_List[GetCVar("realmName")] or {} Postman:RegisterDB("PostmanDB") Postman:RegisterDefaults("profile", { autosend = nil }) Postman:RegisterDefaults("realm", { recipients = initlist, lastrecip = nil }) -- Why yes, server IS redundant information, sadly so. Unfortunately it is -- difficult to parse out server names from the localized AceDB -- PLAYER_OF_REALM key that indexes the PostmanDB.chars array, and after -- pondering using different db types to store the tree, such as account, -- this still felt best. I'll gladly consider other points of view. Postman:RegisterDefaults("char", { intransit = {}, server = nil }) function Postman:OnInitialize() recips = self.db.realm.recipients -- Add self to list if not present. if not self:RecipFind(me) then table.insert(recips, me) end self.db.char.server = server --self:SetDebugging(true) end function Postman:OnEnable() self:Hook("SendMailFrame_SendMail", "SendMail", true) self:Hook("SendMailFrame_SendeeAutocomplete", "RecipComplete", true) self:Hook("ContainerFrameItemButton_OnModifiedClick", "PlaceItem", true) self:Hook("InboxFrame_OnClick", "TakeItem", true) self:HookScript(SendMailNameEditBox, "OnEditFocusGained", "FillRecip") self:HookScript(TradeFrame, "OnShow", "OnTradeShow") --- Delay reporting for 10 seconds to get past spammy load messages. self:ScheduleEvent(self.ReportDelivery, 10, self) self:ScheduleEvent(self.ReportTransit, 10, self) -- self:ScheduleEvent(self.ReportExpiry, 10, self) end --[[ Postman:Recip* functions are all for the drop down selection box. ]]-- -- The recipients list is an indexed list to make it easier to know its size, -- and sort it but LUA doesn't make it easy to look up an element by value. -- Could have made it a dictionary to make some things easier, but then the -- other things become mildly harder. Sort of a toss-up. function Postman:RecipFind(recip) for idx, name in pairs(recips) do if name:lower() == recip:lower() then return idx end end return nil end function Postman:RecipMenuInit() -- UIDROPDOWNMENU_MAXBUTTONS=4 -- for testing. -- Sanity check. The recipients list can only grow up to the maximum -- number of lines less one, reserving one for the Add/Remove lines button. -- Since you can't send mail to yourself, though, the layer you are -- logged in as is not included in the list. while #recips > UIDROPDOWNMENU_MAXBUTTONS do Postman:Print(L["%s removed; list too long"]: format(table.remove(recips))) end -- XXXtale why does this not seem to affect the order of the drop down? -- the indices reflect a correct order but they don't come out that way. table.sort(recips) local boxname = SendMailNameEditBox:GetText() -- If the filled in name is known, remember its index in our recip list local known = Postman:RecipFind(boxname) -- Make the list of recipients, skipping self. local info for idx, name in ipairs(recips) do if name ~= me then info = { text = name, value = idx, func = Postman.RecipSelect } info.checked = idx == known and 1 or nil UIDropDownMenu_AddButton(info) end end -- Now give options to add or remove names from the list. if boxname ~= "" then info = { notCheckable = 1 } if known then info.text = L["[Remove %s]"]:format(boxname) info.value = known info.func = Postman.RecipRemove elseif #recips < UIDROPDOWNMENU_MAXBUTTONS then info.text = L["[Add %s]"]:format(boxname) info.value = boxname info.func = Postman.RecipAdd else -- Suggestions for a better message are welcome. info.text = L["Recipient List Full"] info.textR = 255 info.textG = 0 info.textB = 0 end UIDropDownMenu_AddButton(info) end end function Postman:RecipMenuShow() this.tooltip = L["Select Recipient"] UIDropDownMenu_Initialize(this:GetParent(), self.RecipMenuInit) UIDropDownMenu_SetAnchor(0, 0, this:GetParent(), "TOPRIGHT", this:GetName(), "BOTTOMRIGHT") end function Postman:RecipSelect() local recip = recips[this.value] SendMailNameEditBox:SetText(recip) SendMailSubjectEditBox:SetFocus() end function Postman:RecipAdd() table.insert(recips, this.value) table.sort(recips) end function Postman:RecipRemove() table.remove(recips, this.value) end -- Autocompletion of recipient names. -- Probably should be cached somehow to allow fast lookup. function Postman:RecipComplete() -- First look in our memorized recipients list, then amongst friends, -- then amongst guild mates. local input = this:GetText() for _, name in pairs(recips) do if self:NameMatch(input, name) then return end end -- Use Blizzard's autocomplete for friends and guildmates. self.hooks["SendMailFrame_SendeeAutocomplete"]() end function Postman:NameMatch(input, name) if name and name:lower():find(input:lower(), 1, true) == 1 then this:SetText(name) this:HighlightText(strlen(input), -1) return true end return false end -- Fills in empty recipient field with the last recipient, called -- as SendMailNameEditBox OnEditFocusGained handler. function Postman:FillRecip(editbox) if editbox:GetText() == "" and self.db.realm.lastrecip and self.db.realm.lastrecip ~= me then editbox:SetText(self.db.realm.lastrecip) -- Most of the time folks are just mailing items around, with -- Subject autofilled and no body, so might as well just clear -- the focus back out to the outer UI. editbox:ClearFocus() else -- Highlight whatever (if anything) is there for possible replacement. editbox:HighlightText(0,-1) end -- Call original script. self.hooks[editbox].OnEditFocusGained() end function Postman:SendMail() -- Remember to whom we're sending. self.db.realm.lastrecip = SendMailNameEditBox:GetText() -- Clear focus so it is regained when frame is reset. SendMailNameEditBox:ClearFocus() -- If an item is being sent, then delivery is delayed. -- Second value is item texture, not needed. local name, _, count = GetSendMailItem() if name then if count > 1 then name = name .. "(" .. count .. ")" end -- Item mail takes a mininum of one hour, but occasionally just a -- little longer. Wait just a little longer to help to head off -- complaints of "Postman said it was delivered, but it wasn't!" local wait = 60 * 62 table.insert(self.db.char.intransit, { to = self.db.realm.lastrecip, n = name, t = time() + wait }) if not self.nextevent then self.nextevent = self:ScheduleEvent(self.ReportDelivery, wait, self) end end -- Call original script. self.hooks["SendMailFrame_SendMail"]() -- XXXtale should take note if something failed, though. managed to -- accidentally try to send mail to to myself (ie, to the character I -- was logged in as) and got back an error, but postman was dutifully -- tracking it as though it was sent. end function Postman:PlaceItem(button) -- Don't even bother doing anything if this isn't an Alt-Left click, -- or if already holding an item. if not (button == "LeftButton" and IsAltKeyDown() and not CursorHasItem()) then self.hooks["ContainerFrameItemButton_OnModifiedClick"](button) return end local bag = this:GetParent():GetID(); local slot = this:GetID(); -- First, see if we're sending mail. if SendMailFrame:IsVisible() then PickupContainerItem(bag, slot) ClickSendMailItemButton() if self.db.profile.autosend then SendMailMailButton:Click() end return -- XXXtale this is where mass mail adding will go. -- Listing an auction, just for UI consistency. elseif AuctionFrameAuctions and AuctionFrameAuctions:IsVisible() then PickupContainerItem(bag, slot) ClickAuctionSellItemButton() return -- Add to existing trade. elseif TradeFrame:IsVisible() then for i = 1, 6 do if not GetTradePlayerItemLink(i) then PickupContainerItem(bag, slot) ClickTradeButton(i) return end end -- Initiate trade. elseif UnitExists("target") and UnitIsFriend("player", "target") and UnitIsPlayer("target") and CheckInteractDistance("target", 2) then InitiateTrade("target") self.trade = { UnitName("target"), bag, slot } -- Postman.trade only has a validity period of 2 seconds; if it -- doesn't get reset to nil by use within that time, force it to nil. self:ScheduleEvent(function() self.trade = nil end, 2) return end end function Postman:OnTradeShow(object) self.hooks[object].OnShow() if self.trade and self.trade[1] == UnitName("NPC") and not CursorHasItem() then PickupContainerItem(self.trade[2], self.trade[3]) ClickTradeButton(1) end self.trade = nil end function Postman:TakeItem(idx) self:Debug("TakeItem: enter") -- Courtesy of WoWWiki: -- packageIcon, stationeryIcon, sender, subject, money, CODAmount, -- daysLeft, hasItem, wasRead, wasReturned, textCreated, canReply -- = GetInboxHeaderInfo(index); local _, _, from, _, money, cod, expires, hasitem, wasread, _, _, _ = GetInboxHeaderInfo(idx) if arg1 ~= "RightButton" or IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown() then self:Debug("TakeItem: calling original OnClick and returning") self.hooks["InboxFrame_OnClick"](idx) return end -- For inexplicable reasons, GetInboxText() nils this:GetChecked() -- the very first time a message is read, which screws with being able -- to use the original InboxFrame_OnClick handler. local checked = this:GetChecked() -- XXXtale todo: report auction summary local body, _, _, invoice = GetInboxText(idx) this:SetChecked(checked) self:Debug(format("hasitem is %s, money=%d, invoice is %s, body is '%s'", hasitem and "true" or "false", money, invoice and "true" or "false", body or "nil")) -- If there is a message with no items or money, show it. -- By design, these messages will never be autodeleted. -- -- For reasons that escape me, just relying on body not being nil fails. -- When a message is first read using GetInboxText(), body is nil no matter -- whether there really is a body or not. Blizzard uses a some sort of -- magical nil value that gets filled in later. I tested this by ripping -- OpenMail_Update out of MailFrame.lua and discovering that bodyText is -- nil even when OpenMailBodyText:SetText(bodyText) is called there. -- After the very first time a message is loaded in the game, things work -- fine for all subsequent times, even after relogging. So weird. -- I'm not sure what happens after server restarts. -- -- Using "not wasread" means a message with only a subject and no -- body/items/money that hadn't yet been read won't be autodeleted, -- but that seems to be reasonable behaviour. if not hasitem and money == 0 and not wasread or (body and body ~= "") then self:Debug("TakeItem: showing message via old OnClick and returning") self.hooks["InboxFrame_OnClick"](idx) return end if cod and cod > 0 then if cod > GetMoney() then StaticPopup_Show("COD_ALERT") else -- In theory I could just keep the message closed, but it -- seems more likely to causes issues with Blizzard's code. -- Also use only this part of Blizzard's InboxFrame_OnClick -- rather than calling it in case the window is already open. InboxFrame.openMailID = idx OpenMail_Update() ShowUIPanel(OpenMailFrame) PlaySound("igSpellBookOpen") StaticPopup_Show("COD_CONFIRMATION") end return end local took = "" -- Taking money apparently *needs* to come before taking items -- because taking the item first seems to require some sort of server -- sync that doesn't allow the money taking to go through. if money > 0 then took = abacus:FormatMoneyFull(money, true) TakeInboxMoney(idx) end -- Can't take money and item without some delay, so this block -- only executes if there is no money. if hasitem and money == 0 then -- name, texture, count, quality, canuse local name, _, count, quality = GetInboxItem(idx) TakeInboxItem(idx) -- r, g, b, hex _, _, _, hex = GetItemQualityColor(quality) if count > 1 then took = count .. " " end took = took .. hex .. name .. "|r" end if not body or body == "" then self:Debug("TakeItem: want to delete message") -- This doesn't seem to work after taking money, for reasons I -- can only vaguely guess are related to syncing with the server. -- Before deleting, attempt to ensure that money and an item are -- no longer attached. _, _, _, _, money, _, _, hasitem = GetInboxHeaderInfo(idx) if not hasitem and money == 0 then self:Debug("TakeItem: no item or money, deleting message") DeleteInboxItem(idx) end end if took ~= "" then self:Print(L["Received %s from %s"]:format(took, from or UNKNOWN)) end end -- "pofr" = formated PLAYER_OF_REALM string from AceDB local function fromstring(pofr, onserver) local s = pofr:gsub(" .*", "") if onserver ~= server then s = s .. "(" .. onserver .. ")" end return s end function Postman:ReportDelivery() local now = time() local next local rangbell for pofr, char in pairs(PostmanDB.chars) do local from = fromstring(pofr, char.server) local reported = 0 for idx, msg in ipairs(char.intransit or {}) do if msg.t <= now then --self:Print(L["%s, %s to %s, delivered."]: -- format(msg.n, from, msg.to)) if not rangbell then -- PlaySound("AuctionWindowOpen") rangbell = true end reported = idx else -- Prepare the next scheduled event. if not next or next > msg.t then next = msg.t end break end end -- Remove the processed messages. -- I suspect this is not the best way to do this. while reported > 0 do table.remove(char.intransit, 1) reported = reported - 1 end end if next then self.nextevent = self:ScheduleEvent(self.ReportDelivery, next - now, self) end end function Postman:ReportTransit() local now = time() for pofr, char in pairs(PostmanDB.chars) do local from = fromstring(pofr, char.server) for idx, msg in ipairs(char.intransit or {}) do --self:Print(L["%s, %s to %s, due in %dm."]: -- format(msg.n, from, msg.to, -- (msg.t - now) / 60 + 1)) end end end