Skip to content

Commit 8676570

Browse files
authored
Merge pull request CloudBotIRC#146 from linuxdaemon/gonzobot+cap-support
IRCv3 CAP support + on_connect hooks
2 parents 90b590f + 73ec3cb commit 8676570

8 files changed

Lines changed: 348 additions & 13 deletions

File tree

cloudbot/clients/irc.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from _ssl import PROTOCOL_SSLv23
21
import asyncio
2+
import logging
33
import re
44
import ssl
5-
import logging
5+
from _ssl import PROTOCOL_SSLv23
66
from ssl import SSLContext
77

88
from cloudbot.client import Client
@@ -122,11 +122,12 @@ def connect(self):
122122
self._transport, self._protocol = yield from self.loop.create_connection(
123123
lambda: _IrcProtocol(self), host=self.server, port=self.port, ssl=self.ssl_context, **optional_params)
124124

125-
# send the password, nick, and user
126-
self.set_pass(self.config["connection"].get("password"))
127-
self.set_nick(self.nick)
128-
self.cmd("USER", self.config.get('user', 'cloudbot'), "3", "*",
129-
self.config.get('realname', 'CloudBot - https://git.io/CloudBot'))
125+
tasks = [
126+
self.bot.plugin_manager.launch(hook, Event(bot=self.bot, conn=self, hook=hook))
127+
for hook in self.bot.plugin_manager.connect_hooks
128+
]
129+
# TODO stop connecting if a connect hook fails?
130+
yield from asyncio.gather(*tasks)
130131

131132
def quit(self, reason=None):
132133
if self._quit:

cloudbot/event.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import asyncio
2+
import concurrent.futures
23
import enum
34
import logging
4-
import concurrent.futures
55

66
logger = logging.getLogger("cloudbot")
77

@@ -383,3 +383,10 @@ def __init__(self, *, bot=None, hook, match, conn=None, base_event=None, event_t
383383
content_raw=content_raw, target=target, channel=channel, nick=nick, user=user, host=host, mask=mask,
384384
irc_raw=irc_raw, irc_prefix=irc_prefix, irc_command=irc_command, irc_paramlist=irc_paramlist)
385385
self.match = match
386+
387+
388+
class CapEvent(Event):
389+
def __init__(self, *args, cap, cap_param=None, **kwargs):
390+
super().__init__(*args, **kwargs)
391+
self.cap = cap
392+
self.cap_param = cap_param

cloudbot/hook.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import collections
12
import inspect
23
import re
3-
import collections
44

55
from cloudbot.event import EventType
66

@@ -177,6 +177,16 @@ def add_hook(self, trigger_param, kwargs):
177177
self.types.update(trigger_param)
178178

179179

180+
class _CapHook(_Hook):
181+
def __init__(self, func, _type):
182+
super().__init__(func, "on_cap_{}".format(_type))
183+
self.caps = set()
184+
185+
def add_hook(self, caps, kwargs):
186+
self._add_hook(kwargs)
187+
self.caps.update(caps)
188+
189+
180190
def _add_hook(func, hook):
181191
if not hasattr(func, "_cloudbot_hook"):
182192
func._cloudbot_hook = {}
@@ -357,3 +367,55 @@ def _on_stop_hook(func):
357367
return lambda func: _on_stop_hook(func)
358368

359369
on_unload = on_stop
370+
371+
372+
def on_cap_available(*caps, **kwargs):
373+
"""External on_cap_available decorator. Must be used as a function that returns a decorator
374+
375+
This hook will fire for each capability in a `CAP LS` response from the server
376+
"""
377+
378+
def _on_cap_available_hook(func):
379+
hook = _get_hook(func, "on_cap_available")
380+
if hook is None:
381+
hook = _CapHook(func, "available")
382+
_add_hook(func, hook)
383+
hook.add_hook(caps, kwargs)
384+
return func
385+
386+
return _on_cap_available_hook
387+
388+
389+
def on_cap_ack(*caps, **kwargs):
390+
"""External on_cap_ack decorator. Must be used as a function that returns a decorator
391+
392+
This hook will fire for each capability that is acknowledged from the server with `CAP ACK`
393+
"""
394+
395+
def _on_cap_ack_hook(func):
396+
hook = _get_hook(func, "on_cap_ack")
397+
if hook is None:
398+
hook = _CapHook(func, "ack")
399+
_add_hook(func, hook)
400+
hook.add_hook(caps, kwargs)
401+
return func
402+
403+
return _on_cap_ack_hook
404+
405+
406+
def on_connect(param=None, **kwargs):
407+
def _on_connect_hook(func):
408+
hook = _get_hook(func, "on_connect")
409+
if hook is None:
410+
hook = _Hook(func, "on_connect")
411+
_add_hook(func, hook)
412+
hook._add_hook(kwargs)
413+
return func
414+
415+
if callable(param):
416+
return _on_connect_hook(param)
417+
else:
418+
return lambda func: _on_connect_hook(func)
419+
420+
421+
connect = on_connect

