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