- handle SendChatMessage ordering if addon Misspelled is also installed - break long texts in chunks of 255 length - debug flag and messages
		
			
				
	
	
		
			366 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			366 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| --[[---------------------------------------------------------------------------
 | |
|   Grichelde
 | |
|   Copyright 2020 Teilzeit-Jedi <tj@teilzeit-jedi.de>
 | |
| 
 | |
|   based on Misspelled developed by Nathan Pieper - nrpieper (@) gmail (dot) com
 | |
| 
 | |
|   This code freely distributed for your use in any GPL compliant project.
 | |
|   See conditions in the LICENSE file attached.
 | |
| 
 | |
|   Unless required by applicable law or agreed to in writing, software
 | |
|   distributed under the License is distributed on an "AS IS" BASIS,
 | |
|   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
|   See the License for the specific language governing permissions and
 | |
|   limitations under the License.
 | |
| 
 | |
| --------------------------------------------------------------------------]] --
 | |
| 
 | |
| local AddonName, AddonTable = ...
 | |
| 
 | |
| local Grichelde = LibStub("AceAddon-3.0"):NewAddon("Grichelde", "AceEvent-3.0", "AceHook-3.0")
 | |
| Grichelde.version = GetAddOnMetadata(AddonName, "Version")
 | |
| Grichelde.build = GetAddOnMetadata(AddonName, "X-Build") or "UNKNOWN"
 | |
| 
 | |
| local L = LibStub("AceLocale-3.0"):GetLocale("Grichelde", true)
 | |
| 
 | |
| local Grichelde_Debug = false
 | |
| 
 | |
| -- faster function lookups by mapping to local refs
 | |
| local string_find = string.find
 | |
| local string_gsub = string.gsub
 | |
| local string_len = string.len
 | |
| local string_rep = string.rep
 | |
| local string_sub = string.sub
 | |
| local strtrim = strtrim
 | |
| local strmatch = strmatch
 | |
| local tostring = tostring
 | |
| local tInsert = table.insert
 | |
| local tContains = tContains
 | |
| local pairs = pairs
 | |
| local ipairs = ipairs
 | |
| 
 | |
| local Grichelde_Hooks = {}
 | |
| 
 | |
| --local Grichelde_ChatTypes = { "SAY", "EMOTE", "YELL", "PARTY", "GUILD", "OFFICER", "RAID", "RAID_WARNING", "INSTANCE_CHAT", "BATTLEGROUND", "WHISPER" }
 | |
| local Grichelde_ChatTypes = { "SAY", "EMOTE", "YELL", "PARTY", "GUILD" }
 | |
| local Grichelde_ChatCommands = { "/s", "/e", "/me", "/y", "/p", "/pl", "/g", "/o", "/raid", "/rl", "/rw", "/i", "bg", "/w", "/r", "/tt" }
 | |
| 
 | |
| -- do not replace these patterns
 | |
