Skip to content

Commit c0dbb30

Browse files
authored
Merge pull request #131 from jagerman/filter-autoreply
Filter responses and more filtering control
2 parents ba7e723 + d81414e commit c0dbb30

File tree

5 files changed

+441
-41
lines changed

5 files changed

+441
-41
lines changed

sogs.ini.filter-sample

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
; This file describes the settings you can use for advanced filtering controls.
2+
;
3+
; Note that when configuring this, it does *not* go in a separate file but rather in your active
4+
; sogs.ini configuration file. (Since everything goes here in a separate section, it doesn't matter
5+
; where in sogs.ini you add it).
6+
7+
8+
;
9+
; Room-specific filtering
10+
;
11+
; To set filtration rules for a specific room you add a [room:TOKEN] section and then set the
12+
; rules that should apply to this specific room. For example, to enable the profanity filter and
13+
; disallow (only) cyrillic characters in the room with token 'sudoku' you would add:
14+
;
15+
;[room:sudoku]
16+
;profanity_filter=yes
17+
;profanity_silent=yes
18+
;alphabet_filters=cyrillic
19+
;
20+
; This overrides the default from the main [messages] config section for any given keys, so it can
21+
; be used to replace or change the rules that apply to a given room. Currently only the
22+
; profanity_filter, profanity_silent, alphabet_filters can be overridden in this way.
23+
24+
;
25+
; Filtration responses
26+
;
27+
; When a message is filtered because of the profanity or alphabet filtrations SOGS can optionally
28+
; send a reply in the room; this reply can either be visible to everyone, or just to the specific
29+
; user. To enable such a reply, add a filter section here: the section name consists of
30+
; 'filter:TYPE:ROOM' where TYPE and ROOM are the filtration type and room token, or '*' to match all
31+
; types/rooms.
32+
;
33+
; Section names for all filtered messages:
34+
;[filter:*:*]
35+
;
36+
; Section names for a particular filtration type:
37+
;[filter:*:profanity]
38+
;[filter:*:alphabet]
39+
;
40+
; The "type" can also be a specific language:
41+
;[filter:*:arabic]
42+
;[filter:*:cyrillic]
43+
; etc.
44+
;
45+
; Room-specific filtration section names:
46+
;
47+
;[filter:fishing:*]
48+
;[filter:sudoku:profanity]
49+
;
50+
; If using both '*' and specific values, the value from the more specific section will be used where
51+
; present.
52+
;
53+
; Within this section there are currently three settings:
54+
;
55+
; - reply -- the body of a reply to send (see details below). If omitted or empty then no reply
56+
; will be sent.
57+
; - profile_name -- the profile name to use in that reply.
58+
; - public -- whether the reply should be seen by everyone or just the poster. The default is 'no'
59+
; (i.e. only the user will see the reply).
60+
;
61+
; The `reply` value should be specified on a single line of the config, and supports the following
62+
; substitutions:
63+
;
64+
; \@ - the profile name, in @tag form, of the poster whose message was declined.
65+
; \p - the profile name in plain text.
66+
; \r - the name of the room
67+
; \t - the token of the room
68+
; \n - a line break
69+
; \\ - a literal \ character
70+
;
71+
; You can also randomize among multiple responses by specifying multiple lines in the config: each
72+
; additional line must be indented in the .ini file to be properly recognized.
73+
;
74+
; For example if you use this config:
75+
;
76+
77+
[messages]
78+
profanity_filter=yes
79+
profanity_silent=yes
80+
alphabet_filters=arabic cyrillic
81+
alphabet_silent=yes
82+
83+
[room:sailors]
84+
profanity_filter=no
85+
86+
[filter:*:*]
87+
profile_name=LanguagePolice
88+
reply=Hi \@, I'm afraid your message couldn't be sent: \r is English-only!
89+
90+
[filter:*:profanity]
91+
profile_name=Swear Jar
92+
reply=Whoa there, \@! That language is too strong for the \r group! Try the Sailors group instead.
93+
94+
[filter:sudoku:profanity]
95+
profile_name=Bot45
96+
public=yes
97+
reply=\@ got a little too enthusiastic today with their solve. Maybe someone can assist?
98+
Uh oh, I think \@ has two 3s in the same row!
99+
I think \@'s sudoku broke 😦
100+
101+
; then arabic/cyrillic/person would be blocked everywhere, profanity would be blocked everywhere
102+
; except the 'sailors' room, and when a message is blocked you would get a message such as one of
103+
; the following depending on the room and the rule applied:
104+
;
105+
;
106+
; (LanguagePolice)
107+
; Hi @Foreignsailor1988, I'm afraid your message couldn't be set: Salty Sailors is English-only!
108+
;
109+
;
110+
; (Swear Jar)
111+
; Whoa there @87yearoldgrandma! That language is too strong for the Cuddly Kittens group! Try the Sailors group instead.
112+
;
113+
;
114+
; (Bot45); [one of the following would be sent randomly, visible to everyone in the group]
115+
; @87yearoldgrandma got a little too enthusiastic today with their solve. Maybe someone can assist?
116+
;
117+
; Uh oh, I think @87yearoldgrandma has two 3s in the same row!
118+
;
119+
; I think @87yearoldgrandma's sudoku broke 😦

