Skip to content

Commit 699e1d1

Browse files
WilliamBergaminClaude
andauthored
feat: add AuthProviderBlock layout block support (#1985)
Co-authored-by: Claude <svc-devxp-claude@slack-corp.com>
1 parent 8fe5157 commit 699e1d1

3 files changed

Lines changed: 278 additions & 0 deletions

File tree

‎slack_sdk/models/blocks/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
from .blocks import (
6565
ActionsBlock,
6666
AlertBlock,
67+
AuthProviderBlock,
6768
Block,
6869
CallBlock,
6970
CardBlock,
@@ -138,6 +139,7 @@
138139
"RichTextSectionElement",
139140
"ActionsBlock",
140141
"AlertBlock",
142+
"AuthProviderBlock",
141143
"Block",
142144
"CallBlock",
143145
"CardBlock",

‎slack_sdk/models/blocks/blocks.py‎

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
ImageElement,
2424
InputInteractiveElement,
2525
InteractiveElement,
26+
OverflowMenuElement,
2627
RichTextElement,
2728
UrlSourceElement,
2829
)
@@ -118,6 +119,8 @@ def parse(cls, block: Union[dict, "Block"]) -> Optional["Block"]:
118119
return ContainerBlock(**block)
119120
elif type == CarouselBlock.type:
120121
return CarouselBlock(**block)
122+
elif type == AuthProviderBlock.type:
123+
return AuthProviderBlock(**block)
121124
else:
122125
cls.logger.warning(f"Unknown block detected and skipped ({block})")
123126
return None
@@ -1157,3 +1160,143 @@ def _validate_elements_present(self):
11571160
@JsonValidator(f"elements attribute cannot exceed {elements_max_length} cards")
11581161
def _validate_elements_length(self):
11591162
return self.elements is None or len(self.elements) <= self.elements_max_length
1163+
1164+
1165+
class AuthProviderBlock(Block):
1166+
type = "auth_provider"
1167+
value_max_length = 150
1168+
name_max_length = 75
1169+
description_max_length = 150
1170+
connected_identity_max_length = 75
1171+
connected_context_max_length = 75
1172+
url_max_length = 3000
1173+
1174+
@property
1175+
def attributes(self) -> Set[str]: # type: ignore[override]
1176+
return super().attributes.union(
1177+
{
1178+
"value",
1179+
"name",
1180+
"is_connected",
1181+
"description",
1182+
"icon_url",
1183+
"auth_url",
1184+
"disconnect_url",
1185+
"connected_identity",
1186+
"connected_context",
1187+
"accessory",
1188+
"fallback",
1189+
}
1190+
)
1191+
1192+
def __init__(
1193+
self,
1194+
*,
1195+
value: str,
1196+
name: Union[str, dict, PlainTextObject],
1197+
is_connected: bool,
1198+
description: Optional[Union[str, dict, PlainTextObject]] = None,
1199+
icon_url: Optional[str] = None,
1200+
auth_url: Optional[str] = None,
1201+
disconnect_url: Optional[str] = None,
1202+
connected_identity: Optional[Union[str, dict, PlainTextObject]] = None,
1203+
connected_context: Optional[Union[str, dict, PlainTextObject]] = None,
1204+
accessory: Optional[Union[dict, OverflowMenuElement]] = None,
1205+
fallback: Optional[Sequence[Union[dict, Block]]] = None,
1206+
block_id: Optional[str] = None,
1207+
**others: dict,
1208+
):
1209+
"""Displays authentication provider information, including its icon, name, and connection state.
1210+
1211+
https://docs.slack.dev/reference/block-kit/blocks/auth-provider-block
1212+
1213+
Args:
1214+
value (required): A unique identifier for this provider within the app. Included in interaction
1215+
payloads and used to derive the connect button's action_id as auth_provider_connect_<value>.
1216+
Maximum length for this field is 150 characters.
1217+
name (required): The provider's display name, in the form of a plain_text text object.
1218+
Maximum length for the text in this field is 75 characters.
1219+
is_connected (required): Whether the user is connected to this provider. Slack stores this value
1220+
for the user, so it must be set explicitly.
1221+
description: A description of the provider, in the form of a plain_text text object.
1222+
Maximum length for the text in this field is 150 characters.
1223+
icon_url: An HTTPS URL for the provider's icon. Defaults to the app's avatar if omitted.
1224+
Maximum length for this field is 3000 characters.
1225+
auth_url: The HTTPS URL opened when the user clicks Connect. Its host must be listed in the
1226+
app manifest's features.user_connections.auth_hosts, or be a subdomain of one.
1227+
Maximum length for this field is 3000 characters.
1228+
disconnect_url: The HTTPS URL opened when the user clicks Disconnect. Its host must be listed in
1229+
the app manifest's features.user_connections.auth_hosts, or be a subdomain of one.
1230+
Maximum length for this field is 3000 characters.
1231+
connected_identity: The display name of the connected account, in the form of a plain_text text object.
1232+
Shown when is_connected is true.
1233+
Maximum length for the text in this field is 75 characters.
1234+
connected_context: Additional context about the connection, such as an organization or workspace name,
1235+
in the form of a plain_text text object.
1236+
Maximum length for the text in this field is 75 characters.
1237+
accessory: An overflow menu element shown when the user is connected.
1238+
fallback: A list of blocks rendered in place of this block by clients that don't support it.
1239+
block_id: A unique identifier for a block. If not specified, a block_id will be generated.
1240+
"""
1241+
super().__init__(type=self.type, block_id=block_id)
1242+
show_unknown_key_warning(self, others)
1243+
1244+
self.value = value
1245+
self.name = TextObject.parse(name, default_type=PlainTextObject.type)
1246+
self.is_connected = is_connected
1247+
self.description = TextObject.parse(description, default_type=PlainTextObject.type) # type: ignore[arg-type]
1248+
self.icon_url = icon_url
1249+
self.auth_url = auth_url
1250+
self.disconnect_url = disconnect_url
1251+
self.connected_identity = TextObject.parse(connected_identity, default_type=PlainTextObject.type) # type: ignore[arg-type]
1252+
self.connected_context = TextObject.parse(connected_context, default_type=PlainTextObject.type) # type: ignore[arg-type]
1253+
self.accessory = BlockElement.parse(accessory) # type: ignore[arg-type]
1254+
self.fallback = Block.parse_all(fallback) if fallback is not None else None
1255+
1256+
@JsonValidator("value attribute must be specified")
1257+
def _validate_value(self):
1258+
return self.value is not None and len(self.value) >= 1
1259+
1260+
@JsonValidator(f"value attribute cannot exceed {value_max_length} characters")
1261+
def _validate_value_length(self):
1262+
return self.value is None or len(self.value) <= self.value_max_length
1263+
1264+
@JsonValidator("name attribute must be specified")
1265+
def _validate_name(self):
1266+
return self.name is not None
1267+
1268+
@JsonValidator(f"name attribute cannot exceed {name_max_length} characters")
1269+
def _validate_name_length(self):
1270+
return self.name is None or len(self.name.text) <= self.name_max_length
1271+
1272+
@JsonValidator("is_connected attribute must be specified")
1273+
def _validate_is_connected(self):
1274+
return self.is_connected is not None
1275+
1276+
@JsonValidator(f"description attribute cannot exceed {description_max_length} characters")
1277+
def _validate_description_length(self):
1278+
return self.description is None or len(self.description.text) <= self.description_max_length
1279+
1280+
@JsonValidator(f"connected_identity attribute cannot exceed {connected_identity_max_length} characters")
1281+
def _validate_connected_identity_length(self):
1282+
return self.connected_identity is None or len(self.connected_identity.text) <= self.connected_identity_max_length
1283+
1284+
@JsonValidator(f"connected_context attribute cannot exceed {connected_context_max_length} characters")
1285+
def _validate_connected_context_length(self):
1286+
return self.connected_context is None or len(self.connected_context.text) <= self.connected_context_max_length
1287+
1288+
@JsonValidator(f"icon_url attribute cannot exceed {url_max_length} characters")
1289+
def _validate_icon_url_length(self):
1290+
return self.icon_url is None or len(self.icon_url) <= self.url_max_length
1291+
1292+
@JsonValidator(f"auth_url attribute cannot exceed {url_max_length} characters")
1293+
def _validate_auth_url_length(self):
1294+
return self.auth_url is None or len(self.auth_url) <= self.url_max_length
1295+
1296+
@JsonValidator(f"disconnect_url attribute cannot exceed {url_max_length} characters")
1297+
def _validate_disconnect_url_length(self):
1298+
return self.disconnect_url is None or len(self.disconnect_url) <= self.url_max_length
1299+
1300+
@JsonValidator("accessory attribute must be an overflow menu element")
1301+
def _validate_accessory(self):
1302+
return self.accessory is None or isinstance(self.accessory, OverflowMenuElement)

‎tests/slack_sdk/models/test_blocks.py‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from slack_sdk.models.blocks import (
66
ActionsBlock,
77
AlertBlock,
8+
AuthProviderBlock,
89
Block,
910
ButtonElement,
1011
CallBlock,
@@ -1987,3 +1988,135 @@ def test_single_card(self):
19871988
def test_empty_elements_validation(self):
19881989
with self.assertRaises(SlackObjectFormationError):
19891990
CarouselBlock(elements=[]).validate_json()
1991+
1992+
1993+
# ----------------------------------------------
1994+
# Auth provider
1995+
# ----------------------------------------------
1996+
1997+
1998+
class AuthProviderBlockTests(unittest.TestCase):
1999+
def test_document_required_fields(self):
2000+
input = {
2001+
"type": "auth_provider",
2002+
"value": "lumon",
2003+
"name": {"type": "plain_text", "text": "Lumon"},
2004+
"is_connected": False,
2005+
}
2006+
self.assertDictEqual(input, AuthProviderBlock(**input).to_dict())
2007+
2008+
def test_document_all_fields(self):
2009+
input = {
2010+
"type": "auth_provider",
2011+
"block_id": "lumon_connection",
2012+
"value": "lumon",
2013+
"name": {"type": "plain_text", "text": "Lumon"},
2014+
"is_connected": True,
2015+
"description": {"type": "plain_text", "text": "Connect your Lumon account"},
2016+
"icon_url": "https://lumon.example.com/icon.png",
2017+
"auth_url": "https://lumon.example.com/oauth/start",
2018+
"disconnect_url": "https://lumon.example.com/oauth/disconnect",
2019+
"connected_identity": {"type": "plain_text", "text": "mark.s@lumon.example.com"},
2020+
"connected_context": {"type": "plain_text", "text": "Macrodata Refinement"},
2021+
"accessory": {
2022+
"type": "overflow",
2023+
"action_id": "lumon_menu",
2024+
"options": [
2025+
{"text": {"type": "plain_text", "text": "Switch accounts"}, "value": "switch"},
2026+
{"text": {"type": "plain_text", "text": "Disconnect"}, "value": "disconnect"},
2027+
],
2028+
},
2029+
"fallback": [
2030+
{"type": "section", "text": {"type": "mrkdwn", "text": "Connected to Lumon"}},
2031+
],
2032+
}
2033+
self.assertDictEqual(input, AuthProviderBlock(**input).to_dict())
2034+
2035+
def test_parse(self):
2036+
input = {
2037+
"type": "auth_provider",
2038+
"value": "lumon",
2039+
"name": {"type": "plain_text", "text": "Lumon"},
2040+
"is_connected": True,
2041+
"accessory": {
2042+
"type": "overflow",
2043+
"action_id": "lumon_menu",
2044+
"options": [{"text": {"type": "plain_text", "text": "Disconnect"}, "value": "disconnect"}],
2045+
},
2046+
"fallback": [{"type": "section", "text": {"type": "mrkdwn", "text": "Connected to Lumon"}}],
2047+
}
2048+
parsed = Block.parse(input)
2049+
self.assertIsInstance(parsed, AuthProviderBlock)
2050+
self.assertIsInstance(parsed.accessory, OverflowMenuElement)
2051+
self.assertIsInstance(parsed.fallback[0], SectionBlock)
2052+
self.assertDictEqual(input, parsed.to_dict())
2053+
2054+
def test_text_strings_become_plain_text(self):
2055+
block = AuthProviderBlock(
2056+
value="lumon",
2057+
name="Lumon",
2058+
is_connected=True,
2059+
description="Connect your Lumon account",
2060+
connected_identity="mark.s@lumon.example.com",
2061+
connected_context="Macrodata Refinement",
2062+
)
2063+
self.assertDictEqual(
2064+
{
2065+
"type": "auth_provider",
2066+
"value": "lumon",
2067+
"name": {"type": "plain_text", "text": "Lumon", "emoji": True},
2068+
"is_connected": True,
2069+
"description": {"type": "plain_text", "text": "Connect your Lumon account", "emoji": True},
2070+
"connected_identity": {"type": "plain_text", "text": "mark.s@lumon.example.com", "emoji": True},
2071+
"connected_context": {"type": "plain_text", "text": "Macrodata Refinement", "emoji": True},
2072+
},
2073+
block.to_dict(),
2074+
)
2075+
2076+
def test_required_fields_validation(self):
2077+
for missing in ["value", "name", "is_connected"]:
2078+
input = {"value": "lumon", "name": "Lumon", "is_connected": False, missing: None}
2079+
with self.subTest(missing=missing), self.assertRaises(SlackObjectFormationError):
2080+
AuthProviderBlock(**input).validate_json()
2081+
2082+
def test_empty_value_validation(self):
2083+
with self.assertRaises(SlackObjectFormationError):
2084+
AuthProviderBlock(value="", name="Lumon", is_connected=False).validate_json()
2085+
2086+
def test_length_validation(self):
2087+
too_long = {
2088+
"value": "a" * 151,
2089+
"name": "a" * 76,
2090+
"description": "a" * 151,
2091+
"connected_identity": "a" * 76,
2092+
"connected_context": "a" * 76,
2093+
"icon_url": "https://example.com/" + "a" * 2981,
2094+
"auth_url": "https://example.com/" + "a" * 2981,
2095+
"disconnect_url": "https://example.com/" + "a" * 2981,
2096+
}
2097+
for field, value in too_long.items():
2098+
input = {"value": "lumon", "name": "Lumon", "is_connected": False, field: value}
2099+
with self.subTest(field=field), self.assertRaises(SlackObjectFormationError):
2100+
AuthProviderBlock(**input).validate_json()
2101+
2102+
def test_max_lengths_are_valid(self):
2103+
AuthProviderBlock(
2104+
value="a" * 150,
2105+
name="a" * 75,
2106+
is_connected=False,
2107+
description="a" * 150,
2108+
connected_identity="a" * 75,
2109+
connected_context="a" * 75,
2110+
icon_url="https://example.com/" + "a" * 2980,
2111+
auth_url="https://example.com/" + "a" * 2980,
2112+
disconnect_url="https://example.com/" + "a" * 2980,
2113+
).validate_json()
2114+
2115+
def test_accessory_must_be_overflow(self):
2116+
with self.assertRaises(SlackObjectFormationError):
2117+
AuthProviderBlock(
2118+
value="lumon",
2119+
name="Lumon",
2120+
is_connected=True,
2121+
accessory={"type": "button", "action_id": "connect", "text": {"type": "plain_text", "text": "Connect"}},
2122+
).validate_json()

0 commit comments

Comments
 (0)