Skip to content

Commit e6bea05

Browse files
authored
Merge pull request #1982 from Exirel/bot-has-privilege
plugin: new `require_bot_privilege` decorator
2 parents c02f52c + d31721c commit e6bea05

File tree

9 files changed

+845
-41
lines changed

9 files changed

+845
-41
lines changed

docs/source/plugin/anatomy.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,18 @@ better to limit who can trigger them. There are decorators for that:
8080
trigger the rule
8181
* :func:`sopel.plugin.require_owner`: only the bot's owner can trigger the rule
8282

83+
Sometimes it's not the channel privilege level of the user who triggers a
84+
command that matters, but the **bot's** privilege level. For that, there are
85+
two options:
86+
87+
* :func:`sopel.plugin.require_bot_privilege`: this decorator is similar to
88+
the ``require_privilege`` decorator, but it checks the bot's privilege level
89+
instead of the user's; works only for channel messages, not private messages;
90+
and you probably want to use it with the ``require_chanmsg`` decorator.
91+
* :meth:`bot.has_channel_privilege() <sopel.bot.Sopel.has_channel_privilege>`
92+
is a method that can be used to check the bot's privilege level in a channel,
93+
which can be used in any callable.
94+
8395
.. __: https://ircv3.net/specs/extensions/account-tag-3.2
8496

8597
Rate limiting

sopel/bot.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,29 @@ def hostmask(self):
200200

201201
return self.users.get(self.nick).hostmask
202202

203+
def has_channel_privilege(self, channel, privilege):
204+
"""Tell if the bot has a ``privilege`` level or above in a ``channel``.
205+
206+
:param str channel: a channel the bot is in
207+
:param int privilege: privilege level to check
208+
:raise ValueError: when the channel is unknown
209+
210+
This method checks the bot's privilege level in a channel, i.e. if it
211+
has this level or higher privileges::
212+
213+
>>> bot.channels['#chan'].privileges[bot.nick] = plugin.OP
214+
>>> bot.has_channel_privilege('#chan', plugin.VOICE)
215+
True
216+
217+
The ``channel`` argument can be either a :class:`str` or a
218+
:class:`sopel.tools.Identifier`, as long as Sopel joined said channel.
219+
If the channel is unknown, a :exc:`ValueError` will be raised.
220+
"""
221+
if channel not in self.channels:
222+
raise ValueError('Unknown channel %s' % channel)
223+
224+
return self.channels[channel].has_privilege(self.nick, privilege)
225+
203226
# signal handlers
204227

205228
def set_signal_handlers(self):

sopel/modules/adminchannel.py

Lines changed: 20 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,14 @@ def default_mask(trigger):
2828

2929

3030
@plugin.require_chanmsg
31-
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV)
31+
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV, reply=True)
32+
@plugin.require_bot_privilege(plugin.OP, ERROR_MESSAGE_NOT_OP, reply=True)
3233
@plugin.command('op')
3334
def op(bot, trigger):
3435
"""
3536
Command to op users in a room. If no nick is given,
3637
Sopel will op the nick who sent the command
3738
"""
38-
if bot.channels[trigger.sender].privileges[bot.nick] < plugin.OP:
39-
bot.reply(ERROR_MESSAGE_NOT_OP)
40-
return
4139
nick = trigger.group(2)
4240
channel = trigger.sender
4341
if not nick:
@@ -46,16 +44,14 @@ def op(bot, trigger):
4644

4745

4846
@plugin.require_chanmsg
49-
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV)
47+
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV, reply=True)
48+
@plugin.require_bot_privilege(plugin.OP, ERROR_MESSAGE_NOT_OP, reply=True)
5049
@plugin.command('deop')
5150
def deop(bot, trigger):
5251
"""
5352
Command to deop users in a room. If no nick is given,
5453
Sopel will deop the nick who sent the command
5554
"""
56-
if bot.channels[trigger.sender].privileges[bot.nick] < plugin.OP:
57-
bot.reply(ERROR_MESSAGE_NOT_OP)
58-
return
5955
nick = trigger.group(2)
6056
channel = trigger.sender
6157
if not nick:
@@ -64,16 +60,14 @@ def deop(bot, trigger):
6460

6561

6662
@plugin.require_chanmsg
67-
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV)
63+
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV, reply=True)
64+
@plugin.require_bot_privilege(plugin.HALFOP, ERROR_MESSAGE_NOT_OP, reply=True)
6865
@plugin.command('voice')
6966
def voice(bot, trigger):
7067
"""
7168
Command to voice users in a room. If no nick is given,
7269
Sopel will voice the nick who sent the command
7370
"""
74-
if bot.channels[trigger.sender].privileges[bot.nick] < plugin.HALFOP:
75-
bot.reply(ERROR_MESSAGE_NOT_OP)
76-
return
7771
nick = trigger.group(2)
7872
channel = trigger.sender
7973
if not nick:
@@ -82,16 +76,14 @@ def voice(bot, trigger):
8276