sogs.ini.sample

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,7 @@
9393

9494

9595
; Whether we should pass words through a profanity filter, rejecting messages that contain profane
96-
; words (and common permutations of those words). (Note that profanity filters are never applied to
97-
; room or server admins).
96+
; words (and common permutations of those words).
9897
;
9998
;profanity_filter = no
10099

@@ -115,6 +114,32 @@
115114
;
116115
;profanity_custom =
117116

117+
; Whether we should reject messages that use a particular alphabet. This is a space or
118+
; comma-separated list of alphabet names; posts with characters in the given language ranges will be
119+
; blocked (unless posted by a mod/admin). Currently supported are: arabic, cyrillic, and persian
120+
; (note that persian is included within arabic).
121+
;
122+
; *This* setting is the default setting for all rooms, but you can also make room-specific filtering
123+
; (see sogs.ini.filter-sample for details).
124+
;
125+
;alphabet_filters =
126+
127+
128+
; Whether the alphabet filter should silently drop (true) or return an error (false).
129+
;
130+
;alphabet_silent = yes
131+
132+
133+
; Whether the above filters should be applied to moderators and admins (=yes) or not (=no). The
134+
; default is no, that is, mods and admins messages are not filtered by default.
135+
;
136+
;filter_mods = no
137+
138+
139+
; The profanity and alphabet filters can be controlled on a per-room setting (which overrides the
140+
; global default set above) and can have automated responses sent by the SOGS server. For details
141+
; and examples see the sogs.ini.filter-sample file.
142+
118143

119144
[web]
120145

sogs/config.py

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,15 @@
3232
PROFANITY_FILTER = False
3333
PROFANITY_SILENT = True
3434
PROFANITY_CUSTOM = None
35+
ALPHABET_FILTERS = set()
36+
ALPHABET_SILENT = True
37+
FILTER_MODS = False
3538
REQUIRE_BLIND_KEYS = False
3639
TEMPLATE_PATH = 'templates'
3740
STATIC_PATH = 'static'
3841
UPLOAD_PATH = 'uploads'
42+
ROOM_OVERRIDES = {}
43+
FILTER_SETTINGS = {}
3944

4045
# Will be true if we're running as a uwsgi app, false otherwise; used where we need to do things
4146
# only in one case or another (e.g. database initialization only via app mode).
@@ -83,13 +88,35 @@ def days_to_seconds(v):
8388
def days_to_seconds_or_none(v):
8489
return days_to_seconds(v) if v else None
8590

91+
def set_of_strs(v):
92+
return {s for s in re.split('[,\\s]+', v) if s != ''}
93+
8694
truthy = ('y', 'yes', 'Y', 'Yes', 'true', 'True', 'on', 'On', '1')
8795
falsey = ('n', 'no', 'N', 'No', 'false', 'False', 'off', 'Off', '0')
8896
booly = truthy + falsey
8997

9098
def bool_opt(name):
9199
return (name, lambda x: x in booly, lambda x: x in truthy)
92100

101+
reply_fields = {
102+
r'\@': '{profile_at}',
103+
r'\p': '{profile_name}',
104+
r'\r': '{room_name}',
105+
r'\t': '{room token}',
106+
'{': '{{',
107+
'}': '}}',
108+
r'\\': '\\',
109+
r'\n': '\n',
110+
}
111+
reply_fields_re = '(?:' + '|'.join(re.escape(k) for k in reply_fields.keys()) + ')'
112+
113+
def reply_to_format(v):
114+
return [
115+
re.sub(reply_fields_re, lambda x: reply_fields[x.group(0)], reply)
116+
for reply in v.split("\n")
117+
if reply != ''
118+
]
119+
93120
# Map of: section => { param => ('GLOBAL', test lambda, value lambda) }
94121
# global is the string name of the global variable to set
95122
# test lambda returns True/False for validation (if None/omitted, accept anything)
@@ -126,6 +153,9 @@ def bool_opt(name):
126153
'profanity_filter': bool_opt('PROFANITY_FILTER'),
127154
'profanity_silent': bool_opt('PROFANITY_SILENT'),
128155
'profanity_custom': ('PROFANITY_CUSTOM', path_exists, val_or_none),
156+
'alphabet_filters': ('ALPHABET_FILTERS', None, set_of_strs),
157+
'alphabet_silent': bool_opt('ALPHABET_SILENT'),
158+
'filter_mods': bool_opt('FILTER_MODS'),
129159
},
130160
'web': {
131161
'template_path': ('TEMPLATE_PATH', path_exists, val_or_none),
@@ -134,33 +164,73 @@ def bool_opt(name):
134164
'log': {'level': ('LOG_LEVEL',)},
135165
}
136166

