class SlackHandler(StreamHandler): """Slack log handler Class Inherits: logging.StreamHandler: Base log StreamHandler class. """ def __init__(self, channel: str, slack_bot_token: str = None, username: str = "Gytrash"): """Initialize the stream handler with some specifics for slack. Args: channel (str): Slack channel to publish logs slack_bot_token (str, optional): Slack bot token to use the slack published app. Defaults to None. username (str, optional): Username of the Slack bot. Defaults to "Gytrash". """ StreamHandler.__init__(self) # Initialize a Web API client if slack_bot_token: self.slack_web_client = WebClient(token=slack_bot_token) else: self.slack_web_client = WebClient( token=os.environ["SLACK_BOT_TOKEN"]) self.channel = channel self.username = username def _send_log(self, message: dict): """Posts the formatted message to slack via the web client. Args: message (dict): Slack message dictionary. Follows the blocks API. """ self.slack_web_client.chat_postMessage(**message) def emit(self, message: "logging.LogRecord"): """Emits a message from the handler. Args: message (logging.LogRecord): Log record from the stream. """ assert isinstance(message, logging.LogRecord) slack_message = self.format(message) # List of LogRecord attributes expected when reading the # documentation of the logging module: expected_attributes = ( "args,asctime,created,exc_info,filename,funcName,levelname," "levelno,lineno,module,msecs,message,msg,name,pathname," "process,processName,relativeCreated,stack_info,thread,threadName") for ea in expected_attributes.split(","): if not hasattr(message, ea): print("UNEXPECTED: LogRecord does not have the '{}' field!". format(ea)) slack_message["channel"] = self.channel slack_message["username"] = self.username self._send_log(slack_message)
def _send_to_listeners(self, message): channels = SlackBotChannel.objects.select_related('bot').filter( bot__is_active=True) for channel in channels: client = WebClient(token=channel.bot.bot_token) client.chat_postMessage(channel=channel.channel_id, attachments=message)
def test_if_it_uses_custom_logger(self): logger = CustomLogger("test-logger") client = WebClient( base_url="http://localhost:8888", token="xoxb-api_test", logger=logger, ) client.chat_postMessage(channel="C111", text="hello") self.assertTrue(logger.called)
def test_blocks_without_text_arg(self): client = WebClient( base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111" ) with self.assertWarns(UserWarning): resp = client.chat_postMessage(channel="C111", blocks=[]) self.assertTrue(resp["ok"])
def test_attachments_without_fallback(self): client = WebClient( base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111" ) with self.assertWarns(UserWarning): resp = client.chat_postMessage(channel="C111", attachments=[{}]) self.assertTrue(resp["ok"])
def test_blocks_as_deserialzed_json_without_text_arg(self): client = WebClient( base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111" ) # this generates a warning because "text" is missing with self.assertWarns(UserWarning): resp = client.chat_postMessage(channel="C111", attachments=json.dumps([])) self.assertTrue(resp["ok"])
def test_blocks_as_deserialized_json_with_text_arg(self): client = WebClient( base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111" ) # this DOESN'T warn because the "text" arg is present resp = client.chat_postMessage( channel="C111", text="test", blocks=json.dumps([]) ) self.assertTrue(resp["ok"])
def per_request(): try: client = WebClient(token=os.environ["SLACK_BOT_TOKEN"], run_async=False) response = client.chat_postMessage( channel="#random", text="You used a new WebClient for posting this message!" ) return str(response) except SlackApiError as e: return make_response(str(e), 400)
def test_attachments_without_fallback_with_text_arg(self): client = WebClient( base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111" ) # this warns because each attachment should have its own fallback, even with "text" with self.assertWarns(UserWarning): resp = client.chat_postMessage( channel="C111", text="test", attachments=[{}] ) self.assertTrue(resp["ok"])
def test_triggers_replies_in_a_thread(self): client = WebClient() client.chat_postMessage = MagicMock() user = "******" channel = "channel" trigger_word = "help me" subject = LoggingBot(client, [trigger_word], [channel], [], [], []) handled = subject.handle_message(channel, user, trigger_word, ts="ts") self.assertTrue(handled) self.assertNotEqual( "", client.chat_postMessage.call_args.kwargs.get("thread_ts", ""))
def test_ignores_unmonitored_channels(self): client = WebClient() client.chat_postMessage = MagicMock() subject = LoggingBot(client, [], ["MONITORED"], [], [], []) handled = subject.handle_message("NOT_MONITORED", "user", "text", bot_profile="bot_profile") self.assertFalse(handled) client.chat_postMessage.assert_not_called()
def test_attachments_as_deserialzed_json_without_text_arg(self): client = WebClient( base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111" ) # this still generates a warning because "text" is missing. The attachment has already # been deserialized, which isn't explicitly prohibited in the docs (but isn't recommended) with self.assertWarns(UserWarning): resp = client.chat_postMessage( channel="C111", attachments=json.dumps([{"fallback": "test"}]) ) self.assertTrue(resp["ok"])
def test_triggers_does_not_trigger_in_threads(self): client = WebClient() client.chat_postMessage = MagicMock() user = "******" channel = "channel" trigger_word = "help me" subject = LoggingBot(client, [trigger_word], [channel], [], [], []) handled = subject.handle_message(channel, user, trigger_word, thread_ts="ts") self.assertFalse(handled) client.chat_postMessage.assert_not_called()
def test_ignores_bot_messages(self): client = WebClient() client.chat_postMessage = MagicMock() logging.debug = MagicMock() subject = LoggingBot(client, [], [], [], [], []) handled = subject.handle_message("channel", "user", "text", bot_profile="bot_profile") self.assertFalse(handled) client.chat_postMessage.assert_not_called() logging.debug.assert_called_with(AnyStringWith("bot"))
def test_ignores_admin_messages(self): client = WebClient() client.chat_postMessage = MagicMock() logging.debug = MagicMock() user = "******" trigger_word = "triggered!" channel = "channel_8" subject = LoggingBot(client, [trigger_word], [channel], [], [user], []) handled = subject.handle_message(channel, user, "text") self.assertFalse(handled) client.chat_postMessage.assert_not_called() logging.debug.assert_called_with(AnyStringWith("admin"))
class TestRTMClient(unittest.TestCase): """Runs integration tests with real Slack API https://github.com/slackapi/python-slack-sdk/issues/605 """ def setUp(self): self.logger = logging.getLogger(__name__) self.bot_token = os.environ[SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN] self.channel_id = os.environ[SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID] self.rtm_client = RTMClient(token=self.bot_token, run_async=False) def tearDown(self): # Reset the decorators by @RTMClient.run_on RTMClient._callbacks = collections.defaultdict(list) @pytest.mark.skipif(condition=is_not_specified(), reason="To avoid rate_limited errors") def test_issue_605(self): self.text = "This message was sent to verify issue #605" self.called = False @RTMClient.run_on(event="message") def process_messages(**payload): self.logger.info(payload) self.called = True def connect(): self.logger.debug("Starting RTM Client...") self.rtm_client.start() t = threading.Thread(target=connect) t.daemon = True try: t.start() self.assertFalse(self.called) time.sleep(3) self.web_client = WebClient( token=self.bot_token, run_async=False, ) new_message = self.web_client.chat_postMessage( channel=self.channel_id, text=self.text) self.assertFalse("error" in new_message) time.sleep(5) self.assertTrue(self.called) finally: t.join(0.3)
def send_slack_message(message, channel): """ Use slack api to send a message to the desired channel """ slack_web_client = WebClient(token=settings.SLACK_SDK_OAUTH_TOKEN) response = slack_web_client.chat_postMessage( channel=channel, username=settings.SLACK_USERNAME, icon_emoji=':robot_face:', text=message, ) if not response.status_code == 200 or response.data.get('ok') is not True: # Here there should be code to manage errors, like logs, etc. pass return response
def test_triggers_trigger_words(self): client = WebClient() client.chat_postMessage = MagicMock() user = "******" channel = "channel" trigger_words = ["help me"] for msg, should_handle in [("good morning", False), ("help", False), ("help me", True)]: subject = LoggingBot(client, trigger_words, [channel], [], [], []) handled = subject.handle_message(channel, user, msg, ts="ts") self.assertEqual(should_handle, handled, msg="Message=%s" % msg) if handled: self.assertEqual( user, client.chat_postMessage.call_args.kwargs.get("user"))
def per_request_async(): try: # This is not optimal and the host should have a large number of FD (File Descriptor) loop_for_this_request = asyncio.new_event_loop() async_client = WebClient( token=os.environ["SLACK_BOT_TOKEN"], run_async=True, loop=loop_for_this_request, ) future = async_client.chat_postMessage( channel="#random", text="You used the singleton WebClient for posting this message!", ) response = loop_for_this_request.run_until_complete(future) return str(response) except SlackApiError as e: return make_response(str(e), 400)
def start_onboarding(user_id: str, channel: str, client: WebClient): # Create a new onboarding tutorial. onboarding_tutorial = OnboardingTutorial(channel) # Get the onboarding message payload message = onboarding_tutorial.get_message_payload() # Post the onboarding message in Slack response = client.chat_postMessage(**message) # Capture the timestamp of the message we've just posted so # we can use it to update the message after a user # has completed an onboarding task. onboarding_tutorial.timestamp = response["ts"] # Store the message sent in onboarding_tutorials_sent if channel not in onboarding_tutorials_sent: onboarding_tutorials_sent[channel] = {} onboarding_tutorials_sent[channel][user_id] = onboarding_tutorial
def test_executes_commands_in_channels(self): client = WebClient() client.chat_postMessage = MagicMock() channel = "channel" subject = LoggingBot(client, [], [channel], [], [], []) # choose the first command cmd = list(consts.COMMANDS.keys())[0] user = "******" handled = subject.handle_message(channel, user, cmd, ts="ts") self.assertTrue(handled) # because we did not supply a thread_ts to handle_message that # indicates the message was NOT within an existing thread so we should # not see a thread_ts (which means post in the original channel) thread_ts = client.chat_postMessage.call_args.kwargs.get("thread_ts") self.assertFalse(thread_ts)
class SlackService: def __init__(self): self.slack_bot_token = get_env('SLACK_BOT_TOKEN') self.slack_signing_secret = get_env('SLACK_SIGNING_SECRET') self.slack_web = WebClient(token=self.slack_bot_token) def start_event_service(self, flask_app): slack_event = SlackEventAdapter(self.slack_signing_secret, "/slack/events", flask_app) def post_notification(self, ticket_data): block_types = SlackBlockTypes() method = ticket_data["method"] # Cycle through possible notification methods # Send JSON payload respective to the notification method if method == "CREATE_TICKET": blocks = block_types.create_ticket_block(ticket_data) message = ticket_data[ "username"] + " Created a Ticket :spiral_note_pad:" elif method == "PICKUP_TICKET": blocks = block_types.pickup_ticket_block(ticket_data) message = ":heavy_check_mark: Ticket picked up by " + ticket_data[ "username"] elif method == "ASSIGN_TICKET": blocks = block_types.assigned_ticket_block(ticket_data) message = ":heavy_check_mark: " + ticket_data[ "username"] + " has been assigned to your ticket!" elif method == "STATUS_UPDATE": blocks = block_types.field_update_block(ticket_data, "status") message = ":heavy_check_mark: Ticket status updated in the UHDA!" elif method == "PRIORITY_UPDATE": blocks = block_types.field_update_block(ticket_data, "priority") message = ":heavy_check_mark: Ticket priority updated in the UHDA!" else: blocks = block_types.error_ticket_block() message = " :four: :zero: :four: " return self.slack_web.chat_postMessage(channel=ticket_data["channel"], text=message, blocks=blocks)
def test_executes_commands_in_threads(self): client = WebClient() client.chat_postMessage = MagicMock() channel = "channel" subject = LoggingBot(client, [], [channel], [], [], []) # choose the first command cmd = list(consts.COMMANDS.keys())[0] user = "******" handled = subject.handle_message(channel, user, cmd, ts="ts", thread_ts="thread_ts") self.assertTrue(handled) # because we supplied a thread_ts to handle_message that indicates the # message was within an existing thread so we should see thread_ts # here. thread_ts = client.chat_postMessage.call_args.kwargs.get( "thread_ts", "") self.assertEqual("thread_ts", thread_ts)
def test_executes_commands_for_admins(self): client = WebClient() client.chat_postMessage = MagicMock() channel = "channel" user = "******" subject = LoggingBot(client, [], [channel], [], [user], []) # choose the first command cmd = list(consts.COMMANDS.keys())[0] handled = subject.handle_message(channel, user, cmd, ts="ts", thread_ts="thread_ts") self.assertTrue(handled, "handled message") args = client.chat_postMessage.call_args.kwargs block = consts.COMMANDS[cmd].items() # block is the message text that gets added when the command is called for key, value in block: self.assertIn(key, args, "key=%s" % key) self.assertEqual(value, args[key], "key=%s value=%s" % (key, value))
# ------------------ # Only for running this script here import sys from os.path import dirname sys.path.insert(1, f"{dirname(__file__)}/../../..") # ------------------ import logging logging.basicConfig(level=logging.DEBUG) # export SLACK_API_TOKEN=xoxb-*** # python3 integration_tests/samples/readme/sending_messages.py import os from slack_sdk.web import WebClient from slack_sdk.errors import SlackApiError client = WebClient(token=os.environ["SLACK_API_TOKEN"]) try: response = client.chat_postMessage(channel="#random", text="Hello world!") assert response["message"]["text"] == "Hello world!" except SlackApiError as e: # You will get a SlackApiError if "ok" is False assert e.response["ok"] is False assert e.response["error"] # str like 'invalid_auth', 'channel_not_found' print(f"Got an error: {e.response['error']}")
class TestRTMClient(unittest.TestCase): """Runs integration tests with real Slack API https://github.com/slackapi/python-slack-sdk/issues/701 """ def setUp(self): self.logger = logging.getLogger(__name__) self.bot_token = os.environ[SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN] def tearDown(self): # Reset the decorators by @RTMClient.run_on RTMClient._callbacks = collections.defaultdict(list) # @pytest.mark.skipif(condition=is_not_specified(), reason="to avoid rate_limited errors") @pytest.mark.skip() def test_receiving_all_messages(self): self.rtm_client = RTMClient(token=self.bot_token, loop=asyncio.new_event_loop()) self.web_client = WebClient(token=self.bot_token) self.call_count = 0 @RTMClient.run_on(event="message") def send_reply(**payload): self.logger.debug(payload) web_client, data = payload["web_client"], payload["data"] web_client.reactions_add(channel=data["channel"], timestamp=data["ts"], name="eyes") self.call_count += 1 def connect(): self.logger.debug("Starting RTM Client...") self.rtm_client.start() rtm = threading.Thread(target=connect) rtm.daemon = True rtm.start() time.sleep(3) total_num = 10 sender_completion = [] def sent_bulk_message(): for i in range(total_num): text = f"Sent by <https://slack.dev/python-slackclient/|python-slackclient>! ({i})" self.web_client.chat_postMessage(channel="#random", text=text) time.sleep(0.1) sender_completion.append(True) num_of_senders = 3 senders = [] for sender_num in range(num_of_senders): sender = threading.Thread(target=sent_bulk_message) sender.daemon = True sender.start() senders.append(sender) while len(sender_completion) < num_of_senders: time.sleep(1) expected_call_count = total_num * num_of_senders wait_seconds = 0 max_wait = 20 while self.call_count < expected_call_count and wait_seconds < max_wait: time.sleep(1) wait_seconds += 1 self.assertEqual(total_num * num_of_senders, self.call_count, "The RTM handler failed") @pytest.mark.skipif(condition=is_not_specified(), reason="to avoid rate_limited errors") @async_test async def test_receiving_all_messages_async(self): self.rtm_client = RTMClient(token=self.bot_token, run_async=True) self.web_client = WebClient(token=self.bot_token, run_async=False) self.call_count = 0 @RTMClient.run_on(event="message") async def send_reply(**payload): self.logger.debug(payload) web_client, data = payload["web_client"], payload["data"] await web_client.reactions_add(channel=data["channel"], timestamp=data["ts"], name="eyes") self.call_count += 1 # intentionally not waiting here self.rtm_client.start() await asyncio.sleep(3) total_num = 10 sender_completion = [] def sent_bulk_message(): for i in range(total_num): text = f"Sent by <https://slack.dev/python-slackclient/|python-slackclient>! ({i})" self.web_client.chat_postMessage(channel="#random", text=text) time.sleep(0.1) sender_completion.append(True) num_of_senders = 3 senders = [] for sender_num in range(num_of_senders): sender = threading.Thread(target=sent_bulk_message) sender.daemon = True sender.start() senders.append(sender) while len(sender_completion) < num_of_senders: await asyncio.sleep(1) expected_call_count = total_num * num_of_senders wait_seconds = 0 max_wait = 20 while self.call_count < expected_call_count and wait_seconds < max_wait: await asyncio.sleep(1) wait_seconds += 1 self.assertEqual(total_num * num_of_senders, self.call_count, "The RTM handler failed")
# Post the onboarding message in Slack return local_message def _flip_coin(): """Flip a single coin""" rand_int = random.randint(0, 1) if rand_int == 0: results = "Heads" else: results = "Tails" text = f"The result is {results}" return text if __name__ == "__main__": from slack_sdk.web import WebClient # Create a slack client slack_web_client = WebClient(token=os.environ.get("SLACK_TOKEN")) # Get a new CoinBot # coin_bot = CoinBot("#testing") message = flip_coin("U021L1T8MAT") # Post the onboarding message in slack slack_web_client.chat_postMessage(**message)
class SlackBot(): def __init__(self, setting): xoxb_token = setting['slack']['xoxb_token'] xapp_token = setting['slack']['xapp_token'] self._web_client = WebClient(token=xoxb_token) self._sm_client = SocketModeClient(app_token=xapp_token, web_client=self._web_client) self.plugins_setting = setting['plugins'] self.plugins_path = setting['bot']['plugins_dir'] self.plugin_modules = [] self.plugin_classes = [] self.plugin_instances = [] self._self = None self._team = None self._users_list = {} # [ 'id' ] => SlackUser self._channels_list = {} self._data = None def _get_rtm_client(self): return self._rtm_client def _set_rtm_client(self, rc): self._rtm_client = rc rtm_client = property(_get_rtm_client, _set_rtm_client) def _get_web_client(self): return self._web_client def _set_web_client(self, rc): self._web_client = rc web_client = property(_get_web_client, _set_web_client) # plugin loader def load_plugins(self): for ps in self.plugins_setting: mod = importlib.import_module(ps['module']) klass_name = ps['name'] klass = getattr(mod, klass_name) self.plugin_classes.append(klass) self.plugin_instances.append(klass(self, ps)) def load_plugins_filename_based(self): plugins_dir = os.listdir(self.plugins_path) # current_dir = os.path.dirname( os.path.abspath( __file__ ) ) for filename in plugins_dir: if filename.endswith('.py'): if filename == "__init__.py": continue klass_name = os.path.splitext(filename)[0] klass_name = klass_name[0].upper() + klass_name[1:] modulePath = self.plugins_path + '/' + filename cpath = os.path.splitext(modulePath)[0].replace( os.path.sep, '.') try: mod = importlib.import_module(cpath) self.plugin_modules.append(mod) klass = getattr(mod, klass_name) self.plugin_classes.append(klass) self.plugin_instances.append(klass(self, klass_name)) except ModuleNotFoundError: print('Module not found') except AttributeError: print('Method not found') def unload_plugins(self): for ins in self.plugin_instances: del (ins) self.plugin_instances = [] for cls in self.plugin_classes: del (cls) self.plugin_classes = [] for mod in self.plugin_modules: del (mod) self.plugin_modules = [] def reload_plugins(self): self.unload_plugins() self.load_plugins() # bot information def self_user(self): return self._self def self_id(self): u = self.self_user() return u.id def self_name(self): return self.self_user().name def team_info(self): return self._team def team_id(self): return self.team_info()['id'] def team_name(self): return self.team_info()['name'] def update_self_user(self, user): self._self = user def update_team_info(self, info): self._team = info def update_users_list(self, users): for user in users: self._users_list[user['id']] = SlackUser(user) def update_groups_list(self, groups): for group in groups: self._channels_list[group['id']] = SlackGroup(group) def update_ims_list(self, ims): for im in ims: self._channels_list[im['id']] = SlackIM(im) def update_channels_list(self, channels): for channel in channels: self._channels_list[channel['id']] = SlackChannel(channel) def resolve_channel_id_from_name(self, name): pass # plugin commands def send_message(self, channel, message, attachments_json=None): self._web_client.chat_postMessage(channel=channel.id, text=message, attachments=attachments_json) def send_mention_message(self, channel, user, message, attachments_json=None): mention_message = "<@" + user.id + "> " + message self._web_client.chat_postMessage(channel=channel.id, text=mention_message, attachments=attachments_json) def send_kick(self, channel, user): self._web_client.channels_kick(channel=channel.id, user=user.id) # plugin events def on_server_connect(self): for plugin in self.plugin_instances: plugin.on_server_connect() def process_message(self, data): channel = self._channels_list[data['channel']] user = self._users_list[data['user']] text = data['text'] # if user.id != self.self_id(): # ignore own message for plugin in self.plugin_instances: plugin.on_message(channel, user, text) def process_message_changed(self, data): channel = self._channels_list[data['channel']] user = self._users_list[data['message']['user']] text = data['message']['text'] prev_user = data['previous_message']['user'] prev_text = data['previous_message']['text'] # if user.id != self.self_id(): # ignore own message for plugin in self.plugin_instances: plugin.on_message_changed(channel, user, text, prev_user, prev_text) def on_message(self, payload): data = payload if 'bot_id' in data: return if 'subtype' in data: if data['subtype'] == 'message_changed': self.process_message_changed(data) else: self.process_message(data) def on_channel_joined(self, **payload): data = payload channel = data['channel'] self._channels_list[channel['id']] = SlackChannel(channel) def on_channel_left(self, **payload): data = payload del self._channels_list[data['channel']] # TODO: It should be not delete the channel and It must be update the status such as a 'is_member'. # self._channels_list[ data[ 'channel' ] ].is_member = False def on_member_joined_channel(self, **payload): data = payload channel = self._channels_list[data['channel']] user = self._users_list[data['user']] for plugin in self.plugin_instances: plugin.on_joined(channel, user) def on_member_left_channel(self, **payload): data = payload channel = self._channels_list[data['channel']] user = self._users_list[data['user']] for plugin in self.plugin_instances: plugin.on_left(channel, user) # process slack rtm def on_socket_mode_request(self, client: SocketModeClient, req: SocketModeRequest): if req.type == "events_api": # Acknowledge the request anyway response = SocketModeResponse(envelope_id=req.envelope_id) client.send_socket_mode_response(response) if req.payload['event']['type'] == 'open': self.on_open(req.payload['event']) elif req.payload['event']['type'] == 'message': self.on_message(req.payload['event']) elif req.payload['event']['type'] == 'channel_joined': self.on_channel_joined(req.payload['event']) elif req.payload['event']['type'] == 'channel_left': self.on_channel_left(req.payload['event']) elif req.payload['event']['type'] == 'member_joined_channel': self.on_member_joined_channel(req.payload['event']) elif req.payload['event']['type'] == 'member_left_channel': self.on_member_left_channel(req.payload['event']) def start(self): self._sm_client.socket_mode_request_listeners.append( self.on_socket_mode_request) self._sm_client.connect() response = self._web_client.users_list() self.update_users_list(response['members']) response = self._web_client.conversations_list() self.update_channels_list(response['channels']) response = self._web_client.team_info() self.update_team_info(response['team']) response = self._web_client.auth_test() self_id = response['user_id'] self.update_self_user(self._users_list[self_id]) self.load_plugins() self.on_server_connect() from threading import Event Event().wait()
def test_text_arg_only(self): client = WebClient( base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111" ) resp = client.chat_postMessage(channel="C111", text="test") self.assertTrue(resp["ok"])
sys.path.insert(1, f"{dirname(__file__)}/../../..") # ------------------ import logging logging.basicConfig(level=logging.DEBUG) # export SLACK_API_TOKEN=xoxb-*** # python3 integration_tests/samples/basic_usage/emoji_reactions.py import os from slack_sdk.web import WebClient client = WebClient(token=os.environ["SLACK_API_TOKEN"]) if __name__ == "__main__": channel_id = "#random" user_id = client.users_list()["members"][0]["id"] else: channel_id = "C0XXXXXX" user_id = "U0XXXXXXX" response = client.chat_postMessage(channel=channel_id, text="Give me some reaction!") # Ensure the channel_id is not a name channel_id = response["channel"] ts = response["message"]["ts"] response = client.reactions_add(channel=channel_id, name="thumbsup", timestamp=ts) response = client.reactions_remove(channel=channel_id, name="thumbsup", timestamp=ts)