diff options
Diffstat (limited to 'wee_slack.py')
-rw-r--r-- | wee_slack.py | 599 |
1 files changed, 485 insertions, 114 deletions
diff --git a/wee_slack.py b/wee_slack.py index a9cd888..2c7adfd 100644 --- a/wee_slack.py +++ b/wee_slack.py @@ -59,9 +59,9 @@ except ImportError: Reversible = object try: - from urllib.parse import quote, urlencode + from urllib.parse import quote, unquote, urlencode except ImportError: - from urllib import quote, urlencode + from urllib import quote, unquote, urlencode try: JSONDecodeError = json.JSONDecodeError @@ -393,6 +393,15 @@ def format_exc_only(): return "".join(decode_from_utf8(traceback.format_exception_only(etype, value))) +def url_encode_if_not_encoded(value): + decoded = unquote(value) + is_encoded = value != decoded + if is_encoded: + return value + else: + return quote(value) + + def get_localvar_type(slack_type): if slack_type in ("im", "mpim"): return "private" @@ -623,7 +632,7 @@ class EventRouter(object): ) team.set_disconnected() if not team.connected: - team.connect(reconnect=True) + team.connect() dbg("reconnecting {}".format(team)) @utf8_decode @@ -659,6 +668,59 @@ class EventRouter(object): message_json["wee_slack_metadata_team"] = team self.receive(message_json) + def http_check_ratelimited(self, request_metadata, response): + headers_end_index = response.index("\r\n\r\n") + headers = response[:headers_end_index].split("\r\n") + http_status = headers[0].split(" ")[1] + + if http_status == "429": + for header in headers[1:]: + name, value = header.split(":", 1) + if name.lower() == "retry-after": + retry_after = int(value.strip()) + request_metadata.retry_time = time.time() + retry_after + return "", "ratelimited" + + body = response[headers_end_index + 4 :] + return body, "" + + def retry_request(self, request_metadata, data, return_code, err): + self.reply_buffer.pop(request_metadata.response_id, None) + self.delete_context(data) + retry_text = ( + "retrying" + if request_metadata.should_try() + else "will not retry after too many failed attempts" + ) + team = ( + "for team {}".format(request_metadata.team) + if request_metadata.team + else "with token {}".format(token_for_print(request_metadata.token)) + ) + w.prnt( + "", + ( + "Failed requesting {} {}, {}. " + + "If this persists, try increasing slack_timeout. Error (code {}): {}" + ).format( + request_metadata.request, + team, + retry_text, + return_code, + err, + ), + ) + dbg( + "{} failed with return_code {} and error {}. stack:\n{}".format( + request_metadata.request, + return_code, + err, + "".join(traceback.format_stack()), + ), + level=5, + ) + self.receive(request_metadata) + @utf8_decode def receive_httprequest_callback(self, data, command, return_code, out, err): """ @@ -683,25 +745,31 @@ class EventRouter(object): if request_metadata.response_id not in self.reply_buffer: self.reply_buffer[request_metadata.response_id] = StringIO() self.reply_buffer[request_metadata.response_id].write(out) - try: - j = json.loads( - self.reply_buffer[request_metadata.response_id].getvalue() - ) - except: - pass - # dbg("Incomplete json, awaiting more", True) - try: - j["wee_slack_process_method"] = request_metadata.request_normalized - if self.recording: - self.record_event( - j, request_metadata.team, "wee_slack_process_method", "http" - ) - j["wee_slack_request_metadata"] = request_metadata - self.reply_buffer.pop(request_metadata.response_id) - self.receive(j) - self.delete_context(data) - except: - dbg("HTTP REQUEST CALLBACK FAILED", True) + + response = self.reply_buffer[request_metadata.response_id].getvalue() + body, error = self.http_check_ratelimited(request_metadata, response) + if error: + self.retry_request(request_metadata, data, return_code, error) + else: + j = json.loads(body) + + try: + j[ + "wee_slack_process_method" + ] = request_metadata.request_normalized + if self.recording: + self.record_event( + j, + request_metadata.team, + "wee_slack_process_method", + "http", + ) + j["wee_slack_request_metadata"] = request_metadata + self.reply_buffer.pop(request_metadata.response_id) + self.receive(j) + self.delete_context(data) + except: + dbg("HTTP REQUEST CALLBACK FAILED", True) # We got an empty reply and this is weird so just ditch it and retry else: dbg("length was zero, probably a bug..") @@ -712,33 +780,7 @@ class EventRouter(object): self.reply_buffer[request_metadata.response_id] = StringIO() self.reply_buffer[request_metadata.response_id].write(out) else: - self.reply_buffer.pop(request_metadata.response_id, None) - self.delete_context(data) - if request_metadata.request.startswith("rtm."): - retry_text = ( - "retrying" - if request_metadata.should_try() - else "will not retry after too many failed attempts" - ) - w.prnt( - "", - ( - "Failed connecting to slack team with token {}, {}. " - + "If this persists, try increasing slack_timeout. Error (code {}): {}" - ).format( - token_for_print(request_metadata.token), - retry_text, - return_code, - err, - ), - ) - dbg( - "rtm.start failed with return_code {}. stack:\n{}".format( - return_code, "".join(traceback.format_stack()) - ), - level=5, - ) - self.receive(request_metadata) + self.retry_request(request_metadata, data, return_code, err) return w.WEECHAT_RC_OK def receive(self, dataobj, slow=False): @@ -810,10 +852,12 @@ class EventRouter(object): team = request.team channel = request.channel metadata = request.metadata + callback = request.callback else: team = j.get("wee_slack_metadata_team") channel = None metadata = {} + callback = None if team: if "channel" in j: @@ -830,7 +874,9 @@ class EventRouter(object): metadata["user"] = team.users.get(user_id) dbg("running {}".format(function_name)) - if ( + if callable(callback): + callback(j, self, team, channel, metadata) + elif ( function_name.startswith("local_") and function_name in self.local_proc ): @@ -919,14 +965,13 @@ def local_process_async_slack_api_request(request, event_router): random.choice(string.ascii_uppercase + string.digits) for _ in range(4) ) ) - params = {"useragent": "wee_slack {}".format(SCRIPT_VERSION)} request.tried() + options = request.options() + options["header"] = "1" context = event_router.store_context(request) - # TODO: let flashcode know about this bug - i have to 'clear' the hashtable or retry requests fail - w.hook_process_hashtable("url:", params, config.slack_timeout, "", context) w.hook_process_hashtable( weechat_request, - params, + options, config.slack_timeout, "receive_httprequest_callback", context, @@ -1427,6 +1472,8 @@ class SlackRequest(object): metadata=None, retries=3, token=None, + cookies=None, + callback=None, ): if team is None and token is None: raise ValueError("Both team and token can't be None") @@ -1436,28 +1483,41 @@ class SlackRequest(object): self.channel = channel self.metadata = metadata if metadata else {} self.retries = retries + self.retry_time = 0 self.token = token if token else team.token + self.cookies = cookies or {} + if ":" in self.token: + token, cookie = self.token.split(":", 1) + self.token = token + if cookie.startswith("d="): + for name, value in [c.split("=") for c in cookie.split(";")]: + self.cookies[name] = value + else: + self.cookies["d"] = cookie + self.callback = callback + self.domain = "api.slack.com" + self.reset() + + def reset(self): self.tries = 0 self.start_time = time.time() - self.request_normalized = re.sub(r"\W+", "", request) - self.domain = "api.slack.com" - self.post_data["token"] = self.token + self.request_normalized = re.sub(r"\W+", "", self.request) self.url = "https://{}/api/{}?{}".format( self.domain, self.request, urlencode(encode_to_utf8(self.post_data)) ) - self.params = {"useragent": "wee_slack {}".format(SCRIPT_VERSION)} self.response_id = sha1_hex("{}{}".format(self.url, self.start_time)) def __repr__(self): return ( "SlackRequest(team={}, request='{}', post_data={}, retries={}, token='{}', " - "tries={}, start_time={})" + "cookies={}, tries={}, start_time={})" ).format( self.team, self.request, self.post_data, self.retries, token_for_print(self.token), + self.cookies, self.tries, self.start_time, ) @@ -1465,6 +1525,26 @@ class SlackRequest(object): def request_string(self): return "{}".format(self.url) + def options(self): + cookies = "; ".join( + [ + "{}={}".format(key, url_encode_if_not_encoded(value)) + for key, value in self.cookies.items() + ] + ) + return { + "useragent": "wee_slack {}".format(SCRIPT_VERSION), + "httpheader": "Authorization: Bearer {}".format(self.token), + "cookie": cookies, + } + + def options_as_cli_args(self): + options = self.options() + options["user-agent"] = options.pop("useragent") + httpheader = options.pop("httpheader") + headers = [": ".join(x) for x in options.items()] + httpheader.split("\n") + return ["-H{}".format(header) for header in headers] + def tried(self): self.tries += 1 self.response_id = sha1_hex("{}{}".format(self.url, time.time())) @@ -1473,7 +1553,10 @@ class SlackRequest(object): return self.tries < self.retries def retry_ready(self): - return (self.start_time + (self.tries**2)) < time.time() + if self.retry_time: + return time.time() > self.retry_time + else: + return (self.start_time + (self.tries**2)) < time.time() class SlackSubteam(object): @@ -1516,7 +1599,7 @@ class SlackTeam(object): users, bots, channels, - **kwargs + **kwargs, ): self.slack_api_translator = copy.deepcopy(SLACK_API_TRANSLATOR) self.identifier = team_info["id"] @@ -1685,7 +1768,7 @@ class SlackTeam(object): def mark_read(self, ts=None, update_remote=True, force=False): pass - def connect(self, reconnect=False): + def connect(self): if not self.connected and not self.connecting_ws: if self.ws_url: self.connecting_ws = True @@ -1737,9 +1820,7 @@ class SlackTeam(object): # The fast reconnect failed, so start over-ish for chan in self.channels: self.channels[chan].history_needs_update = True - s = initiate_connection( - self.token, retries=999, team=self, reconnect=reconnect - ) + s = get_rtm_connect_request(self.token, retries=999, team=self) self.eventrouter.receive(s) self.connecting_rtm = True @@ -2262,13 +2343,15 @@ class SlackChannel(SlackChannelCommon): self.create_buffer() return - # Only check is_member if is_open is not set, because in some cases - # (e.g. group DMs), is_member should be ignored in favor of is_open. - is_open = self.is_open if hasattr(self, "is_open") else self.is_member - if is_open or self.unread_count_display: + if ( + getattr(self, "is_open", False) + or self.unread_count_display + or self.type not in ["im", "mpim"] + and getattr(self, "is_member", False) + ): self.create_buffer() - elif self.type == "im": - # If it is an IM, we still might want to open it if there are unread messages. + elif self.type in ["im", "mpim"]: + # If it is an IM or MPIM, we still might want to open it if there are unread messages. info_method = self.team.slack_api_translator[self.type].get("info") if info_method: s = SlackRequest( @@ -2501,7 +2584,7 @@ class SlackChannel(SlackChannelCommon): self.pending_history_requests.add(self.identifier) self.get_members() - post_data = {"channel": self.identifier, "count": config.history_fetch_count} + post_data = {"channel": self.identifier, "limit": config.history_fetch_count} if self.got_history and self.messages and not full: post_data["oldest"] = next(reversed(self.messages)) @@ -2870,7 +2953,7 @@ class SlackMPDMChannel(SlackChannel): """ def __init__(self, eventrouter, team_users, myidentifier, **kwargs): - if "members" in kwargs: + if kwargs.get("members"): kwargs["name"] = self.name_from_members( team_users, kwargs["members"], myidentifier ) @@ -3243,7 +3326,7 @@ class SlackBot(SlackUser): """ def __init__(self, originating_team_id, **kwargs): - super(SlackBot, self).__init__(originating_team_id, is_bot=True, **kwargs) + super(SlackBot, self).__init__(originating_team_id, **kwargs) class SlackMessage(object): @@ -3689,7 +3772,7 @@ def handle_rtmstart(login_data, eventrouter, team, channel, metadata): t.set_reconnect_url(login_data["url"]) t.connecting_rtm = False - t.connect(metadata.metadata["reconnect"]) + t.connect() def handle_rtmconnect(login_data, eventrouter, team, channel, metadata): @@ -3707,7 +3790,7 @@ def handle_rtmconnect(login_data, eventrouter, team, channel, metadata): return team.set_reconnect_url(login_data["url"]) - team.connect(metadata.metadata["reconnect"]) + team.connect() def handle_emojilist(emoji_json, eventrouter, team, channel, metadata): @@ -3715,28 +3798,50 @@ def handle_emojilist(emoji_json, eventrouter, team, channel, metadata): team.emoji_completions.extend(emoji_json["emoji"].keys()) -def handle_channelsinfo(channel_json, eventrouter, team, channel, metadata): - channel.set_unread_count_display( - channel_json["channel"].get("unread_count_display", 0) - ) - channel.set_members(channel_json["channel"]["members"]) - - -def handle_groupsinfo(group_json, eventrouter, team, channel, metadatas): - channel.set_unread_count_display(group_json["group"].get("unread_count_display", 0)) - channel.set_members(group_json["group"]["members"]) +def handle_conversationsinfo(channel_json, eventrouter, team, channel, metadata): + channel_info = channel_json["channel"] + if "unread_count_display" in channel_info: + unread_count = channel_info["unread_count_display"] + if unread_count and channel.channel_buffer is None: + channel.create_buffer() + channel.set_unread_count_display(unread_count) + if "last_read" in channel_info: + channel.last_read = SlackTS(channel_info["last_read"]) + if "members" in channel_info: + channel.set_members(channel_info["members"]) + + # MPIMs don't have unread_count_display so we have to request the history to check if there are unread messages + if channel.type == "mpim" and not channel.channel_buffer: + s = SlackRequest( + team, + "conversations.history", + {"channel": channel.identifier, "limit": 1}, + channel=channel, + metadata={"only_set_unread": True}, + ) + eventrouter.receive(s) def handle_conversationsopen( conversation_json, eventrouter, team, channel, metadata, object_name="channel" ): - # Set unread count if the channel isn't new - if channel: - unread_count_display = conversation_json[object_name].get( - "unread_count_display", 0 + channel_info = conversation_json[object_name] + if not channel: + channel = create_channel_from_info( + eventrouter, channel_info, team, team.myidentifier, team.users ) + team.channels[channel_info["id"]] = channel + + if channel.channel_buffer is None: + channel.create_buffer() + + unread_count_display = channel_info.get("unread_count_display") + if unread_count_display is not None: channel.set_unread_count_display(unread_count_display) + if metadata.get("switch") and config.switch_buffer_on_join: + w.buffer_set(channel.channel_buffer, "display", "1") + def handle_mpimopen( mpim_json, eventrouter, team, channel, metadata, object_name="group" @@ -3749,6 +3854,16 @@ def handle_mpimopen( def handle_history( message_json, eventrouter, team, channel, metadata, includes_threads=True ): + if metadata.get("only_set_unread"): + if message_json["messages"]: + latest = message_json["messages"][0] + latest_ts = SlackTS(latest["ts"]) + if latest_ts > channel.last_read: + if not channel.channel_buffer: + channel.create_buffer() + channel.set_unread_count_display(1) + return + channel.got_history = True channel.history_needs_update = False for message in reversed(message_json["messages"]): @@ -3830,14 +3945,6 @@ def handle_conversationsmembers(members_json, eventrouter, team, channel, metada ) -def handle_conversationsinfo(message_json, eventrouter, team, channel, metadata): - if message_json["channel"]["is_im"]: - unread = message_json["channel"]["unread_count_display"] - if unread: - channel.check_should_open(True) - channel.set_unread_count_display(unread) - - def handle_usersinfo(user_json, eventrouter, team, channel, metadata): user_info = user_json["user"] if not metadata.get("user"): @@ -4090,11 +4197,12 @@ def download_files(message_json, channel): for fileout in fileout_iter(os.path.join(download_location, filename)): if os.path.isfile(fileout): continue + curl_options = SlackRequest(channel.team, "").options() w.hook_process_hashtable( "url:" + f["url_private"], { + **curl_options, "file_out": fileout, - "httpheader": "Authorization: Bearer " + channel.team.token, }, config.slack_timeout, "", @@ -5173,7 +5281,6 @@ def command_register(data, current_buffer, args): "client_id={}&client_secret={}&redirect_uri={}&code={}" ).format(CLIENT_ID, CLIENT_SECRET, redirect_uri, code) params = {"useragent": "wee_slack {}".format(SCRIPT_VERSION)} - w.hook_process_hashtable("url:", params, config.slack_timeout, "", "") w.hook_process_hashtable( "url:{}".format(uri), params, config.slack_timeout, "register_callback", "" ) @@ -5429,6 +5536,7 @@ def join_query_command_cb(data, current_buffer, args): team, team.slack_api_translator[channel_type]["join"], {"users": ",".join(users)}, + metadata={"switch": True}, ) EVENTROUTER.receive(s) @@ -5862,10 +5970,12 @@ def command_upload(data, current_buffer, args): if isinstance(channel, SlackThreadChannel): post_data["thread_ts"] = channel.thread_ts - url = SlackRequest( - channel.team, "files.upload", post_data, channel=channel - ).request_string() - options = ["-s", "-Ffile=@{}".format(file_path), url] + request = SlackRequest(channel.team, "files.upload", post_data, channel=channel) + options = request.options_as_cli_args() + [ + "-s", + "-Ffile=@{}".format(file_path), + request.request_string(), + ] proxy_string = ProxyWrapper().curl() if proxy_string: @@ -6687,21 +6797,275 @@ def trace_calls(frame, event, arg): return -def initiate_connection(token, retries=3, team=None, reconnect=False): - request_type = "connect" if team else "start" - post_data = {"batch_presence_aware": 1} - if request_type == "start": - post_data["mpim_aware"] = "true" +def get_rtm_connect_request(token, retries=3, team=None, callback=None): return SlackRequest( team, - "rtm.{}".format(request_type), - post_data, + "rtm.connect", + {"batch_presence_aware": 1}, retries=retries, token=token, - metadata={"reconnect": reconnect}, + callback=callback, ) +def get_next_page(response_json): + next_cursor = response_json.get("response_metadata", {}).get("next_cursor") + if next_cursor: + request = response_json["wee_slack_request_metadata"] + request.post_data["cursor"] = next_cursor + request.reset() + EVENTROUTER.receive(request) + return True + else: + return False + + +def initiate_connection(token): + initial_data = { + "channels": [], + "members": [], + "usergroups": [], + "remaining": { + "channels": 2, + "members": 1, + "usergroups": 1, + "prefs": 1, + "presence": 1, + }, + "errors": [], + } + + def handle_initial(data_type): + def handle(response_json, eventrouter, team, channel, metadata): + if not response_json["ok"]: + if response_json["error"] == "user_is_restricted": + w.prnt( + "", + "You are a restricted user in this team, " + f"{data_type} not loaded", + ) + else: + initial_data["errors"].append( + f'{data_type}: {response_json["error"]}' + ) + initial_data["remaining"][data_type] -= 1 + create_team(token, initial_data) + return + + initial_data[data_type].extend(response_json[data_type]) + + if not get_next_page(response_json): + initial_data["remaining"][data_type] -= 1 + create_team(token, initial_data) + + return handle + + def handle_prefs(response_json, eventrouter, team, channel, metadata): + if not response_json["ok"]: + initial_data["errors"].append(f'prefs: {response_json["error"]}') + initial_data["remaining"]["prefs"] -= 1 + create_team(token, initial_data) + return + + initial_data["prefs"] = response_json["prefs"] + initial_data["remaining"]["prefs"] -= 1 + create_team(token, initial_data) + + def handle_getPresence(response_json, eventrouter, team, channel, metadata): + if not response_json["ok"]: + initial_data["errors"].append(f'presence: {response_json["error"]}') + initial_data["remaining"]["presence"] -= 1 + create_team(token, initial_data) + return + + initial_data["presence"] = response_json + initial_data["remaining"]["presence"] -= 1 + create_team(token, initial_data) + + s = SlackRequest( + None, + "conversations.list", + { + "exclude_archived": True, + "types": "public_channel,private_channel,im", + "limit": 1000, + }, + token=token, + callback=handle_initial("channels"), + ) + EVENTROUTER.receive(s) + s = SlackRequest( + None, + "conversations.list", + { + "exclude_archived": True, + "types": "mpim", + "limit": 1000, + }, + token=token, + callback=handle_initial("channels"), + ) + EVENTROUTER.receive(s) + s = SlackRequest( + None, + "users.list", + {"limit": 1000}, + token=token, + callback=handle_initial("members"), + ) + EVENTROUTER.receive(s) + s = SlackRequest( + None, + "usergroups.list", + {"include_users": True}, + token=token, + callback=handle_initial("usergroups"), + ) + EVENTROUTER.receive(s) + s = SlackRequest( + None, + "users.prefs.get", + token=token, + callback=handle_prefs, + ) + EVENTROUTER.receive(s) + s = SlackRequest( + None, + "users.getPresence", + token=token, + callback=handle_getPresence, + ) + EVENTROUTER.receive(s) + + +def create_channel_from_info(eventrouter, channel_info, team, myidentifier, users): + if channel_info.get("is_im"): + return SlackDMChannel(eventrouter, users, team=team, **channel_info) + elif channel_info.get("is_mpim"): + return SlackMPDMChannel( + eventrouter, users, myidentifier, team=team, **channel_info + ) + elif channel_info.get("is_shared"): + return SlackSharedChannel(eventrouter, team=team, **channel_info) + elif channel_info.get("is_private"): + return SlackPrivateChannel(eventrouter, team=team, **channel_info) + else: + return SlackChannel(eventrouter, team=team, **channel_info) + + +def create_team(token, initial_data): + if not any(initial_data["remaining"].values()): + if initial_data["errors"]: + w.prnt( + "", + "ERROR: Failed connecting to Slack with token {}: {}".format( + token_for_print(token), ", ".join(initial_data["errors"]) + ), + ) + if not re.match(r"^xo\w\w(-\d+){3}-[0-9a-f]+(:.*)?$", token): + w.prnt( + "", + "ERROR: Token does not look like a valid Slack token. " + "Ensure it is a valid token and not just a OAuth code.", + ) + + return + + def handle_rtmconnect(response_json, eventrouter, team, channel, metadata): + if not response_json["ok"]: + print(response_json["error"]) + return + + team_id = response_json["team"]["id"] + myidentifier = response_json["self"]["id"] + + users = {} + bots = {} + for member in initial_data["members"]: + if member.get("is_bot"): + bots[member["id"]] = SlackBot(team_id, **member) + else: + users[member["id"]] = SlackUser(team_id, **member) + + self_nick = nick_from_profile( + users[myidentifier].profile, response_json["self"]["name"] + ) + + channels = {} + for channel_info in initial_data["channels"]: + channels[channel_info["id"]] = create_channel_from_info( + eventrouter, channel_info, None, myidentifier, users + ) + + subteams = {} + for usergroup in initial_data["usergroups"]: + is_member = myidentifier in usergroup["users"] + subteams[usergroup["id"]] = SlackSubteam( + team_id, is_member=is_member, **usergroup + ) + + manual_presence = ( + "away" if initial_data["presence"]["manual_away"] else "active" + ) + + team_info = { + "id": team_id, + "name": response_json["team"]["id"], + "domain": response_json["team"]["domain"], + } + + team_hash = SlackTeam.generate_team_hash( + team_id, response_json["team"]["domain"] + ) + if not eventrouter.teams.get(team_hash): + team = SlackTeam( + eventrouter, + token, + team_hash, + response_json["url"], + team_info, + subteams, + self_nick, + myidentifier, + manual_presence, + users, + bots, + channels, + muted_channels=initial_data["prefs"]["muted_channels"], + highlight_words=initial_data["prefs"]["highlight_words"], + ) + eventrouter.register_team(team) + team.connect() + else: + team = eventrouter.teams.get(team_hash) + if team.myidentifier != myidentifier: + print_error( + "The Slack team {} has tokens for two different users, this is not supported. The " + "token {} is for user {}, and the token {} is for user {}. Please remove one of " + "them.".format( + team.team_info["name"], + token_for_print(team.token), + team.nick, + token_for_print(token), + self_nick, + ) + ) + else: + print_error( + "Ignoring duplicate Slack tokens for the same team ({}) and user ({}). The two " + "tokens are {} and {}.".format( + team.team_info["name"], + team.nick, + token_for_print(team.token), + token_for_print(token), + ), + warning=True, + ) + + s = get_rtm_connect_request(token, callback=handle_rtmconnect) + EVENTROUTER.receive(s) + + if __name__ == "__main__": w = WeechatWrapper(weechat) @@ -6777,6 +7141,13 @@ if __name__ == "__main__": ), ) for t in tokens: - s = initiate_connection(t) - EVENTROUTER.receive(s) + if t.startswith("xoxc-") and ":" not in t: + w.prnt( + "", + "{}When using an xoxc token, you need to also provide the d cookie in the format token:cookie".format( + w.prefix("error") + ), + ) + else: + initiate_connection(t) EVENTROUTER.handle_next() |