------------------------------------------------------ -- SpamMeNot by Rob Stiles -- You may know me from such characters as... -- Octane on Bladefist (EU) -- Blurr on Lightninghoof (US) -- Kebian on Lethon (US) -- In game command: /SpamMeNot or /smn --[[ * NEW - CanComplainChat(lineID) -- determines if we should show the menu for reporting a line of chat spam (lineID comes from the player link in the chat line) * NEW - ComplainChat(lineID) -- complains about a particular line of chat spam * NEW - CanComplainInboxItem(index) -- determines if we should show the “report spam” button on a mail item * NEW - ComplainInboxItem(index) -- complains about a particular mail item ]] SpamMeNot = AceLibrary("AceAddon-2.0"):new("AceConsole-2.0", "AceHook-2.1", "AceEvent-2.0", "AceDB-2.0", "AceComm-2.0", "FuBarPlugin-2.0") local L = AceLibrary("AceLocale-2.2"):new("SpamMeNot") SpamMeNot:RegisterDB("SpamMeNotDB", "SpamMeNotDBPC") SpamMeNot:RegisterDefaults("profile", { minimumLevel = 2, alertEnabled = true, lookupInformEnabled = false, updateMonitorEnabled = true, spamMessage = L["You have been identified as a gold spammer and have been automatically ignored."], excludeChannels = {}, } ) SpamMeNot:RegisterDefaults('account', { totalSpamsBlocked = 0, }) function SpamMeNot:ToggleDebugMode() SpamMeNot.debugMode = not SpamMeNot.debugMode end function SpamMeNot:Debug(msg) if self.debugMode == true then self:Print("(Debug) " .. msg) end end function SpamMeNot:OnInitialize() self.weights = L:GetWeights() self.options = { type='group', args = { spam = { type='group', name=L["Spam"], desc=L["Spam options."], args = { alert = { name = L["Alert"], type = 'toggle', desc = L["Alert in chat panel when a new spammer is blocked."], get = function() return SpamMeNot:GetAlertEnabled() end, set = function() SpamMeNot:ToggleAlertEnabled() end, order = 1, --map = { [false] = L["Disabled"], [true] = L["Enabled"] } }, test = { name = L["Test"], type = 'text', desc = L["Show the spam rating of the supplied text."], get = false, set = function(value) SpamMeNot:TestText(value) end, usage = L[""], }, add = { name = L["Add"], type = 'text', desc = L["Add a player to the spammer list."], get = false, set = function(value) SpamMeNot:AddToSpammerList(value) end, usage = L[""], order = 2, }, remove = { name = L["Remove"], type = 'text', desc = L["Remove a player from the spammer list."], get = false, set = function(value) SpamMeNot:RemoveFromSpammerList(value) end, usage = L[""], order = 3, validate = {}, hidden = function() if #self.options.args.spam.args.remove.validate > 0 then return false else return true end end, }, message = { name = L["Message"], type = 'text', desc = L["The message displayed to a spammer that whispers you."], get = function () return SpamMeNot:GetSpamMessage() end, set = function(value) SpamMeNot:SetSpamMessage(value) end, usage = L[""], }, openui= { name = L["Open UI"], type = 'execute', desc = L["Opens the SpamMeNot UI."], func = function() SpamMeNot:OpenUI() end, hidden = function() if #self.options.args.spam.args.remove.validate == 0 or (SpamMeNot_Frame and SpamMeNot_Frame:IsVisible()) then return true else return false end end, }, tradechannel = { name = L["Trade Channel"], type = 'toggle', desc = L["Whether or not to monitor the trade channel."], get = function() return SpamMeNot:IsTradeMonitored() end, set = function() SpamMeNot:ToggleTradeMonitored() end, }, }, }, levelblock = { type='group', name=L["Level Block"], desc=L["Level based whisper blocking options."], args = { level = { name = L["Level"], desc = L["The minimum level of the people you will accept tells from."], type = 'range', get = function() return SpamMeNot:GetMinimumLevel() end, set = function (v) SpamMeNot:SetMinimumLevel(v) end, min = 1, max = 70, step = 1, }, inform = { name = L["Inform"], type = 'toggle', desc = L["Whether to inform the whisperer that their details are being looked up."], get = function() return SpamMeNot:GetLookupInformEnabled() end, set = function() SpamMeNot:ToggleLookupInformEnabled() end, } }, }, count = { name = L["Count"], type = 'execute', desc = L["Shows the number of blocked messages."], func = function () SpamMeNot:ShowBlockedCount() end }, updatemonitor = { name = L["Update monitor"], type = 'toggle', desc = L["Monitors the AceComm channel for SpamMeNot updates."], get = function() return SpamMeNot:IsUpdateMonitorEnabled() end, set = function() SpamMeNot:ToggleUpdateMonitor() end, }, debugmode = { name = "Debug", desc = "Toggle debug text", type = 'toggle', get = function() return SpamMeNot.debugMode end, set = function() SpamMeNot:ToggleDebugMode() end, hidden = function() return true end, }, }, } self:RegisterChatCommand(L["Slash-Commands"], self.options) self.OnMenuRequest = self.options self.tablet = AceLibrary("Tablet-2.0") self.debugMode = false self.spammers = {} self.suspects = {} self.whoCache = {} self.messageHold = {} self.whoQ = {} self.otherSpamMeNots = {} self.goAwayMsg = "[SpamMeNot] " .. self.db.profile.spamMessage self.playerName = UnitName("player") self.numSpamsBlocked = 0 self.numSpamsReported = 0 self:SetCommPrefix("SpamMeNot") self.skipNextWhisper = false for myMajor, myMinor in string.gmatch(self.version, "(%d+).(%d+)") do self.versionMajor = myMajor -0 self.versionMinor = myMinor -0 break end -- Localize GUI setglobal("SMN_TEXT_INNOCENT", L["Innocent"]) setglobal("SMN_TEXT_SPAMMER_TITLE", L["Spammer:"]) setglobal("SMN_TEXT_RATING_TITLE", L["Rating:"]) setglobal("SMN_TEXT_DATE_TITLE", L["Date:"]) setglobal("SMN_TEXT_TIME_TITLE", L["Time:"]) setglobal("SMN_TEXT_CANCEL", L["Cancel"]) setglobal("SMN_TEXT_ACCEPT", L["Accept"]) setglobal("SMN_TEXT_CLOSE", L["Close"]) setglobal("SMN_TEXT_REPORT", L["Report"]) end function SpamMeNot:IsTradeMonitored() local found = false for i=0,#self.db.profile.excludeChannels do if self.db.profile.excludeChannels[i] == L["Trade - City"] then found = true break end end return not found end function SpamMeNot:ToggleTradeMonitored() for i=0,#self.db.profile.excludeChannels do if self.db.profile.excludeChannels[i] == L["Trade - City"] then -- We were monitoring it table.remove(self.db.profile.excludeChannels, i) return end end -- If we get this far then we're not monitoring it table.insert(self.db.profile.excludeChannels, L["Trade - City"]) end function SpamMeNot:IsChannelExcluded(id) local _, channelName = GetChannelName(id) if (channelName) then for i=1,#self.db.profile.excludeChannels do local dbChannel = self.db.profile.excludeChannels[i] if string.sub(channelName, 1, string.len(dbChannel)) == dbChannel then return true end end end return false end function SpamMeNot:IsUpdateMonitorEnabled() return self.db.profile.updateMonitorEnabled end function SpamMeNot:ToggleUpdateMonitor() self.db.profile.updateMonitorEnabled = not self.db.profile.updateMonitorEnabled if (self.db.profile.updateMonitorEnabled) then self:RegisterComm(self.commPrefix, "GLOBAL") else self:UnregisterAllComms() end end function SpamMeNot:GetSpamMessage() return self.db.profile.spamMessage end function SpamMeNot:SetSpamMessage(value) self.db.profile.spamMessage = value self.goAwayMsg = "[SpamMeNot] " .. self.db.profile.spamMessage end function SpamMeNot:GetLookupInformEnabled() return self.db.profile.lookupInformEnabled end function SpamMeNot:ToggleLookupInformEnabled() self.db.profile.lookupInformEnabled = not self.db.profile.lookupInformEnabled end function SpamMeNot:GetAlertEnabled() return self.db.profile.alertEnabled end function SpamMeNot:ToggleAlertEnabled() self.db.profile.alertEnabled = not self.db.profile.alertEnabled end function SpamMeNot:TestText(text) self:Print(string.format(L["Supplied text had a rating of %d."], self:RateMessage(text))) end -- Sanitizes a provided name. For example "frED" becomes "Fred" function SpamMeNot:CapitalizeName(name) if (name) then name = string.lower(name) name = string.gsub(name, " ", "") if (string.len(name)) then local f = string.upper(string.sub(name, 1, 1)) name = string.lower(string.sub(name, 2, -1)) name = f .. name end end return name end function SpamMeNot:EnableFlash(enable) if (self.flashEnabled == enable) then return end self.flashEnabled = enable if (enable) then self:ScheduleEvent(self.FlashOn, 1, self) end end function SpamMeNot:FlashOn() self.IsFlashOn = true self:UpdateText() self:ScheduleEvent(self.FlashOff, 1, self) end function SpamMeNot:FlashOff() self.IsFlashOn = false self:UpdateText() if (self.flashEnabled == true) then self:ScheduleEvent(self.FlashOn, 1, self) end end function SpamMeNot:AddToSpammerList(name) if (name) then name = self:CapitalizeName(name) if (name == self.playerName) then self:Print(L["You can't add yourself to your own spammer list!"]) elseif (self.spammers[name]) then self:Print(string.format(L["%s is already on the spammer list."], name)) else local combinedText = "" local lineIDs = nil if (self.suspects[name]) then for i=1,#self.suspects[name].text do combinedText = combinedText .. self.suspects[name].text[i] .. "\n" end if #self.suspects[name].lineIDs then lineIDs = self.suspects[name].lineIDs end end local rating = self:RateMessage(combinedText) self:AddSpammer(name, lineIDs, rating, combinedText) self.suspects[name] = nil self:Print(string.format(L["You have added %s to your spammer list."], self:MakeSpammerLink(name))) end end end function SpamMeNot:RemoveFromSpammerList(name) if (name) then name = self:CapitalizeName(name) if (self.spammers[name]) then self.spammers[name] = nil local newTable = self.options.args.spam.args.remove.validate for i=1,#newTable do if (newTable[i] == name) then table.remove(newTable, i) break end end self.options.args.spam.args.remove.validate = newTable if SpamMeNot_Frame then self:InitializeSpammerCombo() end self:Print(string.format(L["%s was removed from the spammer list."], name)) else self:Print(string.format(L["%s is not on the spammer list."], name)) end end end function SpamMeNot:MakeSpammerLink(name, display) if (not display) then display = name end return string.format("|cffff5555|HSpamMeNot:%s|h[%s]|h|r", name, display) end function SpamMeNot:ListSpammers() self:Print(L["Spammer List"]) for spammer, data in pairs(self.spammers) do self:Print(" - " .. self:MakeSpammerLink(spammer)) end end function SpamMeNot:GetMinimumLevel() return self.db.profile.minimumLevel end function SpamMeNot:SetMinimumLevel(v) self.db.profile.minimumLevel = v end function SpamMeNot:ShowBlockedCount() self:Print(string.format(L["%d message(s) have been blocked."], self.numSpamsBlocked)) end function SpamMeNot:IsAssociate(name) if (name == self.playerName) or (self:IsFriend(name)) or (self:IsGuildie(name)) or (self:IsGroupMember(name)) or (self:IsRaidMember(name)) then return true else return false end end function SpamMeNot:IsCrossServerPlayer(name) if (string.find(name, "-") ~= nil) then return true else return false end end function SpamMeNot:IsGroupMember(name) for i=1,GetNumPartyMembers() do local unitName = UnitName("party"..i, true) if (name == unitName) then return true end end return false end function SpamMeNot:IsRaidMember(name) for i=1,GetNumRaidMembers() do local raidName = GetRaidRosterInfo(i); if (raidName == name) then return true end end return false end function SpamMeNot:IsFriend(name) local friend = false for i=1,GetNumFriends() do local lookup = GetFriendInfo(i); if (lookup == name) then friend = true break end end return friend end function SpamMeNot:IsGuildie(name) local guildie = false if (IsInGuild()) then for i=1,GetNumGuildMembers(true) do lookup = GetGuildRosterInfo(i); if (lookup == name) then guildie = true end end end return guildie end function SpamMeNot:OnEnable() self:RegisterEvent("CHAT_MSG_WHISPER") self:RegisterEvent("CHAT_MSG_SAY") self:RegisterEvent("CHAT_MSG_YELL") self:RegisterEvent("CHAT_MSG_CHANNEL") if (self.db.profile.updateMonitorEnabled) then self:RegisterComm(self.commPrefix, "GLOBAL") end self.flashing = false -- Install hook in to AceEvent!! -- This should mean I don't have to write individual code -- for every Ace2 mod that deals with chat messages. Most but not all, I'm sure. local AceEvent = AceLibrary:GetInstance("AceEvent-2.0") self:Hook(AceEvent, "TriggerEvent", "AceEvent_TriggerEvent", true) -- Hook SetItemRef self:Hook("SetItemRef", "SetItemRef", true) -- Install chat frame and friends frame hooks self:Hook("ChatFrame_OnEvent", "ChatFrame_OnEvent", true) self:Hook("FriendsFrame_OnEvent", "FriendsFrame_OnEvent", true) -- Hook unit pop-up self:SecureHook("UnitPopup_OnClick") -- Hook older WIM if (WIM_ChatFrame_OnEvent) then self:Hook("WIM_ChatFrame_OnEvent", "WIM_ChatFrame_OnEvent", true) end -- Hook newer WIM if (WIM_ChatFrame_MessageEventHandler) then self:Hook("WIM_ChatFrame_MessageEventHandler", "WIM_ChatFrame_MessageEventHandler", true) end -- Hook ForgottenChat if (FCCC_OnEvent) then self:Hook("FCCC_OnEvent", "FCCC_OnEvent", true) end -- Make sure we get a guild list from the server so we can white list everyone in it if (IsInGuild() == true) then GuildRoster() end -- And a friends list ShowFriends() -- Setup scheduled functions self:ScheduleEvent(self.BroadcastVersion, 15, self) self:ScheduleRepeatingEvent(self.PurgeSuspects, 15, self) -- calls self:PurgeSuspects() every 15 seconds self:ScheduleRepeatingEvent(self.WhoMaintenance, 5, self) self:ScheduleRepeatingEvent(self.HoldMaintenance, 5, self) -- Attach to right-click menus UnitPopupButtons["SPAMMENOT"] = { text = "[SpamMeNot] " .. L["Spammer!"], dist = 0 }; table.insert(UnitPopupMenus["FRIEND"],5, "SPAMMENOT") end function SpamMeNot:OnDisable() self:UnhookAll() self:UnregisterAllComms() -- Remove from right-click menus for i=1,#UnitPopupMenus["FRIEND"] do if UnitPopupMenus["FRIEND"][i] == "SPAMMENOT" then table.remove(UnitPopupMenus["FRIEND"], i) break end end end -- New version notification function SpamMeNot:BroadcastVersion() if (self.db.profile.updateMonitorEnabled) then self:SendCommMessage("GLOBAL", "Version", {["major"]=self.versionMajor, ["minor"]=self.versionMinor}) end end SpamMeNot.OnCommReceive = {} function SpamMeNot.OnCommReceive:Version(prefix, sender, distribution, otherVersion) self.otherSpamMeNots[sender] = string.format("%d.%.3d", otherVersion.major, otherVersion.minor) if (otherVersion.major > self.versionMajor) or (otherVersion.major == self.versionMajor and otherVersion.minor > self.versionMinor) then if (not self.newVersionAvailable) then self.newVersionAvailable = true self:Print(L["There is a new version of SpamMeNot available."]) PlaySound("TellMessage"); end end end function SpamMeNot:IncSpamCount() self.numSpamsBlocked = self.numSpamsBlocked +1 self.db.account.totalSpamsBlocked = self.db.account.totalSpamsBlocked +1 self:UpdateText() end function SpamMeNot:IncSpamsReportedCount() self.numSpamsReported = self.numSpamsReported +1 self:UpdateText() end -- Every time we see messages from someone we add it as suspect data -- and then combine it with the last 2 messages to see if the combined -- text is rated as spam. function SpamMeNot:AddSuspectData(name, lineID, text) if (name == "") then return false end if (self:IsAssociate(name)) then return false end if lineID then self:Debug("Added suspect data with lineID of " .. lineID) else self:Debug("Added suspect data but there was no lineID") end if (not self.suspects[name]) then self.suspects[name] = {} self.suspects[name].text = {} self.suspects[name].lineIDs = {} end -- We only want to 3 messages at most so we pop the oldest from the -- stack here. if (#self.suspects[name].text > 2) then table.remove(self.suspects[name].text,1) table.remove(self.suspects[name].lineIDs,1) end table.insert(self.suspects[name].text, text) table.insert(self.suspects[name].lineIDs, lineID) self.suspects[name].lastUpdate = GetTime() -- combine all stored text on the user and rate it local combinedText = "" for i=1,#self.suspects[name].text do combinedText = combinedText .. self.suspects[name].text[i] end local rating = self:RateMessage(combinedText) self:Debug(string.format("Suspect %s now has a rating of %d (%d texts stored)", name, rating, #self.suspects[name].text)) if (rating > 100) then self:AddSpammer(name, self.suspects[name].lineIDs, rating, combinedText) if (self.db.profile.alertEnabled == true) then self:Print(string.format(L["Gold spammer %s has been blocked (rating %d)."], self:MakeSpammerLink(name), rating)) end self.suspects[name] = nil return true end return false end function SpamMeNot:PurgeSuspects() for name,data in pairs(self.suspects) do if (data.lastUpdate + 60 < GetTime()) then self.suspects[name] = nil end end end function SpamMeNot:NotifySpammerOfBlock(Spammer) if (self.spammers[Spammer]) then if (not self.spammers[Spammer].notified) then self.spammers[Spammer].notified = true SendChatMessage(self.goAwayMsg, "WHISPER", nil, Spammer) end end end function SpamMeNot:CHAT_MSG_WHISPER() if (self.skipNextWhisper == true) then self.skipNextWhisper = false else if (self:FilterEvent("CHAT_MSG_WHISPER")) then if (self:IsSpammer(arg2)) then self:IncSpamCount() self:NotifySpammerOfBlock(arg2) else -- They've been filtered for a reason other than spamming -- i.e. a level block local data = {} data.name = arg2 data.message = arg1 data.time = GetTime() data.language = arg3 data.lineID = arg11 table.insert(self.messageHold, data) --self:Print("Filtered because of unknown level..") if (self.whoQ[data.name]) then if (not self.whoQ[data.name].askedToWait) and (self.db.profile.lookupInformEnabled) then self:Debug("Sending please wait message to " .. arg2) self.whoQ[data.name].askedToWait = true SendChatMessage("[SpamMeNot] " .. L["Please wait while I authorize you..."], "WHISPER", nil, arg2) end end end elseif (arg6 ~= "GM") then -- Apparently SpamMeNot once accused a GM of spamming and blocked them :) if (self:AddSuspectData(arg2, arg11, arg1)) then self:NotifySpammerOfBlock(arg2) end end end end function SpamMeNot:CHAT_MSG_SAY() if (self:FilterEvent("CHAT_MSG_SAY")) then self:IncSpamCount() else self:AddSuspectData(arg2, arg11, arg1) end end function SpamMeNot:CHAT_MSG_YELL() if (self:FilterEvent("CHAT_MSG_YELL")) then self:IncSpamCount() else self:AddSuspectData(arg2, arg11, arg1) end end function SpamMeNot:CHAT_MSG_CHANNEL() if (arg7 ~= 0) then if not self:IsChannelExcluded(arg7) then if (self:FilterEvent("CHAT_MSG_CHANNEL")) then self:IncSpamCount() else self:AddSuspectData(arg2, arg11, arg1) end end end end function SpamMeNot:FriendsFrame_OnEvent(event) if (event == WHO_LIST_UPDATE) then local found = false local totalCount, numWhos = GetNumWhoResults() for i=1,numWhos do local name, guild, level, race, class, zone = GetWhoInfo(i) if (self.whoQ[name]) then found = true self.whoQ[name] = nil -- remove from Who queue end self.whoCache[name] = {} self.whoCache[name].level = level end SetWhoToUI(0); if ((not found) and ((numWhos > 0) or getglobal("FriendsFrame"):IsVisible())) then -- We didn't find any names that we'd requested. self.hooks["FriendsFrame_OnEvent"](event) end else self.hooks["FriendsFrame_OnEvent"](event) end end function SpamMeNot:UnitPopup_OnClick() local menu = getglobal(UIDROPDOWNMENU_INIT_MENU) local selected = this.value local unit = menu.unit local name = menu.name if selected == "SPAMMENOT" then if not name and unit then name = UnitName(unit) end if (name) then SpamMeNot:AddToSpammerList(name) end end end function SpamMeNot:WhoMaintenance() --self:Print("Performing who maintenance") for name, data in pairs(self.whoQ) do if (data.timeAdded + 30 < GetTime()) then self.whoQ[name] = nil end end -- Determine which name to look up -- We don't want to keep asking for the same name in case that -- person doesn't exist because it will delay the other lookups if (not self.lastServerWhoRequest) then self.lastServerWhoRequest = "" end local nameToLookup = nil for name, data in pairs(self.whoQ) do nameToLookup = name if (nameToLookup ~= self.lastServerWhoRequest) then break end end if (nameToLookup) then self:ServerWhoLookup(nameToLookup) self.lastServerWhoRequest = nameToLookup end --self:Print("Queue size is now " .. #self.whoQ) end -- Asks the server for the who information on "name" function SpamMeNot:ServerWhoLookup(name) if (not self.whoCache[name]) then if (not self.whoQ[name]) then self.whoQ[name] = {} self.whoQ[name].timeAdded = GetTime() end --self:Print(string.format("Asking server for who info on %s", name)) SetWhoToUI(1); SendWho(string.format("n-\"%s\"", name)) end end -- Get who data from our cache. If we don't have it cached then ask the -- server for it. function SpamMeNot:GetWhoData(name) local data = nil if (name) then if (not self.whoCache[name]) and (not self.whoQ[name]) then self:ServerWhoLookup(name) elseif (self.whoCache[name]) then data = self.whoCache[name] end end return data end -- Release a whisper from the hold since the sender has passed the -- level block function SpamMeNot:ReleaseHoldMessage(index) local data = self.messageHold[index] if (data) then if (not self:IsSpammer(data.name)) then if (self.whoCache[data.name]) then if (not self.whoCache[data.name].authorized) and (self.db.profile.lookupInformEnabled) then self.whoCache[data.name].authorized = true SendChatMessage("[SpamMeNot] " .. L["Chat session has been authorized."], "WHISPER", nil, data.name) end end self:FakeIncomingTell(data.name, data.lineID, data.message) else self:IncSpamCount() end end end -- Because we've already received (and hidden) the original whisper we need to now "fake" it function SpamMeNot:FakeIncomingTell(player, lineID, msg) if (not lineID) then lineID = 0 end local message = string.format("%s %s: %s", "|Hplayer:" .. player .. ":" .. lineID .. "|h[" .. player .. "]|h", L["whispers"], msg) local WIMMsg = string.format("%s: %s", "|Hplayer:" .. player .. ":" .. lineID .. "|h[" .. player .. "]|h", msg) local FCMsg = msg arg1 = msg arg2 = player arg3 = "" arg4 = "" arg5 = "" arg6 = "" arg7 = "" arg8 = "" arg9 = "" arg10 = "" arg11 = lineID -- Fake whisper event to all Ace2Addons local AceEvent = AceLibrary:GetInstance("AceEvent-2.0") self.skipNextWhisper = true -- Don't need to let SpamMeNot know again. self.hooks[AceEvent].TriggerEvent(AceEvent, "CHAT_MSG_WHISPER", msg, player, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11) if (WIM_PostMessage) then WIM_PostMessage(player, WIMMsg, 1, player, WIMMsg) elseif (FCCC_IncomingMessage) then FCCC_IncomingMessage(player, FCMsg) else local oldThis = this for i=1,NUM_CHAT_WINDOWS do local chatWin = getglobal("ChatFrame"..i); if (chatWin) then local channels = {GetChatWindowMessages(chatWin:GetID())} for x=1,#channels do if (channels[x] == "WHISPER") then this = chatWin self.hooks["ChatFrame_OnEvent"]("CHAT_MSG_WHISPER") break end end end end this = oldThis end end -- We were unable to lookup the sender's who information. function SpamMeNot:TimeoutHoldMessage(index) --self:Print("Whisper timed out."); local data = self.messageHold[index] if (data) then SendChatMessage("[SpamMeNot] " .. L["Timed out while attempting to look up your details. Your message was undelivered."], "WHISPER", nil, data.name) self:DenyHoldMessage(index) end end -- The message in the hold needs to be disposed of as the sender is not allowed -- to send us whispers. function SpamMeNot:DenyHoldMessage(index) --self:Print("Hold message denied.") local data = self.messageHold[index] if (data) then if (string.find(data.message, "[SpamMeNot]", 1, true) == nil) then -- Don't reply to "[SpamMeNot] messages or we'll get in to a loop if the sender is also using it. self:IncSpamCount() self:NotifyOfLevelBlock(data.name) end end end function SpamMeNot:NotifyOfLevelBlock(player) if (self.whoCache[player]) then if (not self.whoCache[player].informedOfLevelBlock) then self.whoCache[player].informedOfLevelBlock = true SendChatMessage("[SpamMeNot] " .. string.format(L["You must be at least level %d to contact me via whispers."], self:GetMinimumLevel()), "WHISPER", nil, player) end end end -- Periodically run as a scheduled event to release / deny / timeout messages in the hold -- as necessary. function SpamMeNot:HoldMaintenance() --self:Print("Performing HoldMaintenance") local i = 1 for x=1,#self.messageHold do if (self.messageHold[i]) then --self:Print("Checking hold message " .. i .. " / " .. #self.messageHold) local data = self:GetWhoData(self.messageHold[i].name) if (data) then if (data.level >= self:GetMinimumLevel()) then self:ReleaseHoldMessage(i) else self:DenyHoldMessage(i) end table.remove(self.messageHold, i) elseif (self.messageHold[i].time + 30 < GetTime()) then self:TimeoutHoldMessage(i) table.remove(self.messageHold, i) else i = i + 1 end end end --self:Print("Hold size is now " .. #self.messageHold) end -- The main spam rating formula. Takes a string and returns a rating. >= 100 is considered -- to be spam. function SpamMeNot:RateMessage(s) if s == self.cachedLastRatedString then return self.cachedWeight end local w = "" local weight = 0 local wordsChecked = {} local regTest = s -- Strip out wow hyperlinks and colors regTest = self:RemoveHyperLinks(s) regTest = string.lower(regTest) -- remove double spacing and replace odd characters used for spaces with real ones local spacestrip = "[^1234567890abcdefghijklmnopqrstuvwxyz&€£$!.,%<>]+" regTest = string.gsub(regTest, spacestrip, " ") self:Debug("regTest = " .. regTest) -- Check individual words for w in string.gmatch(regTest, "%w+") do if (not wordsChecked[w]) then if (self.weights[w]) then self:Debug("Word matched in regular test 1: " .. w) weight = weight + self.weights[w] end wordsChecked[w] = 1; end end if (weight < 100) then -- k let's try reverting some numerics back to letters to see if that finds anything new regTest = string.gsub(regTest, "0" , "o") regTest = string.gsub(regTest, "1" , "l") regTest = string.gsub(regTest, "3" , "e") regTest = string.gsub(regTest, "4" , "a") regTest = string.gsub(regTest, "5" , "s") -- Check individual words for w in string.gmatch(regTest, "%w+") do if (not wordsChecked[w]) then if (self.weights[w]) then self:Debug("Word matched in regular test 2: " .. w) weight = weight + self.weights[w] end wordsChecked[w] = 1; end end end -- and the other way... if (weight < 100) then -- k let's try reverting some numerics back to letters to see if that finds anything new regTest = string.gsub(regTest, "o" , "0") regTest = string.gsub(regTest, "l" , "1") regTest = string.gsub(regTest, "e" , "3") regTest = string.gsub(regTest, "a" , "4") regTest = string.gsub(regTest, "s" , "5") self:Debug("regTest=" .. regTest) -- Check individual words for w in string.gmatch(regTest, "%w+") do if (not wordsChecked[w]) then if (self.weights[w]) then self:Debug("Word matched in regular test 3: " .. w) weight = weight + self.weights[w] end wordsChecked[w] = 1; end end end self:Debug("Rating: " .. weight) if (weight < 100) then -- If they've not flagged up then lets try removing all the spaces and then search -- for words within the text. local compact = self:RemoveHyperLinks(s) compact = string.lower(compact) compact = string.gsub(compact, spacestrip , "") self:Debug("compacted to: " .. compact) local compactWeight = 0 for word, value in pairs(self.weights) do if (string.match(compact, word)) then self:Debug("word matched in compact test 1: " .. word) compactWeight = compactWeight + value else -- Revert numerics word = string.gsub(word, "0" , "o") word = string.gsub(word, "1" , "l") word = string.gsub(word, "3" , "e") word = string.gsub(word, "4" , "a") word = string.gsub(word, "5" , "s") if (string.match(compact, word)) then self:Debug("word matched in compact test 2: " .. word) compactWeight = compactWeight + value else -- and backwards word = string.gsub(word, "o" , "0") word = string.gsub(word, "l" , "1") word = string.gsub(word, "e" , "3") word = string.gsub(word, "a" , "4") word = string.gsub(word, "s" , "5") if (string.match(compact, word)) then self:Debug("word matched in compact test 3: " .. word) compactWeight = compactWeight + value end end end end self:Debug("Compact Rating: " .. compactWeight) if (compactWeight > weight) then weight = compactWeight end end if weight > 0 then --self:Debug(string.format("(%.3d) %s", weight, s)) end self.cachedWeight = weight self.cachedLastRatedString = s return weight end -- Our ChatFrame_OnEvent hook function SpamMeNot:ChatFrame_OnEvent(event) local filter = self:FilterEvent(event) if (not filter) then self.hooks["ChatFrame_OnEvent"](event) end end -- WIM_ChatFrame_MessageEventHandler hook for newer versions of WIM function SpamMeNot:WIM_ChatFrame_MessageEventHandler(event, internalEvent) if (not self:FilterEvent(event)) then self.hooks["WIM_ChatFrame_MessageEventHandler"](event, internalEvent) end end -- WIM_ChatFrame_OnEvent hook for older versions of WIM. function SpamMeNot:WIM_ChatFrame_OnEvent(event) --self:Print("WIM_ChatFrame_OnEvent called") if (not self:FilterEvent(event)) then self.hooks["WIM_ChatFrame_OnEvent"](event) end end -- FCCC_OnEvent for ForgottenChat function SpamMeNot:FCCC_OnEvent(event) if (not self:FilterEvent(event)) then self.hooks["FCCC_OnEvent"](event) end end function SpamMeNot:AceEvent_TriggerEvent(obj, event, ...) if (obj) then local AceEvent = AceLibrary:GetInstance("AceEvent-2.0") if not self:FilterEvent(event) then --self:Print("Calling original TriggerEvent for " .. event) self.hooks[AceEvent].TriggerEvent(AceEvent, event, ...) elseif (event == "CHAT_MSG_WHISPER") then SpamMeNot:CHAT_MSG_WHISPER() end else --self:Print("The caller didn't call TriggerEvent as an object method for some reason") end end -- Checks our spammer cache to see if the "name" is considered to be -- a spammer function SpamMeNot:IsSpammer(name) if (self.spammers[name]) then return true else return false end end function SpamMeNot:GetServerOffset() -- Borrowed this from GEM :P local hour = GetGameTime(); local today = date("*t"); local offset = hour - today.hour; if(math.abs(offset) > 12) then if(offset > 0) then offset = offset - 24; else offset = offset + 24; end end return offset end -- lineIDs is a table of... erm... lineIDs (the ID Blizz uses to reference a piece of spam text) function SpamMeNot:AddSpammer(name, lineIDs, rating, text) if (name ~= self.playerName) then local serverHour, serverMinute = GetGameTime() local d = date("*t") --"%Y-%m-%d" if d.hour + self:GetServerOffset() < 0 then d.day = d.day -1 elseif d.hour + self:GetServerOffset() > 24 then d.day = d.day +1 end self.spammers[name] = {} self.spammers[name].rating = rating self.spammers[name].text = text self.spammers[name].reported = false self.spammers[name].date = string.format("%d-%.2d-%.2d", d.year, d.month, d.day) self.spammers[name].time = string.format("%.2d:%.2d", serverHour, serverMinute) self.spammers[name].lineIDs = {} if lineIDs then for i=1,#lineIDs do table.insert(self.spammers[name].lineIDs, lineIDs[i]) end end if SpamMeNot_Frame then self:InitializeSpammerCombo() end end table.insert(self.options.args.spam.args.remove.validate, name) end -- Our main event filter. All the event hooks end up calling this to see if the event passes -- our filter. function SpamMeNot:FilterEvent(event, args) local filtered = false -- All of the events we filter require these to be set. if not arg1 or not arg2 then -- Occasionally we seem to get a whisper from a sender that is nil. I can't explain that, -- but we've nothing to work with if that happens so there's no point in trying to filter it. return filtered end if ("CHAT_MSG_WHISPER" == event) or ("CHAT_MSG_SAY" == event) or ("CHAT_MSG_YELL" == event) or ("CHAT_MSG_CHANNEL" == event and arg7 ~= 0) then local sender = arg2 if (not self:IsSpammer(sender)) then local sentence = arg1 local rating = self:RateMessage(sentence) if "CHAT_MSG_WHISPER" == event and arg6 == "GM" then --filter = false -- Never ignore GMS :P Well YOU can but SpamMeNot won't... elseif (self:IsAssociate(sender)) then --filter = false -- Never filter friends, party members, guildies, or raid members elseif "CHAT_MSG_CHANNEL" == event and self:IsChannelExcluded(arg7) then -- We're not monitoring this channel -- filter = false elseif (rating >= 100) then self:AddSpammer(sender, {arg11}, rating, arg1) filtered = true if (self.db.profile.alertEnabled == true) then self:Print(string.format(L["Gold spammer %s has been blocked (rating %d)."], self:MakeSpammerLink(arg2), rating)) end elseif ("CHAT_MSG_WHISPER" == event) and (self.db.profile.minimumLevel > 1) then if not self:IsCrossServerPlayer(sender) then local whoData = self:GetWhoData(sender) local whoOK = false if (whoData) then if (whoData.level >= self:GetMinimumLevel()) then whoOK = true -- Make sure any earlier messages are flushed first self:HoldMaintenance() end end if (not whoOK) then -- They didn't trigger a spam alert when they whispered us but we don't know -- what level they are filtered = true end end end else filtered = true end elseif ("CHAT_MSG_WHISPER_INFORM" == event) and (string.find(arg1, "[SpamMeNot]", 1, true) == nil) then -- Show tells we send as long as they're not automated from SpamMeNot filtered = false elseif ("CHAT_MSG_WHISPER_INFORM" == event) and (string.find(arg1, "[SpamMeNot]", 1, true) ~= nil) then -- Don't show tells sent from our SpamMeNot filtered = true elseif ("CHAT_MSG_WHISPER_INFORM" == event) or ("CHAT_MSG_AFK" == event) or ("CHAT_MSG_DND" == event) then -- arg2 is the recipient local recipient = arg2 if (self:IsSpammer(recipient)) then filtered = true else local whoData = self:GetWhoData(recipient) local whoOK = false if (whoData) then if (whoData.level >= self:GetMinimumLevel()) then whoOK = true end end if (not whoOK) then filtered = true end end end return filtered end function SpamMeNot:SetItemRef(...) local msg = select(1, ...) local spammer = select(3, msg:find("^SpamMeNot:(.+)")) if spammer then self:ShowSpam(spammer) else self.hooks["SetItemRef"](...) end end function SpamMeNot:InitializeSpammerCombo() local f = SpamMeNot_Frame_HeaderFrame_SpammerCombo UIDropDownMenu_Initialize(f, SpamMeNot_PopulateSpammerCombo); if SpamMeNot.currentSpammer then UIDropDownMenu_SetSelectedValue(f, SpamMeNot.currentSpammer); end UIDropDownMenu_SetWidth(100, f); end function SpamMeNot_PopulateSpammerCombo() local info = {}; for spammer, data in pairs(SpamMeNot.spammers) do info = {} info.text = spammer info.value = spammer info.justifyH = "LEFT" info.func = function () SpamMeNot:ShowSpam(this.value) UIDropDownMenu_SetSelectedValue(SpamMeNot_Frame_HeaderFrame_SpammerCombo, this.value) end UIDropDownMenu_AddButton(info) end end function SpamMeNot:OpenUI() if (SpamMeNot_Frame) then if (SpamMeNot_Frame:IsVisible()) then return end end local name = "" for spammer, data in pairs(self.spammers) do name = spammer break end self:ShowSpam(name) end function SpamMeNot:ShowSpam(spammer) if not SpamMeNot_Frame then CreateFrame("Frame", "SpamMeNot_Frame", UIParent, "SMN_SpamDisplayTemplate") end local data = self.spammers[spammer] if (data) then self.currentSpammer = spammer SpamMeNot_Frame_HeaderFrame_SpammerName:SetText(spammer) SpamMeNot_Frame_HeaderFrame_Rating:SetText(data.rating) SpamMeNot_Frame_HeaderFrame_Time:SetText(data.time) SpamMeNot_Frame_HeaderFrame_Date:SetText(data.date) SpamMeNot_Frame_TextScoller_TextFrame_Text:SetText(data.text) local blizzCanComplain = false for i=1,#data.lineIDs do if CanComplainChat(data.lineIDs[i]) then blizzCanComplain = true break end end if data.text ~= nil and string.len(data.text) > 0 and not self.spammers[spammer].reported and blizzCanComplain then SpamMeNot_Frame_ReportButton:Enable() else SpamMeNot_Frame_ReportButton:Disable() end SpamMeNot_Frame_InnocentButton:Enable() SpamMeNot_Frame:Show() UIDropDownMenu_SetSelectedValue(SpamMeNot_Frame_HeaderFrame_SpammerCombo, self.currentSpammer) end end function SpamMeNot:UIInnocent() SpamMeNot_Frame_InnocentButton:Disable() self:RemoveFromSpammerList(SpamMeNot_Frame_HeaderFrame_SpammerName:GetText()) end function SpamMeNot:UIReportSpammer() local spammer = SpamMeNot_Frame_HeaderFrame_SpammerName:GetText() SpamMeNot_Frame_ReportButton:Disable() self:ReportSpammer(spammer) end function SpamMeNot:ReportSpammer(name) --[[ * NEW - CanComplainChat(lineID) -- determines if we should show the menu for reporting a line of chat spam (lineID comes from the player link in the chat line) * NEW - ComplainChat(lineID) -- complains about a particular line of chat spam * NEW - CanComplainInboxItem(index) -- determines if we should show the “report spam” button on a mail item * NEW - ComplainInboxItem(index) -- complains about a particular mail item arg11 possibly new lineID? ]] local data = self.spammers[spammer] if (data) then if (data.lineIDs) then for i=1,#data.lineIDs do if CanComplainChat(data.lineIDs[i]) then ComplainChat(data.lineIDs[i]) end end end end IncSpamsReportedCount() end function SpamMeNot:RemoveHyperLinks(text) text = string.gsub(text, "|H.-|h(.-)|h", "%1") text = string.gsub(text, "|c%w%w%w%w%w%w%w%w(.-)|r", "%1") if (self.debugMode == true) then text = string.gsub(text, "|", "#") end --self:Debug("RemoveHyperLinks: " .. text) return text end function SpamMeNot:Test() self:AddSpammer("Joe", nil, 130, "This is Joe's spam text") self:AddSpammer("Fred", nil, 130, "Buyz meh Goldz") end -- Fu Stuff SpamMeNot.OnMenuRequest = SpamMeNot.options SpamMeNot.hasNoColor = true SpamMeNot.hasIcon = "Interface\\Icons\\Ability_Warrior_Defensivestance.jpg" SpamMeNot.defaultMinimapPosition = 210 SpamMeNot.cannotDetachTooltip = true SpamMeNot.hideWithoutStandby = true function SpamMeNot:OnTextUpdate() if(IsAddOnLoaded("FuBar")) then local text = string.format("%s |cffffffff%d|r", L["Msgs Blocked:"], self.numSpamsBlocked) if (self.IsFlashOn) then text = "|cffff5555" .. text .. "|r"; end self:SetText(text) end end function SpamMeNot:OnClick() end function SpamMeNot:OnTooltipUpdate() self.tablet:SetTitle("") local cat = self.tablet:AddCategory( 'columns', 2, 'child_textR', 1, 'child_textG', 1, 'child_textB', 1, 'child_text2R', 1, 'child_text2G', 1, 'child_text2B', 1 ) cat:AddLine('text', "SpamMeNot", 'text2', self.version) cat = self.tablet:AddCategory( 'columns', 2, 'child_text2R', 1, 'child_text2G', 1, 'child_text2B', 1 ) cat:AddLine('text', L["Spams Blocked"], 'text2', self.numSpamsBlocked) cat:AddLine('text', L["Lifetime Blocked"], 'text2', self.db.account.totalSpamsBlocked) cat:AddLine('text', L["Spammers Reported"], 'text2', self.numSpamsReported) cat = self.tablet:AddCategory( 'columns', 2, 'child_textR', 1, 'child_textG', 1, 'child_textB', 1, 'child_text2R', 1, 'child_text2G', 1, 'child_text2B', 1 ) cat:AddLine('text', L["Menu"], 'text2', L["Right-Click"]) end