8377

8478
@plugin.require_chanmsg
85-
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV)
79+
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV, reply=True)
80+
@plugin.require_bot_privilege(plugin.HALFOP, ERROR_MESSAGE_NOT_OP, reply=True)
8681
@plugin.command('devoice')
8782
def devoice(bot, trigger):
8883
"""
8984
Command to devoice users in a room. If no nick is given,
9085
Sopel will devoice the nick who sent the command
9186
"""
92-
if bot.channels[trigger.sender].privileges[bot.nick] < plugin.HALFOP:
93-
bot.reply(ERROR_MESSAGE_NOT_OP)
94-
return
9587
nick = trigger.group(2)
9688
channel = trigger.sender
9789
if not nick:
@@ -100,14 +92,12 @@ def devoice(bot, trigger):
10092

10193

10294
@plugin.require_chanmsg
103-
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV)
95+
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV, reply=True)
96+
@plugin.require_bot_privilege(plugin.HALFOP, ERROR_MESSAGE_NOT_OP, reply=True)
10497
@plugin.command('kick')
10598
@plugin.priority('high')
10699
def kick(bot, trigger):
107100
"""Kick a user from the channel."""
108-
if bot.channels[trigger.sender].privileges[bot.nick] < plugin.HALFOP:
109-
bot.reply(ERROR_MESSAGE_NOT_OP)
110-
return
111101
text = trigger.group().split()
112102
argc = len(text)
113103
if argc < 2:
@@ -153,17 +143,15 @@ def configureHostMask(mask):
153143

154144

155145
@plugin.require_chanmsg
156-
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV)
146+
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV, reply=True)
147+
@plugin.require_bot_privilege(plugin.HALFOP, ERROR_MESSAGE_NOT_OP, reply=True)
157148
@plugin.command('ban')
158149
@plugin.priority('high')
159150
def ban(bot, trigger):
160151
"""Ban a user from the channel
161152
162153
The bot must be a channel operator for this command to work.
163154
"""
164-
if bot.channels[trigger.sender].privileges[bot.nick] < plugin.HALFOP:
165-
bot.reply(ERROR_MESSAGE_NOT_OP)
166-
return
167155
text = trigger.group().split()
168156
argc = len(text)
169157
if argc < 2:
@@ -183,16 +171,14 @@ def ban(bot, trigger):
183171

184172

185173
@plugin.require_chanmsg
186-
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV)
174+
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV, reply=True)
175+
@plugin.require_bot_privilege(plugin.HALFOP, ERROR_MESSAGE_NOT_OP, reply=True)
187176
@plugin.command('unban')
188177
def unban(bot, trigger):
189178
"""Unban a user from the channel
190179
191180
The bot must be a channel operator for this command to work.
192181
"""
193-
if bot.channels[trigger.sender].privileges[bot.nick] < plugin.HALFOP:
194-
bot.reply(ERROR_MESSAGE_NOT_OP)
195-
return
196182
text = trigger.group().split()
197183
argc = len(text)
198184
if argc < 2:
@@ -212,16 +198,14 @@ def unban(bot, trigger):
212198

213199

214200
@plugin.require_chanmsg
215-
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV)
201+
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV, reply=True)
202+
@plugin.require_bot_privilege(plugin.OP, ERROR_MESSAGE_NOT_OP, reply=True)
216203
@plugin.command('quiet')
217204
def quiet(bot, trigger):
218205
"""Quiet a user
219206
220207
The bot must be a channel operator for this command to work.
221208
"""
222-
if bot.channels[trigger.sender].privileges[bot.nick] < plugin.OP:
223-
bot.reply(ERROR_MESSAGE_NOT_OP)
224-
return
225209
text = trigger.group().split()
226210
argc = len(text)
227211
if argc < 2:
@@ -241,16 +225,14 @@ def quiet(bot, trigger):
241225

242226

243227
@plugin.require_chanmsg
244-
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV)
228+
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV, reply=True)
229+
@plugin.require_bot_privilege(plugin.OP, ERROR_MESSAGE_NOT_OP, reply=True)
245230
@plugin.command('unquiet')
246231
def unquiet(bot, trigger):
247232
"""Unquiet a user
248233
249234
The bot must be a channel operator for this command to work.
250235
"""
251-
if bot.channels[trigger.sender].privileges[bot.nick] < plugin.OP:
252-
bot.reply(ERROR_MESSAGE_NOT_OP)
253-
return
254236
text = trigger.group().split()
255237
argc = len(text)
256238
if argc < 2:
@@ -270,7 +252,8 @@ def unquiet(bot, trigger):
270252

