Repository: sopel-irc/sopel

Issue:
We need a way to blacklist commands per channel
It would be useful to have a way for irc channel ops to dynamically blacklist certain commands in their channels.



Discussion:
I've been taking a look at this, and it seems like the way to use this that makes the most sense is to add some code to the dispatch command to open a dictionary put together by a module with a config function to build said dictionary and two commands to modify the dictionary (one to add modules to the blacklist and one to remove them) I'll try to have some code by Monday.

I'd lean more toward using the config file for this. Poking through the code, it looks like there's an undocumented ability for this already built in, to an extent. It's module-by-module, rather than by function. It's not entirely dynamic, though like anything most of the config file it can be modified temporarily.

From the looks of it, a config attribute called 'limit' is a dict of channel names to modules. If we change func.**module** to func.**name** (I think that's what it's called), that should do it. All that would be needed then is an ability to re-write the config file, which is a feature I'd like to figure out at some point.

wouldn't config.write do that?

Question is, how do we safely write config file without destroying user comments or module configuration. Maybe we should move away from using python for the configuration file and chose something which is easier for a machine to parse, for example the simple syntax of

```
[section]
key = value
otherkey = othervalue
[othersection]
yetanotherkey = value
```

@embolalia theres also a jenni.variables component thats worth taking a look at. as far as i can tell its a dictionary symbol table with all the callable functions. maybe we could simply modify that to be channel by channel and rewrite that?
however, in my current code I do make use of the config file, so being able to rewrite that would be useful. Im also looking for a way to move it to use the settingsDB instead. I dont have sql installed, so ill need help with testing when i get to that stage

for the record, i abandoned this project in favor of porting the core, so somebody else needs to pick this up unless we want to wait for twisted-willie or even make it part of twisted-willie's core

to the question of user comments and whatnot in config files, it is very simple to hold all of the config contents in memory, change what needs to be changed, then write it back to the system. not familiar with the current parser, but it shouldn't be a hard change.

Is it necessary to write to the main config file? What about a separate db file, like the reminders and seen databases?

We could do a separate file, as long as it's properly cached and written back (like the config file is). My instinct is not to, but I'm having a bit of a hard time at the moment thinking of why.

The main issue, since the config file has been dynamically rewritable for ages now, is really more about user friendliness. How do we refer to callables on the user side of things, without exposing unnecessary implementation details? What I suggested a year ago, using the `__name__` of the callable, is not a very good idea (since the source code may not be visible to the end user, and may or may not have any relation to the function that it performs, though I certainly hope both are always the case). 


Resolving patch:
diff --git a/sopel/bot.py b/sopel/bot.py
--- a/sopel/bot.py
+++ b/sopel/bot.py
@@ -7,6 +7,7 @@
 
 from __future__ import unicode_literals, absolute_import, print_function, division
 
+from ast import literal_eval
 import collections
 import os
 import re
@@ -466,6 +467,28 @@ def call(self, func, sopel, trigger):
                     )
                     return
 
+        # if channel has its own config section, check for excluded modules/modules methods
+        if trigger.sender in self.config:
+            channel_config = self.config[trigger.sender]
+
+            # disable listed modules completely on provided channel
+            if 'disable_modules' in channel_config:
+                disabled_modules = channel_config.disable_modules.split(',')
+
+                # if "*" is used, we are disabling all modules on provided channel
+                if '*' in disabled_modules:
+                    return
+                if func.__module__ in disabled_modules:
+                    return
+
+            # disable chosen methods from modules
+            if 'disable_commands' in channel_config:
+                disabled_commands = literal_eval(channel_config.disable_commands)
+
+                if func.__module__ in disabled_commands:
+                    if func.__name__ in disabled_commands[func.__module__]:
+                        return
+
         try:
             exit_code = func(sopel, trigger)
         except Exception:
diff --git a/sopel/config/__init__.py b/sopel/config/__init__.py
--- a/sopel/config/__init__.py
+++ b/sopel/config/__init__.py
@@ -138,6 +138,9 @@ def __init__(self, name, items, parent):
         def __getattr__(self, name):
             return None
 
+        def __contains__(self, name):
+            return name in vars(self)
+
         def __setattr__(self, name, value):
             object.__setattr__(self, name, value)
             if type(value) is list:
@@ -164,6 +167,12 @@ def __getattr__(self, name):
             raise AttributeError("%r object has no attribute %r"
                                  % (type(self).__name__, name))
 
+    def __getitem__(self, name):
+        return self.__getattr__(name)
+
+    def __contains__(self, name):
+        return name in self.parser.sections()
+
     def option(self, question, default=False):
         """Ask "y/n" and return the corresponding boolean answer.
 


Test patch: