Skip to content

Commit d04e409

Browse files
committed
Merge branch 'gonzobot' into gonzobot+horoscope-fix
2 parents 8fb3491 + 8857dc7 commit d04e409

132 files changed

Lines changed: 2019 additions & 1024 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ python:
77

88
install:
99
- "sudo apt-get update -q"
10-
- "sudo apt-get install -y python3-lxml"
10+
- "sudo apt-get install -y python3-lxml libenchant-dev"
1111
- "pip install -r ./travis/requirements.txt"
1212

1313
script:
1414
- "python ./travis/test_json.py"
15-
- "git diff --diff-filter=d --name-only ${TRAVIS_COMMIT_RANGE} | xargs pylint --rcfile=travis/pylintrc"
15+
- "git diff --diff-filter=d --name-only ${TRAVIS_COMMIT_RANGE} | grep -i '\\.py$' | xargs -r pylint --rcfile=travis/pylintrc"
1616
- "py.test . -v --cov . --cov-report term-missing"
1717

1818
after_success:

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ CloudBot is a simple, fast, expandable open-source Python IRC Bot!
55
## Getting CloudBot
66

77
There are currently four different branches of this repository, each with a different level of stability:
8-
- **gonzobot** *(stable)*: This branch contains everything in the **master** branch plus additional plugins added for Snoonet IRC.
8+
- **gonzobot** *(stable)*: This branch contains everything in the **master** branch plus additional plugins added for Snoonet IRC. This branch is the currently maintained branch which will also contain many fixes for various bugs from the master branch.
99
- **gonzobot-dev** *(unstable)*: This branch is based off of the **gonzobot** branch and includes new plugins that are not fully tested.
10-
- **master** *(stable)*: This branch contains stable, tested code. This is the branch you should be using if you just want to run your own CloudBot! This is also the branch that EDI on Snoonet uses.
11-
- **python3.4** *(unstable)*: This branch is where where test and develop new features. If you would like to help develop CloudBot, you can use this branch.
10+
- **master** *(stable (old))*: This branch contains stable, tested code. This branch is based directly on the upstream master branch and is not currently maintained.
11+
- **python3.4** *(unstable (old))*: This is the outdated testing branch from the upstream repo.
1212

1313
New releases will be pushed from **python3.4** to **master** whenever we have a stable version to release. These changes will be merged into **gonzobot** then deployed. This should happen on a fairly regular basis, so you'll never be too far behind the latest improvements.
1414

cloudbot/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
__version__ = "1.0.9"
1414

15-
__all__ = ["util", "bot", "connection", "config", "permissions", "plugin", "event", "hook", "log_dir"]
15+
__all__ = ["clients", "util", "bot", "client", "config", "event", "hook", "permissions", "plugin", "reloader", "logging_dir"]
1616

1717

1818
def _setup():

cloudbot/__main__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import signal
44
import sys
55
import time
6+
67
# store the original working directory, for use when restarting
7-
from functools import partial
88

99
original_wd = os.path.realpath(".")
1010

@@ -47,9 +47,7 @@ def exit_gracefully(signum, frame):
4747
# we are currently in the process of restarting
4848
stopped_while_restarting = True
4949
else:
50-
_bot.loop.call_soon_threadsafe(
51-
partial(async_util.wrap_future, _bot.stop("Killed (Received SIGINT {})".format(signum)), loop=_bot.loop)
52-
)
50+
async_util.run_coroutine_threadsafe(_bot.stop("Killed (Received SIGINT {})".format(signum)), _bot.loop)
5351

5452
logger.warning("Bot received Signal Interrupt ({})".format(signum))
5553

cloudbot/bot.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from cloudbot.hook import Action
2121
from cloudbot.plugin import PluginManager
2222
from cloudbot.reloader import PluginReloader, ConfigReloader
23-
from cloudbot.util import database, formatting
23+
from cloudbot.util import database, formatting, async_util
2424

2525
try:
2626
from cloudbot.web.main import WebInterface
@@ -65,7 +65,7 @@ def __init__(self, loop=asyncio.get_event_loop()):
6565
self.start_time = time.time()
6666
self.running = True
6767
# future which will be called when the bot stopsIf you
68-
self.stopped_future = asyncio.Future(loop=self.loop)
68+
self.stopped_future = async_util.create_future(self.loop)
6969

7070
# stores each bot server connection
7171
self.connections = {}
@@ -221,7 +221,7 @@ def _init_routine(self):
221221
self.observer.start()
222222

223223
# Connect to servers
224-
yield from asyncio.gather(*[conn.connect() for conn in self.connections.values()], loop=self.loop)
224+
yield from asyncio.gather(*[conn.try_connect() for conn in self.connections.values()], loop=self.loop)
225225

226226
# Activate web interface.
227227
if self.config.get("web", {}).get("enabled", False) and web_installed:

cloudbot/client.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
2-
import logging
32
import collections
3+
import logging
4+
import random
45

56
from cloudbot.permissions import PermissionManager
67

@@ -59,7 +60,20 @@ def describe_server(self):
5960
raise NotImplementedError
6061

