SlackSensor   B
last analyzed

Complexity

Total Complexity 44

Size/Duplication

Total Lines 219
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 219
rs 8.3396
c 0
b 0
f 0
wmc 44

17 Methods

Rating   Name   Duplication   Size   Complexity  
A poll() 0 11 3
A cleanup() 0 2 1
A _get_user_info() 0 12 3
A setup() 0 11 2
A _get_group_info() 0 12 3
A remove_trigger() 0 2 1
A _set_last_message_timestamp() 0 6 1
A _populate_cache() 0 14 4
A update_trigger() 0 2 1
B _handle_result() 0 23 6
C _handle_message() 0 53 8
A add_trigger() 0 2 1
A __init__() 0 17 1
A _get_last_message_timestamp() 0 10 3
A _get_channel_info() 0 12 3
A _handle_message_ignore_errors() 0 7 2
A _api_call() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like SlackSensor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import json
2
import re
3
4
import eventlet
5
from slackclient import SlackClient
6
7
from st2reactor.sensor.base import PollingSensor
8
9
eventlet.monkey_patch(
10
    os=True,
11
    select=True,
12
    socket=True,
13
    thread=True,
14
    time=True)
15
16
EVENT_TYPE_WHITELIST = [
17
    'message'
18
]
19
20
21
class SlackSensor(PollingSensor):
22
    DATASTORE_KEY_NAME = 'last_message_timestamp'
23
24
    def __init__(self, sensor_service, config=None, poll_interval=None):
25
        super(SlackSensor, self).__init__(sensor_service=sensor_service,
26
                                          config=config,
27
                                          poll_interval=poll_interval)
28
        self._logger = self._sensor_service.get_logger(__name__)
29
        self._token = self._config['sensor']['token']
30
        self._strip_formatting = self._config['sensor'].get('strip_formatting',
31
                                                            False)
32
        self._handlers = {
33
            'message': self._handle_message_ignore_errors,
34
        }
35
36
        self._user_info_cache = {}
37
        self._channel_info_cache = {}
38
        self._group_info_cache = {}
39
40
        self._last_message_timestamp = None
41
42
    def setup(self):
43
        self._client = SlackClient(self._token)
44
        data = self._client.rtm_connect()
45
46
        if not data:
47
            msg = 'Failed to connect to the Slack API. Invalid token?'
48
            raise Exception(msg)
49
50
        self._populate_cache(user_data=self._api_call('users.list'),
51
                             channel_data=self._api_call('channels.list'),
52
                             group_data=self._api_call('groups.list'),)
53
54
    def poll(self):
55
        result = self._client.rtm_read()
56
57
        if not result:
58
            return
59
60
        last_message_timestamp = self._handle_result(result=result)
61
62
        if last_message_timestamp:
63
            self._set_last_message_timestamp(
64
                last_message_timestamp=last_message_timestamp)
65
66
    def cleanup(self):
67
        pass
68
69
    def add_trigger(self, trigger):
70
        pass
71
72
    def update_trigger(self, trigger):
73
        pass
74
75
    def remove_trigger(self, trigger):
76
        pass
77
78
    def _get_last_message_timestamp(self):
79
        """
80
        :rtype: ``int``
81
        """
82
        if not self._last_message_timestamp:
83
            name = self.DATASTORE_KEY_NAME
84
            value = self._sensor_service.get_value(name=name)
85
            self._last_message_timestamp = int(value) if value else 0
86
87
        return self._last_message_timestamp
88
89
    def _set_last_message_timestamp(self, last_message_timestamp):
90
        self._last_message_timestamp = last_message_timestamp
91
        name = self.DATASTORE_KEY_NAME
92
        value = str(last_message_timestamp)
93
        self._sensor_service.set_value(name=name, value=value)
94
        return last_message_timestamp
95
96
    def _populate_cache(self, user_data, channel_data, group_data):
97
        """
98
        Populate users, channels and group cache from info which is returned on
99
        rtm.start
100
        """
101
102
        for user in user_data.get('members', []):
103
            self._user_info_cache[user['id']] = user
104
105
        for channel in channel_data.get('channels', []):
106
            self._channel_info_cache[channel['id']] = channel
107
108
        for group in group_data.get('groups', []):
109
            self._group_info_cache[group['id']] = group
110
111
    def _handle_result(self, result):
