Skip to content

Commit 42b1a2a

Browse files
committed
Merge branch 'gonzobot' into gonzobot+pager
2 parents d5dcd74 + 9a67c26 commit 42b1a2a

107 files changed

Lines changed: 7015 additions & 5222 deletions

Some content is hidden

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

cloudbot/__main__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
import signal
77

88
# store the original working directory, for use when restarting
9+
from functools import partial
10+
11+
from cloudbot.util import async_util
12+
913
original_wd = os.path.realpath(".")
1014

1115
# set up environment - we need to make sure we are in the install directory
@@ -47,9 +51,10 @@ def exit_gracefully(signum, frame):
4751
stopped_while_restarting = True
4852
else:
4953
_bot.loop.call_soon_threadsafe(
50-
lambda: asyncio.async(_bot.stop("Killed (Received SIGINT {})".format(signum)), loop=_bot.loop))
54+
partial(async_util.wrap_future, _bot.stop("Killed (Received SIGINT {})".format(signum)), loop=_bot.loop)
55+
)
5156

52-
logger.warn("Bot received Signal Interrupt ({})".format(signum))
57+
logger.warning("Bot received Signal Interrupt ({})".format(signum))
5358

5459
# restore the original handler so if they do it again it triggers
5560
signal.signal(signal.SIGINT, original_sigint)

cloudbot/client.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,14 @@ def part(self, channel):
122122
"""
123123
raise NotImplementedError
124124

125+
def is_nick_valid(self, nick):
126+
"""
127+
Determines if a nick is valid for this connection
128+
:param nick: The nick to check
129+
:return: True if it is valid, otherwise false
130+
"""
131+
raise NotImplementedError
132+
125133
@property
126134
def connected(self):
127135
raise NotImplementedError

cloudbot/clients/irc.py

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from ssl import SSLContext
77

88
from cloudbot.client import Client
9-
from cloudbot.event import Event, EventType
9+
from cloudbot.event import Event, EventType, IrcOutEvent
10+
from cloudbot.util import async_util
1011

1112
logger = logging.getLogger("cloudbot")
1213

@@ -15,11 +16,15 @@
1516
irc_netmask_re = re.compile(r"([^!@]*)!([^@]*)@(.*)")
1617
irc_param_re = re.compile(r"(?:^|(?<= ))(:.*|[^ ]+)")
1718

19+
irc_nick_re = re.compile(r'[A-Za-z0-9^{\}\[\]\-`_|\\]+')
20+
1821
irc_bad_chars = ''.join([chr(x) for x in list(range(0, 1)) + list(range(4, 32)) + list(range(127, 160))])
1922
irc_clean_re = re.compile('[{}]'.format(re.escape(irc_bad_chars)))
2023

24+
2125
def irc_clean(dirty):
22-
return irc_clean_re.sub('',dirty)
26+
return irc_clean_re.sub('', dirty)
27+
2328

2429
irc_command_to_event_type = {
2530
"PRIVMSG": EventType.message,
@@ -218,13 +223,15 @@ def _send(self, line):
218223
:type line: str
219224
"""
220225
logger.info("[{}] >> {}".format(self.name, line))
221-
asyncio.async(self._protocol.send(line), loop=self.loop)
222-
226+
async_util.wrap_future(self._protocol.send(line), loop=self.loop)
223227

224228
@property
225229
def connected(self):
226230
return self._connected
227231

232+
def is_nick_valid(self, nick):
233+
return bool(irc_nick_re.fullmatch(nick))
234+
228235

229236
class _IrcProtocol(asyncio.Protocol):
230237
"""
@@ -272,24 +279,54 @@ def connection_lost(self, exc):
272279
# we've been closed intentionally, so don't reconnect
273280
return
274281
logger.error("[{}] Connection lost: {}".format(self.conn.name, exc))
275-
asyncio.async(self.conn.connect(), loop=self.loop)
282+
async_util.wrap_future(self.conn.connect(), loop=self.loop)
276283

277284
def eof_received(self):
278285
self._connected = False
279286
# create a new connected_future for when we are connected.
280287
self._connected_future = asyncio.Future(loop=self.loop)
281288
logger.info("[{}] EOF received.".format(self.conn.name))
282-
asyncio.async(self.conn.connect(), loop=self.loop)
289+
async_util.wrap_future(self.conn.connect(), loop=self.loop)
283290
return True
284291

285292
@asyncio.coroutine
286293
def send(self, line):
287294
# make sure we are connected before sending
288295
if not self._connected:
289296
yield from self._connected_future
290-
line = line[:510] + "\r\n"
291-
data = line.encode("utf-8", "replace")
292-
self._transport.write(data)
297+
298+
old_line = line
299+
filtered = bool(self.bot.plugin_manager.out_sieves)
300+
301+
for out_sieve in self.bot.plugin_manager.out_sieves:
302+
event = IrcOutEvent(
303+
bot=self.bot, hook=out_sieve, conn=self.conn, irc_raw=line
304+
)
305+
306+
ok, new_line = yield from self.bot.plugin_manager.internal_launch(out_sieve, event)
307+
if not ok:
308+
logger.warning("Error occurred in outgoing sieve, falling back to old behavior")
309+
logger.debug("Line was: %s", line)
310+
filtered = False
311+
break
312+
313+
line = new_line
314+
if line is not None and not isinstance(line, bytes):
315+
line = str(line)
316+
317+
if not line:
318+
return
319+
320+
if not filtered:
321+
# No outgoing sieves loaded or one of the sieves errored, fall back to old behavior
322+
line = old_line[:510] + "\r\n"
323+
line = line.encode("utf-8", "replace")
324+
325+
if not isinstance(line, bytes):
326+
# the line must be encoded before we send it, one of the sieves didn't encode it, fall back to the default
327+
line = line.encode("utf-8", "replace")
328+
329+
self._transport.write(line)
293330

294331
def data_received(self, data):
295332
self._input_buffer += data
@@ -339,7 +376,7 @@ def data_received(self, data):
339376
# Reply to pings immediately
340377

341378
if command == "PING":
342-
asyncio.async(self.send("PONG " + command_params[-1]), loop=self.loop)
379+
async_util.wrap_future(self.send("PONG " + command_params[-1]), loop=self.loop)
343380

344381
# Parse the command and params
345382

@@ -406,13 +443,4 @@ def data_received(self, data):
406443
irc_prefix=prefix, irc_command=command, irc_paramlist=command_params, irc_ctcp_text=ctcp_text)
407444

408445
# handle the message, async
409-
asyncio.async(self.bot.process(event), loop=self.loop)
410-
411-
# Channel Commands
412-
# NOTICE #chan :Text
413-
# PRIVMSG #chan :Text
414-
# KICK #chan nick :reason
415-
# JOIN #chan
416-
# PART #chan :reason
417-
# MODE #chan +<modes>
418-
# INVITE nick :#chan
446+
async_util.wrap_future(self.bot.process(event), loop=self.loop)

cloudbot/event.py

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
import concurrent.futures
33
import enum
44
import logging
5+
import warnings
6+
from functools import partial
7+
8+
import sys
9+
10+
from cloudbot.util.parsers.irc import Message
511

612
logger = logging.getLogger("cloudbot")
713

@@ -153,7 +159,7 @@ def prepare(self):
153159
# we're running a coroutine hook with a db, so initialise an executor pool
154160
self.db_executor = concurrent.futures.ThreadPoolExecutor(1)
155161
# be sure to initialize the db in the database executor, so it will be accessible in that thread.
156-
self.db = yield from self.async(self.bot.db_session)
162+
self.db = yield from self.async_call(self.bot.db_session)
157163

158164
def prepare_threaded(self):
159165
"""
@@ -189,7 +195,7 @@ def close(self):
189195
if self.db is not None:
190196
#logger.debug("Closing database session for {}:threaded=False".format(self.hook.description))
191197
# be sure the close the database in the database executor, as it is only accessable in that one thread
192-
yield from self.async(self.db.close)
198+
yield from self.async_call(self.db.close)
193199
self.db = None
194200

195201
def close_threaded(self):
@@ -310,17 +316,42 @@ def has_permission(self, permission, notice=True):
310316
return self.conn.permissions.has_perm_mask(self.mask, permission, notice=notice)
311317

312318
@asyncio.coroutine
313-
def async(self, function, *args, **kwargs):
319+
def async_call(self, func, *args, **kwargs):
314320
if self.db_executor is not None:
315321
executor = self.db_executor
316322
else:
317323
executor = None
318-
if kwargs:
319-
result = yield from self.loop.run_in_executor(executor, function, *args)
320-
else:
321-
result = yield from self.loop.run_in_executor(executor, lambda: function(*args, **kwargs))
324+
325+
part = partial(func, *args, **kwargs)
326+
result = yield from self.loop.run_in_executor(executor, part)
322327
return result
323328

329+
def is_nick_valid(self, nick):
330+
"""
331+
Returns whether a nick is valid for a given connection
332+
:param nick: The nick to check
333+
:return: Whether or not it is valid
334+
"""
335+
return self.conn.is_nick_valid(nick)
336+
337+
if sys.version_info < (3, 7, 0):
338+
# noinspection PyCompatibility
339+
@asyncio.coroutine
340+
def async_(self, function, *args, **kwargs):
341+
warnings.warn(
342+
"event.async() is deprecated, use event.async_call() instead.",
343+
DeprecationWarning, stacklevel=2
344+
)
345+
result = yield from self.async_call(function, *args, **kwargs)
346+
return result
347+
348+
349+
# Silence deprecation warnings about use of the 'async' name as a function
350+
try:
351+
setattr(Event, 'async', getattr(Event, 'async_'))
352+
except AttributeError:
353+
pass
354+
324355

325356
class CommandEvent(Event):
326357
"""
@@ -390,3 +421,43 @@ def __init__(self, *args, cap, cap_param=None, **kwargs):
390421
super().__init__(*args, **kwargs)
391422
self.cap = cap
392423
self.cap_param = cap_param
424+
425+
426+
class IrcOutEvent(Event):
427+
def __init__(self, *args, **kwargs):
428+
super().__init__(*args, **kwargs)
429+
self.parsed_line = None
430+
431+
@asyncio.coroutine
432+
def prepare(self):
433+
yield from super().prepare()
434+
435+
if "parsed_line" in self.hook.required_args:
436+
try:
437+
self.parsed_line = Message.parse(self.line)
438+
except Exception:
439+
logger.exception("Unable to parse line requested by hook %s", self.hook)
440+
self.parsed_line = None
441+
442+
def prepare_threaded(self):
443+
super().prepare_threaded()
444+
445+
if "parsed_line" in self.hook.required_args:
446+
try:
447+
self.parsed_line = Message.parse(self.line)
448+
except Exception:
449+
logger.exception("Unable to parse line requested by hook %s", self.hook)
450+
self.parsed_line = None
451+
452+
@property
453+
def line(self):
454+
return str(self.irc_raw)
455+
456+
457+
class PostHookEvent(Event):
458+
def __init__(self, *args, launched_hook=None, launched_event=None, result=None, error=None, **kwargs):
459+
super().__init__(*args, **kwargs)
460+
self.launched_hook = launched_hook
461+
self.launched_event = launched_event
462+
self.result = result
463+
self.error = error

0 commit comments

Comments
 (0)