wobin@17: -- A Twitter client of sorts for World of Warcraft wobin@17: -- Author: Wobin wobin@17: -- Email: wobster@gmail.com wobin@17: -- wobin@17: Squawk = LibStub("AceAddon-3.0"):NewAddon("Squawk") wobin@17: wobin@17: Squawk.Model = {} wobin@17: Squawk.View = {} wobin@17: Squawk.Controller = {} wobin@17: wobin@17: local Model = Squawk.Model wobin@17: local View = Squawk.View wobin@17: local Controller = Squawk.Controller wobin@17: wobin@17: Model.UserSettings = {} wobin@17: local Settings = Model.UserSettings wobin@17: wobin@17: local defaults = { wobin@17: profile = { wobin@17: Squawks = {}, wobin@17: Follower = {}, wobin@17: Following = {}, wobin@17: Pending = {}, wobin@17: Requested = {}, wobin@17: Blocked = {}, wobin@17: } wobin@17: } wobin@17: wobin@17: function Squawk:OnInitialize() wobin@17: Model.db = LibStub("AceDB-3.0"):New("SquawkDB", defaults) wobin@17: Model.Squawks = Model.db.profile.Squawks wobin@17: Settings.Follower = Model.db.profile.Follower wobin@17: Settings.Following = Model.db.profile.Following wobin@17: Settings.Pending = Model.db.profile.Pending wobin@17: Settings.Requested = Model.db.profile.Requested wobin@17: Settings.Blocked = Model.db.profile.Blocked wobin@17: Settings.Private = Model.db.profile.Private wobin@17: wobin@17: LibStub("AceComm-3.0"):Embed(Controller) wobin@17: LibStub("AceTimer-3.0"):Embed(Controller) wobin@17: Controller:RegisterComm("Squawk", Controller.ReceiveMessage) wobin@17: LibStub("AceConsole-3.0"):Embed(View) wobin@17: wobin@17: end wobin@17: wobin@17: -- Model -- wobin@17: --[[ wobin@17: --Each Squawk will have the following information: wobin@17: -- * Owner (Name) wobin@17: -- * Time (Epoch) wobin@17: -- * Message (140 characters) wobin@17: -- * ReplyTo (Name) wobin@17: -- * Related (Names) wobin@17: -- wobin@17: -- Each User will have the following lists: wobin@17: -- * Follower wobin@17: -- * Following wobin@17: -- * Blocked wobin@17: -- * Pending (Requests to follow that you haven't acted on) wobin@17: -- * Requested (Requests to follow that you have made) wobin@17: -- * Privacy State wobin@17: -- wobin@17: -- A user can only request to follow an online person. Requests can be approved wobin@17: -- on or offline, but the initial request must be made online. wobin@17: -- wobin@17: -- When a user makes a request to follow a private user, the subsequent paths occur: wobin@17: -- - Followee is added to Settings.Requested wobin@17: -- - Followee receives 'follow request' -> (their) Settings.Pending wobin@17: -- - Followee acts on request -> (their) Settings.Pending cleared wobin@17: -- 1) Follwer is online wobin@17: -- - Follower receives 'request accepted' -> Added to Settings.Following and wobin@17: -- cleared from Settings.Requested wobin@17: -- 2) Follower is offline wobin@17: -- - The next time Follower is online and recieves a Squawk we check if there wobin@17: -- is a Settings.Requested for that name, and if so assume they have approved wobin@17: -- and clear/add records appropriately. wobin@17: -- wobin@17: -- For updating, there can be a few methods. wobin@17: -- wobin@17: -- Guild open: you're not private, you broadcast to the guild your last X wobin@17: -- squawks on login wobin@17: -- wobin@17: -- followers: wobin@17: --]] wobin@17: Model.Squawks = {} wobin@17: local Squawks = Model.Squawks wobin@17: Squawks.Main = {} wobin@17: Squawks.Owners = {} wobin@17: wobin@17: local function wrap(str, limit) wobin@17: limit = limit or 72 wobin@17: local here = 1 wobin@17: return str:gsub("(%s+)()(%S+)()", wobin@17: function(sp, st, word, fi) wobin@17: if fi-here > limit then wobin@17: here = st wobin@17: return "\n"..word wobin@17: end wobin@17: end) wobin@17: end wobin@17: wobin@17: function Squawks:new(Message, Owner) wobin@17: local o = {} wobin@17: o.Owner = Owner or UnitName("player") wobin@17: o.Message = wrap(Message) wobin@17: o.Time = time() wobin@17: local reply, to = strsplit("@", ((strsplit(" ", Message)))) wobin@17: if reply == "" then wobin@17: o.ReplyTo = to wobin@17: end wobin@17: wobin@17: o.Related = {} wobin@17: wobin@17: for word in string.gmatch(Message, "@(%a+)") do wobin@17: if word ~= o.ReplyTo or "" then wobin@17: table.insert(o.Related, word) wobin@17: end wobin@17: end wobin@17: wobin@17: table.insert(self.Main, o) wobin@17: wobin@17: if not self.Owners[Owner] then wobin@17: self.Owners[Owner] = {} wobin@17: end wobin@17: table.insert(self.Owners[Owner], o) wobin@17: wobin@17: return o wobin@17: end wobin@17: wobin@17: function Squawks:Sort(Squawks) wobin@17: table.sort(Squawks or self.Main, function(a,b) return a.Time > b.Time end) wobin@17: return Squawks or self.Main wobin@17: end wobin@17: wobin@17: function Squawks:GetOwn(Squawks) wobin@17: local mine = {} wobin@17: for _, squawk in ipairs(Squawks or self.Main) do wobin@17: if squawk.Owner == UnitName("player") then wobin@17: table.insert(mine, squawk) wobin@17: end wobin@17: end wobin@17: return self:Sort(mine) wobin@17: end wobin@17: wobin@17: function Squawks:GetLast10(Squawks) wobin@17: local mine = {} wobin@17: Squawks = Squawks or self.Main wobin@17: local limit = #Squawks < 10 and #Squawks or 10 wobin@17: wobin@17: Squawks = Squawk:Sort(Squawks) wobin@17: wobin@17: for i=1,limit do wobin@17: table.insert(mine, Squawks[i]) wobin@17: end wobin@17: return mine wobin@17: end wobin@17: wobin@17: -- initially called with no arguments to get the latest timestamp of wobin@17: -- my squawks, or with a name to find the latest timestamp of all wobin@17: -- squawks from that user wobin@17: function Squawks:GetLatestTimestamp(Name, Squawks) wobin@17: if Name then wobin@17: if self.Owners[Name] then wobin@17: return self:GetLatestTimestamp(nil, self.Owners[Name]) wobin@17: else wobin@17: return -1 -- No squawks exist for that name in our records wobin@17: end wobin@17: end wobin@17: wobin@17: Squawks = Squawks or self.Main or {} wobin@17: local latest = self:Sort(Squawks) wobin@17: return latest and #latest > 0 and latest[1].Time or -1 wobin@17: end wobin@17: wobin@17: function Squawks:GetLatestSquawks(Timestamp) wobin@17: local latest = {} wobin@17: for i, squawk in ipairs(self:Sort()) do wobin@17: if squawk.Time > Timestamp and i < 10 then wobin@17: table.insert(latest, squawk) wobin@17: else wobin@17: return latest wobin@17: end wobin@17: end wobin@17: end wobin@17: wobin@17: function Settings:IsPrivate() wobin@17: return Settings.Private wobin@17: end wobin@17: wobin@17: function Settings:TogglePrivate() wobin@17: Settings.Private = not Settings.Private wobin@17: end wobin@17: wobin@17: function Settings:AddFollower(Name) wobin@17: Settings.Follower[Name] = 1 wobin@17: self:RemovePending(Name) wobin@17: end wobin@17: wobin@17: function Settings:AddFollowing(Name) wobin@17: Settings.Following[Name] = 1 wobin@17: self:RemoveRequested(Name) wobin@17: end wobin@17: wobin@17: function Settings:AddBlock(Name) wobin@17: Settings.Blocked[Name] = 1 wobin@17: self:RemoveFollower(Name) wobin@17: self:RemoveFollowing(Name) wobin@17: end wobin@17: wobin@17: function Settings:AddPending(Name) wobin@17: Settings.Pending[Name] = 1 wobin@17: end wobin@17: wobin@17: function Settings:AddRequested(Name) wobin@17: Settings.Requested[Name] = 1 wobin@17: end wobin@17: wobin@17: function Settings:RemoveFollower(Name) wobin@17: if Settings.Follower[Name] then wobin@17: Settings.Follower[Name] = nil wobin@17: end wobin@17: end wobin@17: wobin@17: function Settings:RemoveFollowing(Name) wobin@17: if Settings.Following[Name] then wobin@17: Settings.Following[Name] = nil wobin@17: end wobin@17: end wobin@17: wobin@17: function Settings:RemoveBlock(Name) wobin@17: if Settings.Blocked[Name] then wobin@17: Settings.Blocked[Name] = nil wobin@17: end wobin@17: end wobin@17: wobin@17: function Settings:RemovePending(Name) wobin@17: if Settings.Pending[Name] then wobin@17: Settings.Pending[Name] = nil wobin@17: end wobin@17: end wobin@17: wobin@17: function Settings:RemoveRequested(Name) wobin@17: if Settings.Requested[Name] then wobin@17: Settings.Requested[Name] = nil wobin@17: end wobin@17: end wobin@17: wobin@17: --Controller-- wobin@17: wobin@17: function Controller:TheyWantToFollowMe(Name) wobin@17: if Settings:IsPrivate() then wobin@17: Settings:AddPending(Name) wobin@17: self:PutForwardFollowRequest(Name) wobin@17: self:SendMessageToTarget(Name, "#Pending|"..UnitName("player")) wobin@17: else wobin@17: Settings:AddFollower(Name) wobin@17: View:NotifyOfNewFollower(Name) wobin@17: self:SendMessageToTarget(Name, "#Follow|"..UnitName("player")) wobin@17: end wobin@17: end wobin@17: wobin@17: function Controller:TheyWantToUnfollowMe(Name) wobin@17: Settings:RemoveFollower(Name) wobin@17: end wobin@17: wobin@17: function Controller:IWantToFollowThem(Name) wobin@17: self:SendMessageToTarget(Name, "#Request|"..UnitName("player")) wobin@17: Settings:AddRequested(Name) wobin@17: end wobin@17: wobin@17: function Controller:IWantToUnfollowThem(Name) wobin@17: Settings:RemoveFollowing(Name) wobin@17: self:SendMessageToTarget(Name, "#Unfollow|"..UnitName("player")) wobin@17: View:NotifyOfUnfollowing(Name) wobin@17: end wobin@17: wobin@17: function Controller:IAmNowFollowingThem(Name) wobin@17: Settings:AddFollowing(Name) wobin@17: View:NotifyOfNewFollowing(Name) wobin@17: end wobin@17: wobin@17: function Controller:AddANewSquawk(Name, Message, Source) wobin@17: if not Settings.Blocked[Name] then wobin@17: wobin@17: if Source == "WHISPER" then wobin@17: if Settings.Requested[Name] then -- We've been approved offline! wobin@17: Settings:AddFollowing(Name) wobin@17: end wobin@17: wobin@17: if not Settings.Following[Name] then -- If we're no longer following this person wobin@17: self:SendMessageToTarget(Name, "#Unfollow|"..UnitName("player")) wobin@17: return wobin@17: end wobin@17: end wobin@17: wobin@17: if Source == "GUILD" and Name == UnitName("player") then wobin@17: return wobin@17: end wobin@17: wobin@17: table.insert(Model.Squawks, Squawk:new(Message, Name)) wobin@17: View:UpdateSquawkList() wobin@17: end wobin@17: end wobin@17: wobin@17: local trigger wobin@17: local function RepressFailure(frame, event, ...) wobin@17: if arg1:match(string.gsub(ERR_CHAT_PLAYER_NOT_FOUND_S, "%%s", "(.*)")) then wobin@17: if trigger then Controller:CancelTimer(trigger, true) end wobin@17: trigger = Controller:ScheduleTimer( wobin@17: function() wobin@17: ChatFrame_RemoveMessageEventFilter("CHAT_MSG_SYSTEM", RepressFailure) wobin@17: end, 3) -- Give it three seconds and then remove the filter. wobin@17: return true wobin@17: else wobin@17: return false, unpack(...) wobin@17: end wobin@17: end wobin@17: wobin@17: function Controller:SendNewSquawk(Message) wobin@17: if not Settings:IsPrivate() then wobin@17: self:SendMessageToGuild("#Squawk|"..UnitName("player").."|"..Message) wobin@17: end wobin@17: wobin@17: self:AddANewSquawk(UnitName("player"), Message) wobin@17: for name, _ in pairs(Settings.Following) do wobin@17: self:SendMessageToTarget(name, "#Squawk|"..UnitName("player").."|"..Message) wobin@17: end wobin@17: end wobin@17: wobin@17: function Controller:ImPending(Name) wobin@17: View:NotifyOfPending(Name) wobin@17: end wobin@17: wobin@17: function Controller:PutForwardFollowRequest(Name) wobin@17: View:NotifyOfPendingRequest(Name) wobin@17: end wobin@17: wobin@17: function Controller:ApprovePendingRequest(Name) wobin@17: Settings:AddFollower(Name) wobin@17: View:NotifyOfNewFollower(Name) wobin@17: self:SendMessageToTarget(Name, "#Follow|"..UnitName("player")) wobin@17: end wobin@17: wobin@17: wobin@17: wobin@17: function Controller:SendMessageToTarget(Name, Message) wobin@17: ChatFrame_AddMessageEventFilter("CHAT_MSG_SYSTEM", RepressFailure) wobin@17: self:SendCommMessage("Squawk", Message, "WHISPER", Name) wobin@17: end wobin@17: wobin@17: function Controller:SendMessageToGuild(Message) wobin@17: self:SendCommMessage("Squawk", Message, "GUILD") wobin@17: end wobin@17: wobin@17: local Parse = { wobin@17: ["#Pending"] = Controller.ImPending, wobin@17: ["#Follow"] = Controller.IAmNowFollowingThem, wobin@17: ["#Unfollow"] = Controller.TheyWantToUnfollowMe, wobin@17: ["#Squawk"] = Controller.AddANewSquawk, wobin@17: ["#Request"] = Controller.TheyWantToFollowMe, wobin@17: } wobin@17: wobin@17: function Controller:ReceiveMessage(Message, Distribution, Sender) wobin@17: local command, name, info = strsplit("|",Message) wobin@17: View:Print(Distribution..":"..Message) wobin@17: Parse[command](Controller, name, info, Distribution) wobin@17: end wobin@17: wobin@17: -- View -- wobin@17: wobin@17: function View:UpdateSquawkList() wobin@17: self:Print("Updated Squawk List") wobin@17: self:ShowMeMySquawks() wobin@17: end wobin@17: wobin@17: function View:NotifyOfPending(Name) wobin@17: self:Print(Name.." will have to approve your request") wobin@17: end wobin@17: wobin@17: function View:NotifyOfPendingRequest(Name) wobin@17: self:Print(Name.." wants to follow you.") wobin@17: end wobin@17: wobin@17: function View:NotifyOfNewFollowing(Name) wobin@17: self:Print("You are now following "..Name) wobin@17: end wobin@17: wobin@17: function View:NotifyOfUnfollowing(Name) wobin@17: self:Print("You are no longer following "..Name) wobin@17: end wobin@17: wobin@17: function View:NotifyOfNewFollower(Name) wobin@17: self:Print(Name.." is now following you") wobin@17: end wobin@17: wobin@17: function View:ShowMeMySquawks() wobin@17: for _,squawk in ipairs(Model.Squawks.Main) do wobin@17: self:Print(squawk.Message) wobin@17: end wobin@17: end wobin@17: wobin@17: function View:ShowMeMyFollowers() wobin@17: self:Print("My followers are:") wobin@17: for name,_ in pairs(Settings.Follower) do wobin@17: self:Print(name) wobin@17: end wobin@17: end wobin@17: wobin@17: function View:ShowMeWhoImFollowing() wobin@17: self:Print("I am following:") wobin@17: for name,_ in pairs(Settings.Following) do wobin@17: self:Print(name) wobin@17: end wobin@17: end wobin@17: wobin@17: function View:ShowMeWhoIveBlocked() wobin@17: self:Print("I've blocked:") wobin@17: for name,_ in pairs(Settings.Blocked) do wobin@17: self:Print(name) wobin@17: end wobin@17: end wobin@17: wobin@17: local TimeSpan = { [1] = {"second", 60, 1}, wobin@17: [2] = {"minute", 3600, 60}, wobin@17: [3] = {"hour", 86400, 3600} } wobin@17: wobin@17: function View:GetTime(stime) wobin@17: local lapsed = difftime(time(), stime) wobin@17: if lapsed < 86400 then -- if we're still in the same day... wobin@17: for _,span in ipairs(TimeSpan) do wobin@17: if lapsed < span[2] then wobin@17: local timespan = math.floor(lapsed/span[3]) wobin@17: if timespan == 1 then wobin@17: timespan = timespan .." ".. span[1] wobin@17: else wobin@17: timespan = timespan .. " ".. span[1].."s" wobin@17: end wobin@17: return timespan.. " ago" wobin@17: end wobin@17: end wobin@17: end wobin@17: return date("%I:%M %p %b %d", stime) wobin@17: end wobin@17: wobin@17: local LDBFeed = LibStub("LibDataBroker-1.1"):NewDataObject("Squawk", {type = "data source", text = "Awk!"}) wobin@17: local QTip = LibStub("LibQTip-1.0") wobin@17: local QTipClick = LibStub("LibQTipClick-1.0") wobin@17: local tooltip = {} wobin@17: wobin@17: local function HideTooltip() wobin@17: if MouseIsOver(tooltip) then return end wobin@17: tooltip:SetScript("OnLeave", nil) wobin@17: tooltip:Hide() wobin@17: QTip:Release(tooltip) wobin@17: tooltip = nil wobin@17: end wobin@17: wobin@17: local function ReplyToMe(cell, Owner, event) wobin@17: View:Print("Replying to @"..Owner) wobin@17: end wobin@17: wobin@17: local function AddLine(tooltip, Line, Number, Owner, TimeStamp) wobin@17: local x,y wobin@17: if #Line < 79 then wobin@17: y,x = tooltip:AddNormalLine(Number, Owner, Line, TimeStamp) wobin@17: else wobin@17: y,x = tooltip:AddNormalLine(Number, Owner, Line:sub(1, 80).."-", TimeStamp) wobin@17: AddLine(tooltip, Line:sub(81)) wobin@17: end wobin@17: if not TimeStamp then return end wobin@17: wobin@17: -- Now add the reply clickback wobin@17: tooltip:SetCell(y, 5, " ", Owner) wobin@17: tooltip.lines[y].cells[5]:SetBackdrop({bgFile= "Interface\\Addons\\Squawk\\reply"}) wobin@17: if not tooltip.lines[y].cells[5]:GetScript("OnHide") then wobin@17: tooltip.lines[y].cells[5]:SetScript("OnHide", function(self) self:SetBackdrop(nil) self:SetScript("OnHide", nil) end) wobin@17: end wobin@17: -- Reply clickback finished wobin@17: end wobin@17: wobin@17: function LDBFeed:OnEnter() wobin@17: tooltip = QTipClick:Acquire("Squawk",5, "LEFT", "CENTER", "LEFT", "RIGHT", "RIGHT") wobin@17: tooltip:Clear() wobin@17: tooltip:SetCallback("OnMouseDown", ReplyToMe) wobin@17: self.tooltip = tooltip wobin@17: for i,squawk in ipairs(Squawk:GetLast10(Model.Squawks)) do wobin@17: local head = true wobin@17: local message = {strsplit("\n",squawk.Message)} wobin@17: for _,line in ipairs(message) do wobin@17: if head then wobin@17: AddLine(tooltip, line, i..".", squawk.Owner, View:GetTime(squawk.Time)) wobin@17: head = false wobin@17: else wobin@17: AddLine(tooltip, line) wobin@17: end wobin@17: end wobin@17: end wobin@17: tooltip:SmartAnchorTo(self) wobin@17: tooltip:SetScript("OnLeave", HideTooltip) wobin@17: tooltip:Show() wobin@17: end wobin@17: wobin@17: function LDBFeed:OnLeave() wobin@17: HideTooltip() wobin@17: end wobin@17: --[[ wobin@17: wobin@17: function LDBFeed:OnClick(button) wobin@17: editbox:ClearAllPoints() wobin@17: editbox:SetPoint(GetTipAnchor(self)) wobin@17: editbox:Show() wobin@17: end wobin@17: wobin@17: local function GetTipAnchor(frame) wobin@17: if not x or not y then return "TOPLEFT", frame, "BOTTOMLEFT" end wobin@17: local hhalf = (x > UIParent:GetWidth()*2/3) and "RIGHT" or (x < UIParent:GetWidth()/3) and "LEFT" or "" wobin@17: local vhalf = (y > UIParent:GetHeight()/2) and "TOP" or "BOTTOM" wobin@17: return vhalf..hhalf, frame, (vhalf == "TOP" and "BOTTOM" or "TOP")..hhalf wobin@17: end wobin@17: wobin@17: local editbox = CreateFrame('EditBox', nil, UIParent) wobin@17: editbox:Hide() wobin@17: editbox:SetAutoFocus(true) wobin@17: editbox:SetHeight(32) wobin@17: editbox:SetWidth(350) wobin@17: editbox:SetFrameStrata("HIGH") wobin@17: editbox:SetFontObject('GameFontHighlightSmall') wobin@17: lib.editbox = editbox wobin@17: wobin@17: editbox:SetScript("OnEscapePressed", editbox.ClearFocus) wobin@17: editbox:SetScript("OnEnterPressed", editbox.ClearFocus) wobin@17: editbox:SetScript("OnEditFocusLost", editbox.Hide) wobin@17: editbox:SetScript("OnEditFocusGained", editbox.HighlightText) wobin@17: editbox:SetScript("OnTextChanged", function(self) wobin@17: self:SetText(self:GetParent().val) wobin@17: self:HighlightText() wobin@17: end) wobin@17: wobin@17: local left = editbox:CreateTexture(nil, "BACKGROUND") wobin@17: left:SetWidth(8) left:SetHeight(20) wobin@17: left:SetPoint("LEFT", -5, 0) wobin@17: left:SetTexture("Interface\\Common\\Common-Input-Border") wobin@17: left:SetTexCoord(0, 0.0625, 0, 0.625) wobin@17: wobin@17: local right = editbox:CreateTexture(nil, "BACKGROUND") wobin@17: right:SetWidth(8) right:SetHeight(20) wobin@17: right:SetPoint("RIGHT", 0, 0) wobin@17: right:SetTexture("Interface\\Common\\Common-Input-Border") wobin@17: right:SetTexCoord(0.9375, 1, 0, 0.625) wobin@17: wobin@17: local center = editbox:CreateTexture(nil, "BACKGROUND") wobin@17: center:SetHeight(20) wobin@17: center:SetPoint("RIGHT", right, "LEFT", 0, 0) wobin@17: center:SetPoint("LEFT", left, "RIGHT", 0, 0) wobin@17: center:SetTexture("Interface\\Common\\Common-Input-Border") wobin@17: center:SetTexCoord(0.0625, 0.9375, 0, 0.625) wobin@17: wobin@17: function lib.OpenEditbox(self) wobin@17: editbox:SetText(self.val) wobin@17: editbox:SetParent(self) wobin@17: editbox:SetPoint("LEFT", self) wobin@17: editbox:SetPoint("RIGHT", self) wobin@17: editbox:Show() wobin@17: end wobin@17: --]] wobin@17: