Update README.md
[KisSync.git] / src / channel / filters.js
blobc72f41dfe1fb73afbcdceb4fb6913e0dfc9e2e1f
1 var FilterList = require("cytubefilters");
2 var ChannelModule = require("./module");
4 const LOGGER = require('@calzoneman/jsli')('filters');
6 /*
7 * Converts JavaScript-style replacements ($1, $2, etc.) with
8 * PCRE-style (\1, \2, etc.)
9 */
10 function fixReplace(replace) {
11 return replace.replace(/\$(\d)/g, "\\$1");
14 function validateFilter(f) {
15 if (typeof f.source !== "string" || typeof f.flags !== "string" ||
16 typeof f.replace !== "string") {
17 return null;
20 if (typeof f.name !== "string") {
21 f.name = f.source;
24 f.replace = fixReplace(f.replace.substring(0, 1000));
25 f.flags = f.flags.substring(0, 4);
27 try {
28 FilterList.checkValidRegex(f.source);
29 } catch (e) {
30 return null;
33 var filter = {
34 name: f.name,
35 source: f.source,
36 replace: fixReplace(f.replace),
37 flags: f.flags,
38 active: !!f.active,
39 filterlinks: !!f.filterlinks
42 return filter;
45 function makeDefaultFilter(name, source, flags, replace) {
46 return {
47 name: name,
48 source: source,
49 flags: flags,
50 replace: replace,
51 active: true,
52 filterlinks: false
56 const DEFAULT_FILTERS = [
57 makeDefaultFilter("monospace", "`(.+?)`", "g", "<code>\\1</code>"),
58 makeDefaultFilter("bold", "\\*(.+?)\\*", "g", "<strong>\\1</strong>"),
59 makeDefaultFilter("italic", "_(.+?)_", "g", "<em>\\1</em>"),
60 makeDefaultFilter("strike", "~~(.+?)~~", "g", "<s>\\1</s>"),
61 makeDefaultFilter("inline spoiler", "\\[sp\\](.*?)\\[\\/sp\\]", "ig",
62 "<span class=\"spoiler\">\\1</span>")
65 function ChatFilterModule(_channel) {
66 ChannelModule.apply(this, arguments);
67 this.filters = new FilterList();
68 this.supportsDirtyCheck = true;
71 ChatFilterModule.prototype = Object.create(ChannelModule.prototype);
73 ChatFilterModule.prototype.load = function (data) {
74 if ("filters" in data) {
75 var filters = data.filters.map(validateFilter).filter(function (f) {
76 return f !== null;
77 });
78 try {
79 this.filters = new FilterList(filters);
80 } catch (e) {
81 LOGGER.error("Filter load failed: " + e + " (channel:" +
82 this.channel.name);
83 this.channel.logger.log("Failed to load filters: " + e);
85 } else {
86 this.filters = new FilterList(DEFAULT_FILTERS);
89 this.dirty = false;
92 ChatFilterModule.prototype.save = function (data) {
93 data.filters = this.filters.pack();
96 ChatFilterModule.prototype.packInfo = function (data, isAdmin) {
97 if (isAdmin) {
98 data.chatFilterCount = this.filters.length;
102 ChatFilterModule.prototype.onUserPostJoin = function (user) {
103 user.socket.on("addFilter", this.handleAddFilter.bind(this, user));
104 user.socket.on("updateFilter", this.handleUpdateFilter.bind(this, user));
105 user.socket.on("importFilters", this.handleImportFilters.bind(this, user));
106 user.socket.on("moveFilter", this.handleMoveFilter.bind(this, user));
107 user.socket.on("removeFilter", this.handleRemoveFilter.bind(this, user));
108 user.socket.on("requestChatFilters", this.handleRequestChatFilters.bind(this, user));
111 ChatFilterModule.prototype.sendChatFilters = function (users) {
112 var f = this.filters.pack();
113 var chan = this.channel;
114 users.forEach(function (u) {
115 if (chan.modules.permissions.canEditFilters(u)) {
116 u.socket.emit("chatFilters", f);
121 ChatFilterModule.prototype.handleAddFilter = function (user, data) {
122 if (typeof data !== "object") {
123 return;
126 if (!this.channel.modules.permissions.canEditFilters(user)) {
127 return;
130 try {
131 FilterList.checkValidRegex(data.source);
132 } catch (e) {
133 user.socket.emit("errorMsg", {
134 msg: "Invalid regex: " + e.message,
135 alert: true
137 return;
140 data = validateFilter(data);
141 if (!data) {
142 return;
145 try {
146 this.filters.addFilter(data);
147 } catch (e) {
148 user.socket.emit("errorMsg", {
149 msg: "Filter add failed: " + e.message,
150 alert: true
152 return;
155 this.dirty = true;
157 user.socket.emit("addFilterSuccess");
159 var chan = this.channel;
160 chan.users.forEach(function (u) {
161 if (chan.modules.permissions.canEditFilters(u)) {
162 u.socket.emit("updateChatFilter", data);
166 chan.logger.log("[mod] " + user.getName() + " added filter: " + data.name + " -> " +
167 "s/" + data.source + "/" + data.replace + "/" + data.flags +
168 " active: " + data.active + ", filterlinks: " + data.filterlinks);
171 ChatFilterModule.prototype.handleUpdateFilter = function (user, data) {
172 if (typeof data !== "object") {
173 return;
176 if (!this.channel.modules.permissions.canEditFilters(user)) {
177 return;
180 try {
181 FilterList.checkValidRegex(data.source);
182 } catch (e) {
183 user.socket.emit("errorMsg", {
184 msg: "Invalid regex: " + e.message,
185 alert: true
187 return;
190 data = validateFilter(data);
191 if (!data) {
192 return;
195 try {
196 this.filters.updateFilter(data);
197 } catch (e) {
198 user.socket.emit("errorMsg", {
199 msg: "Filter update failed: " + e.message,
200 alert: true
202 return;
205 this.dirty = true;
207 var chan = this.channel;
208 chan.users.forEach(function (u) {
209 if (chan.modules.permissions.canEditFilters(u)) {
210 u.socket.emit("updateChatFilter", data);
214 chan.logger.log("[mod] " + user.getName() + " updated filter: " + data.name + " -> " +
215 "s/" + data.source + "/" + data.replace + "/" + data.flags +
216 " active: " + data.active + ", filterlinks: " + data.filterlinks);
219 ChatFilterModule.prototype.handleImportFilters = function (user, data) {
220 if (!(data instanceof Array)) {
221 return;
224 /* Note: importing requires a different permission node than simply
225 updating/removing */
226 if (!this.channel.modules.permissions.canImportFilters(user)) {
227 return;
230 try {
231 this.filters = new FilterList(data.map(validateFilter).filter(function (f) {
232 return f !== null;
233 }));
234 } catch (e) {
235 user.socket.emit("errorMsg", {
236 msg: "Filter import failed: " + e.message,
237 alert: true
239 return;
242 this.dirty = true;
244 this.channel.logger.log("[mod] " + user.getName() + " imported the filter list");
245 this.sendChatFilters(this.channel.users);
248 ChatFilterModule.prototype.handleRemoveFilter = function (user, data) {
249 if (typeof data !== "object") {
250 return;
253 if (!this.channel.modules.permissions.canEditFilters(user)) {
254 return;
257 if (typeof data.name !== "string") {
258 return;
261 try {
262 this.filters.removeFilter(data);
263 } catch (e) {
264 user.socket.emit("errorMsg", {
265 msg: "Filter removal failed: " + e.message,
266 alert: true
268 return;
271 this.dirty = true;
273 var chan = this.channel;
274 chan.users.forEach(function (u) {
275 if (chan.modules.permissions.canEditFilters(u)) {
276 u.socket.emit("deleteChatFilter", data);
280 this.channel.logger.log("[mod] " + user.getName() + " removed filter: " + data.name);
283 ChatFilterModule.prototype.handleMoveFilter = function (user, data) {
284 if (typeof data !== "object") {
285 return;
288 if (!this.channel.modules.permissions.canEditFilters(user)) {
289 return;
292 if (typeof data.to !== "number" || typeof data.from !== "number") {
293 return;
296 try {
297 this.filters.moveFilter(data.from, data.to);
298 } catch (e) {
299 user.socket.emit("errorMsg", {
300 msg: "Filter move failed: " + e.message,
301 alert: true
303 return;
306 this.dirty = true;
309 ChatFilterModule.prototype.handleRequestChatFilters = function (user) {
310 this.sendChatFilters([user]);
313 module.exports = ChatFilterModule;