1/*
2#include "censor.h"
3
4#include <base/log.h>
5
6#include <engine/engine.h>
7#include <engine/external/json-parser/json.h>
8#include <engine/shared/config.h>
9
10#include <optional>
11#include <utility>
12
13static void ReplaceWords(char *pBuffer, const std::vector<std::string> &vWords, char Replacement)
14{
15 if(!pBuffer)
16 return;
17
18 for(const auto &Word : vWords)
19 {
20 const char *pEnd = nullptr;
21 const char *pStart = pBuffer;
22
23 while(pStart)
24 {
25 pStart = str_utf8_find_nocase(pStart, Word.c_str(), &pEnd);
26 if(!pStart)
27 continue;
28 if((pStart == pBuffer || str_utf8_isspace(*(pStart - 1))) && (str_utf8_isspace(*(pEnd))))
29 {
30 while(pStart != pEnd)
31 {
32 pBuffer[pStart - pBuffer] = Replacement;
33 pStart++;
34 }
35 }
36 pStart = pEnd;
37 }
38 }
39}
40
41void CCensor::ConchainRefreshCensorList(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
42{
43 pfnCallback(pResult, pCallbackUserData);
44 if(!g_Config.m_ClCensorChat)
45 log_warn("censor", "The config cl_censor_chat is currently off! You have to set it to 1 to use the censor feature.");
46 if(pResult->NumArguments() && str_comp(g_Config.m_ClCensorUrl, pResult->GetString(1)))
47 ((CCensor *)pUserData)->Reset();
48}
49
50CCensor::CCensor()
51{
52 m_vCensoredWords = {};
53}
54
55void CCensor::OnInit()
56{
57 Reset();
58}
59
60void CCensor::OnConsoleInit()
61{
62 Console()->Chain("cl_censor_url", ConchainRefreshCensorList, this);
63}
64
65void CCensor::Reset()
66{
67 m_vCensoredWords.clear();
68
69 if(m_pCensorListDownloadJob)
70 {
71 m_pCensorListDownloadJob->Abort();
72 }
73
74 m_pCensorListDownloadJob = std::make_shared<CCensorListDownloadJob>(this, g_Config.m_ClCensorUrl, "censored_words_online.json");
75 Engine()->AddJob(m_pCensorListDownloadJob);
76}
77
78void CCensor::OnRender()
79{
80 if(m_pCensorListDownloadJob && m_pCensorListDownloadJob->Done())
81 {
82 if(m_pCensorListDownloadJob->m_vLoadedWords)
83 m_vCensoredWords = std::move(*m_pCensorListDownloadJob->m_vLoadedWords);
84 m_pCensorListDownloadJob = nullptr;
85 }
86}
87
88void CCensor::OnShutdown()
89{
90 if(m_pCensorListDownloadJob)
91 {
92 m_pCensorListDownloadJob->Abort();
93 }
94}
95
96void CCensor::CensorMessage(char *pMessage) const
97{
98 if(!g_Config.m_ClCensorChat)
99 return;
100
101 if(!*pMessage)
102 return;
103 ReplaceWords(pMessage, m_vCensoredWords, '*');
104}
105
106std::optional<std::vector<std::string>> CCensor::LoadCensorListFromFile(const char *pFilePath) const
107{
108 void *pFileData;
109 unsigned FileSize;
110 if(!Storage()->ReadFile(pFilePath, IStorage::TYPE_SAVE, &pFileData, &FileSize))
111 {
112 log_error("censor", "Failed to open/read word censor file '%s'", pFilePath);
113 return std::nullopt;
114 }
115
116 std::optional<std::vector<std::string>> vWordList = LoadCensorList(pFileData, FileSize);
117 free(pFileData);
118 return vWordList;
119}
120
121std::optional<std::vector<std::string>> CCensor::LoadCensorList(const void *pListText, size_t ListTextLen) const
122{
123 std::vector<std::string> vWordList = {};
124
125 json_value *pData = nullptr;
126 json_settings JsonSettings{};
127 char aError[256];
128 pData = json_parse_ex(&JsonSettings, static_cast<const json_char *>(pListText), ListTextLen, aError);
129
130 if(!pData)
131 {
132 log_error("censor", "Failed to parse censor list: %s", aError);
133 return std::nullopt;
134 }
135
136 if(pData->type != json_object)
137 {
138 log_error("censor", "Censor list malformed: root must be an object");
139 return std::nullopt;
140 }
141
142 const json_value &Words = (*pData)["words"];
143
144 if(Words.type != json_array)
145 {
146 log_error("censor", "Censor list malformed: 'words' must be an array");
147 return std::nullopt;
148 }
149 int Length = Words.u.array.length;
150
151 for(int i = 0; i < Length; ++i)
152 {
153 const json_value &JsonWord = Words[i];
154
155 if(JsonWord.type != json_string)
156 {
157 log_error("censor", "Censor list malformed: 'words' must be an array of strings (error at index %d)", i);
158 return std::nullopt;
159 }
160
161 const char *pWord = JsonWord.u.string.ptr;
162 if(*pWord)
163 {
164 vWordList.emplace_back(pWord);
165 }
166 }
167
168 json_value_free(pData);
169
170 log_info("censor", "Loaded %" PRIzu " words from censor list", vWordList.size());
171 return vWordList;
172}
173
174CCensor::CCensorListDownloadJob::CCensorListDownloadJob(CCensor *pCensor, const char *pUrl, const char *pSaveFilePath) :
175 m_pCensor(pCensor)
176{
177 str_copy(m_aUrl, pUrl);
178 str_copy(m_aSaveFilePath, pSaveFilePath);
179 m_vLoadedWords = std::nullopt;
180 Abortable(true);
181}
182
183bool CCensor::CCensorListDownloadJob::Abort()
184{
185 if(!IJob::Abort())
186 {
187 return false;
188 }
189
190 const CLockScope LockScope(m_Lock);
191 if(m_pGetRequest)
192 {
193 m_pGetRequest->Abort();
194 m_pGetRequest = nullptr;
195 }
196 return true;
197}
198
199void CCensor::CCensorListDownloadJob::Run()
200{
201 const CTimeout Timeout{10000, 0, 8192, 10};
202 const size_t MaxResponseSize = 50 * 1024 * 1024; // 50 MiB
203
204 std::shared_ptr<CHttpRequest> pGet = HttpGetBoth(m_aUrl, m_pCensor->Storage(), m_aSaveFilePath, IStorage::TYPE_SAVE);
205 pGet->Timeout(Timeout);
206 pGet->MaxResponseSize(MaxResponseSize);
207 pGet->SkipByFileTime(true);
208 pGet->ValidateBeforeOverwrite(true);
209 pGet->FailOnErrorStatus(false);
210 pGet->LogProgress(HTTPLOG::ALL);
211 {
212 const CLockScope LockScope(m_Lock);
213 m_pGetRequest = pGet;
214 }
215 m_pCensor->Http()->Run(pGet);
216
217 // Load existing file while waiting for the HTTP request
218 {
219 m_vLoadedWords = m_pCensor->LoadCensorListFromFile(m_aSaveFilePath);
220 }
221
222 pGet->Wait();
223 {
224 const CLockScope LockScope(m_Lock);
225 m_pGetRequest = nullptr;
226 }
227 if(pGet->State() != EHttpState::DONE || State() == IJob::STATE_ABORTED)
228 {
229 return;
230 }
231 if(pGet->StatusCode() >= 400)
232 {
233 log_info("censor", "got http error %d", pGet->StatusCode());
234 return;
235 }
236
237 if(pGet->StatusCode() == 304) // 304 Not Modified
238 {
239 return;
240 }
241
242 if(State() == IJob::STATE_ABORTED)
243 {
244 return;
245 }
246
247 unsigned char *pResult;
248 size_t ResultSize;
249 pGet->Result(&pResult, &ResultSize);
250
251 std::optional<std::vector<std::string>> vLoadedWords = m_pCensor->LoadCensorList((const char *)pResult, ResultSize);
252 bool Success = vLoadedWords.has_value();
253 pGet->OnValidation(Success);
254 if(Success)
255 {
256 m_vLoadedWords = std::move(vLoadedWords);
257 }
258}
259*/
260