112
        """
113
        Handle / process the result and return timestamp of the last message.
114
        """
115
        existing_last_message_timestamp = self._get_last_message_timestamp()
116
        new_last_message_timestamp = existing_last_message_timestamp
117
118
        for item in result:
119
            item_type = item['type']
120
            item_timestamp = int(float(item.get('ts', 0)))
121
122
            if (existing_last_message_timestamp and
123
                    item_timestamp <= existing_last_message_timestamp):
124
                # We have already seen this message, skip it
125
                continue
126
127
            if item_timestamp > new_last_message_timestamp:
128
                new_last_message_timestamp = item_timestamp
129
130
            handler_func = self._handlers.get(item_type, lambda data: data)
131
            handler_func(data=item)
132
133
        return new_last_message_timestamp
134
135
    def _handle_message(self, data):
136
        trigger = 'slack.message'
137
        event_type = data['type']
138
139
        if event_type not in EVENT_TYPE_WHITELIST or 'subtype' in data:
140
            # Skip unsupported event
141
            return
142
143
        # Note: We resolve user and channel information to provide more context
144
        user_info = self._get_user_info(user_id=data['user'])
145
        channel_info = None
146
        channel_id = data.get('channel', '')
147
        # Grabbing info based on the type of channel the message is in.
148
        if channel_id.startswith('C'):
149
            channel_info = self._get_channel_info(channel_id=channel_id)
150
        elif channel_id.startswith('G'):
151
            channel_info = self._get_group_info(group_id=channel_id)
152
153
        if not user_info or not channel_info:
154
            # Deleted user or channel
155
            return
156
157
        # Removes formatting from messages if enabled by the user in config
158
        if self._strip_formatting:
159
            text = re.sub("<http.*[|](.*)>", "\\1", data['text'])
160
        else:
161
            text = data['text']
162
163
        payload = {
164
            'user': {
165
                'id': user_info['id'],
166
                'name': user_info['name'],
167
                'first_name': user_info['profile'].get('first_name',
168
                                                       'Unknown'),
169
                'last_name': user_info['profile'].get('last_name',
170
                                                      'Unknown'),
171
                'real_name': user_info['profile'].get('real_name',
172
                                                      'Unknown'),
173
                'is_admin': user_info['is_admin'],
174
                'is_owner': user_info['is_owner']
175
            },
176
            'channel': {
177
                'id': channel_info['id'],
178
                'name': channel_info['name'],
179
                'topic': channel_info['topic']['value'],
180
                'is_group': channel_info.get('is_group', False),
181
            },
182
            'timestamp': int(float(data['ts'])),
183
            'timestamp_raw': data['ts'],
184
            'text': text
185
        }
186
187
        self._sensor_service.dispatch(trigger=trigger, payload=payload)
188
189
    def _handle_message_ignore_errors(self, data):
190
        try:
191
            self._handle_message(data)
192
        except Exception as exc:
193
            self._logger.info("Slack sensor encountered an error "
194
                              "handling message: %s" % exc)
195
            pass
196
197
    def _get_user_info(self, user_id):
198
        if user_id not in self._user_info_cache:
199
            result = self._api_call('users.info', user=user_id)
200
201
            if 'user' not in result:
202
                # User doesn't exist or other error
203
                return None
204
205
            result = result['user']
206
            self._user_info_cache[user_id] = result
207
208
        return self._user_info_cache[user_id]
209
210
    def _get_channel_info(self, channel_id):
211
        if channel_id not in self._channel_info_cache:
212
            result = self._api_call('channels.info', channel=channel_id)
213
214
            if 'channel' not in result:
215
                # Channel doesn't exist or other error
216
                return None
217
218
            result = result['channel']
219
            self._channel_info_cache[channel_id] = result
220
221
        return self._channel_info_cache[channel_id]
222
223
    def _get_group_info(self, group_id):
224
        if group_id not in self._group_info_cache:
225
            result = self._api_call('groups.info', channel=group_id)
226
            self._logger.warn('GROUP DATA: %s' % result)
227
            if 'group' not in result:
228
                # Group doesn't exist or other error
229
                return None
230
231
            result = result['group']
232
            self._group_info_cache[group_id] = result
233
234
        return self._group_info_cache[group_id]
235
236
    def _api_call(self, method, **kwargs):
237
        result = self._client.api_call(method, **kwargs)
238
        result = json.loads(result)
239
        return result
240