6162
@asyncio.coroutine
62-
def connect(self):
63+
def try_connect(self):
64+
timeout = 30
65+
while True:
66+
try:
67+
yield from self.connect(timeout)
68+
except Exception:
69+
logger.exception("[%s] Error occurred while connecting.")
70+
else:
71+
break
72+
73+
yield from asyncio.sleep(random.randrange(timeout))
74+
75+
@asyncio.coroutine
76+
def connect(self, timeout=None):
6377
"""
6478
Connects to the server, or reconnects if already connected.
6579
"""
@@ -85,6 +99,14 @@ def message(self, target, *text):
8599
"""
86100
raise NotImplementedError
87101

102+
def admin_log(self, text, console=True):
103+
"""
104+
Log a message to the configured admin channel
105+
:type text: str
106+
:type console: bool
107+
"""
108+
raise NotImplementedError
109+
88110
def action(self, target, text):
89111
"""
90112
Sends an action (or /me) to the given target channel

cloudbot/clients/irc.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import asyncio
22
import logging
3+
import random
34
import re
45
import ssl
56
from _ssl import PROTOCOL_SSLv23
7+
from functools import partial
68
from ssl import SSLContext
79

810
from cloudbot.client import Client
@@ -106,7 +108,20 @@ def describe_server(self):
106108
return "{}:{}".format(self.server, self.port)
107109

108110
@asyncio.coroutine
109-
def connect(self):
111+
def try_connect(self):
112+
timeout = self.config["connection"].get("timeout", 30)
113+
while True:
114+
try:
115+
yield from self.connect(timeout)
116+
except (asyncio.TimeoutError, OSError):
117+
logger.exception("[%s] Error occurred while connecting", self.name)
118+
else:
119+
break
120+
121+
yield from asyncio.sleep(random.randrange(timeout))
122+
123+
@asyncio.coroutine
124+
def connect(self, timeout=None):
110125
"""
111126
Connects to the IRC server, or reconnects if already connected.
112127
"""
@@ -125,8 +140,15 @@ def connect(self):
125140
optional_params = {}
126141
if self.local_bind:
127142
optional_params["local_addr"] = self.local_bind
128-
self._transport, self._protocol = yield from self.loop.create_connection(
129-
lambda: _IrcProtocol(self), host=self.server, port=self.port, ssl=self.ssl_context, **optional_params)
143+
144+
coro = self.loop.create_connection(
145+
partial(_IrcProtocol, self), host=self.server, port=self.port, ssl=self.ssl_context, **optional_params
146+
)
147+
148+
if timeout is not None:
149+
coro = asyncio.wait_for(coro, timeout)
150+
151+
self._transport, self._protocol = yield from coro
130152

