Skip to content

Commit 6a30558

Browse files
committed
New script censor.py: censor messages for display
1 parent ad6a83d commit 6a30558

1 file changed

Lines changed: 187 additions & 0 deletions

File tree

‎python/censor.py‎

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# SPDX-FileCopyrightText: 2025-2026 D. Bohdan <dbohdan@dbohdan.com>
2+
# SPDX-License-Identifier: MIT
3+
#
4+
# Censor messages by nick and text using color while still logging them.
5+
# This is an alternative to /ignore and triggers that remove the message from logs.
6+
#
7+
# --------------------------------------------------------------------------------------
8+
# INSTALLATION & USAGE
9+
# --------------------------------------------------------------------------------------
10+
# This script requires WeeChat with the Python plugin and Python 3.9 or later.
11+
#
12+
# 1. Save this file with WeeChat Python scripts:
13+
# ~/.local/share/weechat/python/censor.py
14+
#
15+
# 2. Load it in WeeChat:
16+
# /python load censor.py
17+
#
18+
# 3. Configure using the following options:
19+
# plugins.var.python.censor.censor_color
20+
# plugins.var.python.censor.nick_re
21+
# plugins.var.python.censor.text_re
22+
#
23+
# The script modifies the display of censored messages
24+
# and removes mIRC color codes from them.
25+
# The message still exists in the buffer and the logs.
26+
# --------------------------------------------------------------------------------------
27+
28+
from __future__ import annotations
29+
30+
import re
31+
from typing import TypedDict
32+
33+
import_ok = True
34+
try:
35+
import weechat
36+
except ImportError:
37+
print("This script must be run under WeeChat.")
38+
print("Get WeeChat now at: https://weechat.org/")
39+
import_ok = False
40+
41+
SCRIPT_NAME = "censor"
42+
SCRIPT_AUTHOR = "D. Bohdan <dbohdan@dbohdan.com>"
43+
SCRIPT_VERSION = "0.3.0"
44+
SCRIPT_LICENSE = "MIT"
45+
SCRIPT_DESC = "Censor messages by nick and text using color while still logging them."
46+
47+
# ---------------------------------------------------------------------------
48+
# CONFIGURATION
49+
# ---------------------------------------------------------------------------
50+
51+
OPTIONS_PREFIX = f"plugins.var.python.{SCRIPT_NAME}."
52+
53+
54+
class RegexCheck:
55+
def __init__(self, pattern: str = "") -> None:
56+
self._regex: re.Pattern | None = None
57+
self.pattern = pattern # Compile the regex.
58+
59+
def __str__(self) -> str:
60+
return self.pattern
61+
62+
def check(self, string: str) -> bool:
63+
return self._regex is not None and bool(self._regex.search(string))
64+
65+
@property
66+
def pattern(self) -> str:
67+
return "" if self._regex is None else self._regex.pattern
68+
69+
@pattern.setter
70+
def pattern(self, pattern: str) -> None:
71+
self._regex = None
72+
73+
if not pattern.strip():
74+
return
75+
76+
try:
77+
self._regex = re.compile(pattern, re.IGNORECASE)
78+
except re.error as e:
79+
weechat.prnt("", f"{SCRIPT_NAME}: invalid regex: {e}")
80+
81+
82+
class ScriptOptions(TypedDict):
83+
censor_color: str
84+
nick_re: RegexCheck
85+
text_re: RegexCheck
86+
87+
88+
SCRIPT_OPTION_DESCS = {
89+
"censor_color": "color (fg,bg) applied to censored messages",
90+
"nick_re": "case-insensitive regex matched against the nick",
91+
"text_re": "case-insensitive regex matched against the message text",
92+
}
93+
94+
95+
script_options: ScriptOptions = {
96+
"censor_color": "darkgray,darkgray",
97+
"nick_re": RegexCheck(),
98+
"text_re": RegexCheck(),
99+
}
100+
101+
102+
# ---------------------------------------------------------------------------
103+
# IMPLEMENTATION
104+
# ---------------------------------------------------------------------------
105+
106+
107+
def tags_nick(tags):
108+
"""Extract nick from tags."""
109+
if not tags:
110+
return ""
111+
112+
for tag in tags.split(","):
113+
if tag.startswith("nick_"):
114+
return tag.removeprefix("nick_")
115+
116+
return ""
117+
118+
119+
def set_script_option(key, value):
120+
if key == "censor_color":
121+
script_options[key] = value
122+
elif key in ("nick_re", "text_re"):
123+
script_options[key] = RegexCheck(value)
124+
125+
126+
def config_cb(_data, option, value):
127+
"""Callback called when a script option is changed."""
128+
key = option.removeprefix(OPTIONS_PREFIX)
129+
130+
if key in script_options:
131+
set_script_option(key, value)
132+
133+
return weechat.WEECHAT_RC_OK
134+
135+
136+
def censor_line_cb(_data, line):
137+
"""hook_line callback: style censored message text."""
138+
tags = line.get("tags", "")
139+
nick = tags_nick(tags)
140+
141+
if not nick:
142+
return {}
143+
144+
plain = weechat.string_remove_color(line["message"], "")
145+
146+
if script_options["nick_re"].check(nick) or script_options["text_re"].check(plain):
147+
color = weechat.color(script_options["censor_color"])
148+
reset = weechat.color("reset")
149+
150+
return {
151+
"message": f"{color}{plain}{reset}",
152+
"highlight": "0",
153+
"notify_level": "-1",
154+
}
155+
156+
return {}
157+
158+
159+
def main():
160+
if not weechat.register(
161+
SCRIPT_NAME,
162+
SCRIPT_AUTHOR,
163+
SCRIPT_VERSION,
164+
SCRIPT_LICENSE,
165+
SCRIPT_DESC,
166+
"",
167+
"",
168+
):
169+
return
170+
171+
for key, default_value in script_options.items():
172+
if weechat.config_is_set_plugin(key):
173+
set_script_option(key, weechat.config_get_plugin(key))
174+
else:
175+
weechat.config_set_plugin(key, str(default_value))
176+
177+
weechat.config_set_desc_plugin(
178+
key,
179+
f'{SCRIPT_OPTION_DESCS[key]} (default: "{default_value}")',
180+
)
181+
182+
weechat.hook_config(OPTIONS_PREFIX + "*", "config_cb", "")
183+
weechat.hook_line("", "", "", "censor_line_cb", "")
184+
185+
186+
if __name__ == "__main__" and import_ok:
187+
main()

0 commit comments

Comments
 (0)