We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
.title command should say an error if no title is found
Example: http://zimage.com/~ant/temp/notitle.html

Sopel should say there was no web page title found.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `sopel/modules/url.py`
Content:
```
1 # coding=utf-8
2 """URL title module"""
3 # Copyright 2010-2011, Michael Yanovich, yanovich.net, Kenneth Sham
4 # Copyright 2012-2013 Elsie Powell
5 # Copyright 2013      Lior Ramati (firerogue517@gmail.com)
6 # Copyright © 2014 Elad Alfassa <elad@fedoraproject.org>
7 # Licensed under the Eiffel Forum License 2.
8 from __future__ import unicode_literals, absolute_import, print_function, division
9 
10 import re
11 from sopel import web, tools, __version__
12 from sopel.module import commands, rule, example
13 from sopel.config.types import ValidatedAttribute, ListAttribute, StaticSection
14 
15 import requests
16 
17 USER_AGENT = 'Sopel/{} (https://sopel.chat)'.format(__version__)
18 default_headers = {'User-Agent': USER_AGENT}
19 url_finder = None
20 # These are used to clean up the title tag before actually parsing it. Not the
21 # world's best way to do this, but it'll do for now.
22 title_tag_data = re.compile('<(/?)title( [^>]+)?>', re.IGNORECASE)
23 quoted_title = re.compile('[\'"]<title>[\'"]', re.IGNORECASE)
24 # This is another regex that presumably does something important.
25 re_dcc = re.compile(r'(?i)dcc\ssend')
26 # This sets the maximum number of bytes that should be read in order to find
27 # the title. We don't want it too high, or a link to a big file/stream will
28 # just keep downloading until there's no more memory. 640k ought to be enough
29 # for anybody.
30 max_bytes = 655360
31 
32 
33 class UrlSection(StaticSection):
34     # TODO some validation rules maybe?
35     exclude = ListAttribute('exclude')
36     exclusion_char = ValidatedAttribute('exclusion_char', default='!')
37 
38 
39 def configure(config):
40     config.define_section('url', UrlSection)
41     config.url.configure_setting(
42         'exclude',
43         'Enter regular expressions for each URL you would like to exclude.'
44     )
45     config.url.configure_setting(
46         'exclusion_char',
47         'Enter a character which can be prefixed to suppress URL titling'
48     )
49 
50 
51 def setup(bot):
52     global url_finder
53 
54     bot.config.define_section('url', UrlSection)
55 
56     if bot.config.url.exclude:
57         regexes = [re.compile(s) for s in bot.config.url.exclude]
58     else:
59         regexes = []
60 
61     # We're keeping these in their own list, rather than putting then in the
62     # callbacks list because 1, it's easier to deal with modules that are still
63     # using this list, and not the newer callbacks list and 2, having a lambda
64     # just to pass is kinda ugly.
65     if not bot.memory.contains('url_exclude'):
66         bot.memory['url_exclude'] = regexes
67     else:
68         exclude = bot.memory['url_exclude']
69         if regexes:
70             exclude.extend(regexes)
71         bot.memory['url_exclude'] = exclude
72 
73     # Ensure that url_callbacks and last_seen_url are in memory
74     if not bot.memory.contains('url_callbacks'):
75         bot.memory['url_callbacks'] = tools.SopelMemory()
76     if not bot.memory.contains('last_seen_url'):
77         bot.memory['last_seen_url'] = tools.SopelMemory()
78 
79     url_finder = re.compile(r'(?u)(%s?(?:http|https|ftp)(?:://\S+))' %
80                             (bot.config.url.exclusion_char), re.IGNORECASE)
81 
82 
83 @commands('title')
84 @example('.title http://google.com', '[ Google ] - google.com')
85 def title_command(bot, trigger):
86     """
87     Show the title or URL information for the given URL, or the last URL seen
88     in this channel.
89     """
90     if not trigger.group(2):
91         if trigger.sender not in bot.memory['last_seen_url']:
92             return
93         matched = check_callbacks(bot, trigger,
94                                   bot.memory['last_seen_url'][trigger.sender],
95                                   True)
96         if matched:
97             return
98         else:
99             urls = [bot.memory['last_seen_url'][trigger.sender]]
100     else:
101         urls = re.findall(url_finder, trigger)
102 
103     results = process_urls(bot, trigger, urls)
104     for title, domain in results[:4]:
105         bot.reply('[ %s ] - %s' % (title, domain))
106 
107 
108 @rule('(?u).*(https?://\S+).*')
109 def title_auto(bot, trigger):
110     """
111     Automatically show titles for URLs. For shortened URLs/redirects, find
112     where the URL redirects to and show the title for that (or call a function
113     from another module to give more information).
114     """
115     if re.match(bot.config.core.prefix + 'title', trigger):
116         return
117 
118     # Avoid fetching known malicious links
119     if 'safety_cache' in bot.memory and trigger in bot.memory['safety_cache']:
120         if bot.memory['safety_cache'][trigger]['positives'] > 1:
121             return
122 
123     urls = re.findall(url_finder, trigger)
124     if len(urls) == 0:
125         return
126 
127     results = process_urls(bot, trigger, urls)
128     bot.memory['last_seen_url'][trigger.sender] = urls[-1]
129 
130     for title, domain in results[:4]:
131         message = '[ %s ] - %s' % (title, domain)
132         # Guard against responding to other instances of this bot.
133         if message != trigger:
134             bot.say(message)
135 
136 
137 def process_urls(bot, trigger, urls):
138     """
139     For each URL in the list, ensure that it isn't handled by another module.
140     If not, find where it redirects to, if anywhere. If that redirected URL
141     should be handled by another module, dispatch the callback for it.
142     Return a list of (title, hostname) tuples for each URL which is not handled by
143     another module.
144     """
145 
146     results = []
147     for url in urls:
148         if not url.startswith(bot.config.url.exclusion_char):
149             # Magic stuff to account for international domain names
150             try:
151                 url = web.iri_to_uri(url)
152             except Exception:  # TODO: Be specific
153                 pass
154             # First, check that the URL we got doesn't match
155             matched = check_callbacks(bot, trigger, url, False)
156             if matched:
157                 continue
158             # Finally, actually show the URL
159             title = find_title(url, verify=bot.config.core.verify_ssl)
160             if title:
161                 results.append((title, get_hostname(url)))
162     return results
163 
164 
165 def check_callbacks(bot, trigger, url, run=True):
166     """
167     Check the given URL against the callbacks list. If it matches, and ``run``
168     is given as ``True``, run the callback function, otherwise pass. Returns
169     ``True`` if the url matched anything in the callbacks list.
170     """
171     # Check if it matches the exclusion list first
172     matched = any(regex.search(url) for regex in bot.memory['url_exclude'])
173     # Then, check if there's anything in the callback list
174     for regex, function in tools.iteritems(bot.memory['url_callbacks']):
175         match = regex.search(url)
176         if match:
177             # Always run ones from @url; they don't run on their own.
178             if run or hasattr(function, 'url_regex'):
179                 function(bot, trigger, match)
180             matched = True
181     return matched
182 
183 
184 def find_title(url, verify=True):
185     """Return the title for the given URL."""
186     try:
187         response = requests.get(url, stream=True, verify=verify,
188                                 headers=default_headers)
189         content = b''
190         for byte in response.iter_content(chunk_size=512):
191             content += byte
192             if b'</title>' in content or len(content) > max_bytes:
193                 break
194         content = content.decode('utf-8', errors='ignore')
195         # Need to close the connection because we have not read all
196         # the data
197         response.close()
198     except requests.exceptions.ConnectionError:
199         return None
200 
201     # Some cleanup that I don't really grok, but was in the original, so
202     # we'll keep it (with the compiled regexes made global) for now.
203     content = title_tag_data.sub(r'<\1title>', content)
204     content = quoted_title.sub('', content)
205 
206     start = content.rfind('<title>')
207     end = content.rfind('</title>')
208     if start == -1 or end == -1:
209         return
210     title = web.decode(content[start + 7:end])
211     title = title.strip()[:200]
212 
213     title = ' '.join(title.split())  # cleanly remove multiple spaces
214 
215     # More cryptic regex substitutions. This one looks to be myano's invention.
216     title = re_dcc.sub('', title)
217 
218     return title or None
219 
220 
221 def get_hostname(url):
222     idx = 7
223     if url.startswith('https://'):
224         idx = 8
225     elif url.startswith('ftp://'):
226         idx = 6
227     hostname = url[idx:]
228     slash = hostname.find('/')
229     if slash != -1:
230         hostname = hostname[:slash]
231     return hostname
232 
233 
234 if __name__ == "__main__":
235     from sopel.test_tools import run_example_tests
236     run_example_tests(__file__)
237
```
--- 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/sopel/modules/url.py b/sopel/modules/url.py
--- a/sopel/modules/url.py
+++ b/sopel/modules/url.py
@@ -104,6 +104,13 @@
     for title, domain in results[:4]:
         bot.reply('[ %s ] - %s' % (title, domain))
 
