This repository was archived by the owner on Nov 26, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathrunner.py
More file actions
238 lines (203 loc) · 9.41 KB
/
runner.py
File metadata and controls
238 lines (203 loc) · 9.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# pylint: disable=W0212
import json
import logging
from datetime import datetime
from django.utils.timezone import utc
import re
import redis
import botbot_plugins.plugins
from botbot_plugins.base import PrivateMessage
from django.core.cache import cache
from django.conf import settings
from django.utils.importlib import import_module
from django_statsd.clients import statsd
from botbot.apps.bots import models as bots_models
from botbot.apps.plugins.utils import convert_nano_timestamp, log_on_error
from .plugin import RealPluginMixin
CACHE_TIMEOUT_2H = 7200
LOG = logging.getLogger('botbot.plugin_runner')
class Line(object):
"""
All the methods and data necessary for a plugin to act on a line
"""
def __init__(self, packet, app):
self.full_text = packet['Content']
self.text = packet['Content']
self.user = packet['User']
# Private attributes not accessible to external plugins
self._chatbot_id = packet['ChatBotId']
self._raw = packet['Raw']
self._channel_name = packet['Channel'].strip()
self._command = packet['Command']
self._is_message = packet['Command'] == 'PRIVMSG'
self._host = packet['Host']
self._received = convert_nano_timestamp(packet['Received'])
self.is_direct_message = self.check_direct_message()
@property
def _chatbot(self):
"""Simple caching for ChatBot model"""
if not hasattr(self, '_chatbot_cache'):
cache_key = 'chatbot:{0}'.format(self._chatbot_id)
chatbot = cache.get(cache_key)
if not chatbot:
chatbot = bots_models.ChatBot.objects.get(id=self._chatbot_id)
cache.set(cache_key, chatbot, CACHE_TIMEOUT_2H)
self._chatbot_cache = chatbot
return self._chatbot_cache
@property
def _channel(self):
"""Simple caching for Channel model"""
if not hasattr(self, '_channel_cache'):
cache_key = 'channel:{0}-{1}'.format(self._chatbot_id, self._channel_name)
channel = cache.get(cache_key)
if not channel and self._channel_name.startswith("#"):
channel = self._chatbot.channel_set.get(
name=self._channel_name)
cache.set(cache_key, channel, CACHE_TIMEOUT_2H)
"""
The following logging is to help out in sentry. For some
channels, we are getting occasional issues with the
``channel_set.get()`` lookup above
"""
LOG.debug(channel)
LOG.debug(self._channel_name)
LOG.debug(cache_key)
LOG.debug("%s", ", ".join(self._chatbot.channel_set.values_list('name', flat=True)))
self._channel_cache = channel
return self._channel_cache
@property
def _active_plugin_slugs(self):
if not hasattr(self, '_active_plugin_slugs_cache'):
if self._channel:
self._active_plugin_slugs_cache = self._channel.active_plugin_slugs
else:
self._active_plugin_slugs_cache = set()
return self._active_plugin_slugs_cache
def check_direct_message(self):
"""
If message is addressed to the bot, strip the bot's nick
and return the rest of the message. Otherwise, return False.
"""
nick = self._chatbot.nick
# Private message
if self._channel_name == nick:
LOG.debug('Private message detected')
# Set channel as user, so plugins reply by PM to correct user
self._channel_name = self.user
return True
if len(nick) == 1:
# support @<plugin> or !<plugin>
regex = ur'^{0}(.*)'.format(re.escape(nick))
else:
# support <nick>: <plugin>
regex = ur'^{0}[:\s](.*)'.format(re.escape(nick))
match = re.match(regex, self.full_text, re.IGNORECASE)
if match:
LOG.debug('Direct message detected')
self.text = match.groups()[0].lstrip()
return True
return False
def __str__(self):
return self.full_text
def __repr__(self):
return str(self)
class PluginRunner(object):
"""
Registration and routing for plugins
Calls to plugins are done via greenlets
"""
def __init__(self):
self.bot_bus = redis.StrictRedis.from_url(
settings.REDIS_PLUGIN_QUEUE_URL)
self.storage = redis.StrictRedis.from_url(
settings.REDIS_PLUGIN_STORAGE_URL)
# plugins that listen to everything coming over the wire
self.firehose_router = {}
# plugins that listen to all messages (aka PRIVMSG)
self.messages_router = {}
# plugins that listen on direct messages (starting with bot nick)
self.mentions_router = {}
def register_all_plugins(self):
"""Iterate over all plugins and register them with the app"""
for core_plugin in ['help', 'logger']:
mod = import_module('botbot.apps.plugins.core.{}'.format(core_plugin))
plugin = mod.Plugin()
self.register(plugin)
for mod in botbot_plugins.plugins.__all__:
plugin = import_module('botbot_plugins.plugins.' + mod).Plugin()
self.register(plugin)
def register(self, plugin):
"""
Introspects the Plugin class instance provided for methods
that need to be registered with the internal app routers.
"""
for key in dir(plugin):
try:
# the config attr bombs if accessed here because it tries
# to access an attribute from the dummyapp
attr = getattr(plugin, key)
except AttributeError:
continue
if (not key.startswith('__') and
getattr(attr, 'route_rule', None)):
LOG.info('Route: %s.%s listens to %s for matches to %s',
plugin.slug, key, attr.route_rule[0],
attr.route_rule[1])
getattr(self, attr.route_rule[0] + '_router').setdefault(
plugin.slug, []).append((attr.route_rule[1], attr, plugin))
def process_line(self, line_json):
LOG.debug('Recieved: %s', line_json)
line = Line(json.loads(line_json), self)
# Calculate the transport latency between go and the plugins.
delta = datetime.utcnow().replace(tzinfo=utc) - line._received
statsd.timing(".".join(["plugins", "latency"]),
delta.total_seconds() * 1000)
self.dispatch(line)
def dispatch(self, line):
"""Given a line, dispatch it to the right plugins & functions."""
# This is a pared down version of the `check_for_plugin_route_matches`
# method for firehose plugins (no regexing or return values)
active_firehose_plugins = line._active_plugin_slugs.intersection(
self.firehose_router.viewkeys())
for plugin_slug in active_firehose_plugins:
for _, func, plugin in self.firehose_router[plugin_slug]:
# firehose gets everything, no rule matching
LOG.info('Match: %s.%s', plugin_slug, func.__name__)
with statsd.timer(".".join(["plugins", plugin_slug])):
channel_plugin = self.setup_plugin_for_channel(
plugin.__class__, line)
new_func = log_on_error(LOG, getattr(channel_plugin,
func.__name__))
channel_plugin.respond(new_func(line))
# pass line to other routers
if line._is_message:
self.check_for_plugin_route_matches(line, self.messages_router)
if line.is_direct_message:
self.check_for_plugin_route_matches(line, self.mentions_router)
def setup_plugin_for_channel(self, fake_plugin_class, line):
"""Given a dummy plugin class, initialize it for the line's channel"""
class RealPlugin(RealPluginMixin, fake_plugin_class):
pass
plugin = RealPlugin(slug=fake_plugin_class.__module__.split('.')[-1],
channel=line._channel,
chatbot_id=line._chatbot_id,
app=self)
return plugin
def check_for_plugin_route_matches(self, line, router):
"""Checks the active plugins' routes and calls functions on matches"""
# get the active routes for this channel
active_slugs = line._active_plugin_slugs.intersection(router.viewkeys())
for plugin_slug in active_slugs:
for rule, func, plugin in router[plugin_slug]:
match = re.match(rule, line.text, re.IGNORECASE)
if match:
LOG.info('Match: %s.%s', plugin_slug, func.__name__)
with statsd.timer(".".join(["plugins", plugin_slug])):
# Instantiate a plugin specific to this channel
channel_plugin = self.setup_plugin_for_channel(
plugin.__class__, line)
# get the method from the channel-specific plugin
new_func = log_on_error(LOG, getattr(channel_plugin,
func.__name__))
channel_plugin.respond(new_func(line,
**match.groupdict()))