cloudbot/plugin.py

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import logging
66
import os
77
import re
8+
from collections import defaultdict
9+
from operator import attrgetter
810

911
import sqlalchemy
1012

@@ -18,7 +20,7 @@ def find_hooks(parent, module):
1820
"""
1921
:type parent: Plugin
2022
:type module: object
21-
:rtype: (list[CommandHook], list[RegexHook], list[RawHook], list[SieveHook], List[EventHook], List[PeriodicHook], list[OnStartHook], List[OnStopHook])
23+
:rtype: (list[CommandHook], list[RegexHook], list[RawHook], list[SieveHook], List[EventHook], List[PeriodicHook], list[OnStartHook], List[OnStopHook], list[OnCapAckHook], list[OnCapAvailableHook], list[OnConnectHook])
2224
"""
2325
# set the loaded flag
2426
module._cloudbot_loaded = True
@@ -30,8 +32,12 @@ def find_hooks(parent, module):
3032
periodic = []
3133
on_start = []
3234
on_stop = []
35+
on_cap_ack = []
36+
on_cap_available = []
37+
on_connect = []
3338
type_lists = {"command": command, "regex": regex, "irc_raw": raw, "sieve": sieve, "event": event,
34-
"periodic": periodic, "on_start": on_start, "on_stop": on_stop}
39+
"periodic": periodic, "on_start": on_start, "on_stop": on_stop, "on_cap_ack": on_cap_ack,
40+
"on_cap_available": on_cap_available, "on_connect": on_connect}
3541
for name, func in module.__dict__.items():
3642
if hasattr(func, "_cloudbot_hook"):
3743
# if it has cloudbot hook
@@ -43,7 +49,7 @@ def find_hooks(parent, module):
4349
# delete the hook to free memory
4450
del func._cloudbot_hook
4551

46-
return command, regex, raw, sieve, event, periodic, on_start, on_stop
52+
return command, regex, raw, sieve, event, periodic, on_start, on_stop, on_cap_ack, on_cap_available, on_connect
4753

4854

4955
def find_tables(code):
@@ -98,6 +104,8 @@ def __init__(self, bot):
98104
self.event_type_hooks = {}
99105
self.regex_hooks = []
100106
self.sieves = []
107+
self.cap_hooks = {"on_available": defaultdict(list), "on_ack": defaultdict(list)}
108+
self.connect_hooks = []
101109
self._hook_waiting_queues = {}
102110

103111
@asyncio.coroutine
@@ -179,6 +187,16 @@ def load_plugin(self, path):
179187

180188
self.plugins[plugin.file_name] = plugin
181189

190+
for on_cap_available_hook in plugin.on_cap_available:
191+
for cap in on_cap_available_hook.caps:
192+
self.cap_hooks["on_available"][cap.casefold()].append(on_cap_available_hook)
193+
self._log_hook(on_cap_available_hook)
194+
195+
for on_cap_ack_hook in plugin.on_cap_ack:
196+
for cap in on_cap_ack_hook.caps:
197+
self.cap_hooks["on_ack"][cap.casefold()].append(on_cap_ack_hook)
198+
self._log_hook(on_cap_ack_hook)
199+
182200
for periodic_hook in plugin.periodic:
183201
task = asyncio.async(self._start_periodic(periodic_hook))
184202
plugin.tasks.append(task)
@@ -227,8 +245,14 @@ def load_plugin(self, path):
227245
self.sieves.append(sieve_hook)
228246
self._log_hook(sieve_hook)
229247

248+
# register connect hooks
249+
for connect_hook in plugin.connect_hooks:
250+
self.connect_hooks.append(connect_hook)
251+
self._log_hook(connect_hook)
252+
230253
# sort sieve hooks by priority
231254
self.sieves.sort(key=lambda x: x.priority)
255+
self.connect_hooks.sort(key=attrgetter("priority"))
232256

233257
# we don't need this anymore
234258
del plugin.run_on_start
@@ -259,6 +283,22 @@ def unload_plugin(self, path):
259283
for task in plugin.tasks:
260284
task.cancel()
261285