+    # Nice to have different failure messages for one-and-only requested URL
+    # failed vs. one-of-many failed.
+    if len(urls) == 1 and not results:
+        bot.reply('Sorry, fetching that title failed. Make sure the site is working.')
+    elif len(urls) > len(results):
+        bot.reply('I couldn\'t get all of the titles, but I fetched what I could!')
+
 
 @rule('(?u).*(https?://\S+).*')
 def title_auto(bot, trigger):

{"golden_diff": "diff --git a/sopel/modules/url.py b/sopel/modules/url.py\n--- a/sopel/modules/url.py\n+++ b/sopel/modules/url.py\n@@ -104,6 +104,13 @@\n     for title, domain in results[:4]:\n         bot.reply('[ %s ] - %s' % (title, domain))\n \n+    # Nice to have different failure messages for one-and-only requested URL\n+    # failed vs. one-of-many failed.\n+    if len(urls) == 1 and not results:\n+        bot.reply('Sorry, fetching that title failed. Make sure the site is working.')\n+    elif len(urls) > len(results):\n+        bot.reply('I couldn\\'t get all of the titles, but I fetched what I could!')\n+\n \n @rule('(?u).*(https?://\\S+).*')\n def title_auto(bot, trigger):\n", "issue": ".title command should say an error if no title is found\nExample: http://zimage.com/~ant/temp/notitle.html\r\n\r\nSopel should say there was no web page title found.\n", "before_files": [{"content": "# coding=utf-8\n\"\"\"URL title module\"\"\"\n# Copyright 2010-2011, Michael Yanovich, yanovich.net, Kenneth Sham\n# Copyright 2012-2013 Elsie Powell\n# Copyright 2013      Lior Ramati (firerogue517@gmail.com)\n# Copyright \u00a9 2014 Elad Alfassa <elad@fedoraproject.org>\n# Licensed under the Eiffel Forum License 2.\nfrom __future__ import unicode_literals, absolute_import, print_function, division\n\nimport re\nfrom sopel import web, tools, __version__\nfrom sopel.module import commands, rule, example\nfrom sopel.config.types import ValidatedAttribute, ListAttribute, StaticSection\n\nimport requests\n\nUSER_AGENT = 'Sopel/{} (https://sopel.chat)'.format(__version__)\ndefault_headers = {'User-Agent': USER_AGENT}\nurl_finder = None\n# These are used to clean up the title tag before actually parsing it. Not the\n# world's best way to do this, but it'll do for now.\ntitle_tag_data = re.compile('<(/?)title( [^>]+)?>', re.IGNORECASE)\nquoted_title = re.compile('[\\'\"]<title>[\\'\"]', re.IGNORECASE)\n# This is another regex that presumably does something important.\nre_dcc = re.compile(r'(?i)dcc\\ssend')\n# This sets the maximum number of bytes that should be read in order to find\n# the title. We don't want it too high, or a link to a big file/stream will\n# just keep downloading until there's no more memory. 640k ought to be enough\n# for anybody.\nmax_bytes = 655360\n\n\nclass UrlSection(StaticSection):\n    # TODO some validation rules maybe?\n    exclude = ListAttribute('exclude')\n    exclusion_char = ValidatedAttribute('exclusion_char', default='!')\n\n\ndef configure(config):\n    config.define_section('url', UrlSection)\n    config.url.configure_setting(\n        'exclude',\n        'Enter regular expressions for each URL you would like to exclude.'\n    )\n    config.url.configure_setting(\n        'exclusion_char',\n        'Enter a character which can be prefixed to suppress URL titling'\n    )\n\n\ndef setup(bot):\n    global url_finder\n\n    bot.config.define_section('url', UrlSection)\n\n    if bot.config.url.exclude:\n        regexes = [re.compile(s) for s in bot.config.url.exclude]\n    else:\n        regexes = []\n\n    # We're keeping these in their own list, rather than putting then in the\n    # callbacks list because 1, it's easier to deal with modules that are still\n    # using this list, and not the newer callbacks list and 2, having a lambda\n    # just to pass is kinda ugly.\n    if not bot.memory.contains('url_exclude'):\n        bot.memory['url_exclude'] = regexes\n    else:\n        exclude = bot.memory['url_exclude']\n        if regexes:\n            exclude.extend(regexes)\n        bot.memory['url_exclude'] = exclude\n\n    # Ensure that url_callbacks and last_seen_url are in memory\n    if not bot.memory.contains('url_callbacks'):\n        bot.memory['url_callbacks'] = tools.SopelMemory()\n    if not bot.memory.contains('last_seen_url'):\n        bot.memory['last_seen_url'] = tools.SopelMemory()\n\n    url_finder = re.compile(r'(?u)(%s?(?:http|https|ftp)(?:://\\S+))' %\n                            (bot.config.url.exclusion_char), re.IGNORECASE)\n\n\n@commands('title')\n@example('.title http://google.com', '[ Google ] - google.com')\ndef title_command(bot, trigger):\n    \"\"\"\n    Show the title or URL information for the given URL, or the last URL seen\n    in this channel.\n    \"\"\"\n    if not trigger.group(2):\n        if trigger.sender not in bot.memory['last_seen_url']:\n            return\n        matched = check_callbacks(bot, trigger,\n                                  bot.memory['last_seen_url'][trigger.sender],\n                                  True)\n        if matched:\n            return\n        else:\n            urls = [bot.memory['last_seen_url'][trigger.sender]]\n    else:\n        urls = re.findall(url_finder, trigger)\n\n    results = process_urls(bot, trigger, urls)\n    for title, domain in results[:4]:\n        bot.reply('[ %s ] - %s' % (title, domain))\n\n\n@rule('(?u).*(https?://\\S+).*')\ndef title_auto(bot, trigger):\n    \"\"\"\n    Automatically show titles for URLs. For shortened URLs/redirects, find\n    where the URL redirects to and show the title for that (or call a function\n    from another module to give more information).\n    \"\"\"\n    if re.match(bot.config.core.prefix + 'title', trigger):\n        return\n\n    # Avoid fetching known malicious links\n    if 'safety_cache' in bot.memory and trigger in bot.memory['safety_cache']:\n        if bot.memory['safety_cache'][trigger]['positives'] > 1:\n            return\n\n    urls = re.findall(url_finder, trigger)\n    if len(urls) == 0:\n        return\n\n    results = process_urls(bot, trigger, urls)\n    bot.memory['last_seen_url'][trigger.sender] = urls[-1]\n\n    for title, domain in results[:4]:\n        message = '[ %s ] - %s' % (title, domain)\n        # Guard against responding to other instances of this bot.\n        if message != trigger:\n            bot.say(message)\n\n\ndef process_urls(bot, trigger, urls):\n    \"\"\"\n    For each URL in the list, ensure that it isn't handled by another module.\n    If not, find where it redirects to, if anywhere. If that redirected URL\n    should be handled by another module, dispatch the callback for it.\n    Return a list of (title, hostname) tuples for each URL which is not handled by\n    another module.\n    \"\"\"\n\n    results = []\n    for url in urls:\n        if not url.startswith(bot.config.url.exclusion_char):\n            # Magic stuff to account for international domain names\n            try:\n                url = web.iri_to_uri(url)\n            except Exception:  # TODO: Be specific\n                pass\n            # First, check that the URL we got doesn't match\n            matched = check_callbacks(bot, trigger, url, False)\n            if matched:\n                continue\n            # Finally, actually show the URL\n            title = find_title(url, verify=bot.config.core.verify_ssl)\n            if title:\n                results.append((title, get_hostname(url)))\n    return results\n\n\ndef check_callbacks(bot, trigger, url, run=True):\n    \"\"\"\n    Check the given URL against the callbacks list. If it matches, and ``run``\n    is given as ``True``, run the callback function, otherwise pass. Returns\n    ``True`` if the url matched anything in the callbacks list.\n    \"\"\"\n    # Check if it matches the exclusion list first\n    matched = any(regex.search(url) for regex in bot.memory['url_exclude'])\n    # Then, check if there's anything in the callback list\n    for regex, function in tools.iteritems(bot.memory['url_callbacks']):\n        match = regex.search(url)\n        if match:\n            # Always run ones from @url; they don't run on their own.\n            if run or hasattr(function, 'url_regex'):\n                function(bot, trigger, match)\n            matched = True\n    return matched\n\n\ndef find_title(url, verify=True):\n    \"\"\"Return the title for the given URL.\"\"\"\n    try:\n        response = requests.get(url, stream=True, verify=verify,\n                                headers=default_headers)\n        content = b''\n        for byte in response.iter_content(chunk_size=512):\n            content += byte\n            if b'</title>' in content or len(content) > max_bytes:\n                break\n        content = content.decode('utf-8', errors='ignore')\n        # Need to close the connection because we have not read all\n        # the data\n        response.close()\n    except requests.exceptions.ConnectionError:\n        return None\n\n    # Some cleanup that I don't really grok, but was in the original, so\n    # we'll keep it (with the compiled regexes made global) for now.\n    content = title_tag_data.sub(r'<\\1title>', content)\n    content = quoted_title.sub('', content)\n\n    start = content.rfind('<title>')\n    end = content.rfind('</title>')\n    if start == -1 or end == -1:\n        return\n    title = web.decode(content[start + 7:end])\n    title = title.strip()[:200]\n\n    title = ' '.join(title.split())  # cleanly remove multiple spaces\n\n    # More cryptic regex substitutions. This one looks to be myano's invention.\n    title = re_dcc.sub('', title)\n\n    return title or None\n\n\ndef get_hostname(url):\n    idx = 7\n    if url.startswith('https://'):\n        idx = 8\n    elif url.startswith('ftp://'):\n        idx = 6\n    hostname = url[idx:]\n    slash = hostname.find('/')\n    if slash != -1:\n        hostname = hostname[:slash]\n    return hostname\n\n\nif __name__ == \"__main__\":\n    from sopel.test_tools import run_example_tests\n    run_example_tests(__file__)\n", "path": "sopel/modules/url.py"}], "after_files": [{"content": "# coding=utf-8\n\"\"\"URL title module\"\"\"\n# Copyright 2010-2011, Michael Yanovich, yanovich.net, Kenneth Sham\n# Copyright 2012-2013 Elsie Powell\n# Copyright 2013      Lior Ramati (firerogue517@gmail.com)\n# Copyright \u00a9 2014 Elad Alfassa <elad@fedoraproject.org>\n# Licensed under the Eiffel Forum License 2.\nfrom __future__ import unicode_literals, absolute_import, print_function, division\n\nimport re\nfrom sopel import web, tools, __version__\nfrom sopel.module import commands, rule, example\nfrom sopel.config.types import ValidatedAttribute, ListAttribute, StaticSection\n\nimport requests\n\nUSER_AGENT = 'Sopel/{} (https://sopel.chat)'.format(__version__)\ndefault_headers = {'User-Agent': USER_AGENT}\nurl_finder = None\n# These are used to clean up the title tag before actually parsing it. Not the\n# world's best way to do this, but it'll do for now.\ntitle_tag_data = re.compile('<(/?)title( [^>]+)?>', re.IGNORECASE)\nquoted_title = re.compile('[\\'\"]<title>[\\'\"]', re.IGNORECASE)\n# This is another regex that presumably does something important.\nre_dcc = re.compile(r'(?i)dcc\\ssend')\n# This sets the maximum number of bytes that should be read in order to find\n# the title. We don't want it too high, or a link to a big file/stream will\n# just keep downloading until there's no more memory. 640k ought to be enough\n# for anybody.\nmax_bytes = 655360\n\n\nclass UrlSection(StaticSection):\n    # TODO some validation rules maybe?\n    exclude = ListAttribute('exclude')\n    exclusion_char = ValidatedAttribute('exclusion_char', default='!')\n\n\ndef configure(config):\n    config.define_section('url', UrlSection)\n    config.url.configure_setting(\n        'exclude',\n        'Enter regular expressions for each URL you would like to exclude.'\n    )\n    config.url.configure_setting(\n        'exclusion_char',\n        'Enter a character which can be prefixed to suppress URL titling'\n    )\n\n\ndef setup(bot):\n    global url_finder\n\n    bot.config.define_section('url', UrlSection)\n\n    if bot.config.url.exclude:\n        regexes = [re.compile(s) for s in bot.config.url.exclude]\n    else:\n        regexes = []\n\n    # We're keeping these in their own list, rather than putting then in the\n    # callbacks list because 1, it's easier to deal with modules that are still\n    # using this list, and not the newer callbacks list and 2, having a lambda\n    # just to pass is kinda ugly.\n    if not bot.memory.contains('url_exclude'):\n        bot.memory['url_exclude'] = regexes\n    else:\n        exclude = bot.memory['url_exclude']\n        if regexes:\n            exclude.extend(regexes)\n        bot.memory['url_exclude'] = exclude\n\n    # Ensure that url_callbacks and last_seen_url are in memory\n    if not bot.memory.contains('url_callbacks'):\n        bot.memory['url_callbacks'] = tools.SopelMemory()\n    if not bot.memory.contains('last_seen_url'):\n        bot.memory['last_seen_url'] = tools.SopelMemory()\n\n    url_finder = re.compile(r'(?u)(%s?(?:http|https|ftp)(?:://\\S+))' %\n                            (bot.config.url.exclusion_char), re.IGNORECASE)\n\n\n@commands('title')\n@example('.title http://google.com', '[ Google ] - google.com')\ndef title_command(bot, trigger):\n    \"\"\"\n    Show the title or URL information for the given URL, or the last URL seen\n    in this channel.\n    \"\"\"\n    if not trigger.group(2):\n        if trigger.sender not in bot.memory['last_seen_url']:\n            return\n        matched = check_callbacks(bot, trigger,\n                                  bot.memory['last_seen_url'][trigger.sender],\n                                  True)\n        if matched:\n            return\n        else:\n            urls = [bot.memory['last_seen_url'][trigger.sender]]\n    else:\n        urls = re.findall(url_finder, trigger)\n\n    results = process_urls(bot, trigger, urls)\n    for title, domain in results[:4]:\n        bot.reply('[ %s ] - %s' % (title, domain))\n\n    # Nice to have different failure messages for one-and-only requested URL\n    # failed vs. one-of-many failed.\n    if len(urls) == 1 and not results:\n        bot.reply('Sorry, fetching that title failed. Make sure the site is working.')\n    elif len(urls) > len(results):\n        bot.reply('I couldn\\'t get all of the titles, but I fetched what I could!')\n\n\n@rule('(?u).*(https?://\\S+).*')\ndef title_auto(bot, trigger):\n    \"\"\"\n    Automatically show titles for URLs. For shortened URLs/redirects, find\n    where the URL redirects to and show the title for that (or call a function\n    from another module to give more information).\n    \"\"\"\n    if re.match(bot.config.core.prefix + 'title', trigger):\n        return\n\n    # Avoid fetching known malicious links\n    if 'safety_cache' in bot.memory and trigger in bot.memory['safety_cache']:\n        if bot.memory['safety_cache'][trigger]['positives'] > 1:\n            return\n\n    urls = re.findall(url_finder, trigger)\n    if len(urls) == 0:\n        return\n\n    results = process_urls(bot, trigger, urls)\n    bot.memory['last_seen_url'][trigger.sender] = urls[-1]\n\n    for title, domain in results[:4]:\n        message = '[ %s ] - %s' % (title, domain)\n        # Guard against responding to other instances of this bot.\n        if message != trigger:\n            bot.say(message)\n\n\ndef process_urls(bot, trigger, urls):\n    \"\"\"\n    For each URL in the list, ensure that it isn't handled by another module.\n    If not, find where it redirects to, if anywhere. If that redirected URL\n    should be handled by another module, dispatch the callback for it.\n    Return a list of (title, hostname) tuples for each URL which is not handled by\n    another module.\n    \"\"\"\n\n    results = []\n    for url in urls:\n        if not url.startswith(bot.config.url.exclusion_char):\n            # Magic stuff to account for international domain names\n            try:\n                url = web.iri_to_uri(url)\n            except Exception:  # TODO: Be specific\n                pass\n            # First, check that the URL we got doesn't match\n            matched = check_callbacks(bot, trigger, url, False)\n            if matched:\n                continue\n            # Finally, actually show the URL\n            title = find_title(url, verify=bot.config.core.verify_ssl)\n            if title:\n                results.append((title, get_hostname(url)))\n    return results\n\n\ndef check_callbacks(bot, trigger, url, run=True):\n    \"\"\"\n    Check the given URL against the callbacks list. If it matches, and ``run``\n    is given as ``True``, run the callback function, otherwise pass. Returns\n    ``True`` if the url matched anything in the callbacks list.\n    \"\"\"\n    # Check if it matches the exclusion list first\n    matched = any(regex.search(url) for regex in bot.memory['url_exclude'])\n    # Then, check if there's anything in the callback list\n    for regex, function in tools.iteritems(bot.memory['url_callbacks']):\n        match = regex.search(url)\n        if match:\n            # Always run ones from @url; they don't run on their own.\n            if run or hasattr(function, 'url_regex'):\n                function(bot, trigger, match)\n            matched = True\n    return matched\n\n\ndef find_title(url, verify=True):\n    \"\"\"Return the title for the given URL.\"\"\"\n    try:\n        response = requests.get(url, stream=True, verify=verify,\n                                headers=default_headers)\n        content = b''\n        for byte in response.iter_content(chunk_size=512):\n            content += byte\n            if b'</title>' in content or len(content) > max_bytes:\n                break\n        content = content.decode('utf-8', errors='ignore')\n        # Need to close the connection because we have not read all\n        # the data\n        response.close()\n    except requests.exceptions.ConnectionError:\n        return None\n\n    # Some cleanup that I don't really grok, but was in the original, so\n    # we'll keep it (with the compiled regexes made global) for now.\n    content = title_tag_data.sub(r'<\\1title>', content)\n    content = quoted_title.sub('', content)\n\n    start = content.rfind('<title>')\n    end = content.rfind('</title>')\n    if start == -1 or end == -1:\n        return\n    title = web.decode(content[start + 7:end])\n    title = title.strip()[:200]\n\n    title = ' '.join(title.split())  # cleanly remove multiple spaces\n\n    # More cryptic regex substitutions. This one looks to be myano's invention.\n    title = re_dcc.sub('', title)\n\n    return title or None\n\n\ndef get_hostname(url):\n    idx = 7\n    if url.startswith('https://'):\n        idx = 8\n    elif url.startswith('ftp://'):\n        idx = 6\n    hostname = url[idx:]\n    slash = hostname.find('/')\n    if slash != -1:\n        hostname = hostname[:slash]\n    return hostname\n\n\nif __name__ == \"__main__\":\n    from sopel.test_tools import run_example_tests\n    run_example_tests(__file__)\n", "path": "sopel/modules/url.py"}]}