271253

272254
@plugin.require_chanmsg
273-
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV)
255+
@plugin.require_privilege(plugin.OP, ERROR_MESSAGE_NO_PRIV, reply=True)
256+
@plugin.require_bot_privilege(plugin.OP, ERROR_MESSAGE_NOT_OP, reply=True)
274257
@plugin.command('kickban', 'kb')
275258
@plugin.example('.kickban [#chan] user1 user!*@* get out of here')
276259
@plugin.priority('high')
@@ -279,9 +262,6 @@ def kickban(bot, trigger):
279262
280263
The bot must be a channel operator for this command to work.
281264
"""
282-
if bot.channels[trigger.sender].privileges[bot.nick] < plugin.HALFOP:
283-
bot.reply(ERROR_MESSAGE_NOT_OP)
284-
return
285265
text = trigger.group().split()
286266
argc = len(text)
287267
if argc < 4:

sopel/plugin.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
'rate',
3636
'require_account',
3737
'require_admin',
38+
'require_bot_privilege',
3839
'require_chanmsg',
3940
'require_owner',
4041
'require_privilege',
@@ -68,6 +69,13 @@
6869
"""Privilege level for the +h channel permission
6970
7071
.. versionadded:: 4.1
72+
73+
.. important::
74+
75+
Not all IRC networks support this privilege mode. If you are writing a
76+
plugin for public distribution, ensure your code behaves sensibly if only
77+
``+v`` (voice) and ``+o`` (op) modes exist.
78+
7179
"""
7280

7381
OP = 4
@@ -80,12 +88,26 @@
8088
"""Privilege level for the +a channel permission
8189
8290
.. versionadded:: 4.1
91+
92+
.. important::
93+
94+
Not all IRC networks support this privilege mode. If you are writing a
95+
plugin for public distribution, ensure your code behaves sensibly if only
96+
``+v`` (voice) and ``+o`` (op) modes exist.
97+
8398
"""
8499

85100
OWNER = 16
86101
"""Privilege level for the +q channel permission
87102
88103
.. versionadded:: 4.1
104+
105+
.. important::
106+
107+
Not all IRC networks support this privilege mode. If you are writing a
108+
plugin for public distribution, ensure your code behaves sensibly if only
109+
``+v`` (voice) and ``+o`` (op) modes exist.
110+
89111
"""
90112

91113
OPER = 32
@@ -95,6 +117,13 @@
95117
store any user's OPER status.
96118
97119
.. versionadded:: 7.0.0
120+
121+
.. important::
122+
123+
Not all IRC networks support this privilege mode. If you are writing a
124+
plugin for public distribution, ensure your code behaves sensibly if only
125+
``+v`` (voice) and ``+o`` (op) modes exist.
126+
98127
"""
99128

100129

@@ -927,6 +956,45 @@ def guarded(bot, trigger, *args, **kwargs):
927956
return actual_decorator
928957

929958

959+
def require_bot_privilege(level, message=None, reply=False):
960+
"""Decorate a function to require a minimum channel privilege for the bot.
961+
962+
:param int level: minimum channel privilege the bot needs for this function
963+
:param str message: optional message said if the bot's channel privilege
964+
level is insufficient
965+
:param bool reply: use :meth:`~.bot.Sopel.reply` instead of
966+
:meth:`~.bot.Sopel.say` when ``True``; defaults to
967+
``False``
968+
969+
``level`` can be one of the privilege level constants defined in this
970+
module. If the bot does not have the privilege, the bot will say
971+
``message`` if given. By default, it uses :meth:`bot.say()
972+
<.bot.Sopel.say>`, but when ``reply`` is true, then it uses
973+
:meth:`bot.reply() <.bot.Sopel.reply>` instead.
974+
975+
Privilege requirements are ignored in private messages.
976+
977+
.. versionadded:: 7.1
978+
"""
979+
def actual_decorator(function):
980+
@functools.wraps(function)
981+
def guarded(bot, trigger, *args, **kwargs):
982+
# If this is a privmsg, ignore privilege requirements
983+
if trigger.is_privmsg:
984+
return function(bot, trigger, *args, **kwargs)
985+
986+
if not bot.has_channel_privilege(trigger.sender, level):
987+
if message and not callable(message):
988+
if reply:
989+
bot.reply(message)
990+
else:
991+
bot.say(message)
992+
else:
993+
return function(bot, trigger, *args, **kwargs)
994+
return guarded
995+
return actual_decorator
996+
997+
930998
def url(*url_rules):
931999
"""Decorate a function to handle URLs.
9321000

0 commit comments

Comments
 (0)