131153
tasks = [
132154
self.bot.plugin_manager.launch(hook, Event(bot=self.bot, conn=self, hook=hook))
@@ -158,6 +180,14 @@ def message(self, target, *messages):
158180
text = "".join(text.splitlines())
159181
self.cmd("PRIVMSG", target, text)
160182

183+
def admin_log(self, text, console=True):
184+
log_chan = self.config.get("log_channel")
185+
if log_chan:
186+
self.message(log_chan, text)
187+
188+
if console:
189+
logger.info("[%s|admin] %s", self.name, text)
190+
161191
def action(self, target, text):
162192
text = "".join(text.splitlines())
163193
self.ctcp(target, "ACTION", text)
@@ -263,7 +293,7 @@ def __init__(self, conn):
263293
self._transport = None
264294

265295
# Future that waits until we are connected
266-
self._connected_future = asyncio.Future(loop=self.loop)
296+
self._connected_future = async_util.create_future(self.loop)
267297

268298
def connection_made(self, transport):
269299
self._transport = transport
@@ -275,7 +305,7 @@ def connection_made(self, transport):
275305
def connection_lost(self, exc):
276306
self._connected = False
277307
# create a new connected_future for when we are connected.
278-
self._connected_future = asyncio.Future(loop=self.loop)
308+
self._connected_future = async_util.create_future(self.loop)
279309
if exc is None:
280310
# we've been closed intentionally, so don't reconnect
281311
return
@@ -285,7 +315,7 @@ def connection_lost(self, exc):
285315
def eof_received(self):
286316
self._connected = False
287317
# create a new connected_future for when we are connected.
288-
self._connected_future = asyncio.Future(loop=self.loop)
318+
self._connected_future = async_util.create_future(self.loop)
289319
logger.info("[{}] EOF received.".format(self.conn.name))
290320
async_util.wrap_future(self.conn.connect(), loop=self.loop)
291321
return True

cloudbot/config.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
import os
44
import sys
55
import time
6+
from collections import OrderedDict
67

78
logger = logging.getLogger("cloudbot")
89

910

10-
class Config(dict):
11+
class Config(OrderedDict):
1112
"""
1213
:type filename: str
1314
:type path: str
@@ -41,8 +42,10 @@ def load_config(self):
4142
sys.exit()
4243

4344
with open(self.path) as f:
44-
self.update(json.load(f))
45-
logger.debug("Config loaded from file.")
45+
data = json.load(f, object_pairs_hook=OrderedDict)
46+
47+
self.update(data)
48+
logger.debug("Config loaded from file.")
4649

4750
# reload permissions
4851
if self.bot.connections:
@@ -51,5 +54,7 @@ def load_config(self):
5154

5255
def save_config(self):
5356
"""saves the contents of the config dict to the config file"""
54-
json.dump(self, open(self.path, 'w'), sort_keys=True, indent=4)
57+
with open(self.path, 'w') as f:
58+
json.dump(self, f, indent=4)
59+
5560
logger.info("Config saved to file.")

cloudbot/event.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
import concurrent.futures
33
import enum
44
import logging
5+
import sys
56
import warnings
67
from functools import partial
78

8-
import sys
9-
109
from cloudbot.util.parsers.irc import Message
1110

1211
logger = logging.getLogger("cloudbot")
@@ -154,7 +153,7 @@ def prepare(self):
154153
raise ValueError("event.hook is required to prepare an event")
155154

156155
if "db" in self.hook.required_args:
157-
#logger.debug("Opening database session for {}:threaded=False".format(self.hook.description))
156+
# logger.debug("Opening database session for {}:threaded=False".format(self.hook.description))
158157

159158
# we're running a coroutine hook with a db, so initialise an executor pool
160159
self.db_executor = concurrent.futures.ThreadPoolExecutor(1)
@@ -175,7 +174,7 @@ def prepare_threaded(self):
175174
raise ValueError("event.hook is required to prepare an event")
176175

177176
if "db" in self.hook.required_args:
178-
#logger.debug("Opening database session for {}:threaded=True".format(self.hook.description))
177+
# logger.debug("Opening database session for {}:threaded=True".format(self.hook.description))
179178

180179
self.db = self.bot.db_session()
181180

@@ -193,7 +192,7 @@ def close(self):
193192
raise ValueError("event.hook is required to close an event")
194193

195194
if self.db is not None:
196-
#logger.debug("Closing database session for {}:threaded=False".format(self.hook.description))
195+
# logger.debug("Closing database session for {}:threaded=False".format(self.hook.description))
197196
# be sure the close the database in the database executor, as it is only accessable in that one thread
198197
yield from self.async_call(self.db.close)
199198
self.db = None
@@ -210,7 +209,7 @@ def close_threaded(self):
210209
if self.hook is None:
211210
raise ValueError("event.hook is required to close an event")
212211
if self.db is not None:
213-
#logger.debug("Closing database session for {}:threaded=True".format(self.hook.description))
212+
# logger.debug("Closing database session for {}:threaded=True".format(self.hook.description))
214213
self.db.close()
215214
self.db = None
216215

@@ -241,7 +240,20 @@ def message(self, message, target=None):
241240
if self.chan is None:
242241
raise ValueError("Target must be specified when chan is not assigned")
243242
target = self.chan
244-
self.conn.message( target, message)
243+
self.conn.message(target, message)
244+
245+
def admin_log(self, message, broadcast=False):
246+
"""Log a message in the current connections admin log
247+
:type message: str
248+
:type broadcast: bool
249+
:param message: The message to log
250+
:param broadcast: Should this be broadcast to all connections
251+
"""
252+
conns = [self.conn] if not broadcast else self.bot.connections.values()
253+
254+
for conn in conns:
255+
if conn and conn.connected:
256+
conn.admin_log(message, console=not broadcast)
245257

246258
def reply(self, *messages, target=None):
247259
"""sends a message to the current channel/user with a prefix

cloudbot/plugin.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,15 +533,26 @@ def _sieve(self, sieve, event, hook):
533533
else:
534534
coro = sieve.function(self.bot, event, hook)
535535

536+
result, error = None, None
536537
task = async_util.wrap_future(coro)
537538
sieve.plugin.tasks.append(task)
538539
try:
539540
result = yield from task
540541
except Exception:
541542
logger.exception("Error running sieve {} on {}:".format(sieve.description, hook.description))
542-
result = None
543+
error = sys.exc_info()
543544

544545
sieve.plugin.tasks.remove(task)
546+
547+
post_event = partial(
548+
PostHookEvent, launched_hook=sieve, launched_event=event, bot=event.bot,
549+
conn=event.conn, result=result, error=error
550+
)
551+
for post_hook in self.hook_hooks["post"]:
552+
success, res = yield from self.internal_launch(post_hook, post_event(hook=post_hook))
553+
if success and res is False:
554+
break
555+
545556
return result
546557

547558
@asyncio.coroutine
@@ -586,7 +597,7 @@ def launch(self, hook, event):
586597
self._hook_waiting_queues[key] = queue
587598
assert isinstance(queue, asyncio.Queue)
588599
# create a future to represent this task
589-
future = asyncio.Future()
600+
future = async_util.create_future(self.bot.loop)
590601
queue.put_nowait(future)
591602
# wait until the last task is completed
592603
yield from future

0 commit comments

Comments
 (0)