286+
for on_cap_available_hook in plugin.on_cap_available:
287+
available_hooks = self.cap_hooks["on_available"]
288+
for cap in on_cap_available_hook.caps:
289+
cap_cf = cap.casefold()
290+
available_hooks[cap_cf].remove(on_cap_available_hook)
291+
if not available_hooks[cap_cf]:
292+
del available_hooks[cap_cf]
293+
294+
for on_cap_ack in plugin.on_cap_ack:
295+
ack_hooks = self.cap_hooks["on_ack"]
296+
for cap in on_cap_ack.caps:
297+
cap_cf = cap.casefold()
298+
ack_hooks[cap_cf].remove(on_cap_ack)
299+
if not ack_hooks[cap_cf]:
300+
del ack_hooks[cap_cf]
301+
262302
# unregister commands
263303
for command_hook in plugin.commands:
264304
for alias in command_hook.aliases:
@@ -294,6 +334,10 @@ def unload_plugin(self, path):
294334
for sieve_hook in plugin.sieves:
295335
self.sieves.remove(sieve_hook)
296336

337+
# unregister connect hooks
338+
for connect_hook in plugin.connect_hooks:
339+
self.connect_hooks.remove(connect_hook)
340+
297341
# Run on_stop hooks
298342
for on_stop_hook in plugin.run_on_stop:
299343
event = Event(bot=self.bot, hook=on_stop_hook)
@@ -521,7 +565,12 @@ def __init__(self, filepath, filename, title, code):
521565
self.file_path = filepath
522566
self.file_name = filename
523567
self.title = title
524-
self.commands, self.regexes, self.raw_hooks, self.sieves, self.events, self.periodic, self.run_on_start, self.run_on_stop = find_hooks(self, code)
568+
# TODO clean up hook lists
569+
hooks = find_hooks(self, code)
570+
self.commands, self.regexes, self.raw_hooks, *hooks = hooks
571+
self.sieves, self.events, self.periodic, *hooks = hooks
572+
self.run_on_start, self.run_on_stop, self.on_cap_ack, *hooks = hooks
573+
self.on_cap_available, self.connect_hooks, *hooks = hooks
525574
# we need to find tables for each plugin so that they can be unloaded from the global metadata when the
526575
# plugin is reloaded
527576
self.tables = find_tables(code)
@@ -776,6 +825,45 @@ def __str__(self):
776825
return "on_stop {} from {}".format(self.function_name, self.plugin.file_name)
777826

778827

828+
class CapHook(Hook):
829+
def __init__(self, _type, plugin, base_hook):
830+
self.caps = base_hook.caps
831+
super().__init__("on_cap_{}".format(_type), plugin, base_hook)
832+
833+
def __repr__(self):
834+
return "{name}[{caps} {base!r}]".format(name=self.type, caps=self.caps, base=super())
835+
836+
def __str__(self):
837+
return "{name} {func} from {file}".format(name=self.type, func=self.function_name, file=self.plugin.file_name)
838+
839+
840+
class OnCapAvaliableHook(CapHook):
841+
def __init__(self, plugin, base_hook):
842+
super().__init__("available", plugin, base_hook)
843+
844+
845+
class OnCapAckHook(CapHook):
846+
def __init__(self, plugin, base_hook):
847+
super().__init__("ack", plugin, base_hook)
848+
849+
850+
class OnConnectHook(Hook):
851+
def __init__(self, plugin, sieve_hook):
852+
"""
853+
:type plugin: Plugin
854+
:type sieve_hook: cloudbot.util.hook._Hook
855+
"""
856+
857+
self.priority = sieve_hook.kwargs.pop("priority", 100)
858+
super().__init__("on_connect", plugin, sieve_hook)
859+
860+
def __repr__(self):
861+
return "{name}[{base!r}]".format(name=self.type, base=super())
862+
863+
def __str__(self):
864+
return "{name} {func} from {file}".format(name=self.type, func=self.function_name, file=self.plugin.file_name)
865+
866+
779867
_hook_name_to_plugin = {
780868
"command": CommandHook,
781869
"regex": RegexHook,
@@ -785,4 +873,7 @@ def __str__(self):
785873
"periodic": PeriodicHook,
786874
"on_start": OnStartHook,
787875
"on_stop": OnStopHook,
876+
"on_cap_available": OnCapAvaliableHook,
877+
"on_cap_ack": OnCapAckHook,
878+
"on_connect": OnConnectHook,
788879
}

config.default.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
],
1919
"disabled_commands": [],
2020
"acls": {},
21+
"sasl": {
22+
"enabled": false,
23+
"mechanism": "PLAIN",
24+
"user": "",
25+
"pass": ""
26+
},
2127
"nickserv": {
2228
"enabled": false,
2329
"nickserv_password": "",

0 commit comments

Comments
 (0)