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