| local Grichelde_IgnorePatterns = {
 | |
|     "|[Cc]%x%x%x%x%x%x%x%x.-|r",    -- colored items (or links)
 | |
|     "|H.-|h",                       -- item links (http://www.wowwiki.com/ItemLink)
 | |
|     "|T.-|t",                       -- textures
 | |
|     "|n",                           -- newline
 | |
|     "{rt[1-8]}",                    -- rumbered raid target icons
 | |
|     "{Star}",                       -- named raid target icon 1
 | |
|     "{Circle}",                     -- named raid target icon 2
 | |
|     "{Coin}",                       -- named raid target icon 2
 | |
|     "{Diamond}",                    -- named raid target icon 3
 | |
|     "{Triangle}",                   -- named raid target icon 4
 | |
|     "{Moon}",                       -- named raid target icon 5
 | |
|     "{Square}",                     -- named raid target icon 6
 | |
|     "{Cross}",                      -- named raid target icon 7
 | |
|     "{X}",                          -- named raid target icon 7
 | |
|     "{Skull}"                       -- named raid target icon 8
 | |
| }
 | |
| 
 | |
| function Grichelde:OnInitialize()
 | |
|     -- Build Interface Options window
 | |
|     --self:CreateInterfaceOptions()
 | |
| 
 | |
|     -- Watch for WIM and Prat to Load, then integrate
 | |
|     self:RegisterEvent("ADDON_LOADED", "HookIntoForOtherChatAddons")
 | |
| end
 | |
| 
 | |
| function Grichelde:OnEnable()
 | |
|     -- Hook in before message is sent to replace all character occurrences where replacements have been defined in the options
 | |
|     self:RawHook("SendChatMessage", true)
 | |
| 
 | |
|     if (Misspelled) then
 | |
|         print("Misspelled detected, Grichelde will have any messsage being cleansed")
 | |
|     end
 | |
| 
 | |
|     -- tell the world we are listening
 | |
|     print(L.AddonLoaded)
 | |
| end
 | |
| 
 | |
| function Grichelde:OnDisable()
 | |
|     self:Unhook("SendChatMessage")
 | |
| end
 | |
| 
 | |
| --- @param event string
 | |
| --- @param addonName string
 | |
| function Grichelde:HookIntoForOtherChatAddons(event, addonName)
 | |
|     if event == "ADDON_LOADED" then
 | |
|         if addonName == "WIM" then
 | |
|             WIM.RegisterWidgetTrigger("msg_box", "whisper,chat,w2w", "OnEnterPressed", Grichelde.EditBox_OnEnterPressed)
 | |
| 
 | |
|             -- If available use the WIM API
 | |
|             if (WIM.RegisterPreSendFilterText) then -- avoid error if WIM not up to date.
 | |
|                 WIM.RegisterPreSendFilterText(function(text)
 | |
|                     return Grichelde:CheckAndReplace(text)
 | |
|                 end)
 | |
|             else
 | |
|                 -- WIM sends its chat messages via the API ChatThrottleLib, which itself hooks the default SendChatMessage api
 | |
|                 -- many times before Grichelde will. ChatThrottleLib might potentially load before Grichelde, so we just hook
 | |
|                 -- into ChatThrottleLib to be on the safe side.
 | |
| 
 | |
|                 if (ChatThrottleLib) then
 | |
|                     Grichelde_Hooks["ChatThrottleLib"] = ChatThrottleLib.SendChatMessage
 | |
| 
 | |
|                     function ChatThrottleLib:SendChatMessage(prio, prefix, text, ...)
 | |
|                         Grichelde:DebugPrint("ChatThrottleLib:SendChatMessage : Hook called")
 | |
|                         local replacedText = Grichelde:CheckAndReplace(text)
 | |
|                         return Grichelde_Hooks["ChatThrottleLib"](ChatThrottleLib, prio, prefix, replacedText, ...)
 | |
|                     end
 | |
|                 end
 | |
|             end
 | |
|         end
 | |
|     end
 | |
| end
 | |
| 
 | |
| --- Before af chat message is sent, check if replacement is required and replace the text accordingly.
 | |
| --- @param message string
 | |
| --- @param type string
 | |
| --- @param language string
 | |
| --- @param channel string
 | |
| function Grichelde:SendChatMessage(message, type, language, channel, ...)
 | |
|     local replacedText = self:CheckAndReplace(message, type)
 | |
| 
 | |
|     self:DebugPrint("SendChatMessage : replacedText: " .. replacedText)
 | |
| 
 | |
|     -- Send text in chunks if length exceeds 255 bytes after replacement
 | |
|     local chunks = self:SplitText(replacedText)
 | |
|     self:DebugPrint("SendChatMessage : #chunks: " .. #chunks)
 | |
| 
 | |
|     for _, chunk in ipairs(chunks) do
 | |
|         self.hooks["SendChatMessage"](chunk, type, language, channel, ...);
 | |
|     end
 | |
| end
 | |
| 
 | |
| function Grichelde:CheckAndReplace(message, type)
 | |
|     local text = message
 | |
|     if (Misspelled) then
 | |
|         self:DebugPrint("Misspelled detected: cleansing message")
 | |
|         text = Misspelled:RemoveHighlighting(text)
 | |
|     end
 | |
|     text = strtrim(text)
 | |
| 
 | |
|     if (self:CheckReplacement(text, type)) then
 | |
|         text = self:ReplaceText(text)
 | |
|     end
 | |
|     return text
 | |
| end
 | |
| 
 | |
| function Grichelde:CheckReplacement(text, type)
 | |
|     -- todo: globally disabled?
 | |
| 
 | |
|     -- check type
 | |
|     if (not tContains(Grichelde_ChatTypes, type)) then
 | |
|         self:DebugPrint("CheckReplacement : skip channel type")
 | |
|         return false
 | |
|     end
 | |
| 
 | |
|     -- don't replace slash commands except chat related commands
 | |
|     if string_sub(text, 1, 1) == "/" then
 | |
|         local firstWord, _ = self:SplitOnFirstMatch(text)
 | |
|         if (firstWord == nil or not tContains(Grichelde_ChatCommands, firstWord)) then
 | |
|             self:DebugPrint("CheckReplacement : ignore slash command")
 | |
|             return false
 | |
|         end
 | |
|     end
 | |
| 
 | |
|     -- in any other case
 | |
|     return true
 | |
| end
 | |
| 
 | |
| --- Checks if the text starts with a preversable ignore pattern, such as itemLinks, textures or raid target icons
 | |
| --- and returns the end location of the match, or 0 if no pattern was found
 | |
| --- @param text string
 | |
| --- @return number
 | |
| function Grichelde:CheckForPreversableText(text)
 | |
|     self:DebugPrint("CheckForPreversableText : text is " .. text)
 | |
|     -- Calling find on ever pattern might be inefficient but its way less code.
 | |
|     for _, pattern in ipairs(Grichelde_IgnorePatterns) do
 | |
|         local pos1, pos2 = string_find(text, pattern)
 | |
|         if pos1 == 1 and pos2 ~= nil then
 | |
|             self:DebugPrint("CheckForPreversableText : Found ignore pattern " .. pattern .. " at (" .. pos1 .. "," .. pos2 .. ")")
 | |
|             return pos2
 | |
|         end
 | |
|     end
 | |
|     self:DebugPrint("CheckForPreversableText : no ignore pattern found")
 | |
|     return 0
 | |
| end
 | |
| 
 | |
| --- Replaces all character occurrences for which replacements have been defined in the options,
 | |
| --- while preserving any itemLinks or textures. (http://www.wowwiki.com/ItemLink)
 | |
| --- @param text string
 | |
| --- @return string
 | |
| function Grichelde:ReplaceText(text)
 | |
|     local finalText = ""
 | |
|     local newText = text
 | |
| 
 | |
|     -- don't replace non-chat related slash commands
 | |
|     local firstWord, line = self:SplitOnFirstMatch(text)
 | |
|     if (firstWord ~= nil and tContains(Grichelde_ChatCommands, firstWord)) then
 | |
|         self:DebugPrint("ReplaceText : Found slash command " .. (firstWord + "") )
 | |
|         -- skip chat slash command
 | |
|         finalText = finalText .. firstWord .. ' '
 | |
|         newText = line
 | |
|     end
 | |
| 
 | |
|     local current = 1
 | |
|     local lastStart = 1
 | |
| 
 | |
|     while current <= string_len(newText) do
 | |
|         local currentChar = string_sub(newText, current, current)
 | |
|         self:DebugPrint("current/char : " .. current .. "," .. currentChar)
 | |
| 
 | |
|         if currentChar ~= '|' and currentChar ~= '{' then
 | |
|             current = current + 1
 | |
|         else
 | |
| 
 | |
|             -- lookahead-check for itemLinks, textures and raid target icons
 | |
|             local textAhead = string_sub(newText, current)
 | |
|             local posEnd = self:CheckForPreversableText(textAhead)
 | |
|             if posEnd > 0 then
 | |
|                 self:DebugPrint("ReplaceText : Found an ignore pattern")
 | |
| 
 | |
|                 local textBehind = string_sub(newText, lastStart, current - 1)
 | |
|                 local replacement = self:ReplaceCharacters(textBehind)
 | |
|                 local preservedText = string_sub(textAhead, 1, posEnd)
 | |
| 
 | |
|                 finalText = finalText .. replacement .. preservedText
 | |
|                 current = current + posEnd
 | |
|                 lastStart = current
 | |
|                 self:DebugPrint("ReplaceText : restarting at " .. lastStart)
 | |
|             else
 | |
|                 -- no corresponding end was found to start pattern, continue loop with next char
 | |
|                 current = current + 1
 | |
|             end
 | |
|         end
 | |
|     end
 | |
| 
 | |
|     -- cleanup remaining text to the end
 | |
|     local remainingText = string_sub(newText, lastStart)
 | |
|     local replacement = self:ReplaceCharacters(remainingText)
 | |
|     finalText = finalText .. replacement
 | |
| 
 | |
|     self:DebugPrint("ReplaceText : replaced \"" .. text .. "\"")
 | |
|     self:DebugPrint("ReplaceText : with         \"" .. finalText .. "\"")
 | |
|     return finalText
 | |
| end
 | |
| 
 | |
| --- Replaces all character occurrences for which replacements have been defined in the options
 | |
| --- @param text string
 | |
| --- @return string
 | |
| function Grichelde:ReplaceCharacters(text)
 | |
|     -- todo: read from options
 | |
|     -- todo: case (in)sensitivity
 | |
|     local replacement = text
 | |
|     replacement = string_gsub(replacement, "s", "ch")
 | |
|     replacement = string_gsub(replacement, "S", "Ch")
 | |
|     replacement = string_gsub(replacement, "t", "k")
 | |
|     replacement = string_gsub(replacement, "T", "K")
 | |
|     self:DebugPrint("ReplaceCharacters : replaced \"" .. text .. "\" with \"" .. replacement .. "\"")
 | |
|     return replacement
 | |
| end
 | |
| 
 | |
| --- Splits a long text in longest possible chunks of <= 255 length, split at last available space
 | |
| --- @param text string
 | |
| --- @return table
 | |
| function Grichelde:SplitText(text)
 | |
|     local chunks = {}
 | |
|     local splitText = text
 | |
|     local textSize = string_len(splitText)
 | |
| 
 | |
|     while textSize > 255 do
 | |
|         local chunk = string_sub(splitText, 1, 255)
 | |
|         local remaining = ""
 | |
| 
 | |
|         -- special case: if space is the start of the next chunk, don't split this chunk
 | |
|         if string_sub(splitText, 256, 256) ~= ' ' then
 | |
|             -- split at last space, don't assign directly as nil might be returned
 | |
|             local left, right = self:SplitOnLastMatch(chunk)
 | |
|             if left ~= nil then
 | |
|                 chunk = left
 | |
|             end
 | |
|             if right ~= nil then
 | |
|                 remaining = right
 | |
|             end
 | |
|         end
 | |
| 
 | |
|         self:DebugPrint("SplitText : chunk: " .. chunk )
 | |
| 
 | |
|         tInsert(chunks, chunk)
 | |
|         splitText = remaining .. string_sub(splitText, 256)
 | |
|         textSize = string_len(splitText)
 | |
|     end
 | |
| 
 | |
|     -- pickup remaining text < 255
 | |
|     self:DebugPrint("SplitText : last chunk: " .. splitText)
 | |
|     tInsert(chunks, splitText)
 | |
| 
 | |
|     return chunks
 | |
| end
 | |
| 
 | |
| -- split first word of a text line
 | |
| function Grichelde:SplitOnFirstMatch(text, start)
 | |
|     self:DebugPrint("SplitOnFirstMatch : text: " .. text .. ", start: " .. self:EmptyIfNil(start))
 | |
|     local pos = 1
 | |
|     if start ~= nil then pos = start end
 | |
|     local left, right = strmatch(text, "^.- .+", pos)
 | |
|     self:DebugPrint("SplitOnFirstMatch : left: " .. self:EmptyIfNil(left) .. ", right: " .. self:EmptyIfNil(right))
 | |
|     return left, right
 | |
| end
 | |
| 
 | |
| function Grichelde:SplitOnLastMatch(text, start)
 | |
|     self:DebugPrint("SplitOnLastMatch : text: " .. text .. ", start: " .. self:EmptyIfNil(start))
 | |
|     local pos = 1
 | |
|     if start ~= nil then pos = start end
 | |
|     local left, right = strmatch(text, ".+ .-$", pos)
 | |
|     self:DebugPrint("SplitOnLastMatch : left: " .. self:EmptyIfNil(left) .. ", right: " .. self:EmptyIfNil(right))
 | |
|     return left, right
 | |
| end
 | |
| 
 | |
| function Grichelde:DebugPrint(message)
 | |
|     if (Grichelde_Debug) then
 | |
|         print(GRAY_FONT_COLOR_CODE .. "Grichelde:" .. FONT_COLOR_CODE_CLOSE .. " " .. message)
 | |
|     end
 | |
| end
 | |
| 
 | |
| function Grichelde:EmptyIfNil(value)
 | |
|     if value == nil then return "" end
 | |
|     return tostring(value)
 | |
| end
 | |
| 
 | |
| function Grichelde:tprint(t, indent, done)
 | |
|     -- in case we run it standalone
 | |
|     local Note = Note or print
 | |
| --  local Tell = Tell or io.write
 | |
| 
 | |
|     -- show strings differently to distinguish them from numbers
 | |
|     local function show(val)
 | |
|         if type(val) == "string" then
 | |
|             return '"' .. val .. '"'
 | |
|         else
 | |
|             return tostring(val)
 | |
|         end
 | |
|     end
 | |
| 
 | |
|     -- entry point here
 | |
|     done = done or {}
 | |
|     indent = indent or 0
 | |
|     for key, value in pairs(t) do
 | |
|         print(string_rep(" ", indent)) -- indent it
 | |
|         if type(value) == "table" and not done[value] then
 | |
|             done[value] = true
 | |
|             Note(show(key), ":");
 | |
|             self:tprint(value, indent + 2, done)
 | |
|         else
 | |
|             print(show(key), "=")
 | |
|             print(show(value))
 | |
|         end
 | |
|     end
 | |
| end
 |