137-
for s in cp.sections():
138-
if s not in setting_map:
139-
logger.warning(f"Ignoring unknown section [{s}] in {conf_ini}")
140-
continue
141-
for opt in cp[s]:
142-
if opt not in setting_map[s]:
143-
logger.warning(f"Ignoring unknown config setting [{s}].{opt} in {conf_ini}")
144-
continue
167+
room_setting_map = {
168+
'profanity_filter': bool_opt('profanity_filter'),
169+
'profanity_silent': bool_opt('profanity_silent'),
170+
'alphabet_filters': ('alphabet_filters', None, set_of_strs),
171+
}
172+
173+
filter_setting_map = {
174+
'public': bool_opt('public'),
175+
'profile_name': ('profile_name',),
176+
'reply': ('reply', None, reply_to_format),
177+
}
145178

146-
value = cp[s][opt]
147-
conf = setting_map[s][opt]
179+
def parse_option(fields, s, opt, *, room=None, filt=None):
180+
conf_type = 'room-specific ' if room else 'filter ' if filt else ''
181+
if opt not in fields:
182+
logger.warning(f"Ignoring unknown {conf_type} config setting [{s}].{opt} in {conf_ini}")
183+
return
184+
conf = fields[opt]
185+
value = cp[s][opt]
148186

149-
assert isinstance(conf, tuple) and 1 <= len(conf) <= 3
187+
assert isinstance(conf, tuple) and 1 <= len(conf) <= 3
188+
if not room and not filt:
150189
assert conf[0] in globals()
151190

152-
logger.debug(f"Loaded config setting [{s}].{opt}={value}")
191+
logger.debug(f"Loaded {'room-specific ' if room else ''}config setting [{s}].{opt}={value}")
153192

154-
if len(conf) >= 2 and conf[1]:
155-
if not conf[1](value):
156-
raise RuntimeError(f"Invalid value [{s}].{opt}={value} in {conf_ini}")
193+
if len(conf) >= 2 and conf[1]:
194+
if not conf[1](value):
195+
raise RuntimeError(f"Invalid value [{s}].{opt}={value} in {conf_ini}")
157196

158-
if len(conf) >= 3 and conf[2]:
159-
value = conf[2](value)
197+
if len(conf) >= 3 and conf[2]:
198+
value = conf[2](value)
160199

200+
if room:
201+
logger.debug(f"Set config.ROOM_OVERRIDES[{room}][{conf[0]}] = {value}")
202+
ROOM_OVERRIDES[room][conf[0]] = value
203+
elif filt:
204+
logger.debug(f"Set config.FILTER_SETTINGS[{filt[0]}][{filt[1]}][{conf[0]}] = {value}")
205+
FILTER_SETTINGS.setdefault(filt[0], {}).setdefault(filt[1], {})[conf[0]] = value
206+
else:
161207
logger.debug(f"Set config.{conf[0]} = {value}")
162208
globals()[conf[0]] = value
163209

210+
for s in cp.sections():
211+
if len(s) > 5 and s.startswith('room:'):
212+
token = s[5:]
213+
if token not in ROOM_OVERRIDES:
214+
ROOM_OVERRIDES[token] = {}
215+
for opt in cp[s]:
216+
parse_option(room_setting_map, s, opt, room=token)
217+
218+
elif s.startswith('filter:'):
219+
filt = s.split(':')[1:]
220+
if len(filt) != 2:
221+
raise RuntimeError(
222+
f"Invalid filter section [{s}] in {conf_ini}: expected [filter:TYPE:ROOM]"
223+
)
224+
for opt in cp[s]:
225+
parse_option(filter_setting_map, s, opt, filt=filt)
226+
227+
elif s in setting_map:
228+
for opt in cp[s]:
229+
parse_option(setting_map[s], s, opt)
230+
231+
else:
232+
logger.warning(f"Ignoring unknown section [{s}] in {conf_ini}")
233+
164234

165235
try:
166236
load_config()

sogs/crypto.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ def verify_sig_from_pk(data, sig, pk):
7272
return VerifyKey(pk).verify(data, sig)
7373

7474

75-
_server_signkey = SigningKey(_privkey_bytes)
75+
server_signkey = SigningKey(_privkey_bytes)
76+
server_verifykey = server_signkey.verify_key
7677

77-
server_verify = _server_signkey.verify_key.verify
78-
79-
server_sign = _server_signkey.sign
78+
server_verify = server_verifykey.verify
79+
server_sign = server_signkey.sign
8080

8181

8282
def server_encrypt(pk, data):

0 commit comments

Comments
 (0)