Skip to content

Commit 8e21ba1

Browse files
author
Maelareth
committed
feat(nicknames): add nickname sharing and sync
Self-broadcast and request nicknames over addon comms with channel validation and throttling, debounced auto-sync on group join, an incoming whole-word profanity filter, and per-sender blocking.
1 parent 473d6e6 commit 8e21ba1

1 file changed

Lines changed: 268 additions & 0 deletions

File tree

Features/NicknamesComm.lua

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
local addonName, DF = ...
2+
3+
-- ============================================================
4+
-- NICKNAMES - SHARING / COMMS (Phase 4)
5+
-- Self-broadcast model: each player broadcasts THEIR OWN nickname for THEIR
6+
-- current character; receivers map sender ("Name-Realm" from CHAT_MSG_ADDON)
7+
-- -> nickname into DF.Nicknames.received (the separate received cache).
8+
-- Mirrors Features/VersionCheck.lua (channel validation, send chokepoint,
9+
-- dispatch table, composition-key debounce, jittered replies).
10+
--
11+
-- Message types ("TYPE\tPAYLOAD"):
12+
-- N <nick> : "my nickname is X" -> receiver AddReceived(sender, X)
13+
-- Q : "who has a nickname?" -> peers reply with N (jittered)
14+
-- ============================================================
15+
16+
local ipairs = ipairs
17+
local match = string.match
18+
local mrandom = math.random
19+
local tsort, tconcat = table.sort, table.concat
20+
21+
DF.NicknamesComm = DF.NicknamesComm or {}
22+
local C = DF.NicknamesComm
23+
24+
C.PREFIX = "DFNick" -- <= 16 chars
25+
C.initialized = false
26+
C.playerFullName = nil
27+
C.pendingSync = false
28+
C.lastGroupKey = nil
29+
C.handlers = {}
30+
31+
local function getPlayerFullName()
32+
local name = UnitName("player")
33+
local realm = (GetRealmName() or ""):gsub("%s", "")
34+
return name .. "-" .. realm
35+
end
36+
37+
-- Defensive Midnight comm-restriction check. There's no global
38+
-- IsCommRestricted(); the real restriction state lives under
39+
-- C_RestrictedActions/C_Secrets (API name unconfirmed), so probe behind pcall
40+
-- and default to "allowed" (cosmetic friendly comms are not restricted).
41+
local function isCommRestricted()
42+
local f = C_RestrictedActions and C_RestrictedActions.IsCommRestricted
43+
if f then
44+
local ok, restricted = pcall(f)
45+
if ok then return restricted and true or false end
46+
end
47+
return false
48+
end
49+
50+
local function nkDB()
51+
return DF.Nicknames and DF.Nicknames:GetDB()
52+
end
53+
54+
-- ============================================================
55+
-- CHANNEL VALIDATION (copied from VersionCheck.lua — proven, avoids
56+
-- ERR_NOT_IN_GROUP spam by funnelling every send through SendMessage).
57+
-- ============================================================
58+
59+
function C:HasPlayerGroupMembers()
60+
if not IsInGroup(LE_PARTY_CATEGORY_HOME) then return false end
61+
if IsInRaid(LE_PARTY_CATEGORY_HOME) then
62+
local n = GetNumGroupMembers()
63+
for i = 1, n do
64+
local token = "raid" .. i
65+
if UnitExists(token) and UnitIsPlayer(token) and not UnitIsUnit(token, "player") then
66+
return true
67+
end
68+
end
69+
return false
70+
end
71+
for i = 1, 4 do
72+
local token = "party" .. i
73+
if UnitExists(token) and UnitIsPlayer(token) then return true end
74+
end
75+
return false
76+
end
77+
78+
function C:IsChannelValid(channel)
79+
if channel == "INSTANCE_CHAT" then
80+
return IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and IsInInstance()
81+
elseif channel == "RAID" then
82+
return IsInRaid(LE_PARTY_CATEGORY_HOME) and self:HasPlayerGroupMembers()
83+
elseif channel == "PARTY" then
84+
return IsInGroup(LE_PARTY_CATEGORY_HOME)
85+
and not IsInRaid(LE_PARTY_CATEGORY_HOME)
86+
and self:HasPlayerGroupMembers()
87+
elseif channel == "GUILD" then
88+
return IsInGuild()
89+
end
90+
return false
91+
end
92+
93+
-- {GUILD?, INSTANCE_CHAT|RAID|PARTY?} currently available.
94+
function C:GetAvailableChannels()
95+
local out = {}
96+
if self:IsChannelValid("GUILD") then out[#out+1] = "GUILD" end
97+
if self:IsChannelValid("INSTANCE_CHAT") then
98+
out[#out+1] = "INSTANCE_CHAT"
99+
elseif self:IsChannelValid("RAID") then
100+
out[#out+1] = "RAID"
101+
elseif self:IsChannelValid("PARTY") then
102+
out[#out+1] = "PARTY"
103+
end
104+
return out
105+
end
106+
107+
-- Does a setting mode ("off"/"raid"/"guild"/"both") permit this channel?
108+
-- Group channels (PARTY/RAID/INSTANCE_CHAT) count as "raid"; GUILD as "guild".
109+
local function modePermits(mode, channel)
110+
if mode == "off" or not mode then return false end
111+
if mode == "both" then return true end
112+
local isGuild = (channel == "GUILD")
113+
if mode == "guild" then return isGuild end
114+
if mode == "raid" then return not isGuild end
115+
return false
116+
end
117+
118+
-- Final send chokepoint: re-validate channel, then send.
119+
function C:SendMessage(msgType, payload, channel)
120+
if not self:IsChannelValid(channel) then return end
121+
local body = payload and (msgType .. "\t" .. payload) or msgType
122+
C_ChatInfo.SendAddonMessage(self.PREFIX, body, channel)
123+
end
124+
125+
-- ============================================================
126+
-- BROADCAST / REQUEST
127+
-- ============================================================
128+
129+
-- Broadcast our own nickname on every channel our shareVia setting permits.
130+
function C:BroadcastSelfNick()
131+
if isCommRestricted() then return end
132+
local data = nkDB()
133+
if not data then return end
134+
local nick = data.selfNick
135+
if not nick or nick == "" then return end
136+
local mode = data.shareVia or "off"
137+
if mode == "off" then return end
138+
for _, ch in ipairs(self:GetAvailableChannels()) do
139+
if modePermits(mode, ch) then
140+
self:SendMessage("N", nick, ch)
141+
end
142+
end
143+
end
144+
145+
-- Ask peers to send us their nicknames (they reply with N), on channels we
146+
-- accept from.
147+
function C:RequestFromGroup()
148+
if isCommRestricted() then return end
149+
local data = nkDB()
150+
if not data then return end
151+
local mode = data.acceptFrom or "off"
152+
if mode == "off" then return end
153+
for _, ch in ipairs(self:GetAvailableChannels()) do
154+
if modePermits(mode, ch) then
155+
self:SendMessage("Q", nil, ch)
156+
end
157+
end
158+
end
159+
160+
-- ============================================================
161+
-- RECEIVE
162+
-- ============================================================
163+
164+
C.handlers["N"] = function(self, sender, payload, channel)
165+
local data = nkDB()
166+
if not data then return end
167+
if not modePermits(data.acceptFrom or "off", channel) then return end -- accept gating
168+
if not payload or payload == "" then return end
169+
-- per-sender block list (persisted)
170+
local rej = data.rejected
171+
if rej and DF.Nicknames and rej[DF.Nicknames:Normalize(sender)] then return end
172+
-- AddReceived runs FilterIncoming (length / escape codes / profanity).
173+
DF.Nicknames:AddReceived(sender, payload, sender)
174+
end
175+
176+
C.handlers["Q"] = function(self, sender, _, channel)
177+
local data = nkDB()
178+
if not data then return end
179+
local mode = data.shareVia or "off"
180+
if mode == "off" or not data.selfNick or data.selfNick == "" then return end
181+
if not modePermits(mode, channel) then return end
182+
-- Reply with our nickname after small jitter (avoid response storms).
183+
local delay = 1 + mrandom() * 2
184+
C_Timer.After(delay, function()
185+
if modePermits(data.shareVia or "off", channel) and data.selfNick ~= "" then
186+
self:SendMessage("N", data.selfNick, channel)
187+
end
188+
end)
189+
end
190+
191+
function C:Dispatch(msgType, sender, payload, channel)
192+
if sender == self.playerFullName then return end -- ignore self
193+
local h = self.handlers[msgType]
194+
if h then h(self, sender, payload, channel) end
195+
end
196+
197+
function C:OnAddonMessage(prefix, message, channel, sender)
198+
if prefix ~= self.PREFIX then return end
199+
local msgType, payload = match(message, "^([^\t]+)\t?(.*)$")
200+
if not msgType then return end
201+
self:Dispatch(msgType, sender, payload, channel)
202+
end
203+
204+
-- ============================================================
205+
-- AUTO-SYNC ON ROSTER JOIN (debounced; composition-key gated)
206+
-- ============================================================
207+
208+
function C:GroupCompositionKey()
209+
if not IsInGroup() then return "solo" end
210+
local parts = {}
211+
local n = GetNumGroupMembers()
212+
local unit = IsInRaid() and "raid" or "party"
213+
for i = 1, n do
214+
local token = (unit == "party" and i == n) and "player" or (unit .. i)
215+
local fullName = GetUnitName(token, true)
216+
if fullName then parts[#parts+1] = fullName end
217+
end
218+
tsort(parts)
219+
return tconcat(parts, ",")
220+
end
221+
222+
function C:DoSync()
223+
local data = nkDB()
224+
if not data or not data.autoSync then return end
225+
self:BroadcastSelfNick()
226+
self:RequestFromGroup()
227+
end
228+
229+
function C:ScheduleSync(delay)
230+
if self.pendingSync then return end
231+
self.pendingSync = true
232+
C_Timer.After(delay, function()
233+
self.pendingSync = false
234+
self:DoSync()
235+
end)
236+
end
237+
238+
-- ============================================================
239+
-- INIT (called from DF.Nicknames:Init)
240+
-- ============================================================
241+
242+
function C:Init()
243+
if self.initialized then return end
244+
self.initialized = true
245+
self.playerFullName = getPlayerFullName()
246+
247+
C_ChatInfo.RegisterAddonMessagePrefix(self.PREFIX)
248+
249+
local f = CreateFrame("Frame")
250+
f:RegisterEvent("CHAT_MSG_ADDON")
251+
f:RegisterEvent("GROUP_ROSTER_UPDATE")
252+
f:SetScript("OnEvent", function(_, event, ...)
253+
if event == "CHAT_MSG_ADDON" then
254+
C:OnAddonMessage(...)
255+
elseif event == "GROUP_ROSTER_UPDATE" then
256+
local key = C:GroupCompositionKey()
257+
if key ~= C.lastGroupKey then
258+
C.lastGroupKey = key
259+
C:ScheduleSync(3)
260+
end
261+
end
262+
end)
263+
self.eventFrame = f
264+
self.lastGroupKey = self:GroupCompositionKey()
265+
266+
-- Initial sync shortly after login (if enabled + in a group/guild).
267+
C_Timer.After(3, function() C:DoSync() end)
268+
end

0 commit comments

Comments
 (0)