Skip to content

Commit c1906d0

Browse files
devin-ai-integration[bot]bot_apk
andauthored
fix(source-slack): skip joining archived channels rejected by Slack API (#76052)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: bot_apk <apk@cognition.ai>
1 parent 5d98b4e commit c1906d0

File tree

6 files changed

+87
-74
lines changed

6 files changed

+87
-74
lines changed

airbyte-integrations/connectors/source-slack/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## 1. Auto-Joining Channels During Sync
44

5-
When `join_channels` is enabled in the config, the `ChannelsRetriever` automatically joins Slack channels by making POST requests to `conversations.join` during the channels stream read. For each channel where the bot's `is_member` property is false, the connector fires a side-effect API call to join that channel before yielding the record. This means reading the channels stream can modify the Slack workspace state.
5+
When `join_channels` is enabled in the config, the `ChannelsRetriever` automatically joins Slack channels by making POST requests to `conversations.join` during the channels stream read. For each non-archived channel where the bot's `is_member` property is false, the connector fires a side-effect API call to join that channel before yielding the record. Archived channels are always skipped because the Slack API rejects `conversations.join` for archived channels. This means reading the channels stream can modify the Slack workspace state.
66

77
**Why this matters:** The channels stream is not read-only when `join_channels` is enabled. It actively modifies the workspace by joining channels, which is a side effect that no other connector stream produces. The Slack API only returns messages from channels the bot has joined, so this behavior exists to ensure the messages and threads streams can actually retrieve data. If the bot lacks permission to join a channel, it logs a warning but does not fail the sync.
88

airbyte-integrations/connectors/source-slack/components.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ def should_join_to_channel(self, config: Mapping[str, Any], record: Record) -> b
106106
The `is_member` property indicates whether the API Bot is already assigned / joined to the channel.
107107
https://api.slack.com/types/conversation#booleans
108108
"""
109+
if record.get("is_archived"):
110+
return False # Slack API rejects conversations.join for archived channels
109111
return config["join_channels"] and not record.get("is_member")
110112

111113
def make_join_channel_slice(self, channel: Mapping[str, Any]) -> Mapping[str, Any]:

airbyte-integrations/connectors/source-slack/metadata.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ data:
1010
connectorSubtype: api
1111
connectorType: source
1212
definitionId: c2281cee-86f9-4a86-bb48-d23286b4c7bd
13-
dockerImageTag: 3.1.15
13+
dockerImageTag: 3.1.16
1414
dockerRepository: airbyte/source-slack
1515
documentationUrl: https://docs.airbyte.com/integrations/sources/slack
1616
externalDocumentationUrls:

airbyte-integrations/connectors/source-slack/unit_tests/test_components.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,21 @@ def get_channels_retriever_instance(token_config, components_module):
5151
)
5252

5353

54-
def test_join_channels_should_join_to_channel(token_config, components_module):
55-
retriever = get_channels_retriever_instance(token_config, components_module)
56-
assert retriever.should_join_to_channel(token_config, {"is_member": False}) is True
57-
assert retriever.should_join_to_channel(token_config, {"is_member": True}) is False
54+
@pytest.mark.parametrize(
55+
"join_channels,record,expected",
56+
[
57+
pytest.param(True, {"is_member": False, "is_archived": False}, True, id="join_enabled_non_member_non_archived"),
58+
pytest.param(True, {"is_member": True, "is_archived": False}, False, id="join_enabled_already_member"),
59+
pytest.param(True, {"is_member": False, "is_archived": True}, False, id="join_enabled_archived_channel_rejected"),
60+
pytest.param(False, {"is_member": False, "is_archived": False}, False, id="join_disabled"),
61+
pytest.param(True, {"is_member": False}, True, id="join_enabled_missing_is_archived"),
62+
pytest.param(True, {"is_member": True, "is_archived": True}, False, id="archived_and_already_member"),
63+
],
64+
)
65+
def test_should_join_to_channel(token_config, components_module, join_channels, record, expected):
66+
config = {**token_config, "join_channels": join_channels}
67+
retriever = get_channels_retriever_instance(config, components_module)
68+
assert retriever.should_join_to_channel(config, record) is expected
5869

5970

6071
def test_join_channels_make_join_channel_slice(token_config, components_module):

airbyte-integrations/connectors/source-slack/unit_tests/test_config_migrations.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
CMD = "check"
1313
TEST_CONFIG_LEGACY_PATH = f"{os.path.dirname(__file__)}/configs/legacy_config.json"
1414
TEST_CONFIG_ACTUAL_PATH = f"{os.path.dirname(__file__)}/configs/actual_config.json"
15-
1615
SOURCE_INPUT_ARGS_LEGACY = [CMD, "--config", TEST_CONFIG_LEGACY_PATH]
1716
SOURCE_INPUT_ARGS_ACTUAL = [CMD, "--config", TEST_CONFIG_ACTUAL_PATH]
1817

0 commit comments

Comments
 (0)