Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for slack events #1451

Open
wants to merge 62 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
9555627
Initial support for slack events
AgarFu Sep 7, 2020
dcf7306
Reusing the slack_rtm.py methods
AgarFu Sep 7, 2020
709d174
Removing temporal patches
AgarFu Sep 7, 2020
1a991b0
Refactoring SlackRTM
AgarFu Sep 7, 2020
e0fa7d1
Merge branch 'no_enough_permissions' into slack_event_api_support
AgarFu Sep 7, 2020
5facafe
Merge branch 'master' of https://github.com/errbotio/errbot into no_e…
AgarFu Nov 8, 2020
277376f
Fixing deprecation errors
AgarFu Nov 9, 2020
814536e
Merge branch 'master' of https://github.com/errbotio/errbot into slac…
AgarFu Nov 9, 2020
63a5fa7
Merge branch 'no_enough_permissions' into slack_event_api_support
AgarFu Nov 9, 2020
882e097
Refactoring SlackRTM
AgarFu Sep 7, 2020
930db2a
fixed rendering issue in docs (#1452)
torgeirl Sep 10, 2020
a373b57
fix: merging configs via --storage-merge cli (#1450)
sijis Sep 10, 2020
d4c20ed
Added email property for slack backend SlackPerson class (#1186)
aaugustinas Sep 28, 2020
22ddf1c
fix: Add missing email property to backends (#1456)
sijis Oct 6, 2020
ee1f27c
chore: Add github actions (#1455)
sijis Oct 6, 2020
d7197c8
chore: update changelog and vcheck for 6.1.5
sijis Oct 10, 2020
2d1250f
fix: Add content type to package long description
sijis Oct 10, 2020
c310e22
fix: Set email property as non-abstract (#1461)
sijis Oct 16, 2020
f55d804
fix: username to userid method signature (#1458)
sijis Oct 17, 2020
b88a75d
fix: AttributeError in callback_reaction (#1467)
sijis Oct 22, 2020
622928e
docs: fix webhook examples (#1471)
sijis Oct 26, 2020
93b050a
fix: merging configs via cli with unknown keys (#1470)
sijis Oct 27, 2020
a4766e1
Fix error when plugin plug file is missing description (#1462)
bribroder Oct 27, 2020
4509b4e
docs: Fix typographical issues in setup guide (#1475)
e4r7hbug Nov 1, 2020
7ecd741
Update code to support markdown 3 (#1473)
b1rger Nov 4, 2020
6f0d91f
refactor: Split changelog by major versions (#1474)
sijis Nov 4, 2020
625411e
Fixing deprecation errors
AgarFu Nov 9, 2020
83b4108
chore: update changelog and vcheck for 6.1.6
sijis Nov 17, 2020
32e9a82
Allow dependabot to check GitHub actions weekly (#1464)
jlosito Nov 19, 2020
e15cd14
Enable testing using Python 3.9 (#1477)
nzlosh Nov 19, 2020
1529be0
Rename backend to slack_sdk to reflect the underlying module and the …
nzlosh Nov 23, 2020
8b94e66
blacken code
nzlosh Nov 23, 2020
caeaad9
Declare module dependencies.
nzlosh Nov 23, 2020
d7611ab
Mash-up slack backends.
nzlosh Nov 23, 2020
269b286
Include aiohttp dependency for slacksdk.
nzlosh Nov 24, 2020
d9d6407
Removed SlackRTMBackend from slack_sdk.py since it breaks errbot back…
nzlosh Nov 24, 2020
c0b1275
Completely merge events errbot into base class.
nzlosh Nov 24, 2020
2727da6
Fixed reference to SlackBackend class.
nzlosh Nov 24, 2020
e632485
Provide a fake serveonce method to satisfy Errbot API contract.
nzlosh Nov 24, 2020
45f5ecc
Use base class message limit methods.
nzlosh Nov 24, 2020
b93732d
Include missing module.
nzlosh Nov 24, 2020
afa67e7
Temporarily use configuration setting to indicate the slack api to use.
nzlosh Nov 25, 2020
88cfbf3
Merge branch 'master' into slack_event_api_support
nzlosh Nov 25, 2020
c950d55
Merge branch 'slack_event_api_support' of https://github.com/AgarFu/e…
nzlosh Nov 25, 2020
1ffdacc
Align backend variable names with slacksdk names.
nzlosh Nov 25, 2020
6e4ebdd
Clean up established network connection when backend is shutdown.
nzlosh Nov 26, 2020
23768b4
Merge branch 'master' of https://github.com/errbotio/errbot into coop…
AgarFu Nov 26, 2020
7a33fe8
Merge remote-tracking branch 'carlos/slack_event_api_support' into sl…
AgarFu Nov 26, 2020
821ee6a
Merge pull request #2 from AgarFu/slack_event_api_refactor
AgarFu Nov 26, 2020
c5dad0d
Skipping slack tests
AgarFu Nov 27, 2020
b69f3f5
Merge branch 'slack_event_api_support' of https://github.com/AgarFu/e…
AgarFu Nov 27, 2020
43cc136
Fixing codestyle
AgarFu Nov 27, 2020
ebe28b9
Codestyle
AgarFu Nov 27, 2020
a57050a
Refactor started, added test for slack markdown infrastructure
AgarFu Nov 27, 2020
cce5e9c
Making codestyle happy again
AgarFu Nov 27, 2020
38ded3f
Making codestyle happy
AgarFu Nov 27, 2020
3ce8dcf
Installing slack_sdk for tests
AgarFu Nov 27, 2020
c684a2a
Testing channel name
AgarFu Nov 27, 2020
f65c444
Not implemented error
AgarFu Nov 27, 2020
8670371
Refactoring to optimize the cache and reduce duplications
AgarFu Nov 27, 2020
72eacc7
Small refactor, done testing SlackPerson
AgarFu Nov 27, 2020
53db013
100% coverage for person
AgarFu Nov 27, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 31 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
sudo: false
language: python
matrix:
include:
- python: 3.6
env: TOXENV=py36
- python: 3.7
env: TOXENV=py37
- python: 3.8
env: TOXENV=py38
- python: 3.9
env: TOXENV=py39
- python: 3.7
env: TOXENV=pypi-lint
- python: 3.7
env: TOXENV=codestyle
- python: 3.7
env: TOXENV=security

install: pip install tox
before_script: cp tests/config-travisci.py config.py
script: tox

# notification for gitter integration
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/788e94bb42a75aa2df4c
on_success: always # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: true # default: false
Empty file.
30 changes: 30 additions & 0 deletions errbot/backends/_slack/markdown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import re
from markdown import Markdown
from markdown.extensions.extra import ExtraExtension
from markdown.preprocessors import Preprocessor
from errbot.rendering.ansiext import AnsiExtension, enable_format, IMTEXT_CHRS

MARKDOWN_LINK_REGEX = re.compile(r"(?<!!)\[(?P<text>[^\]]+?)\]\((?P<uri>[a-zA-Z0-9]+?:\S+?)\)")


def slack_markdown_converter(compact_output=False):
"""
This is a Markdown converter for use with Slack.
"""
enable_format("imtext", IMTEXT_CHRS, borders=not compact_output)
md = Markdown(output_format="imtext", extensions=[ExtraExtension(), AnsiExtension()])
md.preprocessors.register(LinkPreProcessor(md), "LinkPreProcessor", 30)
md.stripTopLevelTags = False
return md


class LinkPreProcessor(Preprocessor):
"""
This preprocessor converts markdown URL notation into Slack URL notation
as described at https://api.slack.com/docs/formatting, section "Linking to URLs".
"""

def run(self, lines):
for i, line in enumerate(lines):
lines[i] = MARKDOWN_LINK_REGEX.sub(r"&lt;\2|\1&gt;", line)
return lines
139 changes: 139 additions & 0 deletions errbot/backends/_slack/person.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@

import logging
from errbot.backends.base import (
Person,
RoomDoesNotExistError
)
from slack_sdk.web import WebClient

log = logging.getLogger(__name__)


class SlackPerson(Person):
"""
This class describes a person on Slack's network.
"""

def __init__(self, webclient: WebClient, userid=None, channelid=None):
if userid is not None and userid[0] not in ("U", "B", "W"):
raise Exception(
f"This is not a Slack user or bot id: {userid} (should start with U, B or W)"
)

if channelid is not None and channelid[0] not in ("D", "C", "G"):
raise Exception(
f"This is not a valid Slack channelid: {channelid} (should start with D, C or G)"
)

self._userid = userid
self._channelid = channelid
self._webclient = webclient
self._username = None # cache
self._fullname = None
self._email = None
self._channelname = None
self._email = None

@property
def userid(self):
return self._userid

@property
def username(self):
"""Convert a Slack user ID to their user name"""
if self._username:
return self._username
self._get_user_info()
if self._username is None:
return f"<{self._userid}>"
return self._username

@property
def fullname(self):
"""Convert a Slack user ID to their full name"""
if self._fullname:
return self._fullname
self._get_user_info()
if self._fullname is None:
return f"<{self._userid}>"
return self._fullname

@property
def email(self):
"""Convert a Slack user ID to their user email"""
if self._email:
return self._email
self._get_user_info()
if self._email is None:
return "<%s>" % self._userid
return self._email

def _get_user_info(self):
"""Cache all user info"""
user = self._webclient.users_info(user=self._userid)["user"]

if user is None:
log.error("Cannot find user with ID %s" % self._userid)
return

self._email = user["profile"]["email"]
self._fullname = user["real_name"]
self._username = user["name"]

@property
def channelid(self):
return self._channelid

@property
def channelname(self):
"""Convert a Slack channel ID to its channel name"""
if self._channelid is None:
return None

if self._channelname:
return self._channelname

channel = [
channel
for channel in self._webclient.conversations_list()["channels"]
if channel["id"] == self._channelid
]

if not channel:
raise RoomDoesNotExistError(f"No channel with ID {self._channelid} exists.")

if not self._channelname:
self._channelname = channel[0]["name"]
return self._channelname

@property
def domain(self):
raise NotImplementedError

# Compatibility with the generic API.
client = channelid
nick = username

# Override for ACLs
@property
def aclattr(self):
# Note: Don't use str(self) here because that will return
# an incorrect format from SlackMUCOccupant.
return f"@{self.username}"

person = aclattr

def __unicode__(self):
return f"@{self.username}"

def __str__(self):
return self.__unicode__()

def __eq__(self, other):
if not isinstance(other, SlackPerson):
log.warning("tried to compare a SlackPerson with a %s", type(other))
return False
return other.userid == self.userid

def __hash__(self):
return self.userid.__hash__()
6 changes: 3 additions & 3 deletions errbot/backends/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
log = logging.getLogger(__name__)

try:
from slackclient import SlackClient
from slackclient import WebClient
except ImportError:
log.exception("Could not start the Slack back-end")
log.fatal(
Expand Down Expand Up @@ -157,7 +157,7 @@ def email(self):
"""Convert a Slack user ID to their user email"""
user = self._sc.server.users.find(self._userid)
if user is None:
log.error("Cannot find user with ID %s", self._userid)
log.error("Cannot find user with ID %s" % self._userid)
return "<%s>" % self._userid
return user.email

Expand Down Expand Up @@ -373,7 +373,7 @@ def update_alternate_prefixes(self):
log.debug('Converted bot_alt_prefixes: %s', self.bot_config.BOT_ALT_PREFIXES)

def serve_once(self):
self.sc = SlackClient(self.token, proxies=self.proxies)
self.sc = WebClient(self.token, proxies=self.proxies)

log.info('Verifying authentication token')
self.auth = self.api_call("auth.test", raise_errors=False)
Expand Down
91 changes: 59 additions & 32 deletions errbot/backends/slack_rtm.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def username(self):
return self._username

user = self._webclient.users_info(user=self._userid)['user']

if user is None:
log.error('Cannot find user with ID %s', self._userid)
return f'<{self._userid}>'
Expand All @@ -153,7 +154,10 @@ def channelname(self):
if self._channelname:
return self._channelname

channel = [channel for channel in self._webclient.channels_list() if channel['id'] == self._channelid][0]
channel = [
channel for channel in self._webclient.conversations_list()['channels']
if channel['id'] == self._channelid
][0]
if channel is None:
raise RoomDoesNotExistError(f'No channel with ID {self._channelid} exists.')
if not self._channelname:
Expand Down Expand Up @@ -301,7 +305,7 @@ def __eq__(self, other):
return other.room.id == self.room.id and other.userid == self.userid


class SlackRTMBackend(ErrBot):
class SlackBackendBase():

@staticmethod
def _unpickle_identifier(identifier_str):
Expand All @@ -323,25 +327,6 @@ def _register_identifiers_pickling(self):
for cls in (SlackPerson, SlackRoomOccupant, SlackRoom):
copyreg.pickle(cls, SlackRTMBackend._pickle_identifier, SlackRTMBackend._unpickle_identifier)

def __init__(self, config):
super().__init__(config)
identity = config.BOT_IDENTITY
self.token = identity.get('token', None)
self.proxies = identity.get('proxies', None)
if not self.token:
log.fatal(
'You need to set your token (found under "Bot Integration" on Slack) in '
'the BOT_IDENTITY setting in your configuration. Without this token I '
'cannot connect to Slack.'
)
sys.exit(1)
self.sc = None # Will be initialized in serve_once
self.webclient = None
self.bot_identifier = None
compact = config.COMPACT_OUTPUT if hasattr(config, 'COMPACT_OUTPUT') else False
self.md = slack_markdown_converter(compact)
self._register_identifiers_pickling()

def update_alternate_prefixes(self):
"""Converts BOT_ALT_PREFIXES to use the slack ID instead of name

Expand Down Expand Up @@ -419,6 +404,16 @@ def _hello_event_handler(self, webclient: WebClient, event):
self.connect_callback()
self.callback_presence(Presence(identifier=self.bot_identifier, status=ONLINE))

def _reaction_added_event_handler(self, webclient: WebClient, event):
"""Event handler for the 'reaction_added' event"""
emoji = event["reaction"]
log.debug('Added reaction: {}'.format(emoji))

def _reaction_removed_event_handler(self, webclient: WebClient, event):
"""Event handler for the 'reaction_removed' event"""
emoji = event["reaction"]
log.debug('Removed reaction: {}'.format(emoji))

def _presence_change_event_handler(self, webclient: WebClient, event):
"""Event handler for the 'presence_change' event"""

Expand Down Expand Up @@ -553,10 +548,10 @@ def channelid_to_channelname(self, id_: str):
def channelname_to_channelid(self, name: str):
"""Convert a Slack channel name to its channel ID"""
name = name.lstrip('#')
channel = [channel for channel in self.webclient.channels_list() if channel.name == name]
channel = [channel for channel in self.webclient.conversations_list()['channels'] if channel['name'] == name]
if not channel:
raise RoomDoesNotExistError(f'No channel named {name} exists')
return channel[0].id
return channel[0]['id']

def channels(self, exclude_archived=True, joined_only=False):
"""
Expand All @@ -574,7 +569,7 @@ def channels(self, exclude_archived=True, joined_only=False):
* https://api.slack.com/methods/channels.list
* https://api.slack.com/methods/groups.list
"""
response = self.webclient.channels_list(exclude_archived=exclude_archived)
response = self.webclient.conversations_list(exclude_archived=exclude_archived)
channels = [channel for channel in response['channels']
if channel['is_member'] or not joined_only]

Expand All @@ -590,7 +585,7 @@ def channels(self, exclude_archived=True, joined_only=False):
def get_im_channel(self, id_):
"""Open a direct message channel to a user"""
try:
response = self.webclient.im_open(user=id_)
response = self.webclient.conversations_open(user=id_)
return response['channel']['id']
except SlackAPIResponseError as e:
if e.error == "cannot_dm_bot":
Expand Down Expand Up @@ -1022,6 +1017,28 @@ def process_mentions(self, text):
return text, mentioned


class SlackRTMBackend(SlackBackendBase, ErrBot):

def __init__(self, config):
super().__init__(config)
identity = config.BOT_IDENTITY
self.token = identity.get('token', None)
self.proxies = identity.get('proxies', None)
if not self.token:
log.fatal(
'You need to set your token (found under "Bot Integration" on Slack) in '
'the BOT_IDENTITY setting in your configuration. Without this token I '
'cannot connect to Slack.'
)
sys.exit(1)
self.sc = None # Will be initialized in serve_once
self.webclient = None
self.bot_identifier = None
compact = config.COMPACT_OUTPUT if hasattr(config, 'COMPACT_OUTPUT') else False
self.md = slack_markdown_converter(compact)
self._register_identifiers_pickling()


class SlackRoom(Room):
def __init__(self, webclient=None, name=None, channelid=None, bot=None):
if channelid is not None and name is not None:
Expand All @@ -1035,7 +1052,7 @@ def __init__(self, webclient=None, name=None, channelid=None, bot=None):
else:
self._name = bot.channelid_to_channelname(channelid)

self._id = None
self._id = channelid
self._bot = bot
self.webclient = webclient

Expand All @@ -1052,12 +1069,22 @@ def _channel(self):
The channel object exposed by SlackClient
"""
_id = None
for channel in self.webclient.conversations_list()['channels']:
if channel['name'] == self.name:
_id = channel['id']
break
else:
raise RoomDoesNotExistError(f"{str(self)} does not exist (or is a private group you don't have access to)")
# Cursors
cursor = ''
while cursor is not None:
conversations_list = self.webclient.conversations_list(cursor=cursor)
cursor = None
for channel in conversations_list['channels']:
if channel['name'] == self.name:
_id = channel['id']
break
else:
if conversations_list['response_metadata']['next_cursor'] is not None:
cursor = conversations_list['response_metadata']['next_cursor']
else:
raise RoomDoesNotExistError(
f"{str(self)} does not exist (or is a private group you don't have access to)"
)
return _id

@property
Expand Down