├── __init__.py ├── setup.cfg ├── requirements.txt ├── slackclient ├── __init__.py ├── _util.py ├── _slackrequest.py ├── _user.py ├── _im.py ├── _channel.py ├── _client.py └── _server.py ├── doc └── examples │ ├── README.md │ └── example.py ├── .gitignore ├── requirements-dev.txt ├── tests ├── data │ ├── im.created.json │ ├── channel.created.json │ └── rtm.start.json ├── test_slackrequest.py ├── test_channel.py ├── conftest.py ├── test_slackclient.py └── test_server.py ├── .travis.yml ├── setup.py ├── tox.ini ├── LICENSE.txt ├── CHANGELOG.md └── README.md /__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | future==0.15.2 2 | six==1.10.0 3 | websocket-client==0.35.0 4 | -------------------------------------------------------------------------------- /slackclient/__init__.py: -------------------------------------------------------------------------------- 1 | from slackclient._client import SlackClient # noqa 2 | -------------------------------------------------------------------------------- /doc/examples/README.md: -------------------------------------------------------------------------------- 1 | need to add examples. in the meantime check out [python-rtmbot](https://github.com/slackhq/python-rtmbot/) 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .cache 3 | .idea 4 | dist 5 | slackclient.egg-info 6 | *.log 7 | env 8 | .tox 9 | *.un~ 10 | 0/ 11 | tests/.cache 12 | .coverage 13 | .cache 14 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | coveralls==1.1 2 | ipdb==0.9.3 3 | ipython==4.1.2 4 | pdbpp==0.8.3 5 | pytest>=2.8.2 6 | pytest-mock>=1.1 7 | pytest-cov==2.2.1 8 | pytest-pythonpath>=0.3 9 | testfixtures==4.9.1 10 | tox>=1.8.0 -------------------------------------------------------------------------------- /tests/data/im.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "im_created", 3 | "user": "U024BE7LH", 4 | "channel": { 5 | "id": "D024BE91L", 6 | "user": "U123BL234", 7 | "created": 1360782804 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/data/channel.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "channel_created", 3 | "channel": { 4 | "id": "C024BE91L", 5 | "name": "fun", 6 | "created": 1360782804, 7 | "creator": "U024BE7LH" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "3.5" 5 | env: 6 | matrix: 7 | - TOX_ENV=py27 8 | - TOX_ENV=py34 9 | - TOX_ENV=py35 10 | - TOX_ENV=flake8 11 | cache: pip 12 | install: 13 | - "travis_retry pip install setuptools --upgrade" 14 | - "travis_retry pip install tox" 15 | script: 16 | - tox -e $TOX_ENV 17 | after_script: 18 | - cat .tox/$TOX_ENV/log/*.log -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='slackclient', 4 | version='1.0.0', 5 | description='Python client for Slack.com', 6 | url='http://github.com/slackhq/python-slackclient', 7 | author='Ryan Huber', 8 | author_email='ryan@slack-corp.com', 9 | license='MIT', 10 | packages=['slackclient'], 11 | install_requires=[ 12 | 'websocket-client', 13 | 'requests', 14 | 'six', 15 | ], 16 | zip_safe=False) 17 | -------------------------------------------------------------------------------- /slackclient/_util.py: -------------------------------------------------------------------------------- 1 | class SearchList(list): 2 | 3 | def find(self, name): 4 | items = [] 5 | for child in self: 6 | if child.__class__ == self.__class__: 7 | items += child.find(name) 8 | else: 9 | if child == name: 10 | items.append(child) 11 | 12 | if len(items) == 1: 13 | return items[0] 14 | elif items: 15 | return items 16 | else: 17 | return None 18 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | py{27,34,35}, 4 | flake8 5 | skipsdist=true 6 | 7 | [flake8] 8 | max-line-length= 100 9 | exclude= tests/* 10 | 11 | [testenv] 12 | passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH 13 | commands = 14 | py.test --cov-report= --cov=slackclient {posargs:tests} 15 | coveralls 16 | 17 | deps = 18 | -r{toxinidir}/requirements-dev.txt 19 | -r{toxinidir}/requirements.txt 20 | basepython = 21 | py27: python2.7 22 | py34: python3.4 23 | py35: python3.5 24 | 25 | [testenv:flake8] 26 | basepython=python 27 | deps=flake8 28 | commands= 29 | flake8 \ 30 | {toxinidir}/slackclient -------------------------------------------------------------------------------- /tests/test_slackrequest.py: -------------------------------------------------------------------------------- 1 | from slackclient._slackrequest import SlackRequest 2 | import json 3 | 4 | 5 | def test_post_attachements(mocker): 6 | requests = mocker.patch('slackclient._slackrequest.requests') 7 | 8 | SlackRequest.do('xoxb-123', 9 | 'chat.postMessage', 10 | {'attachments': [{'title': 'hello'}]}) 11 | 12 | assert requests.post.call_count == 1 13 | args, kwargs = requests.post.call_args 14 | assert 'https://slack.com/api/chat.postMessage' == args[0] 15 | assert {'attachments': json.dumps([{'title': 'hello'}]), 16 | 'token': 'xoxb-123'} == kwargs['data'] 17 | assert None == kwargs['files'] 18 | -------------------------------------------------------------------------------- /slackclient/_slackrequest.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | import six 5 | 6 | 7 | class SlackRequest(object): 8 | 9 | @staticmethod 10 | def do(token, request="?", post_data=None, domain="slack.com"): 11 | post_data = post_data or {} 12 | 13 | for k, v in six.iteritems(post_data): 14 | if not isinstance(v, six.string_types): 15 | post_data[k] = json.dumps(v) 16 | 17 | url = 'https://{0}/api/{1}'.format(domain, request) 18 | post_data['token'] = token 19 | files = {'file': post_data.pop('file')} if 'file' in post_data else None 20 | 21 | return requests.post(url, data=post_data, files=files) 22 | -------------------------------------------------------------------------------- /slackclient/_user.py: -------------------------------------------------------------------------------- 1 | class User(object): 2 | def __init__(self, server, name, user_id, real_name, tz): 3 | self.tz = tz 4 | self.name = name 5 | self.real_name = real_name 6 | self.server = server 7 | self.id = user_id 8 | 9 | def __eq__(self, compare_str): 10 | if compare_str in (self.id, self.name): 11 | return True 12 | else: 13 | return False 14 | 15 | def __hash__(self): 16 | return hash(self.id) 17 | 18 | def __str__(self): 19 | data = "" 20 | for key in list(self.__dict__.keys()): 21 | if key != "server": 22 | data += "{0} : {1}\n".format(key, str(self.__dict__[key])[:40]) 23 | return data 24 | 25 | def __repr__(self): 26 | return self.__str__() 27 | -------------------------------------------------------------------------------- /tests/test_channel.py: -------------------------------------------------------------------------------- 1 | from slackclient._channel import Channel 2 | import pytest 3 | 4 | 5 | def test_channel(channel): 6 | assert type(channel) == Channel 7 | 8 | def test_channel_eq(channel): 9 | channel = Channel( 10 | 'test-server', 11 | 'test-channel', 12 | 'C12345678', 13 | ) 14 | assert channel == 'test-channel' 15 | assert channel == '#test-channel' 16 | assert channel == 'C12345678' 17 | assert (channel == 'foo') is False 18 | 19 | def test_channel_is_hashable(channel): 20 | channel = Channel( 21 | 'test-server', 22 | 'test-channel', 23 | 'C12345678', 24 | ) 25 | channel_map = {channel: channel.id} 26 | assert channel_map[channel] == 'C12345678' 27 | assert (channel_map[channel] == 'foo') is False 28 | 29 | @pytest.mark.xfail 30 | def test_channel_send_message(channel): 31 | channel.send_message('hi') 32 | -------------------------------------------------------------------------------- /slackclient/_im.py: -------------------------------------------------------------------------------- 1 | class Im(object): 2 | def __init__(self, server, user, im_id): 3 | self.server = server 4 | self.user = user 5 | self.id = im_id 6 | 7 | def __eq__(self, compare_str): 8 | if self.id == compare_str or self.user == compare_str: 9 | return True 10 | else: 11 | return False 12 | 13 | def __hash__(self): 14 | return hash(self.id) 15 | 16 | def __str__(self): 17 | data = "" 18 | for key in list(self.__dict__.keys()): 19 | if key != "server": 20 | data += "{0} : {1}\n".format(key, str(self.__dict__[key])[:40]) 21 | return data 22 | 23 | def __repr__(self): 24 | return self.__str__() 25 | 26 | def send_message(self, message): 27 | message_json = {"type": "message", "channel": self.id, "text": message} 28 | self.server.send_to_websocket(message_json) 29 | -------------------------------------------------------------------------------- /doc/examples/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import random 4 | from slackclient import SlackClient 5 | import time 6 | 7 | #get your personal token from https://api.slack.com/web, bottom of the page. 8 | api_key = '' 9 | client = SlackClient(api_key) 10 | 11 | if client.rtm_connect(): 12 | while True: 13 | last_read = client.rtm_read() 14 | if last_read: 15 | try: 16 | parsed = last_read[0]['text'] 17 | #reply to channel message was found in. 18 | message_channel = last_read[0]['channel'] 19 | if parsed and 'food:' in parsed: 20 | choice = random.choice(['hamburger', 'pizza']) 21 | client.rtm_send_message(message_channel, 22 | 'Today you\'ll eat %s.' % choice) 23 | except: 24 | pass 25 | time.sleep(1) 26 | -------------------------------------------------------------------------------- /slackclient/_channel.py: -------------------------------------------------------------------------------- 1 | class Channel(object): 2 | def __init__(self, server, name, channel_id, members=None): 3 | self.server = server 4 | self.name = name 5 | self.id = channel_id 6 | self.members = [] if members is None else members 7 | 8 | def __eq__(self, compare_str): 9 | if self.name == compare_str or "#" + self.name == compare_str or self.id == compare_str: 10 | return True 11 | else: 12 | return False 13 | 14 | def __hash__(self): 15 | return hash(self.id) 16 | 17 | def __str__(self): 18 | data = "" 19 | for key in list(self.__dict__.keys()): 20 | data += "{0} : {1}\n".format(key, str(self.__dict__[key])[:40]) 21 | return data 22 | 23 | def __repr__(self): 24 | return self.__str__() 25 | 26 | def send_message(self, message): 27 | message_json = {"type": "message", "channel": self.id, "text": message} 28 | self.server.send_to_websocket(message_json) 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 17 | THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | from slackclient._channel import Channel 4 | from slackclient._server import Server 5 | from slackclient._client import SlackClient 6 | 7 | 8 | # This is so that tests work on Travis for python 2.6, it's really hacky, but expedient 9 | def get_unverified_post(): 10 | requests_post = requests.post 11 | 12 | def unverified_post(*args, **kwargs): 13 | # don't throw SSL errors plz 14 | kwargs['verify'] = False 15 | return requests_post(*args, **kwargs) 16 | 17 | return unverified_post 18 | 19 | requests.post = get_unverified_post() 20 | 21 | 22 | @pytest.fixture 23 | def server(monkeypatch): 24 | my_server = Server('xoxp-1234123412341234-12341234-1234', False) 25 | return my_server 26 | 27 | 28 | @pytest.fixture 29 | def slackclient(server): 30 | my_slackclient = SlackClient('xoxp-1234123412341234-12341234-1234') 31 | return my_slackclient 32 | 33 | 34 | @pytest.fixture 35 | def channel(server): 36 | my_channel = Channel(server, "somechannel", "C12341234", ["user"]) 37 | return my_channel 38 | 39 | -------------------------------------------------------------------------------- /tests/test_slackclient.py: -------------------------------------------------------------------------------- 1 | from slackclient._client import SlackClient 2 | from slackclient._channel import Channel 3 | import json 4 | import pytest 5 | 6 | 7 | @pytest.fixture 8 | def channel_created_fixture(): 9 | file_channel_created_data = open('tests/data/channel.created.json', 'r').read() 10 | json_channel_created_data = json.loads(file_channel_created_data) 11 | return json_channel_created_data 12 | 13 | 14 | @pytest.fixture 15 | def im_created_fixture(): 16 | file_channel_created_data = open('tests/data/im.created.json', 'r').read() 17 | json_channel_created_data = json.loads(file_channel_created_data) 18 | return json_channel_created_data 19 | 20 | 21 | def test_SlackClient(slackclient): 22 | assert type(slackclient) == SlackClient 23 | 24 | 25 | def test_SlackClient_process_changes(slackclient, channel_created_fixture, im_created_fixture): 26 | slackclient.process_changes(channel_created_fixture) 27 | assert type(slackclient.server.channels.find('fun')) == Channel 28 | slackclient.process_changes(im_created_fixture) 29 | assert type(slackclient.server.channels.find('U123BL234')) == Channel 30 | 31 | -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- 1 | from slackclient._user import User 2 | from slackclient._server import Server, SlackLoginError 3 | from slackclient._channel import Channel 4 | import json 5 | import pytest 6 | 7 | 8 | @pytest.fixture 9 | def login_fixture(): 10 | file_login_data = open('tests/data/rtm.start.json', 'r').read() 11 | json_login_data = json.loads(file_login_data) 12 | return json_login_data 13 | 14 | 15 | def test_Server(server): 16 | assert type(server) == Server 17 | 18 | 19 | def test_Server_is_hashable(server): 20 | server_map = {server: server.token} 21 | assert server_map[server] == 'xoxp-1234123412341234-12341234-1234' 22 | assert (server_map[server] == 'foo') is False 23 | 24 | 25 | def test_Server_parse_channel_data(server, login_fixture): 26 | server.parse_channel_data(login_fixture["channels"]) 27 | assert type(server.channels.find('general')) == Channel 28 | 29 | 30 | def test_Server_parse_user_data(server, login_fixture): 31 | server.parse_user_data(login_fixture["users"]) 32 | assert type(server.users.find('fakeuser')) == User 33 | 34 | 35 | def test_Server_cantconnect(server): 36 | with pytest.raises(SlackLoginError): 37 | reply = server.ping() 38 | 39 | 40 | @pytest.mark.xfail 41 | def test_Server_ping(server, monkeypatch): 42 | # monkeypatch.setattr("", lambda: True) 43 | monkeypatch.setattr("websocket.create_connection", lambda: True) 44 | reply = server.ping() 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v1.0.0 (2016-02-28) 2 | 3 | * the `api_call` function now returns a decoded JSON object, rather than a JSON encoded string 4 | * some `api_call` calls now call actions on the parent server object: 5 | * `im.open` 6 | * `mpim.open`, `groups.create`, `groups.createChild` 7 | * `channels.create`, `channels.join` 8 | 9 | ### v0.18.0 (2016-02-21) 10 | 11 | * Moves to use semver for versioning 12 | * Adds support for private groups and MPDMs 13 | * Switches to use requests instead of urllib 14 | * Gets Travis CI integration working 15 | * Fixes some formatting issues so the code will work for python 2.6 16 | * Cleans up some unused imports, some PEP-8 fixes and a couple bad default args fixes 17 | 18 | ### v0.17 (2016-02-15) 19 | 20 | * Fixes the server so that it doesn't add duplicate users or channels to its internal lists, https://github.com/slackhq/python-slackclient/commit/0cb4bcd6e887b428e27e8059b6278b86ee661aaa 21 | * README updates: 22 | * Updates the URLs pointing to Slack docs for configuring authentication, https://github.com/slackhq/python-slackclient/commit/7d01515cebc80918a29100b0e4793790eb83e7b9 23 | * s/channnels/channels, https://github.com/slackhq/python-slackclient/commit/d45285d2f1025899dcd65e259624ee73771f94bb 24 | * Adds users to the local cache when they join the team, https://github.com/slackhq/python-slackclient/commit/f7bb8889580cc34471ba1ddc05afc34d1a5efa23 25 | * Fixes urllib py 2/3 compatibility, https://github.com/slackhq/python-slackclient/commit/1046cc2375a85a22e94573e2aad954ba7287c886 26 | 27 | -------------------------------------------------------------------------------- /slackclient/_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # mostly a proxy object to abstract how some of this works 3 | 4 | import json 5 | 6 | from slackclient._server import Server 7 | 8 | 9 | class SlackClient(object): 10 | def __init__(self, token): 11 | self.token = token 12 | self.server = Server(self.token, False) 13 | 14 | def rtm_connect(self): 15 | try: 16 | self.server.rtm_connect() 17 | return True 18 | except: 19 | return False 20 | 21 | def api_call(self, method, **kwargs): 22 | result = json.loads(self.server.api_call(method, **kwargs)) 23 | if self.server: 24 | if method == 'im.open': 25 | if "ok" in result and result["ok"]: 26 | self.server.attach_channel(kwargs["user"], result["channel"]["id"]) 27 | elif method in ('mpim.open', 'groups.create', 'groups.createchild'): 28 | if "ok" in result and result["ok"]: 29 | self.server.attach_channel( 30 | result['group']['name'], 31 | result['group']['id'], 32 | result['group']['members'] 33 | ) 34 | elif method in ('channels.create', 'channels.join'): 35 | if 'ok' in result and result['ok']: 36 | self.server.attach_channel( 37 | result['channel']['name'], 38 | result['channel']['id'], 39 | result['channel']['members'] 40 | ) 41 | return result 42 | 43 | def rtm_read(self): 44 | # in the future, this should handle some events internally i.e. channel 45 | # creation 46 | if self.server: 47 | json_data = self.server.websocket_safe_read() 48 | data = [] 49 | if json_data != '': 50 | for d in json_data.split('\n'): 51 | data.append(json.loads(d)) 52 | for item in data: 53 | self.process_changes(item) 54 | return data 55 | else: 56 | raise SlackNotConnected 57 | 58 | def rtm_send_message(self, channel, message): 59 | return self.server.channels.find(channel).send_message(message) 60 | 61 | def process_changes(self, data): 62 | if "type" in data.keys(): 63 | if data["type"] in ('channel_created', 'group_joined'): 64 | channel = data["channel"] 65 | self.server.attach_channel(channel["name"], channel["id"], []) 66 | if data["type"] == 'im_created': 67 | channel = data["channel"] 68 | self.server.attach_channel(channel["user"], channel["id"], []) 69 | if data["type"] == "team_join": 70 | user = data["user"] 71 | self.server.parse_user_data([user]) 72 | pass 73 | 74 | 75 | class SlackNotConnected(Exception): 76 | pass 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-slackclient 2 | ================ 3 | 4 | [![Build Status](https://travis-ci.org/slackhq/python-slackclient.svg?branch=master)](https://travis-ci.org/slackhq/python-slackclient) 5 | [![Coverage Status](https://coveralls.io/repos/github/slackhq/python-slackclient/badge.svg?branch=master)](https://coveralls.io/github/slackhq/python-slackclient?branch=master) 6 | 7 | A basic client for Slack.com, which can optionally connect to the Slack Real Time Messaging (RTM) API. 8 | 9 | Overview 10 | --------- 11 | This plugin is a light wrapper around the [Slack API](https://api.slack.com/). In its basic form, it can be used to call any API method and be expected to return a dict of the JSON reply. 12 | 13 | The optional RTM connection allows you to create a persistent websocket connection, from which you can read events just like an official Slack client. This allows you to respond to events in real time without polling and send messages without making a full HTTPS request. 14 | 15 | See [python-rtmbot](https://github.com/slackhq/python-rtmbot/) for an active project utilizing this library. 16 | 17 | Installation 18 | ---------- 19 | 20 | #### Automatic w/ PyPI ([virtualenv](http://virtualenv.readthedocs.org/en/latest/) is recommended.) 21 | 22 | pip install slackclient 23 | 24 | #### Manual 25 | 26 | git clone https://github.com/slackhq/python-slackclient.git 27 | pip install -r requirements.txt 28 | 29 | Usage 30 | ----- 31 | See examples in [doc/examples](doc/examples/) 32 | 33 | _Note:_ You must obtain a token for the user/bot. You can find or generate these at the [Slack API](https://api.slack.com/web) page. 34 | 35 | ###Basic API methods 36 | 37 | ```python 38 | from slackclient import SlackClient 39 | 40 | token = "xoxp-28192348123947234198234" # found at https://api.slack.com/web#authentication 41 | sc = SlackClient(token) 42 | print sc.api_call("api.test") 43 | print sc.api_call("channels.info", channel="1234567890") 44 | print sc.api_call( 45 | "chat.postMessage", channel="#general", text="Hello from Python! :tada:", 46 | username='pybot', icon_emoji=':robot_face:', 47 | attachments=[{'title': 'This is an attachment', 'color': 'good'}] 48 | ) 49 | ``` 50 | 51 | ### Real Time Messaging 52 | --------- 53 | ```python 54 | import time 55 | from slackclient import SlackClient 56 | 57 | token = "xoxp-28192348123947234198234"# found at https://api.slack.com/web#authentication 58 | sc = SlackClient(token) 59 | if sc.rtm_connect(): 60 | while True: 61 | print sc.rtm_read() 62 | time.sleep(1) 63 | else: 64 | print "Connection Failed, invalid token?" 65 | ``` 66 | 67 | ####Objects 68 | ----------- 69 | 70 | [SlackClient.**server**] 71 | Server object owns the websocket and all nested channel information. 72 | 73 | [SlackClient.server.**channels**] 74 | A searchable list of all known channels within the parent server. Call `print (sc instance)` to see the entire list. 75 | 76 | ####Methods 77 | ----------- 78 | 79 | | Method | Description | 80 | | ----- | ----- | 81 | | SlackClient.**rtm_connect()** | Connect to a Slack RTM websocket. This is a persistent connection from which you can read events. | 82 | | SlackClient.**rtm_read()** | Read all data from the RTM websocket. Multiple events may be returned, always returns a list [], which is empty if there are no incoming messages. | 83 | | SlackClient.**rtm_send_message([channel, message])** | Sends the text in [message] to [channel], which can be a name or identifier i.e. "#general" or "C182391" | 84 | | SlackClient.**api_call([method])** | Call the Slack method [method]. Arguments can be passed as kwargs, for instance: sc.api_call('users.info', user='U0L85V3B4')_ | 85 | | SlackClient.server.**send_to_websocket([data])** | Send a JSON message directly to the websocket. See RTM documentation for allowed types.| 86 | | SlackClient.server.**channels.find([identifier])** | The identifier can be either name or Slack channel ID. See above for examples. | 87 | | SlackClient.server.**channels[int].send_message([text])** | Send message [text] to [int] channel in the channels list. | 88 | | SlackClient.server.**channels.find([identifier]).send_message([text])** | Send message [text] to channel [identifier], which can be either channel name or ID. Ex "#general" or "C182391" | 89 | -------------------------------------------------------------------------------- /slackclient/_server.py: -------------------------------------------------------------------------------- 1 | from slackclient._slackrequest import SlackRequest 2 | from slackclient._channel import Channel 3 | from slackclient._user import User 4 | from slackclient._util import SearchList 5 | from ssl import SSLError 6 | 7 | from websocket import create_connection 8 | import json 9 | 10 | 11 | class Server(object): 12 | def __init__(self, token, connect=True): 13 | self.token = token 14 | self.username = None 15 | self.domain = None 16 | self.login_data = None 17 | self.websocket = None 18 | self.users = SearchList() 19 | self.channels = SearchList() 20 | self.connected = False 21 | self.pingcounter = 0 22 | self.ws_url = None 23 | self.api_requester = SlackRequest() 24 | 25 | if connect: 26 | self.rtm_connect() 27 | 28 | def __eq__(self, compare_str): 29 | if compare_str == self.domain or compare_str == self.token: 30 | return True 31 | else: 32 | return False 33 | 34 | def __hash__(self): 35 | return hash(self.token) 36 | 37 | def __str__(self): 38 | data = "" 39 | for key in list(self.__dict__.keys()): 40 | data += "{} : {}\n".format(key, str(self.__dict__[key])[:40]) 41 | return data 42 | 43 | def __repr__(self): 44 | return self.__str__() 45 | 46 | def rtm_connect(self, reconnect=False): 47 | reply = self.api_requester.do(self.token, "rtm.start") 48 | if reply.status_code != 200: 49 | raise SlackConnectionError 50 | else: 51 | login_data = reply.json() 52 | if login_data["ok"]: 53 | self.ws_url = login_data['url'] 54 | if not reconnect: 55 | self.parse_slack_login_data(login_data) 56 | self.connect_slack_websocket(self.ws_url) 57 | else: 58 | raise SlackLoginError 59 | 60 | def parse_slack_login_data(self, login_data): 61 | self.login_data = login_data 62 | self.domain = self.login_data["team"]["domain"] 63 | self.username = self.login_data["self"]["name"] 64 | self.parse_channel_data(login_data["channels"]) 65 | self.parse_channel_data(login_data["groups"]) 66 | self.parse_channel_data(login_data["ims"]) 67 | self.parse_user_data(login_data["users"]) 68 | 69 | def connect_slack_websocket(self, ws_url): 70 | try: 71 | self.websocket = create_connection(ws_url) 72 | self.websocket.sock.setblocking(0) 73 | except: 74 | raise SlackConnectionError 75 | 76 | def parse_channel_data(self, channel_data): 77 | for channel in channel_data: 78 | if "name" not in channel: 79 | channel["name"] = channel["id"] 80 | if "members" not in channel: 81 | channel["members"] = [] 82 | self.attach_channel(channel["name"], 83 | channel["id"], 84 | channel["members"]) 85 | 86 | def parse_user_data(self, user_data): 87 | for user in user_data: 88 | if "tz" not in user: 89 | user["tz"] = "unknown" 90 | if "real_name" not in user: 91 | user["real_name"] = user["name"] 92 | self.attach_user(user["name"], user["id"], user["real_name"], user["tz"]) 93 | 94 | def send_to_websocket(self, data): 95 | """Send (data) directly to the websocket.""" 96 | try: 97 | data = json.dumps(data) 98 | self.websocket.send(data) 99 | except: 100 | self.rtm_connect(reconnect=True) 101 | 102 | def ping(self): 103 | return self.send_to_websocket({"type": "ping"}) 104 | 105 | def websocket_safe_read(self): 106 | """ Returns data if available, otherwise ''. Newlines indicate multiple 107 | messages 108 | """ 109 | 110 | data = "" 111 | while True: 112 | try: 113 | data += "{0}\n".format(self.websocket.recv()) 114 | except SSLError as e: 115 | if e.errno == 2: 116 | # errno 2 occurs when trying to read or write data, but more 117 | # data needs to be received on the underlying TCP transport 118 | # before the request can be fulfilled. 119 | # 120 | # Python 2.7.9+ and Python 3.3+ give this its own exception, 121 | # SSLWantReadError 122 | return '' 123 | raise 124 | return data.rstrip() 125 | 126 | def attach_user(self, name, channel_id, real_name, tz): 127 | if self.users.find(channel_id) is None: 128 | self.users.append(User(self, name, channel_id, real_name, tz)) 129 | 130 | def attach_channel(self, name, channel_id, members=None): 131 | if members is None: 132 | members = [] 133 | if self.channels.find(channel_id) is None: 134 | self.channels.append(Channel(self, name, channel_id, members)) 135 | 136 | def join_channel(self, name): 137 | print(self.api_requester.do(self.token, 138 | "channels.join?name={}".format(name)).text) 139 | 140 | def api_call(self, method, **kwargs): 141 | return self.api_requester.do(self.token, method, kwargs).text 142 | 143 | 144 | class SlackConnectionError(Exception): 145 | pass 146 | 147 | 148 | class SlackLoginError(Exception): 149 | pass 150 | -------------------------------------------------------------------------------- /tests/data/rtm.start.json: -------------------------------------------------------------------------------- 1 | { 2 | "ok": true, 3 | "self": { 4 | "id": "U10CX1234", 5 | "name": "fakeuser", 6 | "prefs": { 7 | "highlight_words": "", 8 | "user_colors": "", 9 | "color_names_in_list": true, 10 | "growls_enabled": true, 11 | "tz": "America\/Los_Angeles", 12 | "push_dm_alert": true, 13 | "push_mention_alert": true, 14 | "push_everything": true, 15 | "push_idle_wait": 2, 16 | "push_sound": "b2.mp3", 17 | "push_loud_channels": "", 18 | "push_mention_channels": "", 19 | "push_loud_channels_set": "", 20 | "email_alerts": "instant", 21 | "email_alerts_sleep_until": 0, 22 | "email_misc": true, 23 | "email_weekly": true, 24 | "welcome_message_hidden": false, 25 | "all_channels_loud": true, 26 | "loud_channels": "", 27 | "never_channels": "", 28 | "loud_channels_set": "", 29 | "show_member_presence": true, 30 | "search_sort": "timestamp", 31 | "expand_inline_imgs": true, 32 | "expand_internal_inline_imgs": true, 33 | "expand_snippets": false, 34 | "posts_formatting_guide": true, 35 | "seen_welcome_2": true, 36 | "seen_ssb_prompt": false, 37 | "search_only_my_channels": false, 38 | "emoji_mode": "default", 39 | "has_invited": false, 40 | "has_uploaded": false, 41 | "has_created_channel": false, 42 | "search_exclude_channels": "", 43 | "messages_theme": "default", 44 | "webapp_spellcheck": true, 45 | "no_joined_overlays": false, 46 | "no_created_overlays": false, 47 | "dropbox_enabled": false, 48 | "seen_user_menu_tip_card": true, 49 | "seen_team_menu_tip_card": true, 50 | "seen_channel_menu_tip_card": true, 51 | "seen_message_input_tip_card": true, 52 | "seen_channels_tip_card": true, 53 | "seen_domain_invite_reminder": false, 54 | "seen_member_invite_reminder": false, 55 | "seen_flexpane_tip_card": true, 56 | "seen_search_input_tip_card": true, 57 | "mute_sounds": false, 58 | "arrow_history": false, 59 | "tab_ui_return_selects": true, 60 | "obey_inline_img_limit": true, 61 | "new_msg_snd": "knock_brush.mp3", 62 | "collapsible": false, 63 | "collapsible_by_click": true, 64 | "require_at": false, 65 | "mac_ssb_bounce": "", 66 | "mac_ssb_bullet": true, 67 | "expand_non_media_attachments": true, 68 | "show_typing": true, 69 | "pagekeys_handled": true, 70 | "last_snippet_type": "", 71 | "display_real_names_override": 0, 72 | "time24": false, 73 | "enter_is_special_in_tbt": false, 74 | "graphic_emoticons": false, 75 | "convert_emoticons": true, 76 | "autoplay_chat_sounds": true, 77 | "ss_emojis": true, 78 | "sidebar_behavior": "", 79 | "mark_msgs_read_immediately": true, 80 | "start_scroll_at_oldest": true, 81 | "snippet_editor_wrap_long_lines": false, 82 | "ls_disabled": false, 83 | "sidebar_theme": "default", 84 | "sidebar_theme_custom_values": "", 85 | "f_key_search": false, 86 | "k_key_omnibox": true, 87 | "speak_growls": false, 88 | "mac_speak_voice": "com.apple.speech.synthesis.voice.Alex", 89 | "mac_speak_speed": 250, 90 | "comma_key_prefs": false, 91 | "at_channel_suppressed_channels": "", 92 | "push_at_channel_suppressed_channels": "", 93 | "prompted_for_email_disabling": false, 94 | "full_text_extracts": false, 95 | "no_text_in_notifications": false, 96 | "muted_channels": "", 97 | "no_macssb1_banner": true, 98 | "no_winssb1_banner": false, 99 | "privacy_policy_seen": true, 100 | "search_exclude_bots": false, 101 | "fuzzy_matching": false, 102 | "load_lato_2": false, 103 | "fuller_timestamps": false, 104 | "last_seen_at_channel_warning": 0, 105 | "enable_flexpane_rework": false, 106 | "flex_resize_window": false, 107 | "msg_preview": false, 108 | "msg_preview_displaces": true, 109 | "msg_preview_persistent": true, 110 | "emoji_autocomplete_big": false, 111 | "winssb_run_from_tray": true 112 | }, 113 | "created": 1421456475, 114 | "manual_presence": "active" 115 | }, 116 | "team": { 117 | "id": "T03CX4S34", 118 | "name": "TESTteam, INC", 119 | "email_domain": "", 120 | "domain": "testteaminc", 121 | "msg_edit_window_mins": -1, 122 | "prefs": { 123 | "default_channels": [ 124 | "C01CX1234", 125 | "C05BX1234" 126 | ], 127 | "msg_edit_window_mins": -1, 128 | "allow_message_deletion": true, 129 | "hide_referers": true, 130 | "display_real_names": false, 131 | "who_can_at_everyone": "regular", 132 | "who_can_at_channel": "ra", 133 | "warn_before_at_channel": "always", 134 | "who_can_create_channels": "regular", 135 | "who_can_archive_channels": "regular", 136 | "who_can_create_groups": "ra", 137 | "who_can_post_general": "ra", 138 | "who_can_kick_channels": "admin", 139 | "who_can_kick_groups": "regular", 140 | "retention_type": 0, 141 | "retention_duration": 0, 142 | "group_retention_type": 0, 143 | "group_retention_duration": 0, 144 | "dm_retention_type": 0, 145 | "dm_retention_duration": 0, 146 | "require_at_for_mention": 0, 147 | "compliance_export_start": 0 148 | }, 149 | "icon": { 150 | "image_34": "https:\/\/slack.global.ssl.fastly.net\/b3b7\/img\/avatars-teams\/ava_0025-34.png", 151 | "image_44": "https:\/\/slack.global.ssl.fastly.net\/b3b7\/img\/avatars-teams\/ava_0025-44.png", 152 | "image_68": "https:\/\/slack.global.ssl.fastly.net\/b3b7\/img\/avatars-teams\/ava_0025-68.png", 153 | "image_88": "https:\/\/slack.global.ssl.fastly.net\/b3b7\/img\/avatars-teams\/ava_0025-88.png", 154 | "image_102": "https:\/\/slack.global.ssl.fastly.net\/b3b7\/img\/avatars-teams\/ava_0025-102.png", 155 | "image_132": "https:\/\/slack.global.ssl.fastly.net\/b3b7\/img\/avatars-teams\/ava_0025-132.png", 156 | "image_default": true 157 | }, 158 | "over_storage_limit": false 159 | }, 160 | "latest_event_ts": "1426103085.000000", 161 | "channels": [ 162 | { 163 | "id": "C01CX1234", 164 | "name": "general", 165 | "is_channel": true, 166 | "created": 1421456475, 167 | "creator": "U03CX4S38", 168 | "is_archived": false, 169 | "is_general": true, 170 | "is_member": true, 171 | "last_read": "0000000000.000000", 172 | "latest": { 173 | "type": "message", 174 | "user": "U03CX4S38", 175 | "text": "a", 176 | "ts": "1425499421.000004" 177 | }, 178 | "unread_count": 0, 179 | "unread_count_display": 0, 180 | "members": [ 181 | "U03CX4S38" 182 | ], 183 | "topic": { 184 | "value": "", 185 | "creator": "", 186 | "last_set": 0 187 | }, 188 | "purpose": { 189 | "value": "This channel is for team-wide communication and announcements. All team members are in this channel.", 190 | "creator": "", 191 | "last_set": 0 192 | } 193 | }, 194 | { 195 | "id": "C05BX1234", 196 | "name": "random", 197 | "is_channel": true, 198 | "created": 1421456475, 199 | "creator": "U03CX4S38", 200 | "is_archived": false, 201 | "is_general": false, 202 | "is_member": true, 203 | "last_read": "0000000000.000000", 204 | "latest": null, 205 | "unread_count": 0, 206 | "unread_count_display": 0, 207 | "members": [ 208 | "U03CX4S38" 209 | ], 210 | "topic": { 211 | "value": "", 212 | "creator": "", 213 | "last_set": 0 214 | }, 215 | "purpose": { 216 | "value": "A place for non-work banter, links, articles of interest, humor or anything else which you'd like concentrated in some place other than work-related channels.", 217 | "creator": "", 218 | "last_set": 0 219 | } 220 | } 221 | ], 222 | "groups": [], 223 | "ims": [ 224 | { 225 | "id": "D03CX4S3E", 226 | "is_im": true, 227 | "user": "USLACKBOT", 228 | "created": 1421456475, 229 | "last_read": "1425318850.000003", 230 | "latest": { 231 | "type": "message", 232 | "user": "USLACKBOT", 233 | "text": "To start, what is your first name?", 234 | "ts": "1425318850.000003" 235 | }, 236 | "unread_count": 0, 237 | "unread_count_display": 0, 238 | "is_open": true 239 | } 240 | ], 241 | "users": [ 242 | { 243 | "id": "U10CX1234", 244 | "name": "fakeuser", 245 | "deleted": false, 246 | "status": null, 247 | "color": "9f69e7", 248 | "real_name": "", 249 | "tz": "America\/Los_Angeles", 250 | "tz_label": "Pacific Daylight Time", 251 | "tz_offset": -25200, 252 | "profile": { 253 | "real_name": "", 254 | "real_name_normalized": "", 255 | "email": "fakeuser@example.com", 256 | "image_24": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=24&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-24.png", 257 | "image_32": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=32&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-32.png", 258 | "image_48": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=48&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F272a%2Fimg%2Favatars%2Fava_0002-48.png", 259 | "image_72": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=72&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-72.png", 260 | "image_192": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=192&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002.png" 261 | }, 262 | "is_admin": true, 263 | "is_owner": true, 264 | "is_primary_owner": true, 265 | "is_restricted": false, 266 | "is_ultra_restricted": false, 267 | "is_bot": false, 268 | "has_files": false, 269 | "presence": "away" 270 | }, 271 | { 272 | "id": "USLACKBOT", 273 | "name": "slackbot", 274 | "deleted": false, 275 | "status": null, 276 | "color": "757575", 277 | "real_name": "Slack Bot", 278 | "tz": null, 279 | "tz_label": "Pacific Daylight Time", 280 | "tz_offset": -25200, 281 | "profile": { 282 | "first_name": "Slack", 283 | "last_name": "Bot", 284 | "image_24": "https:\/\/slack-assets2.s3-us-west-2.amazonaws.com\/10068\/img\/slackbot_24.png", 285 | "image_32": "https:\/\/slack-assets2.s3-us-west-2.amazonaws.com\/10068\/img\/slackbot_32.png", 286 | "image_48": "https:\/\/slack-assets2.s3-us-west-2.amazonaws.com\/10068\/img\/slackbot_48.png", 287 | "image_72": "https:\/\/slack-assets2.s3-us-west-2.amazonaws.com\/10068\/img\/slackbot_72.png", 288 | "image_192": "https:\/\/slack-assets2.s3-us-west-2.amazonaws.com\/10068\/img\/slackbot_192.png", 289 | "real_name": "Slack Bot", 290 | "real_name_normalized": "Slack Bot", 291 | "email": null 292 | }, 293 | "is_admin": false, 294 | "is_owner": false, 295 | "is_primary_owner": false, 296 | "is_restricted": false, 297 | "is_ultra_restricted": false, 298 | "is_bot": false, 299 | "presence": "active" 300 | } 301 | ], 302 | "bots": [], 303 | "cache_version": "v5-dog", 304 | "url": "wss:\/\/ms9999.slack-msgs.com\/websocket\/rvyiQ_oxNhQ2C6_613rtqs1PFfT0AmivZTokv\/VOVQCmq3bk\/KarC2Z2ZMFfdMMtxn4kx9ILl6sE7JgvKv6Bct5okT0Lgru416DXsKJolJQ=" 305 | } 306 | --------------------------------------------------------------------------------