We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Configuration validator warning could be more useful + accurate
<!-- Before you post an issue or if you are unsure about something join our matrix channel https://riot.im/app/#/room/#opsdroid-general:matrix.org and ask away! We are more than happy to help you. -->
# Description
The Opsdroid warning "Opsdroid has a new configuration format since version 0.17.0, we will change your configuration now. Please read on how to migrate in the documentation." indicates that the daemon itself will re-write your configuration file, but this is not the case.  

It could also include a link to how one might be able to migrate their config-file, saving frantic seconds of Googling :-) 

## Steps to Reproduce
Just use an otherwise-working config-file that doesn't conform to the new standard 

## Expected Functionality
Warning message should reflect reality (shouldn't say "We will change your config now") and hint to the user where they can directly read up on the new format (such as via including a link)


## Experienced Functionality
Explain what happened instead(Please include the debug log).

## Versions
- **Opsdroid version:** 0.19.0
- **Python version:** 3.8.4
- **OS/Docker version:**

## Configuration File
Please include your version of the configuration file below.

```yaml
# Your code goes here.

```

## Additional Details
Any other details you wish to include such as screenshots, console messages, etc.


<!-- Love opsdroid? Please consider supporting our collective:
 +👉  https://opencollective.com/opsdroid/donate -->


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `opsdroid/helper.py`
Content:
```
1 """Helper functions to use within OpsDroid."""
2 
3 import datetime
4 import os
5 import stat
6 import logging
7 import json
8 
9 import nbformat
10 from nbconvert import PythonExporter
11 
12 _LOGGER = logging.getLogger(__name__)
13 
14 
15 # pylint: disable=inconsistent-return-statements
16 def get_opsdroid():
17     """Return the running opsdroid instance.
18 
19     Returns:
20         object: opsdroid instance.
21 
22     """
23     from opsdroid.core import OpsDroid
24 
25     if len(OpsDroid.instances) == 1:
26         return OpsDroid.instances[0]
27 
28 
29 def del_rw(action, name, exc):
30     """Error handler for removing read only files.
31 
32     Args:
33         action: the function that raised the exception
34         name: path name passed to the function (path and file name)
35         exc: exception information return by sys.exc_info()
36 
37     Raises:
38         OsError : If the file to be removed is a directory.
39 
40     """
41     os.chmod(name, stat.S_IWRITE)
42     os.remove(name)
43 
44 
45 # This is meant to provide backwards compatibility for versions
46 # prior to  0.16.0 in the future this will be deleted
47 
48 
49 def convert_dictionary(modules):
50     """Convert dictionary to new format.
51 
52     We iterate over all the modules in the list and change the dictionary
53     to be in the format 'name_of_module: { config_params}'
54 
55     Args:
56         modules (list): List of dictionaries that contain the module configuration
57 
58     Return:
59         List: New modified list following the new format.
60 
61     """
62     config = dict()
63 
64     if isinstance(modules, list):
65         _LOGGER.warning(
66             "Opsdroid has a new configuration format since version 0.17.0, we will change your configuration now. Please read on how to migrate in the documentation."
67         )
68         for module in modules:
69             module_copy = module.copy()
70             del module_copy["name"]
71 
72             if module.get("access-token") or module.get("api-token"):
73                 _LOGGER.warning(
74                     _(
75                         "Configuration param for %s has been deprecated in favor of 'token', please update your config."
76                     ),
77                     module["name"],
78                 )
79                 module_copy["token"] = module.get("access-token") or module.get(
80                     "api-token"
81                 )
82 
83             config[module["name"]] = module_copy
84 
85         return config
86     else:
87         return modules
88 
89 
90 def update_pre_0_17_config_format(config):
91     """Update each configuration param that contains 'name'.
92 
93     We decided to ditch the name param and instead divide each module by it's name.
94     This change was due to validation issues. Now instead of a list of dictionaries
95     without any pointer to what they are, we are using the name of the module and then a
96     dictionary containing the configuration params for said module.
97 
98     Args:
99         config (dict): Dictionary containing config got from configuration.yaml
100 
101     Returns:
102         dict: updated configuration.
103 
104     """
105     updated_config = {}
106     for config_type, modules in config.items():
107         if config_type in ("parsers", "connectors", "skills", "databases"):
108             updated_config[config_type] = convert_dictionary(modules)
109 
110     config.update(updated_config)
111 
112     return config
113 
114 
115 def file_is_ipython_notebook(path):
116     """Check whether a file is an iPython Notebook.
117 
118     Args:
119         path (str): path to the file.
120 
121     Examples:
122         path : source path with .ipynb file '/path/src/my_file.ipynb.
123 
124     """
125     return path.lower().endswith(".ipynb")
126 
127 
128 def convert_ipynb_to_script(notebook_path, output_path):
129     """Convert an iPython Notebook to a python script.
130 
131     Args:
132         notebook_path (str): path to the notebook file.
133         output_path (str): path to the script file destination.
134 
135     Examples:
136         notebook_path : source path with .ipynb file '/path/src/my_file.ipynb.
137         output_path : destination path with .py file '/path/src/my_file.py.
138 
139     """
140     with open(notebook_path, "r") as notebook_path_handle:
141         raw_notebook = notebook_path_handle.read()
142         notebook = nbformat.reads(raw_notebook, as_version=4)
143         script, _ = PythonExporter().from_notebook_node(notebook)
144         with open(output_path, "w") as output_path_handle:
145             output_path_handle.write(script)
146 
147 
148 def extract_gist_id(gist_string):
149     """Extract the gist ID from a url.
150 
151     Will also work if simply passed an ID.
152 
153     Args:
154         gist_string (str): Gist URL.
155 
156     Returns:
157         string: The gist ID.
158 
159     Examples:
160         gist_string : Gist url 'https://gist.github.com/{user}/{id}'.
161 
162     """
163     return gist_string.split("/")[-1]
164 
165 
166 def add_skill_attributes(func):
167     """Add the attributes which makes a function a skill.
168 
169     Args:
170         func (func): Skill function.
171 
172     Returns:
173         func: The skill function with the new attributes.
174 
175     """
176     if not hasattr(func, "skill"):
177         func.skill = True
178     if not hasattr(func, "matchers"):
179         func.matchers = []
180     if not hasattr(func, "constraints"):
181         func.constraints = []
182     return func
183 
184 
185 def get_parser_config(name, modules):
186     """Get parser from modules list.
187 
188     After the change to the configuration we are adding the "enabled" flag to each
189     active module, this allows us to disable to module if there is any problem with
190     it. This helper method helps getting the config from the list of active parsers.
191 
192     Args:
193         name (string): Name of the parser to be fetched.
194         modules (list): List of all active modules.
195 
196     Returns:
197         dict or None: The module config or None if not found.
198 
199     """
200     if modules:
201         for parser in modules:
202             if parser["config"]["name"] == name:
203                 return parser["config"]
204     return None
205 
206 
207 def get_config_option(options, config, found, not_found):
208     """Get config details and return useful information to list active modules.
209 
210     When we list modules we have to do a lot of search and get, this function serves as an
211     helper to get all the needed information to show in a list format. Since we are using
212     different formats and need to get 3 different details from the config we will either
213     return them or use the placeholder from `not_found`.
214 
215     Args:
216         options(list): list of all possible options to search in config.
217         config(dict): This will be a section of the configuration (connectors, parsers, skills, etc).
218         found(str, bool): Expected text if option exists in config.
219         not_found(str): expected text if option doesn't exist in config.
220 
221     """
222     try:
223         for option in options:
224             if config.get(option):
225                 return found, option, config.get(option)
226         return not_found, not_found, not_found
227     except (TypeError, AttributeError):
228         return not_found, not_found, not_found
229 
230 
231 class JSONEncoder(json.JSONEncoder):
232     """A extended JSONEncoder class.
233 
234     This class is customised JSONEncoder class which helps to convert
235     dict to JSON. The datetime objects are converted to dict with fields
236     as keys.
237 
238     """
239 
240     # pylint: disable=method-hidden
241     # See https://github.com/PyCQA/pylint/issues/414 for reference
242 
243     serializers = {}
244 
245     def default(self, o):
246         """Convert the given datetime object to dict.
247 
248         Args:
249             o (object): The datetime object to be marshalled.
250 
251         Returns:
252             dict (object): A dict with datatime object data.
253 
254         Example:
255             A dict which is returned after marshalling::
256 
257                 {
258                     "__class__": "datetime",
259                     "year": 2018,
260                     "month": 10,
261                     "day": 2,
262                     "hour": 0,
263                     "minute": 41,
264                     "second": 17,
265                     "microsecond": 74644
266                 }
267 
268         """
269         marshaller = self.serializers.get(type(o), super(JSONEncoder, self).default)
270         return marshaller(o)
271 
272 
273 class JSONDecoder:
274     """A JSONDecoder class.
275 
276     This class will convert dict containing datetime values
277     to datetime objects.
278 
279     """
280 
281     decoders = {}
282 
283     def __call__(self, dct):
284         """Convert given dict to datetime objects.
285 
286         Args:
287             dct (object): A dict containing datetime values and class type.
288 
289         Returns:
290             object or dct: The datetime object for given dct, or dct if
291                              respective class decoder is not found.
292 
293         Example:
294             A datetime object returned after decoding::
295 
296                 datetime.datetime(2018, 10, 2, 0, 41, 17, 74644)
297 
298         """
299         if dct.get("__class__") in self.decoders:
300             return self.decoders[dct["__class__"]](dct)
301         return dct
302 
303 
304 def register_json_type(type_cls, fields, decode_fn):
305     """Register JSON types.
306 
307     This method will register the serializers and decoders for the
308     JSONEncoder and JSONDecoder classes respectively.
309 
310     Args:
311         type_cls (object): A datetime object.
312         fields (list): List of fields used to store data in dict.
313         decode_fn (object): A lambda function object for decoding.
314 
315     """
316     type_name = type_cls.__name__
317     JSONEncoder.serializers[type_cls] = lambda obj: dict(
318         __class__=type_name, **{field: getattr(obj, field) for field in fields}
319     )
320     JSONDecoder.decoders[type_name] = decode_fn
321 
322 
323 register_json_type(
324     datetime.datetime,
325     ["year", "month", "day", "hour", "minute", "second", "microsecond"],
326     lambda dct: datetime.datetime(
327         dct["year"],
328         dct["month"],
329         dct["day"],
330         dct["hour"],
331         dct["minute"],
332         dct["second"],
333         dct["microsecond"],
334     ),
335 )
336 
337 register_json_type(
338     datetime.date,
339     ["year", "month", "day"],
340     lambda dct: datetime.date(dct["year"], dct["month"], dct["day"]),
341 )
342 
343 register_json_type(
344     datetime.time,
345     ["hour", "minute", "second", "microsecond"],
346     lambda dct: datetime.time(
347         dct["hour"], dct["minute"], dct["second"], dct["microsecond"]
348     ),
349 )
350
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/opsdroid/helper.py b/opsdroid/helper.py
--- a/opsdroid/helper.py
+++ b/opsdroid/helper.py
@@ -63,7 +63,7 @@
 
     if isinstance(modules, list):
         _LOGGER.warning(
-            "Opsdroid has a new configuration format since version 0.17.0, we will change your configuration now. Please read on how to migrate in the documentation."
+            "Opsdroid has a new configuration format since version 0.17.0. Please read on how to migrate in the documentation at https://docs.opsdroid.dev/en/stable/configuration.html#migrate-to-new-configuration-layout."
         )
         for module in modules:
             module_copy = module.copy()

{"golden_diff": "diff --git a/opsdroid/helper.py b/opsdroid/helper.py\n--- a/opsdroid/helper.py\n+++ b/opsdroid/helper.py\n@@ -63,7 +63,7 @@\n \n     if isinstance(modules, list):\n         _LOGGER.warning(\n-            \"Opsdroid has a new configuration format since version 0.17.0, we will change your configuration now. Please read on how to migrate in the documentation.\"\n+            \"Opsdroid has a new configuration format since version 0.17.0. Please read on how to migrate in the documentation at https://docs.opsdroid.dev/en/stable/configuration.html#migrate-to-new-configuration-layout.\"\n         )\n         for module in modules:\n             module_copy = module.copy()\n", "issue": "Configuration validator warning could be more useful + accurate\n<!-- Before you post an issue or if you are unsure about something join our matrix channel https://riot.im/app/#/room/#opsdroid-general:matrix.org and ask away! We are more than happy to help you. -->\r\n# Description\r\nThe Opsdroid warning \"Opsdroid has a new configuration format since version 0.17.0, we will change your configuration now. Please read on how to migrate in the documentation.\" indicates that the daemon itself will re-write your configuration file, but this is not the case.  \r\n\r\nIt could also include a link to how one might be able to migrate their config-file, saving frantic seconds of Googling :-) \r\n\r\n## Steps to Reproduce\r\nJust use an otherwise-working config-file that doesn't conform to the new standard \r\n\r\n## Expected Functionality\r\nWarning message should reflect reality (shouldn't say \"We will change your config now\") and hint to the user where they can directly read up on the new format (such as via including a link)\r\n\r\n\r\n## Experienced Functionality\r\nExplain what happened instead(Please include the debug log).\r\n\r\n## Versions\r\n- **Opsdroid version:** 0.19.0\r\n- **Python version:** 3.8.4\r\n- **OS/Docker version:**\r\n\r\n## Configuration File\r\nPlease include your version of the configuration file below.\r\n\r\n```yaml\r\n# Your code goes here.\r\n\r\n```\r\n\r\n## Additional Details\r\nAny other details you wish to include such as screenshots, console messages, etc.\r\n\r\n\r\n<!-- Love opsdroid? Please consider supporting our collective:\r\n +\ud83d\udc49  https://opencollective.com/opsdroid/donate -->\r\n\n", "before_files": [{"content": "\"\"\"Helper functions to use within OpsDroid.\"\"\"\n\nimport datetime\nimport os\nimport stat\nimport logging\nimport json\n\nimport nbformat\nfrom nbconvert import PythonExporter\n\n_LOGGER = logging.getLogger(__name__)\n\n\n# pylint: disable=inconsistent-return-statements\ndef get_opsdroid():\n    \"\"\"Return the running opsdroid instance.\n\n    Returns:\n        object: opsdroid instance.\n\n    \"\"\"\n    from opsdroid.core import OpsDroid\n\n    if len(OpsDroid.instances) == 1:\n        return OpsDroid.instances[0]\n\n\ndef del_rw(action, name, exc):\n    \"\"\"Error handler for removing read only files.\n\n    Args:\n        action: the function that raised the exception\n        name: path name passed to the function (path and file name)\n        exc: exception information return by sys.exc_info()\n\n    Raises:\n        OsError : If the file to be removed is a directory.\n\n    \"\"\"\n    os.chmod(name, stat.S_IWRITE)\n    os.remove(name)\n\n\n# This is meant to provide backwards compatibility for versions\n# prior to  0.16.0 in the future this will be deleted\n\n\ndef convert_dictionary(modules):\n    \"\"\"Convert dictionary to new format.\n\n    We iterate over all the modules in the list and change the dictionary\n    to be in the format 'name_of_module: { config_params}'\n\n    Args:\n        modules (list): List of dictionaries that contain the module configuration\n\n    Return:\n        List: New modified list following the new format.\n\n    \"\"\"\n    config = dict()\n\n    if isinstance(modules, list):\n        _LOGGER.warning(\n            \"Opsdroid has a new configuration format since version 0.17.0, we will change your configuration now. Please read on how to migrate in the documentation.\"\n        )\n        for module in modules:\n            module_copy = module.copy()\n            del module_copy[\"name\"]\n\n            if module.get(\"access-token\") or module.get(\"api-token\"):\n                _LOGGER.warning(\n                    _(\n                        \"Configuration param for %s has been deprecated in favor of 'token', please update your config.\"\n                    ),\n                    module[\"name\"],\n                )\n                module_copy[\"token\"] = module.get(\"access-token\") or module.get(\n                    \"api-token\"\n                )\n\n            config[module[\"name\"]] = module_copy\n\n        return config\n    else:\n        return modules\n\n\ndef update_pre_0_17_config_format(config):\n    \"\"\"Update each configuration param that contains 'name'.\n\n    We decided to ditch the name param and instead divide each module by it's name.\n    This change was due to validation issues. Now instead of a list of dictionaries\n    without any pointer to what they are, we are using the name of the module and then a\n    dictionary containing the configuration params for said module.\n\n    Args:\n        config (dict): Dictionary containing config got from configuration.yaml\n\n    Returns:\n        dict: updated configuration.\n\n    \"\"\"\n    updated_config = {}\n    for config_type, modules in config.items():\n        if config_type in (\"parsers\", \"connectors\", \"skills\", \"databases\"):\n            updated_config[config_type] = convert_dictionary(modules)\n\n    config.update(updated_config)\n\n    return config\n\n\ndef file_is_ipython_notebook(path):\n    \"\"\"Check whether a file is an iPython Notebook.\n\n    Args:\n        path (str): path to the file.\n\n    Examples:\n        path : source path with .ipynb file '/path/src/my_file.ipynb.\n\n    \"\"\"\n    return path.lower().endswith(\".ipynb\")\n\n\ndef convert_ipynb_to_script(notebook_path, output_path):\n    \"\"\"Convert an iPython Notebook to a python script.\n\n    Args:\n        notebook_path (str): path to the notebook file.\n        output_path (str): path to the script file destination.\n\n    Examples:\n        notebook_path : source path with .ipynb file '/path/src/my_file.ipynb.\n        output_path : destination path with .py file '/path/src/my_file.py.\n\n    \"\"\"\n    with open(notebook_path, \"r\") as notebook_path_handle:\n        raw_notebook = notebook_path_handle.read()\n        notebook = nbformat.reads(raw_notebook, as_version=4)\n        script, _ = PythonExporter().from_notebook_node(notebook)\n        with open(output_path, \"w\") as output_path_handle:\n            output_path_handle.write(script)\n\n\ndef extract_gist_id(gist_string):\n    \"\"\"Extract the gist ID from a url.\n\n    Will also work if simply passed an ID.\n\n    Args:\n        gist_string (str): Gist URL.\n\n    Returns:\n        string: The gist ID.\n\n    Examples:\n        gist_string : Gist url 'https://gist.github.com/{user}/{id}'.\n\n    \"\"\"\n    return gist_string.split(\"/\")[-1]\n\n\ndef add_skill_attributes(func):\n    \"\"\"Add the attributes which makes a function a skill.\n\n    Args:\n        func (func): Skill function.\n\n    Returns:\n        func: The skill function with the new attributes.\n\n    \"\"\"\n    if not hasattr(func, \"skill\"):\n        func.skill = True\n    if not hasattr(func, \"matchers\"):\n        func.matchers = []\n    if not hasattr(func, \"constraints\"):\n        func.constraints = []\n    return func\n\n\ndef get_parser_config(name, modules):\n    \"\"\"Get parser from modules list.\n\n    After the change to the configuration we are adding the \"enabled\" flag to each\n    active module, this allows us to disable to module if there is any problem with\n    it. This helper method helps getting the config from the list of active parsers.\n\n    Args:\n        name (string): Name of the parser to be fetched.\n        modules (list): List of all active modules.\n\n    Returns:\n        dict or None: The module config or None if not found.\n\n    \"\"\"\n    if modules:\n        for parser in modules:\n            if parser[\"config\"][\"name\"] == name:\n                return parser[\"config\"]\n    return None\n\n\ndef get_config_option(options, config, found, not_found):\n    \"\"\"Get config details and return useful information to list active modules.\n\n    When we list modules we have to do a lot of search and get, this function serves as an\n    helper to get all the needed information to show in a list format. Since we are using\n    different formats and need to get 3 different details from the config we will either\n    return them or use the placeholder from `not_found`.\n\n    Args:\n        options(list): list of all possible options to search in config.\n        config(dict): This will be a section of the configuration (connectors, parsers, skills, etc).\n        found(str, bool): Expected text if option exists in config.\n        not_found(str): expected text if option doesn't exist in config.\n\n    \"\"\"\n    try:\n        for option in options:\n            if config.get(option):\n                return found, option, config.get(option)\n        return not_found, not_found, not_found\n    except (TypeError, AttributeError):\n        return not_found, not_found, not_found\n\n\nclass JSONEncoder(json.JSONEncoder):\n    \"\"\"A extended JSONEncoder class.\n\n    This class is customised JSONEncoder class which helps to convert\n    dict to JSON. The datetime objects are converted to dict with fields\n    as keys.\n\n    \"\"\"\n\n    # pylint: disable=method-hidden\n    # See https://github.com/PyCQA/pylint/issues/414 for reference\n\n    serializers = {}\n\n    def default(self, o):\n        \"\"\"Convert the given datetime object to dict.\n\n        Args:\n            o (object): The datetime object to be marshalled.\n\n        Returns:\n            dict (object): A dict with datatime object data.\n\n        Example:\n            A dict which is returned after marshalling::\n\n                {\n                    \"__class__\": \"datetime\",\n                    \"year\": 2018,\n                    \"month\": 10,\n                    \"day\": 2,\n                    \"hour\": 0,\n                    \"minute\": 41,\n                    \"second\": 17,\n                    \"microsecond\": 74644\n                }\n\n        \"\"\"\n        marshaller = self.serializers.get(type(o), super(JSONEncoder, self).default)\n        return marshaller(o)\n\n\nclass JSONDecoder:\n    \"\"\"A JSONDecoder class.\n\n    This class will convert dict containing datetime values\n    to datetime objects.\n\n    \"\"\"\n\n    decoders = {}\n\n    def __call__(self, dct):\n        \"\"\"Convert given dict to datetime objects.\n\n        Args:\n            dct (object): A dict containing datetime values and class type.\n\n        Returns:\n            object or dct: The datetime object for given dct, or dct if\n                             respective class decoder is not found.\n\n        Example:\n            A datetime object returned after decoding::\n\n                datetime.datetime(2018, 10, 2, 0, 41, 17, 74644)\n\n        \"\"\"\n        if dct.get(\"__class__\") in self.decoders:\n            return self.decoders[dct[\"__class__\"]](dct)\n        return dct\n\n\ndef register_json_type(type_cls, fields, decode_fn):\n    \"\"\"Register JSON types.\n\n    This method will register the serializers and decoders for the\n    JSONEncoder and JSONDecoder classes respectively.\n\n    Args:\n        type_cls (object): A datetime object.\n        fields (list): List of fields used to store data in dict.\n        decode_fn (object): A lambda function object for decoding.\n\n    \"\"\"\n    type_name = type_cls.__name__\n    JSONEncoder.serializers[type_cls] = lambda obj: dict(\n        __class__=type_name, **{field: getattr(obj, field) for field in fields}\n    )\n    JSONDecoder.decoders[type_name] = decode_fn\n\n\nregister_json_type(\n    datetime.datetime,\n    [\"year\", \"month\", \"day\", \"hour\", \"minute\", \"second\", \"microsecond\"],\n    lambda dct: datetime.datetime(\n        dct[\"year\"],\n        dct[\"month\"],\n        dct[\"day\"],\n        dct[\"hour\"],\n        dct[\"minute\"],\n        dct[\"second\"],\n        dct[\"microsecond\"],\n    ),\n)\n\nregister_json_type(\n    datetime.date,\n    [\"year\", \"month\", \"day\"],\n    lambda dct: datetime.date(dct[\"year\"], dct[\"month\"], dct[\"day\"]),\n)\n\nregister_json_type(\n    datetime.time,\n    [\"hour\", \"minute\", \"second\", \"microsecond\"],\n    lambda dct: datetime.time(\n        dct[\"hour\"], dct[\"minute\"], dct[\"second\"], dct[\"microsecond\"]\n    ),\n)\n", "path": "opsdroid/helper.py"}], "after_files": [{"content": "\"\"\"Helper functions to use within OpsDroid.\"\"\"\n\nimport datetime\nimport os\nimport stat\nimport logging\nimport json\n\nimport nbformat\nfrom nbconvert import PythonExporter\n\n_LOGGER = logging.getLogger(__name__)\n\n\n# pylint: disable=inconsistent-return-statements\ndef get_opsdroid():\n    \"\"\"Return the running opsdroid instance.\n\n    Returns:\n        object: opsdroid instance.\n\n    \"\"\"\n    from opsdroid.core import OpsDroid\n\n    if len(OpsDroid.instances) == 1:\n        return OpsDroid.instances[0]\n\n\ndef del_rw(action, name, exc):\n    \"\"\"Error handler for removing read only files.\n\n    Args:\n        action: the function that raised the exception\n        name: path name passed to the function (path and file name)\n        exc: exception information return by sys.exc_info()\n\n    Raises:\n        OsError : If the file to be removed is a directory.\n\n    \"\"\"\n    os.chmod(name, stat.S_IWRITE)\n    os.remove(name)\n\n\n# This is meant to provide backwards compatibility for versions\n# prior to  0.16.0 in the future this will be deleted\n\n\ndef convert_dictionary(modules):\n    \"\"\"Convert dictionary to new format.\n\n    We iterate over all the modules in the list and change the dictionary\n    to be in the format 'name_of_module: { config_params}'\n\n    Args:\n        modules (list): List of dictionaries that contain the module configuration\n\n    Return:\n        List: New modified list following the new format.\n\n    \"\"\"\n    config = dict()\n\n    if isinstance(modules, list):\n        _LOGGER.warning(\n            \"Opsdroid has a new configuration format since version 0.17.0. Please read on how to migrate in the documentation at https://docs.opsdroid.dev/en/stable/configuration.html#migrate-to-new-configuration-layout.\"\n        )\n        for module in modules:\n            module_copy = module.copy()\n            del module_copy[\"name\"]\n\n            if module.get(\"access-token\") or module.get(\"api-token\"):\n                _LOGGER.warning(\n                    _(\n                        \"Configuration param for %s has been deprecated in favor of 'token', please update your config.\"\n                    ),\n                    module[\"name\"],\n                )\n                module_copy[\"token\"] = module.get(\"access-token\") or module.get(\n                    \"api-token\"\n                )\n\n            config[module[\"name\"]] = module_copy\n\n        return config\n    else:\n        return modules\n\n\ndef update_pre_0_17_config_format(config):\n    \"\"\"Update each configuration param that contains 'name'.\n\n    We decided to ditch the name param and instead divide each module by it's name.\n    This change was due to validation issues. Now instead of a list of dictionaries\n    without any pointer to what they are, we are using the name of the module and then a\n    dictionary containing the configuration params for said module.\n\n    Args:\n        config (dict): Dictionary containing config got from configuration.yaml\n\n    Returns:\n        dict: updated configuration.\n\n    \"\"\"\n    updated_config = {}\n    for config_type, modules in config.items():\n        if config_type in (\"parsers\", \"connectors\", \"skills\", \"databases\"):\n            updated_config[config_type] = convert_dictionary(modules)\n\n    config.update(updated_config)\n\n    return config\n\n\ndef file_is_ipython_notebook(path):\n    \"\"\"Check whether a file is an iPython Notebook.\n\n    Args:\n        path (str): path to the file.\n\n    Examples:\n        path : source path with .ipynb file '/path/src/my_file.ipynb.\n\n    \"\"\"\n    return path.lower().endswith(\".ipynb\")\n\n\ndef convert_ipynb_to_script(notebook_path, output_path):\n    \"\"\"Convert an iPython Notebook to a python script.\n\n    Args:\n        notebook_path (str): path to the notebook file.\n        output_path (str): path to the script file destination.\n\n    Examples:\n        notebook_path : source path with .ipynb file '/path/src/my_file.ipynb.\n        output_path : destination path with .py file '/path/src/my_file.py.\n\n    \"\"\"\n    with open(notebook_path, \"r\") as notebook_path_handle:\n        raw_notebook = notebook_path_handle.read()\n        notebook = nbformat.reads(raw_notebook, as_version=4)\n        script, _ = PythonExporter().from_notebook_node(notebook)\n        with open(output_path, \"w\") as output_path_handle:\n            output_path_handle.write(script)\n\n\ndef extract_gist_id(gist_string):\n    \"\"\"Extract the gist ID from a url.\n\n    Will also work if simply passed an ID.\n\n    Args:\n        gist_string (str): Gist URL.\n\n    Returns:\n        string: The gist ID.\n\n    Examples:\n        gist_string : Gist url 'https://gist.github.com/{user}/{id}'.\n\n    \"\"\"\n    return gist_string.split(\"/\")[-1]\n\n\ndef add_skill_attributes(func):\n    \"\"\"Add the attributes which makes a function a skill.\n\n    Args:\n        func (func): Skill function.\n\n    Returns:\n        func: The skill function with the new attributes.\n\n    \"\"\"\n    if not hasattr(func, \"skill\"):\n        func.skill = True\n    if not hasattr(func, \"matchers\"):\n        func.matchers = []\n    if not hasattr(func, \"constraints\"):\n        func.constraints = []\n    return func\n\n\ndef get_parser_config(name, modules):\n    \"\"\"Get parser from modules list.\n\n    After the change to the configuration we are adding the \"enabled\" flag to each\n    active module, this allows us to disable to module if there is any problem with\n    it. This helper method helps getting the config from the list of active parsers.\n\n    Args:\n        name (string): Name of the parser to be fetched.\n        modules (list): List of all active modules.\n\n    Returns:\n        dict or None: The module config or None if not found.\n\n    \"\"\"\n    if modules:\n        for parser in modules:\n            if parser[\"config\"][\"name\"] == name:\n                return parser[\"config\"]\n    return None\n\n\ndef get_config_option(options, config, found, not_found):\n    \"\"\"Get config details and return useful information to list active modules.\n\n    When we list modules we have to do a lot of search and get, this function serves as an\n    helper to get all the needed information to show in a list format. Since we are using\n    different formats and need to get 3 different details from the config we will either\n    return them or use the placeholder from `not_found`.\n\n    Args:\n        options(list): list of all possible options to search in config.\n        config(dict): This will be a section of the configuration (connectors, parsers, skills, etc).\n        found(str, bool): Expected text if option exists in config.\n        not_found(str): expected text if option doesn't exist in config.\n\n    \"\"\"\n    try:\n        for option in options:\n            if config.get(option):\n                return found, option, config.get(option)\n        return not_found, not_found, not_found\n    except (TypeError, AttributeError):\n        return not_found, not_found, not_found\n\n\nclass JSONEncoder(json.JSONEncoder):\n    \"\"\"A extended JSONEncoder class.\n\n    This class is customised JSONEncoder class which helps to convert\n    dict to JSON. The datetime objects are converted to dict with fields\n    as keys.\n\n    \"\"\"\n\n    # pylint: disable=method-hidden\n    # See https://github.com/PyCQA/pylint/issues/414 for reference\n\n    serializers = {}\n\n    def default(self, o):\n        \"\"\"Convert the given datetime object to dict.\n\n        Args:\n            o (object): The datetime object to be marshalled.\n\n        Returns:\n            dict (object): A dict with datatime object data.\n\n        Example:\n            A dict which is returned after marshalling::\n\n                {\n                    \"__class__\": \"datetime\",\n                    \"year\": 2018,\n                    \"month\": 10,\n                    \"day\": 2,\n                    \"hour\": 0,\n                    \"minute\": 41,\n                    \"second\": 17,\n                    \"microsecond\": 74644\n                }\n\n        \"\"\"\n        marshaller = self.serializers.get(type(o), super(JSONEncoder, self).default)\n        return marshaller(o)\n\n\nclass JSONDecoder:\n    \"\"\"A JSONDecoder class.\n\n    This class will convert dict containing datetime values\n    to datetime objects.\n\n    \"\"\"\n\n    decoders = {}\n\n    def __call__(self, dct):\n        \"\"\"Convert given dict to datetime objects.\n\n        Args:\n            dct (object): A dict containing datetime values and class type.\n\n        Returns:\n            object or dct: The datetime object for given dct, or dct if\n                             respective class decoder is not found.\n\n        Example:\n            A datetime object returned after decoding::\n\n                datetime.datetime(2018, 10, 2, 0, 41, 17, 74644)\n\n        \"\"\"\n        if dct.get(\"__class__\") in self.decoders:\n            return self.decoders[dct[\"__class__\"]](dct)\n        return dct\n\n\ndef register_json_type(type_cls, fields, decode_fn):\n    \"\"\"Register JSON types.\n\n    This method will register the serializers and decoders for the\n    JSONEncoder and JSONDecoder classes respectively.\n\n    Args:\n        type_cls (object): A datetime object.\n        fields (list): List of fields used to store data in dict.\n        decode_fn (object): A lambda function object for decoding.\n\n    \"\"\"\n    type_name = type_cls.__name__\n    JSONEncoder.serializers[type_cls] = lambda obj: dict(\n        __class__=type_name, **{field: getattr(obj, field) for field in fields}\n    )\n    JSONDecoder.decoders[type_name] = decode_fn\n\n\nregister_json_type(\n    datetime.datetime,\n    [\"year\", \"month\", \"day\", \"hour\", \"minute\", \"second\", \"microsecond\"],\n    lambda dct: datetime.datetime(\n        dct[\"year\"],\n        dct[\"month\"],\n        dct[\"day\"],\n        dct[\"hour\"],\n        dct[\"minute\"],\n        dct[\"second\"],\n        dct[\"microsecond\"],\n    ),\n)\n\nregister_json_type(\n    datetime.date,\n    [\"year\", \"month\", \"day\"],\n    lambda dct: datetime.date(dct[\"year\"], dct[\"month\"], dct[\"day\"]),\n)\n\nregister_json_type(\n    datetime.time,\n    [\"hour\", \"minute\", \"second\", \"microsecond\"],\n    lambda dct: datetime.time(\n        dct[\"hour\"], dct[\"minute\"], dct[\"second\"], dct[\"microsecond\"]\n    ),\n)\n", "path": "opsdroid/helper.py"}]}