1/* (c) Shereef Marzouk. See "licence DDRace.txt" and the readme.txt in the root of the distribution for more information. */
2#include "gamecontext.h"
3
4#include <base/log.h>
5#include <base/net.h>
6#include <base/time.h>
7
8#include <engine/shared/config.h>
9
10static NETADDR KeyAddress(NETADDR Addr)
11{
12 if(Addr.type == NETTYPE_WEBSOCKET_IPV4)
13 {
14 Addr.type = NETTYPE_IPV4;
15 }
16 else if(Addr.type == NETTYPE_WEBSOCKET_IPV6)
17 {
18 Addr.type = NETTYPE_IPV6;
19 }
20 Addr.port = 0;
21 return Addr;
22}
23
24int CMute::SecondsLeft() const
25{
26 return m_Expire - time_timestamp();
27}
28
29CMutes::CMutes(const char *pSystemName) :
30 m_pSystemName(pSystemName)
31{
32}
33
34bool CMutes::Mute(const NETADDR *pAddr, int Seconds, const char *pReason, const char *pClientName, bool InitialDelay)
35{
36 if(!in_range(a: Seconds, lower: 1, upper: 365 * 24 * 60 * 60))
37 {
38 log_info(m_pSystemName, "Invalid mute duration: %d", Seconds);
39 return false;
40 }
41
42 // Find a matching mute for this IP, update expiration time if found
43 const int64_t Expire = time_timestamp() + Seconds;
44 CMute &Mute = m_Mutes[KeyAddress(Addr: *pAddr)];
45 if(!Mute.m_Initialized)
46 {
47 Mute.m_Initialized = true;
48 Mute.m_Expire = Expire;
49 str_copy(dst&: Mute.m_aReason, src: pReason);
50 Mute.m_InitialDelay = InitialDelay;
51 if(pClientName)
52 {
53 str_copy(dst&: Mute.m_aClientName, src: pClientName);
54 Mute.m_NameKnown = true;
55 }
56 else
57 {
58 Mute.m_aClientName[0] = '\0';
59 Mute.m_NameKnown = false;
60 }
61
62 return true;
63 }
64 if(Expire > Mute.m_Expire)
65 {
66 Mute.m_Expire = Expire;
67 str_copy(dst&: Mute.m_aReason, src: pReason);
68 Mute.m_InitialDelay = InitialDelay;
69 }
70 return true;
71}
72
73void CMutes::UnmuteIndex(int Index)
74{
75 if(Index < 0 || Index >= (int)m_Mutes.size())
76 {
77 log_info(m_pSystemName, "Invalid index to unmute: %d", Index);
78 return;
79 }
80 auto It = std::next(x: m_Mutes.begin(), n: Index);
81 char aAddrString[NETADDR_MAXSTRSIZE];
82 net_addr_str(addr: &It->first, string: aAddrString, max_length: sizeof(aAddrString), add_port: false);
83 if(It->second.m_NameKnown)
84 log_info(m_pSystemName, "Unmuted: '%s' (<{%s}>)", It->second.m_aClientName, aAddrString);
85 else
86 log_info(m_pSystemName, "Unmuted: (unknown) (<{%s}>)", aAddrString);
87 m_Mutes.erase(position: It);
88}
89
90void CMutes::UnmuteAddr(const NETADDR *pAddr)
91{
92 NETADDR KeyAddr = KeyAddress(Addr: *pAddr);
93 char aAddrString[NETADDR_MAXSTRSIZE];
94 net_addr_str(addr: &KeyAddr, string: aAddrString, max_length: sizeof(aAddrString), add_port: false);
95 auto It = m_Mutes.find(x: KeyAddr);
96 if(It == m_Mutes.end())
97 {
98 log_info(m_pSystemName, "No mutes for this IP address found: %s", aAddrString);
99 return;
100 }
101 if(It->second.m_NameKnown)
102 log_info(m_pSystemName, "Unmuted: '%s' (<{%s}>)", It->second.m_aClientName, aAddrString);
103 else
104 log_info(m_pSystemName, "Unmuted: (unknown) (<{%s}>)", aAddrString);
105 m_Mutes.erase(position: It);
106}
107
108std::optional<CMute> CMutes::IsMuted(const NETADDR *pAddr, bool RespectInitialDelay) const
109{
110 const auto It = m_Mutes.find(x: KeyAddress(Addr: *pAddr));
111 if(It == m_Mutes.end())
112 {
113 return std::nullopt;
114 }
115 if(!RespectInitialDelay && !It->second.m_InitialDelay)
116 {
117 return std::nullopt;
118 }
119 return It->second;
120}
121
122void CMutes::UnmuteExpired()
123{
124 const int64_t Now = time_timestamp();
125 for(auto It = m_Mutes.begin(); It != m_Mutes.end();)
126 {
127 if(It->second.m_Expire <= Now)
128 {
129 It = m_Mutes.erase(position: It);
130 }
131 else
132 {
133 ++It;
134 }
135 }
136}
137
138void CMutes::Print(int Page) const
139{
140 if(m_Mutes.empty())
141 {
142 log_info(m_pSystemName, "The mute list is empty.");
143 return;
144 }
145
146 static constexpr int ENTRIES_PER_PAGE = 20;
147 const int NumPages = std::ceil(x: m_Mutes.size() / (float)ENTRIES_PER_PAGE);
148 if(!in_range(a: Page, lower: 1, upper: NumPages))
149 {
150 log_info(m_pSystemName, "Invalid page number. There %s %d %s available.",
151 NumPages == 1 ? "is" : "are", NumPages, NumPages == 1 ? "page" : "pages");
152 return;
153 }
154
155 const int Start = (Page - 1) * ENTRIES_PER_PAGE;
156 const int End = Page * ENTRIES_PER_PAGE;
157
158 int Count = 0;
159 for(const auto &[Addr, MuteEntry] : m_Mutes)
160 {
161 if(Count < Start)
162 {
163 Count++;
164 continue;
165 }
166 else if(Count >= End)
167 {
168 break;
169 }
170
171 char aAddrString[NETADDR_MAXSTRSIZE];
172 net_addr_str(addr: &Addr, string: aAddrString, max_length: sizeof(aAddrString), add_port: false);
173
174 if(MuteEntry.m_NameKnown)
175 {
176 log_info(
177 m_pSystemName,
178 "#%d '%s' (<{%s}>) muted for %d seconds (%s)",
179 Count,
180 MuteEntry.m_aClientName,
181 aAddrString,
182 MuteEntry.SecondsLeft(),
183 MuteEntry.m_aReason[0] == '\0' ? "No reason given" : MuteEntry.m_aReason);
184 }
185 else
186 {
187 log_info(
188 m_pSystemName,
189 "#%d (unknown) (<{%s}>) muted for %d seconds (%s)",
190 Count,
191 aAddrString,
192 MuteEntry.SecondsLeft(),
193 MuteEntry.m_aReason[0] == '\0' ? "No reason given" : MuteEntry.m_aReason);
194 }
195
196 Count++;
197 }
198 log_info(m_pSystemName, "%d %s, showing entries %d - %d (page %d/%d)",
199 (int)m_Mutes.size(), m_Mutes.size() == 1 ? "mute" : "mutes", Start, Count - 1, Page, NumPages);
200}
201
202void CGameContext::MuteWithMessage(const NETADDR *pAddr, int Seconds, const char *pReason, const char *pDisplayName)
203{
204 if(!m_Mutes.Mute(pAddr, Seconds, pReason, pClientName: pDisplayName, InitialDelay: false))
205 {
206 return;
207 }
208
209 char aChatMessage[256];
210 if(pReason[0] != '\0')
211 {
212 str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "'%s' has been muted for %d seconds (%s)", pDisplayName, Seconds, pReason);
213 }
214 else
215 {
216 str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "'%s' has been muted for %d seconds", pDisplayName, Seconds);
217 }
218 SendChat(ClientId: -1, Team: TEAM_ALL, pText: aChatMessage);
219}
220
221void CGameContext::VoteMuteWithMessage(const NETADDR *pAddr, int Seconds, const char *pReason, const char *pDisplayName)
222{
223 if(!m_VoteMutes.Mute(pAddr, Seconds, pReason, pClientName: pDisplayName, InitialDelay: false))
224 {
225 return;
226 }
227
228 char aChatMessage[256];
229 if(pReason[0] != '\0')
230 {
231 str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "'%s' has been banned from voting for %d seconds (%s)", pDisplayName, Seconds, pReason);
232 }
233 else
234 {
235 str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "'%s' has been banned from voting for %d seconds", pDisplayName, Seconds);
236 }
237 SendChat(ClientId: -1, Team: TEAM_ALL, pText: aChatMessage);
238}
239
240void CGameContext::ConMute(IConsole::IResult *pResult, void *pUserData)
241{
242 log_info("mutes", "This command is deprecated. Use either 'muteid <client_id> <seconds> <reason>' or 'muteip <ip> <seconds> <reason>'");
243}
244
245void CGameContext::ConMuteId(IConsole::IResult *pResult, void *pUserData)
246{
247 CGameContext *pSelf = (CGameContext *)pUserData;
248
249 const int Victim = pResult->GetVictim();
250 if(!CheckClientId(ClientId: Victim) || !pSelf->m_apPlayers[Victim])
251 {
252 log_info("mutes", "Client ID not found: %d", Victim);
253 return;
254 }
255
256 const char *pReason = pResult->NumArguments() > 2 ? pResult->GetString(Index: 2) : "";
257 pSelf->MuteWithMessage(pAddr: pSelf->Server()->ClientAddr(ClientId: Victim), Seconds: pResult->GetInteger(Index: 1), pReason, pDisplayName: pSelf->Server()->ClientName(ClientId: Victim));
258}
259
260void CGameContext::ConMuteIp(IConsole::IResult *pResult, void *pUserData)
261{
262 CGameContext *pSelf = (CGameContext *)pUserData;
263
264 NETADDR Addr;
265 if(net_addr_from_str(addr: &Addr, string: pResult->GetString(Index: 0)) != 0)
266 {
267 log_info("mutes", "Invalid IP address to mute: %s", pResult->GetString(0));
268 return;
269 }
270
271 const char *pReason = pResult->NumArguments() > 2 ? pResult->GetString(Index: 2) : "";
272
273 pSelf->m_Mutes.Mute(pAddr: &Addr, Seconds: pResult->GetInteger(Index: 1), pReason, pClientName: nullptr, InitialDelay: false);
274}
275
276void CGameContext::ConUnmute(IConsole::IResult *pResult, void *pUserData)
277{
278 CGameContext *pSelf = (CGameContext *)pUserData;
279
280 pSelf->m_Mutes.UnmuteIndex(Index: pResult->GetInteger(Index: 0));
281}
282
283void CGameContext::ConUnmuteId(IConsole::IResult *pResult, void *pUserData)
284{
285 CGameContext *pSelf = (CGameContext *)pUserData;
286
287 const int Victim = pResult->GetVictim();
288 if(!CheckClientId(ClientId: Victim) || !pSelf->m_apPlayers[Victim])
289 {
290 log_info("mutes", "Client ID not found: %d", Victim);
291 return;
292 }
293
294 pSelf->m_Mutes.UnmuteAddr(pAddr: pSelf->Server()->ClientAddr(ClientId: Victim));
295}
296
297void CGameContext::ConUnmuteIp(IConsole::IResult *pResult, void *pUserData)
298{
299 CGameContext *pSelf = (CGameContext *)pUserData;
300
301 NETADDR Addr;
302 if(net_addr_from_str(addr: &Addr, string: pResult->GetString(Index: 0)) != 0)
303 {
304 log_info("mutes", "Invalid IP address to unmute: %s", pResult->GetString(0));
305 return;
306 }
307
308 pSelf->m_Mutes.UnmuteAddr(pAddr: &Addr);
309}
310
311void CGameContext::ConMutes(IConsole::IResult *pResult, void *pUserData)
312{
313 CGameContext *pSelf = (CGameContext *)pUserData;
314
315 pSelf->m_Mutes.Print(Page: pResult->NumArguments() > 0 ? pResult->GetInteger(Index: 0) : 1);
316}
317
318void CGameContext::ConVoteMute(IConsole::IResult *pResult, void *pUserData)
319{
320 log_info("votemutes", "Use either 'vote_muteid <client_id> <seconds> <reason>' or 'vote_muteip <ip> <seconds> <reason>'");
321}
322
323void CGameContext::ConVoteMuteId(IConsole::IResult *pResult, void *pUserData)
324{
325 CGameContext *pSelf = (CGameContext *)pUserData;
326
327 const int Victim = pResult->GetVictim();
328 if(!CheckClientId(ClientId: Victim) || !pSelf->m_apPlayers[Victim])
329 {
330 log_info("votemutes", "Client ID not found: %d", Victim);
331 return;
332 }
333
334 const char *pReason = pResult->NumArguments() > 2 ? pResult->GetString(Index: 2) : "";
335 pSelf->VoteMuteWithMessage(pAddr: pSelf->Server()->ClientAddr(ClientId: Victim), Seconds: pResult->GetInteger(Index: 1), pReason, pDisplayName: pSelf->Server()->ClientName(ClientId: Victim));
336}
337
338void CGameContext::ConVoteMuteIp(IConsole::IResult *pResult, void *pUserData)
339{
340 CGameContext *pSelf = (CGameContext *)pUserData;
341
342 NETADDR Addr;
343 if(net_addr_from_str(addr: &Addr, string: pResult->GetString(Index: 0)) != 0)
344 {
345 log_info("votemutes", "Invalid IP address to mute: %s", pResult->GetString(0));
346 return;
347 }
348
349 const char *pReason = pResult->NumArguments() > 2 ? pResult->GetString(Index: 2) : "";
350
351 pSelf->m_VoteMutes.Mute(pAddr: &Addr, Seconds: pResult->GetInteger(Index: 1), pReason, pClientName: nullptr, InitialDelay: false);
352}
353
354void CGameContext::ConVoteUnmute(IConsole::IResult *pResult, void *pUserData)
355{
356 CGameContext *pSelf = (CGameContext *)pUserData;
357
358 pSelf->m_VoteMutes.UnmuteIndex(Index: pResult->GetInteger(Index: 0));
359}
360
361void CGameContext::ConVoteUnmuteId(IConsole::IResult *pResult, void *pUserData)
362{
363 CGameContext *pSelf = (CGameContext *)pUserData;
364
365 const int Victim = pResult->GetVictim();
366 if(!CheckClientId(ClientId: Victim) || !pSelf->m_apPlayers[Victim])
367 {
368 log_info("votemutes", "Client ID not found: %d", Victim);
369 return;
370 }
371
372 pSelf->m_VoteMutes.UnmuteAddr(pAddr: pSelf->Server()->ClientAddr(ClientId: Victim));
373}
374
375void CGameContext::ConVoteUnmuteIp(IConsole::IResult *pResult, void *pUserData)
376{
377 CGameContext *pSelf = (CGameContext *)pUserData;
378
379 NETADDR Addr;
380 if(net_addr_from_str(addr: &Addr, string: pResult->GetString(Index: 0)) != 0)
381 {
382 log_info("votemutes", "Invalid IP address to unmute: %s", pResult->GetString(0));
383 return;
384 }
385
386 pSelf->m_VoteMutes.UnmuteAddr(pAddr: &Addr);
387}
388
389void CGameContext::ConVoteMutes(IConsole::IResult *pResult, void *pUserData)
390{
391 CGameContext *pSelf = (CGameContext *)pUserData;
392
393 pSelf->m_VoteMutes.Print(Page: pResult->NumArguments() > 0 ? pResult->GetInteger(Index: 0) : 1);
394}
395