diff --git a/.sqlx/query-32c807a8fae0af855391049ebc09c800888830ae050c4647a795c842e60f9d0c.json b/.sqlx/query-085ce27dd33eb6e55bca93a8859190f42b32c236c605b5c22fcc2fb762def411.json
similarity index 66%
rename from .sqlx/query-32c807a8fae0af855391049ebc09c800888830ae050c4647a795c842e60f9d0c.json
rename to .sqlx/query-085ce27dd33eb6e55bca93a8859190f42b32c236c605b5c22fcc2fb762def411.json
index 42c9511e297..55266fddee2 100644
--- a/.sqlx/query-32c807a8fae0af855391049ebc09c800888830ae050c4647a795c842e60f9d0c.json
+++ b/.sqlx/query-085ce27dd33eb6e55bca93a8859190f42b32c236c605b5c22fcc2fb762def411.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "SELECT g.id,g.name,g.user_id,g.backfill_state,g.backfill_imported,\n (SELECT count(*) FROM whatsapp_group_message m WHERE m.binding_id = g.id AND m.message_id IS NOT NULL\n AND m.deleted_at IS NULL AND m.origin <> 'history'\n AND NOT EXISTS (SELECT 1 FROM whatsapp_external_person p WHERE p.id = m.person_id AND p.user_id = $2)\n AND (r.message_id IS NULL OR (m.source_at,m.id) > (r.read_at,r.message_id))) AS \"unread_count!\",\n (SELECT max(source_at) FROM whatsapp_group_message WHERE binding_id = g.id AND message_id IS NOT NULL) AS last_message_at\n FROM whatsapp_group g LEFT JOIN whatsapp_group_read_state r ON r.group_id = g.id AND r.user_id = $2\n WHERE g.id = $1",
+ "query": "SELECT g.id,g.name,g.user_id,g.backfill_state,g.backfill_imported,\n (SELECT count(*) FROM whatsapp_group_message m WHERE m.binding_id = g.id AND m.message_id IS NOT NULL\n AND m.deleted_at IS NULL AND m.origin <> 'history' AND m.kind <> 'system'\n AND NOT EXISTS (SELECT 1 FROM whatsapp_external_person p WHERE p.id = m.person_id AND p.user_id = $2)\n AND (r.message_id IS NULL OR (m.source_at,m.id) > (r.read_at,r.message_id))) AS \"unread_count!\",\n (SELECT max(source_at) FROM whatsapp_group_message WHERE binding_id = g.id AND message_id IS NOT NULL) AS last_message_at\n FROM whatsapp_group g LEFT JOIN whatsapp_group_read_state r ON r.group_id = g.id AND r.user_id = $2\n WHERE g.id = $1",
"describe": {
"columns": [
{
@@ -55,5 +55,5 @@
null
]
},
- "hash": "32c807a8fae0af855391049ebc09c800888830ae050c4647a795c842e60f9d0c"
+ "hash": "085ce27dd33eb6e55bca93a8859190f42b32c236c605b5c22fcc2fb762def411"
}
diff --git a/.sqlx/query-8d7c3fc8a759d0bc6e72dc9a7c97cf274d7f92f55683af8645dca6ca5125ed46.json b/.sqlx/query-1e7e87d03030988ea937936c2ad13d417ecf46fdd43f6e4478e4634974e22faa.json
similarity index 54%
rename from .sqlx/query-8d7c3fc8a759d0bc6e72dc9a7c97cf274d7f92f55683af8645dca6ca5125ed46.json
rename to .sqlx/query-1e7e87d03030988ea937936c2ad13d417ecf46fdd43f6e4478e4634974e22faa.json
index f431ff62374..397513b4501 100644
--- a/.sqlx/query-8d7c3fc8a759d0bc6e72dc9a7c97cf274d7f92f55683af8645dca6ca5125ed46.json
+++ b/.sqlx/query-1e7e87d03030988ea937936c2ad13d417ecf46fdd43f6e4478e4634974e22faa.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "SELECT g.id,g.name,g.user_id,g.group_jid,g.state,g.adopted_at,g.backfill_state,g.backfill_imported,\n (SELECT count(*) FROM whatsapp_group_message m WHERE m.binding_id=g.id AND m.message_id IS NOT NULL\n AND m.deleted_at IS NULL AND m.origin<>'history'\n AND NOT EXISTS(SELECT 1 FROM whatsapp_external_person p WHERE p.id=m.person_id AND p.user_id=$1)\n AND (r.message_id IS NULL OR (m.source_at,m.id)>(r.read_at,r.message_id))) AS \"unread_count!\",\n (SELECT max(source_at) FROM whatsapp_group_message WHERE binding_id=g.id AND message_id IS NOT NULL) AS last_message_at,\n roster.metadata AS \"metadata?\"\n FROM whatsapp_group g\n LEFT JOIN whatsapp_group_read_state r ON r.group_id=g.id AND r.user_id=$1\n LEFT JOIN whatsapp_linked_account a ON a.user_id=$1\n LEFT JOIN whatsapp_group_roster roster ON roster.account_id=a.id AND roster.group_jid=g.group_jid\n WHERE whatsapp_group_can_view(g.id,$1)\n AND (r.user_id IS NOT NULL OR EXISTS (SELECT 1 FROM entity_access ea\n WHERE ea.entity_id=g.id AND ea.entity_type='whatsapp_group'\n AND (ea.source_id=$1 OR ea.source_id IN (SELECT team_id::text FROM team_user WHERE user_id=$1)\n OR ea.source_id IN (SELECT channel_id::text FROM comms_channel_participants WHERE user_id=$1 AND left_at IS NULL))))\n ORDER BY g.adopted_at DESC LIMIT 2000",
+ "query": "SELECT g.id,g.name,g.user_id,g.group_jid,g.state,g.adopted_at,g.backfill_state,g.backfill_imported,\n (SELECT count(*) FROM whatsapp_group_message m WHERE m.binding_id=g.id AND m.message_id IS NOT NULL\n AND m.deleted_at IS NULL AND m.origin<>'history' AND m.kind<>'system'\n AND NOT EXISTS(SELECT 1 FROM whatsapp_external_person p WHERE p.id=m.person_id AND p.user_id=$1)\n AND (r.message_id IS NULL OR (m.source_at,m.id)>(r.read_at,r.message_id))) AS \"unread_count!\",\n (SELECT max(source_at) FROM whatsapp_group_message WHERE binding_id=g.id AND message_id IS NOT NULL) AS last_message_at,\n roster.metadata AS \"metadata?\"\n FROM whatsapp_group g\n LEFT JOIN whatsapp_group_read_state r ON r.group_id=g.id AND r.user_id=$1\n LEFT JOIN whatsapp_linked_account a ON a.user_id=$1\n LEFT JOIN whatsapp_group_roster roster ON roster.account_id=a.id AND roster.group_jid=g.group_jid\n WHERE whatsapp_group_can_view(g.id,$1)\n AND (r.user_id IS NOT NULL OR EXISTS (SELECT 1 FROM entity_access ea\n WHERE ea.entity_id=g.id AND ea.entity_type='whatsapp_group'\n AND (ea.source_id=$1 OR ea.source_id IN (SELECT team_id::text FROM team_user WHERE user_id=$1)\n OR ea.source_id IN (SELECT channel_id::text FROM comms_channel_participants WHERE user_id=$1 AND left_at IS NULL))))\n ORDER BY g.adopted_at DESC LIMIT 2000",
"describe": {
"columns": [
{
@@ -78,5 +78,5 @@
false
]
},
- "hash": "8d7c3fc8a759d0bc6e72dc9a7c97cf274d7f92f55683af8645dca6ca5125ed46"
+ "hash": "1e7e87d03030988ea937936c2ad13d417ecf46fdd43f6e4478e4634974e22faa"
}
diff --git a/.sqlx/query-1ec648607537f38f6f11d01800720aa9d036a1d349e2b3ff71c8ecb299720b5a.json b/.sqlx/query-1ec648607537f38f6f11d01800720aa9d036a1d349e2b3ff71c8ecb299720b5a.json
new file mode 100644
index 00000000000..10b1e7e5174
--- /dev/null
+++ b/.sqlx/query-1ec648607537f38f6f11d01800720aa9d036a1d349e2b3ff71c8ecb299720b5a.json
@@ -0,0 +1,29 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "INSERT INTO whatsapp_group_attachment (binding_id, person_id, source_id, ordinal, document_id, sha256, state, account_id, locator, mime, filename, size,\n width, height, duration_seconds, presentation)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)\n ON CONFLICT (binding_id, person_id, source_id, ordinal) DO UPDATE SET account_id = excluded.account_id, locator = excluded.locator\n WHERE whatsapp_group_attachment.state = 'pending'\n AND (whatsapp_group_attachment.claimed_until IS NULL OR whatsapp_group_attachment.claimed_until < now())",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Uuid",
+ "Uuid",
+ "Text",
+ "Int2",
+ "Uuid",
+ "Bytea",
+ "Text",
+ "Uuid",
+ "Bytea",
+ "Text",
+ "Text",
+ "Int8",
+ "Int4",
+ "Int4",
+ "Int4",
+ "Text"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "1ec648607537f38f6f11d01800720aa9d036a1d349e2b3ff71c8ecb299720b5a"
+}
diff --git a/.sqlx/query-27ae44ded89969dc23fce3b695f1cb3307aeb8223387bcfdbb78888b661e6153.json b/.sqlx/query-27ae44ded89969dc23fce3b695f1cb3307aeb8223387bcfdbb78888b661e6153.json
deleted file mode 100644
index 2adf6a8003b..00000000000
--- a/.sqlx/query-27ae44ded89969dc23fce3b695f1cb3307aeb8223387bcfdbb78888b661e6153.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "INSERT INTO whatsapp_group_message (id, binding_id, person_id, source_id, origin, source_at, content_revision, deleted_at, revision_content, revised_at)\n VALUES ($11, $1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (binding_id, person_id, source_id) DO UPDATE SET\n deleted_at = COALESCE(whatsapp_group_message.deleted_at, excluded.deleted_at),\n revision_content = CASE WHEN excluded.content_revision > whatsapp_group_message.content_revision\n THEN excluded.revision_content ELSE whatsapp_group_message.revision_content END,\n revised_at = CASE WHEN excluded.content_revision > whatsapp_group_message.content_revision\n THEN excluded.revised_at ELSE whatsapp_group_message.revised_at END,\n content_revision = GREATEST(whatsapp_group_message.content_revision, excluded.content_revision),\n source_at = CASE WHEN $10 THEN whatsapp_group_message.source_at ELSE excluded.source_at END",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Uuid",
- "Uuid",
- "Text",
- "Text",
- "Timestamptz",
- "Int8",
- "Timestamptz",
- "Text",
- "Timestamptz",
- "Bool",
- "Uuid"
- ]
- },
- "nullable": []
- },
- "hash": "27ae44ded89969dc23fce3b695f1cb3307aeb8223387bcfdbb78888b661e6153"
-}
diff --git a/.sqlx/query-2a9d397b081f000ba0f5088b50aec57e7693363c025bb243975e6985745036aa.json b/.sqlx/query-2a9d397b081f000ba0f5088b50aec57e7693363c025bb243975e6985745036aa.json
new file mode 100644
index 00000000000..302a5db4783
--- /dev/null
+++ b/.sqlx/query-2a9d397b081f000ba0f5088b50aec57e7693363c025bb243975e6985745036aa.json
@@ -0,0 +1,40 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "SELECT v.message_id, v.selected, p.user_id, COALESCE(p.display_name, 'WhatsApp participant') AS \"name!\"\n FROM whatsapp_group_poll_vote v JOIN whatsapp_external_person p ON p.id = v.person_id\n WHERE v.message_id = ANY($1) AND cardinality(v.selected) > 0\n ORDER BY v.updated_at",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "message_id",
+ "type_info": "Uuid"
+ },
+ {
+ "ordinal": 1,
+ "name": "selected",
+ "type_info": "TextArray"
+ },
+ {
+ "ordinal": 2,
+ "name": "user_id",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 3,
+ "name": "name!",
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "UuidArray"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ true,
+ null
+ ]
+ },
+ "hash": "2a9d397b081f000ba0f5088b50aec57e7693363c025bb243975e6985745036aa"
+}
diff --git a/.sqlx/query-7eabbeb6c17149237050a2f03995c515031a1f354dee34b417e8f5e094eef458.json b/.sqlx/query-2eec5aa6fdc9ad732028862d85eb30a925145637fbf3265c9f2cdade195a5222.json
similarity index 66%
rename from .sqlx/query-7eabbeb6c17149237050a2f03995c515031a1f354dee34b417e8f5e094eef458.json
rename to .sqlx/query-2eec5aa6fdc9ad732028862d85eb30a925145637fbf3265c9f2cdade195a5222.json
index f4c3893bab3..ae983877b4d 100644
--- a/.sqlx/query-7eabbeb6c17149237050a2f03995c515031a1f354dee34b417e8f5e094eef458.json
+++ b/.sqlx/query-2eec5aa6fdc9ad732028862d85eb30a925145637fbf3265c9f2cdade195a5222.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "SELECT b.id AS group_id, m.message_id AS \"message_id!\", m.effect_revision, m.published_revision, m.origin, m.content_revision, m.deleted_at, p.user_id\n FROM whatsapp_group_message m\n JOIN whatsapp_group b ON b.id = m.binding_id\n JOIN whatsapp_external_person p ON p.id = m.person_id\n WHERE m.message_id IS NOT NULL AND m.published_revision < m.effect_revision AND m.notification_retry_at <= now()\n ORDER BY m.notification_retry_at, m.message_id LIMIT $1",
+ "query": "SELECT b.id AS group_id, m.message_id AS \"message_id!\", m.effect_revision, m.published_revision, m.origin, m.content_revision, m.deleted_at, p.user_id, m.kind\n FROM whatsapp_group_message m\n JOIN whatsapp_group b ON b.id = m.binding_id\n JOIN whatsapp_external_person p ON p.id = m.person_id\n WHERE m.message_id IS NOT NULL AND m.published_revision < m.effect_revision AND m.notification_retry_at <= now()\n ORDER BY m.notification_retry_at, m.message_id LIMIT $1",
"describe": {
"columns": [
{
@@ -42,6 +42,11 @@
"ordinal": 7,
"name": "user_id",
"type_info": "Text"
+ },
+ {
+ "ordinal": 8,
+ "name": "kind",
+ "type_info": "Text"
}
],
"parameters": {
@@ -57,8 +62,9 @@
false,
false,
true,
- true
+ true,
+ false
]
},
- "hash": "7eabbeb6c17149237050a2f03995c515031a1f354dee34b417e8f5e094eef458"
+ "hash": "2eec5aa6fdc9ad732028862d85eb30a925145637fbf3265c9f2cdade195a5222"
}
diff --git a/.sqlx/query-5ccda92d15fe26c668003a56bed94e33492b672347814cba041174ae1b579d0e.json b/.sqlx/query-5ccda92d15fe26c668003a56bed94e33492b672347814cba041174ae1b579d0e.json
new file mode 100644
index 00000000000..973af5835b7
--- /dev/null
+++ b/.sqlx/query-5ccda92d15fe26c668003a56bed94e33492b672347814cba041174ae1b579d0e.json
@@ -0,0 +1,29 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "SELECT a.jid, p.display_name AS \"display_name!\" FROM whatsapp_identity_alias a\n JOIN whatsapp_external_person p ON p.id = a.person_id\n WHERE a.binding_id = $1 AND a.jid = ANY($2) AND p.display_name IS NOT NULL",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "jid",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 1,
+ "name": "display_name!",
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Uuid",
+ "TextArray"
+ ]
+ },
+ "nullable": [
+ false,
+ true
+ ]
+ },
+ "hash": "5ccda92d15fe26c668003a56bed94e33492b672347814cba041174ae1b579d0e"
+}
diff --git a/.sqlx/query-ec54337faac38b631e72921bc03c8d7aa33aa63beafa8aa13b032488a892e47d.json b/.sqlx/query-6f2c4bb9d028f5fe23c3358858dfa8524707cbf5e38ebb03e2f8bcbd683fb493.json
similarity index 57%
rename from .sqlx/query-ec54337faac38b631e72921bc03c8d7aa33aa63beafa8aa13b032488a892e47d.json
rename to .sqlx/query-6f2c4bb9d028f5fe23c3358858dfa8524707cbf5e38ebb03e2f8bcbd683fb493.json
index c8c94731d6a..f8dd9156c22 100644
--- a/.sqlx/query-ec54337faac38b631e72921bc03c8d7aa33aa63beafa8aa13b032488a892e47d.json
+++ b/.sqlx/query-6f2c4bb9d028f5fe23c3358858dfa8524707cbf5e38ebb03e2f8bcbd683fb493.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "SELECT m.id,m.person_id,p.user_id,COALESCE(p.display_name,'WhatsApp participant') AS \"name!\",\n m.body,m.reply_to,m.source_at,m.ingested_at,m.revised_at,m.deleted_at,\n (SELECT state FROM whatsapp_send s WHERE s.message_id = m.id AND s.user_id = $2 ORDER BY s.created_at DESC LIMIT 1) AS delivery\n FROM whatsapp_group_message m JOIN whatsapp_external_person p ON p.id = m.person_id\n WHERE m.binding_id = $1 AND m.message_id IS NOT NULL\n AND ($3::uuid IS NULL OR (m.source_at,m.id) < (SELECT source_at,id FROM whatsapp_group_message WHERE id = $3 AND binding_id = $1 AND message_id IS NOT NULL))\n ORDER BY m.source_at DESC,m.id DESC LIMIT $4",
+ "query": "SELECT m.id,m.person_id,p.user_id,COALESCE(p.display_name,'WhatsApp participant') AS \"name!\",\n m.body,m.reply_to,m.source_at,m.ingested_at,m.revised_at,m.deleted_at,m.kind,m.rich,m.mentions,m.forwarded,\n (SELECT state FROM whatsapp_send s WHERE s.message_id = m.id AND s.user_id = $2 ORDER BY s.created_at DESC LIMIT 1) AS delivery\n FROM whatsapp_group_message m JOIN whatsapp_external_person p ON p.id = m.person_id\n WHERE m.binding_id = $1 AND m.message_id IS NOT NULL\n AND ($3::uuid IS NULL OR (m.source_at,m.id) < (SELECT source_at,id FROM whatsapp_group_message WHERE id = $3 AND binding_id = $1 AND message_id IS NOT NULL))\n ORDER BY m.source_at DESC,m.id DESC LIMIT $4",
"describe": {
"columns": [
{
@@ -55,6 +55,26 @@
},
{
"ordinal": 10,
+ "name": "kind",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 11,
+ "name": "rich",
+ "type_info": "Jsonb"
+ },
+ {
+ "ordinal": 12,
+ "name": "mentions",
+ "type_info": "TextArray"
+ },
+ {
+ "ordinal": 13,
+ "name": "forwarded",
+ "type_info": "Bool"
+ },
+ {
+ "ordinal": 14,
"name": "delivery",
"type_info": "Text"
}
@@ -78,8 +98,12 @@
false,
true,
true,
+ false,
+ true,
+ false,
+ false,
null
]
},
- "hash": "ec54337faac38b631e72921bc03c8d7aa33aa63beafa8aa13b032488a892e47d"
+ "hash": "6f2c4bb9d028f5fe23c3358858dfa8524707cbf5e38ebb03e2f8bcbd683fb493"
}
diff --git a/.sqlx/query-33a09037b30fb14df0aed97f03702d4753f9d9a1628df317838730b3f2614b74.json b/.sqlx/query-739be933b590e21d87681ef4cbdfb28c3e30223e01ecd48c65049d4d6a194062.json
similarity index 50%
rename from .sqlx/query-33a09037b30fb14df0aed97f03702d4753f9d9a1628df317838730b3f2614b74.json
rename to .sqlx/query-739be933b590e21d87681ef4cbdfb28c3e30223e01ecd48c65049d4d6a194062.json
index bed821a4182..da1061db1d2 100644
--- a/.sqlx/query-33a09037b30fb14df0aed97f03702d4753f9d9a1628df317838730b3f2614b74.json
+++ b/.sqlx/query-739be933b590e21d87681ef4cbdfb28c3e30223e01ecd48c65049d4d6a194062.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "SELECT m.message_id AS \"message_id!\", a.ordinal, a.state, a.filename, a.mime, a.size, a.document_id\n FROM whatsapp_group_attachment a\n JOIN whatsapp_group_message m ON m.binding_id = a.binding_id AND m.person_id = a.person_id AND m.source_id = a.source_id\n WHERE a.binding_id = $1 AND m.message_id = ANY($2) AND a.state <> 'deleted'\n ORDER BY m.message_id, a.ordinal",
+ "query": "SELECT m.message_id AS \"message_id!\", a.ordinal, a.state, a.filename, a.mime, a.size, a.document_id,\n a.width, a.height, a.duration_seconds, a.presentation\n FROM whatsapp_group_attachment a\n JOIN whatsapp_group_message m ON m.binding_id = a.binding_id AND m.person_id = a.person_id AND m.source_id = a.source_id\n WHERE a.binding_id = $1 AND m.message_id = ANY($2) AND a.state <> 'deleted'\n ORDER BY m.message_id, a.ordinal",
"describe": {
"columns": [
{
@@ -37,6 +37,26 @@
"ordinal": 6,
"name": "document_id",
"type_info": "Uuid"
+ },
+ {
+ "ordinal": 7,
+ "name": "width",
+ "type_info": "Int4"
+ },
+ {
+ "ordinal": 8,
+ "name": "height",
+ "type_info": "Int4"
+ },
+ {
+ "ordinal": 9,
+ "name": "duration_seconds",
+ "type_info": "Int4"
+ },
+ {
+ "ordinal": 10,
+ "name": "presentation",
+ "type_info": "Text"
}
],
"parameters": {
@@ -52,8 +72,12 @@
true,
true,
true,
- false
+ false,
+ true,
+ true,
+ true,
+ true
]
},
- "hash": "33a09037b30fb14df0aed97f03702d4753f9d9a1628df317838730b3f2614b74"
+ "hash": "739be933b590e21d87681ef4cbdfb28c3e30223e01ecd48c65049d4d6a194062"
}
diff --git a/.sqlx/query-783e09251a6d6a0f29260c9fed03b41c9116d8cbe6f145f27a77a140fa1254bc.json b/.sqlx/query-783e09251a6d6a0f29260c9fed03b41c9116d8cbe6f145f27a77a140fa1254bc.json
deleted file mode 100644
index ab9c9a17006..00000000000
--- a/.sqlx/query-783e09251a6d6a0f29260c9fed03b41c9116d8cbe6f145f27a77a140fa1254bc.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "INSERT INTO whatsapp_group_attachment (binding_id, person_id, source_id, ordinal, document_id, sha256, state, account_id, locator, mime, filename, size)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)\n ON CONFLICT (binding_id, person_id, source_id, ordinal) DO UPDATE SET account_id = excluded.account_id, locator = excluded.locator\n WHERE whatsapp_group_attachment.state = 'pending'\n AND (whatsapp_group_attachment.claimed_until IS NULL OR whatsapp_group_attachment.claimed_until < now())",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Uuid",
- "Uuid",
- "Text",
- "Int2",
- "Uuid",
- "Bytea",
- "Text",
- "Uuid",
- "Bytea",
- "Text",
- "Text",
- "Int8"
- ]
- },
- "nullable": []
- },
- "hash": "783e09251a6d6a0f29260c9fed03b41c9116d8cbe6f145f27a77a140fa1254bc"
-}
diff --git a/.sqlx/query-d1850dbabaf90338694f2db99e2ac8a74805f05038e9d0c93168c80077e78d68.json b/.sqlx/query-d1850dbabaf90338694f2db99e2ac8a74805f05038e9d0c93168c80077e78d68.json
new file mode 100644
index 00000000000..959d0557fa2
--- /dev/null
+++ b/.sqlx/query-d1850dbabaf90338694f2db99e2ac8a74805f05038e9d0c93168c80077e78d68.json
@@ -0,0 +1,21 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "INSERT INTO whatsapp_participant_receipt (account_id, binding_id, source_id, participant_jid, receipt_state, source_at)\n SELECT $1, b.id, source.id, $5, $6, $7\n FROM whatsapp_group b JOIN whatsapp_group_membership m ON m.binding_id = b.id\n CROSS JOIN (SELECT DISTINCT unnest($4::text[]) AS id) source\n WHERE b.group_jid = $2 AND m.account_id = $1 AND m.generation = $3 AND m.consent_ended_at IS NULL\n AND whatsapp_group_can_view(b.id, $8)\n ON CONFLICT (account_id, binding_id, source_id, participant_jid, receipt_state)\n DO UPDATE SET source_at = GREATEST(whatsapp_participant_receipt.source_at, excluded.source_at)",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Uuid",
+ "Text",
+ "Int8",
+ "TextArray",
+ "Text",
+ "Text",
+ "Timestamptz",
+ "Text"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "d1850dbabaf90338694f2db99e2ac8a74805f05038e9d0c93168c80077e78d68"
+}
diff --git a/.sqlx/query-dc8378e67806eb8241b98861b755d67fe24dba43990b1f8464faf7e7d3b28837.json b/.sqlx/query-dc8378e67806eb8241b98861b755d67fe24dba43990b1f8464faf7e7d3b28837.json
new file mode 100644
index 00000000000..589660b4b45
--- /dev/null
+++ b/.sqlx/query-dc8378e67806eb8241b98861b755d67fe24dba43990b1f8464faf7e7d3b28837.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "INSERT INTO whatsapp_group_poll_vote (message_id, person_id, selected, user_id, source_id, revision, updated_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT (message_id, person_id) DO UPDATE SET selected = excluded.selected, user_id = excluded.user_id,\n source_id = excluded.source_id, revision = excluded.revision, updated_at = excluded.updated_at\n WHERE (excluded.revision, excluded.updated_at, excluded.source_id)\n > (whatsapp_group_poll_vote.revision, whatsapp_group_poll_vote.updated_at, whatsapp_group_poll_vote.source_id)",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Uuid",
+ "Uuid",
+ "TextArray",
+ "Text",
+ "Text",
+ "Int8",
+ "Timestamptz"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "dc8378e67806eb8241b98861b755d67fe24dba43990b1f8464faf7e7d3b28837"
+}
diff --git a/.sqlx/query-dfb1b287b5508eb4a8b5ba8f107e6876e54fb96526d7374f8aa29321577a0711.json b/.sqlx/query-dfb1b287b5508eb4a8b5ba8f107e6876e54fb96526d7374f8aa29321577a0711.json
new file mode 100644
index 00000000000..8af0dbfe432
--- /dev/null
+++ b/.sqlx/query-dfb1b287b5508eb4a8b5ba8f107e6876e54fb96526d7374f8aa29321577a0711.json
@@ -0,0 +1,14 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "DELETE FROM whatsapp_participant_receipt WHERE source_at < now() - make_interval(days => $1)",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Int4"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "dfb1b287b5508eb4a8b5ba8f107e6876e54fb96526d7374f8aa29321577a0711"
+}
diff --git a/.sqlx/query-e61fad563dae898406a707db2cbc7d88956e66269abd7ccf4bbbf87d2c809be6.json b/.sqlx/query-e61fad563dae898406a707db2cbc7d88956e66269abd7ccf4bbbf87d2c809be6.json
new file mode 100644
index 00000000000..1f80f9e646c
--- /dev/null
+++ b/.sqlx/query-e61fad563dae898406a707db2cbc7d88956e66269abd7ccf4bbbf87d2c809be6.json
@@ -0,0 +1,35 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "SELECT a.jid, p.user_id, p.display_name FROM whatsapp_identity_alias a\n JOIN whatsapp_external_person p ON p.id = a.person_id\n WHERE a.binding_id = $1 AND a.jid = ANY($2)",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "jid",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 1,
+ "name": "user_id",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 2,
+ "name": "display_name",
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Uuid",
+ "TextArray"
+ ]
+ },
+ "nullable": [
+ false,
+ true,
+ true
+ ]
+ },
+ "hash": "e61fad563dae898406a707db2cbc7d88956e66269abd7ccf4bbbf87d2c809be6"
+}
diff --git a/.sqlx/query-e65bc85a48020c879d7664a21d4af72f4d6591e9ceb5e7205b802fbf49106066.json b/.sqlx/query-e65bc85a48020c879d7664a21d4af72f4d6591e9ceb5e7205b802fbf49106066.json
deleted file mode 100644
index 306f0312d66..00000000000
--- a/.sqlx/query-e65bc85a48020c879d7664a21d4af72f4d6591e9ceb5e7205b802fbf49106066.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "INSERT INTO whatsapp_participant_receipt (account_id, binding_id, source_id, participant_jid, receipt_state, source_at)\n SELECT $1, b.id, $4, $5, $6, $7 FROM whatsapp_group b JOIN whatsapp_group_membership m ON m.binding_id = b.id\n WHERE b.group_jid = $2 AND m.account_id = $1 AND m.generation = $3 AND m.consent_ended_at IS NULL\n AND whatsapp_group_can_view(b.id, $8)\n ON CONFLICT (account_id, binding_id, source_id, participant_jid, receipt_state)\n DO UPDATE SET source_at = GREATEST(whatsapp_participant_receipt.source_at, excluded.source_at)",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Uuid",
- "Text",
- "Int8",
- "Text",
- "Text",
- "Text",
- "Timestamptz",
- "Text"
- ]
- },
- "nullable": []
- },
- "hash": "e65bc85a48020c879d7664a21d4af72f4d6591e9ceb5e7205b802fbf49106066"
-}
diff --git a/.sqlx/query-f8fe3772d8b504250171669bb2ca171127e5e9e5c4db119fd65e351d232fc5e9.json b/.sqlx/query-f8fe3772d8b504250171669bb2ca171127e5e9e5c4db119fd65e351d232fc5e9.json
new file mode 100644
index 00000000000..dbf8c3373f4
--- /dev/null
+++ b/.sqlx/query-f8fe3772d8b504250171669bb2ca171127e5e9e5c4db119fd65e351d232fc5e9.json
@@ -0,0 +1,22 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "SELECT r.metadata FROM whatsapp_group_roster r\n JOIN whatsapp_linked_account a ON a.id = r.account_id\n JOIN whatsapp_group g ON g.user_id = a.user_id AND g.group_jid = r.group_jid\n WHERE g.id = $1",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "metadata",
+ "type_info": "Jsonb"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Uuid"
+ ]
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "f8fe3772d8b504250171669bb2ca171127e5e9e5c4db119fd65e351d232fc5e9"
+}
diff --git a/.sqlx/query-ffe827ebcfa81b6aa7e166f299aa3df33f6582f18848f092e28801b131536f27.json b/.sqlx/query-ffe827ebcfa81b6aa7e166f299aa3df33f6582f18848f092e28801b131536f27.json
new file mode 100644
index 00000000000..9df29e01839
--- /dev/null
+++ b/.sqlx/query-ffe827ebcfa81b6aa7e166f299aa3df33f6582f18848f092e28801b131536f27.json
@@ -0,0 +1,28 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "INSERT INTO whatsapp_group_message (id, binding_id, person_id, source_id, origin, source_at, content_revision, deleted_at, revision_content, revised_at,\n kind, rich, mentions, forwarded)\n VALUES ($11, $1, $2, $3, $4, $5, $6, $7, $8, $9, $12, $13, $14, $15)\n ON CONFLICT (binding_id, person_id, source_id) DO UPDATE SET\n deleted_at = COALESCE(whatsapp_group_message.deleted_at, excluded.deleted_at),\n revision_content = CASE WHEN excluded.content_revision > whatsapp_group_message.content_revision\n THEN excluded.revision_content ELSE whatsapp_group_message.revision_content END,\n revised_at = CASE WHEN excluded.content_revision > whatsapp_group_message.content_revision\n THEN excluded.revised_at ELSE whatsapp_group_message.revised_at END,\n content_revision = GREATEST(whatsapp_group_message.content_revision, excluded.content_revision),\n source_at = CASE WHEN $10 THEN whatsapp_group_message.source_at ELSE excluded.source_at END,\n kind = CASE WHEN $10 THEN whatsapp_group_message.kind ELSE excluded.kind END,\n rich = CASE WHEN $10 THEN whatsapp_group_message.rich ELSE COALESCE(excluded.rich, whatsapp_group_message.rich) END,\n mentions = CASE WHEN $10 OR cardinality(excluded.mentions) = 0 THEN whatsapp_group_message.mentions ELSE excluded.mentions END,\n forwarded = whatsapp_group_message.forwarded OR excluded.forwarded",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Uuid",
+ "Uuid",
+ "Text",
+ "Text",
+ "Timestamptz",
+ "Int8",
+ "Timestamptz",
+ "Text",
+ "Timestamptz",
+ "Bool",
+ "Uuid",
+ "Text",
+ "Jsonb",
+ "TextArray",
+ "Bool"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "ffe827ebcfa81b6aa7e166f299aa3df33f6582f18848f092e28801b131536f27"
+}
diff --git a/Cargo.lock b/Cargo.lock
index 83783cd2455..5db4fa0fa5d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -21659,6 +21659,7 @@ dependencies = [
"sha2 0.10.9",
"share_permission_db_utils",
"sqlx",
+ "strum 0.27.2",
"thiserror 2.0.18",
"tokio",
"tower 0.5.3",
@@ -21687,6 +21688,7 @@ dependencies = [
"macro_env",
"macro_env_var",
"macro_service_urls",
+ "nom 8.0.0",
"rootcause",
"rustls",
"serde",
diff --git a/apps/web/src/features/channel/Input/ChannelInput.tsx b/apps/web/src/features/channel/Input/ChannelInput.tsx
index f8c001be707..19be6e1b6bd 100644
--- a/apps/web/src/features/channel/Input/ChannelInput.tsx
+++ b/apps/web/src/features/channel/Input/ChannelInput.tsx
@@ -102,8 +102,12 @@ import { hasSendableInputContent } from './utils/sendable-content';
* Hosts that carry only those, such as a WhatsApp group, switch the rest off.
*/
export type ChannelInputFeatures = {
- /** `@` mentions of people, agents and Macro items, including Ask Macro. */
- mentions: boolean;
+ /**
+ * `@` mentions of people, agents and Macro items, including Ask Macro.
+ * `'participants'` offers only the `participants` prop: no agents, items,
+ * `@here` or mention tracking (a WhatsApp group's own members).
+ */
+ mentions: boolean | 'participants';
/** Creating Macro items from the composer: tasks, documents, polls, forms. */
createItems: boolean;
gifs: boolean;
@@ -358,19 +362,25 @@ export function ChannelInput(props: ChannelInputProps) {
: () => [];
// Connection-prompt behavior for the built-in agents lives in
// useAgentMentionUsers; participants and parent agents feed it here.
- const mentionUsers = useAgentMentionUsers(() => [
- ...(props.participants?.() ?? parentParticipants()),
- ...(props.bots?.() ?? parentBots()),
- ]);
-
- const groupMentions = !props.parent || props.parent.type === 'channel';
// The editor is configured once, at mount.
const initialFeatures = features();
+ const participantsOnly = initialFeatures.mentions === 'participants';
+ const mentionUsers = participantsOnly
+ ? () => props.participants?.() ?? []
+ : useAgentMentionUsers(() => [
+ ...(props.participants?.() ?? parentParticipants()),
+ ...(props.bots?.() ?? parentBots()),
+ ]);
+
+ const groupMentions =
+ !participantsOnly && (!props.parent || props.parent.type === 'channel');
const markdownEditor = createConfiguredChannelMarkdownEditor({
groupMentions,
namespace: props.markdownNamespace ?? 'channel-input-markdown',
resolveAppLink: useMacroMentionLinkResolver(),
- enableMentions: initialFeatures.mentions,
+ enableMentions: initialFeatures.mentions !== false,
+ mentionSources: participantsOnly ? ['users'] : undefined,
+ disableMentionTracking: participantsOnly || undefined,
composerActions: initialFeatures.createItems,
ignoreActionIds: [
...(initialFeatures.createItems ? [] : CREATE_ITEM_ACTION_IDS),
@@ -496,7 +506,7 @@ export function ChannelInput(props: ChannelInputProps) {
},
];
};
- const macro = features().mentions ? macroUser() : undefined;
+ const macro = features().mentions === true ? macroUser() : undefined;
const startCall = props.onStartCall;
const callInProgress = !!props.callInProgress;
return [
diff --git a/apps/web/src/features/channel/components/SystemActivityRow.tsx b/apps/web/src/features/channel/components/SystemActivityRow.tsx
index 1752758219f..355fdeb1080 100644
--- a/apps/web/src/features/channel/components/SystemActivityRow.tsx
+++ b/apps/web/src/features/channel/components/SystemActivityRow.tsx
@@ -30,36 +30,70 @@ export function SystemActivityRow(props: {
}) {
const description = () =>
systemActivityDescription(props.event, props.participantName);
- const label = () =>
- `${props.actorName} ${describeSystemActivity(props.event, props.participantName)}`;
+ return (
+
+ );
+}
+
+/**
+ * The row itself, for any conversation's system events (a WhatsApp group's
+ * included): icon in the avatar column, bold actor and subject, time.
+ */
+export function SystemActivityLine(props: {
+ id: string;
+ icon: typeof PencilIcon;
+ iconClass?: string;
+ /** Omitted when nobody is named; the verb then starts the line. */
+ actor?: string;
+ verb: string;
+ subject?: string;
+ /** The whole line, for the tooltip. */
+ label: string;
+ occurredAt: string;
+}) {
return (
- {props.actorName} {' '}
- {description().verb}
-
+
+ {(actor) => (
+ <>
+ {actor()} {' '}
+ >
+ )}
+
+ {props.verb}
+
{(subject) => (
<>
{' '}
@@ -71,10 +105,10 @@ export function SystemActivityRow(props: {
);
diff --git a/apps/web/src/features/whatsapp-group/AGENTS.md b/apps/web/src/features/whatsapp-group/AGENTS.md
index b0d8110ab65..a9cb0719a5e 100644
--- a/apps/web/src/features/whatsapp-group/AGENTS.md
+++ b/apps/web/src/features/whatsapp-group/AGENTS.md
@@ -43,7 +43,28 @@ lists belong to `features/whatsapp-linked` and `features/external-view`.
- Images, video and audio play inline: `fileUrl` resolves the imported
document's presigned URL and `InboundMedia` hands it to Chat's
`MediaPreview` (or an audio player); `groupMessageData` keeps them out of
- the document chips. DSS sends no dimensions, so images size on load.
+ the document chips. DSS's declared width/height reserve the space (keeps
+ the open-at-bottom scroll right); stickers, GIFs and voice notes follow the
+ attachment's `presentation`.
+- `kind: 'system'` rows render through Chat's `SystemActivityLine`
+ (`components/system-line.tsx`, words in `core/rich.ts`) and enter the list
+ meta as `activity` entries; they never count toward the unread divider.
+- `rich` polls, locations, contacts and events are small cards
+ (`components/rich-cards.tsx`); votes happen in WhatsApp only. Map links are
+ built from coordinates; nothing is fetched to draw them. Sender-supplied
+ links (a location's URL, an event's join link) survive decoding only as
+ http(s) (`webLink`).
+- Received text is untrusted: `whatsappToMarkdown` turns every punctuation
+ mark into a `NN;` reference, so no Markdown, HTML or Macro tag a sender
+ types takes effect. Only what it writes itself is Markdown: WhatsApp's
+ formatting, links for bare http(s) URLs (so Chat's link preview applies),
+ and for each `@` DSS matched, Chat's user-mention chip (verified
+ Macro users) or the WhatsApp name in bold. Inline code stays a code span
+ only when it holds nothing a tag could start with.
+- The composer runs `ChannelInput` with `mentions: 'participants'`: the `@`
+ menu lists the roster (ids are JIDs, minus the sender) and nothing else;
+ `composerMentions` turns the chips into `@` plus `mentions` on the
+ text send.
- Senders show their WhatsApp name (`imported_author`), Macro users included.
- Use backend limits for text, files, edits and deletes.
- Keep phone numbers and message bodies out of telemetry.
diff --git a/apps/web/src/features/whatsapp-group/components/inbound-media.tsx b/apps/web/src/features/whatsapp-group/components/inbound-media.tsx
index 0ea82fb2c25..b354ed9b683 100644
--- a/apps/web/src/features/whatsapp-group/components/inbound-media.tsx
+++ b/apps/web/src/features/whatsapp-group/components/inbound-media.tsx
@@ -1,6 +1,9 @@
import type { MediaItem } from '@channel/Media/media-items';
import { MediaPreview } from '@channel/Message/MediaPreview';
-import { For, Show } from 'solid-js';
+import { constrainImageDimensions } from '@macro-inc/lexical-core/utils/media';
+import MicrophoneIcon from '@phosphor/microphone.svg';
+import { For, Match, Show, Switch } from 'solid-js';
+import { formatDuration } from '../core/rich';
/** An imported image, video or voice note with its resolved file URL. */
export type InboundMediaFile = {
@@ -9,55 +12,187 @@ export type InboundMediaFile = {
fileName: string;
/** Undefined while the URL resolves. */
url: string | undefined;
+ /** Declared by WhatsApp; reserves the space before the file loads. */
+ width?: number;
+ height?: number;
+ durationSeconds?: number;
+ presentation?: 'sticker' | 'gif' | 'voice_note';
};
+/** Chat's single-image bound, so a GIF sits like a photo would. */
+const MEDIA_MAX = 400;
+const STICKER_MAX = 144;
+
+function boxFor(file: InboundMediaFile, max: number) {
+ return (
+ constrainImageDimensions(file.width, file.height, max, max) ?? {
+ width: max,
+ height: max,
+ }
+ );
+}
+
+/** Placeholder the size the file will be, so the timeline doesn't jump. */
+function Reserved(props: { file: InboundMediaFile; max: number }) {
+ return (
+
+ );
+}
+
+/** Sticker size, no frame, like WhatsApp. */
+function Sticker(props: { file: InboundMediaFile }) {
+ return (
+ }
+ >
+ {(url) => (
+
+ )}
+
+ );
+}
+
+/** A GIF loops silently in place, like Chat's inline GIFs. */
+function Gif(props: { file: InboundMediaFile }) {
+ return (
+ }
+ >
+ {(url) => (
+ {
+ video.muted = true;
+ }}
+ width={boxFor(props.file, MEDIA_MAX).width}
+ height={boxFor(props.file, MEDIA_MAX).height}
+ class="block h-auto max-w-full rounded-2xl border border-edge bg-surface"
+ />
+ )}
+
+ );
+}
+
+function Audio(props: { file: InboundMediaFile }) {
+ const voice = () => props.file.presentation === 'voice_note';
+ const label = () =>
+ [
+ voice() ? 'Voice note' : props.file.fileName,
+ props.file.durationSeconds
+ ? formatDuration(props.file.durationSeconds)
+ : undefined,
+ ]
+ .filter(Boolean)
+ .join(' · ');
+ return (
+
+
+
+
+
+
+ {label()}
+
+
+
+ }
+ >
+ {(url) => (
+
+ )}
+
+
+ );
+}
+
/**
* WhatsApp media inline, like Chat: images and video in Chat's media grid
- * (click opens the viewer), audio in a player.
+ * (click opens the viewer), GIFs looping, stickers small and unframed,
+ * audio and voice notes in a player with their length.
*/
export function InboundMedia(props: { files: InboundMediaFile[] }) {
+ const grid = () =>
+ props.files.filter(
+ (file) =>
+ file.kind !== 'audio' &&
+ file.presentation !== 'sticker' &&
+ file.presentation !== 'gif'
+ );
const visual = (): MediaItem[] =>
- props.files.flatMap((file) =>
- file.kind !== 'audio' && file.url
- ? [{ id: file.id, src: file.url, fullSrc: file.url, kind: file.kind }]
+ grid().flatMap((file) =>
+ file.url && file.kind !== 'audio'
+ ? [
+ {
+ id: file.id,
+ src: file.url,
+ fullSrc: file.url,
+ kind: file.kind,
+ width: file.width,
+ height: file.height,
+ },
+ ]
: []
);
- const loading = () =>
- props.files.some((file) => file.kind !== 'audio' && !file.url);
return (
0}>
-
-
-
-
file.kind === 'audio')}>
+ !file.url)}>
+ {(file) => }
+
+
{(file) => (
-
- }
- >
- {(url) => (
-
- )}
-
+
+
+
+
+
+
+
+
+
+
+
)}
diff --git a/apps/web/src/features/whatsapp-group/components/rich-cards.tsx b/apps/web/src/features/whatsapp-group/components/rich-cards.tsx
new file mode 100644
index 00000000000..60117ef320f
--- /dev/null
+++ b/apps/web/src/features/whatsapp-group/components/rich-cards.tsx
@@ -0,0 +1,243 @@
+import { openExternalUrl } from '@core/util/url';
+import AddressBookIcon from '@phosphor/address-book.svg';
+import CalendarIcon from '@phosphor/calendar-blank.svg';
+import ChartBarIcon from '@phosphor/chart-bar-horizontal.svg';
+import CheckIcon from '@phosphor/check.svg';
+import MapPinIcon from '@phosphor/map-pin.svg';
+import { cn } from '@ui';
+import { For, type JSX, Match, Show, Switch } from 'solid-js';
+import {
+ type Contacts,
+ eventWhen,
+ formatCoordinates,
+ type Location,
+ mapsUrl,
+ namesList,
+ type Poll,
+ type RichContent,
+ type WhatsAppEvent,
+} from '../core/rich';
+
+function Card(props: {
+ kind: string;
+ icon: JSX.Element;
+ label: string;
+ children: JSX.Element;
+}) {
+ return (
+
+
+
+ {props.icon}
+
+ {props.label}
+
+ {props.children}
+
+ );
+}
+
+function ExternalLink(props: { href: string; children: JSX.Element }) {
+ return (
+ {
+ // Like Chat's links: plain clicks open through the app (Tauri).
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
+ return;
+ event.preventDefault();
+ openExternalUrl(props.href);
+ }}
+ >
+ {props.children}
+
+ );
+}
+
+/** Tallies only: votes are cast in WhatsApp. */
+function PollCard(props: { poll: Poll }) {
+ const most = () =>
+ Math.max(1, ...props.poll.options.map((option) => option.votes));
+ const total = () =>
+ props.poll.options.reduce((sum, option) => sum + option.votes, 0);
+ return (
+ }
+ label={
+ props.poll.selectableCount === 1
+ ? 'Poll · Select one'
+ : 'Poll · Select one or more'
+ }
+ >
+ {props.poll.question}
+
+
+ {(option) => (
+
+
+
+
+
+
+
+
+ {option.name}
+
+
+ {option.votes}
+
+
+
+
+ )}
+
+
+
+ {total() === 1 ? '1 vote' : `${total()} votes`}
+ {props.poll.voters > 0 &&
+ ` from ${props.poll.voters === 1 ? '1 person' : `${props.poll.voters} people`}`}{' '}
+ · Vote in WhatsApp
+
+
+ );
+}
+
+function LocationCard(props: { location: Location }) {
+ return (
+ }
+ label={props.location.live ? 'Live location' : 'Location'}
+ >
+
+
+ {(name) => {name()}
}
+
+
+ {(address) => {address()}
}
+
+
+ {formatCoordinates(props.location)}
+
+
+ {(comment) => {comment()}
}
+
+
+
+ Open in Maps
+
+ {(url) => Open link }
+
+
+
+ );
+}
+
+function ContactsCard(props: { contacts: Contacts }) {
+ return (
+ }
+ label={props.contacts.contacts.length === 1 ? 'Contact' : 'Contacts'}
+ >
+
+
+ {(contact) => (
+
+ {contact.name}
+
+ {(phone) => (
+
+ {phone}
+
+ )}
+
+
+ )}
+
+
+
+ );
+}
+
+function EventCard(props: { event: WhatsAppEvent }) {
+ return (
+ }
+ label={props.event.canceled ? 'Event · Canceled' : 'Event'}
+ >
+
+
+ {props.event.name}
+
+
+ {(when) => {when()}
}
+
+
+ {(location) => {location()}
}
+
+
+ {(description) => (
+
+ {description()}
+
+ )}
+
+
+
+ {(link) => (
+
+ Join
+
+ )}
+
+
+ );
+}
+
+/** The card for a message's rich content; system lines render elsewhere. */
+export function RichCard(props: { rich: RichContent }) {
+ return (
+
+
+ {(poll) => }
+
+
+ {(location) => }
+
+
+ {(contacts) => }
+
+
+ {(event) => }
+
+
+ );
+}
diff --git a/apps/web/src/features/whatsapp-group/components/system-line.tsx b/apps/web/src/features/whatsapp-group/components/system-line.tsx
new file mode 100644
index 00000000000..b4fb60753a7
--- /dev/null
+++ b/apps/web/src/features/whatsapp-group/components/system-line.tsx
@@ -0,0 +1,51 @@
+import { SystemActivityLine } from '@channel/components/SystemActivityRow';
+import ImageIcon from '@phosphor/image.svg';
+import PencilIcon from '@phosphor/pencil-simple.svg';
+import PushPinIcon from '@phosphor/push-pin.svg';
+import TimerIcon from '@phosphor/timer.svg';
+import UserMinusIcon from '@phosphor/user-minus.svg';
+import UserPlusIcon from '@phosphor/user-plus.svg';
+import PhoneIcon from '@phosphor-fill/phone-fill.svg';
+import {
+ describeSystemLine,
+ type SystemLine,
+ type SystemLineText,
+ systemLineSentence,
+} from '../core/rich';
+
+const ICONS = {
+ added: UserPlusIcon,
+ removed: UserMinusIcon,
+ edit: PencilIcon,
+ picture: ImageIcon,
+ pin: PushPinIcon,
+ call: PhoneIcon,
+ timer: TimerIcon,
+} satisfies Record;
+
+/** A WhatsApp group event, drawn like Chat's system activity rows. */
+export function WhatsAppSystemLine(props: {
+ id: string;
+ line: SystemLine;
+ occurredAt: string;
+}) {
+ const text = () => describeSystemLine(props.line);
+ return (
+
+ );
+}
diff --git a/apps/web/src/features/whatsapp-group/core/attachments.ts b/apps/web/src/features/whatsapp-group/core/attachments.ts
index c0787035f9c..5e0b5429c3f 100644
--- a/apps/web/src/features/whatsapp-group/core/attachments.ts
+++ b/apps/web/src/features/whatsapp-group/core/attachments.ts
@@ -6,6 +6,11 @@ export type InboundAttachment = {
mime: string;
/** The Macro document holding the file, once ready. */
documentId: string | undefined;
+ /** As WhatsApp declared them, to reserve space before the file loads. */
+ width?: number;
+ height?: number;
+ durationSeconds?: number;
+ presentation?: 'sticker' | 'gif' | 'voice_note';
};
/** How a ready file plays inline, like Chat's media; undefined for a file chip. */
diff --git a/apps/web/src/features/whatsapp-group/core/formatting.test.ts b/apps/web/src/features/whatsapp-group/core/formatting.test.ts
index aa8827ab02c..eb22827c035 100644
--- a/apps/web/src/features/whatsapp-group/core/formatting.test.ts
+++ b/apps/web/src/features/whatsapp-group/core/formatting.test.ts
@@ -172,6 +172,21 @@ describe('WhatsApp to Markdown for display', () => {
]);
});
+ it('chips only verified Macro users, and a name cannot close the chip', () => {
+ const markdown = whatsappToMarkdown('hi @1555, @1666 and @15550', [
+ {
+ token: '1555',
+ name: 'Ben{} ',
+ userId: 'macro|ben@x.test',
+ },
+ { token: '1666', name: 'Cy_[x]' },
+ ]);
+ const { nodes } = rendered(markdown);
+ expect(nodes.filter((node) => !node.startsWith('text'))).toHaveLength(1);
+ expect(nodes).toContain('text[bold]:@Cy_[x]');
+ expect(nodes.at(-1)).toBe('text: and @15550');
+ });
+
it('round-trips an edited message back to the same WhatsApp text', () => {
const body = '*hi* _there_ ~x~ a < b & # c $d$ `e`';
const editor = createHeadlessEditor({ nodes: SupportedNodeTypes });
diff --git a/apps/web/src/features/whatsapp-group/core/formatting.ts b/apps/web/src/features/whatsapp-group/core/formatting.ts
index 5c388a89d7c..30590d52ee1 100644
--- a/apps/web/src/features/whatsapp-group/core/formatting.ts
+++ b/apps/web/src/features/whatsapp-group/core/formatting.ts
@@ -4,6 +4,8 @@
* renderer speak Markdown. These convert at the send and display edges.
*/
+import type { Mention } from './rich';
+
const FENCE = /^```/;
/** Markdown inline syntax the composer writes, one alternative per group. */
@@ -35,8 +37,8 @@ function markdownInline(text: string): string {
if (italic !== undefined) return `_${markdownInline(italic)}_`;
if (strike !== undefined) return `~${markdownInline(strike)}~`;
if (linkText !== undefined && linkUrl !== undefined)
- return linkText === linkUrl
- ? linkUrl
+ return linkText === linkUrl || `https://${linkText}` === linkUrl
+ ? linkText
: `${markdownInline(linkText)} (${linkUrl})`;
return autolink ?? '';
}
@@ -73,7 +75,7 @@ export function markdownToWhatsApp(markdown: string): string {
const WHATSAPP_INLINE =
/(`[^`\n]+`)|((?:https?:\/\/|www\.)[^\s<>]*[^\s<>.,;:!?'")\]*_~])|(^|[^\p{L}\p{N}*_~])([*_~])(?!\s)([^\n]*?[^\s*_~]|[^\s*_~])\4(?![\p{L}\p{N}*_~])/gu;
-/** ASCII punctuation, except `@`. */
+/** ASCII punctuation, minus the `@` that mentions are found by. */
const PUNCTUATION = /[!-/:-?[-`{-~]/g;
/**
@@ -98,8 +100,52 @@ function urlLink(url: string): string {
}
const MARKDOWN_FOR: Record = { '*': '**', _: '*', '~': '~~' };
-function plainText(text: string): string {
- return literal(text);
+/** `@` for one of `mentions`, not inside a longer word or number. */
+function mentionPattern(mentions: Mention[]): RegExp | undefined {
+ if (mentions.length === 0) return undefined;
+ const tokens = [...new Set(mentions.map((mention) => mention.token))]
+ .sort((a, b) => b.length - a.length)
+ .map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
+ return new RegExp(`@(${tokens.join('|')})(?![\\p{L}\\p{N}])`, 'gu');
+}
+
+/** `macro|ana@example.com` → its email; the mention chip resolves the name. */
+function macroEmail(userId: string): string | undefined {
+ return userId.startsWith('macro|')
+ ? userId.slice('macro|'.length)
+ : undefined;
+}
+
+/**
+ * A mentioned person: a verified Macro user becomes Chat's mention chip,
+ * anyone else their WhatsApp name or number in bold, the way WhatsApp shows
+ * it. The chip's JSON escapes `<` and `>`, so a name can't close the tag.
+ */
+function mentionMarkdown(mention: Mention): string {
+ const email = mention.userId ? macroEmail(mention.userId) : undefined;
+ if (!mention.userId || !email) return `**@${literal(mention.name)}**`;
+ const json = JSON.stringify({
+ userId: mention.userId,
+ email,
+ displayName: mention.name,
+ })
+ .replace(//g, '\\u003e');
+ return `${json} `;
+}
+
+function plainText(text: string, mentions: Mention[]): string {
+ const pattern = mentionPattern(mentions);
+ if (!pattern) return literal(text);
+ let out = '';
+ let last = 0;
+ for (const found of text.matchAll(pattern)) {
+ const mention = mentions.find((each) => each.token === found[1]);
+ if (!mention) continue;
+ out += literal(text.slice(last, found.index)) + mentionMarkdown(mention);
+ last = found.index + found[0].length;
+ }
+ return out + literal(text.slice(last));
}
/**
@@ -120,30 +166,30 @@ function inlineCode(code: string, before: string, after: string | undefined) {
return literal(code);
}
-function whatsappInline(text: string): string {
+function whatsappInline(text: string, mentions: Mention[]): string {
let out = '';
let last = 0;
for (const found of text.matchAll(WHATSAPP_INLINE)) {
const [whole, code, url, before = '', marker = '', inner = ''] = found;
- out += plainText(text.slice(last, found.index));
+ out += plainText(text.slice(last, found.index), mentions);
last = found.index + whole.length;
if (code !== undefined) out += inlineCode(code, out, text[last]);
else if (url !== undefined) out += urlLink(url);
else {
const markdown = MARKDOWN_FOR[marker] ?? '';
- out += `${plainText(before)}${markdown}${whatsappInline(inner)}${markdown}`;
+ out += `${plainText(before, mentions)}${markdown}${whatsappInline(inner, mentions)}${markdown}`;
}
}
- return out + plainText(text.slice(last));
+ return out + plainText(text.slice(last), mentions);
}
/** WhatsApp's line formats: `- ` / `* ` bullets, `1. ` numbers and `> ` quotes. */
const WHATSAPP_LINE = /^(?:([-*]) |(\d{1,9})\. |> ?)/;
-function whatsappLine(line: string): string {
+function whatsappLine(line: string, mentions: Mention[]): string {
const block = WHATSAPP_LINE.exec(line);
- if (!block) return whatsappInline(line);
- const rest = whatsappInline(line.slice(block[0].length));
+ if (!block) return whatsappInline(line, mentions);
+ const rest = whatsappInline(line.slice(block[0].length), mentions);
if (block[1]) return `- ${rest}`;
if (block[2]) return `${block[2]}. ${rest}`;
return `> ${rest}`;
@@ -151,16 +197,23 @@ function whatsappLine(line: string): string {
/**
* A received WhatsApp body as Markdown for the shared message renderer.
- * Only what this function writes is Markdown: WhatsApp's own formatting and
- * links for bare http(s) URLs. Everything else is literal text.
+ * Only what this function writes is Markdown: WhatsApp's own formatting,
+ * links for bare http(s) URLs, and a mention chip for each `@` that
+ * DSS matched to a verified Macro user. Everything else is literal text.
*/
-export function whatsappToMarkdown(text: string): string {
+export function whatsappToMarkdown(
+ text: string,
+ mentions: Mention[] = []
+): string {
return text
.split(/(```[\s\S]*?```)/)
.map((part) =>
part.startsWith('```') && part.endsWith('```') && part.length >= 6
? `\n\`\`\`\n${part.slice(3, -3).replace(/^\n|\n$/g, '')}\n\`\`\`\n`
- : part.split('\n').map(whatsappLine).join('\n')
+ : part
+ .split('\n')
+ .map((line) => whatsappLine(line, mentions))
+ .join('\n')
)
.join('')
.trim();
diff --git a/apps/web/src/features/whatsapp-group/core/group.ts b/apps/web/src/features/whatsapp-group/core/group.ts
index 27dfdbb1673..0f1b4e2dce0 100644
--- a/apps/web/src/features/whatsapp-group/core/group.ts
+++ b/apps/web/src/features/whatsapp-group/core/group.ts
@@ -1,5 +1,6 @@
import { match, P } from 'ts-pattern';
import type { InboundAttachment } from './attachments';
+import type { Mention, RichContent } from './rich';
/** Feature vocabulary; transport decoding lives in queries/. */
export type GroupSummary = {
@@ -41,8 +42,16 @@ export type GroupDetail = {
};
export type GroupMessage = {
id: string;
+ /** `system`: a group event (`rich.type === 'system'`), drawn as a line. */
+ kind: 'message' | 'system';
sender: { name: string; userId?: string };
body: string;
+ /** People `@` in the body stands for. */
+ mentions: Mention[];
+ /** WhatsApp marked it forwarded. */
+ forwarded: boolean;
+ /** A poll, location, contact card, event or system line. */
+ rich?: RichContent;
sentAt: string;
edited: boolean;
deleted: boolean;
@@ -54,7 +63,8 @@ export type GroupMessage = {
};
export type MessagePage = { messages: GroupMessage[]; before?: string };
export type GroupSend =
- | { kind: 'text'; text: string; replyTo?: string }
+ /** `mentions`: participant ids; the text names each as `@`. */
+ | { kind: 'text'; text: string; replyTo?: string; mentions?: string[] }
| { kind: 'media'; uploadId: string; caption?: string; replyTo?: string }
| { kind: 'reaction'; messageId: string; emoji: string }
| { kind: 'edit'; messageId: string; text: string }
diff --git a/apps/web/src/features/whatsapp-group/core/mentions.ts b/apps/web/src/features/whatsapp-group/core/mentions.ts
new file mode 100644
index 00000000000..50a13d2ba0e
--- /dev/null
+++ b/apps/web/src/features/whatsapp-group/core/mentions.ts
@@ -0,0 +1,33 @@
+const USER_MENTION = /(.*?)<\/m-user-mention>/g;
+
+/** The digits WhatsApp shows after `@` for a participant id. */
+export function mentionToken(jid: string): string {
+ return jid.split('@')[0]?.split(':')[0] ?? jid;
+}
+
+/**
+ * The composer's mention chips for group participants, turned into the
+ * `@` text WhatsApp shows and the participant ids it notifies.
+ * Chips for anyone outside `participants` stay as their name.
+ */
+export function composerMentions(
+ markdown: string,
+ participants: string[]
+): { markdown: string; mentions: string[] } {
+ const known = new Set(participants);
+ const mentions: string[] = [];
+ const text = markdown.replace(USER_MENTION, (_whole, json: string) => {
+ let data: { userId?: unknown; displayName?: unknown };
+ try {
+ data = JSON.parse(json);
+ } catch {
+ return '';
+ }
+ const jid = typeof data.userId === 'string' ? data.userId : '';
+ if (!known.has(jid))
+ return typeof data.displayName === 'string' ? `@${data.displayName}` : '';
+ if (!mentions.includes(jid)) mentions.push(jid);
+ return `@${mentionToken(jid)}`;
+ });
+ return { markdown: text, mentions };
+}
diff --git a/apps/web/src/features/whatsapp-group/core/message-data.ts b/apps/web/src/features/whatsapp-group/core/message-data.ts
index 458f1b0b0c8..3caad99b731 100644
--- a/apps/web/src/features/whatsapp-group/core/message-data.ts
+++ b/apps/web/src/features/whatsapp-group/core/message-data.ts
@@ -13,7 +13,9 @@ export function groupMessageData(
): MessageData {
return {
id: message.id,
- content: message.deleted ? '' : whatsappToMarkdown(message.body),
+ content: message.deleted
+ ? ''
+ : whatsappToMarkdown(message.body, message.mentions),
sender_id: message.sender.userId ?? '',
// WhatsApp's name for everyone, Macro users included: a Macro profile
// without a name would otherwise show its email.
diff --git a/apps/web/src/features/whatsapp-group/core/rich.test.ts b/apps/web/src/features/whatsapp-group/core/rich.test.ts
new file mode 100644
index 00000000000..56473cc761e
--- /dev/null
+++ b/apps/web/src/features/whatsapp-group/core/rich.test.ts
@@ -0,0 +1,126 @@
+import { describe, expect, it } from 'vitest';
+import { composerMentions } from './mentions';
+import {
+ describeSystemLine,
+ formatDuration,
+ type SystemLine,
+ systemLineSentence,
+} from './rich';
+
+const ana = { name: 'Ana' };
+const ben = { name: 'Ben' };
+const line = (partial: Partial): SystemLine => ({
+ type: 'system',
+ action: 'icon_changed',
+ actor: ana,
+ participants: [],
+ ...partial,
+});
+const sentence = (partial: Partial) =>
+ systemLineSentence(describeSystemLine(line(partial)));
+
+describe('system lines', () => {
+ it('words every group event like WhatsApp', () => {
+ expect(sentence({ action: 'group_created', value: 'Launch' })).toBe(
+ 'Ana created the group “Launch”'
+ );
+ expect(
+ sentence({
+ action: 'participants_added',
+ participants: [ben, { name: '+1 555' }],
+ })
+ ).toBe('Ana added Ben and +1 555');
+ expect(
+ sentence({ action: 'participants_removed', participants: [ben] })
+ ).toBe('Ana removed Ben');
+ expect(
+ sentence({
+ action: 'participants_left',
+ actor: undefined,
+ participants: [ben],
+ })
+ ).toBe('Ben left');
+ expect(
+ sentence({ action: 'participants_joined', participants: [ben] })
+ ).toBe("Ben joined using this group's invite link");
+ expect(
+ sentence({ action: 'participants_promoted', participants: [ben] })
+ ).toBe('Ana made Ben an admin');
+ expect(
+ sentence({ action: 'participants_demoted', participants: [ben] })
+ ).toBe('Ana dismissed Ben as admin');
+ expect(sentence({ action: 'subject_changed', value: 'Launch' })).toBe(
+ 'Ana changed the subject to “Launch”'
+ );
+ expect(sentence({ action: 'icon_changed' })).toBe(
+ 'Ana changed the group icon'
+ );
+ expect(sentence({ action: 'description_changed', value: 'x' })).toBe(
+ 'Ana changed the group description'
+ );
+ expect(sentence({ action: 'description_changed' })).toBe(
+ 'Ana removed the group description'
+ );
+ expect(sentence({ action: 'disappearing_changed', value: '604800' })).toBe(
+ 'Ana turned on disappearing messages (7 days)'
+ );
+ expect(sentence({ action: 'disappearing_changed', value: '0' })).toBe(
+ 'Ana turned off disappearing messages'
+ );
+ expect(sentence({ action: 'message_pinned' })).toBe('Ana pinned a message');
+ expect(sentence({ action: 'message_unpinned' })).toBe(
+ 'Ana unpinned a message'
+ );
+ expect(
+ sentence({
+ action: 'call',
+ call: { video: true, outcome: 'missed' },
+ })
+ ).toBe('Missed group video call');
+ expect(
+ sentence({
+ action: 'call',
+ call: { video: false, outcome: 'answered', durationSeconds: 125 },
+ })
+ ).toBe('Group voice call · 2:05');
+ });
+
+ it('starts with the verb when WhatsApp did not say who', () => {
+ expect(sentence({ action: 'icon_changed', actor: undefined })).toBe(
+ 'Changed the group icon'
+ );
+ });
+
+ it('shortens long participant lists', () => {
+ expect(
+ sentence({
+ action: 'participants_added',
+ participants: [ben, { name: 'Cy' }, { name: 'Dee' }, { name: 'Eve' }],
+ })
+ ).toBe('Ana added Ben, Cy and 2 others');
+ });
+});
+
+describe('durations', () => {
+ it('reads like a media player', () => {
+ expect(formatDuration(42)).toBe('0:42');
+ expect(formatDuration(725)).toBe('12:05');
+ expect(formatDuration(3723)).toBe('1:02:03');
+ });
+});
+
+describe('mentions', () => {
+ it('turns composer chips into WhatsApp mentions of participants only', () => {
+ const chip = (userId: string, displayName: string) =>
+ `${JSON.stringify({ userId, email: 'x', displayName })} `;
+ expect(
+ composerMentions(
+ `${chip('1555@s.whatsapp.net', 'Ben')} ${chip('42@lid', 'Cy')} ${chip('macro|z@x.test', 'Zed')} ${chip('1555@s.whatsapp.net', 'Ben')}`,
+ ['1555@s.whatsapp.net', '42@lid']
+ )
+ ).toEqual({
+ markdown: '@1555 @42 @Zed @1555',
+ mentions: ['1555@s.whatsapp.net', '42@lid'],
+ });
+ });
+});
diff --git a/apps/web/src/features/whatsapp-group/core/rich.ts b/apps/web/src/features/whatsapp-group/core/rich.ts
new file mode 100644
index 00000000000..e8db1e28157
--- /dev/null
+++ b/apps/web/src/features/whatsapp-group/core/rich.ts
@@ -0,0 +1,331 @@
+import { match, P } from 'ts-pattern';
+
+/** Someone WhatsApp named; `userId` when they are a verified Macro user. */
+export type Person = { name: string; userId?: string };
+
+/** `@` in a body stands for this person. */
+export type Mention = Person & { token: string };
+
+type SystemAction =
+ | 'group_created'
+ | 'participants_added'
+ | 'participants_removed'
+ | 'participants_left'
+ | 'participants_joined'
+ | 'participants_promoted'
+ | 'participants_demoted'
+ | 'subject_changed'
+ | 'icon_changed'
+ | 'description_changed'
+ | 'disappearing_changed'
+ | 'message_pinned'
+ | 'message_unpinned'
+ | 'call';
+
+type CallSummary = {
+ video: boolean;
+ outcome: 'answered' | 'missed' | 'rejected' | 'other';
+ durationSeconds?: number;
+};
+
+/** A group event WhatsApp shows as a centered line, not a message. */
+export type SystemLine = {
+ type: 'system';
+ action: SystemAction;
+ actor?: Person;
+ participants: Person[];
+ value?: string;
+ call?: CallSummary;
+};
+
+export type Poll = {
+ type: 'poll';
+ question: string;
+ options: { name: string; votes: number; mine: boolean; voters: Person[] }[];
+ /** 0: any number of options; 1: a single choice. */
+ selectableCount: number;
+ /** People who voted at all. */
+ voters: number;
+};
+
+export type Location = {
+ type: 'location';
+ latitude: number;
+ longitude: number;
+ name?: string;
+ address?: string;
+ /** A link the sender attached. */
+ url?: string;
+ /** Live location: where it started. */
+ live: boolean;
+ comment?: string;
+};
+
+export type Contacts = {
+ type: 'contacts';
+ contacts: { name: string; phones: string[] }[];
+};
+
+export type WhatsAppEvent = {
+ type: 'event';
+ name: string;
+ description?: string;
+ startAt?: string;
+ endAt?: string;
+ location?: string;
+ joinLink?: string;
+ canceled: boolean;
+};
+
+export type RichContent =
+ | SystemLine
+ | Poll
+ | Location
+ | Contacts
+ | WhatsAppEvent;
+
+/** A short label for a quoted or replied-to rich message. */
+export function richPreview(rich: RichContent): string {
+ return match(rich)
+ .with({ type: 'system' }, () => 'WhatsApp update')
+ .with({ type: 'poll' }, ({ question }) => `Poll: ${question}`)
+ .with(
+ { type: 'location' },
+ ({ name, live }) => name ?? (live ? 'Live location' : 'Location')
+ )
+ .with({ type: 'contacts' }, ({ contacts }) =>
+ contacts.length === 1
+ ? `Contact: ${contacts[0]?.name ?? ''}`
+ : `${contacts.length} contacts`
+ )
+ .with({ type: 'event' }, ({ name }) => `Event: ${name}`)
+ .exhaustive();
+}
+
+/**
+ * A sender-supplied link, kept only when it is an http(s) URL: anything else
+ * (`javascript:`, `file:`, app schemes) would run or open in the app.
+ */
+export function webLink(value: string | null | undefined): string | undefined {
+ if (!value) return undefined;
+ try {
+ const url = new URL(value);
+ return url.protocol === 'http:' || url.protocol === 'https:'
+ ? url.href
+ : undefined;
+ } catch {
+ return undefined;
+ }
+}
+
+/**
+ * A maps link built here from the coordinates, never one the sender
+ * supplied; nothing is fetched to render it.
+ */
+export function mapsUrl(location: Location): string {
+ const query = `${location.latitude},${location.longitude}`;
+ return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`;
+}
+
+/** Coordinates as people read them: `40.7128° N, 74.0060° W`. */
+export function formatCoordinates(location: Location): string {
+ const latitude = `${Math.abs(location.latitude).toFixed(4)}° ${location.latitude < 0 ? 'S' : 'N'}`;
+ const longitude = `${Math.abs(location.longitude).toFixed(4)}° ${location.longitude < 0 ? 'W' : 'E'}`;
+ return `${latitude}, ${longitude}`;
+}
+
+/** `0:42`, `12:05`, `1:02:03`. */
+export function formatDuration(totalSeconds: number): string {
+ const seconds = Math.max(0, Math.round(totalSeconds));
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const rest = String(seconds % 60).padStart(2, '0');
+ return hours > 0
+ ? `${hours}:${String(minutes).padStart(2, '0')}:${rest}`
+ : `${minutes}:${rest}`;
+}
+
+/** WhatsApp's own disappearing-message timer names. */
+function disappearingLabel(value: string | undefined): string | undefined {
+ const seconds = Number(value ?? '0');
+ if (!Number.isFinite(seconds) || seconds <= 0) return undefined;
+ const days = seconds / 86400;
+ if (Number.isInteger(days)) return days === 1 ? '24 hours' : `${days} days`;
+ const hours = seconds / 3600;
+ if (Number.isInteger(hours)) return hours === 1 ? '1 hour' : `${hours} hours`;
+ return `${Math.round(seconds / 60)} minutes`;
+}
+
+/** "Ben", "Ben and Cy", "Ben, Cy and 2 others". */
+export function namesList(people: Person[]): string {
+ const names = people.map((person) => person.name);
+ if (names.length === 0) return 'someone';
+ if (names.length === 1) return names[0] ?? '';
+ if (names.length <= 3)
+ return `${names.slice(0, -1).join(', ')} and ${names.at(-1)}`;
+ return `${names.slice(0, 2).join(', ')} and ${names.length - 2} others`;
+}
+
+/**
+ * A system line in words, split like Chat's activity rows: who did it
+ * (bold), what they did, and what it was done to (bold).
+ */
+export type SystemLineText = {
+ actor?: string;
+ verb: string;
+ subject?: string;
+ icon: 'added' | 'removed' | 'edit' | 'picture' | 'pin' | 'call' | 'timer';
+};
+
+export function describeSystemLine(line: SystemLine): SystemLineText {
+ const text = systemLineParts(line);
+ // WhatsApp didn't say who: the verb starts the sentence.
+ return text.actor
+ ? text
+ : {
+ ...text,
+ verb: `${text.verb.charAt(0).toUpperCase()}${text.verb.slice(1)}`,
+ };
+}
+
+function systemLineParts(line: SystemLine): SystemLineText {
+ const actor = line.actor?.name;
+ const people = namesList(line.participants);
+ return match(line)
+ .returnType()
+ .with({ action: 'group_created' }, ({ value }) =>
+ value
+ ? {
+ actor,
+ verb: 'created the group',
+ subject: `“${value}”`,
+ icon: 'added',
+ }
+ : { actor, verb: 'created the group', icon: 'added' }
+ )
+ .with({ action: 'participants_added' }, () => ({
+ actor,
+ verb: 'added',
+ subject: people,
+ icon: 'added',
+ }))
+ .with({ action: 'participants_removed' }, () => ({
+ actor,
+ verb: 'removed',
+ subject: people,
+ icon: 'removed',
+ }))
+ .with({ action: 'participants_left' }, () => ({
+ actor: people,
+ verb: 'left',
+ icon: 'removed',
+ }))
+ .with({ action: 'participants_joined' }, () => ({
+ actor: people,
+ verb: "joined using this group's invite link",
+ icon: 'added',
+ }))
+ .with({ action: 'participants_promoted' }, () => ({
+ actor,
+ verb: 'made',
+ subject: `${people} ${line.participants.length === 1 ? 'an admin' : 'admins'}`,
+ icon: 'edit',
+ }))
+ .with({ action: 'participants_demoted' }, () => ({
+ actor,
+ verb: 'dismissed',
+ subject: `${people} as ${line.participants.length === 1 ? 'admin' : 'admins'}`,
+ icon: 'edit',
+ }))
+ .with({ action: 'subject_changed' }, ({ value }) =>
+ value
+ ? {
+ actor,
+ verb: 'changed the subject to',
+ subject: `“${value}”`,
+ icon: 'edit',
+ }
+ : { actor, verb: 'changed the subject', icon: 'edit' }
+ )
+ .with({ action: 'icon_changed' }, () => ({
+ actor,
+ verb: 'changed the group icon',
+ icon: 'picture',
+ }))
+ .with({ action: 'description_changed' }, ({ value }) => ({
+ actor,
+ verb: value
+ ? 'changed the group description'
+ : 'removed the group description',
+ icon: 'edit',
+ }))
+ .with({ action: 'disappearing_changed' }, ({ value }) => {
+ const timer = disappearingLabel(value);
+ return timer
+ ? {
+ actor,
+ verb: 'turned on disappearing messages',
+ subject: `(${timer})`,
+ icon: 'timer',
+ }
+ : { actor, verb: 'turned off disappearing messages', icon: 'timer' };
+ })
+ .with({ action: 'message_pinned' }, () => ({
+ actor,
+ verb: 'pinned a message',
+ icon: 'pin',
+ }))
+ .with({ action: 'message_unpinned' }, () => ({
+ actor,
+ verb: 'unpinned a message',
+ icon: 'pin',
+ }))
+ .with({ action: 'call' }, ({ call }) => ({
+ verb: callLine(call),
+ icon: 'call',
+ }))
+ .exhaustive();
+}
+
+function callLine(call: CallSummary | undefined): string {
+ const kind = call?.video ? 'video call' : 'voice call';
+ return match(call?.outcome)
+ .with('missed', () => `Missed group ${kind}`)
+ .with('rejected', () => `Declined group ${kind}`)
+ .with('answered', () =>
+ call?.durationSeconds
+ ? `Group ${kind} · ${formatDuration(call.durationSeconds)}`
+ : `Group ${kind}`
+ )
+ .with(P.union('other', undefined), () => `Group ${kind}`)
+ .exhaustive();
+}
+
+/** The line as one sentence, for tooltips and screen readers. */
+export function systemLineSentence(text: SystemLineText): string {
+ return [text.actor, text.verb, text.subject].filter(Boolean).join(' ');
+}
+
+const DAY = new Intl.DateTimeFormat(undefined, {
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+});
+const TIME = new Intl.DateTimeFormat(undefined, {
+ hour: 'numeric',
+ minute: '2-digit',
+});
+
+/** "Fri, Oct 24, 2026 · 6:00 PM – 8:00 PM"; undefined without a start. */
+export function eventWhen(event: WhatsAppEvent): string | undefined {
+ if (!event.startAt) return undefined;
+ const start = new Date(event.startAt);
+ if (Number.isNaN(start.getTime())) return undefined;
+ const from = `${DAY.format(start)} · ${TIME.format(start)}`;
+ const end = event.endAt ? new Date(event.endAt) : undefined;
+ if (!end || Number.isNaN(end.getTime())) return from;
+ return DAY.format(end) === DAY.format(start)
+ ? `${from} – ${TIME.format(end)}`
+ : `${from} – ${DAY.format(end)} · ${TIME.format(end)}`;
+}
diff --git a/apps/web/src/features/whatsapp-group/primitives/conversation.test.ts b/apps/web/src/features/whatsapp-group/primitives/conversation.test.ts
index 666773759e4..6ae9a01404c 100644
--- a/apps/web/src/features/whatsapp-group/primitives/conversation.test.ts
+++ b/apps/web/src/features/whatsapp-group/primitives/conversation.test.ts
@@ -40,7 +40,7 @@ describe('sending to a WhatsApp group', () => {
await delivery._unsafeUnwrap();
const key = state.fake.capabilities.send.mock.calls[0]?.[1];
expect(state.fake.capabilities.send).toHaveBeenCalledWith(
- { kind: 'text', text: 'On *it*', replyTo: 'message-1' },
+ { kind: 'text', text: 'On *it*', replyTo: 'message-1', mentions: [] },
key
);
state.fake.setDelivery([
@@ -51,7 +51,10 @@ describe('sending to a WhatsApp group', () => {
...messages,
{
id: 'message-3',
+ kind: 'message',
sender: { name: 'Me', userId: group.ownerId },
+ mentions: [],
+ forwarded: false,
body: 'On *it*',
sentAt: new Date().toISOString(),
edited: false,
@@ -191,7 +194,7 @@ describe('sending to a WhatsApp group', () => {
caption: undefined,
replyTo: undefined,
},
- { kind: 'text', text: 'Listen' },
+ { kind: 'text', text: 'Listen', mentions: [] },
{
kind: 'media',
uploadId: 'upload-2',
diff --git a/apps/web/src/features/whatsapp-group/primitives/conversation.ts b/apps/web/src/features/whatsapp-group/primitives/conversation.ts
index 62d228c06a1..23725c22fa8 100644
--- a/apps/web/src/features/whatsapp-group/primitives/conversation.ts
+++ b/apps/web/src/features/whatsapp-group/primitives/conversation.ts
@@ -21,6 +21,7 @@ import {
recordedOutcome,
type SendFailure,
} from '../core/group';
+import { composerMentions, mentionToken } from '../core/mentions';
/** What the composer hands over: its Markdown and the files it staged. */
export type Draft = {
@@ -204,6 +205,7 @@ export function createConversation(
const partsOf = (
draft: Draft,
text: string,
+ mentions: string[],
sentTarget: ConversationTarget | undefined
): GroupSend[] => {
if (sentTarget?.kind === 'edit')
@@ -211,10 +213,13 @@ export function createConversation(
const replyTo =
sentTarget?.kind === 'reply' ? sentTarget.message.id : undefined;
if (draft.attachments.length === 0)
- return [{ kind: 'text', text, replyTo }];
- const captioned = draft.attachments.find(
- (file) => !audioUploads.has(file.id)
- );
+ return [{ kind: 'text', text, replyTo, mentions }];
+ // WhatsApp mentions ride on text only, so a mentioning text follows the
+ // files instead of captioning them.
+ const captioned =
+ mentions.length > 0
+ ? undefined
+ : draft.attachments.find((file) => !audioUploads.has(file.id));
const media = draft.attachments.map(
(file, index): GroupSend => ({
kind: 'media',
@@ -224,7 +229,9 @@ export function createConversation(
})
);
// WhatsApp refuses captions on audio, so the text follows on its own.
- return text && !captioned ? [...media, { kind: 'text', text }] : media;
+ return text && !captioned
+ ? [...media, { kind: 'text', text, mentions }]
+ : media;
};
/**
@@ -236,7 +243,11 @@ export function createConversation(
const senderId = userId();
if (!group || !senderId) return err('This group is still loading.');
if (!group.canSend) return err(composerNote(group));
- const text = markdownToWhatsApp(draft.markdown);
+ const mentioned = composerMentions(
+ draft.markdown,
+ group.members.map((member) => member.id)
+ );
+ const text = markdownToWhatsApp(mentioned.markdown);
const sentTarget = target();
if (new TextEncoder().encode(text).length > group.limits.textBytes)
return err('This message is too long for WhatsApp.');
@@ -255,7 +266,10 @@ export function createConversation(
)
return err('This message can no longer be edited.');
}
- const requests = partsOf(draft, text, sentTarget);
+ const requests = partsOf(draft, text, mentioned.mentions, sentTarget);
+ const names = new Map(
+ group.members.map((member) => [member.id, member.name])
+ );
if (requests.length === 0) return err('Write a message first.');
const draftFingerprint = fingerprint(draft, sentTarget);
const key = draftKeys.get(draftFingerprint) ?? crypto.randomUUID();
@@ -276,7 +290,16 @@ export function createConversation(
target: sentTarget,
row: {
id: partKey,
+ kind: 'message',
sender: { name: '', userId: senderId },
+ mentions:
+ request.kind === 'text'
+ ? (request.mentions ?? []).map((jid) => ({
+ token: mentionToken(jid),
+ name: names.get(jid) ?? `+${mentionToken(jid)}`,
+ }))
+ : [],
+ forwarded: false,
body:
request.kind === 'text'
? request.text
diff --git a/apps/web/src/features/whatsapp-group/queries/production-api.test.ts b/apps/web/src/features/whatsapp-group/queries/production-api.test.ts
index 92d4959ee4d..a7722788570 100644
--- a/apps/web/src/features/whatsapp-group/queries/production-api.test.ts
+++ b/apps/web/src/features/whatsapp-group/queries/production-api.test.ts
@@ -1,5 +1,6 @@
import type {
WhatsAppGroupDetail,
+ WhatsAppGroupMessage,
WhatsAppGroupMessages,
} from '@service-storage/generated/schemas';
import { err, ok } from 'neverthrow';
@@ -66,8 +67,12 @@ it('decodes pagination, deleted flags, reaction ownership and attachment states
messages: [
{
id: '019a061e-0000-7000-8000-000000000003',
+ kind: 'message',
sender: { person_id: 'person-1', user_id: null, name: 'Ana Ruiz' },
body: '',
+ mentions: [],
+ forwarded: false,
+ rich: null,
reply_to: null,
sent_at: '2026-10-21T12:00:00Z',
received_at: '2026-10-21T12:00:01Z',
@@ -102,6 +107,182 @@ it('decodes pagination, deleted flags, reaction ownership and attachment states
});
});
+it('decodes mentions, forwarding, media presentation and every rich type, keeping only http(s) links', async () => {
+ const base: Omit = {
+ sender: { person_id: 'person-1', user_id: null, name: 'Ana Ruiz' },
+ body: '',
+ reply_to: null,
+ sent_at: '2026-10-21T12:00:00Z',
+ received_at: '2026-10-21T12:00:01Z',
+ edited_at: null,
+ deleted: false,
+ reactions: [],
+ attachments: [],
+ delivery_state: null,
+ kind: 'message',
+ mentions: [],
+ forwarded: false,
+ rich: null,
+ };
+ const page = {
+ next_cursor: null,
+ messages: [
+ {
+ ...base,
+ id: 'mention',
+ body: 'Hi @15550001111 and @15550002222',
+ forwarded: true,
+ mentions: [
+ { token: '15550001111', name: 'Ben', userId: 'macro|ben@x.test' },
+ { token: '15550002222', name: '+1 555-000-2222', userId: null },
+ ],
+ },
+ {
+ ...base,
+ id: 'sticker',
+ attachments: [
+ {
+ ordinal: 0,
+ state: 'ready',
+ file_name: 'sticker.webp',
+ mime: 'image/webp',
+ size: 2048,
+ document_id: 'doc-1',
+ width: 512,
+ height: 512,
+ duration_seconds: null,
+ presentation: 'sticker',
+ },
+ ],
+ },
+ {
+ ...base,
+ id: 'poll',
+ rich: {
+ type: 'poll',
+ question: 'Lunch?',
+ options: [
+ {
+ name: 'Tacos',
+ votes: 2,
+ mine: true,
+ voters: [{ name: 'Ben', userId: null }],
+ },
+ ],
+ selectableCount: 1,
+ voters: 2,
+ },
+ },
+ {
+ ...base,
+ id: 'location',
+ rich: {
+ type: 'location',
+ latitude: 40.7,
+ longitude: -74,
+ name: null,
+ address: null,
+ url: 'javascript:alert(document.cookie)',
+ live: true,
+ comment: null,
+ },
+ },
+ {
+ ...base,
+ id: 'contacts',
+ rich: {
+ type: 'contacts',
+ contacts: [{ name: 'Cy', phones: ['+1 555 0100'] }],
+ },
+ },
+ {
+ ...base,
+ id: 'event',
+ rich: {
+ type: 'event',
+ name: 'Launch',
+ description: null,
+ startAt: '2026-10-24T18:00:00Z',
+ endAt: null,
+ location: null,
+ joinLink: 'https://meet.example/launch',
+ canceled: false,
+ },
+ },
+ {
+ ...base,
+ id: 'system',
+ kind: 'system',
+ rich: {
+ type: 'system',
+ action: 'call',
+ actor: null,
+ participants: [],
+ value: null,
+ call: { video: true, outcome: 'missed', durationSeconds: null },
+ },
+ },
+ ],
+ } satisfies WhatsAppGroupMessages;
+ client.messages.mockResolvedValue(ok(page));
+ const { messages } = await whatsappGroupApi.messages('group-id', undefined);
+ expect(messages.map((message) => [message.id, message.kind])).toEqual([
+ ['mention', 'message'],
+ ['sticker', 'message'],
+ ['poll', 'message'],
+ ['location', 'message'],
+ ['contacts', 'message'],
+ ['event', 'message'],
+ ['system', 'system'],
+ ]);
+ expect(messages[0]).toMatchObject({
+ forwarded: true,
+ mentions: [
+ { token: '15550001111', name: 'Ben', userId: 'macro|ben@x.test' },
+ { token: '15550002222', name: '+1 555-000-2222', userId: undefined },
+ ],
+ rich: undefined,
+ });
+ expect(messages[1]?.attachments[0]).toMatchObject({
+ width: 512,
+ height: 512,
+ durationSeconds: undefined,
+ presentation: 'sticker',
+ });
+ expect(messages[2]?.rich).toEqual({
+ type: 'poll',
+ question: 'Lunch?',
+ options: [
+ { name: 'Tacos', votes: 2, mine: true, voters: [{ name: 'Ben' }] },
+ ],
+ selectableCount: 1,
+ voters: 2,
+ });
+ expect(messages[3]?.rich).toEqual({
+ type: 'location',
+ latitude: 40.7,
+ longitude: -74,
+ live: true,
+ });
+ expect(messages[4]?.rich).toEqual({
+ type: 'contacts',
+ contacts: [{ name: 'Cy', phones: ['+1 555 0100'] }],
+ });
+ expect(messages[5]?.rich).toEqual({
+ type: 'event',
+ name: 'Launch',
+ startAt: '2026-10-24T18:00:00Z',
+ joinLink: 'https://meet.example/launch',
+ canceled: false,
+ });
+ expect(messages[6]?.rich).toEqual({
+ type: 'system',
+ action: 'call',
+ participants: [],
+ call: { video: true, outcome: 'missed' },
+ });
+});
+
it('preserves the deliberate send key and maps quotes and reactions without a channel request', async () => {
client.send.mockResolvedValue(ok({ id: 'send-1' }));
await whatsappGroupApi.send(
@@ -111,9 +292,27 @@ it('preserves the deliberate send key and maps quotes and reactions without a ch
);
expect(client.send).toHaveBeenCalledWith('group-id', {
idempotency_key: 'intent-key',
- message: { type: 'text', text: 'Reply' },
+ message: { type: 'text', text: 'Reply', mentions: [] },
reply_to: 'quoted-id',
});
+ await whatsappGroupApi.send(
+ 'group-id',
+ {
+ kind: 'text',
+ text: 'Hi @15550001111',
+ mentions: ['15550001111@s.whatsapp.net'],
+ },
+ 'mention-key'
+ );
+ expect(client.send).toHaveBeenLastCalledWith('group-id', {
+ idempotency_key: 'mention-key',
+ message: {
+ type: 'text',
+ text: 'Hi @15550001111',
+ mentions: ['15550001111@s.whatsapp.net'],
+ },
+ reply_to: null,
+ });
await whatsappGroupApi.send(
'group-id',
{ kind: 'reaction', messageId: 'message-id', emoji: '' },
diff --git a/apps/web/src/features/whatsapp-group/queries/production-api.ts b/apps/web/src/features/whatsapp-group/queries/production-api.ts
index decaa8906d7..d7dd5689a4d 100644
--- a/apps/web/src/features/whatsapp-group/queries/production-api.ts
+++ b/apps/web/src/features/whatsapp-group/queries/production-api.ts
@@ -4,16 +4,90 @@ import type { BaseFetchErrorCode } from '@core/util/safeFetch';
import type { WhatsAppGroupApi } from '@queries/whatsapp-groups';
import type { LinkedReplyContent } from '@service-storage/generated/schemas/linkedReplyContent';
import type { LinkedSendProgress } from '@service-storage/generated/schemas/linkedSendProgress';
+import type { WhatsAppPersonRef } from '@service-storage/generated/schemas/whatsAppPersonRef';
+import type { WhatsAppRichContent } from '@service-storage/generated/schemas/whatsAppRichContent';
import type { WhatsAppErrorCode } from '@service-storage/whatsapp-errors';
import { whatsappGroupsClient as client } from '@service-storage/whatsapp-groups';
import { err, ok } from 'neverthrow';
import { match, P } from 'ts-pattern';
import { phoneFromJid } from '../../whatsapp-linked/core/account';
import type { GroupSend, SendFailure, SendProgress } from '../core/group';
+import { type Person, type RichContent, webLink } from '../core/rich';
+
+function person(ref: WhatsAppPersonRef): Person {
+ return { name: ref.name, userId: ref.userId ?? undefined };
+}
+
+/** DSS's rich content in feature terms; nulls become absent. */
+function richContent(rich: WhatsAppRichContent): RichContent | undefined {
+ return (
+ match(rich)
+ .returnType()
+ .with({ type: 'poll' }, (poll) => ({
+ type: 'poll',
+ question: poll.question,
+ options: poll.options.map((option) => ({
+ name: option.name,
+ votes: option.votes,
+ mine: option.mine,
+ voters: option.voters.map(person),
+ })),
+ selectableCount: poll.selectableCount,
+ voters: poll.voters,
+ }))
+ .with({ type: 'location' }, (location) => ({
+ type: 'location',
+ latitude: location.latitude,
+ longitude: location.longitude,
+ name: location.name ?? undefined,
+ address: location.address ?? undefined,
+ url: webLink(location.url),
+ live: location.live ?? false,
+ comment: location.comment ?? undefined,
+ }))
+ .with({ type: 'contacts' }, (contacts) => ({
+ type: 'contacts',
+ contacts: contacts.contacts.map((card) => ({
+ name: card.name,
+ phones: card.phones,
+ })),
+ }))
+ .with({ type: 'event' }, (event) => ({
+ type: 'event',
+ name: event.name,
+ description: event.description ?? undefined,
+ startAt: event.startAt ?? undefined,
+ endAt: event.endAt ?? undefined,
+ location: event.location ?? undefined,
+ joinLink: webLink(event.joinLink),
+ canceled: event.canceled ?? false,
+ }))
+ .with({ type: 'system' }, (line) => ({
+ type: 'system',
+ action: line.action,
+ actor: line.actor ? person(line.actor) : undefined,
+ participants: line.participants.map(person),
+ value: line.value ?? undefined,
+ call: line.call
+ ? {
+ video: line.call.video,
+ outcome: line.call.outcome,
+ durationSeconds: line.call.durationSeconds ?? undefined,
+ }
+ : undefined,
+ }))
+ // A newer server's content type renders as the message body alone.
+ .exhaustive(() => undefined)
+ );
+}
function content(request: GroupSend): LinkedReplyContent {
return match(request)
- .with({ kind: 'text' }, ({ text }) => ({ type: 'text' as const, text }))
+ .with({ kind: 'text' }, ({ text, mentions }) => ({
+ type: 'text' as const,
+ text,
+ mentions: mentions ?? [],
+ }))
.with({ kind: 'media' }, ({ uploadId, caption }) => ({
type: 'media' as const,
upload_id: uploadId,
@@ -164,11 +238,18 @@ export const whatsappGroupApi: WhatsAppGroupApi = {
before: page.next_cursor ?? undefined,
messages: page.messages.map((message) => ({
id: message.id,
+ kind: message.kind,
sender: {
name: message.sender.name,
userId: message.sender.user_id ?? undefined,
},
body: message.body,
+ mentions: message.mentions.map((mention) => ({
+ token: mention.token,
+ ...person(mention),
+ })),
+ forwarded: message.forwarded,
+ rich: message.rich ? richContent(message.rich) : undefined,
sentAt: message.sent_at,
edited: !!message.edited_at,
deleted: message.deleted,
@@ -184,6 +265,10 @@ export const whatsappGroupApi: WhatsAppGroupApi = {
fileName: attachment.file_name,
mime: attachment.mime,
documentId: attachment.document_id ?? undefined,
+ width: attachment.width ?? undefined,
+ height: attachment.height ?? undefined,
+ durationSeconds: attachment.duration_seconds ?? undefined,
+ presentation: attachment.presentation ?? undefined,
})),
})),
};
diff --git a/apps/web/src/features/whatsapp-group/tests/fake-capabilities.ts b/apps/web/src/features/whatsapp-group/tests/fake-capabilities.ts
index ca74ca53221..46500d18979 100644
--- a/apps/web/src/features/whatsapp-group/tests/fake-capabilities.ts
+++ b/apps/web/src/features/whatsapp-group/tests/fake-capabilities.ts
@@ -33,7 +33,10 @@ export const group: GroupDetail = {
export const messages: GroupMessage[] = [
{
id: 'message-1',
+ kind: 'message',
sender: { name: 'Ana Ruiz' },
+ mentions: [],
+ forwarded: false,
body: 'The *updated* designs are ready for review.',
sentAt: '2026-10-21T10:00:00Z',
edited: false,
@@ -43,7 +46,10 @@ export const messages: GroupMessage[] = [
},
{
id: 'message-2',
+ kind: 'message',
sender: { name: 'Me', userId: 'macro|me@example.test' },
+ mentions: [],
+ forwarded: false,
body: 'Looking now. Thanks, Ana!',
sentAt: new Date().toISOString(),
edited: false,
diff --git a/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.test.tsx b/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.test.tsx
index 6fe7304fae8..9e2debb07ad 100644
--- a/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.test.tsx
+++ b/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.test.tsx
@@ -11,6 +11,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/solid-query';
import { err } from 'neverthrow';
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { WhatsAppGroupProvider } from '../context/whatsapp-group-context';
+import type { InboundAttachment } from '../core/attachments';
import { createFakeGroup, group, messages } from '../tests/fake-capabilities';
import { WhatsAppGroupView } from './whatsapp-group-view';
@@ -116,7 +117,7 @@ it('replies through the shared composer and sends WhatsApp formatting', async ()
expect(screen.getByText('Replying to Ana Ruiz')).toBeTruthy();
expect(mocks.focus).toHaveBeenCalled();
expect(mocks.composer?.features).toEqual({
- mentions: false,
+ mentions: 'participants',
createItems: false,
gifs: false,
richBlocks: false,
@@ -124,7 +125,12 @@ it('replies through the shared composer and sends WhatsApp formatting', async ()
await mocks.composer?.onSend?.(snapshot('Looks **great**'));
await waitFor(() =>
expect(fake.capabilities.send).toHaveBeenCalledWith(
- { kind: 'text', text: 'Looks *great*', replyTo: 'message-1' },
+ {
+ kind: 'text',
+ text: 'Looks *great*',
+ replyTo: 'message-1',
+ mentions: [],
+ },
expect.any(String)
)
);
@@ -277,3 +283,254 @@ it('plays imported images and voice notes inline, like Chat', () => {
// Not as document chips.
expect(screen.queryByText('photo.jpeg')).toBeNull();
});
+
+it('mentions group participants from the composer as WhatsApp mentions', async () => {
+ const fake = createFakeGroup({
+ group: {
+ ...group,
+ members: [
+ { id: '14155550142@s.whatsapp.net', name: 'Me', admin: false },
+ { id: '15550001111@s.whatsapp.net', name: 'Ana Ruiz', admin: true },
+ { id: '42424242@lid', name: '+42424242', admin: false },
+ ],
+ },
+ });
+ mount(fake);
+ // The group's own people, not the sender, Macro users or agents.
+ expect(mocks.composer?.participants?.()).toEqual([
+ {
+ id: '15550001111@s.whatsapp.net',
+ name: 'Ana Ruiz',
+ email: '+15550001111',
+ },
+ { id: '42424242@lid', name: '+42424242', email: '+42424242' },
+ ]);
+ await mocks.composer?.onSend?.(
+ snapshot(
+ 'Thanks {"userId":"15550001111@s.whatsapp.net","email":"+15550001111","displayName":"Ana"} !'
+ )
+ );
+ await waitFor(() =>
+ expect(fake.capabilities.send).toHaveBeenCalledWith(
+ {
+ kind: 'text',
+ text: 'Thanks @15550001111!',
+ replyTo: undefined,
+ mentions: ['15550001111@s.whatsapp.net'],
+ },
+ expect.any(String)
+ )
+ );
+ // The pending row names the person, as the stored one will.
+ expect(screen.getByText('@Ana Ruiz')).toBeTruthy();
+});
+
+it('renders every WhatsApp message type with Chat parts and small cards', () => {
+ const at = (minute: number) =>
+ `2026-10-21T10:${String(minute).padStart(2, '0')}:00Z`;
+ const base = { ...messages[0], body: '', reactions: [], sentAt: at(0) };
+ const ready = (
+ ordinal: number,
+ mime: string,
+ extra: Partial
+ ): InboundAttachment => ({
+ ordinal,
+ state: 'ready',
+ fileName: `file-${ordinal}`,
+ mime,
+ documentId: `document-${ordinal}`,
+ ...extra,
+ });
+ const fake = createFakeGroup({
+ messages: [
+ {
+ ...base,
+ id: 'created',
+ kind: 'system',
+ sentAt: at(1),
+ rich: {
+ type: 'system',
+ action: 'participants_added',
+ actor: { name: 'Ana Ruiz' },
+ participants: [{ name: 'Ben' }, { name: '+1 555 0100' }],
+ },
+ },
+ {
+ ...base,
+ id: 'mentions',
+ sentAt: at(2),
+ forwarded: true,
+ body: 'Hey @15550001111 and @15550002222, see https://example.com',
+ mentions: [
+ { token: '15550001111', name: 'Ben', userId: 'macro|ben@x.test' },
+ { token: '15550002222', name: 'Cy Ode' },
+ ],
+ },
+ {
+ ...base,
+ id: 'poll',
+ sentAt: at(3),
+ rich: {
+ type: 'poll',
+ question: 'Lunch?',
+ options: [
+ { name: 'Tacos', votes: 2, mine: true, voters: [] },
+ { name: 'Pho', votes: 1, mine: false, voters: [] },
+ ],
+ selectableCount: 1,
+ voters: 3,
+ },
+ },
+ {
+ ...base,
+ id: 'location',
+ sentAt: at(4),
+ rich: {
+ type: 'location',
+ latitude: 40.7128,
+ longitude: -74.006,
+ name: 'City Hall',
+ live: false,
+ },
+ },
+ {
+ ...base,
+ id: 'contacts',
+ sentAt: at(5),
+ rich: {
+ type: 'contacts',
+ contacts: [{ name: 'Dee', phones: ['+1 (555) 010-0200'] }],
+ },
+ },
+ {
+ ...base,
+ id: 'event',
+ sentAt: at(6),
+ rich: {
+ type: 'event',
+ name: 'Launch party',
+ startAt: '2026-10-24T22:00:00Z',
+ location: 'Rooftop',
+ canceled: false,
+ joinLink: 'https://call.example.com/x',
+ },
+ },
+ {
+ ...base,
+ id: 'media',
+ sentAt: at(7),
+ attachments: [
+ ready(0, 'image/webp', {
+ presentation: 'sticker',
+ width: 512,
+ height: 512,
+ }),
+ ready(1, 'video/mp4', {
+ presentation: 'gif',
+ width: 320,
+ height: 240,
+ }),
+ ready(2, 'audio/ogg', {
+ presentation: 'voice_note',
+ durationSeconds: 42,
+ }),
+ ready(3, 'image/jpeg', { width: 800, height: 600 }),
+ ],
+ },
+ {
+ ...base,
+ id: 'call',
+ kind: 'system',
+ sentAt: at(8),
+ rich: {
+ type: 'system',
+ action: 'call',
+ participants: [],
+ call: { video: true, outcome: 'missed' },
+ },
+ },
+ ],
+ });
+ mount(fake);
+ const line = document.querySelector('[data-system-activity="created"]');
+ expect(line?.textContent).toContain('Ana Ruiz added Ben and +1 555 0100');
+ // System lines are not messages: no avatar row, no actions.
+ expect(document.querySelector('[data-message-id="created"]')).toBeNull();
+ expect(
+ document.querySelector('[data-system-activity="call"]')?.textContent
+ ).toContain('Missed group video call');
+
+ expect(screen.getByText('Forwarded')).toBeTruthy();
+ // A Macro user becomes Chat's mention chip; anyone else their name.
+ expect(
+ document.querySelector(
+ '[data-user-mention][data-user-id="macro|ben@x.test"]'
+ )
+ ).toBeTruthy();
+ expect(screen.getByText('@Cy Ode')).toBeTruthy();
+ expect(screen.queryByText(/@15550002222/)).toBeNull();
+ // The URL stays a link, with Chat's hover preview.
+ expect(
+ screen
+ .getByRole('link', { name: 'https://example.com' })
+ .getAttribute('href')
+ ).toBe('https://example.com');
+
+ const poll = document.querySelector('[data-whatsapp-card="poll"]');
+ expect(poll?.textContent).toContain('Lunch?');
+ expect(poll?.textContent).toContain('Poll · Select one');
+ expect(poll?.textContent).toContain('3 votes from 3 people');
+ expect(screen.getByLabelText('Your vote')).toBeTruthy();
+ expect(screen.queryByRole('button', { name: /Tacos/ })).toBeNull();
+
+ const location = document.querySelector('[data-whatsapp-card="location"]');
+ expect(location?.textContent).toContain('City Hall');
+ expect(location?.textContent).toContain('40.7128° N, 74.0060° W');
+ expect(
+ screen.getByRole('link', { name: 'Open in Maps' }).getAttribute('href')
+ ).toBe('https://www.google.com/maps/search/?api=1&query=40.7128%2C-74.006');
+
+ expect(
+ screen.getByRole('link', { name: '+1 (555) 010-0200' }).getAttribute('href')
+ ).toBe('tel:+15550100200');
+ const event = document.querySelector('[data-whatsapp-card="event"]');
+ expect(event?.textContent).toContain('Launch party');
+ expect(event?.textContent).toContain('Rooftop');
+ expect(screen.getByRole('link', { name: 'Join' })).toBeTruthy();
+
+ const sticker = document.querySelector('[data-whatsapp-sticker]');
+ expect(sticker?.getAttribute('width')).toBe('144');
+ const gif = document.querySelector('[data-whatsapp-gif]');
+ expect(gif?.loop).toBe(true);
+ expect(gif?.muted).toBe(true);
+ expect(gif?.autoplay).toBe(true);
+ expect(gif?.getAttribute('width')).toBe('320');
+ expect(screen.getByLabelText('Voice note · 0:42')).toBeTruthy();
+ // The photo keeps its declared shape before it loads.
+ expect(
+ screen.getByRole('button', { name: 'Open image viewer' }).style.width
+ ).toBe('400px');
+});
+
+it('quotes stickers, voice notes and cards by what they are', () => {
+ const fake = createFakeGroup({
+ messages: [
+ {
+ ...messages[0],
+ body: '',
+ rich: {
+ type: 'poll',
+ question: 'Lunch?',
+ options: [],
+ selectableCount: 0,
+ voters: 0,
+ },
+ },
+ { ...messages[1], replyTo: 'message-1' },
+ ],
+ });
+ mount(fake);
+ expect(
+ screen.getByRole('button', { name: 'Ana Ruiz: Poll: Lunch?' })
+ ).toBeTruthy();
+});
diff --git a/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.tsx b/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.tsx
index e2d185f32f5..2c1c0514b87 100644
--- a/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.tsx
+++ b/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.tsx
@@ -17,7 +17,9 @@ import type {
MessageActionContext,
MessageActions,
} from '@core/messages/types';
+import type { IUser } from '@core/user/types';
import { fireAndForget } from '@core/util/fireAndForget';
+import ShareFatIcon from '@phosphor/share-fat.svg';
import UsersThreeIcon from '@phosphor/users-three.svg';
import WhatsappIcon from '@phosphor/whatsapp-logo.svg';
import { Button, ConfirmDialog } from '@ui';
@@ -32,11 +34,14 @@ import {
onMount,
Show,
} from 'solid-js';
+import { match } from 'ts-pattern';
import { InboundAttachmentPlaceholder } from '../components/inbound-attachment';
import {
InboundMedia,
type InboundMediaFile,
} from '../components/inbound-media';
+import { RichCard } from '../components/rich-cards';
+import { WhatsAppSystemLine } from '../components/system-line';
import { useWhatsAppGroup } from '../context/whatsapp-group-context';
import { type InboundAttachment, inlineMediaKind } from '../core/attachments';
import { whatsappToMarkdown } from '../core/formatting';
@@ -45,30 +50,72 @@ import {
canChangeMessage,
composerNote,
composerPlaceholder,
+ type GroupDetail,
type GroupMessage,
historyStartNote,
} from '../core/group';
+import { mentionToken } from '../core/mentions';
import { groupMessageData, senderKey } from '../core/message-data';
+import { richPreview } from '../core/rich';
import {
type ConversationTarget,
createConversation,
type Draft,
} from '../primitives/conversation';
-/** WhatsApp carries text, formatting and files; nothing Macro-only. */
+/**
+ * WhatsApp carries text, formatting, files and mentions of the group's own
+ * participants; nothing Macro-only.
+ */
const WHATSAPP_COMPOSER = {
- mentions: false,
+ mentions: 'participants',
createItems: false,
gifs: false,
richBlocks: false,
} as const;
+/**
+ * The group's WhatsApp participants as the composer's `@` people, minus the
+ * sender. The chip carries the participant id; sending turns it into
+ * `@` and a WhatsApp mention.
+ */
+function mentionable(group: GroupDetail): IUser[] {
+ const own = group.phone?.replace(/\D/g, '');
+ return group.members.flatMap((member) => {
+ const phone = `+${mentionToken(member.id)}`;
+ if (own && mentionToken(member.id) === own) return [];
+ return [{ id: member.id, name: member.name, email: phone }];
+ });
+}
+
/** A draft the conversation refused; the composer keeps it. */
class DraftRefusal extends Error {}
function quotePreview(message: GroupMessage) {
if (message.deleted) return 'Deleted message';
- return message.body || message.attachments[0]?.fileName || 'Attachment';
+ if (message.body) return message.body;
+ if (message.rich) return richPreview(message.rich);
+ const attachment = message.attachments[0];
+ if (!attachment) return 'Attachment';
+ return match(attachment.presentation)
+ .with('sticker', () => 'Sticker')
+ .with('gif', () => 'GIF')
+ .with('voice_note', () => 'Voice note')
+ .with(undefined, () => attachment.fileName)
+ .exhaustive();
+}
+
+/** WhatsApp's own "Forwarded" mark, above the message. */
+function ForwardedTag() {
+ return (
+
+
+ Forwarded
+
+ );
}
/** A message's images, video and voice notes, played inline. */
@@ -89,6 +136,10 @@ function MessageMedia(props: { attachments: InboundAttachment[] }) {
kind: file.kind,
fileName: file.attachment.fileName,
url: url.data,
+ width: file.attachment.width,
+ height: file.attachment.height,
+ durationSeconds: file.attachment.durationSeconds,
+ presentation: file.attachment.presentation,
});
});
return (
@@ -137,6 +188,27 @@ export function WhatsAppGroupView(props: {
onCleanup(() => {
alive = false;
});
+ // Media WhatsApp sent no size for grows after it loads; a reader who was
+ // at the bottom stays there. Rows with loaded media are watched for that
+ // growth (it lands after the load event).
+ const pinBottom = () => {
+ if (alive && scroller && fromBottom < 80)
+ scroller.scrollTop = scroller.scrollHeight;
+ };
+ const grown = new ResizeObserver(pinBottom);
+ onCleanup(() => grown.disconnect());
+ const holdBottom = (event: Event) => {
+ const row =
+ event.target instanceof Element
+ ? event.target.closest('[data-message-id]')
+ : null;
+ if (row) grown.observe(row);
+ };
+ const setScroller = (element: HTMLDivElement) => {
+ scroller = element;
+ element.addEventListener('load', holdBottom, true);
+ element.addEventListener('loadedmetadata', holdBottom, true);
+ };
/** Choosing a reply or an edit puts the composer in that mode. */
const setTarget = (target: ConversationTarget | undefined) => {
@@ -171,11 +243,21 @@ export function WhatsAppGroupView(props: {
const unreadIndex = () => {
const unread = conversation.initialUnreadCount();
const read = conversation.initialRead();
+ const stored = source.timeline.data ?? [];
const index = read
- ? (source.timeline.data ?? []).findIndex((message) => message.id === read)
+ ? stored.findIndex((message) => message.id === read)
: -1;
if (index >= 0) return index + 1;
- return unread ? Math.max(0, storedCount() - unread) : -1;
+ if (!unread) return -1;
+ // System lines never count as unread: the first new row is the
+ // `unread`-th message from the end.
+ let remaining = unread;
+ for (let at = stored.length - 1; at >= 0; at--) {
+ if (stored[at]?.kind === 'system') continue;
+ remaining--;
+ if (remaining === 0) return at;
+ }
+ return 0;
};
const listMeta = createMemo(() => {
const firstNew = unreadIndex();
@@ -185,16 +267,23 @@ export function WhatsAppGroupView(props: {
: (source.timeline.data ?? []).slice(firstNew).map((row) => row.id)
);
return buildChannelMessageListMeta(
- rows().map((message) => ({
- type: 'message' as const,
- message: {
- id: message.id,
- sender_id: senderKey(message),
- created_at: message.sentAt,
- attachments: [],
- deleted_at: message.deleted ? message.sentAt : null,
- },
- })),
+ rows().map((message) =>
+ message.kind === 'system'
+ ? {
+ type: 'activity' as const,
+ activity: { id: message.id, occurred_at: message.sentAt },
+ }
+ : {
+ type: 'message' as const,
+ message: {
+ id: message.id,
+ sender_id: senderKey(message),
+ created_at: message.sentAt,
+ attachments: [],
+ deleted_at: message.deleted ? message.sentAt : null,
+ },
+ }
+ ),
(message) => newIds.has(message.id),
!source.hasOlder()
);
@@ -402,7 +491,7 @@ export function WhatsAppGroupView(props: {
{(message) => {
- const meta = () => listMeta()[message.id];
+ const systemLine = () =>
+ message.kind === 'system' && message.rich?.type === 'system'
+ ? message.rich
+ : undefined;
+ const meta = () =>
+ listMeta()[
+ message.kind === 'system'
+ ? `activity:${message.id}`
+ : message.id
+ ];
const quoted = () =>
message.replyTo
? messagesById().get(message.replyTo)
@@ -481,7 +579,26 @@ export function WhatsAppGroupView(props: {
(attachment) => attachment.state !== 'ready'
);
return (
- <>
+
+
+
+ {(line) => (
+
+ )}
+
+ >
+ }
+ >
(
<>
+
+
+
{(id) => (
{content}
+
+ {(rich) => }
+
0}>
@@ -548,7 +671,7 @@ export function WhatsAppGroupView(props: {
>
)}
/>
- >
+
);
}}
@@ -626,6 +749,7 @@ export function WhatsAppGroupView(props: {
placeholder: composerPlaceholder(group()),
}}
features={WHATSAPP_COMPOSER}
+ participants={() => mentionable(group())}
fileAccept={group().limits.attachmentTypes.join(',')}
uploadAttachment={async (file) => {
const result = await conversation.upload(file);
diff --git a/apps/web/src/lib/core/messages/configured-message-editor.ts b/apps/web/src/lib/core/messages/configured-message-editor.ts
index 86530c2ae41..506396b6e33 100644
--- a/apps/web/src/lib/core/messages/configured-message-editor.ts
+++ b/apps/web/src/lib/core/messages/configured-message-editor.ts
@@ -1,4 +1,5 @@
import { buildConfig } from '@core/component/LexicalMarkdown/builder/MarkdownConfigBuilder';
+import type { MentionBucketId } from '@core/component/LexicalMarkdown/component/menu/MentionsMenu/MentionsMenuController';
import {
DefaultShortcuts,
type ItemMention,
@@ -27,6 +28,8 @@ export type MessageEditorOptions = {
namespace: string;
resolveAppLink?: MentionLinkResolver;
enableMentions?: boolean;
+ /** Restrict the `@` menu, e.g. to `['users']` for a fixed people list. */
+ mentionSources?: MentionBucketId[];
onMentionCreate?: (mention: ItemMention) => void;
onMentionRemove?: (mention: ItemMention) => void;
users?: () => IUser[];
@@ -57,6 +60,7 @@ export function createConfiguredMessageEditor(options: MessageEditorOptions) {
onCreate: options.onMentionCreate,
onRemove: options.onMentionRemove,
users: options.users,
+ sources: options.mentionSources,
block: options.groupMentions ? 'channel' : undefined,
disableMentionTracking: options.disableMentionTracking,
});
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/index.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/index.ts
index 192b389fc18..44180d30d1a 100644
--- a/apps/web/src/lib/service-clients/service-storage/generated/schemas/index.ts
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/index.ts
@@ -1209,7 +1209,11 @@ export * from './linkedAccountViewAccount';
export * from './linkedAdoptGroup';
export * from './linkedAttachment';
export * from './linkedAttachmentDocumentId';
+export * from './linkedAttachmentDurationSeconds';
+export * from './linkedAttachmentHeight';
+export * from './linkedAttachmentPresentation';
export * from './linkedAttachmentState';
+export * from './linkedAttachmentWidth';
export * from './linkedBackfill';
export * from './linkedBackfillState';
export * from './linkedCapabilities';
@@ -1233,6 +1237,7 @@ export * from './linkedIdentity';
export * from './linkedIdentityAlternateJid';
export * from './linkedIdentityDisplayName';
export * from './linkedMediaKind';
+export * from './linkedMediaPresentation';
export * from './linkedMessageAttachments';
export * from './linkedMutation';
export * from './linkedPairing';
@@ -2246,6 +2251,17 @@ export * from './webhookFilter';
export * from './webhookFilterIds';
export * from './webhookScope';
export * from './webhookStatus';
+export * from './whatsAppCall';
+export * from './whatsAppCallDurationSeconds';
+export * from './whatsAppCallOutcome';
+export * from './whatsAppContactCard';
+export * from './whatsAppContacts';
+export * from './whatsAppEvent';
+export * from './whatsAppEventDescription';
+export * from './whatsAppEventEndAt';
+export * from './whatsAppEventJoinLink';
+export * from './whatsAppEventLocation';
+export * from './whatsAppEventStartAt';
export * from './whatsAppGroupDetail';
export * from './whatsAppGroupDetailAllOf';
export * from './whatsAppGroupDetailAllOfRoster';
@@ -2254,6 +2270,7 @@ export * from './whatsAppGroupMessage';
export * from './whatsAppGroupMessageDeliveryState';
export * from './whatsAppGroupMessageEditedAt';
export * from './whatsAppGroupMessageReplyTo';
+export * from './whatsAppGroupMessageRich';
export * from './whatsAppGroupMessages';
export * from './whatsAppGroupMessagesNextCursor';
export * from './whatsAppGroupReaction';
@@ -2263,6 +2280,39 @@ export * from './whatsAppGroupSummary';
export * from './whatsAppGroupSummaryBackfill';
export * from './whatsAppGroupSummaryLastMessageAt';
export * from './whatsAppGroupSummarySendReason';
+export * from './whatsAppLocation';
+export * from './whatsAppLocationAddress';
+export * from './whatsAppLocationComment';
+export * from './whatsAppLocationName';
+export * from './whatsAppLocationUrl';
+export * from './whatsAppMention';
+export * from './whatsAppMentionAllOf';
+export * from './whatsAppMessageKind';
+export * from './whatsAppPersonRef';
+export * from './whatsAppPersonRefUserId';
+export * from './whatsAppPoll';
+export * from './whatsAppPollOption';
+export * from './whatsAppRichContent';
+export * from './whatsAppRichContentOneOf';
+export * from './whatsAppRichContentOneOfAllOf';
+export * from './whatsAppRichContentOneOfAllOfType';
+export * from './whatsAppRichContentOneOfFour';
+export * from './whatsAppRichContentOneOfFourAllOf';
+export * from './whatsAppRichContentOneOfFourAllOfType';
+export * from './whatsAppRichContentOneOfOnethree';
+export * from './whatsAppRichContentOneOfOnethreeAllOf';
+export * from './whatsAppRichContentOneOfOnethreeAllOfType';
+export * from './whatsAppRichContentOneOfOnezero';
+export * from './whatsAppRichContentOneOfOnezeroAllOf';
+export * from './whatsAppRichContentOneOfOnezeroAllOfType';
+export * from './whatsAppRichContentOneOfSeven';
+export * from './whatsAppRichContentOneOfSevenAllOf';
+export * from './whatsAppRichContentOneOfSevenAllOfType';
+export * from './whatsAppSystemAction';
+export * from './whatsAppSystemLine';
+export * from './whatsAppSystemLineActor';
+export * from './whatsAppSystemLineCall';
+export * from './whatsAppSystemLineValue';
export * from './whatsappGroupMessagesParams';
export * from './widget';
export * from './withCallId';
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachment.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachment.ts
index 692adbc5c0c..f535bab6eb2 100644
--- a/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachment.ts
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachment.ts
@@ -5,7 +5,11 @@
* OpenAPI spec version: 0.1.0
*/
import type { LinkedAttachmentDocumentId } from './linkedAttachmentDocumentId';
+import type { LinkedAttachmentDurationSeconds } from './linkedAttachmentDurationSeconds';
+import type { LinkedAttachmentHeight } from './linkedAttachmentHeight';
+import type { LinkedAttachmentPresentation } from './linkedAttachmentPresentation';
import type { LinkedAttachmentState } from './linkedAttachmentState';
+import type { LinkedAttachmentWidth } from './linkedAttachmentWidth';
/**
* One attachment of a linked message.
@@ -13,12 +17,23 @@ import type { LinkedAttachmentState } from './linkedAttachmentState';
export interface LinkedAttachment {
/** The private document, once ready. */
document_id?: LinkedAttachmentDocumentId;
+ /**
+ * Length of audio or video in whole seconds, when declared.
+ * @minimum 0
+ */
+ duration_seconds?: LinkedAttachmentDurationSeconds;
/** Display filename. */
file_name: string;
+ /**
+ * Pixel height, when declared.
+ * @minimum 0
+ */
+ height?: LinkedAttachmentHeight;
/** Declared MIME type. */
mime: string;
/** Position within the message. */
ordinal: number;
+ presentation?: LinkedAttachmentPresentation;
/**
* Declared size in bytes.
* @minimum 0
@@ -26,4 +41,9 @@ export interface LinkedAttachment {
size: number;
/** Current state. */
state: LinkedAttachmentState;
+ /**
+ * Pixel width of an image, sticker or video, when WhatsApp declared it.
+ * @minimum 0
+ */
+ width?: LinkedAttachmentWidth;
}
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachmentDurationSeconds.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachmentDurationSeconds.ts
new file mode 100644
index 00000000000..9316dede861
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachmentDurationSeconds.ts
@@ -0,0 +1,12 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * Length of audio or video in whole seconds, when declared.
+ * @minimum 0
+ */
+export type LinkedAttachmentDurationSeconds = number | null;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachmentHeight.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachmentHeight.ts
new file mode 100644
index 00000000000..71bc5af57ed
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachmentHeight.ts
@@ -0,0 +1,12 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * Pixel height, when declared.
+ * @minimum 0
+ */
+export type LinkedAttachmentHeight = number | null;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachmentPresentation.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachmentPresentation.ts
new file mode 100644
index 00000000000..7f51d20adcc
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachmentPresentation.ts
@@ -0,0 +1,9 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { LinkedMediaPresentation } from './linkedMediaPresentation';
+
+export type LinkedAttachmentPresentation = null | LinkedMediaPresentation;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachmentWidth.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachmentWidth.ts
new file mode 100644
index 00000000000..895bc8d1cc3
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedAttachmentWidth.ts
@@ -0,0 +1,12 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * Pixel width of an image, sticker or video, when WhatsApp declared it.
+ * @minimum 0
+ */
+export type LinkedAttachmentWidth = number | null;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedMediaPresentation.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedMediaPresentation.ts
new file mode 100644
index 00000000000..5e27e4e2373
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedMediaPresentation.ts
@@ -0,0 +1,19 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * How WhatsApp shows an attachment, when not as a plain file of its kind.
+ */
+export type LinkedMediaPresentation =
+ (typeof LinkedMediaPresentation)[keyof typeof LinkedMediaPresentation];
+
+// eslint-disable-next-line @typescript-eslint/no-redeclare
+export const LinkedMediaPresentation = {
+ sticker: 'sticker',
+ gif: 'gif',
+ voice_note: 'voice_note',
+} as const;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedReplyContentOneOf.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedReplyContentOneOf.ts
index 58faafd2025..0ee3297760b 100644
--- a/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedReplyContentOneOf.ts
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/linkedReplyContentOneOf.ts
@@ -10,6 +10,10 @@ import type { LinkedReplyContentOneOfType } from './linkedReplyContentOneOfType'
* Plain text.
*/
export type LinkedReplyContentOneOf = {
+ /** Group participants the text @-mentions, by their WhatsApp id
+(`jid` of a roster participant); the text must contain
+`@
` for each. */
+ mentions?: string[];
/** UTF-8 text, at most [`TEXT_LIMIT_BYTES`] bytes. */
text: string;
type: LinkedReplyContentOneOfType;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppCall.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppCall.ts
new file mode 100644
index 00000000000..efabd4de7fc
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppCall.ts
@@ -0,0 +1,23 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppCallDurationSeconds } from './whatsAppCallDurationSeconds';
+import type { WhatsAppCallOutcome } from './whatsAppCallOutcome';
+
+/**
+ * A call in the group.
+ */
+export interface WhatsAppCall {
+ /**
+ * Length, when it connected.
+ * @minimum 0
+ */
+ durationSeconds?: WhatsAppCallDurationSeconds;
+ /** How it went for the account that saw it. */
+ outcome: WhatsAppCallOutcome;
+ /** Video rather than voice. */
+ video: boolean;
+}
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppCallDurationSeconds.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppCallDurationSeconds.ts
new file mode 100644
index 00000000000..7da8254b4db
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppCallDurationSeconds.ts
@@ -0,0 +1,12 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * Length, when it connected.
+ * @minimum 0
+ */
+export type WhatsAppCallDurationSeconds = number | null;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppCallOutcome.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppCallOutcome.ts
new file mode 100644
index 00000000000..29e1f2aab22
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppCallOutcome.ts
@@ -0,0 +1,20 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * How a call went.
+ */
+export type WhatsAppCallOutcome =
+ (typeof WhatsAppCallOutcome)[keyof typeof WhatsAppCallOutcome];
+
+// eslint-disable-next-line @typescript-eslint/no-redeclare
+export const WhatsAppCallOutcome = {
+ answered: 'answered',
+ missed: 'missed',
+ rejected: 'rejected',
+ other: 'other',
+} as const;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppContactCard.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppContactCard.ts
new file mode 100644
index 00000000000..90e21ed27ad
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppContactCard.ts
@@ -0,0 +1,16 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * One contact card, from its vCard.
+ */
+export interface WhatsAppContactCard {
+ /** Display name. */
+ name: string;
+ /** Phone numbers as written on the card. */
+ phones: string[];
+}
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppContacts.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppContacts.ts
new file mode 100644
index 00000000000..6032d1d9bdc
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppContacts.ts
@@ -0,0 +1,15 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppContactCard } from './whatsAppContactCard';
+
+/**
+ * Contact cards.
+ */
+export interface WhatsAppContacts {
+ /** The cards, in order. */
+ contacts: WhatsAppContactCard[];
+}
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEvent.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEvent.ts
new file mode 100644
index 00000000000..adb50e97ae0
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEvent.ts
@@ -0,0 +1,31 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppEventDescription } from './whatsAppEventDescription';
+import type { WhatsAppEventEndAt } from './whatsAppEventEndAt';
+import type { WhatsAppEventJoinLink } from './whatsAppEventJoinLink';
+import type { WhatsAppEventLocation } from './whatsAppEventLocation';
+import type { WhatsAppEventStartAt } from './whatsAppEventStartAt';
+
+/**
+ * A WhatsApp event.
+ */
+export interface WhatsAppEvent {
+ /** Canceled by its organizer. */
+ canceled?: boolean;
+ /** Description. */
+ description?: WhatsAppEventDescription;
+ /** End. */
+ endAt?: WhatsAppEventEndAt;
+ /** Call link for an online event. */
+ joinLink?: WhatsAppEventJoinLink;
+ /** Place name or address. */
+ location?: WhatsAppEventLocation;
+ /** Title. */
+ name: string;
+ /** Start. */
+ startAt?: WhatsAppEventStartAt;
+}
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEventDescription.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEventDescription.ts
new file mode 100644
index 00000000000..253a315a52a
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEventDescription.ts
@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * Description.
+ */
+export type WhatsAppEventDescription = string | null;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEventEndAt.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEventEndAt.ts
new file mode 100644
index 00000000000..502bea64259
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEventEndAt.ts
@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * End.
+ */
+export type WhatsAppEventEndAt = string | null;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEventJoinLink.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEventJoinLink.ts
new file mode 100644
index 00000000000..5d4e75dde99
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEventJoinLink.ts
@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * Call link for an online event.
+ */
+export type WhatsAppEventJoinLink = string | null;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEventLocation.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEventLocation.ts
new file mode 100644
index 00000000000..ada43e98573
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEventLocation.ts
@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * Place name or address.
+ */
+export type WhatsAppEventLocation = string | null;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEventStartAt.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEventStartAt.ts
new file mode 100644
index 00000000000..40f82d8b2ea
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppEventStartAt.ts
@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * Start.
+ */
+export type WhatsAppEventStartAt = string | null;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppGroupMessage.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppGroupMessage.ts
index e212b8ef8b5..fe30bedbca7 100644
--- a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppGroupMessage.ts
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppGroupMessage.ts
@@ -8,8 +8,11 @@ import type { LinkedAttachment } from './linkedAttachment';
import type { WhatsAppGroupMessageDeliveryState } from './whatsAppGroupMessageDeliveryState';
import type { WhatsAppGroupMessageEditedAt } from './whatsAppGroupMessageEditedAt';
import type { WhatsAppGroupMessageReplyTo } from './whatsAppGroupMessageReplyTo';
+import type { WhatsAppGroupMessageRich } from './whatsAppGroupMessageRich';
import type { WhatsAppGroupReaction } from './whatsAppGroupReaction';
import type { WhatsAppGroupSender } from './whatsAppGroupSender';
+import type { WhatsAppMention } from './whatsAppMention';
+import type { WhatsAppMessageKind } from './whatsAppMessageKind';
/**
* One message in an adopted group.
@@ -24,14 +27,21 @@ export interface WhatsAppGroupMessage {
delivery_state?: WhatsAppGroupMessageDeliveryState;
/** Last edit timestamp. */
edited_at?: WhatsAppGroupMessageEditedAt;
+ /** Forwarded from another chat. */
+ forwarded: boolean;
/** Stable message id. */
id: string;
+ /** A message, or a system line. */
+ kind: WhatsAppMessageKind;
+ /** People the body @-mentions. */
+ mentions: WhatsAppMention[];
/** Reactions grouped by emoji. */
reactions: WhatsAppGroupReaction[];
/** Timestamp first received by Macro. */
received_at: string;
/** Message quoted by this one. */
reply_to?: WhatsAppGroupMessageReplyTo;
+ rich?: WhatsAppGroupMessageRich;
/** Sender. */
sender: WhatsAppGroupSender;
/** Timestamp assigned by WhatsApp. */
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppGroupMessageRich.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppGroupMessageRich.ts
new file mode 100644
index 00000000000..94c11bb6829
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppGroupMessageRich.ts
@@ -0,0 +1,9 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppRichContent } from './whatsAppRichContent';
+
+export type WhatsAppGroupMessageRich = null | WhatsAppRichContent;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppLocation.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppLocation.ts
new file mode 100644
index 00000000000..0e1a21b3fc5
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppLocation.ts
@@ -0,0 +1,30 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppLocationAddress } from './whatsAppLocationAddress';
+import type { WhatsAppLocationComment } from './whatsAppLocationComment';
+import type { WhatsAppLocationName } from './whatsAppLocationName';
+import type { WhatsAppLocationUrl } from './whatsAppLocationUrl';
+
+/**
+ * A shared place.
+ */
+export interface WhatsAppLocation {
+ /** Street address. */
+ address?: WhatsAppLocationAddress;
+ /** Caption or comment. */
+ comment?: WhatsAppLocationComment;
+ /** Degrees. */
+ latitude: number;
+ /** Shared as a live location (this is where it started). */
+ live?: boolean;
+ /** Degrees. */
+ longitude: number;
+ /** Place name. */
+ name?: WhatsAppLocationName;
+ /** Link the sender attached. */
+ url?: WhatsAppLocationUrl;
+}
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppLocationAddress.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppLocationAddress.ts
new file mode 100644
index 00000000000..82ccc5fd082
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppLocationAddress.ts
@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * Street address.
+ */
+export type WhatsAppLocationAddress = string | null;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppLocationComment.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppLocationComment.ts
new file mode 100644
index 00000000000..7a0dbe2c266
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppLocationComment.ts
@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * Caption or comment.
+ */
+export type WhatsAppLocationComment = string | null;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppLocationName.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppLocationName.ts
new file mode 100644
index 00000000000..f65e67014a4
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppLocationName.ts
@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * Place name.
+ */
+export type WhatsAppLocationName = string | null;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppLocationUrl.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppLocationUrl.ts
new file mode 100644
index 00000000000..c1cba49f687
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppLocationUrl.ts
@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * Link the sender attached.
+ */
+export type WhatsAppLocationUrl = string | null;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppMention.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppMention.ts
new file mode 100644
index 00000000000..f68b9dc1012
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppMention.ts
@@ -0,0 +1,14 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+import type { WhatsAppMentionAllOf } from './whatsAppMentionAllOf';
+import type { WhatsAppPersonRef } from './whatsAppPersonRef';
+
+/**
+ * An @-mention in a message body.
+ */
+export type WhatsAppMention = WhatsAppPersonRef & WhatsAppMentionAllOf;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppMentionAllOf.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppMentionAllOf.ts
new file mode 100644
index 00000000000..4deb4254a9b
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppMentionAllOf.ts
@@ -0,0 +1,12 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+export type WhatsAppMentionAllOf = {
+ /** What follows the `@` in the body (the digits of the person's WhatsApp
+id); replace `@` with the name. */
+ token: string;
+};
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppMessageKind.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppMessageKind.ts
new file mode 100644
index 00000000000..2cc84feea8c
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppMessageKind.ts
@@ -0,0 +1,18 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * What a timeline entry is.
+ */
+export type WhatsAppMessageKind =
+ (typeof WhatsAppMessageKind)[keyof typeof WhatsAppMessageKind];
+
+// eslint-disable-next-line @typescript-eslint/no-redeclare
+export const WhatsAppMessageKind = {
+ message: 'message',
+ system: 'system',
+} as const;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppPersonRef.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppPersonRef.ts
new file mode 100644
index 00000000000..b8ad6cfc6ee
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppPersonRef.ts
@@ -0,0 +1,18 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppPersonRefUserId } from './whatsAppPersonRefUserId';
+
+/**
+ * A person named in a message: a Macro user when verified, else their
+WhatsApp name, else their phone number.
+ */
+export interface WhatsAppPersonRef {
+ /** Display name, never authentication evidence. */
+ name: string;
+ /** Verified Macro user, if any. */
+ userId?: WhatsAppPersonRefUserId;
+}
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppPersonRefUserId.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppPersonRefUserId.ts
new file mode 100644
index 00000000000..2c561156262
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppPersonRefUserId.ts
@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * Verified Macro user, if any.
+ */
+export type WhatsAppPersonRefUserId = string | null;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppPoll.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppPoll.ts
new file mode 100644
index 00000000000..ecdf762fa66
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppPoll.ts
@@ -0,0 +1,24 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppPollOption } from './whatsAppPollOption';
+
+/**
+ * A poll, read-only in Macro.
+ */
+export interface WhatsAppPoll {
+ /** Options in order, with tallies. */
+ options: WhatsAppPollOption[];
+ /** The question. */
+ question: string;
+ /**
+ * How many options one person may choose; 0 means any number.
+ * @minimum 0
+ */
+ selectableCount: number;
+ /** People who voted for anything. */
+ voters: number;
+}
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppPollOption.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppPollOption.ts
new file mode 100644
index 00000000000..12a59b2e616
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppPollOption.ts
@@ -0,0 +1,21 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppPersonRef } from './whatsAppPersonRef';
+
+/**
+ * One option of a poll with its votes.
+ */
+export interface WhatsAppPollOption {
+ /** The caller chose it. */
+ mine: boolean;
+ /** Option text. */
+ name: string;
+ /** Who chose it. */
+ voters: WhatsAppPersonRef[];
+ /** People who chose it. */
+ votes: number;
+}
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContent.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContent.ts
new file mode 100644
index 00000000000..e201bc6b4d4
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContent.ts
@@ -0,0 +1,21 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppRichContentOneOf } from './whatsAppRichContentOneOf';
+import type { WhatsAppRichContentOneOfFour } from './whatsAppRichContentOneOfFour';
+import type { WhatsAppRichContentOneOfOnethree } from './whatsAppRichContentOneOfOnethree';
+import type { WhatsAppRichContentOneOfOnezero } from './whatsAppRichContentOneOfOnezero';
+import type { WhatsAppRichContentOneOfSeven } from './whatsAppRichContentOneOfSeven';
+
+/**
+ * Structured content of a message besides its body.
+ */
+export type WhatsAppRichContent =
+ | WhatsAppRichContentOneOf
+ | WhatsAppRichContentOneOfFour
+ | WhatsAppRichContentOneOfSeven
+ | WhatsAppRichContentOneOfOnezero
+ | WhatsAppRichContentOneOfOnethree;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOf.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOf.ts
new file mode 100644
index 00000000000..b5a662f92c7
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOf.ts
@@ -0,0 +1,14 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppPoll } from './whatsAppPoll';
+import type { WhatsAppRichContentOneOfAllOf } from './whatsAppRichContentOneOfAllOf';
+
+/**
+ * A poll with current votes.
+ */
+export type WhatsAppRichContentOneOf = WhatsAppPoll &
+ WhatsAppRichContentOneOfAllOf;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfAllOf.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfAllOf.ts
new file mode 100644
index 00000000000..3efbeafb164
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfAllOf.ts
@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppRichContentOneOfAllOfType } from './whatsAppRichContentOneOfAllOfType';
+
+export type WhatsAppRichContentOneOfAllOf = {
+ type: WhatsAppRichContentOneOfAllOfType;
+};
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfAllOfType.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfAllOfType.ts
new file mode 100644
index 00000000000..2d7756cf4ad
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfAllOfType.ts
@@ -0,0 +1,14 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+export type WhatsAppRichContentOneOfAllOfType =
+ (typeof WhatsAppRichContentOneOfAllOfType)[keyof typeof WhatsAppRichContentOneOfAllOfType];
+
+// eslint-disable-next-line @typescript-eslint/no-redeclare
+export const WhatsAppRichContentOneOfAllOfType = {
+ poll: 'poll',
+} as const;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfFour.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfFour.ts
new file mode 100644
index 00000000000..3ada745471d
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfFour.ts
@@ -0,0 +1,14 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppLocation } from './whatsAppLocation';
+import type { WhatsAppRichContentOneOfFourAllOf } from './whatsAppRichContentOneOfFourAllOf';
+
+/**
+ * A place (or the start of a live location); link to a maps URL.
+ */
+export type WhatsAppRichContentOneOfFour = WhatsAppLocation &
+ WhatsAppRichContentOneOfFourAllOf;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfFourAllOf.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfFourAllOf.ts
new file mode 100644
index 00000000000..2202cf1db46
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfFourAllOf.ts
@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppRichContentOneOfFourAllOfType } from './whatsAppRichContentOneOfFourAllOfType';
+
+export type WhatsAppRichContentOneOfFourAllOf = {
+ type: WhatsAppRichContentOneOfFourAllOfType;
+};
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfFourAllOfType.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfFourAllOfType.ts
new file mode 100644
index 00000000000..1d19da6b130
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfFourAllOfType.ts
@@ -0,0 +1,14 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+export type WhatsAppRichContentOneOfFourAllOfType =
+ (typeof WhatsAppRichContentOneOfFourAllOfType)[keyof typeof WhatsAppRichContentOneOfFourAllOfType];
+
+// eslint-disable-next-line @typescript-eslint/no-redeclare
+export const WhatsAppRichContentOneOfFourAllOfType = {
+ location: 'location',
+} as const;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnethree.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnethree.ts
new file mode 100644
index 00000000000..66c371cd411
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnethree.ts
@@ -0,0 +1,15 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+import type { WhatsAppRichContentOneOfOnethreeAllOf } from './whatsAppRichContentOneOfOnethreeAllOf';
+import type { WhatsAppSystemLine } from './whatsAppSystemLine';
+
+/**
+ * A system line.
+ */
+export type WhatsAppRichContentOneOfOnethree = WhatsAppSystemLine &
+ WhatsAppRichContentOneOfOnethreeAllOf;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnethreeAllOf.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnethreeAllOf.ts
new file mode 100644
index 00000000000..576c02e548f
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnethreeAllOf.ts
@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppRichContentOneOfOnethreeAllOfType } from './whatsAppRichContentOneOfOnethreeAllOfType';
+
+export type WhatsAppRichContentOneOfOnethreeAllOf = {
+ type: WhatsAppRichContentOneOfOnethreeAllOfType;
+};
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnethreeAllOfType.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnethreeAllOfType.ts
new file mode 100644
index 00000000000..05434dd5d2d
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnethreeAllOfType.ts
@@ -0,0 +1,14 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+export type WhatsAppRichContentOneOfOnethreeAllOfType =
+ (typeof WhatsAppRichContentOneOfOnethreeAllOfType)[keyof typeof WhatsAppRichContentOneOfOnethreeAllOfType];
+
+// eslint-disable-next-line @typescript-eslint/no-redeclare
+export const WhatsAppRichContentOneOfOnethreeAllOfType = {
+ system: 'system',
+} as const;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnezero.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnezero.ts
new file mode 100644
index 00000000000..2732c7b4198
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnezero.ts
@@ -0,0 +1,14 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppEvent } from './whatsAppEvent';
+import type { WhatsAppRichContentOneOfOnezeroAllOf } from './whatsAppRichContentOneOfOnezeroAllOf';
+
+/**
+ * A WhatsApp event.
+ */
+export type WhatsAppRichContentOneOfOnezero = WhatsAppEvent &
+ WhatsAppRichContentOneOfOnezeroAllOf;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnezeroAllOf.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnezeroAllOf.ts
new file mode 100644
index 00000000000..9cd14d3a0b6
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnezeroAllOf.ts
@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppRichContentOneOfOnezeroAllOfType } from './whatsAppRichContentOneOfOnezeroAllOfType';
+
+export type WhatsAppRichContentOneOfOnezeroAllOf = {
+ type: WhatsAppRichContentOneOfOnezeroAllOfType;
+};
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnezeroAllOfType.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnezeroAllOfType.ts
new file mode 100644
index 00000000000..ce102c2cddf
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfOnezeroAllOfType.ts
@@ -0,0 +1,14 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+export type WhatsAppRichContentOneOfOnezeroAllOfType =
+ (typeof WhatsAppRichContentOneOfOnezeroAllOfType)[keyof typeof WhatsAppRichContentOneOfOnezeroAllOfType];
+
+// eslint-disable-next-line @typescript-eslint/no-redeclare
+export const WhatsAppRichContentOneOfOnezeroAllOfType = {
+ event: 'event',
+} as const;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfSeven.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfSeven.ts
new file mode 100644
index 00000000000..d3582a8a643
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfSeven.ts
@@ -0,0 +1,14 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppContacts } from './whatsAppContacts';
+import type { WhatsAppRichContentOneOfSevenAllOf } from './whatsAppRichContentOneOfSevenAllOf';
+
+/**
+ * Contact cards.
+ */
+export type WhatsAppRichContentOneOfSeven = WhatsAppContacts &
+ WhatsAppRichContentOneOfSevenAllOf;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfSevenAllOf.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfSevenAllOf.ts
new file mode 100644
index 00000000000..126f4ae3782
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfSevenAllOf.ts
@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppRichContentOneOfSevenAllOfType } from './whatsAppRichContentOneOfSevenAllOfType';
+
+export type WhatsAppRichContentOneOfSevenAllOf = {
+ type: WhatsAppRichContentOneOfSevenAllOfType;
+};
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfSevenAllOfType.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfSevenAllOfType.ts
new file mode 100644
index 00000000000..18350d96919
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppRichContentOneOfSevenAllOfType.ts
@@ -0,0 +1,14 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+export type WhatsAppRichContentOneOfSevenAllOfType =
+ (typeof WhatsAppRichContentOneOfSevenAllOfType)[keyof typeof WhatsAppRichContentOneOfSevenAllOfType];
+
+// eslint-disable-next-line @typescript-eslint/no-redeclare
+export const WhatsAppRichContentOneOfSevenAllOfType = {
+ contacts: 'contacts',
+} as const;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppSystemAction.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppSystemAction.ts
new file mode 100644
index 00000000000..92f5875062b
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppSystemAction.ts
@@ -0,0 +1,30 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * Kinds of system lines.
+ */
+export type WhatsAppSystemAction =
+ (typeof WhatsAppSystemAction)[keyof typeof WhatsAppSystemAction];
+
+// eslint-disable-next-line @typescript-eslint/no-redeclare
+export const WhatsAppSystemAction = {
+ group_created: 'group_created',
+ participants_added: 'participants_added',
+ participants_removed: 'participants_removed',
+ participants_left: 'participants_left',
+ participants_joined: 'participants_joined',
+ participants_promoted: 'participants_promoted',
+ participants_demoted: 'participants_demoted',
+ subject_changed: 'subject_changed',
+ icon_changed: 'icon_changed',
+ description_changed: 'description_changed',
+ disappearing_changed: 'disappearing_changed',
+ message_pinned: 'message_pinned',
+ message_unpinned: 'message_unpinned',
+ call: 'call',
+} as const;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppSystemLine.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppSystemLine.ts
new file mode 100644
index 00000000000..8ad6ca19beb
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppSystemLine.ts
@@ -0,0 +1,27 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+import type { WhatsAppPersonRef } from './whatsAppPersonRef';
+import type { WhatsAppSystemAction } from './whatsAppSystemAction';
+import type { WhatsAppSystemLineActor } from './whatsAppSystemLineActor';
+import type { WhatsAppSystemLineCall } from './whatsAppSystemLineCall';
+import type { WhatsAppSystemLineValue } from './whatsAppSystemLineValue';
+
+/**
+ * Something that happened in the group.
+ */
+export interface WhatsAppSystemLine {
+ /** What happened. */
+ action: WhatsAppSystemAction;
+ actor?: WhatsAppSystemLineActor;
+ call?: WhatsAppSystemLineCall;
+ /** Who it happened to (added, removed, promoted, …). */
+ participants: WhatsAppPersonRef[];
+ /** New subject or description; disappearing-messages timer in seconds
+(`"0"` is off). */
+ value?: WhatsAppSystemLineValue;
+}
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppSystemLineActor.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppSystemLineActor.ts
new file mode 100644
index 00000000000..f19560ab7a2
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppSystemLineActor.ts
@@ -0,0 +1,9 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppPersonRef } from './whatsAppPersonRef';
+
+export type WhatsAppSystemLineActor = null | WhatsAppPersonRef;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppSystemLineCall.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppSystemLineCall.ts
new file mode 100644
index 00000000000..8d37d78628e
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppSystemLineCall.ts
@@ -0,0 +1,9 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+import type { WhatsAppCall } from './whatsAppCall';
+
+export type WhatsAppSystemLineCall = null | WhatsAppCall;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppSystemLineValue.ts b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppSystemLineValue.ts
new file mode 100644
index 00000000000..c2a6f98c7ad
--- /dev/null
+++ b/apps/web/src/lib/service-clients/service-storage/generated/schemas/whatsAppSystemLineValue.ts
@@ -0,0 +1,12 @@
+/**
+ * Generated by orval v7.21.0 🍺
+ * Do not edit manually.
+ * document_storage_service
+ * OpenAPI spec version: 0.1.0
+ */
+
+/**
+ * New subject or description; disappearing-messages timer in seconds
+(`"0"` is off).
+ */
+export type WhatsAppSystemLineValue = string | null;
diff --git a/apps/web/src/lib/service-clients/service-storage/generated/zod.ts b/apps/web/src/lib/service-clients/service-storage/generated/zod.ts
index d1e988f4bda..6da28b37ca2 100644
--- a/apps/web/src/lib/service-clients/service-storage/generated/zod.ts
+++ b/apps/web/src/lib/service-clients/service-storage/generated/zod.ts
@@ -49940,6 +49940,12 @@ export const linkedDeliveryResponseItem = zod
.union([
zod
.object({
+ mentions: zod
+ .array(zod.string())
+ .optional()
+ .describe(
+ 'Group participants the text @-mentions, by their WhatsApp id\n(`jid` of a roster participant); the text must contain\n`@` for each.'
+ ),
text: zod
.string()
.describe('UTF-8 text, at most [`TEXT_LIMIT_BYTES`] bytes.'),
@@ -50025,8 +50031,18 @@ export const whatsappGroupMessagesQueryParams = zod.object({
limit: zod.number().nullish().describe('Page size, 1 through 100.'),
});
+export const whatsappGroupMessagesResponseMessagesItemAttachmentsItemDurationSecondsMin = 0;
+
+export const whatsappGroupMessagesResponseMessagesItemAttachmentsItemHeightMin = 0;
+
export const whatsappGroupMessagesResponseMessagesItemAttachmentsItemSizeMin = 0;
+export const whatsappGroupMessagesResponseMessagesItemAttachmentsItemWidthMin = 0;
+
+export const whatsappGroupMessagesResponseMessagesItemRichSelectableCountMin = 0;
+
+export const whatsappGroupMessagesResponseMessagesItemRichCallDurationSecondsMin = 0;
+
export const whatsappGroupMessagesResponse = zod
.object({
messages: zod
@@ -50041,11 +50057,37 @@ export const whatsappGroupMessagesResponse = zod
.uuid()
.nullish()
.describe('The private document, once ready.'),
+ duration_seconds: zod
+ .number()
+ .min(
+ whatsappGroupMessagesResponseMessagesItemAttachmentsItemDurationSecondsMin
+ )
+ .nullish()
+ .describe(
+ 'Length of audio or video in whole seconds, when declared.'
+ ),
file_name: zod.string().describe('Display filename.'),
+ height: zod
+ .number()
+ .min(
+ whatsappGroupMessagesResponseMessagesItemAttachmentsItemHeightMin
+ )
+ .nullish()
+ .describe('Pixel height, when declared.'),
mime: zod.string().describe('Declared MIME type.'),
ordinal: zod
.number()
.describe('Position within the message.'),
+ presentation: zod
+ .union([
+ zod.null(),
+ zod
+ .enum(['sticker', 'gif', 'voice_note'])
+ .describe(
+ 'How WhatsApp shows an attachment, when not as a plain file of its kind.'
+ ),
+ ])
+ .optional(),
size: zod
.number()
.min(
@@ -50057,6 +50099,15 @@ export const whatsappGroupMessagesResponse = zod
.describe(
'Where an attachment of a linked message stands.'
),
+ width: zod
+ .number()
+ .min(
+ whatsappGroupMessagesResponseMessagesItemAttachmentsItemWidthMin
+ )
+ .nullish()
+ .describe(
+ 'Pixel width of an image, sticker or video, when WhatsApp declared it.'
+ ),
})
.describe('One attachment of a linked message.')
)
@@ -50086,7 +50137,38 @@ export const whatsappGroupMessagesResponse = zod
.datetime({})
.nullish()
.describe('Last edit timestamp.'),
+ forwarded: zod.boolean().describe('Forwarded from another chat.'),
id: zod.uuid().describe('Stable message id.'),
+ kind: zod
+ .enum(['message', 'system'])
+ .describe('What a timeline entry is.'),
+ mentions: zod
+ .array(
+ zod
+ .object({
+ name: zod
+ .string()
+ .describe('Display name, never authentication evidence.'),
+ userId: zod
+ .string()
+ .nullish()
+ .describe('Verified Macro user, if any.'),
+ })
+ .describe(
+ 'A person named in a message: a Macro user when verified, else their\nWhatsApp name, else their phone number.'
+ )
+ .and(
+ zod.object({
+ token: zod
+ .string()
+ .describe(
+ "What follows the `@` in the body (the digits of the person's WhatsApp\nid); replace `@` with the name."
+ ),
+ })
+ )
+ .describe('An @-mention in a message body.')
+ )
+ .describe('People the body @-mentions.'),
reactions: zod
.array(
zod
@@ -50105,6 +50187,266 @@ export const whatsappGroupMessagesResponse = zod
.uuid()
.nullish()
.describe('Message quoted by this one.'),
+ rich: zod
+ .union([
+ zod.null(),
+ zod
+ .union([
+ zod
+ .object({
+ options: zod
+ .array(
+ zod
+ .object({
+ mine: zod
+ .boolean()
+ .describe('The caller chose it.'),
+ name: zod.string().describe('Option text.'),
+ voters: zod
+ .array(
+ zod
+ .object({
+ name: zod
+ .string()
+ .describe(
+ 'Display name, never authentication evidence.'
+ ),
+ userId: zod
+ .string()
+ .nullish()
+ .describe(
+ 'Verified Macro user, if any.'
+ ),
+ })
+ .describe(
+ 'A person named in a message: a Macro user when verified, else their\nWhatsApp name, else their phone number.'
+ )
+ )
+ .describe('Who chose it.'),
+ votes: zod
+ .number()
+ .describe('People who chose it.'),
+ })
+ .describe('One option of a poll with its votes.')
+ )
+ .describe('Options in order, with tallies.'),
+ question: zod.string().describe('The question.'),
+ selectableCount: zod
+ .number()
+ .min(
+ whatsappGroupMessagesResponseMessagesItemRichSelectableCountMin
+ )
+ .describe(
+ 'How many options one person may choose; 0 means any number.'
+ ),
+ voters: zod
+ .number()
+ .describe('People who voted for anything.'),
+ })
+ .describe('A poll, read-only in Macro.')
+ .and(
+ zod.object({
+ type: zod.enum(['poll']),
+ })
+ )
+ .describe('A poll with current votes.'),
+ zod
+ .object({
+ address: zod
+ .string()
+ .nullish()
+ .describe('Street address.'),
+ comment: zod
+ .string()
+ .nullish()
+ .describe('Caption or comment.'),
+ latitude: zod.number().describe('Degrees.'),
+ live: zod
+ .boolean()
+ .optional()
+ .describe(
+ 'Shared as a live location (this is where it started).'
+ ),
+ longitude: zod.number().describe('Degrees.'),
+ name: zod.string().nullish().describe('Place name.'),
+ url: zod
+ .string()
+ .nullish()
+ .describe('Link the sender attached.'),
+ })
+ .describe('A shared place.')
+ .and(
+ zod.object({
+ type: zod.enum(['location']),
+ })
+ )
+ .describe(
+ 'A place (or the start of a live location); link to a maps URL.'
+ ),
+ zod
+ .object({
+ contacts: zod
+ .array(
+ zod
+ .object({
+ name: zod.string().describe('Display name.'),
+ phones: zod
+ .array(zod.string())
+ .describe(
+ 'Phone numbers as written on the card.'
+ ),
+ })
+ .describe('One contact card, from its vCard.')
+ )
+ .describe('The cards, in order.'),
+ })
+ .describe('Contact cards.')
+ .and(
+ zod.object({
+ type: zod.enum(['contacts']),
+ })
+ )
+ .describe('Contact cards.'),
+ zod
+ .object({
+ canceled: zod
+ .boolean()
+ .optional()
+ .describe('Canceled by its organizer.'),
+ description: zod
+ .string()
+ .nullish()
+ .describe('Description.'),
+ endAt: zod.iso.datetime({}).nullish().describe('End.'),
+ joinLink: zod
+ .string()
+ .nullish()
+ .describe('Call link for an online event.'),
+ location: zod
+ .string()
+ .nullish()
+ .describe('Place name or address.'),
+ name: zod.string().describe('Title.'),
+ startAt: zod.iso
+ .datetime({})
+ .nullish()
+ .describe('Start.'),
+ })
+ .describe('A WhatsApp event.')
+ .and(
+ zod.object({
+ type: zod.enum(['event']),
+ })
+ )
+ .describe('A WhatsApp event.'),
+ zod
+ .object({
+ action: zod
+ .enum([
+ 'group_created',
+ 'participants_added',
+ 'participants_removed',
+ 'participants_left',
+ 'participants_joined',
+ 'participants_promoted',
+ 'participants_demoted',
+ 'subject_changed',
+ 'icon_changed',
+ 'description_changed',
+ 'disappearing_changed',
+ 'message_pinned',
+ 'message_unpinned',
+ 'call',
+ ])
+ .describe('Kinds of system lines.'),
+ actor: zod
+ .union([
+ zod.null(),
+ zod
+ .object({
+ name: zod
+ .string()
+ .describe(
+ 'Display name, never authentication evidence.'
+ ),
+ userId: zod
+ .string()
+ .nullish()
+ .describe('Verified Macro user, if any.'),
+ })
+ .describe(
+ 'A person named in a message: a Macro user when verified, else their\nWhatsApp name, else their phone number.'
+ ),
+ ])
+ .optional(),
+ call: zod
+ .union([
+ zod.null(),
+ zod
+ .object({
+ durationSeconds: zod
+ .number()
+ .min(
+ whatsappGroupMessagesResponseMessagesItemRichCallDurationSecondsMin
+ )
+ .nullish()
+ .describe('Length, when it connected.'),
+ outcome: zod
+ .enum([
+ 'answered',
+ 'missed',
+ 'rejected',
+ 'other',
+ ])
+ .describe('How a call went.'),
+ video: zod
+ .boolean()
+ .describe('Video rather than voice.'),
+ })
+ .describe('A call in the group.'),
+ ])
+ .optional(),
+ participants: zod
+ .array(
+ zod
+ .object({
+ name: zod
+ .string()
+ .describe(
+ 'Display name, never authentication evidence.'
+ ),
+ userId: zod
+ .string()
+ .nullish()
+ .describe('Verified Macro user, if any.'),
+ })
+ .describe(
+ 'A person named in a message: a Macro user when verified, else their\nWhatsApp name, else their phone number.'
+ )
+ )
+ .describe(
+ 'Who it happened to (added, removed, promoted, …).'
+ ),
+ value: zod
+ .string()
+ .nullish()
+ .describe(
+ 'New subject or description; disappearing-messages timer in seconds\n(`\"0\"` is off).'
+ ),
+ })
+ .describe('Something that happened in the group.')
+ .and(
+ zod.object({
+ type: zod.enum(['system']),
+ })
+ )
+ .describe('A system line.'),
+ ])
+ .describe(
+ 'Structured content of a message besides its body.'
+ ),
+ ])
+ .optional(),
sender: zod
.object({
name: zod
@@ -50144,8 +50486,14 @@ export const linkedMessageAttachmentsBody = zod.array(
linkedMessageAttachmentsBodyItem
);
+export const linkedMessageAttachmentsResponseLinkedAttachmentsItemDurationSecondsMin = 0;
+
+export const linkedMessageAttachmentsResponseLinkedAttachmentsItemHeightMin = 0;
+
export const linkedMessageAttachmentsResponseLinkedAttachmentsItemSizeMin = 0;
+export const linkedMessageAttachmentsResponseLinkedAttachmentsItemWidthMin = 0;
+
export const linkedMessageAttachmentsResponseItem = zod
.object({
linked_attachments: zod
@@ -50156,9 +50504,35 @@ export const linkedMessageAttachmentsResponseItem = zod
.uuid()
.nullish()
.describe('The private document, once ready.'),
+ duration_seconds: zod
+ .number()
+ .min(
+ linkedMessageAttachmentsResponseLinkedAttachmentsItemDurationSecondsMin
+ )
+ .nullish()
+ .describe(
+ 'Length of audio or video in whole seconds, when declared.'
+ ),
file_name: zod.string().describe('Display filename.'),
+ height: zod
+ .number()
+ .min(
+ linkedMessageAttachmentsResponseLinkedAttachmentsItemHeightMin
+ )
+ .nullish()
+ .describe('Pixel height, when declared.'),
mime: zod.string().describe('Declared MIME type.'),
ordinal: zod.number().describe('Position within the message.'),
+ presentation: zod
+ .union([
+ zod.null(),
+ zod
+ .enum(['sticker', 'gif', 'voice_note'])
+ .describe(
+ 'How WhatsApp shows an attachment, when not as a plain file of its kind.'
+ ),
+ ])
+ .optional(),
size: zod
.number()
.min(linkedMessageAttachmentsResponseLinkedAttachmentsItemSizeMin)
@@ -50166,6 +50540,15 @@ export const linkedMessageAttachmentsResponseItem = zod
state: zod
.enum(['loading', 'ready', 'unavailable', 'unsupported'])
.describe('Where an attachment of a linked message stands.'),
+ width: zod
+ .number()
+ .min(
+ linkedMessageAttachmentsResponseLinkedAttachmentsItemWidthMin
+ )
+ .nullish()
+ .describe(
+ 'Pixel width of an image, sticker or video, when WhatsApp declared it.'
+ ),
})
.describe('One attachment of a linked message.')
)
@@ -50318,6 +50701,12 @@ export const whatsappGroupSendsResponseItem = zod
.union([
zod
.object({
+ mentions: zod
+ .array(zod.string())
+ .optional()
+ .describe(
+ 'Group participants the text @-mentions, by their WhatsApp id\n(`jid` of a roster participant); the text must contain\n`@` for each.'
+ ),
text: zod
.string()
.describe('UTF-8 text, at most [`TEXT_LIMIT_BYTES`] bytes.'),
@@ -50408,6 +50797,12 @@ export const linkedReplyBody = zod
.union([
zod
.object({
+ mentions: zod
+ .array(zod.string())
+ .optional()
+ .describe(
+ 'Group participants the text @-mentions, by their WhatsApp id\n(`jid` of a roster participant); the text must contain\n`@` for each.'
+ ),
text: zod
.string()
.describe('UTF-8 text, at most [`TEXT_LIMIT_BYTES`] bytes.'),
@@ -50481,6 +50876,12 @@ export const linkedReplyResponse = zod
.union([
zod
.object({
+ mentions: zod
+ .array(zod.string())
+ .optional()
+ .describe(
+ 'Group participants the text @-mentions, by their WhatsApp id\n(`jid` of a roster participant); the text must contain\n`@` for each.'
+ ),
text: zod
.string()
.describe('UTF-8 text, at most [`TEXT_LIMIT_BYTES`] bytes.'),
diff --git a/apps/web/src/lib/service-clients/service-storage/openapi.json b/apps/web/src/lib/service-clients/service-storage/openapi.json
index 063bea67be5..40ef0d2c478 100644
--- a/apps/web/src/lib/service-clients/service-storage/openapi.json
+++ b/apps/web/src/lib/service-clients/service-storage/openapi.json
@@ -33379,10 +33379,22 @@
"format": "uuid",
"description": "The private document, once ready."
},
+ "duration_seconds": {
+ "type": ["integer", "null"],
+ "format": "int32",
+ "description": "Length of audio or video in whole seconds, when declared.",
+ "minimum": 0
+ },
"file_name": {
"type": "string",
"description": "Display filename."
},
+ "height": {
+ "type": ["integer", "null"],
+ "format": "int32",
+ "description": "Pixel height, when declared.",
+ "minimum": 0
+ },
"mime": {
"type": "string",
"description": "Declared MIME type."
@@ -33392,6 +33404,17 @@
"format": "int32",
"description": "Position within the message."
},
+ "presentation": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/components/schemas/LinkedMediaPresentation",
+ "description": "Sticker, GIF or voice note; `null` for an ordinary file of its kind."
+ }
+ ]
+ },
"size": {
"type": "integer",
"format": "int64",
@@ -33401,6 +33424,12 @@
"state": {
"$ref": "#/components/schemas/LinkedAttachmentState",
"description": "Current state."
+ },
+ "width": {
+ "type": ["integer", "null"],
+ "format": "int32",
+ "description": "Pixel width of an image, sticker or video, when WhatsApp declared it.",
+ "minimum": 0
}
}
},
@@ -33739,6 +33768,11 @@
"description": "Attachment categories WhatsApp distinguishes.",
"enum": ["image", "audio", "video", "document"]
},
+ "LinkedMediaPresentation": {
+ "type": "string",
+ "description": "How WhatsApp shows an attachment, when not as a plain file of its kind.",
+ "enum": ["sticker", "gif", "voice_note"]
+ },
"LinkedMessageAttachments": {
"type": "object",
"description": "The attachments of one linked group message.",
@@ -33872,6 +33906,13 @@
"description": "Plain text.",
"required": ["text", "type"],
"properties": {
+ "mentions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Group participants the text @-mentions, by their WhatsApp id\n(`jid` of a roster participant); the text must contain\n`@` for each."
+ },
"text": {
"type": "string",
"description": "UTF-8 text, at most [`TEXT_LIMIT_BYTES`] bytes."
@@ -44540,6 +44581,101 @@
}
}
},
+ "WhatsAppCall": {
+ "type": "object",
+ "description": "A call in the group.",
+ "required": ["video", "outcome"],
+ "properties": {
+ "durationSeconds": {
+ "type": ["integer", "null"],
+ "format": "int32",
+ "description": "Length, when it connected.",
+ "minimum": 0
+ },
+ "outcome": {
+ "$ref": "#/components/schemas/WhatsAppCallOutcome",
+ "description": "How it went for the account that saw it."
+ },
+ "video": {
+ "type": "boolean",
+ "description": "Video rather than voice."
+ }
+ }
+ },
+ "WhatsAppCallOutcome": {
+ "type": "string",
+ "description": "How a call went.",
+ "enum": ["answered", "missed", "rejected", "other"]
+ },
+ "WhatsAppContactCard": {
+ "type": "object",
+ "description": "One contact card, from its vCard.",
+ "required": ["name", "phones"],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Display name."
+ },
+ "phones": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Phone numbers as written on the card."
+ }
+ }
+ },
+ "WhatsAppContacts": {
+ "type": "object",
+ "description": "Contact cards.",
+ "required": ["contacts"],
+ "properties": {
+ "contacts": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WhatsAppContactCard"
+ },
+ "description": "The cards, in order."
+ }
+ }
+ },
+ "WhatsAppEvent": {
+ "type": "object",
+ "description": "A WhatsApp event.",
+ "required": ["name"],
+ "properties": {
+ "canceled": {
+ "type": "boolean",
+ "description": "Canceled by its organizer."
+ },
+ "description": {
+ "type": ["string", "null"],
+ "description": "Description."
+ },
+ "endAt": {
+ "type": ["string", "null"],
+ "format": "date-time",
+ "description": "End."
+ },
+ "joinLink": {
+ "type": ["string", "null"],
+ "description": "Call link for an online event."
+ },
+ "location": {
+ "type": ["string", "null"],
+ "description": "Place name or address."
+ },
+ "name": {
+ "type": "string",
+ "description": "Title."
+ },
+ "startAt": {
+ "type": ["string", "null"],
+ "format": "date-time",
+ "description": "Start."
+ }
+ }
+ },
"WhatsAppGroupDetail": {
"allOf": [
{
@@ -44593,7 +44729,10 @@
"received_at",
"deleted",
"reactions",
- "attachments"
+ "attachments",
+ "kind",
+ "forwarded",
+ "mentions"
],
"properties": {
"attachments": {
@@ -44627,11 +44766,26 @@
"format": "date-time",
"description": "Last edit timestamp."
},
+ "forwarded": {
+ "type": "boolean",
+ "description": "Forwarded from another chat."
+ },
"id": {
"type": "string",
"format": "uuid",
"description": "Stable message id."
},
+ "kind": {
+ "$ref": "#/components/schemas/WhatsAppMessageKind",
+ "description": "A message, or a system line."
+ },
+ "mentions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WhatsAppMention"
+ },
+ "description": "People the body @-mentions."
+ },
"reactions": {
"type": "array",
"items": {
@@ -44649,6 +44803,17 @@
"format": "uuid",
"description": "Message quoted by this one."
},
+ "rich": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/components/schemas/WhatsAppRichContent",
+ "description": "Poll, location, contact cards, event or system line."
+ }
+ ]
+ },
"sender": {
"$ref": "#/components/schemas/WhatsAppGroupSender",
"description": "Sender."
@@ -44775,6 +44940,302 @@
}
}
},
+ "WhatsAppLocation": {
+ "type": "object",
+ "description": "A shared place.",
+ "required": ["latitude", "longitude"],
+ "properties": {
+ "address": {
+ "type": ["string", "null"],
+ "description": "Street address."
+ },
+ "comment": {
+ "type": ["string", "null"],
+ "description": "Caption or comment."
+ },
+ "latitude": {
+ "type": "number",
+ "format": "double",
+ "description": "Degrees."
+ },
+ "live": {
+ "type": "boolean",
+ "description": "Shared as a live location (this is where it started)."
+ },
+ "longitude": {
+ "type": "number",
+ "format": "double",
+ "description": "Degrees."
+ },
+ "name": {
+ "type": ["string", "null"],
+ "description": "Place name."
+ },
+ "url": {
+ "type": ["string", "null"],
+ "description": "Link the sender attached."
+ }
+ }
+ },
+ "WhatsAppMention": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/WhatsAppPersonRef",
+ "description": "Who it is."
+ },
+ {
+ "type": "object",
+ "required": ["token"],
+ "properties": {
+ "token": {
+ "type": "string",
+ "description": "What follows the `@` in the body (the digits of the person's WhatsApp\nid); replace `@` with the name."
+ }
+ }
+ }
+ ],
+ "description": "An @-mention in a message body."
+ },
+ "WhatsAppMessageKind": {
+ "type": "string",
+ "description": "What a timeline entry is.",
+ "enum": ["message", "system"]
+ },
+ "WhatsAppPersonRef": {
+ "type": "object",
+ "description": "A person named in a message: a Macro user when verified, else their\nWhatsApp name, else their phone number.",
+ "required": ["name"],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Display name, never authentication evidence."
+ },
+ "userId": {
+ "type": ["string", "null"],
+ "description": "Verified Macro user, if any."
+ }
+ }
+ },
+ "WhatsAppPoll": {
+ "type": "object",
+ "description": "A poll, read-only in Macro.",
+ "required": ["question", "options", "selectableCount", "voters"],
+ "properties": {
+ "options": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WhatsAppPollOption"
+ },
+ "description": "Options in order, with tallies."
+ },
+ "question": {
+ "type": "string",
+ "description": "The question."
+ },
+ "selectableCount": {
+ "type": "integer",
+ "format": "int32",
+ "description": "How many options one person may choose; 0 means any number.",
+ "minimum": 0
+ },
+ "voters": {
+ "type": "integer",
+ "format": "int64",
+ "description": "People who voted for anything."
+ }
+ }
+ },
+ "WhatsAppPollOption": {
+ "type": "object",
+ "description": "One option of a poll with its votes.",
+ "required": ["name", "votes", "mine", "voters"],
+ "properties": {
+ "mine": {
+ "type": "boolean",
+ "description": "The caller chose it."
+ },
+ "name": {
+ "type": "string",
+ "description": "Option text."
+ },
+ "voters": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WhatsAppPersonRef"
+ },
+ "description": "Who chose it."
+ },
+ "votes": {
+ "type": "integer",
+ "format": "int64",
+ "description": "People who chose it."
+ }
+ }
+ },
+ "WhatsAppRichContent": {
+ "oneOf": [
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/WhatsAppPoll",
+ "description": "A poll with current votes."
+ },
+ {
+ "type": "object",
+ "required": ["type"],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["poll"]
+ }
+ }
+ }
+ ],
+ "description": "A poll with current votes."
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/WhatsAppLocation",
+ "description": "A place (or the start of a live location); link to a maps URL."
+ },
+ {
+ "type": "object",
+ "required": ["type"],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["location"]
+ }
+ }
+ }
+ ],
+ "description": "A place (or the start of a live location); link to a maps URL."
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/WhatsAppContacts",
+ "description": "Contact cards."
+ },
+ {
+ "type": "object",
+ "required": ["type"],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["contacts"]
+ }
+ }
+ }
+ ],
+ "description": "Contact cards."
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/WhatsAppEvent",
+ "description": "A WhatsApp event."
+ },
+ {
+ "type": "object",
+ "required": ["type"],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["event"]
+ }
+ }
+ }
+ ],
+ "description": "A WhatsApp event."
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/WhatsAppSystemLine",
+ "description": "A system line."
+ },
+ {
+ "type": "object",
+ "required": ["type"],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["system"]
+ }
+ }
+ }
+ ],
+ "description": "A system line."
+ }
+ ],
+ "description": "Structured content of a message besides its body."
+ },
+ "WhatsAppSystemAction": {
+ "type": "string",
+ "description": "Kinds of system lines.",
+ "enum": [
+ "group_created",
+ "participants_added",
+ "participants_removed",
+ "participants_left",
+ "participants_joined",
+ "participants_promoted",
+ "participants_demoted",
+ "subject_changed",
+ "icon_changed",
+ "description_changed",
+ "disappearing_changed",
+ "message_pinned",
+ "message_unpinned",
+ "call"
+ ]
+ },
+ "WhatsAppSystemLine": {
+ "type": "object",
+ "description": "Something that happened in the group.",
+ "required": ["action", "participants"],
+ "properties": {
+ "action": {
+ "$ref": "#/components/schemas/WhatsAppSystemAction",
+ "description": "What happened."
+ },
+ "actor": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/components/schemas/WhatsAppPersonRef",
+ "description": "Who did it; absent when WhatsApp did not say."
+ }
+ ]
+ },
+ "call": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/components/schemas/WhatsAppCall",
+ "description": "For `call`: video or voice, and how it went."
+ }
+ ]
+ },
+ "participants": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WhatsAppPersonRef"
+ },
+ "description": "Who it happened to (added, removed, promoted, …)."
+ },
+ "value": {
+ "type": ["string", "null"],
+ "description": "New subject or description; disappearing-messages timer in seconds\n(`\"0\"` is off)."
+ }
+ }
+ },
"Widget": {
"type": "string",
"description": "How a question is asked. Each column kind takes a few, the first its\ndefault; kinds asked one way only (numbers, checkboxes, entity and row\npickers) take none.",
diff --git a/apps/web/tests/functional/whatsapp-linked.spec.ts b/apps/web/tests/functional/whatsapp-linked.spec.ts
index b4e70ddc32f..61011172313 100644
--- a/apps/web/tests/functional/whatsapp-linked.spec.ts
+++ b/apps/web/tests/functional/whatsapp-linked.spec.ts
@@ -101,6 +101,10 @@ function server(): LinkedServer {
const GROUP_ID = '019a061e-0000-7000-8000-000000000001';
const incomingMessage = {
id: 'message-1',
+ kind: 'message',
+ mentions: [],
+ forwarded: false,
+ rich: null,
sender: { name: 'Ana Ruiz', user_id: null },
body: 'The updated designs are ready for review.',
sent_at: '2026-10-21T10:00:00Z',
@@ -430,6 +434,11 @@ async function mount(page: Page, state: LinkedServer, query = '') {
}
return route.fulfill({ status: 404, json: {} });
}
+ // Chat's channel list feeds the composer's `@` menu; this account has none.
+ if (url.pathname === '/dss/channels/activity')
+ return route.fulfill({ json: [] });
+ if (url.pathname === '/dss/comms/channels')
+ return route.fulfill({ json: { items: [], next_cursor: null } });
if (/^\/(auth|dss|notification|connection-gateway)\//.test(url.pathname))
return route.fulfill({ json: {} });
return route.fulfill({ response: await route.fetch() });
@@ -675,6 +684,263 @@ test('an independent group reads, replies, reacts and pages through channel rows
expect(unexpected).toEqual([]);
});
+/** One of each WhatsApp message type Macro shows, oldest first. */
+function everyMessageType() {
+ const at = (minute: number) =>
+ `2026-10-21T09:${String(minute).padStart(2, '0')}:00Z`;
+ const base = {
+ ...incomingMessage,
+ body: '',
+ reactions: [],
+ attachments: [],
+ };
+ const file = (id: string, mime: string, extra: Record) => ({
+ ordinal: 0,
+ state: 'ready',
+ file_name: id,
+ mime,
+ size: 1024,
+ document_id: `document-${id}`,
+ width: null,
+ height: null,
+ duration_seconds: null,
+ presentation: null,
+ ...extra,
+ });
+ return [
+ {
+ ...base,
+ id: 'type-system',
+ kind: 'system',
+ sent_at: at(1),
+ rich: {
+ type: 'system',
+ action: 'participants_added',
+ actor: { name: 'Ana Ruiz', userId: null },
+ participants: [{ name: '+1 555 0100', userId: null }],
+ value: null,
+ call: null,
+ },
+ },
+ {
+ ...base,
+ id: 'type-mention',
+ sent_at: at(2),
+ forwarded: true,
+ body: 'Ping @15550001111 about https://example.com',
+ mentions: [{ token: '15550001111', name: 'Ben Ode', userId: null }],
+ },
+ {
+ ...base,
+ id: 'type-poll',
+ sent_at: at(3),
+ rich: {
+ type: 'poll',
+ question: 'Lunch?',
+ options: [
+ { name: 'Tacos', votes: 2, mine: true, voters: [] },
+ { name: 'Pho', votes: 0, mine: false, voters: [] },
+ ],
+ selectableCount: 1,
+ voters: 2,
+ },
+ },
+ {
+ ...base,
+ id: 'type-location',
+ sent_at: at(4),
+ rich: {
+ type: 'location',
+ latitude: 40.7128,
+ longitude: -74.006,
+ name: 'City Hall',
+ address: null,
+ url: null,
+ live: false,
+ comment: null,
+ },
+ },
+ {
+ ...base,
+ id: 'type-contacts',
+ sent_at: at(5),
+ rich: {
+ type: 'contacts',
+ contacts: [{ name: 'Dee Vee', phones: ['+1 555 0200'] }],
+ },
+ },
+ {
+ ...base,
+ id: 'type-event',
+ sent_at: at(6),
+ rich: {
+ type: 'event',
+ name: 'Launch party',
+ description: null,
+ startAt: '2026-10-24T22:00:00Z',
+ endAt: null,
+ location: 'Rooftop',
+ joinLink: null,
+ canceled: false,
+ },
+ },
+ {
+ ...base,
+ id: 'type-sticker',
+ sent_at: at(7),
+ attachments: [
+ file('sticker.webp', 'image/webp', {
+ presentation: 'sticker',
+ width: 512,
+ height: 512,
+ }),
+ ],
+ },
+ {
+ ...base,
+ id: 'type-gif',
+ sent_at: at(8),
+ attachments: [
+ file('gif.mp4', 'video/mp4', {
+ presentation: 'gif',
+ width: 320,
+ height: 240,
+ }),
+ ],
+ },
+ {
+ ...base,
+ id: 'type-voice',
+ sent_at: at(9),
+ attachments: [
+ file('voice.ogg', 'audio/ogg', {
+ presentation: 'voice_note',
+ duration_seconds: 42,
+ }),
+ ],
+ },
+ {
+ ...base,
+ id: 'type-photo',
+ sent_at: at(10),
+ attachments: [
+ file('photo.jpg', 'image/jpeg', { width: 800, height: 400 }),
+ ],
+ },
+ {
+ ...base,
+ id: 'type-call',
+ kind: 'system',
+ sent_at: at(11),
+ rich: {
+ type: 'system',
+ action: 'call',
+ actor: null,
+ participants: [],
+ value: null,
+ call: { video: true, outcome: 'missed', durationSeconds: null },
+ },
+ },
+ {
+ ...base,
+ id: 'type-view-once',
+ sent_at: at(12),
+ body: 'View-once message — open it on your phone',
+ },
+ ];
+}
+
+test('every WhatsApp message type renders with Chat parts and small cards', async ({
+ page,
+}) => {
+ const state = server();
+ state.imported = everyMessageType();
+ const unexpected = await mount(page, state, `?group=${GROUP_ID}`);
+ const timeline = page.getByRole('log', { name: 'WhatsApp messages' });
+ await expect(
+ timeline.locator('[data-system-activity="type-system"]')
+ ).toContainText('Ana Ruiz added +1 555 0100');
+ await expect(
+ timeline.locator('[data-system-activity="type-call"]')
+ ).toContainText('Missed group video call');
+ // System lines are rows of their own, never messages with actions.
+ await expect(timeline.locator('[data-message-id="type-system"]')).toHaveCount(
+ 0
+ );
+ const mention = timeline.locator('[data-message-id="type-mention"]');
+ await expect(mention.getByText('Forwarded')).toBeVisible();
+ await expect(mention.getByText('@Ben Ode')).toBeVisible();
+ await expect(mention.getByText('@15550001111')).toHaveCount(0);
+ await expect(
+ mention.getByRole('link', { name: 'https://example.com' })
+ ).toHaveAttribute('href', 'https://example.com');
+ const poll = timeline.locator('[data-whatsapp-card="poll"]');
+ await expect(poll).toContainText('Lunch?');
+ await expect(poll).toContainText('2 votes from 2 people');
+ await expect(poll.getByRole('button')).toHaveCount(0);
+ await expect(
+ timeline.getByRole('link', { name: 'Open in Maps' })
+ ).toHaveAttribute(
+ 'href',
+ 'https://www.google.com/maps/search/?api=1&query=40.7128%2C-74.006'
+ );
+ await expect(
+ timeline.locator('[data-whatsapp-card="contacts"]')
+ ).toContainText('Dee Vee');
+ await expect(timeline.locator('[data-whatsapp-card="event"]')).toContainText(
+ 'Launch party'
+ );
+ await expect(timeline.getByText('Voice note · 0:42')).toBeVisible();
+ // Media reserves its declared shape while it loads.
+ const reserved = await timeline
+ .locator('[role="status"][aria-label^="Loading "]')
+ .evaluateAll((boxes) =>
+ boxes.map((box) => [
+ box.getAttribute('aria-label'),
+ Math.round(box.getBoundingClientRect().width),
+ Math.round(box.getBoundingClientRect().height),
+ ])
+ );
+ expect(reserved).toEqual(
+ expect.arrayContaining([
+ ['Loading sticker.webp', 144, 144],
+ ['Loading gif.mp4', 320, 240],
+ ['Loading photo.jpg', 400, 200],
+ ])
+ );
+ await expect(
+ timeline.getByText('View-once message — open it on your phone')
+ ).toBeVisible();
+ // The newest row is in view: the timeline opened at the bottom.
+ await expect(
+ timeline.locator('[data-message-id="message-2"]')
+ ).toBeInViewport();
+ expect(unexpected).toEqual([]);
+});
+
+test('the composer mentions group participants as WhatsApp mentions', async ({
+ page,
+}) => {
+ const state = server();
+ const unexpected = await mount(page, state, `?group=${GROUP_ID}`);
+ await typeInComposer(page, 'Thanks @Ana');
+ // Only the group's own people: no agents, Macro items or @here.
+ await expect(page.getByText('+14155550199')).toBeVisible();
+ await expect(page.getByText('@here')).toHaveCount(0);
+ await page.keyboard.press('Enter');
+ await page.keyboard.type('!');
+ await page.getByRole('button', { name: 'Send message' }).click();
+ await expect.poll(() => state.outgoing.length).toBe(1);
+ expect(state.outgoing[0]).toMatchObject({
+ message: {
+ type: 'text',
+ text: expect.stringMatching(/^Thanks @14155550199 ?!$/),
+ mentions: ['14155550199@s.whatsapp.net'],
+ },
+ });
+ expect(unexpected).toEqual([]);
+});
+
test('history imported after adding shows up older, with the import state', async ({
page,
}) => {
diff --git a/apps/web/tests/functional/whatsapp-linked/fixture.tsx b/apps/web/tests/functional/whatsapp-linked/fixture.tsx
index e62c34106e3..87063f9c7d3 100644
--- a/apps/web/tests/functional/whatsapp-linked/fixture.tsx
+++ b/apps/web/tests/functional/whatsapp-linked/fixture.tsx
@@ -27,6 +27,9 @@ import { AnalyticsContextProvider } from '@app/lib/analytics/analytics-context';
import { PosthogProvider } from '@app/lib/analytics/posthog';
import { FloatRegionHost } from '@components/app/mobile/float-regions/FloatRegionHost';
import { ToastRegion } from '@core/component/Toast/ToastRegion';
+import { ChannelsContextProvider } from '@core/context/channels';
+import { QuickAccessContextProvider } from '@core/context/quickAccess/context';
+import type { QuickAccessContextValue } from '@core/context/quickAccess/types';
import { UserContextProvider } from '@core/context/user';
import { isTouchDevice } from '@core/mobile/isTouchDevice';
import { queryClient } from '@queries/client';
@@ -56,6 +59,23 @@ const nativePasskey: PasskeyCapability = {
},
};
+const noQuickAccess: QuickAccessContextValue = {
+ useList: () => ({
+ error: () => undefined,
+ items: () => [],
+ totalCount: () => 0,
+ hasMore: () => false,
+ isLoading: () => false,
+ isLoadingMore: () => false,
+ loadMore: async () => {},
+ }),
+ usesRecordSelection: () => false,
+ usesSearchProjection: () => false,
+ isLoading: () => false,
+ refresh: () => {},
+ getById: () => undefined,
+};
+
function GroupHarness(props: { groupId: string }) {
const userId = useUserId();
const source = createWhatsAppGroupCapabilities(
@@ -164,16 +184,22 @@ render(
Loading identity…}>
- (
- <>
- {props.children}
-
- >
- )}
- >
-
-
+ {/* The app root provides Macro's items; the composer's `@` menu
+ reads them. A WhatsApp group offers only its people. */}
+
+
+ (
+ <>
+ {props.children}
+
+ >
+ )}
+ >
+
+
+
+
diff --git a/crates/agent_code_mode/src/domain/documentation.json b/crates/agent_code_mode/src/domain/documentation.json
index 7be9d807a4c..6731c47ba4f 100644
--- a/crates/agent_code_mode/src/domain/documentation.json
+++ b/crates/agent_code_mode/src/domain/documentation.json
@@ -81,7 +81,7 @@
"name": "channels",
"description": "Read and send channel messages; read and share standalone WhatsApp groups, discover remote groups, and send from the credential owner’s own member account with media and delivery. Pairing stays in the app.",
"overview": "\n# Class: ChannelNamespace\n\n## Constructors\n\n### Constructor\n\n> **new ChannelNamespace**(`client`): `ChannelNamespace`\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n\n#### Returns\n\n`ChannelNamespace`\n\n## Properties\n\n| Property | Modifier | Type | Description |\n| ------ | ------ | ------ | ------ |\n| `labels` | `readonly` | [`ChannelLabelsNamespace`](../../labels/classes/ChannelLabelsNamespace.md) | Team-shared labels that group channels in the Chat sidebar. |\n\n## Methods\n\n### activity()\n\n> **activity**(): `Promise`\\<[`ChannelActivity`](../../activity/classes/ChannelActivity.md)[]\\>\n\nRecent channel activity, with channel and user handles.\n\n#### Returns\n\n`Promise`\\<[`ChannelActivity`](../../activity/classes/ChannelActivity.md)[]\\>\n\n***\n\n### byId()\n\n> **byId**(`id`): [`Channel`](../../channel/classes/Channel.md)\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `id` | `string` |\n\n#### Returns\n\n[`Channel`](../../channel/classes/Channel.md)\n\n***\n\n### create()\n\n> **create**(`opts`): `Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\nCreate a channel. The caller becomes the owner.\n\n#### Parameters\n\n| Parameter | Type | Description |\n| ------ | ------ | ------ |\n| `opts` | \\{ `name?`: `string`; `participants?`: [`User`](../../../users/user/classes/User.md)[]; `team?`: [`Team`](../../../teams/team/classes/Team.md); `type`: [`ChannelType`](../../../../../generated/storage/types.gen/type-aliases/ChannelType.md); \\} | - |\n| `opts.name?` | `string` | - |\n| `opts.participants?` | [`User`](../../../users/user/classes/User.md)[] | Participants to add, excluding the owner. |\n| `opts.team?` | [`Team`](../../../teams/team/classes/Team.md) | Team, for team channels. |\n| `opts.type` | [`ChannelType`](../../../../../generated/storage/types.gen/type-aliases/ChannelType.md) | - |\n\n#### Returns\n\n`Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\n***\n\n### dm()\n\n> **dm**(`recipient`): `Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `recipient` | [`User`](../../../users/user/classes/User.md) |\n\n#### Returns\n\n`Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\n***\n\n### list()\n\n> **list**(): `Promise`\\<[`Channel`](../../channel/classes/Channel.md)[]\\>\n\nAll channels the authenticated user is a member of.\n\n#### Returns\n\n`Promise`\\<[`Channel`](../../channel/classes/Channel.md)[]\\>\n\n***\n\n### private()\n\n> **private**(`recipients`): `Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\nOpen (creating if needed) the private group channel with a set of users.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `recipients` | [`User`](../../../users/user/classes/User.md)[] |\n\n#### Returns\n\n`Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\n***\n\n### search()\n\n> **search**(`query`, `opts?`): `AsyncGenerator`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\nSearch channels by name (client-side fuzzy filter on the full channel list)\nor by content (server-side unified search).\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `query` | `string` |\n| `opts?` | [`SearchOpts`](../../../search/interfaces/SearchOpts.md) |\n\n#### Returns\n\n`AsyncGenerator`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\n\n\n# Class: WhatsAppNamespace\n\nOperate on an account already paired through the Macro app.\n\n## Constructors\n\n### Constructor\n\n> **new WhatsAppNamespace**(`client`): `WhatsAppNamespace`\n\nBind existing-account operations to this authenticated client.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n\n#### Returns\n\n`WhatsAppNamespace`\n\n## Methods\n\n### account()\n\n> **account**(): `Promise`\\<[`WhatsAppAccount`](../../account/classes/WhatsAppAccount.md) \\| `undefined`\\>\n\nCurrent owner's account, or undefined when no account has been connected.\n\n#### Returns\n\n`Promise`\\<[`WhatsAppAccount`](../../account/classes/WhatsAppAccount.md) \\| `undefined`\\>\n\n***\n\n### byId()\n\n> **byId**(`id`): [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)\n\nReference an adopted entity without fetching it.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `id` | `string` |\n\n#### Returns\n\n[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)\n\n***\n\n### groups()\n\n> **groups**(): `Promise`\\<[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)[]\\>\n\nAdopted groups shared with the caller, including without WhatsApp.\n\n#### Returns\n\n`Promise`\\<[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)[]\\>\n",
- "documentation": "# Macro SDK: channels\n\nGenerated by TypeDoc. Paths below refer to the canonical Markdown hierarchy. Other entity handles are documented in their named section; request core for shared SDK helpers.\n\n\n# Type Alias: BulkUpdateEntityPropertyOptionsResponse\n\n> **BulkUpdateEntityPropertyOptionsResponse** = \\{ `properties`: [`EntityPropertyOptionSelectionResponse`](EntityPropertyOptionSelectionResponse.md)[]; \\}\n\nResponse for a bulk option update: each property's reconciled final ids.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `properties` | [`EntityPropertyOptionSelectionResponse`](EntityPropertyOptionSelectionResponse.md)[] | The final option ids per updated property. |\n\n\n\n# Type Alias: DataType\n\n> **DataType** = `\"BOOLEAN\"` \\| `\"DATE\"` \\| `\"NUMBER\"` \\| `\"STRING\"` \\| `\"SELECT_NUMBER\"` \\| `\"SELECT_STRING\"` \\| `\"TAG\"` \\| `\"ENTITY\"` \\| `\"LINK\"`\n\nData type for property values, determining storage and validation.\n\n\n\n# Type Alias: EntityProperty\n\n> **EntityProperty** = \\{ `created_at`: `string`; `entity_id`: `string`; `entity_type`: [`EntityType`](EntityType.md); `id`: `string`; `property_definition_id`: `string`; `updated_at`: `string`; \\}\n\nAssignment of a property definition to a specific entity (service representation).\n\n## Properties\n\n| Property | Type |\n| ------ | ------ |\n| `created_at` | `string` |\n| `entity_id` | `string` |\n| `entity_type` | [`EntityType`](EntityType.md) |\n| `id` | `string` |\n| `property_definition_id` | `string` |\n| `updated_at` | `string` |\n\n\n\n# Type Alias: EntityPropertyOptionSelectionResponse\n\n> **EntityPropertyOptionSelectionResponse** = \\{ `option_ids`: `string`[]; `property_id`: `string`; \\}\n\nThe reconciled final option ids stored for one property after a bulk update.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `option_ids` | `string`[] | The final option ids the server persisted, in stored order. |\n| `property_id` | `string` | The property definition the options belong to. |\n\n\n\n# Type Alias: EntityPropertyWithDefinition\n\n> **EntityPropertyWithDefinition** = \\{ `definition`: [`PropertyDefinition`](PropertyDefinition.md); `options?`: [`PropertyOption`](PropertyOption.md)[] \\| `null`; `property`: [`EntityProperty`](EntityProperty.md); `value?`: `null` \\| [`PropertyValue`](PropertyValue.md); \\}\n\nEntity property with its definition, value, and options (service representation).\n\n## Properties\n\n| Property | Type |\n| ------ | ------ |\n| `definition` | [`PropertyDefinition`](PropertyDefinition.md) |\n| `options?` | [`PropertyOption`](PropertyOption.md)[] \\| `null` |\n| `property` | [`EntityProperty`](EntityProperty.md) |\n| `value?` | `null` \\| [`PropertyValue`](PropertyValue.md) |\n\n\n\n# Type Alias: EntityReference\n\n> **EntityReference** = \\{ `entity_id`: `string`; `entity_type`: [`EntityType`](EntityType.md); `specific_message_id?`: `string` \\| `null`; \\}\n\nEntity reference for entity-type property values.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `entity_id` | `string` | - |\n| `entity_type` | [`EntityType`](EntityType.md) | - |\n| `specific_message_id?` | `string` \\| `null` | For CHANNEL, CHAT, THREAD entity types - optional specific message ID. This allows referencing a specific message within a thread/channel/chat. |\n\n\n\n# Type Alias: EntityType\n\n> **EntityType** = `\"CALENDAR_EVENT\"` \\| `\"CALL_RECORD\"` \\| `\"CHANNEL\"` \\| `\"CHAT\"` \\| `\"COMPANY\"` \\| `\"DATABASE_ROW\"` \\| `\"CONTACT\"` \\| `\"DOCUMENT\"` \\| `\"INITIATIVE\"` \\| `\"PROJECT\"` \\| `\"TASK\"` \\| `\"THREAD\"` \\| `\"USER\"`\n\nType of entity that can be referenced by entity properties.\n\n\n\n# Type Alias: PropertyDefinition\n\n> **PropertyDefinition** = \\{ `created_at`: `string`; `data_type`: [`DataType`](DataType.md); `display_name`: `string`; `id`: `string`; `is_metadata`: `boolean`; `is_multi_select`: `boolean`; `is_system`: `boolean`; `owner`: [`PropertyOwner`](PropertyOwner.md); `specific_entity_type`: `null` \\| [`EntityType`](EntityType.md); `updated_at`: `string`; \\}\n\nProperty definition model (service representation).\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `created_at` | `string` | - |\n| `data_type` | [`DataType`](DataType.md) | - |\n| `display_name` | `string` | - |\n| `id` | `string` | - |\n| `is_metadata` | `boolean` | Flag to indicate if this is a system-generated metadata property. Not stored in database - computed at service layer. |\n| `is_multi_select` | `boolean` | - |\n| `is_system` | `boolean` | Flag to indicate if this is a system property (stored in DB). |\n| `owner` | [`PropertyOwner`](PropertyOwner.md) | - |\n| `specific_entity_type` | `null` \\| [`EntityType`](EntityType.md) | - |\n| `updated_at` | `string` | - |\n\n\n\n# Type Alias: PropertyOption\n\n> **PropertyOption** = \\{ `color`: `string` \\| `null`; `created_at`: `string`; `display_order`: `number`; `id`: `string`; `property_definition_id`: `string`; `updated_at`: `string`; `value`: [`PropertyOptionValue`](PropertyOptionValue.md); \\}\n\nA selectable option for select-type properties (service representation).\n\n## Properties\n\n| Property | Type |\n| ------ | ------ |\n| `color` | `string` \\| `null` |\n| `created_at` | `string` |\n| `display_order` | `number` |\n| `id` | `string` |\n| `property_definition_id` | `string` |\n| `updated_at` | `string` |\n| `value` | [`PropertyOptionValue`](PropertyOptionValue.md) |\n\n\n\n# Type Alias: PropertyOptionValue\n\n> **PropertyOptionValue** = \\{ `type`: `\"string\"`; `value`: `string`; \\} \\| \\{ `type`: `\"number\"`; `value`: `number`; \\}\n\nThe value of a property option - either a string or a number.\n\n## Union Members\n\n### Type Literal\n\n\\{ `type`: `\"string\"`; `value`: `string`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"string\"` | - |\n| `value` | `string` | String value for SelectString properties |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"number\"`; `value`: `number`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"number\"` | - |\n| `value` | `number` | Number value for SelectNumber properties |\n\n\n\n# Type Alias: PropertyOwner\n\n> **PropertyOwner** = \\{ `scope`: `\"user\"`; `user_id`: `string`; \\} \\| \\{ `scope`: `\"team\"`; `team_id`: `string`; \\} \\| \\{ `database_id`: `string`; `scope`: `\"database\"`; \\} \\| \\{ `scope`: `\"system\"`; \\}\n\nDefines who owns a property - user-scoped, team-scoped, database-scoped, or system.\n\n\n\n# Type Alias: PropertyTargetEntityType\n\n> **PropertyTargetEntityType** = `\"CALL_RECORD\"` \\| `\"CHANNEL\"` \\| `\"CHAT\"` \\| `\"COMPANY\"` \\| `\"DATABASE_ROW\"` \\| `\"DOCUMENT\"` \\| `\"INITIATIVE\"` \\| `\"PROJECT\"` \\| `\"THREAD\"` \\| `\"USER\"`\n\nCanonical type of an entity receiving properties.\n\nTasks are documents at API boundaries. `Task` intentionally does not exist\nhere; task classification is resolved by the properties domain from the\ndocument subtype.\n\n\n\n# Type Alias: PropertyTargetReference\n\n> **PropertyTargetReference** = \\{ `entity_id`: `string`; `entity_type`: [`PropertyTargetEntityType`](PropertyTargetEntityType.md); \\}\n\nCanonical reference to an entity receiving properties.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `entity_id` | `string` | Entity identifier. |\n| `entity_type` | [`PropertyTargetEntityType`](PropertyTargetEntityType.md) | Canonical entity type. |\n\n\n\n# Type Alias: PropertyValue\n\n> **PropertyValue** = \\{ `type`: `\"Boolean\"`; `value`: `boolean`; \\} \\| \\{ `type`: `\"Number\"`; `value`: `number`; \\} \\| \\{ `type`: `\"String\"`; `value`: `string`; \\} \\| \\{ `type`: `\"Date\"`; `value`: `string`; \\} \\| \\{ `type`: `\"SelectOption\"`; `value`: `string`[]; \\} \\| \\{ `type`: `\"EntityReference\"`; `value`: [`EntityReference`](EntityReference.md)[]; \\} \\| \\{ `type`: `\"Link\"`; `value`: `string`[]; \\}\n\nProperty value (service representation).\n\nRepresents the actual value stored for an entity property.\nThis is serialized to/from JSONB in the database.\n\n## Union Members\n\n### Type Literal\n\n\\{ `type`: `\"Boolean\"`; `value`: `boolean`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"Boolean\"` | - |\n| `value` | `boolean` | Boolean value Serializes as: {\"type\": \"Boolean\", \"value\": true} |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"Number\"`; `value`: `number`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"Number\"` | - |\n| `value` | `number` | Numeric value Serializes as: {\"type\": \"Number\", \"value\": 42.5} |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"String\"`; `value`: `string`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"String\"` | - |\n| `value` | `string` | String value Serializes as: {\"type\": \"String\", \"value\": \"text\"} |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"Date\"`; `value`: `string`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"Date\"` | - |\n| `value` | `string` | Date/timestamp value Serializes as: {\"type\": \"Date\", \"value\": \"2025-01-01T00:00:00Z\"} |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"SelectOption\"`; `value`: `string`[]; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"SelectOption\"` | - |\n| `value` | `string`[] | Select option(s) - always an array (check is_multi_select to determine if single or multi) Single-select: {\"type\": \"SelectOption\", \"value\": [\"uuid\"]} (length 0 or 1) Multi-select: {\"type\": \"SelectOption\", \"value\": [\"uuid1\", \"uuid2\", ...]} (length 0+) |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"EntityReference\"`; `value`: [`EntityReference`](EntityReference.md)[]; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"EntityReference\"` | - |\n| `value` | [`EntityReference`](EntityReference.md)[] | Entity reference(s) - always an array (check is_multi_select to determine if single or multi) Single-select: {\"type\": \"EntityReference\", \"value\": [{...}]} (length 0 or 1) Multi-select: {\"type\": \"EntityReference\", \"value\": [{...}, {...}, ...]} (length 0+) |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"Link\"`; `value`: `string`[]; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"Link\"` | - |\n| `value` | `string`[] | Link value(s) - always an array (check is_multi_select to determine if single or multi) Single-select: {\"type\": \"Link\", \"value\": [\"https://example.com\"]} (length 0 or 1) Multi-select: {\"type\": \"Link\", \"value\": [\"https://example.com\", \"https://other.com\"]} (length 0+) |\n\n\n\n# Type Alias: SetPropertyValue\n\n> **SetPropertyValue** = \\{ `type`: `\"boolean\"`; `value`: `boolean`; \\} \\| \\{ `type`: `\"date\"`; `value`: `string`; \\} \\| \\{ `type`: `\"number\"`; `value`: `number`; \\} \\| \\{ `type`: `\"string\"`; `value`: `string`; \\} \\| \\{ `option_id`: `string`; `type`: `\"select_option\"`; \\} \\| \\{ `option_ids`: `string`[]; `type`: `\"multi_select_option\"`; \\} \\| \\{ `reference`: [`EntityReference`](EntityReference.md); `type`: `\"entity_reference\"`; \\} \\| \\{ `references`: [`EntityReference`](EntityReference.md)[]; `type`: `\"multi_entity_reference\"`; \\} \\| \\{ `type`: `\"link\"`; `url`: `string`; \\} \\| \\{ `type`: `\"multi_link\"`; `urls`: `string`[]; \\}\n\nType-safe enum for setting entity property values - provides compile-time validation.\n\n\n\n# Type Alias: AccessLevel\n\n> **AccessLevel** = `\"view\"` \\| `\"comment\"` \\| `\"edit\"` \\| `\"owner\"`\n\nOrdered from least to most access top -> bottom\n\n\n\n# Type Alias: ActivityType\n\n> **ActivityType** = `\"view\"` \\| `\"interact\"`\n\nThe kind of activity a user performs in a channel.\n\n\n\n# Type Alias: ApiActivity\n\n> **ApiActivity** = \\{ `channel_id`: `string`; `created_at`: `string`; `id`: `string`; `interacted_at?`: `string` \\| `null`; `updated_at`: `string`; `user_id`: `string`; `viewed_at?`: `string` \\| `null`; \\}\n\nA user's activity (view/interaction) within a channel.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channel_id` | `string` | Channel id. |\n| `created_at` | `string` | When the activity row was created. |\n| `id` | `string` | Activity id. |\n| `interacted_at?` | `string` \\| `null` | The last time the user interacted with the channel. |\n| `updated_at` | `string` | When the activity row was last updated. |\n| `user_id` | `string` | User id. |\n| `viewed_at?` | `string` \\| `null` | The last time the user viewed the channel. |\n\n\n\n# Type Alias: ApiChannelAttachment\n\n> **ApiChannelAttachment** = \\{ `channel_id`: `string`; `created_at`: `string`; `entity_id`: `string`; `entity_type`: `string`; `height?`: `number` \\| `null`; `id`: `string`; `message_id`: `string`; `sender_id`: `string`; `width?`: `number` \\| `null`; \\}\n\nA channel-level attachment.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channel_id` | `string` | Channel id. |\n| `created_at` | `string` | When the attachment was created. |\n| `entity_id` | `string` | Entity id. |\n| `entity_type` | `string` | Type of entity. |\n| `height?` | `number` \\| `null` | Height (for images). |\n| `id` | `string` | Attachment id. |\n| `message_id` | `string` | Message id this attachment belongs to. |\n| `sender_id` | `string` | The user who sent the message containing this attachment. |\n| `width?` | `number` \\| `null` | Width (for images). |\n\n\n\n# Type Alias: ApiChannelAttachmentsPage\n\n> **ApiChannelAttachmentsPage** = \\{ `items`: [`ApiChannelAttachment`](ApiChannelAttachment.md)[]; `next_cursor?`: `string` \\| `null`; \\}\n\nPaginated response of channel attachments.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `items` | [`ApiChannelAttachment`](ApiChannelAttachment.md)[] | Attachments on this page. |\n| `next_cursor?` | `string` \\| `null` | Cursor for the next page, null if no more pages. |\n\n\n\n# Type Alias: ApiChannelDetail\n\n> **ApiChannelDetail** = \\{ `channel_id`: `string`; `channel_name`: `string`; `channel_type`: [`ChannelType`](ChannelType.md); `participants`: [`ApiChannelParticipant`](ApiChannelParticipant.md)[]; \\}\n\nChannel detail: metadata and active participants.\n\nMessages are read through `/messages/channel/{channel_id}`.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channel_id` | `string` | Channel id. |\n| `channel_name` | `string` | Resolved display name from the viewer's perspective. |\n| `channel_type` | [`ChannelType`](ChannelType.md) | Channel type. |\n| `participants` | [`ApiChannelParticipant`](ApiChannelParticipant.md)[] | Active participants. |\n\n\n\n# Type Alias: ApiChannelParticipant\n\n> **ApiChannelParticipant** = \\{ `channel_id`: `string`; `joined_at`: `string`; `role`: [`ApiParticipantRole`](ApiParticipantRole.md); `user_id`: `string`; \\}\n\nA channel participant.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channel_id` | `string` | Channel id. |\n| `joined_at` | `string` | When the user joined. |\n| `role` | [`ApiParticipantRole`](ApiParticipantRole.md) | Role in the channel. |\n| `user_id` | `string` | User id. |\n\n\n\n# Type Alias: ApiParticipantRole\n\n> **ApiParticipantRole** = `\"owner\"` \\| `\"admin\"` \\| `\"member\"`\n\nParticipant role in a channel.\n\n\n\n# Type Alias: BotSenderProfile\n\n> **BotSenderProfile** = \\{ `avatar_url?`: `string` \\| `null`; `name`: `string`; \\}\n\nPublic bot profile attached to bot-authored messages.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `avatar_url?` | `string` \\| `null` | Bot avatar URL. |\n| `name` | `string` | Bot display name. |\n\n\n\n# Type Alias: ChannelLabel\n\n> **ChannelLabel** = \\{ `channelCount`: `number`; `channelIds`: `string`[]; `createdAt`: `string`; `id`: `string`; `name`: `string`; `rule?`: `null` \\| [`ChannelLabelRule`](ChannelLabelRule.md); `sortOrder`: `number`; `teamId?`: `string` \\| `null`; `updatedAt`: `string`; \\}\n\nA shared or account-private label grouping chat channels in the sidebar.\n\n`channel_ids` is viewer-relative: it lists only the labelled channels the\nrequesting user participates in. `channel_count` counts every channel in\nthe label so clients can warn accurately before a delete.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channelCount` | `number` | All assignments for a manual label; visible matches for a smart tag. |\n| `channelIds` | `string`[] | Channels in this label that the requesting user participates in. |\n| `createdAt` | `string` | When the label was created. |\n| `id` | `string` | Stable label id. |\n| `name` | `string` | Display name, unique within the scope (case-insensitive). |\n| `rule?` | `null` \\| [`ChannelLabelRule`](ChannelLabelRule.md) | - |\n| `sortOrder` | `number` | Manual ordering value within the scope; lower sorts first. |\n| `teamId?` | `string` \\| `null` | Owning team, or `None` for account-private labels. |\n| `updatedAt` | `string` | When the label was last renamed or reordered. |\n\n\n\n# Type Alias: ChannelLabelRule\n\n> **ChannelLabelRule** = \\{ `attribute`: `\"name\"`; `contains`: `string`; \\}\n\nCase-insensitive, literal substring matching on the channel name.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `attribute` | `\"name\"` | - |\n| `contains` | `string` | The substring to find anywhere in the name. |\n\n\n\n# Type Alias: ChannelSharePermission\n\n> **ChannelSharePermission** = \\{ `access_level`: [`AccessLevel`](AccessLevel.md); `channel_id`: `string`; \\}\n\nThe channel share permission\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `access_level` | [`AccessLevel`](AccessLevel.md) | The access level for the channel |\n| `channel_id` | `string` | The channel id |\n\n\n\n# Type Alias: ChannelType\n\n> **ChannelType** = `\"public\"` \\| `\"private\"` \\| `\"direct_message\"` \\| `\"team\"`\n\nType of channel.\n\n\n\n# Type Alias: CountedReaction\n\n> **CountedReaction** = \\{ `emoji`: `string`; `users`: `string`[]; \\}\n\nReaction emoji and the users who added it.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `emoji` | `string` | Emoji being reacted with. |\n| `users` | `string`[] | User identifiers. |\n\n\n\n# Type Alias: DocumentId\n\n> **DocumentId** = `string`\n\nA validated document identifier. Historical document ids need not be UUIDs.\n\n\n\n# Type Alias: EntityPermission\n\n> **EntityPermission** = \\{ `access_level`: [`AccessLevel`](AccessLevel.md); `type`: `\"access_level\"`; \\} \\| \\{ `type`: `\"channel_view_only\"`; \\} \\| \\{ `role`: [`ParticipantRole`](ParticipantRole.md); `type`: `\"channel_role\"`; \\} \\| \\{ `role`: [`TeamRole`](TeamRole.md); `type`: `\"team_role\"`; \\}\n\nA user's permission for an entity, discriminated by entity kind.\n\nItems (documents, chats, projects, threads) use access levels.\nChannels use view-only permission or participant roles.\n\n## Union Members\n\n### Type Literal\n\n\\{ `access_level`: [`AccessLevel`](AccessLevel.md); `type`: `\"access_level\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `access_level` | [`AccessLevel`](AccessLevel.md) | The access level the user has. |\n| `type` | `\"access_level\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"channel_view_only\"`; \\}\n\n***\n\n### Type Literal\n\n\\{ `role`: [`ParticipantRole`](ParticipantRole.md); `type`: `\"channel_role\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `role` | [`ParticipantRole`](ParticipantRole.md) | The role the user has in the channel. |\n| `type` | `\"channel_role\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `role`: [`TeamRole`](TeamRole.md); `type`: `\"team_role\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `role` | [`TeamRole`](TeamRole.md) | The role the user has in the team. |\n| `type` | `\"team_role\"` | - |\n\n\n\n# Type Alias: GetChannelAttachmentsResponses\n\n> **GetChannelAttachmentsResponses** = \\{ `200`: [`ApiChannelAttachmentsPage`](ApiChannelAttachmentsPage.md); \\}\n\n## Properties\n\n| Property | Type |\n| ------ | ------ |\n| `200` | [`ApiChannelAttachmentsPage`](ApiChannelAttachmentsPage.md) |\n\n\n\n# Type Alias: GetChannelParticipantsResponses\n\n> **GetChannelParticipantsResponses** = \\{ `200`: [`ApiChannelParticipant`](ApiChannelParticipant.md)[]; \\}\n\n## Properties\n\n| Property | Type |\n| ------ | ------ |\n| `200` | [`ApiChannelParticipant`](ApiChannelParticipant.md)[] |\n\n\n\n# Type Alias: ImportedAuthor\n\n> **ImportedAuthor** = \\{ `name`: `string`; \\}\n\nDisplay attribution for a comment imported from an external document.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `name` | `string` | Original author text; never interpreted as an authenticated principal. |\n\n\n\n# Type Alias: LinkShare\n\n> **LinkShare** = `\"PUBLIC\"` \\| `\"TEAM\"`\n\nDefines who can access an item through its share link.\n\n\n\n# Type Alias: LinkedAccount\n\n> **LinkedAccount** = \\{ `authorization_generation`: `number`; `id`: `string`; `identity_jid?`: `string` \\| `null`; `state`: [`LinkedAccountState`](LinkedAccountState.md); `user_id`: `string`; \\}\n\nOne person's account, never owned by a workspace.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `authorization_generation` | `number` | Explicit consent generation; reconnecting a socket never changes it. |\n| `id` | `string` | Stable account identity. |\n| `identity_jid?` | `string` \\| `null` | WhatsApp-verified identity, absent until the first pairing succeeds. |\n| `state` | [`LinkedAccountState`](LinkedAccountState.md) | Lifecycle. |\n| `user_id` | `string` | Authenticated owner. |\n\n\n\n# Type Alias: LinkedAccountState\n\n> **LinkedAccountState** = `\"pairing\"` \\| `\"connected\"` \\| `\"logged_out\"` \\| `\"disconnected\"`\n\nAccount lifecycle. Transport outages are not stored: a connected account\nstays connected while its session service reconnects.\n\n\n\n# Type Alias: LinkedAttachment\n\n> **LinkedAttachment** = \\{ `document_id?`: `string` \\| `null`; `file_name`: `string`; `mime`: `string`; `ordinal`: `number`; `size`: `number`; `state`: [`LinkedAttachmentState`](LinkedAttachmentState.md); \\}\n\nOne attachment of a linked message.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `document_id?` | `string` \\| `null` | The private document, once ready. |\n| `file_name` | `string` | Display filename. |\n| `mime` | `string` | Declared MIME type. |\n| `ordinal` | `number` | Position within the message. |\n| `size` | `number` | Declared size in bytes. |\n| `state` | [`LinkedAttachmentState`](LinkedAttachmentState.md) | Current state. |\n\n\n\n# Type Alias: LinkedAttachmentState\n\n> **LinkedAttachmentState** = `\"loading\"` \\| `\"ready\"` \\| `\"unavailable\"` \\| `\"unsupported\"`\n\nWhere an attachment of a linked message stands.\n\n\n\n# Type Alias: LinkedBackfill\n\n> **LinkedBackfill** = \\{ `imported`: `number`; `state`: [`LinkedBackfillState`](LinkedBackfillState.md); \\}\n\nEarlier messages imported after adding a group.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `imported` | `number` | Messages received from the phone so far (including ones already shown). |\n| `state` | [`LinkedBackfillState`](LinkedBackfillState.md) | Progress. |\n\n\n\n# Type Alias: LinkedBackfillState\n\n> **LinkedBackfillState** = `\"pending\"` \\| `\"running\"` \\| `\"waiting_for_phone\"` \\| `\"done\"` \\| `\"partial\"`\n\nWhere importing a group's earlier messages stands.\n\n\n\n# Type Alias: LinkedCapabilities\n\n> **LinkedCapabilities** = \\{ `attachment_limit_bytes`: `number`; `attachment_types`: `string`[]; `max_attachments`: `number`; `mutation_window_seconds`: `number`; `mutations`: [`LinkedMutation`](LinkedMutation.md)[]; `text_limit_bytes`: `number`; \\}\n\nOperations implemented for linked groups, independent of current permission.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `attachment_limit_bytes` | `number` | Largest attachment in bytes. |\n| `attachment_types` | `string`[] | Supported attachment MIME types. |\n| `max_attachments` | `number` | Attachments per send. |\n| `mutation_window_seconds` | `number` | Edits and deletes are offered this long after sending. |\n| `mutations` | [`LinkedMutation`](LinkedMutation.md)[] | Supported message operations. |\n| `text_limit_bytes` | `number` | Maximum UTF-8 bytes per text send. |\n\n\n\n# Type Alias: LinkedChat\n\n> **LinkedChat** = \\{ `archived`: `boolean`; `canAdd`: `boolean`; `cannotAddReason?`: `string` \\| `null`; `group?`: `null` \\| [`LinkedGroupReference`](LinkedGroupReference.md); `jid`: `string`; `kind`: [`LinkedChatKind`](LinkedChatKind.md); `lastActivityAt?`: `string` \\| `null`; `name`: `string`; `pinned`: `boolean`; `shared?`: `null` \\| [`LinkedSharedGroupReference`](LinkedSharedGroupReference.md); \\}\n\nOne chat of the caller's WhatsApp chat list, as WhatsApp Web lists it.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `archived` | `boolean` | Archived in WhatsApp. |\n| `canAdd` | `boolean` | \"Add to Macro\" works now. |\n| `cannotAddReason?` | `string` \\| `null` | Why it can't be added, when it is neither added, shared nor addable. |\n| `group?` | `null` \\| [`LinkedGroupReference`](LinkedGroupReference.md) | - |\n| `jid` | `string` | WhatsApp chat JID: a group, or the other person's phone-number JID (LID when WhatsApp never told us the number). |\n| `kind` | [`LinkedChatKind`](LinkedChatKind.md) | Group or direct chat. |\n| `lastActivityAt?` | `string` \\| `null` | Newest activity WhatsApp reported (message or history sync); `null` when unknown. The list is sorted by it, newest first, unknown last. |\n| `name` | `string` | Group: the subject, or member names for an unnamed group. Direct: the contact or WhatsApp name, else `+`. Always non-empty. |\n| `pinned` | `boolean` | Pinned in WhatsApp. |\n| `shared?` | `null` \\| [`LinkedSharedGroupReference`](LinkedSharedGroupReference.md) | - |\n\n\n\n# Type Alias: LinkedChatKind\n\n> **LinkedChatKind** = `\"group\"` \\| `\"direct\"`\n\nWhether a WhatsApp chat is a group or a one-to-one chat.\n\n\n\n# Type Alias: LinkedDeliveryState\n\n> **LinkedDeliveryState** = `\"pending\"` \\| `\"sending\"` \\| `\"unknown\"` \\| `\"accepted\"` \\| `\"sent\"` \\| `\"delivered\"` \\| `\"read\"` \\| `\"failed\"`\n\nObserved send state. `unknown` means the request may have reached WhatsApp;\nit is never sent again automatically.\n\n\n\n# Type Alias: LinkedGroupInfo\n\n> **LinkedGroupInfo** = \\{ `announce`: `boolean`; `community`: `boolean`; `disappearing`: `boolean`; `group?`: `null` \\| [`LinkedGroupReference`](LinkedGroupReference.md); `jid`: `string`; `name`: `string`; `observedAt`: `string`; `participants`: [`LinkedParticipant`](LinkedParticipant.md)[]; `shared?`: `null` \\| [`LinkedSharedGroupReference`](LinkedSharedGroupReference.md); \\}\n\nOne full metadata read of a group, never an incomplete membership delta.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `announce` | `boolean` | Only admins may post. |\n| `community` | `boolean` | Community announcement or parent group, outside the supported surface. |\n| `disappearing` | `boolean` | Disappearing messages are on; content is not retained. |\n| `group?` | `null` \\| [`LinkedGroupReference`](LinkedGroupReference.md) | - |\n| `jid` | `string` | Remote group JID. |\n| `name` | `string` | Current subject. |\n| `observedAt` | `string` | When the session service read this metadata. |\n| `participants` | [`LinkedParticipant`](LinkedParticipant.md)[] | Full roster. |\n| `shared?` | `null` \\| [`LinkedSharedGroupReference`](LinkedSharedGroupReference.md) | - |\n\n\n\n# Type Alias: LinkedGroupReference\n\n> **LinkedGroupReference** = \\{ `id`: `string`; `name`: `string`; \\}\n\nA group entity bound to a remote group.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | Group entity id. |\n| `name` | `string` | Display name, following WhatsApp. |\n\n\n\n# Type Alias: LinkedIdentity\n\n> **LinkedIdentity** = \\{ `alternateJid?`: `string` \\| `null`; `displayName?`: `string` \\| `null`; `jid`: `string`; \\}\n\nVerified sender aliases, supplied only by protocol metadata.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `alternateJid?` | `string` \\| `null` | Verified alternate identity, never inferred from a name or phone typed by a user. |\n| `displayName?` | `string` \\| `null` | Untrusted display name. |\n| `jid` | `string` | Phone-number or LID account JID. |\n\n\n\n# Type Alias: LinkedMediaKind\n\n> **LinkedMediaKind** = `\"image\"` \\| `\"audio\"` \\| `\"video\"` \\| `\"document\"`\n\nAttachment categories WhatsApp distinguishes.\n\n\n\n# Type Alias: LinkedMutation\n\n> **LinkedMutation** = `\"reaction\"` \\| `\"edit\"` \\| `\"delete\"`\n\nDeliberate operations on an existing linked-group message.\n\n\n\n# Type Alias: LinkedParticipant\n\n> **LinkedParticipant** = [`LinkedIdentity`](LinkedIdentity.md) & \\{ `admin`: `boolean`; \\}\n\nCurrent remote participant; Macro access is a separate grant.\n\n## Type Declaration\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `admin` | `boolean` | Group admin; only admins post in announcement groups. |\n\n\n\n# Type Alias: LinkedReplyBlock\n\n> **LinkedReplyBlock** = `\"sending_disabled\"` \\| `\"no_account\"` \\| `\"disconnected\"` \\| `\"not_observing\"` \\| `\"not_allowed_in_group\"` \\| `\"group_unavailable\"`\n\nWhy the viewer cannot send now.\n\n\n\n# Type Alias: LinkedReplyContent\n\n> **LinkedReplyContent** = \\{ `text`: `string`; `type`: `\"text\"`; \\} \\| \\{ `caption?`: `string` \\| `null`; `type`: `\"media\"`; `upload_id`: `string`; \\} \\| \\{ `message_id`: `string`; `operation`: [`LinkedMutation`](LinkedMutation.md); `text?`: `string` \\| `null`; `type`: `\"mutation\"`; \\}\n\nContent of a deliberate send.\n\n## Union Members\n\n### Type Literal\n\n\\{ `text`: `string`; `type`: `\"text\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `text` | `string` | UTF-8 text, at most [`TEXT_LIMIT_BYTES`] bytes. |\n| `type` | `\"text\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `caption?`: `string` \\| `null`; `type`: `\"media\"`; `upload_id`: `string`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `caption?` | `string` \\| `null` | Optional caption; audio has none. |\n| `type` | `\"media\"` | - |\n| `upload_id` | `string` | The staged upload, which becomes the message's private document. |\n\n***\n\n### Type Literal\n\n\\{ `message_id`: `string`; `operation`: [`LinkedMutation`](LinkedMutation.md); `text?`: `string` \\| `null`; `type`: `\"mutation\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `message_id` | `string` | Existing group message, resolved to its WhatsApp source by the server. |\n| `operation` | [`LinkedMutation`](LinkedMutation.md) | Explicit operation. |\n| `text?` | `string` \\| `null` | Replacement text or emoji; absent for delete, empty to remove a reaction. |\n| `type` | `\"mutation\"` | - |\n\n\n\n# Type Alias: LinkedSendProgress\n\n> **LinkedSendProgress** = \\{ `created_at`: `string`; `error_code?`: `string` \\| `null`; `group_id`: `string`; `id`: `string`; `message_id?`: `string` \\| `null`; `payload`: [`LinkedSendReply`](LinkedSendReply.md); `state`: [`LinkedDeliveryState`](LinkedDeliveryState.md); `user_id`: `string`; \\}\n\nDurable result of a deliberate send.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `created_at` | `string` | Intent creation time. |\n| `error_code?` | `string` \\| `null` | Sanitized failure code. |\n| `group_id` | `string` | Group entity. |\n| `id` | `string` | Stable intent id. |\n| `message_id?` | `string` \\| `null` | Group message, once WhatsApp accepted it. |\n| `payload` | [`LinkedSendReply`](LinkedSendReply.md) | The original request, so a failed draft can be recovered. |\n| `state` | [`LinkedDeliveryState`](LinkedDeliveryState.md) | Current state. |\n| `user_id` | `string` | Person who submitted it. |\n\n\n\n# Type Alias: LinkedSendReply\n\n> **LinkedSendReply** = \\{ `idempotency_key`: `string`; `message`: [`LinkedReplyContent`](LinkedReplyContent.md); `reply_to?`: `string` \\| `null`; \\}\n\nExplicit send action from a person. Only deliberate sends reach WhatsApp.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `idempotency_key` | `string` | Stable client intent key. A changed payload with this key is a conflict. |\n| `message` | [`LinkedReplyContent`](LinkedReplyContent.md) | Content. |\n| `reply_to?` | `string` \\| `null` | Group message being quoted, within this group. |\n\n\n\n# Type Alias: LinkedSharedGroupReference\n\n> **LinkedSharedGroupReference** = \\{ `id`: `string`; `name`: `string`; `owner_id`: `string`; \\}\n\nAnother person's group entity the caller can view.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | Group entity id. |\n| `name` | `string` | Display name, following WhatsApp. |\n| `owner_id` | `string` | Macro user who added the group and shared it. |\n\n\n\n# Type Alias: LinkedUploadedAttachment\n\n> **LinkedUploadedAttachment** = \\{ `filename`: `string`; `kind`: [`LinkedMediaKind`](LinkedMediaKind.md); `upload_id`: `string`; \\}\n\nA staged attachment, sent only by a later deliberate send.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `filename` | `string` | Display filename. |\n| `kind` | [`LinkedMediaKind`](LinkedMediaKind.md) | Category. |\n| `upload_id` | `string` | Use as `upload_id` in a media send; also the future document id. |\n\n\n\n# Type Alias: Message\n\n> **Message** = \\{ `attachments`: [`MessageAttachment`](MessageAttachment.md)[]; `bot_profile?`: `null` \\| [`BotSenderProfile`](BotSenderProfile.md); `content`: `string`; `created_at`: `string`; `deleted_at?`: `string` \\| `null`; `edited_at?`: `string` \\| `null`; `guest_name?`: `string` \\| `null`; `id`: `string`; `imported_author?`: `null` \\| [`ImportedAuthor`](ImportedAuthor.md); `mentions`: [`SimpleMention`](SimpleMention.md)[]; `parent`: [`MessageParent`](MessageParent.md); `reactions`: [`CountedReaction`](CountedReaction.md)[]; `sender_id`: `string`; `thread_id?`: `string` \\| `null`; `triggered_by?`: `string` \\| `null`; `updated_at`: `string`; \\}\n\nShared message representation for channel timelines and entity discussions.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `attachments` | [`MessageAttachment`](MessageAttachment.md)[] | Attached entities. |\n| `bot_profile?` | `null` \\| [`BotSenderProfile`](BotSenderProfile.md) | - |\n| `content` | `string` | Macro Markdown body. |\n| `created_at` | `string` | Creation time. |\n| `deleted_at?` | `string` \\| `null` | Message tombstone, independent of thread deletion. |\n| `edited_at?` | `string` \\| `null` | Last content edit, if any. |\n| `guest_name?` | `string` \\| `null` | Display name of a verified call guest, retained after the call ends. |\n| `id` | `string` | Message UUID. |\n| `imported_author?` | `null` \\| [`ImportedAuthor`](ImportedAuthor.md) | - |\n| `mentions` | [`SimpleMention`](SimpleMention.md)[] | Tracked mentions, retained when a caller changes attachments only. |\n| `parent` | [`MessageParent`](MessageParent.md) | Entity that owns this conversation. |\n| `reactions` | [`CountedReaction`](CountedReaction.md)[] | Aggregated reactions. |\n| `sender_id` | `string` | Authenticated actor or owner of imported content. |\n| `thread_id?` | `string` \\| `null` | Root message UUID for replies; absent on roots. |\n| `triggered_by?` | `string` \\| `null` | User who triggered a bot-authored message. |\n| `updated_at` | `string` | Last persisted update. |\n\n\n\n# Type Alias: MessageAttachment\n\n> **MessageAttachment** = \\{ `created_at`: `string`; `entity_id`: `string`; `entity_type`: `string`; `height?`: `number` \\| `null`; `id`: `string`; `width?`: `number` \\| `null`; \\}\n\nAn entity attached to a message.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `created_at` | `string` | When the attachment was added. |\n| `entity_id` | `string` | Attached entity identifier. |\n| `entity_type` | `string` | Attached entity type. |\n| `height?` | `number` \\| `null` | Optional media height. |\n| `id` | `string` | Attachment UUID. |\n| `width?` | `number` \\| `null` | Optional media width. |\n\n\n\n# Type Alias: MessageCursor\n\n> **MessageCursor** = \\{ `created_at`: `string`; `id`: `string`; \\}\n\nCursor for a chronological parent timeline.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `created_at` | `string` | Last message creation time or activity occurrence time. |\n| `id` | `string` | Last entry UUID, used to break timestamp ties across both sources. |\n\n\n\n# Type Alias: MessageParent\n\n> **MessageParent** = \\{ `id`: `string`; `type`: `\"channel\"`; \\} \\| \\{ `id`: [`DocumentId`](DocumentId.md); `type`: `\"document\"`; \\} \\| \\{ `id`: `string`; `type`: `\"initiative\"`; \\} \\| \\{ `id`: `string`; `type`: `\"crm_company\"`; \\} \\| \\{ `id`: `string`; `type`: `\"crm_contact\"`; \\} \\| \\{ `id`: `string`; `type`: `\"call\"`; \\}\n\nThe entity whose permissions and lifecycle govern a message.\n\n## Union Members\n\n### Type Literal\n\n\\{ `id`: `string`; `type`: `\"channel\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | A channel, including direct messages. |\n| `type` | `\"channel\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `id`: [`DocumentId`](DocumentId.md); `type`: `\"document\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `id` | [`DocumentId`](DocumentId.md) | A document, including tasks and PDFs. |\n| `type` | `\"document\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `id`: `string`; `type`: `\"initiative\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | An initiative, presented as a project in the application. |\n| `type` | `\"initiative\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `id`: `string`; `type`: `\"crm_company\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | A CRM company. |\n| `type` | `\"crm_company\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `id`: `string`; `type`: `\"crm_contact\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | A CRM contact. |\n| `type` | `\"crm_contact\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `id`: `string`; `type`: `\"call\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | A video call and its persistent chat thread. |\n| `type` | `\"call\"` | - |\n\n\n\n# Type Alias: ParticipantRole\n\n> **ParticipantRole** = `\"owner\"` \\| `\"admin\"` \\| `\"member\"`\n\nThe role a user has within a channel.\n\n\n\n# Type Alias: SharePermissionV2\n\n> **SharePermissionV2** = \\{ `channelSharePermissions?`: [`ChannelSharePermission`](ChannelSharePermission.md)[] \\| `null`; `id`: `string`; `linkShare?`: `null` \\| [`LinkShare`](LinkShare.md); `linkShareAccessLevel?`: `null` \\| [`AccessLevel`](AccessLevel.md); `owner`: `string`; `teamShareAccessLevel?`: `null` \\| [`AccessLevel`](AccessLevel.md); \\}\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channelSharePermissions?` | [`ChannelSharePermission`](ChannelSharePermission.md)[] \\| `null` | The channel share permissions for the item |\n| `id` | `string` | The share permission id |\n| `linkShare?` | `null` \\| [`LinkShare`](LinkShare.md) | - |\n| `linkShareAccessLevel?` | `null` \\| [`AccessLevel`](AccessLevel.md) | - |\n| `owner` | `string` | The owner of the item |\n| `teamShareAccessLevel?` | `null` \\| [`AccessLevel`](AccessLevel.md) | - |\n\n\n\n# Type Alias: SimpleMention\n\n> **SimpleMention** = \\{ `entity_id`: `string`; `entity_type`: `string`; \\}\n\nA mention tracked in a message body.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `entity_id` | `string` | Mentioned entity identifier. |\n| `entity_type` | `string` | Mentioned entity type. |\n\n\n\n# Type Alias: SmartTagChannelMatch\n\n> **SmartTagChannelMatch** = \\{ `id`: `string`; `name`: `string`; \\}\n\nA channel visible to the caller that matches a smart tag rule.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | Channel id. |\n| `name` | `string` | Channel display name. |\n\n\n\n# Type Alias: SmartTagPreview\n\n> **SmartTagPreview** = \\{ `channels`: [`SmartTagChannelMatch`](SmartTagChannelMatch.md)[]; `totalCount`: `number`; \\}\n\nA bounded preview and the total number of visible channels matching a rule.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channels` | [`SmartTagChannelMatch`](SmartTagChannelMatch.md)[] | First matches, in alphabetical order. |\n| `totalCount` | `number` | Number of matching channels the caller participates in, including overflow. |\n\n\n\n# Type Alias: TeamRole\n\n> **TeamRole** = `\"member\"` \\| `\"admin\"` \\| `\"owner\"`\n\nThe role a user has within a team.\n\nOrdered least to most privileged so comparisons reflect access strength.\n\n\n\n# Type Alias: UpdateChannelSharePermission\n\n> **UpdateChannelSharePermission** = \\{ `accessLevel?`: `null` \\| [`AccessLevel`](AccessLevel.md); `channelId`: `string`; `operation`: [`UpdateOperation`](UpdateOperation.md); \\}\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `accessLevel?` | `null` \\| [`AccessLevel`](AccessLevel.md) | - |\n| `channelId` | `string` | The channel id |\n| `operation` | [`UpdateOperation`](UpdateOperation.md) | The type of operation to be performed on the chanel share permission You can add, remove or replace and existing permission |\n\n\n\n# Type Alias: UpdateOperation\n\n> **UpdateOperation** = `\"add\"` \\| `\"remove\"` \\| `\"replace\"`\n\n\n\n# Type Alias: UpdateSharePermissionRequestV2\n\n> **UpdateSharePermissionRequestV2** = \\{ `channelSharePermissions?`: [`UpdateChannelSharePermission`](UpdateChannelSharePermission.md)[] \\| `null`; `linkShare?`: `null` \\| [`LinkShare`](LinkShare.md); `linkShareAccessLevel?`: `null` \\| [`AccessLevel`](AccessLevel.md); `teamShareAccessLevel?`: `null` \\| [`AccessLevel`](AccessLevel.md); \\}\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channelSharePermissions?` | [`UpdateChannelSharePermission`](UpdateChannelSharePermission.md)[] \\| `null` | Any channel share permissions to be created/updated/removed |\n| `linkShare?` | `null` \\| [`LinkShare`](LinkShare.md) | - |\n| `linkShareAccessLevel?` | `null` \\| [`AccessLevel`](AccessLevel.md) | - |\n| `teamShareAccessLevel?` | `null` \\| [`AccessLevel`](AccessLevel.md) | - |\n\n\n\n# Type Alias: WhatsAppGroupDetail\n\n> **WhatsAppGroupDetail** = [`WhatsAppGroupSummary`](WhatsAppGroupSummary.md) & \\{ `access_level`: [`AccessLevel`](AccessLevel.md); `limits`: [`LinkedCapabilities`](LinkedCapabilities.md); `roster?`: `null` \\| [`LinkedGroupInfo`](LinkedGroupInfo.md); `sending_identity?`: `string` \\| `null`; `share_permission`: [`SharePermissionV2`](SharePermissionV2.md); \\}\n\nFull group detail and current viewer capabilities.\n\n## Type Declaration\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `access_level` | [`AccessLevel`](AccessLevel.md) | Caller's effective permission. |\n| `limits` | [`LinkedCapabilities`](LinkedCapabilities.md) | Size and mutation limits. |\n| `roster?` | `null` \\| [`LinkedGroupInfo`](LinkedGroupInfo.md) | - |\n| `sending_identity?` | `string` \\| `null` | Caller's own phone identity, never another account's identity. |\n| `share_permission` | [`SharePermissionV2`](SharePermissionV2.md) | Owner-controlled standard sharing configuration. |\n\n\n\n# Type Alias: WhatsAppGroupMessage\n\n> **WhatsAppGroupMessage** = \\{ `attachments`: [`LinkedAttachment`](LinkedAttachment.md)[]; `body`: `string`; `deleted`: `boolean`; `delivery_state?`: `null` \\| [`LinkedDeliveryState`](LinkedDeliveryState.md); `edited_at?`: `string` \\| `null`; `id`: `string`; `reactions`: [`WhatsAppGroupReaction`](WhatsAppGroupReaction.md)[]; `received_at`: `string`; `reply_to?`: `string` \\| `null`; `sender`: [`WhatsAppGroupSender`](WhatsAppGroupSender.md); `sent_at`: `string`; \\}\n\nOne message in an adopted group.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `attachments` | [`LinkedAttachment`](LinkedAttachment.md)[] | Imported or pending attachments. |\n| `body` | `string` | Plain WhatsApp text. |\n| `deleted` | `boolean` | Deleted for everyone. |\n| `delivery_state?` | `null` \\| [`LinkedDeliveryState`](LinkedDeliveryState.md) | - |\n| `edited_at?` | `string` \\| `null` | Last edit timestamp. |\n| `id` | `string` | Stable message id. |\n| `reactions` | [`WhatsAppGroupReaction`](WhatsAppGroupReaction.md)[] | Reactions grouped by emoji. |\n| `received_at` | `string` | Timestamp first received by Macro. |\n| `reply_to?` | `string` \\| `null` | Message quoted by this one. |\n| `sender` | [`WhatsAppGroupSender`](WhatsAppGroupSender.md) | Sender. |\n| `sent_at` | `string` | Timestamp assigned by WhatsApp. |\n\n\n\n# Type Alias: WhatsAppGroupReaction\n\n> **WhatsAppGroupReaction** = \\{ `count`: `number`; `emoji`: `string`; `mine`: `boolean`; \\}\n\nGrouped reactions.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `count` | `number` | Number of reacting people. |\n| `emoji` | `string` | Emoji. |\n| `mine` | `boolean` | Caller has this reaction. |\n\n\n\n# Type Alias: WhatsAppGroupSender\n\n> **WhatsAppGroupSender** = \\{ `name`: `string`; `person_id`: `string`; `user_id?`: `string` \\| `null`; \\}\n\nMessage sender from protocol-verified identity evidence.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `name` | `string` | Display name, never authentication evidence. |\n| `person_id` | `string` | External person identity. |\n| `user_id?` | `string` \\| `null` | Verified Macro user association, if present. |\n\n\n\n# Type Alias: WhatsAppGroupSummary\n\n> **WhatsAppGroupSummary** = \\{ `backfill?`: `null` \\| [`LinkedBackfill`](LinkedBackfill.md); `can_send`: `boolean`; `id`: `string`; `last_message_at?`: `string` \\| `null`; `name`: `string`; `send_reason?`: `null` \\| [`LinkedReplyBlock`](LinkedReplyBlock.md); `unread_count`: `number`; `user_id`: `string`; \\}\n\nAdopted group visible through ordinary entity sharing.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `backfill?` | `null` \\| [`LinkedBackfill`](LinkedBackfill.md) | - |\n| `can_send` | `boolean` | Caller may currently send from their own account. |\n| `id` | `string` | Entity identifier. |\n| `last_message_at?` | `string` \\| `null` | Latest message timestamp. |\n| `name` | `string` | Display name. |\n| `send_reason?` | `null` \\| [`LinkedReplyBlock`](LinkedReplyBlock.md) | - |\n| `unread_count` | `number` | Unread live messages for this viewer. |\n| `user_id` | `string` | Actual entity owner. |\n\n\n\n# Class: ChannelActivity\n\nA user's channel activity marker.\n\n## Constructors\n\n### Constructor\n\n> **new ChannelActivity**(`client`, `channel`, `record`): `ChannelActivity`\n\n#### Parameters\n\n| Parameter | Type | Description |\n| ------ | ------ | ------ |\n| `client` | `MacroClient` | - |\n| `channel` | [`Channel`](../../channel/classes/Channel.md) | Related channel. |\n| `record` | [`ApiActivity`](../../../../../generated/storage/types.gen/type-aliases/ApiActivity.md) | - |\n\n#### Returns\n\n`ChannelActivity`\n\n## Properties\n\n| Property | Modifier | Type | Description |\n| ------ | ------ | ------ | ------ |\n| `channel` | `readonly` | [`Channel`](../../channel/classes/Channel.md) | Related channel. |\n\n## Accessors\n\n### id\n\n#### Get Signature\n\n> **get** **id**(): `string`\n\nActivity identity.\n\n##### Returns\n\n`string`\n\n## Methods\n\n### createdAt()\n\n> **createdAt**(): `string`\n\nCreation time.\n\n#### Returns\n\n`string`\n\n***\n\n### recordInteraction()\n\n> **recordInteraction**(`type`): `Promise`\\<`ChannelActivity`\\>\n\nRecord a view or interaction in this channel.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `type` | [`ActivityType`](../../../../../generated/storage/types.gen/type-aliases/ActivityType.md) |\n\n#### Returns\n\n`Promise`\\<`ChannelActivity`\\>\n\n***\n\n### toJSON()\n\n> **toJSON**(): [`ApiActivity`](../../../../../generated/storage/types.gen/type-aliases/ApiActivity.md)\n\nSerialize activity without credentials.\n\n#### Returns\n\n[`ApiActivity`](../../../../../generated/storage/types.gen/type-aliases/ApiActivity.md)\n\n***\n\n### user()\n\n> **user**(): [`User`](../../../users/user/classes/User.md)\n\nUser who interacted.\n\n#### Returns\n\n[`User`](../../../users/user/classes/User.md)\n\n\n\n# Class: Channel\n\nA channel.\n\n## Extends\n\n- [`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md)\\<`ChannelDetail`\\>\n\n## Properties\n\n| Property | Modifier | Type | Default value | Description | Overrides | Inherited from |\n| ------ | ------ | ------ | ------ | ------ | ------ | ------ |\n| `entityType` | `readonly` | `\"channel\"` | `'channel'` | Favorites identify channels as `channel`. | [`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`entityType`](../../../entity/classes/PropertiedEntity.md#property-entitytype) | - |\n| `id` | `public readonly` | `string` | `undefined` | - | - | [`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`id`](../../../entity/classes/PropertiedEntity.md#property-id) |\n| `name` | `readonly` | () => `Promise`\\<`string`\\> | `...` | The channel's display name, resolved from the viewer's perspective. | - | - |\n| `type` | `readonly` | () => `Promise`\\<[`ChannelType`](../../../../../generated/storage/types.gen/type-aliases/ChannelType.md)\\> | `...` | The channel's type (e.g. `public`, `dm`, `private`). | - | - |\n| `search` | `static` | (`client`, `query`, `searchOpts?`) => `AsyncGenerator`\\<`Channel`\\> | `...` | Search channels by content, most relevant first, auto-paginated. | - | - |\n\n## Methods\n\n### addParticipants()\n\n> **addParticipants**(`users`): `Promise`\\<`void`\\>\n\nAdd users to the channel.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `users` | [`User`](../../../users/user/classes/User.md)[] |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### agentConversations()\n\n> **agentConversations**(): `Promise`\\<[`AgentConversation`](../../../agent-sessions/conversation/classes/AgentConversation.md)[]\\>\n\nConversations maintained by agents in this channel.\n\n#### Returns\n\n`Promise`\\<[`AgentConversation`](../../../agent-sessions/conversation/classes/AgentConversation.md)[]\\>\n\n***\n\n### attachments()\n\n> **attachments**(`opts?`): `AsyncGenerator`\\<[`ApiChannelAttachment`](../../../../../generated/storage/types.gen/type-aliases/ApiChannelAttachment.md)\\>\n\nThe attachments posted in this channel, auto-paginated.\n\n#### Parameters\n\n| Parameter | Type | Description |\n| ------ | ------ | ------ |\n| `opts?` | \\{ `pageSize?`: `number`; `type?`: `string`; \\} | - |\n| `opts.pageSize?` | `number` | - |\n| `opts.type?` | `string` | Filter by type: `static` for images/videos, `dss` for documents. |\n\n#### Returns\n\n`AsyncGenerator`\\<[`ApiChannelAttachment`](../../../../../generated/storage/types.gen/type-aliases/ApiChannelAttachment.md)\\>\n\n***\n\n### delete()\n\n> **delete**(): `Promise`\\<`void`\\>\n\nDelete the channel. This is not reversible.\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### deleteProperty()\n\n> **deleteProperty**(`assignment`): `Promise`\\<`void`\\>\n\nRemove a property from this entity, given its [properties](#properties) entry.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `assignment` | [`EntityPropertyWithDefinition`](../../../../../generated/properties/types.gen/type-aliases/EntityPropertyWithDefinition.md) |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`deleteProperty`](../../../entity/classes/PropertiedEntity.md#deleteproperty)\n\n***\n\n### favorite()\n\n> **favorite**(): `Promise`\\<`Channel`\\>\n\nAdd this entity to the user's favorites. Returns this handle for chaining.\nPlain unwrap: favoriting alters the user's favorites collection, not this\nentity's own detail, so there's nothing cached to invalidate.\n\n#### Returns\n\n`Promise`\\<`Channel`\\>\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`favorite`](../../../entity/classes/PropertiedEntity.md#favorite)\n\n***\n\n### join()\n\n> **join**(): `Promise`\\<`void`\\>\n\nJoin the channel as the current user.\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### joinCode()\n\n> **joinCode**(): `Promise`\\<`string`\\>\n\nA reusable code that lets a user join this channel.\n\n#### Returns\n\n`Promise`\\<`string`\\>\n\n***\n\n### leave()\n\n> **leave**(): `Promise`\\<`void`\\>\n\nLeave the channel as the current user.\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### message()\n\n> **message**(`id`): [`Message`](../../message/classes/Message.md)\n\nA handle to a message in this channel by id.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `id` | `string` |\n\n#### Returns\n\n[`Message`](../../message/classes/Message.md)\n\n***\n\n### messages()\n\n> **messages**(`opts?`): `AsyncGenerator`\\<[`Message`](../../message/classes/Message.md)\\>\n\nRoot messages in this channel, most recent first, auto-paginated.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `opts?` | \\{ `pageSize?`: `number`; \\} |\n| `opts.pageSize?` | `number` |\n\n#### Returns\n\n`AsyncGenerator`\\<[`Message`](../../message/classes/Message.md)\\>\n\n***\n\n### messagesAfter()\n\n> **messagesAfter**(`after`, `opts?`): `AsyncGenerator`\\<[`Message`](../../message/classes/Message.md)\\>\n\nRoot messages created or replied to at or after `after`, most recent\nfirst, auto-paginated. Use it to catch up after a disconnect.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `after` | `string` \\| `Date` |\n| `opts?` | \\{ `pageSize?`: `number`; \\} |\n| `opts.pageSize?` | `number` |\n\n#### Returns\n\n`AsyncGenerator`\\<[`Message`](../../message/classes/Message.md)\\>\n\n***\n\n### messageThreadById()\n\n> **messageThreadById**(`id`): [`MessageThread`](../../../messages/thread/classes/MessageThread.md)\n\nA shared message thread scoped to this resource.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `id` | `string` |\n\n#### Returns\n\n[`MessageThread`](../../../messages/thread/classes/MessageThread.md)\n\n***\n\n### participants()\n\n> **participants**(): `Promise`\\<[`ApiChannelParticipant`](../../../../../generated/storage/types.gen/type-aliases/ApiChannelParticipant.md)[]\\>\n\nThe channel's members, with their roles and join times.\n\n#### Returns\n\n`Promise`\\<[`ApiChannelParticipant`](../../../../../generated/storage/types.gen/type-aliases/ApiChannelParticipant.md)[]\\>\n\n***\n\n### permission()\n\n> **permission**(): `Promise`\\<\\{ `permission`: [`EntityPermission`](../../../../../generated/storage/types.gen/type-aliases/EntityPermission.md); `status`: `\"access\"`; \\} \\| \\{ `status`: `\"no_access\"`; \\}\\>\n\nRead the caller's effective permission on this entity.\n\n#### Returns\n\n`Promise`\\<\\{ `permission`: [`EntityPermission`](../../../../../generated/storage/types.gen/type-aliases/EntityPermission.md); `status`: `\"access\"`; \\} \\| \\{ `status`: `\"no_access\"`; \\}\\>\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`permission`](../../../entity/classes/PropertiedEntity.md#permission)\n\n***\n\n### properties()\n\n> **properties**(`opts?`): `Promise`\\<[`EntityPropertyWithDefinition`](../../../../../generated/properties/types.gen/type-aliases/EntityPropertyWithDefinition.md)[]\\>\n\nThe properties set on this entity, each with its definition, value, and options.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `opts?` | \\{ `includeMetadata?`: `boolean`; \\} |\n| `opts.includeMetadata?` | `boolean` |\n\n#### Returns\n\n`Promise`\\<[`EntityPropertyWithDefinition`](../../../../../generated/properties/types.gen/type-aliases/EntityPropertyWithDefinition.md)[]\\>\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`properties`](../../../entity/classes/PropertiedEntity.md#properties)\n\n***\n\n### propertyReference()\n\n> **propertyReference**(): [`PropertyTargetReference`](../../../../../generated/properties/types.gen/type-aliases/PropertyTargetReference.md)\n\nThis entity's reference for bulk property operations.\n\n#### Returns\n\n[`PropertyTargetReference`](../../../../../generated/properties/types.gen/type-aliases/PropertyTargetReference.md)\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`propertyReference`](../../../entity/classes/PropertiedEntity.md#propertyreference)\n\n***\n\n### recordActivity()\n\n> **recordActivity**(`type`): `Promise`\\<[`ChannelActivity`](../../activity/classes/ChannelActivity.md)\\>\n\nRecord a view or interaction in this channel.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `type` | [`ActivityType`](../../../../../generated/storage/types.gen/type-aliases/ActivityType.md) |\n\n#### Returns\n\n`Promise`\\<[`ChannelActivity`](../../activity/classes/ChannelActivity.md)\\>\n\n***\n\n### removeParticipants()\n\n> **removeParticipants**(`users`): `Promise`\\<`void`\\>\n\nRemove users from the channel.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `users` | [`User`](../../../users/user/classes/User.md)[] |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### rename()\n\n> **rename**(`name`): `Promise`\\<`void`\\>\n\nRename the channel.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `name` | `string` |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### send()\n\n> **send**(`body`, `opts?`): `Promise`\\<[`Message`](../../message/classes/Message.md)\\>\n\nPost a message. Plain text, or a rich body composed with `msg`.\n\nWorks with a channel webhook token as well as a normal one — see\n[postToChannel](../../post/functions/postToChannel.md) for which endpoint that picks and what it costs.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `body` | `string` \\| [`RichMessage`](../../../../mentions/interfaces/RichMessage.md) |\n| `opts?` | \\{ `thread?`: [`Thread`](../../thread/classes/Thread.md); \\} |\n| `opts.thread?` | [`Thread`](../../thread/classes/Thread.md) |\n\n#### Returns\n\n`Promise`\\<[`Message`](../../message/classes/Message.md)\\>\n\n***\n\n### setPicture()\n\n> **setPicture**(`picture?`): `Promise`\\<`void`\\>\n\nSet an uploaded profile picture, or clear it.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `picture?` | [`ChannelPicture`](../../picture/classes/ChannelPicture.md) |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### setProperty()\n\n> **setProperty**(`property`, `value?`): `Promise`\\<`void`\\>\n\nSet a property value on this entity, or attach the property without a\nvalue when `value` is omitted.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `property` | [`PropertyDefinition`](../../../properties/property-definition/classes/PropertyDefinition.md) |\n| `value?` | [`SetPropertyValue`](../../../../../generated/properties/types.gen/type-aliases/SetPropertyValue.md) |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`setProperty`](../../../entity/classes/PropertiedEntity.md#setproperty)\n\n***\n\n### timelinePage()\n\n> **timelinePage**(`selection?`): `Promise`\\<\\{ `entries`: (\\{ `activity`: \\{ `action`: `string`; `actor`: [`User`](../../../users/user/classes/User.md); `actor_id`: `string`; `id`: `string`; `occurred_at`: `string`; `payload?`: `unknown`; \\}; `message?`: `undefined`; `type`: `\"activity\"`; \\} \\| \\{ `activity?`: `undefined`; `message`: [`Message`](../../message/classes/Message.md); `type`: `\"message\"`; \\})[]; `nextCursor`: [`MessageCursor`](../../../../../generated/storage/types.gen/type-aliases/MessageCursor.md) \\| `undefined`; `previousCursor`: [`MessageCursor`](../../../../../generated/storage/types.gen/type-aliases/MessageCursor.md) \\| `undefined`; \\}\\>\n\nA mixed message and activity timeline page.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `selection?` | [`TimelineOptions`](../../../messages/thread/interfaces/TimelineOptions.md)\\<[`Message`](../../message/classes/Message.md)\\> |\n\n#### Returns\n\n`Promise`\\<\\{ `entries`: (\\{ `activity`: \\{ `action`: `string`; `actor`: [`User`](../../../users/user/classes/User.md); `actor_id`: `string`; `id`: `string`; `occurred_at`: `string`; `payload?`: `unknown`; \\}; `message?`: `undefined`; `type`: `\"activity\"`; \\} \\| \\{ `activity?`: `undefined`; `message`: [`Message`](../../message/classes/Message.md); `type`: `\"message\"`; \\})[]; `nextCursor`: [`MessageCursor`](../../../../../generated/storage/types.gen/type-aliases/MessageCursor.md) \\| `undefined`; `previousCursor`: [`MessageCursor`](../../../../../generated/storage/types.gen/type-aliases/MessageCursor.md) \\| `undefined`; \\}\\>\n\n***\n\n### toJSON()\n\n> **toJSON**(): \\{ `detail?`: [`ApiChannelDetail`](../../../../../generated/storage/types.gen/type-aliases/ApiChannelDetail.md); `id`: `string`; \\}\n\n#### Returns\n\n\\{ `detail?`: [`ApiChannelDetail`](../../../../../generated/storage/types.gen/type-aliases/ApiChannelDetail.md); `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `detail?` | [`ApiChannelDetail`](../../../../../generated/storage/types.gen/type-aliases/ApiChannelDetail.md) |\n| `id` | `string` |\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`toJSON`](../../../entity/classes/PropertiedEntity.md#tojson)\n\n***\n\n### typing()\n\n> **typing**(`action`, `opts?`): `Promise`\\<`void`\\>\n\nBroadcast a typing indicator, optionally scoped to a thread.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `action` | `\"stop\"` \\| `\"start\"` |\n| `opts?` | \\{ `thread?`: [`Thread`](../../thread/classes/Thread.md); \\} |\n| `opts.thread?` | [`Thread`](../../thread/classes/Thread.md) |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### unfavorite()\n\n> **unfavorite**(): `Promise`\\<`Channel`\\>\n\nRemove this entity from the user's favorites. Returns this handle for chaining.\n\n#### Returns\n\n`Promise`\\<`Channel`\\>\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`unfavorite`](../../../entity/classes/PropertiedEntity.md#unfavorite)\n\n***\n\n### updatePropertyOptions()\n\n> **updatePropertyOptions**(`changes`): `Promise`\\<[`BulkUpdateEntityPropertyOptionsResponse`](../../../../../generated/properties/types.gen/type-aliases/BulkUpdateEntityPropertyOptionsResponse.md)\\>\n\nApply multi-select option deltas atomically across this entity's properties.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `changes` | [`PropertyOptionDelta`](../../../entity/interfaces/PropertyOptionDelta.md)[] |\n\n#### Returns\n\n`Promise`\\<[`BulkUpdateEntityPropertyOptionsResponse`](../../../../../generated/properties/types.gen/type-aliases/BulkUpdateEntityPropertyOptionsResponse.md)\\>\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`updatePropertyOptions`](../../../entity/classes/PropertiedEntity.md#updatepropertyoptions)\n\n***\n\n### webUrl()\n\n> **webUrl**(): `string`\n\nThe channel's URL in the Macro web app.\n\n#### Returns\n\n`string`\n\n***\n\n### byId()\n\n> `static` **byId**(`client`, `id`): `Channel`\n\nA handle to a channel by id. Details load on first access.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `id` | `string` |\n\n#### Returns\n\n`Channel`\n\n***\n\n### create()\n\n> `static` **create**(`client`, `opts`): `Promise`\\<`Channel`\\>\n\nCreate a channel. The caller becomes the owner.\n\n#### Parameters\n\n| Parameter | Type | Description |\n| ------ | ------ | ------ |\n| `client` | `MacroClient` | - |\n| `opts` | \\{ `name?`: `string`; `participants?`: [`User`](../../../users/user/classes/User.md)[]; `team?`: [`Team`](../../../teams/team/classes/Team.md); `type`: [`ChannelType`](../../../../../generated/storage/types.gen/type-aliases/ChannelType.md); \\} | - |\n| `opts.name?` | `string` | - |\n| `opts.participants?` | [`User`](../../../users/user/classes/User.md)[] | Participants to add, excluding the owner. |\n| `opts.team?` | [`Team`](../../../teams/team/classes/Team.md) | Team, for team channels. |\n| `opts.type` | [`ChannelType`](../../../../../generated/storage/types.gen/type-aliases/ChannelType.md) | - |\n\n#### Returns\n\n`Promise`\\<`Channel`\\>\n\n***\n\n### dm()\n\n> `static` **dm**(`client`, `recipient`): `Promise`\\<`Channel`\\>\n\nOpen (creating if needed) the DM channel with a user.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `recipient` | [`User`](../../../users/user/classes/User.md) |\n\n#### Returns\n\n`Promise`\\<`Channel`\\>\n\n***\n\n### private()\n\n> `static` **private**(`client`, `recipients`): `Promise`\\<`Channel`\\>\n\nOpen (creating if needed) the private group channel with a set of users.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `recipients` | [`User`](../../../users/user/classes/User.md)[] |\n\n#### Returns\n\n`Promise`\\<`Channel`\\>\n\n\n\n# Type Alias: ChannelAttachment\n\n> **ChannelAttachment** = [`GetChannelAttachmentsResponses`](../../../../../generated/storage/types.gen/type-aliases/GetChannelAttachmentsResponses.md)\\[`200`\\]\\[`\"items\"`\\]\\[`number`\\]\n\nA file or entity attached to a message in a channel.\n\n\n\n# Type Alias: ChannelParticipant\n\n> **ChannelParticipant** = [`GetChannelParticipantsResponses`](../../../../../generated/storage/types.gen/type-aliases/GetChannelParticipantsResponses.md)\\[`200`\\]\\[`number`\\]\n\nA member of a channel, with their role and join time.\n\n\n\n# Class: ChannelLabelsNamespace\n\nShared or private channel labels: named groups of channels shown in the Chat\nsidebar. Every member of the team sees the same labels in the same order,\nand any member may create, rename, delete, or move channels between them;\neach change applies to the whole team. Users without a team have private\nlabels belonging to their account.\n\nA label's `channelIds` lists only the channels the authenticated user is a\nmember of. Manual labels count all assignments; smart tags count visible\nmatches. A channel can be in one manual label and any number of matching\nsmart tags per team or account. Direct messages cannot be grouped.\n\n## Constructors\n\n### Constructor\n\n> **new ChannelLabelsNamespace**(`client`): `ChannelLabelsNamespace`\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n\n#### Returns\n\n`ChannelLabelsNamespace`\n\n## Methods\n\n### create()\n\n> **create**(`name`, `channels?`): `Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\nCreate a label and atomically move `channels` into it. Names are unique\nwithin the team or account, case-insensitively.\n\n#### Parameters\n\n| Parameter | Type | Default value |\n| ------ | ------ | ------ |\n| `name` | `string` | `undefined` |\n| `channels?` | [`Channel`](../../channel/classes/Channel.md)[] | `[]` |\n\n#### Returns\n\n`Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\n***\n\n### createSmartTag()\n\n> **createSmartTag**(`name`, `rule`): `Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\nCreate a smart tag whose membership follows an attribute rule automatically.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `name` | `string` |\n| `rule` | [`ChannelLabelRule`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabelRule.md) |\n\n#### Returns\n\n`Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\n***\n\n### delete()\n\n> **delete**(`label`): `Promise`\\<`void`\\>\n\nDelete a label for the whole team; its channels return to the plain list.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `label` | [`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md) |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### list()\n\n> **list**(): `Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)[]\\>\n\nEvery label in the authenticated user's shared or private scope.\n\n#### Returns\n\n`Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)[]\\>\n\n***\n\n### previewSmartTag()\n\n> **previewSmartTag**(`rule`): `Promise`\\<[`SmartTagPreview`](../../../../../generated/storage/types.gen/type-aliases/SmartTagPreview.md)\\>\n\nPreview the first matches and total visible count without creating a tag.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `rule` | [`ChannelLabelRule`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabelRule.md) |\n\n#### Returns\n\n`Promise`\\<[`SmartTagPreview`](../../../../../generated/storage/types.gen/type-aliases/SmartTagPreview.md)\\>\n\n***\n\n### rename()\n\n> **rename**(`label`, `name`): `Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\nRename a label for the whole team.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `label` | [`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md) |\n| `name` | `string` |\n\n#### Returns\n\n`Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\n***\n\n### setLabel()\n\n> **setLabel**(`channel`, `label`): `Promise`\\<`void`\\>\n\nMove a channel into `label`, or out of any label when `label` is `undefined`.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `channel` | [`Channel`](../../channel/classes/Channel.md) |\n| `label` | [`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md) \\| `undefined` |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### updateSmartTag()\n\n> **updateSmartTag**(`label`, `name`, `rule`): `Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\nUpdate the name and matching rule of an existing smart tag.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `label` | [`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md) |\n| `name` | `string` |\n| `rule` | [`ChannelLabelRule`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabelRule.md) |\n\n#### Returns\n\n`Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\n\n\n# Class: Message\n\nA message in a channel. A thin handle: `id` and `channelId` are known up\nfront, and record-backed fields (`content`, `author`) load lazily on first\naccess and cache. Actions carry the ids for you.\n\n## Extends\n\n- [`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md)\\<[`Message`](../../../../../generated/storage/types.gen/type-aliases/Message.md)\\>\n\n## Properties\n\n| Property | Modifier | Type | Default value | Description | Overrides | Inherited from |\n| ------ | ------ | ------ | ------ | ------ | ------ | ------ |\n| `channelId` | `readonly` | `string` | `undefined` | - | - | - |\n| `content` | `readonly` | () => `Promise`\\<`string`\\> | `...` | The message body. | - | - |\n| `createdAt` | `readonly` | () => `Promise`\\<`string`\\> | `...` | When the message was created. | - | - |\n| `editedAt` | `readonly` | () => `Promise`\\<`string` \\| `undefined`\\> | `...` | When the message was last edited, if it has been. | - | - |\n| `entityType` | `readonly` | `\"channel_message\"` | `'channel_message'` | Favorites identify channel messages as `channel_message`. | [`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`entityType`](../../../entity/classes/FavoritableEntity.md#property-entitytype) | - |\n| `id` | `public readonly` | `string` | `undefined` | - | - | [`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`id`](../../../entity/classes/FavoritableEntity.md#property-id) |\n| `mentions` | `readonly` | [`SimpleMention`](../../../../../generated/storage/types.gen/type-aliases/SimpleMention.md)[] | `[]` | Entities mentioned in this message, when a list, thread, or event supplied them. | - | - |\n| `updatedAt` | `readonly` | () => `Promise`\\<`string`\\> | `...` | When the message was last updated. | - | - |\n\n## Methods\n\n### author()\n\n> **author**(): `Promise`\\<[`User`](../../../users/user/classes/User.md)\\>\n\nThe user who sent this message.\n\n#### Returns\n\n`Promise`\\<[`User`](../../../users/user/classes/User.md)\\>\n\n***\n\n### channel()\n\n> **channel**(): [`Channel`](../../channel/classes/Channel.md)\n\nThe channel this message is in.\n\n#### Returns\n\n[`Channel`](../../channel/classes/Channel.md)\n\n***\n\n### delete()\n\n> **delete**(): `Promise`\\<`void`\\>\n\nDelete this message.\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### edit()\n\n> **edit**(`body`): `Promise`\\<`Message`\\>\n\nReplace this message's body.\n\n#### Parameters\n\n| Parameter | Type | Description |\n| ------ | ------ | ------ |\n| `body` | `string` \\| [`RichMessage`](../../../../mentions/interfaces/RichMessage.md) | Plain text, or a rich body composed with msg. |\n\n#### Returns\n\n`Promise`\\<`Message`\\>\n\n***\n\n### favorite()\n\n> **favorite**(): `Promise`\\<`Message`\\>\n\nAdd this entity to the user's favorites. Returns this handle for chaining.\nPlain unwrap: favoriting alters the user's favorites collection, not this\nentity's own detail, so there's nothing cached to invalidate.\n\n#### Returns\n\n`Promise`\\<`Message`\\>\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`favorite`](../../../entity/classes/FavoritableEntity.md#favorite)\n\n***\n\n### permission()\n\n> **permission**(): `Promise`\\<\\{ `permission`: [`EntityPermission`](../../../../../generated/storage/types.gen/type-aliases/EntityPermission.md); `status`: `\"access\"`; \\} \\| \\{ `status`: `\"no_access\"`; \\}\\>\n\nRead the caller's effective permission on this entity.\n\n#### Returns\n\n`Promise`\\<\\{ `permission`: [`EntityPermission`](../../../../../generated/storage/types.gen/type-aliases/EntityPermission.md); `status`: `\"access\"`; \\} \\| \\{ `status`: `\"no_access\"`; \\}\\>\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`permission`](../../../entity/classes/FavoritableEntity.md#permission)\n\n***\n\n### react()\n\n> **react**(`emoji`): `Promise`\\<`Message`\\>\n\nAdd an emoji reaction to this message.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `emoji` | `string` |\n\n#### Returns\n\n`Promise`\\<`Message`\\>\n\n***\n\n### reply()\n\n> **reply**(`body`): `Promise`\\<`Message`\\>\n\nPost a reply in this message's thread.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `body` | `string` \\| [`RichMessage`](../../../../mentions/interfaces/RichMessage.md) |\n\n#### Returns\n\n`Promise`\\<`Message`\\>\n\n***\n\n### thread()\n\n> **thread**(): [`Thread`](../../thread/classes/Thread.md)\n\nThe thread rooted at this message.\n\n#### Returns\n\n[`Thread`](../../thread/classes/Thread.md)\n\n***\n\n### toJSON()\n\n> **toJSON**(): \\{ `detail?`: [`Message`](../../../../../generated/storage/types.gen/type-aliases/Message.md); `id`: `string`; \\}\n\n#### Returns\n\n\\{ `detail?`: [`Message`](../../../../../generated/storage/types.gen/type-aliases/Message.md); `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `detail?` | [`Message`](../../../../../generated/storage/types.gen/type-aliases/Message.md) |\n| `id` | `string` |\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`toJSON`](../../../entity/classes/FavoritableEntity.md#tojson)\n\n***\n\n### unfavorite()\n\n> **unfavorite**(): `Promise`\\<`Message`\\>\n\nRemove this entity from the user's favorites. Returns this handle for chaining.\n\n#### Returns\n\n`Promise`\\<`Message`\\>\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`unfavorite`](../../../entity/classes/FavoritableEntity.md#unfavorite)\n\n***\n\n### unreact()\n\n> **unreact**(`emoji`): `Promise`\\<`Message`\\>\n\nRemove one of your emoji reactions from this message.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `emoji` | `string` |\n\n#### Returns\n\n`Promise`\\<`Message`\\>\n\n***\n\n### byId()\n\n> `static` **byId**(`client`, `channelId`, `id`, `mentions?`): `Message`\n\nA handle to a message by id. Fields load on first access.\n\n#### Parameters\n\n| Parameter | Type | Default value |\n| ------ | ------ | ------ |\n| `client` | `MacroClient` | `undefined` |\n| `channelId` | `string` | `undefined` |\n| `id` | `string` | `undefined` |\n| `mentions?` | [`SimpleMention`](../../../../../generated/storage/types.gen/type-aliases/SimpleMention.md)[] | `[]` |\n\n#### Returns\n\n`Message`\n\n***\n\n### from()\n\n> `static` **from**(`client`, `channelId`, `data`): `Message`\n\nBuild a root or reply from a message record (pre-seeded, no fetch).\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `channelId` | `string` |\n| `data` | [`Message`](../../../../../generated/storage/types.gen/type-aliases/Message.md) |\n\n#### Returns\n\n`Message`\n\n\n\n# Type Alias: MessageData\n\n> **MessageData** = [`Message`](../../../../../generated/storage/types.gen/type-aliases/Message.md)\n\nThe API's representation of a message; the same record the document comment API returns.\n\n\n\n# Class: ChannelNamespace\n\n## Constructors\n\n### Constructor\n\n> **new ChannelNamespace**(`client`): `ChannelNamespace`\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n\n#### Returns\n\n`ChannelNamespace`\n\n## Properties\n\n| Property | Modifier | Type | Description |\n| ------ | ------ | ------ | ------ |\n| `labels` | `readonly` | [`ChannelLabelsNamespace`](../../labels/classes/ChannelLabelsNamespace.md) | Team-shared labels that group channels in the Chat sidebar. |\n\n## Methods\n\n### activity()\n\n> **activity**(): `Promise`\\<[`ChannelActivity`](../../activity/classes/ChannelActivity.md)[]\\>\n\nRecent channel activity, with channel and user handles.\n\n#### Returns\n\n`Promise`\\<[`ChannelActivity`](../../activity/classes/ChannelActivity.md)[]\\>\n\n***\n\n### byId()\n\n> **byId**(`id`): [`Channel`](../../channel/classes/Channel.md)\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `id` | `string` |\n\n#### Returns\n\n[`Channel`](../../channel/classes/Channel.md)\n\n***\n\n### create()\n\n> **create**(`opts`): `Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\nCreate a channel. The caller becomes the owner.\n\n#### Parameters\n\n| Parameter | Type | Description |\n| ------ | ------ | ------ |\n| `opts` | \\{ `name?`: `string`; `participants?`: [`User`](../../../users/user/classes/User.md)[]; `team?`: [`Team`](../../../teams/team/classes/Team.md); `type`: [`ChannelType`](../../../../../generated/storage/types.gen/type-aliases/ChannelType.md); \\} | - |\n| `opts.name?` | `string` | - |\n| `opts.participants?` | [`User`](../../../users/user/classes/User.md)[] | Participants to add, excluding the owner. |\n| `opts.team?` | [`Team`](../../../teams/team/classes/Team.md) | Team, for team channels. |\n| `opts.type` | [`ChannelType`](../../../../../generated/storage/types.gen/type-aliases/ChannelType.md) | - |\n\n#### Returns\n\n`Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\n***\n\n### dm()\n\n> **dm**(`recipient`): `Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `recipient` | [`User`](../../../users/user/classes/User.md) |\n\n#### Returns\n\n`Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\n***\n\n### list()\n\n> **list**(): `Promise`\\<[`Channel`](../../channel/classes/Channel.md)[]\\>\n\nAll channels the authenticated user is a member of.\n\n#### Returns\n\n`Promise`\\<[`Channel`](../../channel/classes/Channel.md)[]\\>\n\n***\n\n### private()\n\n> **private**(`recipients`): `Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\nOpen (creating if needed) the private group channel with a set of users.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `recipients` | [`User`](../../../users/user/classes/User.md)[] |\n\n#### Returns\n\n`Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\n***\n\n### search()\n\n> **search**(`query`, `opts?`): `AsyncGenerator`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\nSearch channels by name (client-side fuzzy filter on the full channel list)\nor by content (server-side unified search).\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `query` | `string` |\n| `opts?` | [`SearchOpts`](../../../search/interfaces/SearchOpts.md) |\n\n#### Returns\n\n`AsyncGenerator`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\n\n\n# Class: ChannelPicture\n\nAn uploaded channel profile picture in static file storage.\n\n## Properties\n\n| Property | Modifier | Type |\n| ------ | ------ | ------ |\n| `id` | `readonly` | `string` |\n\n## Methods\n\n### toJSON()\n\n> **toJSON**(): \\{ `id`: `string`; \\}\n\nSerialize the uploaded picture reference.\n\n#### Returns\n\n\\{ `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `id` | `string` |\n\n***\n\n### byId()\n\n> `static` **byId**(`id`): `ChannelPicture`\n\nConstruct a reference from the upload receipt.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `id` | `string` |\n\n#### Returns\n\n`ChannelPicture`\n\n\n\n# Function: postToChannel()\n\n> **postToChannel**(`client`, `target`, `body`): `Promise`\\<`string`\\>\n\nPost a message using the endpoint supported by the caller's credentials.\n\nMost credentials use `POST /messages/channel/{id}`. User-owned bots without\nan acting user instead use `POST /channels/{id}/webhook`, which authorizes\nthrough channel membership. The bot owner is read from `GET /bots/me` and\ncached by the client.\n\nThe webhook endpoint supports content only. Mentions in `` tags still\nwork, but threads and attachments are not supported.\n\nPosting to a `Thread` puts the message in that thread; to a `Channel`, at the\nchannel root. One target rather than a channel plus a thread id, so the two\ncan't disagree.\n\n## Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `target` | [`Channel`](../../channel/classes/Channel.md) \\| [`Thread`](../../thread/classes/Thread.md) |\n| `body` | [`RichMessage`](../../../../mentions/interfaces/RichMessage.md) |\n\n## Returns\n\n`Promise`\\<`string`\\>\n\n\n\n# Class: Thread\n\nA thread within a channel, rooted at a top-level message. Replies and\nthread-scoped posting live here.\n\n## Constructors\n\n### Constructor\n\n> **new Thread**(`client`, `channelId`, `rootId`): `Thread`\n\n#### Parameters\n\n| Parameter | Type | Description |\n| ------ | ------ | ------ |\n| `client` | `MacroClient` | - |\n| `channelId` | `string` | - |\n| `rootId` | `string` | The id of the message this thread hangs off. |\n\n#### Returns\n\n`Thread`\n\n## Properties\n\n| Property | Modifier | Type | Description |\n| ------ | ------ | ------ | ------ |\n| `channelId` | `readonly` | `string` | - |\n| `rootId` | `readonly` | `string` | The id of the message this thread hangs off. |\n\n## Methods\n\n### channel()\n\n> **channel**(): [`Channel`](../../channel/classes/Channel.md)\n\nThe channel this thread is in.\n\n#### Returns\n\n[`Channel`](../../channel/classes/Channel.md)\n\n***\n\n### replies()\n\n> **replies**(): `Promise`\\<[`Message`](../../message/classes/Message.md)[]\\>\n\nList the replies in this thread, oldest first.\n\n#### Returns\n\n`Promise`\\<[`Message`](../../message/classes/Message.md)[]\\>\n\n***\n\n### reply()\n\n> **reply**(`body`): `Promise`\\<[`Message`](../../message/classes/Message.md)\\>\n\nPost a reply in this thread.\n\n#### Parameters\n\n| Parameter | Type | Description |\n| ------ | ------ | ------ |\n| `body` | `string` \\| [`RichMessage`](../../../../mentions/interfaces/RichMessage.md) | Plain text, or a rich body composed with msg. |\n\n#### Returns\n\n`Promise`\\<[`Message`](../../message/classes/Message.md)\\>\n\nThe created reply.\n\n***\n\n### root()\n\n> **root**(): [`Message`](../../message/classes/Message.md)\n\nThe message this thread hangs off.\n\n#### Returns\n\n[`Message`](../../message/classes/Message.md)\n\n***\n\n### toJSON()\n\n> **toJSON**(): \\{ `channelId`: `string`; `rootId`: `string`; \\}\n\nSerialize the thread identity without its authenticated client.\n\n#### Returns\n\n\\{ `channelId`: `string`; `rootId`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `channelId` | `string` |\n| `rootId` | `string` |\n\n\n\n# Class: WhatsAppAccount\n\nThe current user's WhatsApp account, connected through the Macro app.\n\n## Extends\n\n- [`MacroEntity`](../../../entity/classes/MacroEntity.md)\\<[`LinkedAccount`](../../../../../generated/storage/types.gen/type-aliases/LinkedAccount.md)\\>\n\n## Properties\n\n| Property | Modifier | Type | Description | Inherited from |\n| ------ | ------ | ------ | ------ | ------ |\n| `id` | `public readonly` | `string` | - | [`MacroEntity`](../../../entity/classes/MacroEntity.md).[`id`](../../../entity/classes/MacroEntity.md#property-id) |\n| `identity` | `readonly` | () => `Promise`\\<`string` \\| `undefined`\\> | Verified remote identity, absent until pairing succeeds. | - |\n| `owner` | `readonly` | () => `Promise`\\<[`User`](../../../users/user/classes/User.md)\\> | The Macro user who owns this account. | - |\n| `state` | `readonly` | () => `Promise`\\<[`LinkedAccountState`](../../../../../generated/storage/types.gen/type-aliases/LinkedAccountState.md)\\> | Account connection state; call refresh before polling it again. | - |\n\n## Methods\n\n### refresh()\n\n> **refresh**(): `Promise`\\<`WhatsAppAccount`\\>\n\nDiscard cached connection state and load its current value.\n\n#### Returns\n\n`Promise`\\<`WhatsAppAccount`\\>\n\n***\n\n### toJSON()\n\n> **toJSON**(): \\{ `detail?`: [`LinkedAccount`](../../../../../generated/storage/types.gen/type-aliases/LinkedAccount.md); `id`: `string`; \\}\n\n#### Returns\n\n\\{ `detail?`: [`LinkedAccount`](../../../../../generated/storage/types.gen/type-aliases/LinkedAccount.md); `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `detail?` | [`LinkedAccount`](../../../../../generated/storage/types.gen/type-aliases/LinkedAccount.md) |\n| `id` | `string` |\n\n#### Inherited from\n\n[`MacroEntity`](../../../entity/classes/MacroEntity.md).[`toJSON`](../../../entity/classes/MacroEntity.md#tojson)\n\n***\n\n### byId()\n\n> `static` **byId**(`client`, `id`): `WhatsAppAccount`\n\nReference an account; reads still enforce the authenticated owner's scope.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `id` | `string` |\n\n#### Returns\n\n`WhatsAppAccount`\n\n***\n\n### from()\n\n> `static` **from**(`client`, `record`): `WhatsAppAccount`\n\nSeed an account handle from an API record.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `record` | [`LinkedAccount`](../../../../../generated/storage/types.gen/type-aliases/LinkedAccount.md) |\n\n#### Returns\n\n`WhatsAppAccount`\n\n\n\n# Class: WhatsAppChat\n\nOne chat of the current user's WhatsApp chat list: a group or a direct chat.\n\n## Extends\n\n- [`MacroEntity`](../../../entity/classes/MacroEntity.md)\\<[`LinkedChat`](../../../../../generated/storage/types.gen/type-aliases/LinkedChat.md)\\>\n\n## Properties\n\n| Property | Modifier | Type | Description | Inherited from |\n| ------ | ------ | ------ | ------ | ------ |\n| `archived` | `readonly` | () => `Promise`\\<`boolean`\\> | Archived in WhatsApp. | - |\n| `canAdd` | `readonly` | () => `Promise`\\<`boolean`\\> | Whether adding it to Macro works now. | - |\n| `cannotAddReason` | `readonly` | () => `Promise`\\<`string` \\| `undefined`\\> | Why it can't be added, when it is neither added, shared nor addable. | - |\n| `group` | `readonly` | () => `Promise`\\<[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) \\| `undefined`\\> | The current user's own Macro group for this chat, once added. | - |\n| `id` | `public readonly` | `string` | - | [`MacroEntity`](../../../entity/classes/MacroEntity.md).[`id`](../../../entity/classes/MacroEntity.md#property-id) |\n| `kind` | `readonly` | () => `Promise`\\<[`LinkedChatKind`](../../../../../generated/storage/types.gen/type-aliases/LinkedChatKind.md)\\> | `group` or `direct`. | - |\n| `lastActivityAt` | `readonly` | () => `Promise`\\<`string` \\| `undefined`\\> | Newest activity WhatsApp reported, when known. | - |\n| `name` | `readonly` | () => `Promise`\\<`string`\\> | Group subject (or member names), or the other person's name or number. | - |\n| `pinned` | `readonly` | () => `Promise`\\<`boolean`\\> | Pinned in WhatsApp. | - |\n| `shared` | `readonly` | () => `Promise`\\<[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) \\| `undefined`\\> | Someone else's Macro group for this chat, shared with the current user. | - |\n\n## Methods\n\n### adopt()\n\n> **adopt**(): `Promise`\\<[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)\\>\n\nAdd a group chat to Macro as the current user's own group; its earlier\nmessages are imported from the phone. Adding it again returns the same\ngroup. Direct chats can't be added yet.\n\n#### Returns\n\n`Promise`\\<[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)\\>\n\n***\n\n### refresh()\n\n> **refresh**(): `Promise`\\<`WhatsAppChat`\\>\n\nReload the chat's list entry.\n\n#### Returns\n\n`Promise`\\<`WhatsAppChat`\\>\n\n***\n\n### toJSON()\n\n> **toJSON**(): \\{ `detail?`: [`LinkedChat`](../../../../../generated/storage/types.gen/type-aliases/LinkedChat.md); `id`: `string`; \\}\n\n#### Returns\n\n\\{ `detail?`: [`LinkedChat`](../../../../../generated/storage/types.gen/type-aliases/LinkedChat.md); `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `detail?` | [`LinkedChat`](../../../../../generated/storage/types.gen/type-aliases/LinkedChat.md) |\n| `id` | `string` |\n\n#### Inherited from\n\n[`MacroEntity`](../../../entity/classes/MacroEntity.md).[`toJSON`](../../../entity/classes/MacroEntity.md#tojson)\n\n***\n\n### byId()\n\n> `static` **byId**(`client`, `id`): `WhatsAppChat`\n\nReference a chat by its WhatsApp JID.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `id` | `string` |\n\n#### Returns\n\n`WhatsAppChat`\n\n***\n\n### from()\n\n> `static` **from**(`client`, `record`): `WhatsAppChat`\n\nSeed a chat handle from the chat list.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `record` | [`LinkedChat`](../../../../../generated/storage/types.gen/type-aliases/LinkedChat.md) |\n\n#### Returns\n\n`WhatsAppChat`\n\n\n\n# Class: WhatsAppGroup\n\nIndependently shareable adopted WhatsApp group entity.\n\n## Extends\n\n- [`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md)\\<[`WhatsAppGroupDetail`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupDetail.md)\\>\n\n## Properties\n\n| Property | Modifier | Type | Description | Overrides | Inherited from |\n| ------ | ------ | ------ | ------ | ------ | ------ |\n| `backfill` | `readonly` | () => `Promise`\\<[`LinkedBackfill`](../../../../../generated/storage/types.gen/type-aliases/LinkedBackfill.md) \\| `undefined`\\> | Importing earlier messages from the owner's phone after adding, if any. | - | - |\n| `canSend` | `readonly` | () => `Promise`\\<`boolean`\\> | Whether the caller can send from their own connected account. | - | - |\n| `entityType` | `readonly` | `\"whatsapp_group\"` | Canonical sharing and favorites kind. | [`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`entityType`](../../../entity/classes/FavoritableEntity.md#property-entitytype) | - |\n| `id` | `public readonly` | `string` | - | - | [`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`id`](../../../entity/classes/FavoritableEntity.md#property-id) |\n| `limits` | `readonly` | () => `Promise`\\<[`LinkedCapabilities`](../../../../../generated/storage/types.gen/type-aliases/LinkedCapabilities.md)\\> | Supported sizes and mutation window. | - | - |\n| `name` | `readonly` | () => `Promise`\\<`string`\\> | Display name. | - | - |\n| `owner` | `readonly` | () => `Promise`\\<[`User`](../../../users/user/classes/User.md)\\> | Actual owner, independent of WhatsApp membership. | - | - |\n| `roster` | `readonly` | () => `Promise`\\<[`LinkedGroupInfo`](../../../../../generated/storage/types.gen/type-aliases/LinkedGroupInfo.md) \\| `undefined`\\> | Current remote roster, when available. | - | - |\n| `sendingIdentity` | `readonly` | () => `Promise`\\<`string` \\| `undefined`\\> | Caller's own verified sending identity. | - | - |\n| `sendReason` | `readonly` | () => `Promise`\\<`NonNullable`\\ \\| `undefined`\\> | Reason sending is blocked. | - | - |\n| `unreadCount` | `readonly` | () => `Promise`\\<`number`\\> | Per-user unread count. | - | - |\n\n## Methods\n\n### delivery()\n\n> **delivery**(`sends`): `Promise`\\<[`WhatsAppSend`](../../send/classes/WhatsAppSend.md)[]\\>\n\nRead delivery for selected caller-owned intents or messages.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `sends` | ([`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md) \\| [`WhatsAppSend`](../../send/classes/WhatsAppSend.md))[] |\n\n#### Returns\n\n`Promise`\\<[`WhatsAppSend`](../../send/classes/WhatsAppSend.md)[]\\>\n\n***\n\n### detach()\n\n> **detach**(): `Promise`\\<`void`\\>\n\nStop observation while preserving history and established sharing.\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### favorite()\n\n> **favorite**(): `Promise`\\<`WhatsAppGroup`\\>\n\nAdd this entity to the user's favorites. Returns this handle for chaining.\nPlain unwrap: favoriting alters the user's favorites collection, not this\nentity's own detail, so there's nothing cached to invalidate.\n\n#### Returns\n\n`Promise`\\<`WhatsAppGroup`\\>\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`favorite`](../../../entity/classes/FavoritableEntity.md#favorite)\n\n***\n\n### markRead()\n\n> **markRead**(`message`): `Promise`\\<`void`\\>\n\nAdvance the caller's own read cursor.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `message` | [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md) |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### messageAttachments()\n\n> **messageAttachments**(`messages`): `Promise`\\<\\{ `attachments`: \\{ `document`: [`Document`](../../../documents/document/classes/Document.md) \\| `undefined`; `fileName`: `string`; `mime`: `string`; `ordinal`: `number`; `size`: `number`; `state`: [`LinkedAttachmentState`](../../../../../generated/storage/types.gen/type-aliases/LinkedAttachmentState.md); \\}[]; `message`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); \\}[]\\>\n\nRead attachment states for selected messages.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `messages` | [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md)[] |\n\n#### Returns\n\n`Promise`\\<\\{ `attachments`: \\{ `document`: [`Document`](../../../documents/document/classes/Document.md) \\| `undefined`; `fileName`: `string`; `mime`: `string`; `ordinal`: `number`; `size`: `number`; `state`: [`LinkedAttachmentState`](../../../../../generated/storage/types.gen/type-aliases/LinkedAttachmentState.md); \\}[]; `message`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); \\}[]\\>\n\n***\n\n### messages()\n\n> **messages**(`options?`): `AsyncGenerator`\\<[`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md)\\>\n\nIterate newest-first timeline pages lazily.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `options?` | \\{ `limit?`: `number`; \\} |\n| `options.limit?` | `number` |\n\n#### Returns\n\n`AsyncGenerator`\\<[`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md)\\>\n\n***\n\n### permission()\n\n> **permission**(): `Promise`\\<\\{ `permission`: [`EntityPermission`](../../../../../generated/storage/types.gen/type-aliases/EntityPermission.md); `status`: `\"access\"`; \\} \\| \\{ `status`: `\"no_access\"`; \\}\\>\n\nRead the caller's effective permission on this entity.\n\n#### Returns\n\n`Promise`\\<\\{ `permission`: [`EntityPermission`](../../../../../generated/storage/types.gen/type-aliases/EntityPermission.md); `status`: `\"access\"`; \\} \\| \\{ `status`: `\"no_access\"`; \\}\\>\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`permission`](../../../entity/classes/FavoritableEntity.md#permission)\n\n***\n\n### permissions()\n\n> **permissions**(): `Promise`\\<[`SharePermissionV2`](../../../../../generated/storage/types.gen/type-aliases/SharePermissionV2.md)\\>\n\nRead standard link/team sharing settings.\n\n#### Returns\n\n`Promise`\\<[`SharePermissionV2`](../../../../../generated/storage/types.gen/type-aliases/SharePermissionV2.md)\\>\n\n***\n\n### refresh()\n\n> **refresh**(): `Promise`\\<`WhatsAppGroup`\\>\n\nReload detail, permissions and caller capabilities.\n\n#### Returns\n\n`Promise`\\<`WhatsAppGroup`\\>\n\n***\n\n### reply()\n\n> **reply**(`content`, `options`): `Promise`\\<[`WhatsAppSend`](../../send/classes/WhatsAppSend.md)\\>\n\nPersonal sessions and owner API credentials can send. Bots never send.\nPreserve the idempotency key; uncertain outcomes are never automatically resent.\nThe server enforces one request per second per user.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `content` | [`WhatsAppReply`](../type-aliases/WhatsAppReply.md) |\n| `options` | \\{ `idempotencyKey`: `string`; `replyTo?`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); \\} |\n| `options.idempotencyKey` | `string` |\n| `options.replyTo?` | [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md) |\n\n#### Returns\n\n`Promise`\\<[`WhatsAppSend`](../../send/classes/WhatsAppSend.md)\\>\n\n***\n\n### sends()\n\n> **sends**(): `Promise`\\<[`WhatsAppSend`](../../send/classes/WhatsAppSend.md)[]\\>\n\nRead only the caller's pending, uncertain or failed sends.\n\n#### Returns\n\n`Promise`\\<[`WhatsAppSend`](../../send/classes/WhatsAppSend.md)[]\\>\n\n***\n\n### toJSON()\n\n> **toJSON**(): \\{ `detail?`: [`WhatsAppGroupDetail`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupDetail.md); `id`: `string`; \\}\n\n#### Returns\n\n\\{ `detail?`: [`WhatsAppGroupDetail`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupDetail.md); `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `detail?` | [`WhatsAppGroupDetail`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupDetail.md) |\n| `id` | `string` |\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`toJSON`](../../../entity/classes/FavoritableEntity.md#tojson)\n\n***\n\n### unfavorite()\n\n> **unfavorite**(): `Promise`\\<`WhatsAppGroup`\\>\n\nRemove this entity from the user's favorites. Returns this handle for chaining.\n\n#### Returns\n\n`Promise`\\<`WhatsAppGroup`\\>\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`unfavorite`](../../../entity/classes/FavoritableEntity.md#unfavorite)\n\n***\n\n### updatePermissions()\n\n> **updatePermissions**(`update`): `Promise`\\<`void`\\>\n\nActual owner updates standard link/team sharing settings.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `update` | [`UpdateSharePermissionRequestV2`](../../../../../generated/storage/types.gen/type-aliases/UpdateSharePermissionRequestV2.md) |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### upload()\n\n> **upload**(`file`): `Promise`\\<[`WhatsAppUpload`](../../upload/classes/WhatsAppUpload.md)\\>\n\nStage a supported attachment for a deliberate send.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `file` | `Blob` \\| `File` |\n\n#### Returns\n\n`Promise`\\<[`WhatsAppUpload`](../../upload/classes/WhatsAppUpload.md)\\>\n\n***\n\n### byId()\n\n> `static` **byId**(`client`, `id`): `WhatsAppGroup`\n\nReference an adopted group; access is checked on each request.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `id` | `string` |\n\n#### Returns\n\n`WhatsAppGroup`\n\n\n\n# Type Alias: WhatsAppReply\n\n> **WhatsAppReply** = \\{ `text`: `string`; `type`: `\"text\"`; \\} \\| \\{ `caption?`: `string`; `type`: `\"media\"`; `upload`: [`WhatsAppUpload`](../../upload/classes/WhatsAppUpload.md); \\} \\| \\{ `message`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); `operation`: `\"reaction\"` \\| `\"edit\"` \\| `\"delete\"`; `text?`: `string`; `type`: `\"mutation\"`; \\}\n\nDeliberate content from the credential owner's own WhatsApp account.\n\n\n\n# Class: WhatsAppMessage\n\nMessage in an independently shared WhatsApp group.\n\n## Extends\n\n- [`MacroEntity`](../../../entity/classes/MacroEntity.md)\\<[`WhatsAppGroupMessage`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupMessage.md)\\>\n\n## Properties\n\n| Property | Modifier | Type | Description | Inherited from |\n| ------ | ------ | ------ | ------ | ------ |\n| `attachments` | `readonly` | () => `Promise`\\<\\{ `document`: [`Document`](../../../documents/document/classes/Document.md) \\| `undefined`; `fileName`: `string`; `mime`: `string`; `ordinal`: `number`; `size`: `number`; `state`: [`LinkedAttachmentState`](../../../../../generated/storage/types.gen/type-aliases/LinkedAttachmentState.md); \\}[]\\> | Attachment state and imported document handles. | - |\n| `body` | `readonly` | () => `Promise`\\<`string`\\> | Plain WhatsApp text. | - |\n| `deleted` | `readonly` | () => `Promise`\\<`boolean`\\> | Deleted for everyone. | - |\n| `deliveryState` | `readonly` | () => `Promise`\\<`NonNullable`\\ \\| `undefined`\\> | Caller-owned send's delivery state, when present. | - |\n| `editedAt` | `readonly` | () => `Promise`\\<`string` \\| `undefined`\\> | Last edit time. | - |\n| `id` | `public readonly` | `string` | - | [`MacroEntity`](../../../entity/classes/MacroEntity.md).[`id`](../../../entity/classes/MacroEntity.md#property-id) |\n| `reactions` | `readonly` | () => `Promise`\\<[`WhatsAppGroupReaction`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupReaction.md)[]\\> | Grouped emoji counts and caller's own reaction. | - |\n| `replyTo` | `readonly` | () => `Promise`\\<`WhatsAppMessage` \\| `undefined`\\> | Quoted message in this group. | - |\n| `sender` | `readonly` | () => `Promise`\\<[`WhatsAppGroupSender`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupSender.md)\\> | Protocol-verified sender association and display name. | - |\n| `sentAt` | `readonly` | () => `Promise`\\<`string`\\> | WhatsApp timestamp. | - |\n\n## Accessors\n\n### groupId\n\n#### Get Signature\n\n> **get** **groupId**(): `string`\n\nGroup identity for checking reply/mutation scope.\n\n##### Returns\n\n`string`\n\n## Methods\n\n### toJSON()\n\n> **toJSON**(): \\{ `detail?`: [`WhatsAppGroupMessage`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupMessage.md); `id`: `string`; \\}\n\n#### Returns\n\n\\{ `detail?`: [`WhatsAppGroupMessage`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupMessage.md); `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `detail?` | [`WhatsAppGroupMessage`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupMessage.md) |\n| `id` | `string` |\n\n#### Inherited from\n\n[`MacroEntity`](../../../entity/classes/MacroEntity.md).[`toJSON`](../../../entity/classes/MacroEntity.md#tojson)\n\n***\n\n### byId()\n\n> `static` **byId**(`client`, `group`, `id`): `WhatsAppMessage`\n\nReference a message within its original group.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `group` | [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) |\n| `id` | `string` |\n\n#### Returns\n\n`WhatsAppMessage`\n\n***\n\n### from()\n\n> `static` **from**(`client`, `group`, `record`): `WhatsAppMessage`\n\nSeed a message handle from a timeline response.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `group` | [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) |\n| `record` | [`WhatsAppGroupMessage`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupMessage.md) |\n\n#### Returns\n\n`WhatsAppMessage`\n\n\n\n# Class: WhatsAppNamespace\n\nOperate on an account already paired through the Macro app.\n\n## Constructors\n\n### Constructor\n\n> **new WhatsAppNamespace**(`client`): `WhatsAppNamespace`\n\nBind existing-account operations to this authenticated client.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n\n#### Returns\n\n`WhatsAppNamespace`\n\n## Methods\n\n### account()\n\n> **account**(): `Promise`\\<[`WhatsAppAccount`](../../account/classes/WhatsAppAccount.md) \\| `undefined`\\>\n\nCurrent owner's account, or undefined when no account has been connected.\n\n#### Returns\n\n`Promise`\\<[`WhatsAppAccount`](../../account/classes/WhatsAppAccount.md) \\| `undefined`\\>\n\n***\n\n### byId()\n\n> **byId**(`id`): [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)\n\nReference an adopted entity without fetching it.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `id` | `string` |\n\n#### Returns\n\n[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)\n\n***\n\n### groups()\n\n> **groups**(): `Promise`\\<[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)[]\\>\n\nAdopted groups shared with the caller, including without WhatsApp.\n\n#### Returns\n\n`Promise`\\<[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)[]\\>\n\n\n\n# Class: WhatsAppSend\n\nOne deliberate send intent. Unknown outcomes must never be automatically resent.\n\n## Extends\n\n- [`MacroEntity`](../../../entity/classes/MacroEntity.md)\\<[`LinkedSendProgress`](../../../../../generated/storage/types.gen/type-aliases/LinkedSendProgress.md)\\>\n\n## Properties\n\n| Property | Modifier | Type | Description | Inherited from |\n| ------ | ------ | ------ | ------ | ------ |\n| `createdAt` | `readonly` | () => `Promise`\\<`string`\\> | When the send intent was created. | - |\n| `errorCode` | `readonly` | () => `Promise`\\<`string` \\| `undefined`\\> | Sanitized failure code, when present. | - |\n| `group` | `readonly` | [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) | Group entity containing this send. | - |\n| `id` | `public readonly` | `string` | - | [`MacroEntity`](../../../entity/classes/MacroEntity.md).[`id`](../../../entity/classes/MacroEntity.md#property-id) |\n| `message` | `readonly` | () => `Promise`\\<[`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md) \\| `undefined`\\> | Group message once WhatsApp accepted the send. | - |\n| `state` | `readonly` | () => `Promise`\\<[`LinkedDeliveryState`](../../../../../generated/storage/types.gen/type-aliases/LinkedDeliveryState.md)\\> | Last observed delivery state; refresh explicitly to poll again. | - |\n\n## Methods\n\n### refresh()\n\n> **refresh**(): `Promise`\\<`WhatsAppSend`\\>\n\nFetch current delivery state without retrying or submitting the message.\n\n#### Returns\n\n`Promise`\\<`WhatsAppSend`\\>\n\n***\n\n### request()\n\n> **request**(): `Promise`\\<\\{ `idempotencyKey`: `string`; `message`: [`WhatsAppReply`](../../group/type-aliases/WhatsAppReply.md); `replyTo?`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); \\}\\>\n\nInspect the original intent for reconciliation; never resubmit an unknown outcome automatically.\n\n#### Returns\n\n`Promise`\\<\\{ `idempotencyKey`: `string`; `message`: [`WhatsAppReply`](../../group/type-aliases/WhatsAppReply.md); `replyTo?`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); \\}\\>\n\n***\n\n### toJSON()\n\n> **toJSON**(): \\{ `detail?`: [`LinkedSendProgress`](../../../../../generated/storage/types.gen/type-aliases/LinkedSendProgress.md); `id`: `string`; \\}\n\n#### Returns\n\n\\{ `detail?`: [`LinkedSendProgress`](../../../../../generated/storage/types.gen/type-aliases/LinkedSendProgress.md); `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `detail?` | [`LinkedSendProgress`](../../../../../generated/storage/types.gen/type-aliases/LinkedSendProgress.md) |\n| `id` | `string` |\n\n#### Inherited from\n\n[`MacroEntity`](../../../entity/classes/MacroEntity.md).[`toJSON`](../../../entity/classes/MacroEntity.md#tojson)\n\n***\n\n### byId()\n\n> `static` **byId**(`client`, `group`, `id`): `WhatsAppSend`\n\nReference a send intent within its original group.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `group` | [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) |\n| `id` | `string` |\n\n#### Returns\n\n`WhatsAppSend`\n\n***\n\n### from()\n\n> `static` **from**(`client`, `group`, `record`): `WhatsAppSend`\n\nSeed a send handle from a server response.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `group` | [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) |\n| `record` | [`LinkedSendProgress`](../../../../../generated/storage/types.gen/type-aliases/LinkedSendProgress.md) |\n\n#### Returns\n\n`WhatsAppSend`\n\n\n\n# Class: WhatsAppUpload\n\nA staged attachment scoped to one adopted group; use it in a deliberate media send.\n\n## Properties\n\n| Property | Modifier | Type | Description |\n| ------ | ------ | ------ | ------ |\n| `filename?` | `readonly` | `string` | Display filename supplied by the server. |\n| `groupId` | `readonly` | `string` | The group this upload can be sent to. |\n| `id` | `readonly` | `string` | Correlation identity of the staged upload. |\n| `kind?` | `readonly` | [`LinkedMediaKind`](../../../../../generated/storage/types.gen/type-aliases/LinkedMediaKind.md) | Media category supplied by the server. |\n\n## Methods\n\n### toJSON()\n\n> **toJSON**(): \\{ `filename`: `string` \\| `undefined`; `groupId`: `string`; `id`: `string`; `kind`: [`LinkedMediaKind`](../../../../../generated/storage/types.gen/type-aliases/LinkedMediaKind.md) \\| `undefined`; \\}\n\nSerialize the upload reference without credentials.\n\n#### Returns\n\n\\{ `filename`: `string` \\| `undefined`; `groupId`: `string`; `id`: `string`; `kind`: [`LinkedMediaKind`](../../../../../generated/storage/types.gen/type-aliases/LinkedMediaKind.md) \\| `undefined`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `filename` | `string` \\| `undefined` |\n| `groupId` | `string` |\n| `id` | `string` |\n| `kind` | [`LinkedMediaKind`](../../../../../generated/storage/types.gen/type-aliases/LinkedMediaKind.md) \\| `undefined` |\n\n***\n\n### byId()\n\n> `static` **byId**(`group`, `id`): `WhatsAppUpload`\n\nReference a previously staged upload in its original group.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `group` | [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) |\n| `id` | `string` |\n\n#### Returns\n\n`WhatsAppUpload`\n\n***\n\n### from()\n\n> `static` **from**(`group`, `record`): `WhatsAppUpload`\n\nSeed a handle from the server's upload receipt.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `group` | [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) |\n| `record` | [`LinkedUploadedAttachment`](../../../../../generated/storage/types.gen/type-aliases/LinkedUploadedAttachment.md) |\n\n#### Returns\n\n`WhatsAppUpload`\n"
+ "documentation": "# Macro SDK: channels\n\nGenerated by TypeDoc. Paths below refer to the canonical Markdown hierarchy. Other entity handles are documented in their named section; request core for shared SDK helpers.\n\n\n# Type Alias: BulkUpdateEntityPropertyOptionsResponse\n\n> **BulkUpdateEntityPropertyOptionsResponse** = \\{ `properties`: [`EntityPropertyOptionSelectionResponse`](EntityPropertyOptionSelectionResponse.md)[]; \\}\n\nResponse for a bulk option update: each property's reconciled final ids.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `properties` | [`EntityPropertyOptionSelectionResponse`](EntityPropertyOptionSelectionResponse.md)[] | The final option ids per updated property. |\n\n\n\n# Type Alias: DataType\n\n> **DataType** = `\"BOOLEAN\"` \\| `\"DATE\"` \\| `\"NUMBER\"` \\| `\"STRING\"` \\| `\"SELECT_NUMBER\"` \\| `\"SELECT_STRING\"` \\| `\"TAG\"` \\| `\"ENTITY\"` \\| `\"LINK\"`\n\nData type for property values, determining storage and validation.\n\n\n\n# Type Alias: EntityProperty\n\n> **EntityProperty** = \\{ `created_at`: `string`; `entity_id`: `string`; `entity_type`: [`EntityType`](EntityType.md); `id`: `string`; `property_definition_id`: `string`; `updated_at`: `string`; \\}\n\nAssignment of a property definition to a specific entity (service representation).\n\n## Properties\n\n| Property | Type |\n| ------ | ------ |\n| `created_at` | `string` |\n| `entity_id` | `string` |\n| `entity_type` | [`EntityType`](EntityType.md) |\n| `id` | `string` |\n| `property_definition_id` | `string` |\n| `updated_at` | `string` |\n\n\n\n# Type Alias: EntityPropertyOptionSelectionResponse\n\n> **EntityPropertyOptionSelectionResponse** = \\{ `option_ids`: `string`[]; `property_id`: `string`; \\}\n\nThe reconciled final option ids stored for one property after a bulk update.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `option_ids` | `string`[] | The final option ids the server persisted, in stored order. |\n| `property_id` | `string` | The property definition the options belong to. |\n\n\n\n# Type Alias: EntityPropertyWithDefinition\n\n> **EntityPropertyWithDefinition** = \\{ `definition`: [`PropertyDefinition`](PropertyDefinition.md); `options?`: [`PropertyOption`](PropertyOption.md)[] \\| `null`; `property`: [`EntityProperty`](EntityProperty.md); `value?`: `null` \\| [`PropertyValue`](PropertyValue.md); \\}\n\nEntity property with its definition, value, and options (service representation).\n\n## Properties\n\n| Property | Type |\n| ------ | ------ |\n| `definition` | [`PropertyDefinition`](PropertyDefinition.md) |\n| `options?` | [`PropertyOption`](PropertyOption.md)[] \\| `null` |\n| `property` | [`EntityProperty`](EntityProperty.md) |\n| `value?` | `null` \\| [`PropertyValue`](PropertyValue.md) |\n\n\n\n# Type Alias: EntityReference\n\n> **EntityReference** = \\{ `entity_id`: `string`; `entity_type`: [`EntityType`](EntityType.md); `specific_message_id?`: `string` \\| `null`; \\}\n\nEntity reference for entity-type property values.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `entity_id` | `string` | - |\n| `entity_type` | [`EntityType`](EntityType.md) | - |\n| `specific_message_id?` | `string` \\| `null` | For CHANNEL, CHAT, THREAD entity types - optional specific message ID. This allows referencing a specific message within a thread/channel/chat. |\n\n\n\n# Type Alias: EntityType\n\n> **EntityType** = `\"CALENDAR_EVENT\"` \\| `\"CALL_RECORD\"` \\| `\"CHANNEL\"` \\| `\"CHAT\"` \\| `\"COMPANY\"` \\| `\"DATABASE_ROW\"` \\| `\"CONTACT\"` \\| `\"DOCUMENT\"` \\| `\"INITIATIVE\"` \\| `\"PROJECT\"` \\| `\"TASK\"` \\| `\"THREAD\"` \\| `\"USER\"`\n\nType of entity that can be referenced by entity properties.\n\n\n\n# Type Alias: PropertyDefinition\n\n> **PropertyDefinition** = \\{ `created_at`: `string`; `data_type`: [`DataType`](DataType.md); `display_name`: `string`; `id`: `string`; `is_metadata`: `boolean`; `is_multi_select`: `boolean`; `is_system`: `boolean`; `owner`: [`PropertyOwner`](PropertyOwner.md); `specific_entity_type`: `null` \\| [`EntityType`](EntityType.md); `updated_at`: `string`; \\}\n\nProperty definition model (service representation).\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `created_at` | `string` | - |\n| `data_type` | [`DataType`](DataType.md) | - |\n| `display_name` | `string` | - |\n| `id` | `string` | - |\n| `is_metadata` | `boolean` | Flag to indicate if this is a system-generated metadata property. Not stored in database - computed at service layer. |\n| `is_multi_select` | `boolean` | - |\n| `is_system` | `boolean` | Flag to indicate if this is a system property (stored in DB). |\n| `owner` | [`PropertyOwner`](PropertyOwner.md) | - |\n| `specific_entity_type` | `null` \\| [`EntityType`](EntityType.md) | - |\n| `updated_at` | `string` | - |\n\n\n\n# Type Alias: PropertyOption\n\n> **PropertyOption** = \\{ `color`: `string` \\| `null`; `created_at`: `string`; `display_order`: `number`; `id`: `string`; `property_definition_id`: `string`; `updated_at`: `string`; `value`: [`PropertyOptionValue`](PropertyOptionValue.md); \\}\n\nA selectable option for select-type properties (service representation).\n\n## Properties\n\n| Property | Type |\n| ------ | ------ |\n| `color` | `string` \\| `null` |\n| `created_at` | `string` |\n| `display_order` | `number` |\n| `id` | `string` |\n| `property_definition_id` | `string` |\n| `updated_at` | `string` |\n| `value` | [`PropertyOptionValue`](PropertyOptionValue.md) |\n\n\n\n# Type Alias: PropertyOptionValue\n\n> **PropertyOptionValue** = \\{ `type`: `\"string\"`; `value`: `string`; \\} \\| \\{ `type`: `\"number\"`; `value`: `number`; \\}\n\nThe value of a property option - either a string or a number.\n\n## Union Members\n\n### Type Literal\n\n\\{ `type`: `\"string\"`; `value`: `string`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"string\"` | - |\n| `value` | `string` | String value for SelectString properties |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"number\"`; `value`: `number`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"number\"` | - |\n| `value` | `number` | Number value for SelectNumber properties |\n\n\n\n# Type Alias: PropertyOwner\n\n> **PropertyOwner** = \\{ `scope`: `\"user\"`; `user_id`: `string`; \\} \\| \\{ `scope`: `\"team\"`; `team_id`: `string`; \\} \\| \\{ `database_id`: `string`; `scope`: `\"database\"`; \\} \\| \\{ `scope`: `\"system\"`; \\}\n\nDefines who owns a property - user-scoped, team-scoped, database-scoped, or system.\n\n\n\n# Type Alias: PropertyTargetEntityType\n\n> **PropertyTargetEntityType** = `\"CALL_RECORD\"` \\| `\"CHANNEL\"` \\| `\"CHAT\"` \\| `\"COMPANY\"` \\| `\"DATABASE_ROW\"` \\| `\"DOCUMENT\"` \\| `\"INITIATIVE\"` \\| `\"PROJECT\"` \\| `\"THREAD\"` \\| `\"USER\"`\n\nCanonical type of an entity receiving properties.\n\nTasks are documents at API boundaries. `Task` intentionally does not exist\nhere; task classification is resolved by the properties domain from the\ndocument subtype.\n\n\n\n# Type Alias: PropertyTargetReference\n\n> **PropertyTargetReference** = \\{ `entity_id`: `string`; `entity_type`: [`PropertyTargetEntityType`](PropertyTargetEntityType.md); \\}\n\nCanonical reference to an entity receiving properties.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `entity_id` | `string` | Entity identifier. |\n| `entity_type` | [`PropertyTargetEntityType`](PropertyTargetEntityType.md) | Canonical entity type. |\n\n\n\n# Type Alias: PropertyValue\n\n> **PropertyValue** = \\{ `type`: `\"Boolean\"`; `value`: `boolean`; \\} \\| \\{ `type`: `\"Number\"`; `value`: `number`; \\} \\| \\{ `type`: `\"String\"`; `value`: `string`; \\} \\| \\{ `type`: `\"Date\"`; `value`: `string`; \\} \\| \\{ `type`: `\"SelectOption\"`; `value`: `string`[]; \\} \\| \\{ `type`: `\"EntityReference\"`; `value`: [`EntityReference`](EntityReference.md)[]; \\} \\| \\{ `type`: `\"Link\"`; `value`: `string`[]; \\}\n\nProperty value (service representation).\n\nRepresents the actual value stored for an entity property.\nThis is serialized to/from JSONB in the database.\n\n## Union Members\n\n### Type Literal\n\n\\{ `type`: `\"Boolean\"`; `value`: `boolean`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"Boolean\"` | - |\n| `value` | `boolean` | Boolean value Serializes as: {\"type\": \"Boolean\", \"value\": true} |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"Number\"`; `value`: `number`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"Number\"` | - |\n| `value` | `number` | Numeric value Serializes as: {\"type\": \"Number\", \"value\": 42.5} |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"String\"`; `value`: `string`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"String\"` | - |\n| `value` | `string` | String value Serializes as: {\"type\": \"String\", \"value\": \"text\"} |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"Date\"`; `value`: `string`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"Date\"` | - |\n| `value` | `string` | Date/timestamp value Serializes as: {\"type\": \"Date\", \"value\": \"2025-01-01T00:00:00Z\"} |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"SelectOption\"`; `value`: `string`[]; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"SelectOption\"` | - |\n| `value` | `string`[] | Select option(s) - always an array (check is_multi_select to determine if single or multi) Single-select: {\"type\": \"SelectOption\", \"value\": [\"uuid\"]} (length 0 or 1) Multi-select: {\"type\": \"SelectOption\", \"value\": [\"uuid1\", \"uuid2\", ...]} (length 0+) |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"EntityReference\"`; `value`: [`EntityReference`](EntityReference.md)[]; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"EntityReference\"` | - |\n| `value` | [`EntityReference`](EntityReference.md)[] | Entity reference(s) - always an array (check is_multi_select to determine if single or multi) Single-select: {\"type\": \"EntityReference\", \"value\": [{...}]} (length 0 or 1) Multi-select: {\"type\": \"EntityReference\", \"value\": [{...}, {...}, ...]} (length 0+) |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"Link\"`; `value`: `string`[]; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `type` | `\"Link\"` | - |\n| `value` | `string`[] | Link value(s) - always an array (check is_multi_select to determine if single or multi) Single-select: {\"type\": \"Link\", \"value\": [\"https://example.com\"]} (length 0 or 1) Multi-select: {\"type\": \"Link\", \"value\": [\"https://example.com\", \"https://other.com\"]} (length 0+) |\n\n\n\n# Type Alias: SetPropertyValue\n\n> **SetPropertyValue** = \\{ `type`: `\"boolean\"`; `value`: `boolean`; \\} \\| \\{ `type`: `\"date\"`; `value`: `string`; \\} \\| \\{ `type`: `\"number\"`; `value`: `number`; \\} \\| \\{ `type`: `\"string\"`; `value`: `string`; \\} \\| \\{ `option_id`: `string`; `type`: `\"select_option\"`; \\} \\| \\{ `option_ids`: `string`[]; `type`: `\"multi_select_option\"`; \\} \\| \\{ `reference`: [`EntityReference`](EntityReference.md); `type`: `\"entity_reference\"`; \\} \\| \\{ `references`: [`EntityReference`](EntityReference.md)[]; `type`: `\"multi_entity_reference\"`; \\} \\| \\{ `type`: `\"link\"`; `url`: `string`; \\} \\| \\{ `type`: `\"multi_link\"`; `urls`: `string`[]; \\}\n\nType-safe enum for setting entity property values - provides compile-time validation.\n\n\n\n# Type Alias: AccessLevel\n\n> **AccessLevel** = `\"view\"` \\| `\"comment\"` \\| `\"edit\"` \\| `\"owner\"`\n\nOrdered from least to most access top -> bottom\n\n\n\n# Type Alias: ActivityType\n\n> **ActivityType** = `\"view\"` \\| `\"interact\"`\n\nThe kind of activity a user performs in a channel.\n\n\n\n# Type Alias: ApiActivity\n\n> **ApiActivity** = \\{ `channel_id`: `string`; `created_at`: `string`; `id`: `string`; `interacted_at?`: `string` \\| `null`; `updated_at`: `string`; `user_id`: `string`; `viewed_at?`: `string` \\| `null`; \\}\n\nA user's activity (view/interaction) within a channel.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channel_id` | `string` | Channel id. |\n| `created_at` | `string` | When the activity row was created. |\n| `id` | `string` | Activity id. |\n| `interacted_at?` | `string` \\| `null` | The last time the user interacted with the channel. |\n| `updated_at` | `string` | When the activity row was last updated. |\n| `user_id` | `string` | User id. |\n| `viewed_at?` | `string` \\| `null` | The last time the user viewed the channel. |\n\n\n\n# Type Alias: ApiChannelAttachment\n\n> **ApiChannelAttachment** = \\{ `channel_id`: `string`; `created_at`: `string`; `entity_id`: `string`; `entity_type`: `string`; `height?`: `number` \\| `null`; `id`: `string`; `message_id`: `string`; `sender_id`: `string`; `width?`: `number` \\| `null`; \\}\n\nA channel-level attachment.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channel_id` | `string` | Channel id. |\n| `created_at` | `string` | When the attachment was created. |\n| `entity_id` | `string` | Entity id. |\n| `entity_type` | `string` | Type of entity. |\n| `height?` | `number` \\| `null` | Height (for images). |\n| `id` | `string` | Attachment id. |\n| `message_id` | `string` | Message id this attachment belongs to. |\n| `sender_id` | `string` | The user who sent the message containing this attachment. |\n| `width?` | `number` \\| `null` | Width (for images). |\n\n\n\n# Type Alias: ApiChannelAttachmentsPage\n\n> **ApiChannelAttachmentsPage** = \\{ `items`: [`ApiChannelAttachment`](ApiChannelAttachment.md)[]; `next_cursor?`: `string` \\| `null`; \\}\n\nPaginated response of channel attachments.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `items` | [`ApiChannelAttachment`](ApiChannelAttachment.md)[] | Attachments on this page. |\n| `next_cursor?` | `string` \\| `null` | Cursor for the next page, null if no more pages. |\n\n\n\n# Type Alias: ApiChannelDetail\n\n> **ApiChannelDetail** = \\{ `channel_id`: `string`; `channel_name`: `string`; `channel_type`: [`ChannelType`](ChannelType.md); `participants`: [`ApiChannelParticipant`](ApiChannelParticipant.md)[]; \\}\n\nChannel detail: metadata and active participants.\n\nMessages are read through `/messages/channel/{channel_id}`.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channel_id` | `string` | Channel id. |\n| `channel_name` | `string` | Resolved display name from the viewer's perspective. |\n| `channel_type` | [`ChannelType`](ChannelType.md) | Channel type. |\n| `participants` | [`ApiChannelParticipant`](ApiChannelParticipant.md)[] | Active participants. |\n\n\n\n# Type Alias: ApiChannelParticipant\n\n> **ApiChannelParticipant** = \\{ `channel_id`: `string`; `joined_at`: `string`; `role`: [`ApiParticipantRole`](ApiParticipantRole.md); `user_id`: `string`; \\}\n\nA channel participant.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channel_id` | `string` | Channel id. |\n| `joined_at` | `string` | When the user joined. |\n| `role` | [`ApiParticipantRole`](ApiParticipantRole.md) | Role in the channel. |\n| `user_id` | `string` | User id. |\n\n\n\n# Type Alias: ApiParticipantRole\n\n> **ApiParticipantRole** = `\"owner\"` \\| `\"admin\"` \\| `\"member\"`\n\nParticipant role in a channel.\n\n\n\n# Type Alias: BotSenderProfile\n\n> **BotSenderProfile** = \\{ `avatar_url?`: `string` \\| `null`; `name`: `string`; \\}\n\nPublic bot profile attached to bot-authored messages.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `avatar_url?` | `string` \\| `null` | Bot avatar URL. |\n| `name` | `string` | Bot display name. |\n\n\n\n# Type Alias: ChannelLabel\n\n> **ChannelLabel** = \\{ `channelCount`: `number`; `channelIds`: `string`[]; `createdAt`: `string`; `id`: `string`; `name`: `string`; `rule?`: `null` \\| [`ChannelLabelRule`](ChannelLabelRule.md); `sortOrder`: `number`; `teamId?`: `string` \\| `null`; `updatedAt`: `string`; \\}\n\nA shared or account-private label grouping chat channels in the sidebar.\n\n`channel_ids` is viewer-relative: it lists only the labelled channels the\nrequesting user participates in. `channel_count` counts every channel in\nthe label so clients can warn accurately before a delete.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channelCount` | `number` | All assignments for a manual label; visible matches for a smart tag. |\n| `channelIds` | `string`[] | Channels in this label that the requesting user participates in. |\n| `createdAt` | `string` | When the label was created. |\n| `id` | `string` | Stable label id. |\n| `name` | `string` | Display name, unique within the scope (case-insensitive). |\n| `rule?` | `null` \\| [`ChannelLabelRule`](ChannelLabelRule.md) | - |\n| `sortOrder` | `number` | Manual ordering value within the scope; lower sorts first. |\n| `teamId?` | `string` \\| `null` | Owning team, or `None` for account-private labels. |\n| `updatedAt` | `string` | When the label was last renamed or reordered. |\n\n\n\n# Type Alias: ChannelLabelRule\n\n> **ChannelLabelRule** = \\{ `attribute`: `\"name\"`; `contains`: `string`; \\}\n\nCase-insensitive, literal substring matching on the channel name.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `attribute` | `\"name\"` | - |\n| `contains` | `string` | The substring to find anywhere in the name. |\n\n\n\n# Type Alias: ChannelSharePermission\n\n> **ChannelSharePermission** = \\{ `access_level`: [`AccessLevel`](AccessLevel.md); `channel_id`: `string`; \\}\n\nThe channel share permission\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `access_level` | [`AccessLevel`](AccessLevel.md) | The access level for the channel |\n| `channel_id` | `string` | The channel id |\n\n\n\n# Type Alias: ChannelType\n\n> **ChannelType** = `\"public\"` \\| `\"private\"` \\| `\"direct_message\"` \\| `\"team\"`\n\nType of channel.\n\n\n\n# Type Alias: CountedReaction\n\n> **CountedReaction** = \\{ `emoji`: `string`; `users`: `string`[]; \\}\n\nReaction emoji and the users who added it.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `emoji` | `string` | Emoji being reacted with. |\n| `users` | `string`[] | User identifiers. |\n\n\n\n# Type Alias: DocumentId\n\n> **DocumentId** = `string`\n\nA validated document identifier. Historical document ids need not be UUIDs.\n\n\n\n# Type Alias: EntityPermission\n\n> **EntityPermission** = \\{ `access_level`: [`AccessLevel`](AccessLevel.md); `type`: `\"access_level\"`; \\} \\| \\{ `type`: `\"channel_view_only\"`; \\} \\| \\{ `role`: [`ParticipantRole`](ParticipantRole.md); `type`: `\"channel_role\"`; \\} \\| \\{ `role`: [`TeamRole`](TeamRole.md); `type`: `\"team_role\"`; \\}\n\nA user's permission for an entity, discriminated by entity kind.\n\nItems (documents, chats, projects, threads) use access levels.\nChannels use view-only permission or participant roles.\n\n## Union Members\n\n### Type Literal\n\n\\{ `access_level`: [`AccessLevel`](AccessLevel.md); `type`: `\"access_level\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `access_level` | [`AccessLevel`](AccessLevel.md) | The access level the user has. |\n| `type` | `\"access_level\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `type`: `\"channel_view_only\"`; \\}\n\n***\n\n### Type Literal\n\n\\{ `role`: [`ParticipantRole`](ParticipantRole.md); `type`: `\"channel_role\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `role` | [`ParticipantRole`](ParticipantRole.md) | The role the user has in the channel. |\n| `type` | `\"channel_role\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `role`: [`TeamRole`](TeamRole.md); `type`: `\"team_role\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `role` | [`TeamRole`](TeamRole.md) | The role the user has in the team. |\n| `type` | `\"team_role\"` | - |\n\n\n\n# Type Alias: GetChannelAttachmentsResponses\n\n> **GetChannelAttachmentsResponses** = \\{ `200`: [`ApiChannelAttachmentsPage`](ApiChannelAttachmentsPage.md); \\}\n\n## Properties\n\n| Property | Type |\n| ------ | ------ |\n| `200` | [`ApiChannelAttachmentsPage`](ApiChannelAttachmentsPage.md) |\n\n\n\n# Type Alias: GetChannelParticipantsResponses\n\n> **GetChannelParticipantsResponses** = \\{ `200`: [`ApiChannelParticipant`](ApiChannelParticipant.md)[]; \\}\n\n## Properties\n\n| Property | Type |\n| ------ | ------ |\n| `200` | [`ApiChannelParticipant`](ApiChannelParticipant.md)[] |\n\n\n\n# Type Alias: ImportedAuthor\n\n> **ImportedAuthor** = \\{ `name`: `string`; \\}\n\nDisplay attribution for a comment imported from an external document.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `name` | `string` | Original author text; never interpreted as an authenticated principal. |\n\n\n\n# Type Alias: LinkShare\n\n> **LinkShare** = `\"PUBLIC\"` \\| `\"TEAM\"`\n\nDefines who can access an item through its share link.\n\n\n\n# Type Alias: LinkedAccount\n\n> **LinkedAccount** = \\{ `authorization_generation`: `number`; `id`: `string`; `identity_jid?`: `string` \\| `null`; `state`: [`LinkedAccountState`](LinkedAccountState.md); `user_id`: `string`; \\}\n\nOne person's account, never owned by a workspace.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `authorization_generation` | `number` | Explicit consent generation; reconnecting a socket never changes it. |\n| `id` | `string` | Stable account identity. |\n| `identity_jid?` | `string` \\| `null` | WhatsApp-verified identity, absent until the first pairing succeeds. |\n| `state` | [`LinkedAccountState`](LinkedAccountState.md) | Lifecycle. |\n| `user_id` | `string` | Authenticated owner. |\n\n\n\n# Type Alias: LinkedAccountState\n\n> **LinkedAccountState** = `\"pairing\"` \\| `\"connected\"` \\| `\"logged_out\"` \\| `\"disconnected\"`\n\nAccount lifecycle. Transport outages are not stored: a connected account\nstays connected while its session service reconnects.\n\n\n\n# Type Alias: LinkedAttachment\n\n> **LinkedAttachment** = \\{ `document_id?`: `string` \\| `null`; `duration_seconds?`: `number` \\| `null`; `file_name`: `string`; `height?`: `number` \\| `null`; `mime`: `string`; `ordinal`: `number`; `presentation?`: `null` \\| [`LinkedMediaPresentation`](LinkedMediaPresentation.md); `size`: `number`; `state`: [`LinkedAttachmentState`](LinkedAttachmentState.md); `width?`: `number` \\| `null`; \\}\n\nOne attachment of a linked message.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `document_id?` | `string` \\| `null` | The private document, once ready. |\n| `duration_seconds?` | `number` \\| `null` | Length of audio or video in whole seconds, when declared. |\n| `file_name` | `string` | Display filename. |\n| `height?` | `number` \\| `null` | Pixel height, when declared. |\n| `mime` | `string` | Declared MIME type. |\n| `ordinal` | `number` | Position within the message. |\n| `presentation?` | `null` \\| [`LinkedMediaPresentation`](LinkedMediaPresentation.md) | - |\n| `size` | `number` | Declared size in bytes. |\n| `state` | [`LinkedAttachmentState`](LinkedAttachmentState.md) | Current state. |\n| `width?` | `number` \\| `null` | Pixel width of an image, sticker or video, when WhatsApp declared it. |\n\n\n\n# Type Alias: LinkedAttachmentState\n\n> **LinkedAttachmentState** = `\"loading\"` \\| `\"ready\"` \\| `\"unavailable\"` \\| `\"unsupported\"`\n\nWhere an attachment of a linked message stands.\n\n\n\n# Type Alias: LinkedBackfill\n\n> **LinkedBackfill** = \\{ `imported`: `number`; `state`: [`LinkedBackfillState`](LinkedBackfillState.md); \\}\n\nEarlier messages imported after adding a group.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `imported` | `number` | Messages received from the phone so far (including ones already shown). |\n| `state` | [`LinkedBackfillState`](LinkedBackfillState.md) | Progress. |\n\n\n\n# Type Alias: LinkedBackfillState\n\n> **LinkedBackfillState** = `\"pending\"` \\| `\"running\"` \\| `\"waiting_for_phone\"` \\| `\"done\"` \\| `\"partial\"`\n\nWhere importing a group's earlier messages stands.\n\n\n\n# Type Alias: LinkedCapabilities\n\n> **LinkedCapabilities** = \\{ `attachment_limit_bytes`: `number`; `attachment_types`: `string`[]; `max_attachments`: `number`; `mutation_window_seconds`: `number`; `mutations`: [`LinkedMutation`](LinkedMutation.md)[]; `text_limit_bytes`: `number`; \\}\n\nOperations implemented for linked groups, independent of current permission.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `attachment_limit_bytes` | `number` | Largest attachment in bytes. |\n| `attachment_types` | `string`[] | Supported attachment MIME types. |\n| `max_attachments` | `number` | Attachments per send. |\n| `mutation_window_seconds` | `number` | Edits and deletes are offered this long after sending. |\n| `mutations` | [`LinkedMutation`](LinkedMutation.md)[] | Supported message operations. |\n| `text_limit_bytes` | `number` | Maximum UTF-8 bytes per text send. |\n\n\n\n# Type Alias: LinkedChat\n\n> **LinkedChat** = \\{ `archived`: `boolean`; `canAdd`: `boolean`; `cannotAddReason?`: `string` \\| `null`; `group?`: `null` \\| [`LinkedGroupReference`](LinkedGroupReference.md); `jid`: `string`; `kind`: [`LinkedChatKind`](LinkedChatKind.md); `lastActivityAt?`: `string` \\| `null`; `name`: `string`; `pinned`: `boolean`; `shared?`: `null` \\| [`LinkedSharedGroupReference`](LinkedSharedGroupReference.md); \\}\n\nOne chat of the caller's WhatsApp chat list, as WhatsApp Web lists it.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `archived` | `boolean` | Archived in WhatsApp. |\n| `canAdd` | `boolean` | \"Add to Macro\" works now. |\n| `cannotAddReason?` | `string` \\| `null` | Why it can't be added, when it is neither added, shared nor addable. |\n| `group?` | `null` \\| [`LinkedGroupReference`](LinkedGroupReference.md) | - |\n| `jid` | `string` | WhatsApp chat JID: a group, or the other person's phone-number JID (LID when WhatsApp never told us the number). |\n| `kind` | [`LinkedChatKind`](LinkedChatKind.md) | Group or direct chat. |\n| `lastActivityAt?` | `string` \\| `null` | Newest activity WhatsApp reported (message or history sync); `null` when unknown. The list is sorted by it, newest first, unknown last. |\n| `name` | `string` | Group: the subject, or member names for an unnamed group. Direct: the contact or WhatsApp name, else `+`. Always non-empty. |\n| `pinned` | `boolean` | Pinned in WhatsApp. |\n| `shared?` | `null` \\| [`LinkedSharedGroupReference`](LinkedSharedGroupReference.md) | - |\n\n\n\n# Type Alias: LinkedChatKind\n\n> **LinkedChatKind** = `\"group\"` \\| `\"direct\"`\n\nWhether a WhatsApp chat is a group or a one-to-one chat.\n\n\n\n# Type Alias: LinkedDeliveryState\n\n> **LinkedDeliveryState** = `\"pending\"` \\| `\"sending\"` \\| `\"unknown\"` \\| `\"accepted\"` \\| `\"sent\"` \\| `\"delivered\"` \\| `\"read\"` \\| `\"failed\"`\n\nObserved send state. `unknown` means the request may have reached WhatsApp;\nit is never sent again automatically.\n\n\n\n# Type Alias: LinkedGroupInfo\n\n> **LinkedGroupInfo** = \\{ `announce`: `boolean`; `community`: `boolean`; `disappearing`: `boolean`; `group?`: `null` \\| [`LinkedGroupReference`](LinkedGroupReference.md); `jid`: `string`; `name`: `string`; `observedAt`: `string`; `participants`: [`LinkedParticipant`](LinkedParticipant.md)[]; `shared?`: `null` \\| [`LinkedSharedGroupReference`](LinkedSharedGroupReference.md); \\}\n\nOne full metadata read of a group, never an incomplete membership delta.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `announce` | `boolean` | Only admins may post. |\n| `community` | `boolean` | Community announcement or parent group, outside the supported surface. |\n| `disappearing` | `boolean` | Disappearing messages are on; content is not retained. |\n| `group?` | `null` \\| [`LinkedGroupReference`](LinkedGroupReference.md) | - |\n| `jid` | `string` | Remote group JID. |\n| `name` | `string` | Current subject. |\n| `observedAt` | `string` | When the session service read this metadata. |\n| `participants` | [`LinkedParticipant`](LinkedParticipant.md)[] | Full roster. |\n| `shared?` | `null` \\| [`LinkedSharedGroupReference`](LinkedSharedGroupReference.md) | - |\n\n\n\n# Type Alias: LinkedGroupReference\n\n> **LinkedGroupReference** = \\{ `id`: `string`; `name`: `string`; \\}\n\nA group entity bound to a remote group.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | Group entity id. |\n| `name` | `string` | Display name, following WhatsApp. |\n\n\n\n# Type Alias: LinkedIdentity\n\n> **LinkedIdentity** = \\{ `alternateJid?`: `string` \\| `null`; `displayName?`: `string` \\| `null`; `jid`: `string`; \\}\n\nVerified sender aliases, supplied only by protocol metadata.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `alternateJid?` | `string` \\| `null` | Verified alternate identity, never inferred from a name or phone typed by a user. |\n| `displayName?` | `string` \\| `null` | Untrusted display name. |\n| `jid` | `string` | Phone-number or LID account JID. |\n\n\n\n# Type Alias: LinkedMediaKind\n\n> **LinkedMediaKind** = `\"image\"` \\| `\"audio\"` \\| `\"video\"` \\| `\"document\"`\n\nAttachment categories WhatsApp distinguishes.\n\n\n\n# Type Alias: LinkedMediaPresentation\n\n> **LinkedMediaPresentation** = `\"sticker\"` \\| `\"gif\"` \\| `\"voice_note\"`\n\nHow WhatsApp shows an attachment, when not as a plain file of its kind.\n\n\n\n# Type Alias: LinkedMutation\n\n> **LinkedMutation** = `\"reaction\"` \\| `\"edit\"` \\| `\"delete\"`\n\nDeliberate operations on an existing linked-group message.\n\n\n\n# Type Alias: LinkedParticipant\n\n> **LinkedParticipant** = [`LinkedIdentity`](LinkedIdentity.md) & \\{ `admin`: `boolean`; \\}\n\nCurrent remote participant; Macro access is a separate grant.\n\n## Type Declaration\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `admin` | `boolean` | Group admin; only admins post in announcement groups. |\n\n\n\n# Type Alias: LinkedReplyBlock\n\n> **LinkedReplyBlock** = `\"sending_disabled\"` \\| `\"no_account\"` \\| `\"disconnected\"` \\| `\"not_observing\"` \\| `\"not_allowed_in_group\"` \\| `\"group_unavailable\"`\n\nWhy the viewer cannot send now.\n\n\n\n# Type Alias: LinkedReplyContent\n\n> **LinkedReplyContent** = \\{ `mentions?`: `string`[]; `text`: `string`; `type`: `\"text\"`; \\} \\| \\{ `caption?`: `string` \\| `null`; `type`: `\"media\"`; `upload_id`: `string`; \\} \\| \\{ `message_id`: `string`; `operation`: [`LinkedMutation`](LinkedMutation.md); `text?`: `string` \\| `null`; `type`: `\"mutation\"`; \\}\n\nContent of a deliberate send.\n\n## Union Members\n\n### Type Literal\n\n\\{ `mentions?`: `string`[]; `text`: `string`; `type`: `\"text\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `mentions?` | `string`[] | Group participants the text @-mentions, by their WhatsApp id (`jid` of a roster participant); the text must contain `@` for each. |\n| `text` | `string` | UTF-8 text, at most [`TEXT_LIMIT_BYTES`] bytes. |\n| `type` | `\"text\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `caption?`: `string` \\| `null`; `type`: `\"media\"`; `upload_id`: `string`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `caption?` | `string` \\| `null` | Optional caption; audio has none. |\n| `type` | `\"media\"` | - |\n| `upload_id` | `string` | The staged upload, which becomes the message's private document. |\n\n***\n\n### Type Literal\n\n\\{ `message_id`: `string`; `operation`: [`LinkedMutation`](LinkedMutation.md); `text?`: `string` \\| `null`; `type`: `\"mutation\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `message_id` | `string` | Existing group message, resolved to its WhatsApp source by the server. |\n| `operation` | [`LinkedMutation`](LinkedMutation.md) | Explicit operation. |\n| `text?` | `string` \\| `null` | Replacement text or emoji; absent for delete, empty to remove a reaction. |\n| `type` | `\"mutation\"` | - |\n\n\n\n# Type Alias: LinkedSendProgress\n\n> **LinkedSendProgress** = \\{ `created_at`: `string`; `error_code?`: `string` \\| `null`; `group_id`: `string`; `id`: `string`; `message_id?`: `string` \\| `null`; `payload`: [`LinkedSendReply`](LinkedSendReply.md); `state`: [`LinkedDeliveryState`](LinkedDeliveryState.md); `user_id`: `string`; \\}\n\nDurable result of a deliberate send.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `created_at` | `string` | Intent creation time. |\n| `error_code?` | `string` \\| `null` | Sanitized failure code. |\n| `group_id` | `string` | Group entity. |\n| `id` | `string` | Stable intent id. |\n| `message_id?` | `string` \\| `null` | Group message, once WhatsApp accepted it. |\n| `payload` | [`LinkedSendReply`](LinkedSendReply.md) | The original request, so a failed draft can be recovered. |\n| `state` | [`LinkedDeliveryState`](LinkedDeliveryState.md) | Current state. |\n| `user_id` | `string` | Person who submitted it. |\n\n\n\n# Type Alias: LinkedSendReply\n\n> **LinkedSendReply** = \\{ `idempotency_key`: `string`; `message`: [`LinkedReplyContent`](LinkedReplyContent.md); `reply_to?`: `string` \\| `null`; \\}\n\nExplicit send action from a person. Only deliberate sends reach WhatsApp.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `idempotency_key` | `string` | Stable client intent key. A changed payload with this key is a conflict. |\n| `message` | [`LinkedReplyContent`](LinkedReplyContent.md) | Content. |\n| `reply_to?` | `string` \\| `null` | Group message being quoted, within this group. |\n\n\n\n# Type Alias: LinkedSharedGroupReference\n\n> **LinkedSharedGroupReference** = \\{ `id`: `string`; `name`: `string`; `owner_id`: `string`; \\}\n\nAnother person's group entity the caller can view.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | Group entity id. |\n| `name` | `string` | Display name, following WhatsApp. |\n| `owner_id` | `string` | Macro user who added the group and shared it. |\n\n\n\n# Type Alias: LinkedUploadedAttachment\n\n> **LinkedUploadedAttachment** = \\{ `filename`: `string`; `kind`: [`LinkedMediaKind`](LinkedMediaKind.md); `upload_id`: `string`; \\}\n\nA staged attachment, sent only by a later deliberate send.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `filename` | `string` | Display filename. |\n| `kind` | [`LinkedMediaKind`](LinkedMediaKind.md) | Category. |\n| `upload_id` | `string` | Use as `upload_id` in a media send; also the future document id. |\n\n\n\n# Type Alias: Message\n\n> **Message** = \\{ `attachments`: [`MessageAttachment`](MessageAttachment.md)[]; `bot_profile?`: `null` \\| [`BotSenderProfile`](BotSenderProfile.md); `content`: `string`; `created_at`: `string`; `deleted_at?`: `string` \\| `null`; `edited_at?`: `string` \\| `null`; `guest_name?`: `string` \\| `null`; `id`: `string`; `imported_author?`: `null` \\| [`ImportedAuthor`](ImportedAuthor.md); `mentions`: [`SimpleMention`](SimpleMention.md)[]; `parent`: [`MessageParent`](MessageParent.md); `reactions`: [`CountedReaction`](CountedReaction.md)[]; `sender_id`: `string`; `thread_id?`: `string` \\| `null`; `triggered_by?`: `string` \\| `null`; `updated_at`: `string`; \\}\n\nShared message representation for channel timelines and entity discussions.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `attachments` | [`MessageAttachment`](MessageAttachment.md)[] | Attached entities. |\n| `bot_profile?` | `null` \\| [`BotSenderProfile`](BotSenderProfile.md) | - |\n| `content` | `string` | Macro Markdown body. |\n| `created_at` | `string` | Creation time. |\n| `deleted_at?` | `string` \\| `null` | Message tombstone, independent of thread deletion. |\n| `edited_at?` | `string` \\| `null` | Last content edit, if any. |\n| `guest_name?` | `string` \\| `null` | Display name of a verified call guest, retained after the call ends. |\n| `id` | `string` | Message UUID. |\n| `imported_author?` | `null` \\| [`ImportedAuthor`](ImportedAuthor.md) | - |\n| `mentions` | [`SimpleMention`](SimpleMention.md)[] | Tracked mentions, retained when a caller changes attachments only. |\n| `parent` | [`MessageParent`](MessageParent.md) | Entity that owns this conversation. |\n| `reactions` | [`CountedReaction`](CountedReaction.md)[] | Aggregated reactions. |\n| `sender_id` | `string` | Authenticated actor or owner of imported content. |\n| `thread_id?` | `string` \\| `null` | Root message UUID for replies; absent on roots. |\n| `triggered_by?` | `string` \\| `null` | User who triggered a bot-authored message. |\n| `updated_at` | `string` | Last persisted update. |\n\n\n\n# Type Alias: MessageAttachment\n\n> **MessageAttachment** = \\{ `created_at`: `string`; `entity_id`: `string`; `entity_type`: `string`; `height?`: `number` \\| `null`; `id`: `string`; `width?`: `number` \\| `null`; \\}\n\nAn entity attached to a message.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `created_at` | `string` | When the attachment was added. |\n| `entity_id` | `string` | Attached entity identifier. |\n| `entity_type` | `string` | Attached entity type. |\n| `height?` | `number` \\| `null` | Optional media height. |\n| `id` | `string` | Attachment UUID. |\n| `width?` | `number` \\| `null` | Optional media width. |\n\n\n\n# Type Alias: MessageCursor\n\n> **MessageCursor** = \\{ `created_at`: `string`; `id`: `string`; \\}\n\nCursor for a chronological parent timeline.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `created_at` | `string` | Last message creation time or activity occurrence time. |\n| `id` | `string` | Last entry UUID, used to break timestamp ties across both sources. |\n\n\n\n# Type Alias: MessageParent\n\n> **MessageParent** = \\{ `id`: `string`; `type`: `\"channel\"`; \\} \\| \\{ `id`: [`DocumentId`](DocumentId.md); `type`: `\"document\"`; \\} \\| \\{ `id`: `string`; `type`: `\"initiative\"`; \\} \\| \\{ `id`: `string`; `type`: `\"crm_company\"`; \\} \\| \\{ `id`: `string`; `type`: `\"crm_contact\"`; \\} \\| \\{ `id`: `string`; `type`: `\"call\"`; \\}\n\nThe entity whose permissions and lifecycle govern a message.\n\n## Union Members\n\n### Type Literal\n\n\\{ `id`: `string`; `type`: `\"channel\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | A channel, including direct messages. |\n| `type` | `\"channel\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `id`: [`DocumentId`](DocumentId.md); `type`: `\"document\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `id` | [`DocumentId`](DocumentId.md) | A document, including tasks and PDFs. |\n| `type` | `\"document\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `id`: `string`; `type`: `\"initiative\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | An initiative, presented as a project in the application. |\n| `type` | `\"initiative\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `id`: `string`; `type`: `\"crm_company\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | A CRM company. |\n| `type` | `\"crm_company\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `id`: `string`; `type`: `\"crm_contact\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | A CRM contact. |\n| `type` | `\"crm_contact\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `id`: `string`; `type`: `\"call\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | A video call and its persistent chat thread. |\n| `type` | `\"call\"` | - |\n\n\n\n# Type Alias: ParticipantRole\n\n> **ParticipantRole** = `\"owner\"` \\| `\"admin\"` \\| `\"member\"`\n\nThe role a user has within a channel.\n\n\n\n# Type Alias: SharePermissionV2\n\n> **SharePermissionV2** = \\{ `channelSharePermissions?`: [`ChannelSharePermission`](ChannelSharePermission.md)[] \\| `null`; `id`: `string`; `linkShare?`: `null` \\| [`LinkShare`](LinkShare.md); `linkShareAccessLevel?`: `null` \\| [`AccessLevel`](AccessLevel.md); `owner`: `string`; `teamShareAccessLevel?`: `null` \\| [`AccessLevel`](AccessLevel.md); \\}\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channelSharePermissions?` | [`ChannelSharePermission`](ChannelSharePermission.md)[] \\| `null` | The channel share permissions for the item |\n| `id` | `string` | The share permission id |\n| `linkShare?` | `null` \\| [`LinkShare`](LinkShare.md) | - |\n| `linkShareAccessLevel?` | `null` \\| [`AccessLevel`](AccessLevel.md) | - |\n| `owner` | `string` | The owner of the item |\n| `teamShareAccessLevel?` | `null` \\| [`AccessLevel`](AccessLevel.md) | - |\n\n\n\n# Type Alias: SimpleMention\n\n> **SimpleMention** = \\{ `entity_id`: `string`; `entity_type`: `string`; \\}\n\nA mention tracked in a message body.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `entity_id` | `string` | Mentioned entity identifier. |\n| `entity_type` | `string` | Mentioned entity type. |\n\n\n\n# Type Alias: SmartTagChannelMatch\n\n> **SmartTagChannelMatch** = \\{ `id`: `string`; `name`: `string`; \\}\n\nA channel visible to the caller that matches a smart tag rule.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `id` | `string` | Channel id. |\n| `name` | `string` | Channel display name. |\n\n\n\n# Type Alias: SmartTagPreview\n\n> **SmartTagPreview** = \\{ `channels`: [`SmartTagChannelMatch`](SmartTagChannelMatch.md)[]; `totalCount`: `number`; \\}\n\nA bounded preview and the total number of visible channels matching a rule.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channels` | [`SmartTagChannelMatch`](SmartTagChannelMatch.md)[] | First matches, in alphabetical order. |\n| `totalCount` | `number` | Number of matching channels the caller participates in, including overflow. |\n\n\n\n# Type Alias: TeamRole\n\n> **TeamRole** = `\"member\"` \\| `\"admin\"` \\| `\"owner\"`\n\nThe role a user has within a team.\n\nOrdered least to most privileged so comparisons reflect access strength.\n\n\n\n# Type Alias: UpdateChannelSharePermission\n\n> **UpdateChannelSharePermission** = \\{ `accessLevel?`: `null` \\| [`AccessLevel`](AccessLevel.md); `channelId`: `string`; `operation`: [`UpdateOperation`](UpdateOperation.md); \\}\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `accessLevel?` | `null` \\| [`AccessLevel`](AccessLevel.md) | - |\n| `channelId` | `string` | The channel id |\n| `operation` | [`UpdateOperation`](UpdateOperation.md) | The type of operation to be performed on the chanel share permission You can add, remove or replace and existing permission |\n\n\n\n# Type Alias: UpdateOperation\n\n> **UpdateOperation** = `\"add\"` \\| `\"remove\"` \\| `\"replace\"`\n\n\n\n# Type Alias: UpdateSharePermissionRequestV2\n\n> **UpdateSharePermissionRequestV2** = \\{ `channelSharePermissions?`: [`UpdateChannelSharePermission`](UpdateChannelSharePermission.md)[] \\| `null`; `linkShare?`: `null` \\| [`LinkShare`](LinkShare.md); `linkShareAccessLevel?`: `null` \\| [`AccessLevel`](AccessLevel.md); `teamShareAccessLevel?`: `null` \\| [`AccessLevel`](AccessLevel.md); \\}\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `channelSharePermissions?` | [`UpdateChannelSharePermission`](UpdateChannelSharePermission.md)[] \\| `null` | Any channel share permissions to be created/updated/removed |\n| `linkShare?` | `null` \\| [`LinkShare`](LinkShare.md) | - |\n| `linkShareAccessLevel?` | `null` \\| [`AccessLevel`](AccessLevel.md) | - |\n| `teamShareAccessLevel?` | `null` \\| [`AccessLevel`](AccessLevel.md) | - |\n\n\n\n# Type Alias: WhatsAppCall\n\n> **WhatsAppCall** = \\{ `durationSeconds?`: `number` \\| `null`; `outcome`: [`WhatsAppCallOutcome`](WhatsAppCallOutcome.md); `video`: `boolean`; \\}\n\nA call in the group.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `durationSeconds?` | `number` \\| `null` | Length, when it connected. |\n| `outcome` | [`WhatsAppCallOutcome`](WhatsAppCallOutcome.md) | How it went for the account that saw it. |\n| `video` | `boolean` | Video rather than voice. |\n\n\n\n# Type Alias: WhatsAppCallOutcome\n\n> **WhatsAppCallOutcome** = `\"answered\"` \\| `\"missed\"` \\| `\"rejected\"` \\| `\"other\"`\n\nHow a call went.\n\n\n\n# Type Alias: WhatsAppContactCard\n\n> **WhatsAppContactCard** = \\{ `name`: `string`; `phones`: `string`[]; \\}\n\nOne contact card, from its vCard.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `name` | `string` | Display name. |\n| `phones` | `string`[] | Phone numbers as written on the card. |\n\n\n\n# Type Alias: WhatsAppContacts\n\n> **WhatsAppContacts** = \\{ `contacts`: [`WhatsAppContactCard`](WhatsAppContactCard.md)[]; \\}\n\nContact cards.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `contacts` | [`WhatsAppContactCard`](WhatsAppContactCard.md)[] | The cards, in order. |\n\n\n\n# Type Alias: WhatsAppEvent\n\n> **WhatsAppEvent** = \\{ `canceled?`: `boolean`; `description?`: `string` \\| `null`; `endAt?`: `string` \\| `null`; `joinLink?`: `string` \\| `null`; `location?`: `string` \\| `null`; `name`: `string`; `startAt?`: `string` \\| `null`; \\}\n\nA WhatsApp event.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `canceled?` | `boolean` | Canceled by its organizer. |\n| `description?` | `string` \\| `null` | Description. |\n| `endAt?` | `string` \\| `null` | End. |\n| `joinLink?` | `string` \\| `null` | Call link for an online event. |\n| `location?` | `string` \\| `null` | Place name or address. |\n| `name` | `string` | Title. |\n| `startAt?` | `string` \\| `null` | Start. |\n\n\n\n# Type Alias: WhatsAppGroupDetail\n\n> **WhatsAppGroupDetail** = [`WhatsAppGroupSummary`](WhatsAppGroupSummary.md) & \\{ `access_level`: [`AccessLevel`](AccessLevel.md); `limits`: [`LinkedCapabilities`](LinkedCapabilities.md); `roster?`: `null` \\| [`LinkedGroupInfo`](LinkedGroupInfo.md); `sending_identity?`: `string` \\| `null`; `share_permission`: [`SharePermissionV2`](SharePermissionV2.md); \\}\n\nFull group detail and current viewer capabilities.\n\n## Type Declaration\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `access_level` | [`AccessLevel`](AccessLevel.md) | Caller's effective permission. |\n| `limits` | [`LinkedCapabilities`](LinkedCapabilities.md) | Size and mutation limits. |\n| `roster?` | `null` \\| [`LinkedGroupInfo`](LinkedGroupInfo.md) | - |\n| `sending_identity?` | `string` \\| `null` | Caller's own phone identity, never another account's identity. |\n| `share_permission` | [`SharePermissionV2`](SharePermissionV2.md) | Owner-controlled standard sharing configuration. |\n\n\n\n# Type Alias: WhatsAppGroupMessage\n\n> **WhatsAppGroupMessage** = \\{ `attachments`: [`LinkedAttachment`](LinkedAttachment.md)[]; `body`: `string`; `deleted`: `boolean`; `delivery_state?`: `null` \\| [`LinkedDeliveryState`](LinkedDeliveryState.md); `edited_at?`: `string` \\| `null`; `forwarded`: `boolean`; `id`: `string`; `kind`: [`WhatsAppMessageKind`](WhatsAppMessageKind.md); `mentions`: [`WhatsAppMention`](WhatsAppMention.md)[]; `reactions`: [`WhatsAppGroupReaction`](WhatsAppGroupReaction.md)[]; `received_at`: `string`; `reply_to?`: `string` \\| `null`; `rich?`: `null` \\| [`WhatsAppRichContent`](WhatsAppRichContent.md); `sender`: [`WhatsAppGroupSender`](WhatsAppGroupSender.md); `sent_at`: `string`; \\}\n\nOne message in an adopted group.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `attachments` | [`LinkedAttachment`](LinkedAttachment.md)[] | Imported or pending attachments. |\n| `body` | `string` | Plain WhatsApp text. |\n| `deleted` | `boolean` | Deleted for everyone. |\n| `delivery_state?` | `null` \\| [`LinkedDeliveryState`](LinkedDeliveryState.md) | - |\n| `edited_at?` | `string` \\| `null` | Last edit timestamp. |\n| `forwarded` | `boolean` | Forwarded from another chat. |\n| `id` | `string` | Stable message id. |\n| `kind` | [`WhatsAppMessageKind`](WhatsAppMessageKind.md) | A message, or a system line. |\n| `mentions` | [`WhatsAppMention`](WhatsAppMention.md)[] | People the body @-mentions. |\n| `reactions` | [`WhatsAppGroupReaction`](WhatsAppGroupReaction.md)[] | Reactions grouped by emoji. |\n| `received_at` | `string` | Timestamp first received by Macro. |\n| `reply_to?` | `string` \\| `null` | Message quoted by this one. |\n| `rich?` | `null` \\| [`WhatsAppRichContent`](WhatsAppRichContent.md) | - |\n| `sender` | [`WhatsAppGroupSender`](WhatsAppGroupSender.md) | Sender. |\n| `sent_at` | `string` | Timestamp assigned by WhatsApp. |\n\n\n\n# Type Alias: WhatsAppGroupReaction\n\n> **WhatsAppGroupReaction** = \\{ `count`: `number`; `emoji`: `string`; `mine`: `boolean`; \\}\n\nGrouped reactions.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `count` | `number` | Number of reacting people. |\n| `emoji` | `string` | Emoji. |\n| `mine` | `boolean` | Caller has this reaction. |\n\n\n\n# Type Alias: WhatsAppGroupSender\n\n> **WhatsAppGroupSender** = \\{ `name`: `string`; `person_id`: `string`; `user_id?`: `string` \\| `null`; \\}\n\nMessage sender from protocol-verified identity evidence.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `name` | `string` | Display name, never authentication evidence. |\n| `person_id` | `string` | External person identity. |\n| `user_id?` | `string` \\| `null` | Verified Macro user association, if present. |\n\n\n\n# Type Alias: WhatsAppGroupSummary\n\n> **WhatsAppGroupSummary** = \\{ `backfill?`: `null` \\| [`LinkedBackfill`](LinkedBackfill.md); `can_send`: `boolean`; `id`: `string`; `last_message_at?`: `string` \\| `null`; `name`: `string`; `send_reason?`: `null` \\| [`LinkedReplyBlock`](LinkedReplyBlock.md); `unread_count`: `number`; `user_id`: `string`; \\}\n\nAdopted group visible through ordinary entity sharing.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `backfill?` | `null` \\| [`LinkedBackfill`](LinkedBackfill.md) | - |\n| `can_send` | `boolean` | Caller may currently send from their own account. |\n| `id` | `string` | Entity identifier. |\n| `last_message_at?` | `string` \\| `null` | Latest message timestamp. |\n| `name` | `string` | Display name. |\n| `send_reason?` | `null` \\| [`LinkedReplyBlock`](LinkedReplyBlock.md) | - |\n| `unread_count` | `number` | Unread live messages for this viewer. |\n| `user_id` | `string` | Actual entity owner. |\n\n\n\n# Type Alias: WhatsAppLocation\n\n> **WhatsAppLocation** = \\{ `address?`: `string` \\| `null`; `comment?`: `string` \\| `null`; `latitude`: `number`; `live?`: `boolean`; `longitude`: `number`; `name?`: `string` \\| `null`; `url?`: `string` \\| `null`; \\}\n\nA shared place.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `address?` | `string` \\| `null` | Street address. |\n| `comment?` | `string` \\| `null` | Caption or comment. |\n| `latitude` | `number` | Degrees. |\n| `live?` | `boolean` | Shared as a live location (this is where it started). |\n| `longitude` | `number` | Degrees. |\n| `name?` | `string` \\| `null` | Place name. |\n| `url?` | `string` \\| `null` | Link the sender attached. |\n\n\n\n# Type Alias: WhatsAppMention\n\n> **WhatsAppMention** = [`WhatsAppPersonRef`](WhatsAppPersonRef.md) & \\{ `token`: `string`; \\}\n\nAn @-mention in a message body.\n\n## Type Declaration\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `token` | `string` | What follows the `@` in the body (the digits of the person's WhatsApp id); replace `@` with the name. |\n\n\n\n# Type Alias: WhatsAppMessageKind\n\n> **WhatsAppMessageKind** = `\"message\"` \\| `\"system\"`\n\nWhat a timeline entry is.\n\n\n\n# Type Alias: WhatsAppPersonRef\n\n> **WhatsAppPersonRef** = \\{ `name`: `string`; `userId?`: `string` \\| `null`; \\}\n\nA person named in a message: a Macro user when verified, else their\nWhatsApp name, else their phone number.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `name` | `string` | Display name, never authentication evidence. |\n| `userId?` | `string` \\| `null` | Verified Macro user, if any. |\n\n\n\n# Type Alias: WhatsAppPoll\n\n> **WhatsAppPoll** = \\{ `options`: [`WhatsAppPollOption`](WhatsAppPollOption.md)[]; `question`: `string`; `selectableCount`: `number`; `voters`: `number`; \\}\n\nA poll, read-only in Macro.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `options` | [`WhatsAppPollOption`](WhatsAppPollOption.md)[] | Options in order, with tallies. |\n| `question` | `string` | The question. |\n| `selectableCount` | `number` | How many options one person may choose; 0 means any number. |\n| `voters` | `number` | People who voted for anything. |\n\n\n\n# Type Alias: WhatsAppPollOption\n\n> **WhatsAppPollOption** = \\{ `mine`: `boolean`; `name`: `string`; `voters`: [`WhatsAppPersonRef`](WhatsAppPersonRef.md)[]; `votes`: `number`; \\}\n\nOne option of a poll with its votes.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `mine` | `boolean` | The caller chose it. |\n| `name` | `string` | Option text. |\n| `voters` | [`WhatsAppPersonRef`](WhatsAppPersonRef.md)[] | Who chose it. |\n| `votes` | `number` | People who chose it. |\n\n\n\n# Type Alias: WhatsAppRichContent\n\n> **WhatsAppRichContent** = [`WhatsAppPoll`](WhatsAppPoll.md) & \\{ `type`: `\"poll\"`; \\} \\| [`WhatsAppLocation`](WhatsAppLocation.md) & \\{ `type`: `\"location\"`; \\} \\| [`WhatsAppContacts`](WhatsAppContacts.md) & \\{ `type`: `\"contacts\"`; \\} \\| [`WhatsAppEvent`](WhatsAppEvent.md) & \\{ `type`: `\"event\"`; \\} \\| [`WhatsAppSystemLine`](WhatsAppSystemLine.md) & \\{ `type`: `\"system\"`; \\}\n\nStructured content of a message besides its body.\n\n\n\n# Type Alias: WhatsAppSystemAction\n\n> **WhatsAppSystemAction** = `\"group_created\"` \\| `\"participants_added\"` \\| `\"participants_removed\"` \\| `\"participants_left\"` \\| `\"participants_joined\"` \\| `\"participants_promoted\"` \\| `\"participants_demoted\"` \\| `\"subject_changed\"` \\| `\"icon_changed\"` \\| `\"description_changed\"` \\| `\"disappearing_changed\"` \\| `\"message_pinned\"` \\| `\"message_unpinned\"` \\| `\"call\"`\n\nKinds of system lines.\n\n\n\n# Type Alias: WhatsAppSystemLine\n\n> **WhatsAppSystemLine** = \\{ `action`: [`WhatsAppSystemAction`](WhatsAppSystemAction.md); `actor?`: `null` \\| [`WhatsAppPersonRef`](WhatsAppPersonRef.md); `call?`: `null` \\| [`WhatsAppCall`](WhatsAppCall.md); `participants`: [`WhatsAppPersonRef`](WhatsAppPersonRef.md)[]; `value?`: `string` \\| `null`; \\}\n\nSomething that happened in the group.\n\n## Properties\n\n| Property | Type | Description |\n| ------ | ------ | ------ |\n| `action` | [`WhatsAppSystemAction`](WhatsAppSystemAction.md) | What happened. |\n| `actor?` | `null` \\| [`WhatsAppPersonRef`](WhatsAppPersonRef.md) | - |\n| `call?` | `null` \\| [`WhatsAppCall`](WhatsAppCall.md) | - |\n| `participants` | [`WhatsAppPersonRef`](WhatsAppPersonRef.md)[] | Who it happened to (added, removed, promoted, …). |\n| `value?` | `string` \\| `null` | New subject or description; disappearing-messages timer in seconds (`\"0\"` is off). |\n\n\n\n# Class: ChannelActivity\n\nA user's channel activity marker.\n\n## Constructors\n\n### Constructor\n\n> **new ChannelActivity**(`client`, `channel`, `record`): `ChannelActivity`\n\n#### Parameters\n\n| Parameter | Type | Description |\n| ------ | ------ | ------ |\n| `client` | `MacroClient` | - |\n| `channel` | [`Channel`](../../channel/classes/Channel.md) | Related channel. |\n| `record` | [`ApiActivity`](../../../../../generated/storage/types.gen/type-aliases/ApiActivity.md) | - |\n\n#### Returns\n\n`ChannelActivity`\n\n## Properties\n\n| Property | Modifier | Type | Description |\n| ------ | ------ | ------ | ------ |\n| `channel` | `readonly` | [`Channel`](../../channel/classes/Channel.md) | Related channel. |\n\n## Accessors\n\n### id\n\n#### Get Signature\n\n> **get** **id**(): `string`\n\nActivity identity.\n\n##### Returns\n\n`string`\n\n## Methods\n\n### createdAt()\n\n> **createdAt**(): `string`\n\nCreation time.\n\n#### Returns\n\n`string`\n\n***\n\n### recordInteraction()\n\n> **recordInteraction**(`type`): `Promise`\\<`ChannelActivity`\\>\n\nRecord a view or interaction in this channel.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `type` | [`ActivityType`](../../../../../generated/storage/types.gen/type-aliases/ActivityType.md) |\n\n#### Returns\n\n`Promise`\\<`ChannelActivity`\\>\n\n***\n\n### toJSON()\n\n> **toJSON**(): [`ApiActivity`](../../../../../generated/storage/types.gen/type-aliases/ApiActivity.md)\n\nSerialize activity without credentials.\n\n#### Returns\n\n[`ApiActivity`](../../../../../generated/storage/types.gen/type-aliases/ApiActivity.md)\n\n***\n\n### user()\n\n> **user**(): [`User`](../../../users/user/classes/User.md)\n\nUser who interacted.\n\n#### Returns\n\n[`User`](../../../users/user/classes/User.md)\n\n\n\n# Class: Channel\n\nA channel.\n\n## Extends\n\n- [`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md)\\<`ChannelDetail`\\>\n\n## Properties\n\n| Property | Modifier | Type | Default value | Description | Overrides | Inherited from |\n| ------ | ------ | ------ | ------ | ------ | ------ | ------ |\n| `entityType` | `readonly` | `\"channel\"` | `'channel'` | Favorites identify channels as `channel`. | [`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`entityType`](../../../entity/classes/PropertiedEntity.md#property-entitytype) | - |\n| `id` | `public readonly` | `string` | `undefined` | - | - | [`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`id`](../../../entity/classes/PropertiedEntity.md#property-id) |\n| `name` | `readonly` | () => `Promise`\\<`string`\\> | `...` | The channel's display name, resolved from the viewer's perspective. | - | - |\n| `type` | `readonly` | () => `Promise`\\<[`ChannelType`](../../../../../generated/storage/types.gen/type-aliases/ChannelType.md)\\> | `...` | The channel's type (e.g. `public`, `dm`, `private`). | - | - |\n| `search` | `static` | (`client`, `query`, `searchOpts?`) => `AsyncGenerator`\\<`Channel`\\> | `...` | Search channels by content, most relevant first, auto-paginated. | - | - |\n\n## Methods\n\n### addParticipants()\n\n> **addParticipants**(`users`): `Promise`\\<`void`\\>\n\nAdd users to the channel.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `users` | [`User`](../../../users/user/classes/User.md)[] |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### agentConversations()\n\n> **agentConversations**(): `Promise`\\<[`AgentConversation`](../../../agent-sessions/conversation/classes/AgentConversation.md)[]\\>\n\nConversations maintained by agents in this channel.\n\n#### Returns\n\n`Promise`\\<[`AgentConversation`](../../../agent-sessions/conversation/classes/AgentConversation.md)[]\\>\n\n***\n\n### attachments()\n\n> **attachments**(`opts?`): `AsyncGenerator`\\<[`ApiChannelAttachment`](../../../../../generated/storage/types.gen/type-aliases/ApiChannelAttachment.md)\\>\n\nThe attachments posted in this channel, auto-paginated.\n\n#### Parameters\n\n| Parameter | Type | Description |\n| ------ | ------ | ------ |\n| `opts?` | \\{ `pageSize?`: `number`; `type?`: `string`; \\} | - |\n| `opts.pageSize?` | `number` | - |\n| `opts.type?` | `string` | Filter by type: `static` for images/videos, `dss` for documents. |\n\n#### Returns\n\n`AsyncGenerator`\\<[`ApiChannelAttachment`](../../../../../generated/storage/types.gen/type-aliases/ApiChannelAttachment.md)\\>\n\n***\n\n### delete()\n\n> **delete**(): `Promise`\\<`void`\\>\n\nDelete the channel. This is not reversible.\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### deleteProperty()\n\n> **deleteProperty**(`assignment`): `Promise`\\<`void`\\>\n\nRemove a property from this entity, given its [properties](#properties) entry.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `assignment` | [`EntityPropertyWithDefinition`](../../../../../generated/properties/types.gen/type-aliases/EntityPropertyWithDefinition.md) |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`deleteProperty`](../../../entity/classes/PropertiedEntity.md#deleteproperty)\n\n***\n\n### favorite()\n\n> **favorite**(): `Promise`\\<`Channel`\\>\n\nAdd this entity to the user's favorites. Returns this handle for chaining.\nPlain unwrap: favoriting alters the user's favorites collection, not this\nentity's own detail, so there's nothing cached to invalidate.\n\n#### Returns\n\n`Promise`\\<`Channel`\\>\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`favorite`](../../../entity/classes/PropertiedEntity.md#favorite)\n\n***\n\n### join()\n\n> **join**(): `Promise`\\<`void`\\>\n\nJoin the channel as the current user.\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### joinCode()\n\n> **joinCode**(): `Promise`\\<`string`\\>\n\nA reusable code that lets a user join this channel.\n\n#### Returns\n\n`Promise`\\<`string`\\>\n\n***\n\n### leave()\n\n> **leave**(): `Promise`\\<`void`\\>\n\nLeave the channel as the current user.\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### message()\n\n> **message**(`id`): [`Message`](../../message/classes/Message.md)\n\nA handle to a message in this channel by id.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `id` | `string` |\n\n#### Returns\n\n[`Message`](../../message/classes/Message.md)\n\n***\n\n### messages()\n\n> **messages**(`opts?`): `AsyncGenerator`\\<[`Message`](../../message/classes/Message.md)\\>\n\nRoot messages in this channel, most recent first, auto-paginated.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `opts?` | \\{ `pageSize?`: `number`; \\} |\n| `opts.pageSize?` | `number` |\n\n#### Returns\n\n`AsyncGenerator`\\<[`Message`](../../message/classes/Message.md)\\>\n\n***\n\n### messagesAfter()\n\n> **messagesAfter**(`after`, `opts?`): `AsyncGenerator`\\<[`Message`](../../message/classes/Message.md)\\>\n\nRoot messages created or replied to at or after `after`, most recent\nfirst, auto-paginated. Use it to catch up after a disconnect.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `after` | `string` \\| `Date` |\n| `opts?` | \\{ `pageSize?`: `number`; \\} |\n| `opts.pageSize?` | `number` |\n\n#### Returns\n\n`AsyncGenerator`\\<[`Message`](../../message/classes/Message.md)\\>\n\n***\n\n### messageThreadById()\n\n> **messageThreadById**(`id`): [`MessageThread`](../../../messages/thread/classes/MessageThread.md)\n\nA shared message thread scoped to this resource.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `id` | `string` |\n\n#### Returns\n\n[`MessageThread`](../../../messages/thread/classes/MessageThread.md)\n\n***\n\n### participants()\n\n> **participants**(): `Promise`\\<[`ApiChannelParticipant`](../../../../../generated/storage/types.gen/type-aliases/ApiChannelParticipant.md)[]\\>\n\nThe channel's members, with their roles and join times.\n\n#### Returns\n\n`Promise`\\<[`ApiChannelParticipant`](../../../../../generated/storage/types.gen/type-aliases/ApiChannelParticipant.md)[]\\>\n\n***\n\n### permission()\n\n> **permission**(): `Promise`\\<\\{ `permission`: [`EntityPermission`](../../../../../generated/storage/types.gen/type-aliases/EntityPermission.md); `status`: `\"access\"`; \\} \\| \\{ `status`: `\"no_access\"`; \\}\\>\n\nRead the caller's effective permission on this entity.\n\n#### Returns\n\n`Promise`\\<\\{ `permission`: [`EntityPermission`](../../../../../generated/storage/types.gen/type-aliases/EntityPermission.md); `status`: `\"access\"`; \\} \\| \\{ `status`: `\"no_access\"`; \\}\\>\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`permission`](../../../entity/classes/PropertiedEntity.md#permission)\n\n***\n\n### properties()\n\n> **properties**(`opts?`): `Promise`\\<[`EntityPropertyWithDefinition`](../../../../../generated/properties/types.gen/type-aliases/EntityPropertyWithDefinition.md)[]\\>\n\nThe properties set on this entity, each with its definition, value, and options.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `opts?` | \\{ `includeMetadata?`: `boolean`; \\} |\n| `opts.includeMetadata?` | `boolean` |\n\n#### Returns\n\n`Promise`\\<[`EntityPropertyWithDefinition`](../../../../../generated/properties/types.gen/type-aliases/EntityPropertyWithDefinition.md)[]\\>\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`properties`](../../../entity/classes/PropertiedEntity.md#properties)\n\n***\n\n### propertyReference()\n\n> **propertyReference**(): [`PropertyTargetReference`](../../../../../generated/properties/types.gen/type-aliases/PropertyTargetReference.md)\n\nThis entity's reference for bulk property operations.\n\n#### Returns\n\n[`PropertyTargetReference`](../../../../../generated/properties/types.gen/type-aliases/PropertyTargetReference.md)\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`propertyReference`](../../../entity/classes/PropertiedEntity.md#propertyreference)\n\n***\n\n### recordActivity()\n\n> **recordActivity**(`type`): `Promise`\\<[`ChannelActivity`](../../activity/classes/ChannelActivity.md)\\>\n\nRecord a view or interaction in this channel.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `type` | [`ActivityType`](../../../../../generated/storage/types.gen/type-aliases/ActivityType.md) |\n\n#### Returns\n\n`Promise`\\<[`ChannelActivity`](../../activity/classes/ChannelActivity.md)\\>\n\n***\n\n### removeParticipants()\n\n> **removeParticipants**(`users`): `Promise`\\<`void`\\>\n\nRemove users from the channel.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `users` | [`User`](../../../users/user/classes/User.md)[] |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### rename()\n\n> **rename**(`name`): `Promise`\\<`void`\\>\n\nRename the channel.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `name` | `string` |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### send()\n\n> **send**(`body`, `opts?`): `Promise`\\<[`Message`](../../message/classes/Message.md)\\>\n\nPost a message. Plain text, or a rich body composed with `msg`.\n\nWorks with a channel webhook token as well as a normal one — see\n[postToChannel](../../post/functions/postToChannel.md) for which endpoint that picks and what it costs.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `body` | `string` \\| [`RichMessage`](../../../../mentions/interfaces/RichMessage.md) |\n| `opts?` | \\{ `thread?`: [`Thread`](../../thread/classes/Thread.md); \\} |\n| `opts.thread?` | [`Thread`](../../thread/classes/Thread.md) |\n\n#### Returns\n\n`Promise`\\<[`Message`](../../message/classes/Message.md)\\>\n\n***\n\n### setPicture()\n\n> **setPicture**(`picture?`): `Promise`\\<`void`\\>\n\nSet an uploaded profile picture, or clear it.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `picture?` | [`ChannelPicture`](../../picture/classes/ChannelPicture.md) |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### setProperty()\n\n> **setProperty**(`property`, `value?`): `Promise`\\<`void`\\>\n\nSet a property value on this entity, or attach the property without a\nvalue when `value` is omitted.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `property` | [`PropertyDefinition`](../../../properties/property-definition/classes/PropertyDefinition.md) |\n| `value?` | [`SetPropertyValue`](../../../../../generated/properties/types.gen/type-aliases/SetPropertyValue.md) |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`setProperty`](../../../entity/classes/PropertiedEntity.md#setproperty)\n\n***\n\n### timelinePage()\n\n> **timelinePage**(`selection?`): `Promise`\\<\\{ `entries`: (\\{ `activity`: \\{ `action`: `string`; `actor`: [`User`](../../../users/user/classes/User.md); `actor_id`: `string`; `id`: `string`; `occurred_at`: `string`; `payload?`: `unknown`; \\}; `message?`: `undefined`; `type`: `\"activity\"`; \\} \\| \\{ `activity?`: `undefined`; `message`: [`Message`](../../message/classes/Message.md); `type`: `\"message\"`; \\})[]; `nextCursor`: [`MessageCursor`](../../../../../generated/storage/types.gen/type-aliases/MessageCursor.md) \\| `undefined`; `previousCursor`: [`MessageCursor`](../../../../../generated/storage/types.gen/type-aliases/MessageCursor.md) \\| `undefined`; \\}\\>\n\nA mixed message and activity timeline page.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `selection?` | [`TimelineOptions`](../../../messages/thread/interfaces/TimelineOptions.md)\\<[`Message`](../../message/classes/Message.md)\\> |\n\n#### Returns\n\n`Promise`\\<\\{ `entries`: (\\{ `activity`: \\{ `action`: `string`; `actor`: [`User`](../../../users/user/classes/User.md); `actor_id`: `string`; `id`: `string`; `occurred_at`: `string`; `payload?`: `unknown`; \\}; `message?`: `undefined`; `type`: `\"activity\"`; \\} \\| \\{ `activity?`: `undefined`; `message`: [`Message`](../../message/classes/Message.md); `type`: `\"message\"`; \\})[]; `nextCursor`: [`MessageCursor`](../../../../../generated/storage/types.gen/type-aliases/MessageCursor.md) \\| `undefined`; `previousCursor`: [`MessageCursor`](../../../../../generated/storage/types.gen/type-aliases/MessageCursor.md) \\| `undefined`; \\}\\>\n\n***\n\n### toJSON()\n\n> **toJSON**(): \\{ `detail?`: [`ApiChannelDetail`](../../../../../generated/storage/types.gen/type-aliases/ApiChannelDetail.md); `id`: `string`; \\}\n\n#### Returns\n\n\\{ `detail?`: [`ApiChannelDetail`](../../../../../generated/storage/types.gen/type-aliases/ApiChannelDetail.md); `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `detail?` | [`ApiChannelDetail`](../../../../../generated/storage/types.gen/type-aliases/ApiChannelDetail.md) |\n| `id` | `string` |\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`toJSON`](../../../entity/classes/PropertiedEntity.md#tojson)\n\n***\n\n### typing()\n\n> **typing**(`action`, `opts?`): `Promise`\\<`void`\\>\n\nBroadcast a typing indicator, optionally scoped to a thread.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `action` | `\"stop\"` \\| `\"start\"` |\n| `opts?` | \\{ `thread?`: [`Thread`](../../thread/classes/Thread.md); \\} |\n| `opts.thread?` | [`Thread`](../../thread/classes/Thread.md) |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### unfavorite()\n\n> **unfavorite**(): `Promise`\\<`Channel`\\>\n\nRemove this entity from the user's favorites. Returns this handle for chaining.\n\n#### Returns\n\n`Promise`\\<`Channel`\\>\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`unfavorite`](../../../entity/classes/PropertiedEntity.md#unfavorite)\n\n***\n\n### updatePropertyOptions()\n\n> **updatePropertyOptions**(`changes`): `Promise`\\<[`BulkUpdateEntityPropertyOptionsResponse`](../../../../../generated/properties/types.gen/type-aliases/BulkUpdateEntityPropertyOptionsResponse.md)\\>\n\nApply multi-select option deltas atomically across this entity's properties.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `changes` | [`PropertyOptionDelta`](../../../entity/interfaces/PropertyOptionDelta.md)[] |\n\n#### Returns\n\n`Promise`\\<[`BulkUpdateEntityPropertyOptionsResponse`](../../../../../generated/properties/types.gen/type-aliases/BulkUpdateEntityPropertyOptionsResponse.md)\\>\n\n#### Inherited from\n\n[`PropertiedEntity`](../../../entity/classes/PropertiedEntity.md).[`updatePropertyOptions`](../../../entity/classes/PropertiedEntity.md#updatepropertyoptions)\n\n***\n\n### webUrl()\n\n> **webUrl**(): `string`\n\nThe channel's URL in the Macro web app.\n\n#### Returns\n\n`string`\n\n***\n\n### byId()\n\n> `static` **byId**(`client`, `id`): `Channel`\n\nA handle to a channel by id. Details load on first access.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `id` | `string` |\n\n#### Returns\n\n`Channel`\n\n***\n\n### create()\n\n> `static` **create**(`client`, `opts`): `Promise`\\<`Channel`\\>\n\nCreate a channel. The caller becomes the owner.\n\n#### Parameters\n\n| Parameter | Type | Description |\n| ------ | ------ | ------ |\n| `client` | `MacroClient` | - |\n| `opts` | \\{ `name?`: `string`; `participants?`: [`User`](../../../users/user/classes/User.md)[]; `team?`: [`Team`](../../../teams/team/classes/Team.md); `type`: [`ChannelType`](../../../../../generated/storage/types.gen/type-aliases/ChannelType.md); \\} | - |\n| `opts.name?` | `string` | - |\n| `opts.participants?` | [`User`](../../../users/user/classes/User.md)[] | Participants to add, excluding the owner. |\n| `opts.team?` | [`Team`](../../../teams/team/classes/Team.md) | Team, for team channels. |\n| `opts.type` | [`ChannelType`](../../../../../generated/storage/types.gen/type-aliases/ChannelType.md) | - |\n\n#### Returns\n\n`Promise`\\<`Channel`\\>\n\n***\n\n### dm()\n\n> `static` **dm**(`client`, `recipient`): `Promise`\\<`Channel`\\>\n\nOpen (creating if needed) the DM channel with a user.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `recipient` | [`User`](../../../users/user/classes/User.md) |\n\n#### Returns\n\n`Promise`\\<`Channel`\\>\n\n***\n\n### private()\n\n> `static` **private**(`client`, `recipients`): `Promise`\\<`Channel`\\>\n\nOpen (creating if needed) the private group channel with a set of users.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `recipients` | [`User`](../../../users/user/classes/User.md)[] |\n\n#### Returns\n\n`Promise`\\<`Channel`\\>\n\n\n\n# Type Alias: ChannelAttachment\n\n> **ChannelAttachment** = [`GetChannelAttachmentsResponses`](../../../../../generated/storage/types.gen/type-aliases/GetChannelAttachmentsResponses.md)\\[`200`\\]\\[`\"items\"`\\]\\[`number`\\]\n\nA file or entity attached to a message in a channel.\n\n\n\n# Type Alias: ChannelParticipant\n\n> **ChannelParticipant** = [`GetChannelParticipantsResponses`](../../../../../generated/storage/types.gen/type-aliases/GetChannelParticipantsResponses.md)\\[`200`\\]\\[`number`\\]\n\nA member of a channel, with their role and join time.\n\n\n\n# Class: ChannelLabelsNamespace\n\nShared or private channel labels: named groups of channels shown in the Chat\nsidebar. Every member of the team sees the same labels in the same order,\nand any member may create, rename, delete, or move channels between them;\neach change applies to the whole team. Users without a team have private\nlabels belonging to their account.\n\nA label's `channelIds` lists only the channels the authenticated user is a\nmember of. Manual labels count all assignments; smart tags count visible\nmatches. A channel can be in one manual label and any number of matching\nsmart tags per team or account. Direct messages cannot be grouped.\n\n## Constructors\n\n### Constructor\n\n> **new ChannelLabelsNamespace**(`client`): `ChannelLabelsNamespace`\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n\n#### Returns\n\n`ChannelLabelsNamespace`\n\n## Methods\n\n### create()\n\n> **create**(`name`, `channels?`): `Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\nCreate a label and atomically move `channels` into it. Names are unique\nwithin the team or account, case-insensitively.\n\n#### Parameters\n\n| Parameter | Type | Default value |\n| ------ | ------ | ------ |\n| `name` | `string` | `undefined` |\n| `channels?` | [`Channel`](../../channel/classes/Channel.md)[] | `[]` |\n\n#### Returns\n\n`Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\n***\n\n### createSmartTag()\n\n> **createSmartTag**(`name`, `rule`): `Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\nCreate a smart tag whose membership follows an attribute rule automatically.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `name` | `string` |\n| `rule` | [`ChannelLabelRule`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabelRule.md) |\n\n#### Returns\n\n`Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\n***\n\n### delete()\n\n> **delete**(`label`): `Promise`\\<`void`\\>\n\nDelete a label for the whole team; its channels return to the plain list.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `label` | [`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md) |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### list()\n\n> **list**(): `Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)[]\\>\n\nEvery label in the authenticated user's shared or private scope.\n\n#### Returns\n\n`Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)[]\\>\n\n***\n\n### previewSmartTag()\n\n> **previewSmartTag**(`rule`): `Promise`\\<[`SmartTagPreview`](../../../../../generated/storage/types.gen/type-aliases/SmartTagPreview.md)\\>\n\nPreview the first matches and total visible count without creating a tag.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `rule` | [`ChannelLabelRule`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabelRule.md) |\n\n#### Returns\n\n`Promise`\\<[`SmartTagPreview`](../../../../../generated/storage/types.gen/type-aliases/SmartTagPreview.md)\\>\n\n***\n\n### rename()\n\n> **rename**(`label`, `name`): `Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\nRename a label for the whole team.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `label` | [`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md) |\n| `name` | `string` |\n\n#### Returns\n\n`Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\n***\n\n### setLabel()\n\n> **setLabel**(`channel`, `label`): `Promise`\\<`void`\\>\n\nMove a channel into `label`, or out of any label when `label` is `undefined`.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `channel` | [`Channel`](../../channel/classes/Channel.md) |\n| `label` | [`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md) \\| `undefined` |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### updateSmartTag()\n\n> **updateSmartTag**(`label`, `name`, `rule`): `Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\nUpdate the name and matching rule of an existing smart tag.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `label` | [`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md) |\n| `name` | `string` |\n| `rule` | [`ChannelLabelRule`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabelRule.md) |\n\n#### Returns\n\n`Promise`\\<[`ChannelLabel`](../../../../../generated/storage/types.gen/type-aliases/ChannelLabel.md)\\>\n\n\n\n# Class: Message\n\nA message in a channel. A thin handle: `id` and `channelId` are known up\nfront, and record-backed fields (`content`, `author`) load lazily on first\naccess and cache. Actions carry the ids for you.\n\n## Extends\n\n- [`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md)\\<[`Message`](../../../../../generated/storage/types.gen/type-aliases/Message.md)\\>\n\n## Properties\n\n| Property | Modifier | Type | Default value | Description | Overrides | Inherited from |\n| ------ | ------ | ------ | ------ | ------ | ------ | ------ |\n| `channelId` | `readonly` | `string` | `undefined` | - | - | - |\n| `content` | `readonly` | () => `Promise`\\<`string`\\> | `...` | The message body. | - | - |\n| `createdAt` | `readonly` | () => `Promise`\\<`string`\\> | `...` | When the message was created. | - | - |\n| `editedAt` | `readonly` | () => `Promise`\\<`string` \\| `undefined`\\> | `...` | When the message was last edited, if it has been. | - | - |\n| `entityType` | `readonly` | `\"channel_message\"` | `'channel_message'` | Favorites identify channel messages as `channel_message`. | [`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`entityType`](../../../entity/classes/FavoritableEntity.md#property-entitytype) | - |\n| `id` | `public readonly` | `string` | `undefined` | - | - | [`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`id`](../../../entity/classes/FavoritableEntity.md#property-id) |\n| `mentions` | `readonly` | [`SimpleMention`](../../../../../generated/storage/types.gen/type-aliases/SimpleMention.md)[] | `[]` | Entities mentioned in this message, when a list, thread, or event supplied them. | - | - |\n| `updatedAt` | `readonly` | () => `Promise`\\<`string`\\> | `...` | When the message was last updated. | - | - |\n\n## Methods\n\n### author()\n\n> **author**(): `Promise`\\<[`User`](../../../users/user/classes/User.md)\\>\n\nThe user who sent this message.\n\n#### Returns\n\n`Promise`\\<[`User`](../../../users/user/classes/User.md)\\>\n\n***\n\n### channel()\n\n> **channel**(): [`Channel`](../../channel/classes/Channel.md)\n\nThe channel this message is in.\n\n#### Returns\n\n[`Channel`](../../channel/classes/Channel.md)\n\n***\n\n### delete()\n\n> **delete**(): `Promise`\\<`void`\\>\n\nDelete this message.\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### edit()\n\n> **edit**(`body`): `Promise`\\<`Message`\\>\n\nReplace this message's body.\n\n#### Parameters\n\n| Parameter | Type | Description |\n| ------ | ------ | ------ |\n| `body` | `string` \\| [`RichMessage`](../../../../mentions/interfaces/RichMessage.md) | Plain text, or a rich body composed with msg. |\n\n#### Returns\n\n`Promise`\\<`Message`\\>\n\n***\n\n### favorite()\n\n> **favorite**(): `Promise`\\<`Message`\\>\n\nAdd this entity to the user's favorites. Returns this handle for chaining.\nPlain unwrap: favoriting alters the user's favorites collection, not this\nentity's own detail, so there's nothing cached to invalidate.\n\n#### Returns\n\n`Promise`\\<`Message`\\>\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`favorite`](../../../entity/classes/FavoritableEntity.md#favorite)\n\n***\n\n### permission()\n\n> **permission**(): `Promise`\\<\\{ `permission`: [`EntityPermission`](../../../../../generated/storage/types.gen/type-aliases/EntityPermission.md); `status`: `\"access\"`; \\} \\| \\{ `status`: `\"no_access\"`; \\}\\>\n\nRead the caller's effective permission on this entity.\n\n#### Returns\n\n`Promise`\\<\\{ `permission`: [`EntityPermission`](../../../../../generated/storage/types.gen/type-aliases/EntityPermission.md); `status`: `\"access\"`; \\} \\| \\{ `status`: `\"no_access\"`; \\}\\>\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`permission`](../../../entity/classes/FavoritableEntity.md#permission)\n\n***\n\n### react()\n\n> **react**(`emoji`): `Promise`\\<`Message`\\>\n\nAdd an emoji reaction to this message.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `emoji` | `string` |\n\n#### Returns\n\n`Promise`\\<`Message`\\>\n\n***\n\n### reply()\n\n> **reply**(`body`): `Promise`\\<`Message`\\>\n\nPost a reply in this message's thread.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `body` | `string` \\| [`RichMessage`](../../../../mentions/interfaces/RichMessage.md) |\n\n#### Returns\n\n`Promise`\\<`Message`\\>\n\n***\n\n### thread()\n\n> **thread**(): [`Thread`](../../thread/classes/Thread.md)\n\nThe thread rooted at this message.\n\n#### Returns\n\n[`Thread`](../../thread/classes/Thread.md)\n\n***\n\n### toJSON()\n\n> **toJSON**(): \\{ `detail?`: [`Message`](../../../../../generated/storage/types.gen/type-aliases/Message.md); `id`: `string`; \\}\n\n#### Returns\n\n\\{ `detail?`: [`Message`](../../../../../generated/storage/types.gen/type-aliases/Message.md); `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `detail?` | [`Message`](../../../../../generated/storage/types.gen/type-aliases/Message.md) |\n| `id` | `string` |\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`toJSON`](../../../entity/classes/FavoritableEntity.md#tojson)\n\n***\n\n### unfavorite()\n\n> **unfavorite**(): `Promise`\\<`Message`\\>\n\nRemove this entity from the user's favorites. Returns this handle for chaining.\n\n#### Returns\n\n`Promise`\\<`Message`\\>\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`unfavorite`](../../../entity/classes/FavoritableEntity.md#unfavorite)\n\n***\n\n### unreact()\n\n> **unreact**(`emoji`): `Promise`\\<`Message`\\>\n\nRemove one of your emoji reactions from this message.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `emoji` | `string` |\n\n#### Returns\n\n`Promise`\\<`Message`\\>\n\n***\n\n### byId()\n\n> `static` **byId**(`client`, `channelId`, `id`, `mentions?`): `Message`\n\nA handle to a message by id. Fields load on first access.\n\n#### Parameters\n\n| Parameter | Type | Default value |\n| ------ | ------ | ------ |\n| `client` | `MacroClient` | `undefined` |\n| `channelId` | `string` | `undefined` |\n| `id` | `string` | `undefined` |\n| `mentions?` | [`SimpleMention`](../../../../../generated/storage/types.gen/type-aliases/SimpleMention.md)[] | `[]` |\n\n#### Returns\n\n`Message`\n\n***\n\n### from()\n\n> `static` **from**(`client`, `channelId`, `data`): `Message`\n\nBuild a root or reply from a message record (pre-seeded, no fetch).\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `channelId` | `string` |\n| `data` | [`Message`](../../../../../generated/storage/types.gen/type-aliases/Message.md) |\n\n#### Returns\n\n`Message`\n\n\n\n# Type Alias: MessageData\n\n> **MessageData** = [`Message`](../../../../../generated/storage/types.gen/type-aliases/Message.md)\n\nThe API's representation of a message; the same record the document comment API returns.\n\n\n\n# Class: ChannelNamespace\n\n## Constructors\n\n### Constructor\n\n> **new ChannelNamespace**(`client`): `ChannelNamespace`\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n\n#### Returns\n\n`ChannelNamespace`\n\n## Properties\n\n| Property | Modifier | Type | Description |\n| ------ | ------ | ------ | ------ |\n| `labels` | `readonly` | [`ChannelLabelsNamespace`](../../labels/classes/ChannelLabelsNamespace.md) | Team-shared labels that group channels in the Chat sidebar. |\n\n## Methods\n\n### activity()\n\n> **activity**(): `Promise`\\<[`ChannelActivity`](../../activity/classes/ChannelActivity.md)[]\\>\n\nRecent channel activity, with channel and user handles.\n\n#### Returns\n\n`Promise`\\<[`ChannelActivity`](../../activity/classes/ChannelActivity.md)[]\\>\n\n***\n\n### byId()\n\n> **byId**(`id`): [`Channel`](../../channel/classes/Channel.md)\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `id` | `string` |\n\n#### Returns\n\n[`Channel`](../../channel/classes/Channel.md)\n\n***\n\n### create()\n\n> **create**(`opts`): `Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\nCreate a channel. The caller becomes the owner.\n\n#### Parameters\n\n| Parameter | Type | Description |\n| ------ | ------ | ------ |\n| `opts` | \\{ `name?`: `string`; `participants?`: [`User`](../../../users/user/classes/User.md)[]; `team?`: [`Team`](../../../teams/team/classes/Team.md); `type`: [`ChannelType`](../../../../../generated/storage/types.gen/type-aliases/ChannelType.md); \\} | - |\n| `opts.name?` | `string` | - |\n| `opts.participants?` | [`User`](../../../users/user/classes/User.md)[] | Participants to add, excluding the owner. |\n| `opts.team?` | [`Team`](../../../teams/team/classes/Team.md) | Team, for team channels. |\n| `opts.type` | [`ChannelType`](../../../../../generated/storage/types.gen/type-aliases/ChannelType.md) | - |\n\n#### Returns\n\n`Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\n***\n\n### dm()\n\n> **dm**(`recipient`): `Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `recipient` | [`User`](../../../users/user/classes/User.md) |\n\n#### Returns\n\n`Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\n***\n\n### list()\n\n> **list**(): `Promise`\\<[`Channel`](../../channel/classes/Channel.md)[]\\>\n\nAll channels the authenticated user is a member of.\n\n#### Returns\n\n`Promise`\\<[`Channel`](../../channel/classes/Channel.md)[]\\>\n\n***\n\n### private()\n\n> **private**(`recipients`): `Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\nOpen (creating if needed) the private group channel with a set of users.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `recipients` | [`User`](../../../users/user/classes/User.md)[] |\n\n#### Returns\n\n`Promise`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\n***\n\n### search()\n\n> **search**(`query`, `opts?`): `AsyncGenerator`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\nSearch channels by name (client-side fuzzy filter on the full channel list)\nor by content (server-side unified search).\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `query` | `string` |\n| `opts?` | [`SearchOpts`](../../../search/interfaces/SearchOpts.md) |\n\n#### Returns\n\n`AsyncGenerator`\\<[`Channel`](../../channel/classes/Channel.md)\\>\n\n\n\n# Class: ChannelPicture\n\nAn uploaded channel profile picture in static file storage.\n\n## Properties\n\n| Property | Modifier | Type |\n| ------ | ------ | ------ |\n| `id` | `readonly` | `string` |\n\n## Methods\n\n### toJSON()\n\n> **toJSON**(): \\{ `id`: `string`; \\}\n\nSerialize the uploaded picture reference.\n\n#### Returns\n\n\\{ `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `id` | `string` |\n\n***\n\n### byId()\n\n> `static` **byId**(`id`): `ChannelPicture`\n\nConstruct a reference from the upload receipt.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `id` | `string` |\n\n#### Returns\n\n`ChannelPicture`\n\n\n\n# Function: postToChannel()\n\n> **postToChannel**(`client`, `target`, `body`): `Promise`\\<`string`\\>\n\nPost a message using the endpoint supported by the caller's credentials.\n\nMost credentials use `POST /messages/channel/{id}`. User-owned bots without\nan acting user instead use `POST /channels/{id}/webhook`, which authorizes\nthrough channel membership. The bot owner is read from `GET /bots/me` and\ncached by the client.\n\nThe webhook endpoint supports content only. Mentions in `` tags still\nwork, but threads and attachments are not supported.\n\nPosting to a `Thread` puts the message in that thread; to a `Channel`, at the\nchannel root. One target rather than a channel plus a thread id, so the two\ncan't disagree.\n\n## Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `target` | [`Channel`](../../channel/classes/Channel.md) \\| [`Thread`](../../thread/classes/Thread.md) |\n| `body` | [`RichMessage`](../../../../mentions/interfaces/RichMessage.md) |\n\n## Returns\n\n`Promise`\\<`string`\\>\n\n\n\n# Class: Thread\n\nA thread within a channel, rooted at a top-level message. Replies and\nthread-scoped posting live here.\n\n## Constructors\n\n### Constructor\n\n> **new Thread**(`client`, `channelId`, `rootId`): `Thread`\n\n#### Parameters\n\n| Parameter | Type | Description |\n| ------ | ------ | ------ |\n| `client` | `MacroClient` | - |\n| `channelId` | `string` | - |\n| `rootId` | `string` | The id of the message this thread hangs off. |\n\n#### Returns\n\n`Thread`\n\n## Properties\n\n| Property | Modifier | Type | Description |\n| ------ | ------ | ------ | ------ |\n| `channelId` | `readonly` | `string` | - |\n| `rootId` | `readonly` | `string` | The id of the message this thread hangs off. |\n\n## Methods\n\n### channel()\n\n> **channel**(): [`Channel`](../../channel/classes/Channel.md)\n\nThe channel this thread is in.\n\n#### Returns\n\n[`Channel`](../../channel/classes/Channel.md)\n\n***\n\n### replies()\n\n> **replies**(): `Promise`\\<[`Message`](../../message/classes/Message.md)[]\\>\n\nList the replies in this thread, oldest first.\n\n#### Returns\n\n`Promise`\\<[`Message`](../../message/classes/Message.md)[]\\>\n\n***\n\n### reply()\n\n> **reply**(`body`): `Promise`\\<[`Message`](../../message/classes/Message.md)\\>\n\nPost a reply in this thread.\n\n#### Parameters\n\n| Parameter | Type | Description |\n| ------ | ------ | ------ |\n| `body` | `string` \\| [`RichMessage`](../../../../mentions/interfaces/RichMessage.md) | Plain text, or a rich body composed with msg. |\n\n#### Returns\n\n`Promise`\\<[`Message`](../../message/classes/Message.md)\\>\n\nThe created reply.\n\n***\n\n### root()\n\n> **root**(): [`Message`](../../message/classes/Message.md)\n\nThe message this thread hangs off.\n\n#### Returns\n\n[`Message`](../../message/classes/Message.md)\n\n***\n\n### toJSON()\n\n> **toJSON**(): \\{ `channelId`: `string`; `rootId`: `string`; \\}\n\nSerialize the thread identity without its authenticated client.\n\n#### Returns\n\n\\{ `channelId`: `string`; `rootId`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `channelId` | `string` |\n| `rootId` | `string` |\n\n\n\n# Class: WhatsAppAccount\n\nThe current user's WhatsApp account, connected through the Macro app.\n\n## Extends\n\n- [`MacroEntity`](../../../entity/classes/MacroEntity.md)\\<[`LinkedAccount`](../../../../../generated/storage/types.gen/type-aliases/LinkedAccount.md)\\>\n\n## Properties\n\n| Property | Modifier | Type | Description | Inherited from |\n| ------ | ------ | ------ | ------ | ------ |\n| `id` | `public readonly` | `string` | - | [`MacroEntity`](../../../entity/classes/MacroEntity.md).[`id`](../../../entity/classes/MacroEntity.md#property-id) |\n| `identity` | `readonly` | () => `Promise`\\<`string` \\| `undefined`\\> | Verified remote identity, absent until pairing succeeds. | - |\n| `owner` | `readonly` | () => `Promise`\\<[`User`](../../../users/user/classes/User.md)\\> | The Macro user who owns this account. | - |\n| `state` | `readonly` | () => `Promise`\\<[`LinkedAccountState`](../../../../../generated/storage/types.gen/type-aliases/LinkedAccountState.md)\\> | Account connection state; call refresh before polling it again. | - |\n\n## Methods\n\n### refresh()\n\n> **refresh**(): `Promise`\\<`WhatsAppAccount`\\>\n\nDiscard cached connection state and load its current value.\n\n#### Returns\n\n`Promise`\\<`WhatsAppAccount`\\>\n\n***\n\n### toJSON()\n\n> **toJSON**(): \\{ `detail?`: [`LinkedAccount`](../../../../../generated/storage/types.gen/type-aliases/LinkedAccount.md); `id`: `string`; \\}\n\n#### Returns\n\n\\{ `detail?`: [`LinkedAccount`](../../../../../generated/storage/types.gen/type-aliases/LinkedAccount.md); `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `detail?` | [`LinkedAccount`](../../../../../generated/storage/types.gen/type-aliases/LinkedAccount.md) |\n| `id` | `string` |\n\n#### Inherited from\n\n[`MacroEntity`](../../../entity/classes/MacroEntity.md).[`toJSON`](../../../entity/classes/MacroEntity.md#tojson)\n\n***\n\n### byId()\n\n> `static` **byId**(`client`, `id`): `WhatsAppAccount`\n\nReference an account; reads still enforce the authenticated owner's scope.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `id` | `string` |\n\n#### Returns\n\n`WhatsAppAccount`\n\n***\n\n### from()\n\n> `static` **from**(`client`, `record`): `WhatsAppAccount`\n\nSeed an account handle from an API record.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `record` | [`LinkedAccount`](../../../../../generated/storage/types.gen/type-aliases/LinkedAccount.md) |\n\n#### Returns\n\n`WhatsAppAccount`\n\n\n\n# Class: WhatsAppChat\n\nOne chat of the current user's WhatsApp chat list: a group or a direct chat.\n\n## Extends\n\n- [`MacroEntity`](../../../entity/classes/MacroEntity.md)\\<[`LinkedChat`](../../../../../generated/storage/types.gen/type-aliases/LinkedChat.md)\\>\n\n## Properties\n\n| Property | Modifier | Type | Description | Inherited from |\n| ------ | ------ | ------ | ------ | ------ |\n| `archived` | `readonly` | () => `Promise`\\<`boolean`\\> | Archived in WhatsApp. | - |\n| `canAdd` | `readonly` | () => `Promise`\\<`boolean`\\> | Whether adding it to Macro works now. | - |\n| `cannotAddReason` | `readonly` | () => `Promise`\\<`string` \\| `undefined`\\> | Why it can't be added, when it is neither added, shared nor addable. | - |\n| `group` | `readonly` | () => `Promise`\\<[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) \\| `undefined`\\> | The current user's own Macro group for this chat, once added. | - |\n| `id` | `public readonly` | `string` | - | [`MacroEntity`](../../../entity/classes/MacroEntity.md).[`id`](../../../entity/classes/MacroEntity.md#property-id) |\n| `kind` | `readonly` | () => `Promise`\\<[`LinkedChatKind`](../../../../../generated/storage/types.gen/type-aliases/LinkedChatKind.md)\\> | `group` or `direct`. | - |\n| `lastActivityAt` | `readonly` | () => `Promise`\\<`string` \\| `undefined`\\> | Newest activity WhatsApp reported, when known. | - |\n| `name` | `readonly` | () => `Promise`\\<`string`\\> | Group subject (or member names), or the other person's name or number. | - |\n| `pinned` | `readonly` | () => `Promise`\\<`boolean`\\> | Pinned in WhatsApp. | - |\n| `shared` | `readonly` | () => `Promise`\\<[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) \\| `undefined`\\> | Someone else's Macro group for this chat, shared with the current user. | - |\n\n## Methods\n\n### adopt()\n\n> **adopt**(): `Promise`\\<[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)\\>\n\nAdd a group chat to Macro as the current user's own group; its earlier\nmessages are imported from the phone. Adding it again returns the same\ngroup. Direct chats can't be added yet.\n\n#### Returns\n\n`Promise`\\<[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)\\>\n\n***\n\n### refresh()\n\n> **refresh**(): `Promise`\\<`WhatsAppChat`\\>\n\nReload the chat's list entry.\n\n#### Returns\n\n`Promise`\\<`WhatsAppChat`\\>\n\n***\n\n### toJSON()\n\n> **toJSON**(): \\{ `detail?`: [`LinkedChat`](../../../../../generated/storage/types.gen/type-aliases/LinkedChat.md); `id`: `string`; \\}\n\n#### Returns\n\n\\{ `detail?`: [`LinkedChat`](../../../../../generated/storage/types.gen/type-aliases/LinkedChat.md); `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `detail?` | [`LinkedChat`](../../../../../generated/storage/types.gen/type-aliases/LinkedChat.md) |\n| `id` | `string` |\n\n#### Inherited from\n\n[`MacroEntity`](../../../entity/classes/MacroEntity.md).[`toJSON`](../../../entity/classes/MacroEntity.md#tojson)\n\n***\n\n### byId()\n\n> `static` **byId**(`client`, `id`): `WhatsAppChat`\n\nReference a chat by its WhatsApp JID.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `id` | `string` |\n\n#### Returns\n\n`WhatsAppChat`\n\n***\n\n### from()\n\n> `static` **from**(`client`, `record`): `WhatsAppChat`\n\nSeed a chat handle from the chat list.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `record` | [`LinkedChat`](../../../../../generated/storage/types.gen/type-aliases/LinkedChat.md) |\n\n#### Returns\n\n`WhatsAppChat`\n\n\n\n# Class: WhatsAppGroup\n\nIndependently shareable adopted WhatsApp group entity.\n\n## Extends\n\n- [`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md)\\<[`WhatsAppGroupDetail`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupDetail.md)\\>\n\n## Properties\n\n| Property | Modifier | Type | Description | Overrides | Inherited from |\n| ------ | ------ | ------ | ------ | ------ | ------ |\n| `backfill` | `readonly` | () => `Promise`\\<[`LinkedBackfill`](../../../../../generated/storage/types.gen/type-aliases/LinkedBackfill.md) \\| `undefined`\\> | Importing earlier messages from the owner's phone after adding, if any. | - | - |\n| `canSend` | `readonly` | () => `Promise`\\<`boolean`\\> | Whether the caller can send from their own connected account. | - | - |\n| `entityType` | `readonly` | `\"whatsapp_group\"` | Canonical sharing and favorites kind. | [`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`entityType`](../../../entity/classes/FavoritableEntity.md#property-entitytype) | - |\n| `id` | `public readonly` | `string` | - | - | [`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`id`](../../../entity/classes/FavoritableEntity.md#property-id) |\n| `limits` | `readonly` | () => `Promise`\\<[`LinkedCapabilities`](../../../../../generated/storage/types.gen/type-aliases/LinkedCapabilities.md)\\> | Supported sizes and mutation window. | - | - |\n| `name` | `readonly` | () => `Promise`\\<`string`\\> | Display name. | - | - |\n| `owner` | `readonly` | () => `Promise`\\<[`User`](../../../users/user/classes/User.md)\\> | Actual owner, independent of WhatsApp membership. | - | - |\n| `roster` | `readonly` | () => `Promise`\\<[`LinkedGroupInfo`](../../../../../generated/storage/types.gen/type-aliases/LinkedGroupInfo.md) \\| `undefined`\\> | Current remote roster, when available. | - | - |\n| `sendingIdentity` | `readonly` | () => `Promise`\\<`string` \\| `undefined`\\> | Caller's own verified sending identity. | - | - |\n| `sendReason` | `readonly` | () => `Promise`\\<`NonNullable`\\ \\| `undefined`\\> | Reason sending is blocked. | - | - |\n| `unreadCount` | `readonly` | () => `Promise`\\<`number`\\> | Per-user unread count. | - | - |\n\n## Methods\n\n### delivery()\n\n> **delivery**(`sends`): `Promise`\\<[`WhatsAppSend`](../../send/classes/WhatsAppSend.md)[]\\>\n\nRead delivery for selected caller-owned intents or messages.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `sends` | ([`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md) \\| [`WhatsAppSend`](../../send/classes/WhatsAppSend.md))[] |\n\n#### Returns\n\n`Promise`\\<[`WhatsAppSend`](../../send/classes/WhatsAppSend.md)[]\\>\n\n***\n\n### detach()\n\n> **detach**(): `Promise`\\<`void`\\>\n\nStop observation while preserving history and established sharing.\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### favorite()\n\n> **favorite**(): `Promise`\\<`WhatsAppGroup`\\>\n\nAdd this entity to the user's favorites. Returns this handle for chaining.\nPlain unwrap: favoriting alters the user's favorites collection, not this\nentity's own detail, so there's nothing cached to invalidate.\n\n#### Returns\n\n`Promise`\\<`WhatsAppGroup`\\>\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`favorite`](../../../entity/classes/FavoritableEntity.md#favorite)\n\n***\n\n### markRead()\n\n> **markRead**(`message`): `Promise`\\<`void`\\>\n\nAdvance the caller's own read cursor.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `message` | [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md) |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### messageAttachments()\n\n> **messageAttachments**(`messages`): `Promise`\\<\\{ `attachments`: \\{ `document`: [`Document`](../../../documents/document/classes/Document.md) \\| `undefined`; `fileName`: `string`; `mime`: `string`; `ordinal`: `number`; `size`: `number`; `state`: [`LinkedAttachmentState`](../../../../../generated/storage/types.gen/type-aliases/LinkedAttachmentState.md); \\}[]; `message`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); \\}[]\\>\n\nRead attachment states for selected messages.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `messages` | [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md)[] |\n\n#### Returns\n\n`Promise`\\<\\{ `attachments`: \\{ `document`: [`Document`](../../../documents/document/classes/Document.md) \\| `undefined`; `fileName`: `string`; `mime`: `string`; `ordinal`: `number`; `size`: `number`; `state`: [`LinkedAttachmentState`](../../../../../generated/storage/types.gen/type-aliases/LinkedAttachmentState.md); \\}[]; `message`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); \\}[]\\>\n\n***\n\n### messages()\n\n> **messages**(`options?`): `AsyncGenerator`\\<[`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md)\\>\n\nIterate newest-first timeline pages lazily.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `options?` | \\{ `limit?`: `number`; \\} |\n| `options.limit?` | `number` |\n\n#### Returns\n\n`AsyncGenerator`\\<[`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md)\\>\n\n***\n\n### permission()\n\n> **permission**(): `Promise`\\<\\{ `permission`: [`EntityPermission`](../../../../../generated/storage/types.gen/type-aliases/EntityPermission.md); `status`: `\"access\"`; \\} \\| \\{ `status`: `\"no_access\"`; \\}\\>\n\nRead the caller's effective permission on this entity.\n\n#### Returns\n\n`Promise`\\<\\{ `permission`: [`EntityPermission`](../../../../../generated/storage/types.gen/type-aliases/EntityPermission.md); `status`: `\"access\"`; \\} \\| \\{ `status`: `\"no_access\"`; \\}\\>\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`permission`](../../../entity/classes/FavoritableEntity.md#permission)\n\n***\n\n### permissions()\n\n> **permissions**(): `Promise`\\<[`SharePermissionV2`](../../../../../generated/storage/types.gen/type-aliases/SharePermissionV2.md)\\>\n\nRead standard link/team sharing settings.\n\n#### Returns\n\n`Promise`\\<[`SharePermissionV2`](../../../../../generated/storage/types.gen/type-aliases/SharePermissionV2.md)\\>\n\n***\n\n### refresh()\n\n> **refresh**(): `Promise`\\<`WhatsAppGroup`\\>\n\nReload detail, permissions and caller capabilities.\n\n#### Returns\n\n`Promise`\\<`WhatsAppGroup`\\>\n\n***\n\n### reply()\n\n> **reply**(`content`, `options`): `Promise`\\<[`WhatsAppSend`](../../send/classes/WhatsAppSend.md)\\>\n\nPersonal sessions and owner API credentials can send. Bots never send.\nPreserve the idempotency key; uncertain outcomes are never automatically resent.\nThe server enforces one request per second per user.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `content` | [`WhatsAppReply`](../type-aliases/WhatsAppReply.md) |\n| `options` | \\{ `idempotencyKey`: `string`; `replyTo?`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); \\} |\n| `options.idempotencyKey` | `string` |\n| `options.replyTo?` | [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md) |\n\n#### Returns\n\n`Promise`\\<[`WhatsAppSend`](../../send/classes/WhatsAppSend.md)\\>\n\n***\n\n### sends()\n\n> **sends**(): `Promise`\\<[`WhatsAppSend`](../../send/classes/WhatsAppSend.md)[]\\>\n\nRead only the caller's pending, uncertain or failed sends.\n\n#### Returns\n\n`Promise`\\<[`WhatsAppSend`](../../send/classes/WhatsAppSend.md)[]\\>\n\n***\n\n### toJSON()\n\n> **toJSON**(): \\{ `detail?`: [`WhatsAppGroupDetail`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupDetail.md); `id`: `string`; \\}\n\n#### Returns\n\n\\{ `detail?`: [`WhatsAppGroupDetail`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupDetail.md); `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `detail?` | [`WhatsAppGroupDetail`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupDetail.md) |\n| `id` | `string` |\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`toJSON`](../../../entity/classes/FavoritableEntity.md#tojson)\n\n***\n\n### unfavorite()\n\n> **unfavorite**(): `Promise`\\<`WhatsAppGroup`\\>\n\nRemove this entity from the user's favorites. Returns this handle for chaining.\n\n#### Returns\n\n`Promise`\\<`WhatsAppGroup`\\>\n\n#### Inherited from\n\n[`FavoritableEntity`](../../../entity/classes/FavoritableEntity.md).[`unfavorite`](../../../entity/classes/FavoritableEntity.md#unfavorite)\n\n***\n\n### updatePermissions()\n\n> **updatePermissions**(`update`): `Promise`\\<`void`\\>\n\nActual owner updates standard link/team sharing settings.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `update` | [`UpdateSharePermissionRequestV2`](../../../../../generated/storage/types.gen/type-aliases/UpdateSharePermissionRequestV2.md) |\n\n#### Returns\n\n`Promise`\\<`void`\\>\n\n***\n\n### upload()\n\n> **upload**(`file`): `Promise`\\<[`WhatsAppUpload`](../../upload/classes/WhatsAppUpload.md)\\>\n\nStage a supported attachment for a deliberate send.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `file` | `Blob` \\| `File` |\n\n#### Returns\n\n`Promise`\\<[`WhatsAppUpload`](../../upload/classes/WhatsAppUpload.md)\\>\n\n***\n\n### byId()\n\n> `static` **byId**(`client`, `id`): `WhatsAppGroup`\n\nReference an adopted group; access is checked on each request.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `id` | `string` |\n\n#### Returns\n\n`WhatsAppGroup`\n\n\n\n# Type Alias: WhatsAppReply\n\n> **WhatsAppReply** = \\{ `mentions?`: `string`[]; `text`: `string`; `type`: `\"text\"`; \\} \\| \\{ `caption?`: `string`; `type`: `\"media\"`; `upload`: [`WhatsAppUpload`](../../upload/classes/WhatsAppUpload.md); \\} \\| \\{ `message`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); `operation`: `\"reaction\"` \\| `\"edit\"` \\| `\"delete\"`; `text?`: `string`; `type`: `\"mutation\"`; \\}\n\nDeliberate content from the credential owner's own WhatsApp account.\n\n## Union Members\n\n### Type Literal\n\n\\{ `mentions?`: `string`[]; `text`: `string`; `type`: `\"text\"`; \\}\n\n| Name | Type | Description |\n| ------ | ------ | ------ |\n| `mentions?` | `string`[] | WhatsApp ids of mentioned participants (`roster.participants[].jid`); the text names each as `@`. |\n| `text` | `string` | - |\n| `type` | `\"text\"` | - |\n\n***\n\n### Type Literal\n\n\\{ `caption?`: `string`; `type`: `\"media\"`; `upload`: [`WhatsAppUpload`](../../upload/classes/WhatsAppUpload.md); \\}\n\n***\n\n### Type Literal\n\n\\{ `message`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); `operation`: `\"reaction\"` \\| `\"edit\"` \\| `\"delete\"`; `text?`: `string`; `type`: `\"mutation\"`; \\}\n\n\n\n# Class: WhatsAppMessage\n\nMessage in an independently shared WhatsApp group.\n\n## Extends\n\n- [`MacroEntity`](../../../entity/classes/MacroEntity.md)\\<[`WhatsAppGroupMessage`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupMessage.md)\\>\n\n## Properties\n\n| Property | Modifier | Type | Description | Inherited from |\n| ------ | ------ | ------ | ------ | ------ |\n| `attachments` | `readonly` | () => `Promise`\\<\\{ `document`: [`Document`](../../../documents/document/classes/Document.md) \\| `undefined`; `durationSeconds`: `number` \\| `undefined`; `fileName`: `string`; `height`: `number` \\| `undefined`; `mime`: `string`; `ordinal`: `number`; `presentation`: [`LinkedMediaPresentation`](../../../../../generated/storage/types.gen/type-aliases/LinkedMediaPresentation.md) \\| `undefined`; `size`: `number`; `state`: [`LinkedAttachmentState`](../../../../../generated/storage/types.gen/type-aliases/LinkedAttachmentState.md); `width`: `number` \\| `undefined`; \\}[]\\> | Attachment state and imported document handles. | - |\n| `body` | `readonly` | () => `Promise`\\<`string`\\> | Plain WhatsApp text. | - |\n| `deleted` | `readonly` | () => `Promise`\\<`boolean`\\> | Deleted for everyone. | - |\n| `deliveryState` | `readonly` | () => `Promise`\\<`NonNullable`\\ \\| `undefined`\\> | Caller-owned send's delivery state, when present. | - |\n| `editedAt` | `readonly` | () => `Promise`\\<`string` \\| `undefined`\\> | Last edit time. | - |\n| `forwarded` | `readonly` | () => `Promise`\\<`boolean`\\> | Forwarded from another chat. | - |\n| `id` | `public readonly` | `string` | - | [`MacroEntity`](../../../entity/classes/MacroEntity.md).[`id`](../../../entity/classes/MacroEntity.md#property-id) |\n| `kind` | `readonly` | () => `Promise`\\<[`WhatsAppMessageKind`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppMessageKind.md)\\> | `message`, or `system` for something that happened in the group (see `rich`). | - |\n| `mentions` | `readonly` | () => `Promise`\\<[`WhatsAppMention`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppMention.md)[]\\> | People the body @-mentions: `@` in the body is this person. | - |\n| `reactions` | `readonly` | () => `Promise`\\<[`WhatsAppGroupReaction`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupReaction.md)[]\\> | Grouped emoji counts and caller's own reaction. | - |\n| `replyTo` | `readonly` | () => `Promise`\\<`WhatsAppMessage` \\| `undefined`\\> | Quoted message in this group. | - |\n| `rich` | `readonly` | () => `Promise`\\<`NonNullable`\\ \\| `undefined`\\> | Poll (with tallies), location, contact cards, event or system line. | - |\n| `sender` | `readonly` | () => `Promise`\\<[`WhatsAppGroupSender`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupSender.md)\\> | Protocol-verified sender association and display name. | - |\n| `sentAt` | `readonly` | () => `Promise`\\<`string`\\> | WhatsApp timestamp. | - |\n\n## Accessors\n\n### groupId\n\n#### Get Signature\n\n> **get** **groupId**(): `string`\n\nGroup identity for checking reply/mutation scope.\n\n##### Returns\n\n`string`\n\n## Methods\n\n### toJSON()\n\n> **toJSON**(): \\{ `detail?`: [`WhatsAppGroupMessage`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupMessage.md); `id`: `string`; \\}\n\n#### Returns\n\n\\{ `detail?`: [`WhatsAppGroupMessage`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupMessage.md); `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `detail?` | [`WhatsAppGroupMessage`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupMessage.md) |\n| `id` | `string` |\n\n#### Inherited from\n\n[`MacroEntity`](../../../entity/classes/MacroEntity.md).[`toJSON`](../../../entity/classes/MacroEntity.md#tojson)\n\n***\n\n### byId()\n\n> `static` **byId**(`client`, `group`, `id`): `WhatsAppMessage`\n\nReference a message within its original group.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `group` | [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) |\n| `id` | `string` |\n\n#### Returns\n\n`WhatsAppMessage`\n\n***\n\n### from()\n\n> `static` **from**(`client`, `group`, `record`): `WhatsAppMessage`\n\nSeed a message handle from a timeline response.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `group` | [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) |\n| `record` | [`WhatsAppGroupMessage`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupMessage.md) |\n\n#### Returns\n\n`WhatsAppMessage`\n\n\n\n# Class: WhatsAppNamespace\n\nOperate on an account already paired through the Macro app.\n\n## Constructors\n\n### Constructor\n\n> **new WhatsAppNamespace**(`client`): `WhatsAppNamespace`\n\nBind existing-account operations to this authenticated client.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n\n#### Returns\n\n`WhatsAppNamespace`\n\n## Methods\n\n### account()\n\n> **account**(): `Promise`\\<[`WhatsAppAccount`](../../account/classes/WhatsAppAccount.md) \\| `undefined`\\>\n\nCurrent owner's account, or undefined when no account has been connected.\n\n#### Returns\n\n`Promise`\\<[`WhatsAppAccount`](../../account/classes/WhatsAppAccount.md) \\| `undefined`\\>\n\n***\n\n### byId()\n\n> **byId**(`id`): [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)\n\nReference an adopted entity without fetching it.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `id` | `string` |\n\n#### Returns\n\n[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)\n\n***\n\n### groups()\n\n> **groups**(): `Promise`\\<[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)[]\\>\n\nAdopted groups shared with the caller, including without WhatsApp.\n\n#### Returns\n\n`Promise`\\<[`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md)[]\\>\n\n\n\n# Class: WhatsAppSend\n\nOne deliberate send intent. Unknown outcomes must never be automatically resent.\n\n## Extends\n\n- [`MacroEntity`](../../../entity/classes/MacroEntity.md)\\<[`LinkedSendProgress`](../../../../../generated/storage/types.gen/type-aliases/LinkedSendProgress.md)\\>\n\n## Properties\n\n| Property | Modifier | Type | Description | Inherited from |\n| ------ | ------ | ------ | ------ | ------ |\n| `createdAt` | `readonly` | () => `Promise`\\<`string`\\> | When the send intent was created. | - |\n| `errorCode` | `readonly` | () => `Promise`\\<`string` \\| `undefined`\\> | Sanitized failure code, when present. | - |\n| `group` | `readonly` | [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) | Group entity containing this send. | - |\n| `id` | `public readonly` | `string` | - | [`MacroEntity`](../../../entity/classes/MacroEntity.md).[`id`](../../../entity/classes/MacroEntity.md#property-id) |\n| `message` | `readonly` | () => `Promise`\\<[`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md) \\| `undefined`\\> | Group message once WhatsApp accepted the send. | - |\n| `state` | `readonly` | () => `Promise`\\<[`LinkedDeliveryState`](../../../../../generated/storage/types.gen/type-aliases/LinkedDeliveryState.md)\\> | Last observed delivery state; refresh explicitly to poll again. | - |\n\n## Methods\n\n### refresh()\n\n> **refresh**(): `Promise`\\<`WhatsAppSend`\\>\n\nFetch current delivery state without retrying or submitting the message.\n\n#### Returns\n\n`Promise`\\<`WhatsAppSend`\\>\n\n***\n\n### request()\n\n> **request**(): `Promise`\\<\\{ `idempotencyKey`: `string`; `message`: [`WhatsAppReply`](../../group/type-aliases/WhatsAppReply.md); `replyTo?`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); \\}\\>\n\nInspect the original intent for reconciliation; never resubmit an unknown outcome automatically.\n\n#### Returns\n\n`Promise`\\<\\{ `idempotencyKey`: `string`; `message`: [`WhatsAppReply`](../../group/type-aliases/WhatsAppReply.md); `replyTo?`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); \\}\\>\n\n***\n\n### toJSON()\n\n> **toJSON**(): \\{ `detail?`: [`LinkedSendProgress`](../../../../../generated/storage/types.gen/type-aliases/LinkedSendProgress.md); `id`: `string`; \\}\n\n#### Returns\n\n\\{ `detail?`: [`LinkedSendProgress`](../../../../../generated/storage/types.gen/type-aliases/LinkedSendProgress.md); `id`: `string`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `detail?` | [`LinkedSendProgress`](../../../../../generated/storage/types.gen/type-aliases/LinkedSendProgress.md) |\n| `id` | `string` |\n\n#### Inherited from\n\n[`MacroEntity`](../../../entity/classes/MacroEntity.md).[`toJSON`](../../../entity/classes/MacroEntity.md#tojson)\n\n***\n\n### byId()\n\n> `static` **byId**(`client`, `group`, `id`): `WhatsAppSend`\n\nReference a send intent within its original group.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `group` | [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) |\n| `id` | `string` |\n\n#### Returns\n\n`WhatsAppSend`\n\n***\n\n### from()\n\n> `static` **from**(`client`, `group`, `record`): `WhatsAppSend`\n\nSeed a send handle from a server response.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `client` | `MacroClient` |\n| `group` | [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) |\n| `record` | [`LinkedSendProgress`](../../../../../generated/storage/types.gen/type-aliases/LinkedSendProgress.md) |\n\n#### Returns\n\n`WhatsAppSend`\n\n\n\n# Class: WhatsAppUpload\n\nA staged attachment scoped to one adopted group; use it in a deliberate media send.\n\n## Properties\n\n| Property | Modifier | Type | Description |\n| ------ | ------ | ------ | ------ |\n| `filename?` | `readonly` | `string` | Display filename supplied by the server. |\n| `groupId` | `readonly` | `string` | The group this upload can be sent to. |\n| `id` | `readonly` | `string` | Correlation identity of the staged upload. |\n| `kind?` | `readonly` | [`LinkedMediaKind`](../../../../../generated/storage/types.gen/type-aliases/LinkedMediaKind.md) | Media category supplied by the server. |\n\n## Methods\n\n### toJSON()\n\n> **toJSON**(): \\{ `filename`: `string` \\| `undefined`; `groupId`: `string`; `id`: `string`; `kind`: [`LinkedMediaKind`](../../../../../generated/storage/types.gen/type-aliases/LinkedMediaKind.md) \\| `undefined`; \\}\n\nSerialize the upload reference without credentials.\n\n#### Returns\n\n\\{ `filename`: `string` \\| `undefined`; `groupId`: `string`; `id`: `string`; `kind`: [`LinkedMediaKind`](../../../../../generated/storage/types.gen/type-aliases/LinkedMediaKind.md) \\| `undefined`; \\}\n\n| Name | Type |\n| ------ | ------ |\n| `filename` | `string` \\| `undefined` |\n| `groupId` | `string` |\n| `id` | `string` |\n| `kind` | [`LinkedMediaKind`](../../../../../generated/storage/types.gen/type-aliases/LinkedMediaKind.md) \\| `undefined` |\n\n***\n\n### byId()\n\n> `static` **byId**(`group`, `id`): `WhatsAppUpload`\n\nReference a previously staged upload in its original group.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `group` | [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) |\n| `id` | `string` |\n\n#### Returns\n\n`WhatsAppUpload`\n\n***\n\n### from()\n\n> `static` **from**(`group`, `record`): `WhatsAppUpload`\n\nSeed a handle from the server's upload receipt.\n\n#### Parameters\n\n| Parameter | Type |\n| ------ | ------ |\n| `group` | [`WhatsAppGroup`](../../group/classes/WhatsAppGroup.md) |\n| `record` | [`LinkedUploadedAttachment`](../../../../../generated/storage/types.gen/type-aliases/LinkedUploadedAttachment.md) |\n\n#### Returns\n\n`WhatsAppUpload`\n"
},
{
"name": "crm",
diff --git a/crates/code_execution/src/outbound/deno/sdk.mjs b/crates/code_execution/src/outbound/deno/sdk.mjs
index 40276b75ba6..e697f377d9a 100644
--- a/crates/code_execution/src/outbound/deno/sdk.mjs
+++ b/crates/code_execution/src/outbound/deno/sdk.mjs
@@ -1,5 +1,5 @@
// Generated by packages/code-mode-sdk/scripts/generate.ts.
-async function cO($,O,j){if(!Number.isInteger(O)||O<1)throw RangeError(`concurrency limit must be a positive integer, got ${O}`);let x=Array($.length),S=0,K=async()=>{while(S<$.length){let X=S;S+=1,x[X]=await j($[X])}},Z=Array.from({length:Math.min(O,$.length)},K);return await Promise.all(Z),x}class W${loader;value;inflight;constructor($,O){this.loader=$;this.value=O}get(){if(this.value!==void 0)return Promise.resolve(this.value);return this.inflight??=this.loader().then(($)=>{return this.value=$,$}),this.inflight}peek(){return this.value}clear(){this.value=void 0,this.inflight=void 0}}async function*u($){let O;do{let j=await $(O),x=j.nextCursor??void 0;if(O!==void 0&&x!==void 0&&JSON.stringify(x)===JSON.stringify(O))throw Error("Pagination cursor did not advance");yield*j.items,O=x}while(O!==void 0)}class f extends Error{constructor($){super($);this.name="MacroError"}}class GO extends f{status;data;constructor($,O){super(`Macro API error ${$}`);this.status=$;this.data=O;this.name="MacroApiError"}}class E extends f{constructor($){super($);this.name="MacroNotFoundError"}}function G($){if($.error!==void 0)throw new GO($.response?.status??0,$.error);return $.data}class M{client;id;detail;constructor($,O,j){this.client=$;this.id=O;this.detail=new W$(()=>this.fetch(),j)}field($){return async()=>(await this.detail.get())[$]??void 0}mappedField($,O){return async()=>O((await this.detail.get())[$]??void 0)}toJSON(){let $=this.detail.peek();return $===void 0?{id:this.id}:{id:this.id,detail:$}}async mutate($){let O=G(await $(this.client));return this.detail.clear(),O}}class z$ extends M{async permission(){return G(await this.client.storage.getEntityPermission({path:{entity_type:this.entityType,entity_id:this.id}}))}async favorite(){return G(await this.client.storage.addFavorite({body:{entityId:this.id,entityType:this.entityType}})),this}async unfavorite(){return G(await this.client.storage.removeFavoriteByEntity({path:{entity_type:this.entityType,entity_id:this.id}})),this}}class X$ extends z${propertyReference(){return{entity_type:this.propertyEntityType,entity_id:this.id}}async properties($){let{properties:O}=G(await this.client.properties.getEntityProperties({path:{entity_type:this.propertyEntityType,entity_id:this.id},query:$?.includeMetadata!==void 0?{include_metadata:$.includeMetadata}:void 0}));return O}async setProperty($,O){G(await this.client.properties.setEntityProperty({path:{...this.propertyReference(),property_id:$.id},body:{value:O??null}}))}async updatePropertyOptions($){return G(await this.client.properties.bulkUpdateEntityPropertyOptions({path:this.propertyReference(),body:{properties:$.map((O)=>({property_id:O.property.id,...O.add?{add_option_ids:O.add.map((j)=>j.id)}:{},...O.remove?{remove_option_ids:O.remove.map((j)=>j.id)}:{}}))}}))}async deleteProperty($){G(await this.client.properties.deleteEntityProperty({path:{entity_property_id:$.property.id}}))}}var w6={"<":"\\u003c",">":"\\u003e","&":"\\u0026"};function i$($,O){let j=JSON.stringify(O).replace(/[<>&]/g,(x)=>w6[x]??x);return`<${$}>${j}${$}>`}function q6($){return typeof $==="object"&&$!==null&&typeof $.toMention==="function"}var f6={toMention:()=>({tag:i$("m-group-mention",{groupAlias:"here"}),mention:{entity_type:"group",entity_id:"here"}})};function b6($,...O){let j="",x=[];for(let[S,K]of $.entries()){j+=K;let Z=O[S];if(Z===void 0)continue;if(q6(Z)){let X=Z.toMention();j+=X.tag,x.push(X.mention)}else j+=String(Z)}return{content:j,mentions:k6(x)}}function k6($){let O=new Set;return $.filter((j)=>{let x=`${j.entity_type}:${j.entity_id}`;if(O.has(x))return!1;return O.add(x),!0})}function O$($){return typeof $==="string"?{content:$,mentions:[]}:$}class B$ extends M{static byId($,O){return new B$($,O)}static from($,O){return new B$($,O.id,O)}async fetch(){let $=G(await this.client.storage.listHarnesses()).find((O)=>O.id===this.id);if(!$)throw new E(`Harness ${this.id} is unavailable`);return $}name=this.field("name");connected=this.field("connected");kind=this.field("kind");async delete(){await this.mutate(($)=>$.storage.deleteHarness({path:{harness_id:this.id}}))}}class I$ extends M{static byId($,O){return new I$($,O)}static from($,O){return new I$($,O.bot.id,O)}async fetch(){let $=G(await this.client.storage.listAgents()).find((O)=>O.bot.id===this.id);if(!$)throw new E(`Agent ${this.id} is unavailable`);return $}name=this.mappedField("bot",($)=>$.name);instructions=this.field("instructions");defaultModel=this.field("default_model");channels=this.mappedField("channel_ids",($)=>$.map((O)=>b.byId(this.client,O)));host=this.mappedField("harness_id",($)=>$?B$.byId(this.client,$):void 0);async update($){await this.mutate((O)=>O.storage.updateAgent({path:{agent_id:this.id},body:gx($)}))}}function gx($){let{channels:O,team:j,host:x,autoAcceptPermissions:S,avatarUrl:K,channelScope:Z,defaultModel:X,isCoding:Y,...L}=$;return{...L,channel_ids:O?.map((Q)=>Q.id),team_id:j?.id,harness_id:x?.id,auto_accept_permissions:S,avatar_url:K,channel_scope:Z,default_model:X,is_coding:Y}}async function Wj($,O,j){let{channelId:x,thread:S}=g6(O);if(await y6($)){let{id:Z}=G(await $.storage.entityMessageCreate({path:{parent_type:"channel",parent_id:x},body:{content:j.content,mentions:j.mentions,attachments:[],thread_id:S?.rootId??null,nonce:crypto.randomUUID()}}));return Z}if(S)throw new f("a user-owned bot can only post via the channel webhook endpoint, which "+"does not support threads — use a team-owned bot, or requestedAs(user), "+"to reply in a thread");let{message_id:K}=G(await $.storage.postChannelBotWebhook({path:{channel_id:x},body:{content:j.content},headers:{"x-macro-bot-scope":"user"}}));return K}function g6($){return"rootId"in $?{channelId:$.channelId,thread:$}:{channelId:$.id}}async function y6($){if($.authConfig.type!=="bot"||$.hasActingUser())return!0;let{owner:O}=await $.selfBot();return O?.type==="team"}class h${client;channelId;rootId;constructor($,O,j){this.client=$;this.channelId=O;this.rootId=j}toJSON(){return{channelId:this.channelId,rootId:this.rootId}}channel(){return b.byId(this.client,this.channelId)}root(){return e.byId(this.client,this.channelId,this.rootId)}async replies(){let{replies:$}=G(await this.client.storage.entityMessageGetThread({path:{parent_type:"channel",parent_id:this.channelId,id:this.rootId}}));return $.map((O)=>e.from(this.client,this.channelId,O))}async reply($){let O=await Wj(this.client,this,O$($));return e.byId(this.client,this.channelId,O)}}class e extends z${channelId;mentions;entityType="channel_message";constructor($,O,j,x=[],S){super($,j,S);this.channelId=O;this.mentions=x}get path(){return{parent_type:"channel",parent_id:this.channelId,id:this.id}}async fetch(){return G(await this.client.storage.entityMessageGetMessage({path:this.path}))}static byId($,O,j,x=[]){return new e($,O,j,x)}static from($,O,j){return new e($,O,j.id,j.mentions,j)}content=this.field("content");createdAt=this.field("created_at");editedAt=this.field("edited_at");updatedAt=this.field("updated_at");async author(){return P.byId(this.client,(await this.detail.get()).sender_id)}channel(){return b.byId(this.client,this.channelId)}thread(){return new h$(this.client,this.channelId,this.id)}async react($){return await this.mutate((O)=>O.storage.entityMessageReact({path:this.path,body:{add:!0,emoji:$}})),this}async unreact($){return await this.mutate((O)=>O.storage.entityMessageReact({path:this.path,body:{add:!1,emoji:$}})),this}async edit($){let{content:O,mentions:j}=O$($);return await this.mutate((x)=>x.storage.entityMessageEdit({path:this.path,body:{content:O,mentions:j}})),this}async delete(){await this.mutate(($)=>$.storage.entityMessageDeleteMessage({path:this.path}))}async reply($){return this.thread().reply($)}}class Bj{client;channel;record;constructor($,O,j){this.client=$;this.channel=O;this.record=j}agent(){return I$.byId(this.client,this.record.botId)}sessions(){return this.record.sessions.map(($)=>({session:i.byId(this.client,$.sessionId),isCurrent:$.isCurrent,createdAt:$.createdAt}))}turns(){return this.record.turns.map(($)=>new yx(this.client,this,$))}async startFresh($){G(await this.client.agentHarness.startFreshAgentConversation({path:{channel_id:this.channel.id,bot_id:this.record.botId},body:{sessionId:$.id}}))}toJSON(){return{channelId:this.channel.id,agentId:this.record.botId}}}class yx{client;conversation;record;constructor($,O,j){this.client=$;this.conversation=O;this.record=j}source(){return e.byId(this.client,this.conversation.channel.id,this.record.sourceMessageId)}session(){return i.byId(this.client,this.record.sessionId)}async retry($){G(await this.client.agentHarness.retryAgentConversationTurn({path:{channel_id:this.conversation.channel.id,bot_id:this.conversation.agent().id,source:this.record.sourceMessageId},body:{actionId:$}}))}toJSON(){return{channelId:this.conversation.channel.id,sourceMessageId:this.record.sourceMessageId}}}class L$ extends M{parent;toJSON(){return{...super.toJSON(),parentType:this.parent.type,parentId:this.parent.entity.id}}constructor($,O,j){super($,j);this.parent=O}static byId($,O,j){return new L$($,O,j)}get path(){return{parent_type:this.parent.type,parent_id:this.parent.entity.id,id:this.id}}async fetch(){return G(await this.client.storage.entityMessageGetThread({path:this.path})).state}resolved=this.field("resolved");async update($){let O={resolved:$.resolved,detach_anchor:$.detachAnchor,nonce:$.nonce};await this.mutate((j)=>j.storage.entityMessagePatchThread({path:this.path,body:O}))}async delete($){await this.mutate((O)=>O.storage.entityMessageDeleteThread({path:this.path,query:{nonce:$}}))}}async function w$($,O,j,x){for(let K of[...x?.messages??[],...x?.around?[x.around]:[]]){let Z="channelId"in K?{type:"channel",id:K.channelId}:("documentId"in K)?{type:"document",id:K.documentId}:("callId"in K)?{type:"call",id:K.callId}:K.parent;if(Z.type!==O.type||Z.id!==O.entity.id)throw Error("Timeline message belongs to another resource")}let S=G(await $.storage.messageTimelineEntries({path:{parent_type:O.type,parent_id:O.entity.id},query:{selection:x?JSON.stringify({cursor:x.cursor,around:x.around?.id,ids:x.messages?.map((K)=>K.id),activity_after:x.activityAfter,activity_before:x.activityBefore,anchored:x.anchored,direction:x.direction,include_deleted_threads:x.includeDeletedThreads,limit:x.limit}):void 0}}));return{entries:S.entries.map((K)=>{if(K.type==="activity")return{type:"activity",activity:{...K.activity,actor:P.byId($,K.activity.actor_id)}};return{type:"message",message:j(K.message)}}),nextCursor:S.next_cursor??void 0,previousCursor:S.previous_cursor??void 0}}var v6=50;function x$($){let{filters:O,type:j,make:x,includeCrm:S}=$;return(K,Z,X)=>{let Y=X?.searchOn??"name_content",L=X?.pageSize??v6;return u(async(Q)=>{let{results:J,next_cursor:I}=G(await K.search.unifiedSearch({body:{query:Z,match_type:"partial",search_on:Y,...O?{filters:O}:{},...S?{include_crm:!0}:{}},query:{page_size:L,...Q?{cursor:Q}:{}}}));return{items:J.filter((W)=>W.type===j).map((W)=>x(K,W)),nextCursor:I}})}}class QO{client;channel;record;constructor($,O,j){this.client=$;this.channel=O;this.record=j}get id(){return this.record.id}user(){return P.byId(this.client,this.record.user_id)}createdAt(){return this.record.created_at}async recordInteraction($){return new QO(this.client,this.channel,G(await this.client.storage.postActivity({body:{activity_type:$,channel_id:this.channel.id}})))}toJSON(){return this.record}}class b extends X${async recordActivity($){return new QO(this.client,this,G(await this.client.storage.postActivity({body:{activity_type:$,channel_id:this.id}})))}async setPicture($){await this.mutate((O)=>O.storage.setChannelPicture({path:{channel_id:this.id},body:{profile_picture_id:$?.id??null}}))}messageThreadById($){return L$.byId(this.client,{type:"channel",entity:this},$)}timelinePage($){return w$(this.client,{type:"channel",entity:this},(O)=>e.from(this.client,this.id,O),$)}async agentConversations(){return G(await this.client.agentHarness.listAgentConversations({path:{channel_id:this.id}})).conversations.map(($)=>new Bj(this.client,this,$))}entityType="channel";propertyEntityType="CHANNEL";async fetch(){return G(await this.client.storage.getChannel({path:{channel_id:this.id}}))}static byId($,O){return new b($,O)}static async dm($,O){let{channel_id:j}=G(await $.storage.getOrCreateDm({body:{recipient_id:O.id}}));return new b($,j)}static async private($,O){let{channel_id:j}=G(await $.storage.getOrCreatePrivate({body:{recipients:O.map((x)=>x.id)}}));return new b($,j)}static async create($,O){let{id:j}=G(await $.storage.createChannel({body:{channel_type:O.type,name:O.name??null,participants:(O.participants??[]).map((x)=>x.id),team_id:O.team?.id??null}}));return new b($,j)}name=this.field("channel_name");type=this.field("channel_type");async send($,O){let j=await Wj(this.client,O?.thread??this,O$($));return e.byId(this.client,this.id,j)}messages($){return this.timeline({limit:$?.pageSize})}messagesAfter($,O){return this.timeline({activity_after:$ instanceof Date?$.toISOString():$,limit:O?.pageSize})}timeline($){return u(async(O)=>{let j=G(await this.client.storage.messageTimeline({path:{parent_type:"channel",parent_id:this.id},query:{selection:JSON.stringify({...$,cursor:O})}}));return{items:j.items.map((x)=>e.from(this.client,this.id,x)),nextCursor:j.next_cursor}})}message($){return e.byId(this.client,this.id,$)}async rename($){await this.mutate((O)=>O.storage.patchChannel({path:{channel_id:this.id},body:{channel_name:$}}))}async delete(){await this.mutate(($)=>$.storage.deleteChannel({path:{channel_id:this.id}}))}async join(){await this.mutate(($)=>$.storage.joinChannel({path:{channel_id:this.id}}))}async joinCode(){let{join_code:$}=G(await this.client.storage.getChannelJoinLink({path:{channel_id:this.id}}));return $}async leave(){await this.mutate(($)=>$.storage.leaveChannel({path:{channel_id:this.id}}))}async participants(){return G(await this.client.storage.getChannelParticipants({path:{channel_id:this.id}}))}async addParticipants($){await this.mutate((O)=>O.storage.addParticipants({path:{channel_id:this.id},body:{participants:$.map((j)=>j.id)}}))}async removeParticipants($){await this.mutate((O)=>O.storage.removeParticipants({path:{channel_id:this.id},body:{participants:$.map((j)=>j.id)}}))}async typing($,O){G(await this.client.storage.entityMessageTyping({path:{parent_type:"channel",parent_id:this.id},body:{active:$==="start",thread_id:O?.thread?.rootId??null}}))}attachments($){return u(async(O)=>{let j=G(await this.client.storage.getChannelAttachments({path:{channel_id:this.id},query:{...$?.pageSize?{limit:$.pageSize}:{},...$?.type?{attachment_type:$.type}:{},...O?{cursor:O}:{}}}));return{items:j.items,nextCursor:j.next_cursor}})}webUrl(){return`${this.client.webAppUrl}/app/channel/${this.id}`}static search=x$({type:"channel",make:($,O)=>new b($,O.channel_id)})}class P extends X${entityType="user";propertyEntityType="USER";async fetch(){let{names:$}=G(await this.client.auth.getUserNamesWithEmail({body:{user_ids:[this.id]}})),O=$.find((j)=>j.id===this.id);if(!O)throw new E(`user ${this.id} not found`);return O}static byId($,O){return new P($,O)}static fromName($,O){return new P($,O.id,O)}static async me($){let{user_id:O}=G(await $.auth.getUserInfo());return new P($,O)}async name(){let $=await this.detail.get();return[$.first_name,$.last_name].filter(Boolean).join(" ")||void 0}email(){let $=this.id.startsWith("macro|")?this.id.slice(6):void 0;return $?.includes("@")?$:void 0}dm(){return b.dm(this.client,this)}toMention(){return{tag:i$("m-user-mention",{userId:this.id}),mention:{entity_type:"user",entity_id:this.id}}}}class o{client;entityType;entity;constructor($,O,j){this.client=$;this.entityType=O;this.entity=j}get path(){return{entity_type:this.entityType,entity_id:this.entity.id}}async state($){let O=G(await this.client.storage.accessRequestsState({path:this.path,query:{offset:$}}));return{accessLevel:O.accessLevel??void 0,canReview:O.canReview,owner:O.owner?P.byId(this.client,O.owner):void 0,pendingCount:O.pendingCount,supportedLevels:O.supportedLevels,nextOffset:O.nextOffset??void 0,mine:O.mine?zO.from(this.client,this,O.mine):void 0,requests:O.requests.map((j)=>zO.from(this.client,this,j))}}async submit($,O){return zO.from(this.client,this,G(await this.client.storage.accessRequestsSubmit({path:this.path,body:{requestedLevel:$,revision:O}})))}async cancel($){G(await this.client.storage.accessRequestsCancel({path:this.path,query:{revision:$}}))}async direct(){return G(await this.client.storage.accessRequestsDirect({path:this.path})).map(($)=>({user:P.byId(this.client,$.userId),accessLevel:$.accessLevel}))}async grant($,O){G(await this.client.storage.accessRequestsSetDirect({path:{...this.path,user_id:$.id},body:{accessLevel:O}}))}async remove($){G(await this.client.storage.accessRequestsRemoveDirect({path:{...this.path,user_id:$.id}}))}async resolve($,O){let j=$.access.toJSON();if(j.entity_type!==this.entityType||j.entity_id!==this.entity.id)throw Error("Access request belongs to another entity");return G(await this.client.storage.accessRequestsResolve({path:{...this.path,request_id:$.id},body:O}))}toJSON(){return this.path}}class zO extends M{access;resolvedRecord;constructor($,O,j){super($,j.id,j);this.access=O}static from($,O,j){return new zO($,O,j)}async fetch(){if(this.resolvedRecord)return this.resolvedRecord;let $;do{let O=await this.access.state($),j=[O.mine,...O.requests].find((x)=>x?.id===this.id);if(j)return j.detail.get();$=O.nextOffset}while($!==void 0);throw new E(`Access request ${this.id} is unavailable`)}revision=this.field("revision");status=this.field("status");requestedLevel=this.field("requestedLevel");requester=this.mappedField("requesterId",($)=>P.byId(this.client,$));async resolve($){this.resolvedRecord=await this.access.resolve(this,{revision:await this.revision(),grantedLevel:$}),this.detail.clear()}}function n$($){return{...$,channelSharePermissions:$.channelSharePermissions===void 0||$.channelSharePermissions===null?$.channelSharePermissions:$.channelSharePermissions.map((O)=>({channelId:O.channel.id,accessLevel:O.accessLevel,operation:O.operation}))}}class N$ extends M{parent;constructor($,O,j,x){super($,j,x);this.parent=O}static byId($,O,j){return new N$($,O,j)}static fromRecord($,O,j){return new N$($,O,j.id,j)}static async create($,O,j){return N$.createForSession($,O.session(),j,O)}static async createForSession($,O,j,x){let S=G(await $.agentHarness.createSessionReview({path:{session_id:O.id},body:{...j,executionId:x?.id??null}}));return N$.fromRecord($,O,S)}static async list($,O){return G(await $.agentHarness.listExecutionReviews({path:{session_id:O.session().id,execution_id:O.id}})).map((x)=>N$.fromRecord($,O.session(),x))}async fetch(){return G(await this.client.agentHarness.getSessionReview({path:{session_id:this.parent.id,review_id:this.id}}))}title=this.field("title");kind=this.field("kind");session(){return this.parent}async reference(){return{kind:await this.kind(),id:this.id}}async items(){return(await this.detail.get()).items.map(($)=>new vx($,{edit:(O)=>this.decide({action:"edit",item:$.id,draft:O}),submit:()=>this.decide({action:"submit",items:[$.id]}),dismiss:()=>this.decide({action:"dismiss",items:[$.id]})}))}async refresh(){return this.detail.clear(),await this.detail.get(),this}async submitAll(){await this.decide({action:"submit",items:[]})}async dismissAll(){await this.decide({action:"dismiss",items:[]})}async decide($){let O=(await this.detail.get()).revision;await this.mutate((j)=>j.agentHarness.decideSessionReview({path:{session_id:this.parent.id,review_id:this.id},body:{...$,revision:O}}))}}class vx{snapshot;actions;constructor($,O){this.snapshot=$;this.actions=O}get id(){return this.snapshot.id}get status(){return this.snapshot.status}get draft(){return this.snapshot.draft}get result(){return this.snapshot.result}async edit($){await this.actions.edit($)}async submit(){await this.actions.submit()}async dismiss(){await this.actions.dismiss()}}class uO extends M{parent;constructor($,O,j){super($,O);this.parent=j}static byId($,O,j){return new uO($,j,O)}async fetch(){return G(await this.client.agentHarness.getCodeExecution({path:{session_id:this.parent.id,execution_id:this.id}}))}reviews={create:($)=>N$.create(this.client,this,$),list:()=>N$.list(this.client,this)};session(){return this.parent}status=this.field("status");source=this.field("source");calls=this.field("calls");error=this.field("error");async result(){return(await this.detail.get()).result}async refresh(){return this.detail.clear(),await this.detail.get(),this}}class Lj{client;sessionId;dto;constructor($,O,j){this.client=$;this.sessionId=O;this.dto=j}static from($,O,j){return new Lj($,O,j)}get actionId(){return this.dto.actionId}get kind(){return this.dto.kind}get prompt(){return this.dto.prompt??void 0}get attachments(){return this.dto.attachments??[]}get actorUserId(){return this.dto.actorUserId??void 0}get createdAt(){return this.dto.createdAt}async edit($){return G(await this.client.agentHarness.editQueuedAction({path:{session_id:this.sessionId,action_id:this.actionId},body:{prompt:$}})),this.dto={...this.dto,prompt:$},this}async steer(){G(await this.client.agentHarness.steerQueuedAction({path:{session_id:this.sessionId,action_id:this.actionId}}))}async remove(){G(await this.client.agentHarness.removeQueuedAction({path:{session_id:this.sessionId,action_id:this.actionId}}))}}class dO{client;session;id;constructor($,O,j){this.client=$;this.session=O;this.id=j}static byId($,O,j){return new dO($,O,j)}async answer($){return G(await this.client.agentHarness.answerAgentSessionToolApproval({path:{session_id:this.session.id,approval_id:this.id},body:{answer:$}}))}toJSON(){return{id:this.id,sessionId:this.session.id}}}class i extends M{access=new o(this.client,"agent_session",this);toolApprovalById($){return dO.byId(this.client,this,$)}async sharePermissions(){return G(await this.client.agentHarness.getAgentSessionPermissions({path:{session_id:this.id}}))}async updateSharePermissions($){return this.mutate((O)=>O.agentHarness.updateAgentSessionPermissions({path:{session_id:this.id},body:n$($)}))}static byId($,O){return new i($,O)}static async createManaged($,O){let{session:j}=G(await $.agentHarness.createAgentSession({body:{prompt:O?.prompt,instructions:O?.instructions,model:O?.model,repoUrl:O?.repoUrl,repoBranch:O?.repoBranch}}));return new i($,j.id,j)}static async repositories($){let{repositories:O}=G(await $.agentHarness.listAgentRepositories());return O.map((j)=>({url:j.url,defaultBranch:j.defaultBranch??void 0}))}static async repositoryBranches($,O){let{branches:j}=G(await $.agentHarness.listAgentRepositoryBranches({query:{repoUrl:O}}));return j}async fetch(){return G(await this.client.agentHarness.getAgentSession({path:{session_id:this.id}}))}name=this.field("name");isArchived=this.field("isArchived");model=this.field("model");harness=this.field("harness");repoUrl=this.field("repoUrl");workspace=this.field("workspace");instructions=this.field("instructions");status=this.field("status");sandboxSize=this.field("sandboxSize");createdAt=this.field("createdAt");modifiedAt=this.field("modifiedAt");static async forPullRequest($,O){let{sessionIds:j}=G(await $.agentHarness.agentSessionsForPullRequest({body:{url:O}}));return j.map((x)=>i.byId($,x))}async pullRequests(){let{pullRequests:$}=G(await this.client.agentHarness.listAgentSessionPullRequests({path:{session_id:this.id}}));return $.map((O)=>({githubKey:O.githubKey,url:O.url,source:O.source,linkedBy:O.linkedBy?P.byId(this.client,O.linkedBy):void 0,createdAt:O.createdAt}))}async linkPullRequest($){G(await this.client.agentHarness.linkAgentSessionPullRequest({path:{session_id:this.id},body:{url:$}}))}async unlinkPullRequest($){G(await this.client.agentHarness.unlinkAgentSessionPullRequest({path:{session_id:this.id},query:{url:$}}))}async rename($){await this.mutate((O)=>O.agentHarness.renameAgentSession({path:{session_id:this.id},body:{name:$}}))}async archive(){await this.setArchived(!0)}async unarchive(){await this.setArchived(!1)}async setArchived($){await this.mutate((O)=>O.agentHarness.setAgentSessionArchived({path:{session_id:this.id},body:{isArchived:$}}))}async setSandboxSize($){let{size:O}=await this.mutate((j)=>j.agentHarness.putAgentSessionSandboxSize({path:{session_id:this.id},body:{size:$}}));return O}static async defaultSandboxSize($){return G(await $.agentHarness.getAgentSandboxSize()).size}static async setDefaultSandboxSize($,O){return G(await $.agentHarness.putAgentSandboxSize({body:{size:O}})).size}static async codingPreferences($){return G(await $.agentHarness.getAgentCodingPreferences())}static async setCodingPreferences($,O){return G(await $.agentHarness.putAgentCodingPreferences({body:O}))}async control($){return this.mutate((O)=>O.agentHarness.controlAgentSession({path:{session_id:this.id},body:$}))}prompt($,O){return this.control({type:"prompt",prompt:$,...O&&O.length>0?{attachments:O}:{}})}async queue(){let{entries:$}=G(await this.client.agentHarness.getAgentSessionQueue({path:{session_id:this.id}}));return $.map((O)=>Lj.from(this.client,this.id,O))}async changes(){return G(await this.client.agentHarness.getAgentSessionChanges({path:{session_id:this.id}}))}async changesPatch(){return G(await this.client.agentHarness.getAgentSessionChangesPatch({path:{session_id:this.id}})).patch}async refreshChanges(){return this.mutate(($)=>$.agentHarness.refreshAgentSessionChanges({path:{session_id:this.id}}))}async foldedLog(){let{bot:$,messages:O}=G(await this.client.agentHarness.getAgentSessionFoldedLog({path:{session_id:this.id}}));return{bot:$,messages:O}}async log(){return G(await this.client.agentHarness.getAgentSessionLog({path:{session_id:this.id}}))}reviews={create:($)=>N$.createForSession(this.client,this,$),byId:($)=>N$.byId(this.client,this,$)};codeExecutions={byId:($)=>uO.byId(this.client,this,$)};async delete(){await this.mutate(($)=>$.agentHarness.deleteAgentSession({path:{session_id:this.id}}))}}class hx{client;constructor($){this.client=$}byId($){return i.byId(this.client,$)}createManaged($){return i.createManaged(this.client,$)}repositories(){return i.repositories(this.client)}repositoryBranches($){return i.repositoryBranches(this.client,$)}forPullRequest($){return i.forPullRequest(this.client,$)}defaultSandboxSize(){return i.defaultSandboxSize(this.client)}setDefaultSandboxSize($){return i.setDefaultSandboxSize(this.client,$)}codingPreferences(){return i.codingPreferences(this.client)}setCodingPreferences($){return i.setCodingPreferences(this.client,$)}}class mx{client;constructor($){this.client=$}byId($){return I$.byId(this.client,$)}async list(){return G(await this.client.storage.listAgents()).map(($)=>I$.from(this.client,$))}async create($){return I$.from(this.client,G(await this.client.storage.createAgent({body:gx($)})))}}class cx{client;async usage($={}){let{users:O,...j}=$;return G(await this.client.cognition.getUsageHandler({body:{...j,include_users:O?.map((x)=>x.id)}}))}constructor($){this.client=$}async structuredCompletion($){return G(await this.client.cognition.structuredCompletion({body:$}))}async decide($){return G(await this.client.cognition.decisions({body:$}))}}class s$ extends M{static byId($,O){return new s$($,O)}static from($,O){return new s$($,O.id,{id:O.id,name:O.name,createdAt:O.createdAt})}async fetch(){let $=G(await this.client.storage.listUserApiKeys()).keys.find((O)=>O.id===this.id);if(!$)throw new E(`API key ${this.id} is unavailable`);return $}name=this.field("name");createdAt=this.field("createdAt");async delete(){await this.mutate(($)=>$.storage.deleteUserApiKey({path:{id:this.id}}))}}class ux{client;constructor($){this.client=$}byId($){return s$.byId(this.client,$)}async list(){return G(await this.client.storage.listUserApiKeys()).keys.map(($)=>s$.from(this.client,$))}async create($){let O=G(await this.client.storage.createUserApiKey({body:{name:$}}));return{key:s$.from(this.client,O),secret:O.key}}async tokenForEmail($){return G(await this.client.auth.macroApiToken({query:{email:$}}))}}class lO{client;constructor($){this.client=$}async me(){if(this.client.authConfig.type!=="bot")throw new f("bots.me() requires bot auth — a user API key has no bot identity");return G(await this.client.storage.getSelfBot())}async channels(){if(this.client.authConfig.type!=="bot")throw new f("bots.channels() requires bot auth — a user API key has no bot identity");let $=await this.me();return G(await this.client.storage.listBotChannels({path:{bot_id:$.id}}))}async myPrincipalId(){if(this.client.authConfig.type!=="bot")throw new f("bots.myPrincipalId() requires bot auth — a user API key has no bot identity");return this.client.myPrincipalId()}}class S$ extends M{async fetch(){let O=G(await this.client.auth.getUserTeams()).find((j)=>j.id===this.id);if(!O)throw new E(`team ${this.id} not found`);return O}static byId($,O){return new S$($,O)}static async current($){let O=G(await $.auth.getTeam());if(!O)throw new E("caller has no current team");return new S$($,O.team.id,O.team)}static async list($){return G(await $.auth.getUserTeams()).map((j)=>new S$($,j.id,j))}name=this.field("name");slug=this.field("slug");defaultLinkShare=this.field("default_link_share");owner=this.mappedField("owner_id",($)=>P.byId(this.client,$));async members(){let $=G(await this.client.auth.getTeam());if(!$||$.team.id!==this.id)throw new f("members are only available for the current team (see Team.current)");return $.members.map((O)=>({user:P.byId(this.client,O.user_id),role:O.role}))}}class T$ extends M{async fetch(){let{links:$}=G(await this.client.email.listLinks()),O=$.find((j)=>j.id===this.id);if(!O)throw new E(`email link ${this.id} not found`);return O}static byId($,O){return new T$($,O)}static async list($){let{links:O}=G(await $.email.listLinks());return O.map((j)=>new T$($,j.id,j))}emailAddress=this.field("email_address");provider=this.field("provider");isPrimary=this.field("is_primary");isSyncActive=this.field("is_sync_active");syncStatus=this.field("sync_status");needsReauth=this.field("needs_reauth");photoUrl=this.field("photo_url");async settingsOperations(){return G(await this.client.email.mailboxSettingsOperations({headers:{"X-Email-Link-Id":this.id}}))}}class m$ extends M{async setContributesToAvailability($){await this.mutate((O)=>O.calendar.setAvailabilityCalendar({path:{calendar_id:this.id},body:{contributesToAvailability:$}}))}async fetch(){let{calendars:$}=G(await this.client.calendar.listCalendars()),O=$.find((j)=>j.id===this.id);if(!O)throw new E(`calendar ${this.id} not found`);return O}static byId($,O){return new m$($,O)}static async list($){let{calendars:O}=G(await $.calendar.listCalendars());return O.map((j)=>new m$($,j.id,j))}name=this.field("name");emailAddress=this.field("emailAddress");isPrimary=this.field("isPrimary");isWritable=this.field("isWritable");isSubscription=this.field("isSubscription");color=this.field("color");syncError=this.field("syncError");emailLink=this.mappedField("emailLinkId",($)=>T$.byId(this.client,$))}class rO{client;id;constructor($,O){this.client=$;this.id=O}async status(){return G(await this.client.calendar.calendarReplacementStatus({path:{operation_id:this.id}}))}async confirm(){return G(await this.client.calendar.confirmCalendarReplacement({path:{operation_id:this.id}}))}async discard(){G(await this.client.calendar.discardCalendarReplacement({path:{operation_id:this.id}}))}}class $$ extends M{async notes($){return this.requireOwnOccurrence($),G(await this.client.calendar.eventNotes({path:{event_id:this.id},query:{occurrenceKey:$.occurrenceKey}})).map((j)=>q$.fromRecord(this.client,this,$,j))}async linkNote($,O,j={}){return this.requireOwnOccurrence(O),G(await this.client.calendar.linkEventNote({path:{event_id:this.id,document_id:$.id},query:{occurrenceKey:O.occurrenceKey},body:{teamId:j.team?.id??null}})),q$.forDocument(this.client,this,O,$)}requireOwnOccurrence($){if($.eventId!==this.id)throw new f("The occurrence belongs to a different event")}async fetch(){throw new f(`calendar event ${this.id} has no fetch-by-id endpoint; read the record returned by calendar.createEvent / event.update / event.rsvp instead`)}static byId($,O){return new $$($,O)}static fromRecord($,O){return new $$($,O.id,O)}title=this.field("title");description=this.field("description");location=this.field("location");time=this.field("time");status=this.field("status");attendees=this.field("attendees");transparency=this.field("transparency");isReadOnly=this.field("isReadOnly");recurrenceLines=this.field("recurrenceLines");guestPolicy=this.field("guestPolicy");viewerCan=this.field("viewerCan");calendar=this.mappedField("calendarId",($)=>$?m$.byId(this.client,$):void 0);record(){return this.detail.get()}async update($){let O=await this.mutate((j)=>j.calendar.updateCalendarEvent({path:{event_id:this.id},body:h6($)}));return $$.fromRecord(this.client,O)}async delete($={}){await this.mutate((O)=>O.calendar.deleteCalendarEvent({path:{event_id:this.id},query:{calendarId:$.calendar?.id,scope:$.scope,recurrenceId:$.recurrenceId,inbox:$.inbox}}))}async prepareReplacement($){let O=G(await this.client.calendar.prepareCalendarReplacement({path:{event_id:this.id},body:{removeConference:$.removeConference,recurrenceId:$.recurrenceId,calendarId:$.calendar?.id}}));return new rO(this.client,O.operationId)}async providerUrl($={}){return G(await this.client.calendar.calendarEventProviderUrl({path:{event_id:this.id},query:{recurrenceId:$.recurrenceId,calendarId:$.calendar?.id}})).url??void 0}async inviteGuests($){let O=await this.mutate((j)=>j.calendar.inviteCalendarEventGuests({path:{event_id:this.id},body:{guests:$.guests,scope:$.scope,recurrenceId:$.recurrenceId,calendarId:$.calendar?.id,inbox:$.inbox}}));return $$.fromRecord(this.client,O)}async addToCalendar($={}){let O=await this.mutate((j)=>j.calendar.copyCalendarEvent({path:{event_id:this.id},body:{calendarId:$.calendar?.id}}));return $$.fromRecord(this.client,O)}async ics(){return G(await this.client.storage.eventIcs({path:{event_id:this.id}}))}async rsvp($){let O=await this.mutate((j)=>j.calendar.rsvpCalendarEvent({path:{event_id:this.id},body:{response:$.response,scope:$.scope,recurrenceId:$.recurrenceId,calendarId:$.calendar?.id}}));return $$.fromRecord(this.client,O)}}function h6($){return{title:$.title,description:$.description,location:$.location,time:$.time,attendees:$.attendees,recurrenceLines:$.recurrenceLines,visibility:$.visibility,transparency:$.transparency,reminders:$.reminders,conference:$.conference,outOfOffice:$.outOfOffice,guestPolicy:$.guestPolicy,scope:$.scope,recurrenceId:$.recurrenceId,calendarId:$.calendar?.id,inbox:$.inbox}}class q$ extends M{event;occurrence;fromDocument;constructor($,O,j,x,S,K=!1){super($,x.id,S);this.event=O;this.occurrence=j;this.fromDocument=K;if(j.eventId!==O.id)throw new f("Choose an occurrence belonging to this event")}static fromDocumentLink($,O,j){return new q$($,$$.byId($,j.eventId),j,O,{documentId:O.id,name:j.documentName,team:j.team,canManage:!0},!0)}static forDocument($,O,j,x){return new q$($,O,j,x)}static fromRecord($,O,j,x){return new q$($,O,j,w.byId($,x.documentId),x)}async fetch(){if(this.fromDocument){let x=G(await this.client.calendar.documentNoteLinks({path:{document_id:this.id}})).find((S)=>S.eventId===this.event.id&&S.occurrenceKey===this.occurrence.occurrenceKey);if(!x)throw new E("This meeting link has been removed");return{documentId:this.id,name:x.documentName,team:x.team,canManage:!0}}let O=G(await this.client.calendar.eventNotes({path:{event_id:this.event.id},query:{occurrenceKey:this.occurrence.occurrenceKey}})).find((j)=>j.documentId===this.id);if(!O)throw new E("This document is no longer linked to the meeting");return O}document=this.mappedField("documentId",($)=>w.byId(this.client,$));name=this.field("name");team=this.mappedField("team",($)=>$?S$.byId(this.client,$.id):void 0);canManage=this.field("canManage");async setTeam($){await this.mutate((O)=>O.calendar.linkEventNote({path:{event_id:this.event.id,document_id:this.id},query:{occurrenceKey:this.occurrence.occurrenceKey},body:{teamId:$?.id??null}}))}async unlink(){await this.mutate(($)=>$.calendar.unlinkEventNote({path:{event_id:this.event.id,document_id:this.id},query:{occurrenceKey:this.occurrence.occurrenceKey}}))}}class n extends X${async sharePermissions(){return G(await this.client.storage.getProjectPermissionsV2({path:{id:this.id}}))}async accessLevel(){return G(await this.client.storage.getProjectUserAccessLevel({path:{id:this.id}}))}access=new o(this.client,"project",this);entityType="project";propertyEntityType="PROJECT";async fetch(){let{data:$}=G(await this.client.storage.getProjectHandler({path:{id:this.id}}));return $.projectMetadata}static byId($,O){return new n($,O)}static async create($,O){let{data:j}=G(await $.storage.createProjectHandler({body:{name:O.name,projectParentId:O.parent?.id??null}}));return new n($,j.id,j)}name=this.field("name");parent=this.mappedField("parentId",($)=>$?n.byId(this.client,$):void 0);createdAt=this.field("createdAt");async items(){let{data:$}=G(await this.client.storage.getProjectContentHandler({path:{id:this.id}}));return $.map((O)=>O.item)}async rename($){await this.mutate((O)=>O.storage.editProjectV2({path:{id:this.id},body:{name:$}}))}async delete($){await this.mutate((O)=>$?.permanent?O.storage.permanentlyDeleteProject({path:{id:this.id}}):O.storage.deleteProjectHandler({path:{id:this.id}}))}async restore(){await this.mutate(($)=>$.storage.revertDeleteProject({path:{id:this.id}}))}static search=x$({filters:{project_filters:{}},type:"project",make:($,O)=>new n($,O.id)})}class JO extends M{document;toJSON(){return{...super.toJSON(),documentId:this.document.id}}constructor($,O,j,x){super($,j,x);this.document=O}static byId($,O,j){return new JO($,O,j)}static from($,O,j){return new JO($,O,j.uuid,j)}async fetch(){let $=G(await this.client.storage.getDocumentAnchors({path:{document_id:this.document.id}})).data.find((O)=>O.uuid===this.id);if(!$)throw new E(`Annotation ${this.id} is unavailable`);return $}kind=this.field("anchorType");page=this.field("page");async edit($){if(await this.kind()!=="placeable")throw Error("Only placeable comment annotations can be edited");await this.mutate((O)=>O.storage.editAnchor({body:{...$,uuid:this.id,anchorType:"free-comment",fileType:"pdf"}}))}async delete(){if(await this.kind()!=="highlight")throw Error("Only standalone highlights can be deleted with this endpoint");await this.mutate(($)=>$.storage.deleteAnchor({body:{uuid:this.id,anchorType:"highlight",fileType:"pdf"}}))}}class c$ extends M{documentId;constructor($,O,j,x){super($,j,x);this.documentId=O}get path(){return{parent_type:"document",parent_id:this.documentId,id:this.id}}async fetch(){return G(await this.client.storage.entityMessageGetMessage({path:this.path}))}static byId($,O,j){return new c$($,O,j)}static from($,O,j){return new c$($,O,j.id,j)}text=this.field("content");createdAt=this.field("created_at");updatedAt=this.field("updated_at");async threadId(){return(await this.detail.get()).thread_id??this.id}document(){return w.byId(this.client,this.documentId)}async author(){return P.byId(this.client,(await this.detail.get()).sender_id)}async reply($){return this.document().comment($,{threadId:await this.threadId()})}async edit($){let{content:O,mentions:j}=O$($);return await this.mutate((x)=>x.storage.entityMessageEdit({path:this.path,body:{content:O,mentions:j}})),this}async delete(){await this.mutate(($)=>$.storage.entityMessageDeleteMessage({path:this.path}))}}class pO{client;document;id;detail;constructor($,O,j){this.client=$;this.document=O;this.id=j;this.detail=new W$(async()=>G(await this.client.storage.getDocumentVersion({path:{document_id:O.id,document_version_id:j}})).data)}static byId($,O,j){return new pO($,O,j)}read(){return this.detail.get()}toJSON(){return{id:this.id,documentId:this.document.id}}}var m6=8;class w extends X${async meetingNotes(){return G(await this.client.calendar.documentNoteLinks({path:{document_id:this.id}})).map((O)=>({note:q$.fromDocumentLink(this.client,this,O),eventTitle:O.eventTitle}))}async anchors(){return G(await this.client.storage.getDocumentAnchors({path:{document_id:this.id}})).data.map(($)=>JO.from(this.client,this,$))}async createHighlight($){return JO.from(this.client,this,G(await this.client.storage.createAnchor({path:{document_id:this.id},body:{...$,fileType:"pdf",anchorType:"highlight"}})))}version($){return pO.byId(this.client,this,$)}async sharePermissions(){return G(await this.client.storage.getDocumentPermissionsV2({path:{document_id:this.id}})).documentPermissions}async permissionsToken(){return G(await this.client.storage.getDocumentPermissionsToken({path:{document_id:this.id}})).token}async prepareSave($){return G(await this.client.storage.presaveDocumentHandler({path:{document_id:this.id},body:{newBom:$}})).data}async save($){return(await this.mutate((O)=>O.storage.saveDocumentHandler({path:{document_id:this.id},body:$}))).data}async simpleSave(){return(await this.mutate(($)=>$.storage.simpleSave({path:{document_id:this.id}}))).data}messageThreadById($){return L$.byId(this.client,{type:"document",entity:this},$)}timelinePage($){return w$(this.client,{type:"document",entity:this},(O)=>c$.from(this.client,this.id,O),$)}access=new o(this.client,"document",this);async fetch(){let{data:$}=G(await this.client.storage.getDocument({path:{document_id:this.id}}));return $.documentMetadata}static byId($,O){return new w($,O)}static async create($,O){let{documentId:j}=G(await $.storage.createMarkdownHandler({body:{documentName:O.name,markdown:O.markdown??null,projectId:O.project?.id??null}}));return new w($,j)}static async createSnippet($,O){let{documentId:j}=G(await $.storage.createSnippetHandler({body:{snippetName:O.name,markdown:O.markdown??null,projectId:O.project?.id??null}}));return new w($,j)}static async createSkill($,O){let{documentId:j}=G(await $.storage.createSkillHandler({body:{skillName:O.name,markdown:O.markdown??null,projectId:O.project?.id??null}}));return new w($,j)}static recent($,O){let j=O?.pageSize??50;return u(async(x)=>{let S=G(await $.storage.getUserDocumentsHandler({query:{limit:j,offset:x?Number(x):0,...O?.fileType!==void 0?{file_type:O.fileType}:{}}})),K=S.data?.next_offset;return{items:(S.data?.documents??[]).map((Z)=>new w($,Z.documentId)),nextCursor:K!=null?String(K):null}})}static async recentlyDeleted($){let{data:O}=G(await $.storage.recentlyDeleted({}));return O.items.filter((j)=>j.type==="document").map((j)=>new w($,j.id))}entityType="document";propertyEntityType="DOCUMENT";name=this.field("documentName");fileType=this.field("fileType");project=this.mappedField("projectId",($)=>$?n.byId(this.client,$):void 0);owner=this.mappedField("owner",($)=>P.byId(this.client,$));branchedFrom=this.mappedField("branchedFromId",($)=>$?w.byId(this.client,$):void 0);createdAt=this.field("createdAt");updatedAt=this.field("updatedAt");deletedAt=this.field("deletedAt");async content(){let{text:$}=G(await this.client.storage.getDocumentTextContent({path:{document_id:this.id}}));if($!==null)return $;let{presigned_url:O}=G(await this.client.storage.exportDocument({path:{document_id:this.id}})),j=await fetch(O);if(!j.ok)throw new GO(j.status,{message:"failed to download document content"});return j.text()}async rename($){await this.mutate((O)=>O.storage.editDocument({path:{document_id:this.id},body:{documentName:$}}))}async move($){await this.mutate((O)=>O.storage.editDocument({path:{document_id:this.id},body:{projectId:$?.id??""}}))}async delete($){await this.mutate((O)=>$?.permanent?O.storage.permanentlyDeleteDocument({path:{document_id:this.id}}):O.storage.deleteDocument({path:{document_id:this.id}}))}async restore(){await this.mutate(($)=>$.storage.revertDeleteDocument({path:{document_id:this.id}}))}async copy($){let{data:O}=G(await this.client.storage.copyDocument({path:{document_id:this.id},query:$.versionId!==void 0?{version_id:$.versionId}:void 0,body:{documentName:$.name}}));return new w(this.client,O.documentMetadata.documentId)}async comments(){let $={parent_type:"document",parent_id:this.id},O=u(async(x)=>{let S=G(await this.client.storage.messageTimeline({path:$,query:{selection:JSON.stringify({limit:100,cursor:x})}}));return{items:S.items,nextCursor:S.next_cursor}}),j=[];for await(let x of O)j.push(x);return cO(j,m6,async(x)=>{let{state:S,root:K,replies:Z}=G(await this.client.storage.entityMessageGetThread({path:{...$,id:x.id}}));return{thread:S,comments:[K,...Z].map((X)=>c$.from(this.client,this.id,X))}})}async comment($,O){let j=await this.mutate((x)=>x.storage.entityMessageCreate({path:{parent_type:"document",parent_id:this.id},body:{...O$($),thread_id:O?.threadId??null}}));return c$.from(this.client,this.id,j)}async shortId(){return G(await this.client.storage.getDocumentShortId({path:{document_id:this.id}}))}async teamShare(){return G(await this.client.storage.getDocumentTeamShare({path:{document_id:this.id}}))}async setTeamShare($){await this.mutate((O)=>O.storage.setDocumentTeamShare({path:{document_id:this.id},body:{shareWithTeam:$}}))}async githubPullRequests(){let{pullRequests:$}=G(await this.client.storage.getDocumentGithubPullRequests({path:{document_id:this.id}}));return $}async branchName(){let{branchName:$}=G(await this.client.storage.getDocumentBranchName({path:{document_id:this.id}}));return $}webUrl(){return`${this.client.webAppUrl}/app/md/${this.id}`}static search=x$({filters:{document_filters:{}},type:"document",make:($,O)=>new w($,O.document_id)});toMention(){let $=this.detail.peek();return{tag:i$("m-document-mention",{documentId:this.id,documentName:$?.documentName??"",blockName:$?.fileType??"md"}),mention:{entity_type:"document",entity_id:this.id}}}}class dx{client;async noteDocuments($=""){let O=G(await this.client.calendar.noteDocuments({query:{query:$}}));return{documents:O.documents.map((j)=>({document:w.byId(this.client,j.documentId),name:j.name})),teams:O.teams.map((j)=>({team:S$.byId(this.client,j.id),name:j.name}))}}async availabilityCalendars(){return G(await this.client.calendar.getAvailabilityCalendars()).calendars.map(($)=>({calendar:m$.byId(this.client,$.calendarId),name:$.name,isPrimary:$.isPrimary,contributesToAvailability:$.contributesToAvailability}))}async teamSharing(){return G(await this.client.calendar.getTeamSharing()).sharing}async setTeamSharing($){return G(await this.client.calendar.setTeamSharing({body:{sharing:$}})).sharing}occurrences($){let O;return u(async(j)=>{let x=G(await this.client.storage.listOccurrences({query:{...$,cursor:j}}));O??=u6(this.client);let S=await O;return{items:x.items.map((K)=>({event:$$.fromRecord(this.client,K.event),occurrence:K.occurrence,inbox:K.event.calendarId?S.get(K.event.calendarId):void 0,role:d6(K.event)})),nextCursor:x.nextCursor}})}async teamPage($){let O=G(await this.client.storage.listTeamCalendar({query:$}));return{items:O.items.map(({ownerId:j,...x})=>({...x,owner:P.byId(this.client,j)})),members:O.members.map((j)=>({user:P.byId(this.client,j.userId),sharing:j.sharing,coverage:j.coverage})),nextCursor:O.nextCursor??void 0}}async teamOutOfOffice($){let O=G(await this.client.storage.listTeamOutOfOffice({query:$}));return{items:O.items.map((j)=>({event:$$.byId(this.client,j.eventId),owner:P.byId(this.client,j.ownerId),occurrenceKey:j.occurrenceKey,time:j.time,title:j.title??void 0})),hasMore:O.hasMore}}constructor($){this.client=$}calendars(){return m$.list(this.client)}calendar($){return m$.byId(this.client,$)}event($){return $$.byId(this.client,$)}replacement($){return new rO(this.client,$)}async createEvent($){let O=G(await this.client.calendar.createCalendarEvent({body:c6($)}));return $$.fromRecord(this.client,O)}}function c6($){return{title:$.title,time:$.time,description:$.description,location:$.location,attendees:$.attendees,recurrenceLines:$.recurrenceLines,visibility:$.visibility,transparency:$.transparency,reminders:$.reminders,conference:$.conference,macroCall:$.macroCall,outOfOffice:$.outOfOffice,guestPolicy:$.guestPolicy,calendarId:$.calendar?.id,emailLinkId:$.emailLink?.id}}async function u6($){let{calendars:O}=G(await $.calendar.listCalendars());return new Map(O.map((j)=>[j.id,j.emailAddress]))}function d6($){let O=$.attendees.find((j)=>j.isSelf);if($.viewerCan.organize||O?.isOrganizer)return"organizer";return O?"guest":void 0}class H$ extends M{lookupToken;constructor($,O,j){super($,O,j);this.lookupToken=j?.shareToken}static byId($,O){return new H$($,O)}static from($,O){return new H$($,O.id,O)}async fetch(){if(this.lookupToken)return G(await this.client.storage.meetingLookup({path:{token:this.lookupToken}}));let $=G(await this.client.storage.meetingList()).meetings.find((O)=>O.id===this.id);if(!$)throw new E(`Meeting ${this.id} is unavailable`);return this.lookupToken=$.shareToken,$}title=this.field("title");scheduledStart=this.field("scheduledStart");scheduledEnd=this.field("scheduledEnd");channel=this.mappedField("channelId",($)=>$?b.byId(this.client,$):void 0);call=this.mappedField("callId",($)=>$?Q$.byId(this.client,$):void 0);async update($){await this.mutate((O)=>O.storage.meetingUpdate({path:{meeting_id:this.id},body:$}))}async cancel(){await this.mutate(($)=>$.storage.meetingCancel({path:{meeting_id:this.id}}))}async invitePermissions(){return G(await this.client.storage.meetingInvitePermissions({path:{token:(await this.detail.get()).shareToken}}))}async invite($){G(await this.client.storage.meetingInvite({path:{token:(await this.detail.get()).shareToken},body:{email:$}}))}async inviteUsers($){G(await this.client.storage.meetingInviteUsers({path:{token:(await this.detail.get()).shareToken},body:{userIds:$.map((O)=>O.id)}}))}async webUrl(){return`${this.client.webAppUrl}/app/meet/join/${encodeURIComponent((await this.detail.get()).shareToken)}`}}class u$ extends M{callId;constructor($,O,j,x){super($,j,x);this.callId=O}get path(){return{parent_type:"call",parent_id:this.callId,id:this.id}}async fetch(){return G(await this.client.storage.entityMessageGetMessage({path:this.path}))}static byId($,O,j){return new u$($,O,j)}static from($,O,j){return new u$($,O,j.id,j)}content=this.field("content");createdAt=this.field("created_at");updatedAt=this.field("updated_at");call(){return Q$.byId(this.client,this.callId)}async author(){return P.byId(this.client,(await this.detail.get()).sender_id)}async reply($){let O=G(await this.client.storage.entityMessageCreate({path:{parent_type:"call",parent_id:this.callId},body:{...O$($),thread_id:this.callId}}));return u$.from(this.client,this.callId,O)}async edit($){let{content:O,mentions:j}=O$($);return await this.mutate((x)=>x.storage.entityMessageEdit({path:this.path,body:{content:O,mentions:j}})),this}async delete(){await this.mutate(($)=>$.storage.entityMessageDeleteMessage({path:this.path}))}}class Q$ extends z${messageThreadById($){return L$.byId(this.client,{type:"call",entity:this},$)}timelinePage($){return w$(this.client,{type:"call",entity:this},(O)=>u$.from(this.client,this.id,O),$)}access=new o(this.client,"call",this);async meeting(){return H$.from(this.client,G(await this.client.storage.meetingShare({path:{call_id:this.id}})))}async editTranscript($){await this.mutate((O)=>O.storage.editCallTranscript({path:{call_id:this.id},body:{assignments:$}}))}async toggleTeamShare(){return this.mutate(($)=>$.storage.toggleShareWithTeam({path:{call_id:this.id}}))}entityType="call";async fetch(){return G(await this.client.storage.getCallRecord({path:{call_id:this.id}}))}static byId($,O){return new Q$($,O)}message($){return u$.byId(this.client,this.id,$)}name=this.field("customName");channel=this.mappedField("channelId",($)=>$?b.byId(this.client,$):void 0);channelName=this.field("channelName");creator=this.mappedField("createdBy",($)=>P.byId(this.client,$));isActive=this.field("isActive");status=this.field("status");roomName=this.field("roomName");startedAt=this.field("startedAt");endedAt=this.field("endedAt");durationMs=this.field("durationMs");summary=this.field("summary");transcript=this.field("transcript");participants=this.field("participants");guests=this.field("guests");teamShareAccessLevel=this.field("teamShareAccessLevel");shareWithTeam=this.field("shareWithTeam");recordingUrl=this.field("recordingUrl");recordingPreviewUrl=this.field("recordingPreviewUrl");recordingStartedAt=this.field("recordingStartedAt");async rename($){await this.mutate((O)=>O.storage.editCallRecord({path:{call_id:this.id},body:{customName:$??""}}))}async setTeamShare($){await this.mutate((O)=>O.storage.editCallRecord({path:{call_id:this.id},body:{sharePermission:{teamShareAccessLevel:$?"view":null}}}))}async delete(){await this.mutate(($)=>$.storage.deleteCallRecord({path:{call_id:this.id}}))}static search=x$({filters:{call_filters:{}},type:"call",make:($,O)=>new Q$($,O.call_id)})}class lx{client;constructor($){this.client=$}byId($){return Q$.byId(this.client,$)}search($,O){return Q$.search(this.client,$,O)}}class rx{client;constructor($){this.client=$}async list(){let{labels:$}=G(await this.client.storage.listChannelLabels());return $}async create($,O=[]){return G(await this.client.storage.createChannelLabel({body:{name:$,channelIds:O.map((j)=>j.id)}}))}async createSmartTag($,O){return G(await this.client.storage.createChannelLabel({body:{name:$,rule:O}}))}async previewSmartTag($){return G(await this.client.storage.previewSmartTag({body:$}))}async updateSmartTag($,O,j){return G(await this.client.storage.renameChannelLabel({path:{label_id:$.id},body:{name:O,rule:j}}))}async rename($,O){return G(await this.client.storage.renameChannelLabel({path:{label_id:$.id},body:{name:O}}))}async delete($){G(await this.client.storage.deleteChannelLabel({path:{label_id:$.id}}))}async setLabel($,O){G(await this.client.storage.setChannelLabel({path:{channel_id:$.id},body:{labelId:O?.id??null}}))}}class px{client;async activity(){return G(await this.client.storage.getActivity()).map(($)=>new QO(this.client,b.byId(this.client,$.channel_id),$))}labels;constructor($){this.client=$;this.labels=new rx($)}byId($){return b.byId(this.client,$)}async dm($){return b.dm(this.client,$)}async private($){return b.private(this.client,$)}async create($){return b.create(this.client,$)}async list(){let{items:$}=G(await this.client.storage.getChannels());return $.map((O)=>b.byId(this.client,O.id.toString()))}async*search($,O){if(!O?.searchOn||O.searchOn==="name"){let{items:j}=G(await this.client.storage.getChannels()),x=$.toLowerCase();for(let S of j)if(S.name?.toLowerCase().includes(x))yield b.byId(this.client,S.id.toString())}else yield*b.search(this.client,$,O)}}class aO{client;chat;id;message;constructor($,O,j,x){this.client=$;this.chat=O;this.id=j;this.message=x}static byId($,O,j,x){return new aO($,O,j,x)}async stop(){G(await this.client.cognition.stopChatStream({body:{chat_id:this.chat.id,stream_id:this.id}}))}toJSON(){return{id:this.id,chatId:this.chat.id,messageId:this.message.id}}}class iO extends M{chat;constructor($,O,j){super($,j);this.chat=O}static byId($,O,j){return new iO($,O,j)}async fetch(){let $=G(await this.client.cognition.getChat({path:{chat_id:this.chat.id}})).chat.messages.find((O)=>O.id===this.id);if(!$)throw new E(`Chat message ${this.id} is not yet available`);return $}content=this.field("content");role=this.field("role");async refresh(){this.detail.clear(),await this.detail.get()}toJSON(){return{...super.toJSON(),chatId:this.chat.id}}}class K$ extends X${async send($){let{attachments:O,additionalInstructions:j,...x}=$,S=await this.mutate((Z)=>Z.cognition.sendChatMessage({body:{...x,chat_id:this.id,additional_instructions:j,attachments:O?.map((X)=>({entity_id:X.entity.id,entity_type:X.type}))}})),K=K$.byId(this.client,S.chat_id);return aO.byId(this.client,K,S.stream_id,iO.byId(this.client,K,S.message_id))}access=new o(this.client,"chat",this);entityType="chat";propertyEntityType="CHAT";async fetch(){let{chat:$}=G(await this.client.cognition.getChat({path:{chat_id:this.id}}));return $}static byId($,O){return new K$($,O)}static async create($,O){let{id:j}=G(await $.cognition.createChat({body:{name:O?.name??null,projectId:O?.project?.id??null}}));return new K$($,j)}name=this.field("name");model=this.field("model");project=this.mappedField("projectId",($)=>$?n.byId(this.client,$):void 0);createdAt=this.field("createdAt");updatedAt=this.field("updatedAt");messages=this.field("messages");async rename($){await this.mutate((O)=>O.cognition.patchChat({path:{chat_id:this.id},body:{name:$}}))}async delete($){await this.mutate((O)=>$?.permanent?O.cognition.permanentlyDeleteChat({path:{chat_id:this.id}}):O.cognition.deleteChat({path:{chat_id:this.id}}))}async copy(){let{id:$}=await this.mutate((O)=>O.cognition.copyChat({path:{chat_id:this.id}}));return new K$(this.client,$)}async restore(){await this.mutate(($)=>$.cognition.revertDeleteChat({path:{chat_id:this.id}}))}async history(){let{conversation:$}=G(await this.client.cognition.getChatHistoryHandler({path:{chat_id:this.id}}));return $}webUrl(){return`${this.client.webAppUrl}/app/chat/${this.id}`}static search=x$({filters:{chat_filters:{}},type:"chat",make:($,O)=>new K$($,O.chat_id)});toMention(){return{tag:i$("m-document-mention",{documentId:this.id,documentName:this.detail.peek()?.name??"",blockName:"chat",blockParams:{}}),mention:{entity_type:"chat",entity_id:this.id}}}}class ax{client;constructor($){this.client=$}byId($){return K$.byId(this.client,$)}create($){return K$.create(this.client,$)}search($,O){return K$.search(this.client,$,O)}}var l6=8;class M$ extends M{parent;constructor($,O,j,x){super($,j,x);this.parent=O}get path(){return{parent_type:this.parent.type,parent_id:this.parent.id,id:this.id}}async fetch(){return G(await this.client.storage.entityMessageGetMessage({path:this.path}))}static byId($,O,j){return new M$($,O,j)}static from($,O,j){return new M$($,O,j.id,j)}text=this.field("content");createdAt=this.field("created_at");updatedAt=this.field("updated_at");deletedAt=this.field("deleted_at");async threadId(){return(await this.detail.get()).thread_id??this.id}async author(){return P.byId(this.client,(await this.detail.get()).sender_id)}async reply($){return nO(this.client,this.parent,$,{threadId:await this.threadId()})}async edit($){let{content:O,mentions:j}=O$($);return await this.mutate((x)=>x.storage.entityMessageEdit({path:this.path,body:{content:O,mentions:j}})),this}async delete(){await this.mutate(($)=>$.storage.entityMessageDeleteMessage({path:this.path}))}}async function Nj($,O){let j={parent_type:O.type,parent_id:O.id},x=u(async(K)=>{let Z=G(await $.storage.messageTimeline({path:j,query:{selection:JSON.stringify({limit:100,cursor:K})}}));return{items:Z.items,nextCursor:Z.next_cursor}}),S=[];for await(let K of x)S.push(K);return cO(S,l6,async(K)=>{let{state:Z,root:X,replies:Y}=G(await $.storage.entityMessageGetThread({path:{...j,id:K.id}}));return{thread:Z,comments:[X,...Y].map((L)=>M$.from($,O,L))}})}async function nO($,O,j,x){let S=G(await $.storage.entityMessageCreate({path:{parent_type:O.type,parent_id:O.id},body:{...O$(j),thread_id:x?.threadId??null}}));return M$.from($,O,S)}class f$ extends z${messageThreadById($){return L$.byId(this.client,{type:"crm_contact",entity:this},$)}timelinePage($){return w$(this.client,{type:"crm_contact",entity:this},(O)=>M$.from(this.client,{type:"crm_contact",id:this.id},O),$)}entityType="crm_contact";async fetch(){return G(await this.client.storage.getContact({path:{contact_id:this.id}}))}static byId($,O){return new f$($,O)}static from($,O){return new f$($,O.id,O)}name=this.field("name");email=this.field("email");company=this.mappedField("companyId",($)=>b$.byId(this.client,$));hidden=this.field("hidden");createdAt=this.field("createdAt");updatedAt=this.field("updatedAt");firstInteraction=this.field("firstInteraction");lastInteraction=this.field("lastInteraction");async hide(){await this.setHidden(!0)}async unhide(){await this.setHidden(!1)}async setHidden($){await this.mutate((O)=>O.storage.setContactHidden({path:{contact_id:this.id},body:{hidden:$}}))}async rename($){await this.mutate((O)=>O.storage.setCrmContactName({path:{contact_id:this.id},body:{name:$}}))}get commentParent(){return{type:"crm_contact",id:this.id}}async comments(){return Nj(this.client,this.commentParent)}async comment($,O){return nO(this.client,this.commentParent,$,O)}commentById($){return M$.byId(this.client,this.commentParent,$)}}class b$ extends X${messageThreadById($){return L$.byId(this.client,{type:"crm_company",entity:this},$)}timelinePage($){return w$(this.client,{type:"crm_company",entity:this},(O)=>M$.from(this.client,{type:"crm_company",id:this.id},O),$)}entityType="crm_company";propertyEntityType="COMPANY";async fetch(){return G(await this.client.storage.getCompany({path:{company_id:this.id}}))}static byId($,O){return new b$($,O)}static from($,O){return new b$($,O.id,O)}name=this.field("name");description=this.field("description");hidden=this.field("hidden");emailSync=this.field("emailSync");team=this.mappedField("teamId",($)=>S$.byId(this.client,$));createdAt=this.field("createdAt");updatedAt=this.field("updatedAt");async domain(){let{domains:$}=await this.detail.get();return $[0]?.domain}async domains(){let{domains:$}=await this.detail.get();return $.map((O)=>O.domain)}async contacts(){return G(await this.client.storage.listCompanyContacts({path:{company_id:this.id}})).map((O)=>f$.from(this.client,O))}static search=x$({filters:{crm_company_filters:{}},type:"company",includeCrm:!0,make:($,O)=>new b$($,O.id)});async hide(){await this.setHidden(!0)}async unhide(){await this.setHidden(!1)}async setHidden($){await this.mutate((O)=>O.storage.setCompanyHidden({path:{company_id:this.id},body:{hidden:$}}))}async setEmailSync($){await this.mutate((O)=>O.storage.setEmailSync({path:{company_id:this.id},body:{email_sync:$}}))}async rename($){await this.mutate((O)=>O.storage.setCrmCompanyName({path:{company_id:this.id},body:{name:$}}))}async createContact($){let O=await this.mutate((j)=>j.storage.createCrmContact({path:{company_id:this.id},body:$}));return f$.from(this.client,O)}get commentParent(){return{type:"crm_company",id:this.id}}async comments(){return Nj(this.client,this.commentParent)}async comment($,O){return nO(this.client,this.commentParent,$,O)}commentById($){return M$.byId(this.client,this.commentParent,$)}}class t$ extends M{tableDetail=new W$(async()=>G(await this.client.storage.getCrmPipelineTable({path:{id:this.id}})));static byId($,O){return new t$($,O)}static from($,O){return new t$($,O.id,O)}async fetch(){return G(await this.client.storage.getCrmPipeline({path:{id:this.id}}))}name=this.field("name");recordType=this.field("recordType");accessLevel=this.field("grant");sharing=this.field("sharing");owner=this.mappedField("userId",($)=>P.byId(this.client,$));team=this.mappedField("teamId",($)=>S$.byId(this.client,$));async rename($){await this.mutate((O)=>O.storage.renameCrmPipeline({path:{id:this.id},body:{name:$}}))}async setSharing($){if($===void 0)throw Error("Sharing is required");await this.mutate((O)=>O.storage.shareCrmPipeline({path:{id:this.id},body:{sharing:$}}))}async setTrashed($){await this.mutate((O)=>O.storage.trashCrmPipeline({path:{id:this.id},body:{trashed:$}}))}async sharePermissions(){return G(await this.client.storage.getCrmPipelinePermissions({path:{id:this.id}}))}async updateSharePermissions($){return this.mutate((O)=>O.storage.updateCrmPipelinePermissions({path:{id:this.id},body:n$($)}))}async table(){return Uj.from(this.client,this,await this.tableRecord())}tableRecord(){return this.tableDetail.get()}rows(){return u(async($)=>{let O=G(await this.client.storage.getCrmPipelineRows({path:{id:this.id},query:{after:$}}));return{items:O.rows.map((j)=>VO.from(this.client,this,j)),nextCursor:O.next}})}queryRows($={}){if($.rows?.some((O)=>O.pipeline.id!==this.id))throw Error("Rows belong to another pipeline");return u(async(O)=>{let j=G(await this.client.storage.queryCrmPipelineRows({path:{id:this.id},body:{after:O,query:$.query,rowIds:$.rows?.map((x)=>x.id)}}));return{items:j.rows.map((x)=>VO.from(this.client,this,x)),nextCursor:j.next}})}async applyOps($,O){if(O?.baseVersions?.some((S)=>S.table.pipeline.id!==this.id))throw Error("Table belongs to another pipeline");let j={ops:$,baseVersions:O?.baseVersions?Object.fromEntries(O.baseVersions.map((S)=>[S.table.id,S.version])):void 0},x=await this.mutate((S)=>S.storage.applyCrmPipelineOps({path:{id:this.id},body:j}));return this.tableDetail.clear(),x}}class Uj extends M{pipeline;toJSON(){return{...super.toJSON(),pipelineId:this.pipeline.id}}constructor($,O,j){super($,j.table.id,j);this.pipeline=O}static from($,O,j){return new Uj($,O,j)}fetch(){return this.pipeline.tableRecord()}async columns(){return(await this.pipeline.tableRecord()).columns.map(($)=>zj.from(this.client,this,$))}async version(){return(await this.pipeline.tableRecord()).table.version}rowById($){return VO.byId(this.client,this.pipeline,$)}rows(){return this.pipeline.rows()}}class VO extends M{pipeline;toJSON(){return{...super.toJSON(),pipelineId:this.pipeline.id}}constructor($,O,j,x){super($,j,x);this.pipeline=O}static byId($,O,j){return new VO($,O,j)}static from($,O,j){return new VO($,O,j.rowId,j)}async fetch(){for await(let $ of this.pipeline.queryRows({rows:[this]}))if($.id===this.id)return $.detail.get();throw new E(`Pipeline row ${this.id} is unavailable`)}cells=this.field("cells");async refresh(){this.detail.clear(),await this.detail.get()}}class zj extends M{table;toJSON(){return{...super.toJSON(),pipelineId:this.table.pipeline.id,tableId:this.table.id}}constructor($,O,j){super($,j.column.id,j);this.table=O}static from($,O,j){return new zj($,O,j)}async fetch(){let $=(await this.table.pipeline.tableRecord()).columns.find((O)=>O.column.id===this.id);if(!$)throw new E(`Pipeline column ${this.id} is unavailable`);return $}async writable(){return(await this.fetch()).writable}async name(){return(await this.fetch()).column.display_name??(await this.fetch()).definition.definition.display_name}}class ix{client;constructor($){this.client=$}byId($){return t$.byId(this.client,$)}async list(){return G(await this.client.storage.listCrmPipelines()).map(($)=>t$.from(this.client,$))}async create($){return t$.from(this.client,G(await this.client.storage.createCrmPipeline({body:$})))}}class nx{client;pipelines;constructor($){this.client=$;this.pipelines=new ix($)}companyById($){return b$.byId(this.client,$)}contactById($){return f$.byId(this.client,$)}async contactByEmail($){let{contact:O}=G(await this.client.storage.getContactByEmail({query:{email:$}}));return O?f$.from(this.client,O):void 0}async searchContacts($,O={}){let{contacts:j}=G(await this.client.storage.searchContacts({query:{query:$,limit:O.limit}}));return j.map((x)=>f$.from(this.client,x))}async createCompany($){return b$.from(this.client,G(await this.client.storage.createCrmCompany({body:$})))}async settings(){return G(await this.client.storage.getCrmTeamSettings())}async updateSettings($){return G(await this.client.storage.putCrmTeamSettings({body:$}))}async setStages($){return G(await this.client.storage.putCrmTeamStages({body:$}))}async resetStages(){G(await this.client.storage.resetCrmTeamStages())}searchCompanies($){return b$.search(this.client,$)}}var G$=Symbol.for("@ts-pattern/matcher"),v1=Symbol.for("@ts-pattern/isVariadic");var sx=($)=>Boolean($&&typeof $=="object"),Ij=($)=>$&&!!$[G$],s=($,O,j)=>{if(Ij($)){let x=$[G$](),{matched:S,selections:K}=x.match(O);return S&&K&&Object.keys(K).forEach((Z)=>j(Z,K[Z])),S}if(sx($)){if(!sx(O))return!1;if(Array.isArray($)){if(!Array.isArray(O))return!1;let x=[],S=[],K=[];for(let Z of $.keys()){let X=$[Z];Ij(X)&&X[v1]?K.push(X):K.length?S.push(X):x.push(X)}if(K.length){if(K.length>1)throw Error("Pattern error: Using `...P.array(...)` several times in a single pattern is not allowed.");if(O.lengths(L,Z[Q],j))&&S.every((L,Q)=>s(L,X[Q],j))&&(K.length===0||s(K[0],Y,j))}return $.length===O.length&&$.every((Z,X)=>s(Z,O[X],j))}return Reflect.ownKeys($).every((x)=>{let S=$[x];return((x in O)||Ij(K=S)&&K[G$]().matcherType==="optional")&&s(S,O[x],j);var K})}return Object.is(O,$)},Y$=($)=>{var O,j,x;return sx($)?Ij($)?(O=(j=(x=$[G$]()).getSelectionKeys)==null?void 0:j.call(x))!=null?O:[]:Array.isArray($)?sO($,Y$):sO(Object.values($),Y$):[]},sO=($,O)=>$.reduce((j,x)=>j.concat(O(x)),[]);function r6(...$){if($.length===1){let[O]=$;return(j)=>s(O,j,()=>{})}if($.length===2){let[O,j]=$;return s(O,j,()=>{})}throw Error(`isMatching wasn't given the right number of arguments: expected 1 or 2, received ${$.length}.`)}function t($){return Object.assign($,{optional:()=>ox($),and:(O)=>d($,O),or:(O)=>h1($,O),select:(O)=>O===void 0?tO($):tO(O,$)})}function tx($){return Object.assign(((O)=>Object.assign(O,{[Symbol.iterator](){let j=0,x=[{value:Object.assign(O,{[v1]:!0}),done:!1},{done:!0,value:void 0}];return{next:()=>{var S;return(S=x[j++])!=null?S:x.at(-1)}}}}))($),{optional:()=>tx(ox($)),select:(O)=>tx(O===void 0?tO($):tO(O,$))})}function ox($){return t({[G$]:()=>({match:(O)=>{let j={},x=(S,K)=>{j[S]=K};return O===void 0?(Y$($).forEach((S)=>x(S,void 0)),{matched:!0,selections:j}):{matched:s($,O,x),selections:j}},getSelectionKeys:()=>Y$($),matcherType:"optional"})})}var p6=($,O)=>{for(let j of $)if(!O(j))return!1;return!0},a6=($,O)=>{for(let[j,x]of $.entries())if(!O(x,j))return!1;return!0},i6=($,O)=>{let j=Reflect.ownKeys($);for(let x of j)if(!O(x,$[x]))return!1;return!0};function d(...$){return t({[G$]:()=>({match:(O)=>{let j={},x=(S,K)=>{j[S]=K};return{matched:$.every((S)=>s(S,O,x)),selections:j}},getSelectionKeys:()=>sO($,Y$),matcherType:"and"})})}function h1(...$){return t({[G$]:()=>({match:(O)=>{let j={},x=(S,K)=>{j[S]=K};return sO($,Y$).forEach((S)=>x(S,void 0)),{matched:$.some((S)=>s(S,O,x)),selections:j}},getSelectionKeys:()=>sO($,Y$),matcherType:"or"})})}function m($){return{[G$]:()=>({match:(O)=>({matched:Boolean($(O))})})}}function tO(...$){let O=typeof $[0]=="string"?$[0]:void 0,j=$.length===2?$[1]:typeof $[0]=="string"?void 0:$[0];return t({[G$]:()=>({match:(x)=>{let S={[O!=null?O:"@ts-pattern/anonymous-select-key"]:x};return{matched:j===void 0||s(j,x,(K,Z)=>{S[K]=Z}),selections:S}},getSelectionKeys:()=>[O!=null?O:"@ts-pattern/anonymous-select-key"].concat(j===void 0?[]:Y$(j))})})}function m1($){return!0}function k$($){return typeof $=="number"}function e$($){return typeof $=="string"}function o$($){return typeof $=="bigint"}var c1=t(m(m1)),n6=t(m(m1)),s6=c1,$O=($)=>Object.assign(t($),{startsWith:(O)=>{return $O(d($,(j=O,m((x)=>e$(x)&&x.startsWith(j)))));var j},endsWith:(O)=>{return $O(d($,(j=O,m((x)=>e$(x)&&x.endsWith(j)))));var j},minLength:(O)=>$O(d($,((j)=>m((x)=>e$(x)&&x.length>=j))(O))),length:(O)=>$O(d($,((j)=>m((x)=>e$(x)&&x.length===j))(O))),maxLength:(O)=>$O(d($,((j)=>m((x)=>e$(x)&&x.length<=j))(O))),includes:(O)=>{return $O(d($,(j=O,m((x)=>e$(x)&&x.includes(j)))));var j},regex:(O)=>{return $O(d($,(j=O,m((x)=>e$(x)&&Boolean(x.match(j))))));var j}}),y1=$O(m(e$)),g$=($)=>Object.assign(t($),{between:(O,j)=>g$(d($,((x,S)=>m((K)=>k$(K)&&x<=K&&S>=K))(O,j))),lt:(O)=>g$(d($,((j)=>m((x)=>k$(x)&&xg$(d($,((j)=>m((x)=>k$(x)&&x>j))(O))),lte:(O)=>g$(d($,((j)=>m((x)=>k$(x)&&x<=j))(O))),gte:(O)=>g$(d($,((j)=>m((x)=>k$(x)&&x>=j))(O))),int:()=>g$(d($,m((O)=>k$(O)&&Number.isInteger(O)))),finite:()=>g$(d($,m((O)=>k$(O)&&Number.isFinite(O)))),positive:()=>g$(d($,m((O)=>k$(O)&&O>0))),negative:()=>g$(d($,m((O)=>k$(O)&&O<0)))}),t6=g$(m(k$)),OO=($)=>Object.assign(t($),{between:(O,j)=>OO(d($,((x,S)=>m((K)=>o$(K)&&x<=K&&S>=K))(O,j))),lt:(O)=>OO(d($,((j)=>m((x)=>o$(x)&&xOO(d($,((j)=>m((x)=>o$(x)&&x>j))(O))),lte:(O)=>OO(d($,((j)=>m((x)=>o$(x)&&x<=j))(O))),gte:(O)=>OO(d($,((j)=>m((x)=>o$(x)&&x>=j))(O))),positive:()=>OO(d($,m((O)=>o$(O)&&O>0))),negative:()=>OO(d($,m((O)=>o$(O)&&O<0)))}),e6=OO(m(o$)),o6=t(m(function($){return typeof $=="boolean"})),$7=t(m(function($){return typeof $=="symbol"})),O7=t(m(function($){return $==null})),j7=t(m(function($){return $!=null})),Z$={__proto__:null,matcher:G$,optional:ox,array:function(...$){return tx({[G$]:()=>({match:(O)=>{if(!Array.isArray(O))return{matched:!1};if($.length===0)return{matched:!0};let j=$[0],x={};if(O.length===0)return Y$(j).forEach((K)=>{x[K]=[]}),{matched:!0,selections:x};let S=(K,Z)=>{x[K]=(x[K]||[]).concat([Z])};return{matched:O.every((K)=>s(j,K,S)),selections:x}},getSelectionKeys:()=>$.length===0?[]:Y$($[0])})})},set:function(...$){return t({[G$]:()=>({match:(O)=>{if(!(O instanceof Set))return{matched:!1};let j={};if(O.size===0)return{matched:!0,selections:j};if($.length===0)return{matched:!0};let x=(K,Z)=>{j[K]=(j[K]||[]).concat([Z])},S=$[0];return{matched:p6(O,(K)=>s(S,K,x)),selections:j}},getSelectionKeys:()=>$.length===0?[]:Y$($[0])})})},map:function(...$){return t({[G$]:()=>({match:(O)=>{if(!(O instanceof Map))return{matched:!1};let j={};if(O.size===0)return{matched:!0,selections:j};let x=(X,Y)=>{j[X]=(j[X]||[]).concat([Y])};if($.length===0)return{matched:!0};var S;if($.length===1)throw Error(`\`P.map\` wasn't given enough arguments. Expected (key, value), received ${(S=$[0])==null?void 0:S.toString()}`);let[K,Z]=$;return{matched:a6(O,(X,Y)=>{let L=s(K,Y,x),Q=s(Z,X,x);return L&&Q}),selections:j}},getSelectionKeys:()=>$.length===0?[]:[...Y$($[0]),...Y$($[1])]})})},record:function(...$){return t({[G$]:()=>({match:(O)=>{if(O===null||typeof O!="object"||Array.isArray(O))return{matched:!1};var j;if($.length===0)throw Error(`\`P.record\` wasn't given enough arguments. Expected (value) or (key, value), received ${(j=$[0])==null?void 0:j.toString()}`);let x={},S=(X,Y)=>{x[X]=(x[X]||[]).concat([Y])},[K,Z]=$.length===1?[y1,$[0]]:$;return{matched:i6(O,(X,Y)=>{let L=typeof X!="string"||Number.isNaN(Number(X))?null:Number(X),Q=L!==null&&s(K,L,S),J=s(K,X,S),I=s(Z,Y,S);return(J||Q)&&I}),selections:x}},getSelectionKeys:()=>$.length===0?[]:[...Y$($[0]),...Y$($[1])]})})},intersection:d,union:h1,not:function($){return t({[G$]:()=>({match:(O)=>({matched:!s($,O,()=>{})}),getSelectionKeys:()=>[],matcherType:"not"})})},when:m,select:tO,any:c1,unknown:n6,_:s6,string:y1,number:t6,bigint:e6,boolean:o6,symbol:$7,nullish:O7,nonNullable:j7,instanceOf:function($){return t(m(function(O){return(j)=>j instanceof O}($)))},shape:function($){return t(m(r6($)))}};class u1 extends Error{constructor($){let O;try{O=JSON.stringify($)}catch(j){O=$}super(`Pattern matching error: no pattern matches value ${O}`),this.input=void 0,this.input=$}}var ex={matched:!1,value:void 0};function c($){return new Tj($,ex)}class Tj{constructor($,O){this.input=void 0,this.state=void 0,this.input=$,this.state=O}with(...$){if(this.state.matched)return this;let O=$[$.length-1],j=[$[0]],x;$.length===3&&typeof $[1]=="function"?x=$[1]:$.length>2&&j.push(...$.slice(1,$.length-1));let S=!1,K={},Z=(Y,L)=>{S=!0,K[Y]=L},X=!j.some((Y)=>s(Y,this.input,Z))||x&&!Boolean(x(this.input))?ex:{matched:!0,value:O(S?"@ts-pattern/anonymous-select-key"in K?K["@ts-pattern/anonymous-select-key"]:K:this.input,this.input)};return new Tj(this.input,X)}when($,O){if(this.state.matched)return this;let j=Boolean($(this.input));return new Tj(this.input,j?{matched:!0,value:O(this.input,this.input)}:ex)}otherwise($){return this.state.matched?this.state.value:$(this.input)}exhaustive($=x7){return this.state.matched?this.state.value:$(this.input)}run(){return this.exhaustive()}returnType(){return this}narrow(){return this}}function x7($){throw new u1($)}var $0,S7=new Uint8Array(16);function Hj(){if(!$0){if(typeof crypto>"u"||!crypto.getRandomValues)throw Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");$0=crypto.getRandomValues.bind(crypto)}return $0(S7)}var j$=[];for(let $=0;$<256;++$)j$.push(($+256).toString(16).slice(1));function d1($,O=0){return(j$[$[O+0]]+j$[$[O+1]]+j$[$[O+2]]+j$[$[O+3]]+"-"+j$[$[O+4]]+j$[$[O+5]]+"-"+j$[$[O+6]]+j$[$[O+7]]+"-"+j$[$[O+8]]+j$[$[O+9]]+"-"+j$[$[O+10]]+j$[$[O+11]]+j$[$[O+12]]+j$[$[O+13]]+j$[$[O+14]]+j$[$[O+15]]).toLowerCase()}var O0={};function Z7($,O,j){let x;if($)x=l1($.random??$.rng?.()??Hj(),$.msecs,$.seq,O,j);else{let S=Date.now(),K=Hj();X7(O0,S,K),x=l1(K,O0.msecs,O0.seq,O,j)}return O??d1(x)}function X7($,O,j){if($.msecs??=-1/0,$.seq??=0,O>$.msecs)$.seq=j[6]<<23|j[7]<<16|j[8]<<8|j[9],$.msecs=O;else if($.seq=$.seq+1|0,$.seq===0)$.msecs++;return $}function l1($,O,j,x,S=0){if($.length<16)throw Error("Random bytes length must be >= 16");if(!x)x=new Uint8Array(16),S=0;else if(S<0||S+16>x.length)throw RangeError(`UUID byte range ${S}:${S+15} is out of buffer bounds`);return O??=Date.now(),j??=$[6]*127<<24|$[7]<<16|$[8]<<8|$[9],x[S++]=O/1099511627776&255,x[S++]=O/4294967296&255,x[S++]=O/16777216&255,x[S++]=O/65536&255,x[S++]=O/256&255,x[S++]=O&255,x[S++]=112|j>>>28&15,x[S++]=j>>>20&255,x[S++]=128|j>>>14&63,x[S++]=j>>>6&255,x[S++]=j<<2&255|$[10]&3,x[S++]=$[11],x[S++]=$[12],x[S++]=$[13],x[S++]=$[14],x[S++]=$[15],x}var d$=Z7;class A${table;id;constructor($,O){this.table=$;this.id=O}static byId($,O){return new A$($,O)}async detail(){let{columns:$}=await this.table.detail(),O=$.find((j)=>j.column.id===this.id);if(!O)throw new E(`column ${this.id} is not on table ${this.table.id}`);return O}async name(){let $=await this.detail();return $.column.display_name??$.definition.definition.display_name}async rename($){return await this.table.database.renameColumn(this,$),this}async inferType($){return await this.table.database.inferColumnType(this,$),this}changeType($){return this.table.database.changeColumnType(this,$)}convertIntoNewColumn($){return this.table.database.convertColumnIntoNewColumn(this,$)}casts(){return this.table.database.columnCasts(this)}async delete(){await this.table.database.deleteColumn(this)}async dataType(){return(await this.detail()).definition.definition.data_type}async isMultiSelect(){return(await this.detail()).definition.definition.is_multi_select}async position(){return(await this.detail()).column.position}async definition(){return(await this.detail()).definition}async config(){return(await this.detail()).column.config??void 0}async addOptions($){return await this.table.database.addColumnOptions(this,$),this}toJSON(){return{id:this.id,tableId:this.table.id}}}class F${table;id;constructor($,O){this.table=$;this.id=O}static byId($,O){return new F$($,O)}history(){return this.table.database.rowHistory(this)}toJSON(){return{id:this.id,tableId:this.table.id}}}class Rj{table;id;constructor($,O){this.table=$;this.id=O}static byId($,O){return new Rj($,O)}async detail(){let{views:$}=await this.table.detail(),O=$.find((j)=>j.id===this.id);if(!O)throw new E(`view ${this.id} is not on table ${this.table.id}`);return O}async name(){return(await this.detail()).name}async position(){return(await this.detail()).position}async layout(){return(await this.detail()).layout}async query(){return(await this.detail()).query}positions(){return this.table.database.viewPositions(this)}toJSON(){return{id:this.id,tableId:this.table.id}}}class J${database;id;constructor($,O){this.database=$;this.id=O}static byId($,O){return new J$($,O)}async detail(){let{tables:$}=await this.database.schema(),O=$.find((j)=>j.table.id===this.id);if(!O)throw new E(`table ${this.id} is not in database ${this.database.id}`);return O}async name(){return(await this.detail()).table.name}async rename($){return await this.database.renameTable(this,$),this}async version(){return(await this.detail()).table.version}async position(){return(await this.detail()).table.position}async columns(){let{columns:$}=await this.detail();return $.map((O)=>A$.byId(this,O.column.id))}addColumn($){return this.database.addColumn(this,$)}async reorderColumns($){return await this.database.reorderColumns(this,$),this}row($){return F$.byId(this,$)}async views(){let{views:$}=await this.detail();return $.map((O)=>Rj.byId(this,O.id))}createWebhook(){return this.database.createWebhook(this)}async delete(){await this.database.deleteTable(this)}toJSON(){return{id:this.id,databaseId:this.database.id}}}async function Dj($,O,j,x={}){let S=G(await $.storage.queryDatabases({body:{sql:j,databaseId:x.database?.id,baseVersions:Object.fromEntries((x.baseVersions??[]).map(({table:Q,version:J})=>[Q.id,J]))}})),K=new Map;for(let Q of[x.database,...(x.baseVersions??[]).map(({table:J})=>J.database)]){if(!Q)continue;let J=K.get(Q.id);if(J)J.add(Q);else K.set(Q.id,new Set([Q]))}let Z=(Q)=>{let J=K.get(Q)?.values().next().value;if(J)return J;let I=O($,Q);return K.set(Q,new Set([I])),I},X=(Q)=>{let J=S.tableDatabases[Q];if(!J)throw Error(`SQL response is missing the owner of table ${Q}`);return J$.byId(Z(J),Q)},Y=new Set(Object.keys(S.newVersions).map((Q)=>X(Q).database.id));if(S.statement.kind==="schema")Y.add(S.statement.databaseId);for(let Q of Y)for(let J of K.get(Q)??[])J.invalidateSchema();let L=c(S.statement).with({kind:"select"},(Q)=>Q).with({kind:"schema"},({databaseId:Q,summary:J})=>({kind:"schema",database:Z(Q),summary:J})).with({kind:"insert"},{kind:"update"},{kind:"delete"},({kind:Q,tableId:J,tableName:I})=>({kind:Q,table:X(J),tableName:I})).with({kind:"alterColumnType"},({tableId:Q,tableName:J,columnId:I,columnName:z,to:W})=>{let V=X(Q);return{kind:"alterColumnType",table:V,tableName:J,column:A$.byId(V,I),columnName:z,to:W}}).exhaustive();return{result:S.result?K7(S.result,X,S.tableDatabases):void 0,changesApplied:S.changesApplied,insertedRows:L.kind==="insert"?S.insertedRowIds.map((Q)=>F$.byId(L.table,Q)):[],newVersions:Object.entries(S.newVersions).map(([Q,J])=>({table:X(Q),version:J})),readVersions:Object.entries(S.readVersions).map(([Q,J])=>({table:X(Q),version:J})),truncatedTables:S.truncatedTables,statement:L}}function K7($,O,j){let x=$.sourceTable&&j[$.sourceTable]?O($.sourceTable):void 0;return{columns:$.columns.map(({relatedTable:S,...K})=>({...K,relatedTable:S&&j[S]?O(S):void 0})),rows:$.rows,sourceTable:x,rowHandles:x?$.rowIds.map((S)=>F$.byId(x,S)):[]}}class _j{table;id;createdBy;record;constructor($,O,j,x){this.table=$;this.id=O;this.createdBy=j;this.record=x}static fromRecord($,O,j){return new _j($,j.id,O,j)}get tokenPrefix(){return this.record.tokenPrefix}get createdAt(){return this.record.createdAt}delete(){return this.table.database.deleteWebhook(this)}toJSON(){return{id:this.id,tableId:this.table.id,tokenPrefix:this.tokenPrefix}}}class S0 extends GO{op;row;column;taken;constructor($,O){super($,O);this.name="MacroOpRefusedError",this.message=O.message,this.op=O.op,this.row=O.row,this.column=O.column,this.taken=O.taken}}function Cj($){return c($).with({type:"relation"},({table:O})=>({type:"relation",database:O.database.id,table:O.id})).with({type:Z$.not("relation")},(O)=>O).exhaustive()}var Y7={USER:"People",DOCUMENT:"Documents",TASK:"Tasks",COMPANY:"Companies",CONTACT:"Contacts",CALL_RECORD:"Calls",CHANNEL:"Channels",CHAT:"Chats",PROJECT:"Projects",THREAD:"Emails",CALENDAR_EVENT:"Events",INITIATIVE:"Initiatives"};function G7($){return c($).with({type:"text"},()=>"Text").with({type:"number"},()=>"Number").with({type:"boolean"},()=>"Checkbox").with({type:"date"},()=>"Date").with({type:"link"},()=>"URL").with({type:"select",multi:!0},()=>"Multi-select").with({type:"select",multi:!1},()=>"Select").with({type:"select_number",multi:!0},()=>"Multi-number select").with({type:"select_number",multi:!1},()=>"Number select").with({type:"tag"},()=>"Tag").with({type:"entity"},({target:O})=>Y7[O]).with({type:"relation"},()=>"Relation").exhaustive(()=>"Column")}function j0($){return $.map((O)=>({id:d$(),label:O}))}function r1($,O,j){return $?.kind===O&&"change"in $&&$.change.kind===j}function x0($){return c($).with({kind:"reorder_tables"},({kind:O})=>O).with({kind:Z$.union("table","column","rows","view")},({kind:O,change:j})=>`${O}.${j.kind}`).exhaustive(()=>"unknown")}function l$($,O,j){let[x]=$;if($.length!==1||!r1(x,O,j))throw new f(`expected one ${O}.${j} result, got ${$.map(x0).join(", ")||"none"}`);return x}class V$ extends M{access=new o(this.client,"database",this);async fetch(){return G(await this.client.storage.getDatabase({path:{id:this.id}}))}assertOwns($,O){if(O.id!==this.id)throw new f(`${$} does not belong to database ${this.id}`)}static byId($,O){return new V$($,O)}static async create($,O){let j=G(await $.storage.createDatabase({body:{name:O.name}}));return new V$($,j.id)}static async list($){return G(await $.storage.listDatabases()).map((j)=>new V$($,j.database.id))}query($,O){return Dj(this.client,V$.byId,$,{...O,database:this})}invalidateSchema(){this.detail.clear()}schema(){return this.detail.get()}name=this.mappedField("database",($)=>$.name);owner=this.mappedField("database",($)=>P.byId(this.client,$.owner_id));createdAt=this.mappedField("database",($)=>$.created_at);trashedAt=this.mappedField("database",($)=>$.trashed_at??void 0);grant=this.field("grant");tables=this.mappedField("tables",($)=>$.map((O)=>J$.byId(this,O.table.id)));async table($){let{tables:O}=await this.schema(),j=O.find((x)=>x.table.name===$);return j?J$.byId(this,j.table.id):void 0}async createTable($){let{table:O}=l$(await this.applyOps([{kind:"table",table:d$(),change:{kind:"create",name:$.name}}]),"table","created");return J$.byId(this,O)}async applyOps($,O={}){let j={ops:$};if(O.baseVersions!==void 0){for(let{table:S}of O.baseVersions)this.assertOwns(`table ${S.id}`,S.database);j.baseVersions=Object.fromEntries(O.baseVersions.map(({table:S,version:K})=>[S.id,K]))}let{results:x}=await this.mutate(async(S)=>{let K=await S.storage.applyDatabaseOps({path:{id:this.id},body:j});if(K.error!==void 0&&"op"in K.error)throw new S0(K.response?.status??0,K.error);return K});return x}async reorderTables($){for(let x of $)this.assertOwns(`table ${x.id}`,x.database);let O=await this.applyOps([{kind:"reorder_tables",order:$.map((x)=>x.id)}]),[j]=O;if(O.length!==1||j?.kind!=="reorder_tables")throw new f(`expected one reorder_tables result, got ${O.map(x0).join(", ")||"none"}`);return j.tables.map(({table:x})=>J$.byId(this,x))}async deleteTable($){this.assertOwns(`table ${$.id}`,$.database),l$(await this.applyOps([{kind:"table",table:$.id,change:{kind:"delete"}}]),"table","deleted")}async columnCasts($){return this.assertOwns(`column ${$.id}`,$.table.database),G(await this.client.storage.listDatabaseColumnCasts({path:{id:this.id,table_id:$.table.id,column_id:$.id}}))}async viewPositions($){this.assertOwns(`view ${$.id}`,$.table.database);let{positions:O}=G(await this.client.storage.getDatabaseViewPositions({path:{id:this.id,view_id:$.id}}));return O}async rowHistory($){this.assertOwns(`row ${$.id}`,$.table.database);let{changes:O}=G(await this.client.storage.getDatabaseRowHistory({path:{id:this.id,table_id:$.table.id,row_id:$.id}}));return O}async importTable($){let O=await this.mutate((j)=>j.storage.importDatabaseTable({path:{id:this.id},body:$}));return J$.byId(this,O.id)}webhookOf($){return _j.fromRecord(J$.byId(this,$.tableId),P.byId(this.client,$.createdBy),$)}async webhooks(){let{webhooks:$}=G(await this.client.storage.listDatabaseWebhooks({path:{id:this.id}}));return $.map((O)=>this.webhookOf(O))}async createWebhook($){this.assertOwns("table",$.database);let O=G(await this.client.storage.createDatabaseWebhook({path:{id:this.id},body:{tableId:$.id}}));return{webhook:this.webhookOf(O.webhook),token:O.token}}async deleteWebhook($){this.assertOwns("webhook",$.table.database),G(await this.client.storage.deleteDatabaseWebhook({path:{id:this.id,webhook_id:$.id}}))}async sharePermissions(){return G(await this.client.storage.getDatabasePermissions({path:{id:this.id}}))}updateSharePermissions($){return this.mutate((O)=>O.storage.updateDatabasePermissions({path:{id:this.id},body:$}))}async changeColumnType($,O){this.assertOwns(`column ${$.id}`,$.table.database);let j=await $.table.version();return l$(await this.applyOps([{kind:"column",table:$.table.id,column:$.id,change:{kind:"change_type",to:Cj(O.to)}}],{baseVersions:[{table:$.table,version:j}]}),"column","type_changed")}async columnConversion($,O){return this.assertOwns(`column ${$.id}`,$.table.database),G(await this.client.storage.convertDatabaseColumn({path:{id:this.id,table_id:$.table.id,column_id:$.id},body:{to:Cj(O)}}))}async convertColumnIntoNewColumn($,O){let j=await this.columnConversion($,O.to),x=O.name??`${await $.name()} (${G7(O.to)})`,S=d$(),K=[{kind:"column",table:$.table.id,column:S,change:{kind:"create",definition:{source:"new",name:x,type:Cj(O.to),options:j0(j.options)},after:$.id}}];if(j.cells.length>0)K.push({kind:"rows",table:$.table.id,change:{kind:"update",changes:{kind:"per_row",rows:j.cells.map(({row:X,value:Y})=>({row:X,cells:[{column:S,value:Y}]}))}}});let[Z]=await this.applyOps(K,{baseVersions:[{table:$.table,version:j.tableVersion}]});if(!r1(Z,"column","created"))throw new f(`expected a column.created result, got ${Z?x0(Z):"none"}`);return A$.byId($.table,Z.column)}async deleteColumn($){this.assertOwns(`column ${$.id}`,$.table.database);let O=await $.table.version(),{tableVersion:j}=l$(await this.applyOps([{kind:"column",table:$.table.id,column:$.id,change:{kind:"delete"}}],{baseVersions:[{table:$.table,version:O}]}),"column","deleted");return j}async reorderColumns($,O){this.assertOwns(`table ${$.id}`,$.database);for(let S of O)if(S.table.id!==$.id)throw new f(`column ${S.id} does not belong to table ${$.id}`);let j=await $.version(),{tableVersion:x}=l$(await this.applyOps([{kind:"table",table:$.id,change:{kind:"reorder_columns",order:O.map((S)=>S.id)}}],{baseVersions:[{table:$,version:j}]}),"table","columns_reordered");if(x===void 0)throw new f(`table ${$.id} reordered its columns without a version`);return x}async renameTable($,O){this.assertOwns(`table ${$.id}`,$.database);let j=await $.name(),{tableVersion:x}=l$(await this.applyOps([{kind:"table",table:$.id,change:{kind:"rename",name:O,previousName:j}}]),"table","renamed");if(x===void 0)throw new f(`table ${$.id} was renamed without a version`);return x}async renameColumn($,O){this.assertOwns(`column ${$.id}`,$.table.database);let j=await $.name(),{tableVersion:x}=l$(await this.applyOps([{kind:"column",table:$.table.id,column:$.id,change:{kind:"rename",name:O,previousName:j}}]),"column","renamed");return x}async inferColumnType($,O){return this.assertOwns(`column ${$.id}`,$.table.database),this.mutate((j)=>j.storage.inferDatabaseColumnType({path:{id:this.id,table_id:$.table.id,column_id:$.id},body:{dataType:O.dataType,baseVersion:O.baseVersion,...O.specificEntityType!==void 0?{specificEntityType:O.specificEntityType}:{}}}))}async addColumn($,O){if(this.assertOwns(`table ${$.id}`,$.database),O.after!==void 0&&O.after.table.id!==$.id)throw new f(`column ${O.after.id} does not belong to table ${$.id}`);let j="property"in O?{source:"existing",property:O.property.id}:{source:"new",name:O.name,type:Cj(O.type),...O.options!==void 0?{options:j0(O.options)}:{},...O.inferType!==void 0?{inferType:O.inferType}:{}},{column:x}=l$(await this.applyOps([{kind:"column",table:$.id,column:d$(),change:{kind:"create",definition:j,...O.after!==void 0?{after:O.after.id}:{}}}]),"column","created");return A$.byId($,x)}async addColumnOptions($,O){return this.assertOwns(`column ${$.id}`,$.table.database),l$(await this.applyOps([{kind:"column",table:$.table.id,column:$.id,change:{kind:"add_options",options:j0(O)}}]),"column","options_added")}}class Z0{client;constructor($){this.client=$}query($,O){return Dj(this.client,V$.byId,$,O)}byId($){return V$.byId(this.client,$)}create($){return V$.create(this.client,$)}list(){return V$.list(this.client)}}class Pj{id;name;constructor($,O){this.id=$;this.name=O}static async list($){let{skills:O}=G(await $.storage.getSystemSkillsHandler());return O.map((j)=>new Pj(j.id,j.name))}}class X0{client;async byTeamSlug($){let O=G(await this.client.storage.getDocumentByTeamSlug({path:{slug:$}}));if(typeof O!=="object"||O===null||!("data"in O)||typeof O.data!=="object"||O.data===null||!("documentMetadata"in O.data)||typeof O.data.documentMetadata!=="object"||O.data.documentMetadata===null||!("documentId"in O.data.documentMetadata)||typeof O.data.documentMetadata.documentId!=="string")throw Error("Invalid team task document response");return w.byId(this.client,O.data.documentMetadata.documentId)}async validatePermissionsToken($){let O=G(await this.client.storage.validateDocumentPermissionsToken({body:{token:$}}));return{document:w.byId(this.client,O.document_id),user:O.user_id?P.byId(this.client,O.user_id):void 0,accessLevel:O.access_level,expiresAt:O.exp,issuer:O.iss}}async createUpload($){let O={documentName:$.name,sha:$.sha,fileType:$.fileType,mimeType:$.mimeType,projectId:$.project?.id,teamId:$.team?.id,branchedFromId:$.branchedFrom?.id,emailAttachmentId:$.attachment?.id,isTask:$.isTask},j=G(await this.client.storage.createDocument({body:O})).data;return{document:w.byId(this.client,j.documentMetadata.documentId),uploadUrl:j.presignedUrl??void 0,contentType:j.contentType,content:j.documentMetadata.content}}constructor($){this.client=$}byId($){return w.byId(this.client,$)}create($){return w.create(this.client,$)}async createCanvas($){let O=G(await this.client.storage.createCanvasHandler({body:{documentName:$.name,projectId:$.project?.id}}));return w.byId(this.client,O.documentId)}createSnippet($){return w.createSnippet(this.client,$)}createSkill($){return w.createSkill(this.client,$)}systemSkills(){return Pj.list(this.client)}search($,O){return w.search(this.client,$,O)}recent($){return w.recent(this.client,$)}recentlyDeleted(){return w.recentlyDeleted(this.client)}}class WO extends M{async fetch(){let{attachment:$}=G(await this.client.email.getAttachment({path:{id:this.id}}));return $}static byId($,O){return new WO($,O)}static from($,O){return new WO($,O.db_id,O)}filename=this.field("filename");mimeType=this.field("mime_type");size=this.field("size_bytes");dataUrl=this.field("data_url");sfsId=this.field("sfs_id");contentId=this.field("content_id");async toDocument(){let{document_id:$}=G(await this.client.email.getAttachmentDocumentId({path:{id:this.id}}));return w.byId(this.client,$)}}class R$ extends X${access=new o(this.client,"email_thread",this);async followup(){return G(await this.client.storage.getEmailFollowup({path:{thread_id:this.id}}))}async setFollowup($){return G(await this.client.storage.setEmailFollowup({path:{thread_id:this.id},body:$}))}async calendarInvitations(){return G(await this.client.email.getThreadCalendarInvitations({path:{thread_id:this.id}}))}async addEventToCalendar($,O,j={}){let x=G(await this.client.email.copyThreadCalendarInvitation({path:{thread_id:this.id,message_id:$.id,invitation_id:O.id},body:{calendarId:j.calendar?.id}}));return $$.fromRecord(this.client,x)}async update($){await this.mutate((O)=>O.storage.editThreadV2({path:{thread_id:this.id},body:{projectId:$.project===void 0?void 0:$.project?.id??null,sharePermission:$.sharePermission?n$($.sharePermission):void 0}}))}entityType="email_thread";propertyEntityType="THREAD";async fetch(){let{thread:$}=G(await this.client.email.getThread({path:{thread_id:this.id},query:{limit:100}}));return $}static byId($,O){return new R$($,O)}createdAt=this.field("created_at");updatedAt=this.field("updated_at");isRead=this.field("is_read");inboxVisible=this.field("inbox_visible");accessLevel=this.field("access_level");providerId=this.field("provider_id");link=this.mappedField("link_id",($)=>T$.byId(this.client,$));project=this.mappedField("project_id",($)=>$?n.byId(this.client,$):void 0);latestInboundAt=this.field("latest_inbound_message_ts");latestOutboundAt=this.field("latest_outbound_message_ts");latestNonSpamAt=this.field("latest_non_spam_message_ts");async subject(){let{messages:$}=await this.detail.get();for(let O of $)if(O.subject)return O.subject;return}async participants(){let{messages:$}=await this.detail.get(),O=new Map;for(let j of $)for(let x of[j.from,...j.to,...j.cc,...j.bcc])if(x&&!O.has(x.email))O.set(x.email,x);return[...O.values()]}async messages($){return G(await this.client.email.getThreadMessagesHandler({path:{id:this.id},query:{...$?.since?{since:$.since}:{},...$?.limit?{limit:$.limit}:{}}})).map((j)=>D$.from(this.client,j))}async attachments(){let{messages:$}=await this.detail.get();return $.flatMap((O)=>O.attachments.map((j)=>WO.from(this.client,j)))}async reply($){let O=$.subject??await this.subject()??"",{message:j}=await this.mutate((x)=>x.email.sendMessage({body:{message:{thread_db_id:this.id,replying_to_id:$.replyingTo?.id??null,to:$.to,cc:$.cc??null,bcc:$.bcc??null,subject:O,body_text:$.bodyText}}}));return j.db_id?D$.byId(this.client,j.db_id):void 0}async archive($=!0){await this.mutate((O)=>O.email.archiveThread({path:{id:this.id},body:{value:$}}))}async updateState($){await this.mutate((O)=>O.email.updateThreadState({path:{id:this.id},body:$}))}async operations(){return G(await this.client.email.threadOperations({path:{id:this.id}}))}async markSeen(){await this.mutate(($)=>$.email.threadSeen({path:{id:this.id}}))}async addLabel($){await this.mutate((O)=>O.email.addRemoveThreadLabel({path:{id:this.id},body:{label_id:$.id,value:!0}}))}async removeLabel($){await this.mutate((O)=>O.email.addRemoveThreadLabel({path:{id:this.id},body:{label_id:$.id,value:!1}}))}async moveToProject($){await this.mutate((O)=>O.email.updateThreadProject({path:{thread_id:this.id},body:{projectId:$?.id??null}}))}static search=x$({filters:{email_filters:{}},type:"email",make:($,O)=>new R$($,O.thread_id)})}class D$ extends M{async fetch(){return G(await this.client.email.getMessage({path:{id:this.id}}))}static byId($,O){return new D$($,O)}static from($,O){return new D$($,O.db_id,O)}static async send($,O){let{message:j}=G(await $.email.sendMessage({body:{message:{to:O.to,cc:O.cc??null,bcc:O.bcc??null,subject:O.subject,body_text:O.bodyText}}}));return j.db_id?D$.byId($,j.db_id):void 0}subject=this.field("subject");from=this.field("from");to=this.field("to");cc=this.field("cc");bcc=this.field("bcc");body=this.field("body_parsed");sentAt=this.field("internal_date_ts");labels=this.field("labels");link=this.mappedField("link_id",($)=>T$.byId(this.client,$));async operationStatus(){return G(await this.client.email.messageOperationStatus({path:{id:this.id}}))}async resolveOperation($){await this.mutate((O)=>O.email.resolveMessageOperation({path:{id:this.id},body:$}))}async completeDraftAttachment($,O=0){let j=await this.link();await this.mutate((x)=>x.email.completeDraftAttachment({path:{id:this.id,attachment_id:$},query:{draft_generation:O},headers:{"X-Email-Link-Id":j.id}}))}async transferDraft($){let O=await this.detail.get();return this.mutate((j)=>j.email.transferDraft({path:{id:this.id},body:{...$,draft_generation:$.draft_generation??O.draft_generation??0},headers:{"X-Email-Link-Id":O.link_id}}))}async addLabel($){await this.mutate((O)=>O.email.addRemoveLabel({body:{label_id:$.id,message_ids:[this.id],value:!0}}))}async removeLabel($){await this.mutate((O)=>O.email.addRemoveLabel({body:{label_id:$.id,message_ids:[this.id],value:!1}}))}async thread(){return R$.byId(this.client,(await this.detail.get()).thread_db_id)}}class BO extends D${async draftContext(){let $=await this.detail.get();return{headers:{"X-Email-Link-Id":$.link_id},query:{draft_generation:$.draft_generation??0}}}static byId($,O){return new BO($,O)}draftGeneration=this.field("draft_generation");async update($){let{headers:O,query:j}=await this.draftContext();await this.mutate((x)=>x.email.createDraft({headers:O,body:{draft:{...K0($),db_id:this.id,draft_generation:$.draftGeneration??j.draft_generation}}}))}async delete(){let{headers:$,query:O}=await this.draftContext();await this.mutate((j)=>j.email.deleteDraft({path:{id:this.id},headers:$,query:O}))}async schedule($,O){let{headers:j,query:x}=await this.draftContext();await this.mutate((S)=>S.email.upsertScheduledMessage({path:{id:this.id},headers:j,query:x,body:{send_time:$,include_signature:O?.includeSignature}}))}async unschedule(){let{headers:$,query:O}=await this.draftContext();await this.mutate((j)=>j.email.deleteScheduledDraft({path:{message_id:this.id},headers:$,query:O}))}async addAttachment($){let O={file_name:$.fileName,sha:$.sha,size:$.size,upload_id:$.uploadId},{headers:j,query:x}=await this.draftContext(),S=await this.mutate((K)=>K.email.addDraftAttachment({path:{id:this.id},headers:j,query:x,body:O}));return{attachment:eO.byId(this.client,this,S.attachment_id,!1,x.draft_generation),uploadUrl:S.upload_url,contentType:S.content_type}}async forwardAttachment($){let{headers:O,query:j}=await this.draftContext(),x=await this.mutate((S)=>S.email.addForwardedAttachment({path:{id:this.id},headers:O,query:j,body:{attachment_id:$.id}}));return eO.byId(this.client,this,x.attachment_id,!0,j.draft_generation)}async removeAttachment($){if($.draft.id!==this.id)throw Error("Attachment belongs to another draft");let{headers:O}=await this.draftContext(),j={draft_generation:$.draftGeneration};if($.forwarded)await this.mutate((x)=>x.email.removeForwardedAttachment({path:{id:this.id,attachment_id:$.id},headers:O,query:j}));else await this.mutate((x)=>x.email.removeDraftAttachment({path:{id:this.id,attachment_id:$.id},headers:O,query:j}))}async verifyAttachment($){if($.draft.id!==this.id||$.forwarded)throw Error("Expected an uploaded attachment on this draft");await this.completeDraftAttachment($.id,$.draftGeneration)}}class eO{draft;id;forwarded;draftGeneration;constructor($,O,j,x){this.draft=$;this.id=O;this.forwarded=j;this.draftGeneration=x}static byId($,O,j,x=!1,S=0){return new eO(O,j,x,S)}complete(){return this.draft.verifyAttachment(this)}remove(){return this.draft.removeAttachment(this)}toJSON(){return{id:this.id,draftId:this.draft.id,forwarded:this.forwarded,draftGeneration:this.draftGeneration}}}function K0($){let{bodyText:O,bodyHtml:j,bodyMacro:x,headers:S,includeSignature:K,draftGeneration:Z,thread:X,replyingTo:Y,...L}=$;return{...L,body_text:O,body_html:j===void 0?void 0:btoa(Array.from(new TextEncoder().encode(j),(Q)=>String.fromCharCode(Q)).join("")).replaceAll("+","-").replaceAll("/","_").replaceAll("=",""),body_macro:x,headers_json:S,include_signature:K,draft_generation:Z,thread_db_id:X?.id,replying_to_id:Y?.id}}class LO extends M{inbox;constructor($,O,j,x){super($,O,j);this.inbox=x}get headers(){return this.inbox?{"X-Email-Link-Id":this.inbox.id}:void 0}static byId($,O,j){return new LO($,O,void 0,j)}static from($,O,j){return new LO($,O.id,O,j)}async fetch(){let $=G(await this.client.email.listEmailFilters({headers:this.headers})).filters.find((O)=>O.id===this.id);if(!$)throw new E(`Email filter ${this.id} is unavailable`);return $}emailAddress=this.field("email_address");emailDomain=this.field("email_domain");isImportant=this.field("is_important");async setImportant($){let O=await this.detail.get();await this.mutate((j)=>j.email.upsertEmailFilter({headers:this.headers,body:{email_address:O.email_address,email_domain:O.email_domain,is_important:$}}))}async delete(){await this.mutate(($)=>$.email.deleteEmailFilter({path:{id:this.id},headers:this.headers}))}}class jO extends M{async fetch(){let{labels:$}=G(await this.client.email.listLabels()),O=$.find((j)=>j.id===this.id);if(!O)throw new E(`label ${this.id} not found`);return O}static byId($,O){return new jO($,O)}static async list($){let{labels:O}=G(await $.email.listLabels());return O.map((j)=>new jO($,j.id,j))}static async create($,O){let{label:j}=G(await $.email.createLabel({body:{label_name:O}}));if(!j.id)throw new f("created label has no id");return new jO($,j.id)}async delete(){G(await this.client.email.deleteLabel({path:{id:this.id}}))}name=this.field("name");linkId=this.field("linkId");type=this.field("type");createdAt=this.field("createdAt");labelListVisibility=this.field("labelListVisibility");messageListVisibility=this.field("messageListVisibility")}class Y0{client;async createDraft($,O){let j=G(await this.client.email.createDraft({body:{draft:K0($)},headers:O?{"X-Email-Link-Id":O.id}:void 0}));if(!j.draft.db_id)throw Error("Draft response has no message id");return BO.byId(this.client,j.draft.db_id)}draftById($){return BO.byId(this.client,$)}async*scheduled($){let O=$?.pageSize??100;if(!Number.isInteger(O)||O<1)throw Error("pageSize must be positive");let j=Math.min(O,1000);for(let x=0;;x+=j){let S=G(await this.client.email.getScheduledMessages({query:{offset:x,limit:j},headers:$?.inbox?{"X-Email-Link-Id":$.inbox.id}:void 0}));for(let K of S.messages)yield BO.byId(this.client,K.db_id);if(S.messages.lengthLO.from(this.client,O,$))}async upsertFilter($,O){return LO.from(this.client,G(await this.client.email.upsertEmailFilter({headers:O?{"X-Email-Link-Id":O.id}:void 0,body:{email_address:$.emailAddress,email_domain:$.emailDomain,is_important:$.isImportant}})).filter,O)}async contacts(){return G(await this.client.email.listContacts()).contacts}reminders($){let{inboxes:O,...j}=$??{};return u(async(x)=>{let S=G(await this.client.storage.listEmailReminders({query:{...j,inboxIds:O?.map((K)=>K.id),cursor:x}}));return{items:S.items.map(({threadId:K,...Z})=>({...Z,thread:R$.byId(this.client,K)})),nextCursor:S.nextCursor}})}constructor($){this.client=$}byId($){return R$.byId(this.client,$)}message($){return D$.byId(this.client,$)}attachment($){return WO.byId(this.client,$)}link($){return T$.byId(this.client,$)}links(){return T$.list(this.client)}async connectionProviders(){return G(await this.client.auth.emailConnectionProviders())}async recoverDraftTransfer($){return G(await this.client.email.recoverDraftTransfer({path:{id:$}}))}send($){return D$.send(this.client,$)}search($,O){return R$.search(this.client,$,O)}inbox($){return u(async(O)=>{let j=G(await this.client.email.previewsInboxCursor({path:{view:"inbox"},query:{...$?.pageSize?{limit:$.pageSize}:{},...$?.sort?{sort_method:$.sort}:{},...O?{cursor:O}:{}}}));return{items:j.items.map((x)=>R$.byId(this.client,x.id)),nextCursor:j.next_cursor}})}label($){return jO.byId(this.client,$)}labels(){return jO.list(this.client)}createLabel($){return jO.create(this.client,$)}async deleteLabel($){G(await this.client.email.deleteLabel({path:{id:$.id}}))}async blockSender($){G(await this.client.email.blockSender({body:{email_address:$}}))}async unblockSender($){G(await this.client.email.unblockSender({body:{email_address:$}}))}async blockedSenders(){return G(await this.client.email.listBlockedSenders()).blocked_emails}}class IO{client;id;constructor($,O){this.client=$;this.id=O}static byId($,O){return new IO($,O)}async delete(){G(await this.client.storage.deleteEntityMention({path:{mention_id:this.id}}))}toJSON(){return{id:this.id}}}class G0{client;constructor($){this.client=$}byId($){return IO.byId(this.client,$)}async create($,O){let j=G(await this.client.storage.createEntityMention({body:{source_entity_id:$.entity.id,source_entity_type:$.type,entity_id:O.entity.id,entity_type:O.type}}));return IO.byId(this.client,j.id)}}class Q0{client;constructor($){this.client=$}async list($={}){let{favorites:O}=G(await this.client.storage.listFavorites({query:{entityType:$.entityTypes,entityId:$.entityIds}}));return O}async add($){return G(await this.client.storage.addFavorite({body:{entityId:$.id,entityType:$.entityType}}))}async remove($){G(await this.client.storage.removeFavoriteByEntity({path:{entity_type:$.entityType,entity_id:$.id}}))}async reorder($){G(await this.client.storage.reorderFavorites({body:{favorites:$}}))}}class xO extends z${entityType="foreign_entity";async fetch(){return G(await this.client.storage.getForeignEntity({path:{id:this.id}}))}static byId($,O){return new xO($,O)}static fromRecord($,O){return new xO($,O.id,O)}foreignEntityId=this.field("foreignEntityId");source=this.field("foreignEntitySource");storedForId=this.field("storedForId");storedForAuthEntity=this.field("storedForAuthEntity");createdAt=this.field("createdAt");updatedAt=this.field("updatedAt");async metadata(){return(await this.detail.get()).metadata}}function p1({login:$,...O}){return{...O,login:$??void 0}}class J0{client;constructor($){this.client=$}byId($){return xO.byId(this.client,$)}async bySource($,O){return xO.fromRecord(this.client,G(await this.client.storage.getForeignEntityBySource({path:{source:$,foreign_entity_id:O}})))}async githubPullRequestFacets(){let{repositories:$,authors:O,assignees:j,labels:x}=G(await this.client.storage.getGithubPullRequestFacets());return{repositories:$,authors:O.map(p1),assignees:j.map(p1),labels:x.map(({color:S,...K})=>({...K,color:S??void 0}))}}}class r${form;id;constructor($,O){this.form=$;this.id=O}static byId($,O){return new r$($,O)}async detail(){let O=(await this.form.schema()).sections.flatMap((j)=>j.kind==="questions"?j.questions:[]).find((j)=>j.id===this.id);if(!O)throw new E(`question ${this.id} is not on form ${this.form.id}`);return O}async title(){return(await this.detail()).title}async column(){return A$.byId(await this.form.table(),(await this.detail()).column)}async options(){return(await this.detail()).options.map(($)=>NO.byId(this,$.id))}}class NO{question;id;constructor($,O){this.question=$;this.id=O}static byId($,O){return new NO($,O)}async detail(){let $=(await this.question.detail()).options.find((O)=>O.id===this.id);if(!$)throw new E(`option ${this.id} is not on question ${this.question.id}`);return $}async label(){return(await this.detail()).label}async color(){return(await this.detail()).color??void 0}}class TO{form;id;detail;constructor($,O,j){this.form=$;this.id=O;this.detail=new W$(()=>this.load(),j)}async load(){let $=await this.form.myResponse();if($.response.id!==this.id)throw new E(`response ${this.id} is not your current response`);return $}static byId($,O,j){return new TO($,O,j)}async status(){return(await this.detail.get()).status}async row(){return(await this.detail.get()).row}async answers(){return(await this.detail.get()).answers}async booking(){return(await this.detail.get()).booking}async refresh(){let $=await this.load();return this.detail=new W$(()=>this.load(),$),this}}class p${form;id;constructor($,O){this.form=$;this.id=O}static byId($,O){return new p$($,O)}async detail(){let $=(await this.form.schema()).sections.find((O)=>O.id===this.id);if(!$)throw new E(`section ${this.id} is not on form ${this.form.id}`);return $}async title(){return(await this.detail()).title}async questions(){let $=await this.detail();return $.kind==="questions"?$.questions.map((O)=>r$.byId(this.form,O.id)):[]}}class Mj{entityType;id;constructor($,O){this.entityType=$;this.id=O}static byId($,O){return new Mj($,O)}}class y$ extends M{async fetch(){return G(await this.client.storage.getForm({path:{id:this.id}}))}static byId($,O){return new y$($,O)}static async create($,O){let j=G(await $.storage.createForm({body:{name:O.name,source:O.table?{kind:"table",databaseId:O.table.database.id,tableId:O.table.id}:{kind:"new"}}}));return new y$($,j.form.id,j)}static async list($,O){if(!O)return G(await $.storage.listAccessibleForms()).map(({form:S})=>new y$($,S.id));return G(await $.storage.listForms({query:{databaseId:O.id}})).map((x)=>new y$($,x.id))}schema(){return this.detail.get()}name=this.mappedField("form",($)=>$.name);description=this.mappedField("form",($)=>$.description);owner=this.mappedField("form",($)=>P.byId(this.client,$.ownerId));database=this.mappedField("form",($)=>V$.byId(this.client,$.databaseId));table=this.mappedField("form",($)=>J$.byId(V$.byId(this.client,$.databaseId),$.tableId));audience=this.mappedField("form",($)=>$.audience);status=this.mappedField("form",($)=>$.status);closesAt=this.mappedField("form",($)=>$.closesAt??void 0);access=this.field("access");async sections(){return(await this.detail.get()).sections.map(($)=>p$.byId(this,$.id))}async questions(){return(await this.detail.get()).sections.flatMap(($)=>$.kind==="questions"?$.questions.map((O)=>r$.byId(this,O.id)):[])}async update($){return await this.mutate((O)=>O.storage.updateForm({path:{id:this.id},body:$})),this}assertOwns($){if($.form.id!==this.id)throw new f(`question or section does not belong to form ${this.id}`)}async replaceLayout($){let O=await this.table(),j=(Z)=>{if(Z.table.id!==O.id||Z.table.database.id!==O.database.id)throw new f(`column does not belong to form ${this.id}'s table`);return Z.id},x=(Z)=>({conjunction:Z.conjunction,conditions:Z.conditions.map((X)=>c(X).with({kind:"group"},(Y)=>({kind:"group",...x(Y)})).with({kind:"condition"},(Y)=>({kind:"condition",column:j(Y.column),test:c(Y.test).with({kind:"options"},(L)=>({...L,options:L.options.map((Q)=>Q.id)})).with({kind:"entities"},(L)=>({...L,entities:L.entities.map((Q)=>Q.id)})).with({kind:Z$.union("presence","text","number","date","checkbox")},(L)=>L).exhaustive()})).exhaustive())}),S={sections:$.map((Z)=>{if(Z.section)this.assertOwns(Z.section);let X={id:Z.section?.id??d$(),title:Z.title,description:Z.description??""};return c(Z).with({kind:"booking"},(Y)=>({...X,kind:"booking",target:Y.target})).with({kind:"gate"},(Y)=>({...X,kind:"gate",rules:x(Y.rules),message:Y.message})).with({kind:"questions"},(Y)=>({...X,kind:"questions",questions:Y.questions.map((L)=>{if(L.question)this.assertOwns(L.question);return{id:L.question?.id??d$(),column:j(L.column),helpText:L.helpText??"",required:L.required??!1,widget:L.widget??null}})})).exhaustive()})},K=await this.mutate((Z)=>Z.storage.putFormLayout({path:{id:this.id},body:S}));return{form:this,publicationError:K.publicationError}}wireAnswers($){return $.map(({question:O,value:j})=>{this.assertOwns(O);let x=c(j).with({type:"rows"},(S)=>({type:"rows",value:S.value.map((K)=>K.id)})).with({type:"options"},(S)=>({type:"options",value:S.value.map((K)=>{if(K.question.id!==O.id||K.question.form.id!==this.id)throw new f("option does not belong to the answered question");return{id:K.id}})})).with({type:"entities"},(S)=>({type:"entities",value:S.value.map(({entityType:K,entity:Z})=>({entityType:K,entityId:Z.id}))})).with({type:Z$.union("text","number","boolean","date","link","clear")},(S)=>S).exhaustive();return{question:O.id,value:x}})}unlockedBooking($){return $?{...$,section:p$.byId(this,$.section)}:void 0}async outcome($,O){return c($).with({outcome:"stopped"},(j)=>({...j,section:p$.byId(this,j.section)})).with({outcome:"submitted"},async(j)=>{let x=F$.byId(await this.table(),j.row),S=this.unlockedBooking(j.booking);return{outcome:"submitted",response:TO.byId(this,j.response,{status:"submitted",row:x,answers:O,booking:S}),row:x,...S?{booking:S}:{}}}).exhaustive()}async submit($){let O={answers:this.wireAnswers($)};return this.outcome(G(await this.client.storage.submitFormResponse({path:{id:this.id},body:O})),$)}async editResponse($){let O={answers:this.wireAnswers($)};return this.outcome(G(await this.client.storage.editMyFormResponse({path:{id:this.id},body:O})),$)}async myResponse(){let $=G(await this.client.storage.getMyFormResponse({path:{id:this.id}})),O=await this.table(),j=await Promise.all($.answers.map(async(K)=>{let Z=r$.byId(this,K.question),X=await c(K.value).with({type:"options"},(Y)=>({type:"options",value:Y.value.map((L)=>{if(!("id"in L))throw new f("stored form answers must reference options by id");return NO.byId(Z,L.id)})})).with({type:"entities"},(Y)=>({type:"entities",value:Y.value.map((L)=>({entityType:L.entityType,entity:Mj.byId(L.entityType,L.entityId)}))})).with({type:"rows"},async(Y)=>{let L=await Z.detail();if(L.kind.type!=="relation")throw new f("row answer requires a relation question");let Q=J$.byId(V$.byId(this.client,L.kind.database),L.kind.table);return{type:"rows",value:Y.value.map((J)=>F$.byId(Q,J))}}).with({type:Z$.union("text","number","boolean","date","link","clear")},(Y)=>Y).exhaustive(()=>{throw new f(`answer to question ${Z.id} has a type this SDK doesn't know`)});return{question:Z,value:X}})),x=$.response.row?F$.byId(O,$.response.row):void 0,S=this.unlockedBooking($.booking);return{...S?{booking:S}:{},response:TO.byId(this,$.response.id,{status:$.response.status,row:x,answers:j,booking:S}),status:$.response.status,stoppedAtSection:$.response.stoppedAtSection?p$.byId(this,$.response.stoppedAtSection):void 0,row:x,submittedAt:$.response.submittedAt,updatedAt:$.response.updatedAt,answers:j}}async responseSummary(){let $=G(await this.client.storage.getFormResponseSummary({path:{id:this.id}}));return{...$,stoppedBySection:$.stoppedBySection.map((O)=>({section:p$.byId(this,O.section),count:O.count}))}}async tally(){return G(await this.client.storage.getFormTally({path:{id:this.id}})).questions.map((O)=>{let j=r$.byId(this,O.question);return{question:j,responses:O.responses,buckets:O.buckets.flatMap((x)=>c(x.value).with({kind:"option"},(S)=>[{count:x.count,value:{kind:"option",option:NO.byId(j,S.option)}}]).with({kind:"checkbox"},(S)=>[{count:x.count,value:S}]).exhaustive((S)=>{return console.warn("macro sdk: unknown form tally bucket",S),[]}))}})}async sharePermissions(){return G(await this.client.storage.getFormPermissions({path:{id:this.id}}))}updateSharePermissions($){return this.mutate((O)=>O.storage.updateFormPermissions({path:{id:this.id},body:$}))}webUrl(){return`${this.client.webAppUrl}/app/form/${this.id}`}respondUrl(){return`${this.client.webAppUrl}/app/form/${this.id}/respond`}}class V0{client;constructor($){this.client=$}byId($){return y$.byId(this.client,$)}create($){return y$.create(this.client,$)}list($){return y$.list(this.client,$)}}class HO extends M{static byId($,O){return new HO($,O)}async fetch(){return G(await this.client.storage.getGithubPullRequest({path:{id:this.id}}))}title=this.field("title");url=this.field("url");status=this.field("status");async changes(){let $=G(await this.client.storage.getGithubPullRequestChanges({path:{id:this.id}}));return{changeset:$.changeset?new W0(this.client,this,$.changeset):void 0,error:$.error??void 0}}}class W0{client;pullRequest;record;constructor($,O,j){this.client=$;this.pullRequest=O;this.record=j}get id(){return this.record.id}async patch(){return G(await this.client.storage.getGithubPullRequestChangesPatch({path:{id:this.pullRequest.id},query:{changeset:this.id}})).patch}toJSON(){return{pullRequestId:this.pullRequest.id,...this.record}}}class B0{client;constructor($){this.client=$}byId($){return HO.byId(this.client,$)}forEntity($){return HO.byId(this.client,$.id)}}class RO extends M{static byId($,O){return new RO($,O)}async fetch(){return G(await this.client.storage.getHarnessPairing({path:{code:this.id}}))}name=this.field("requested_name");expiresAt=this.field("expires_at");async approve($){let O={name:$?.name,team_id:$?.team?.id,allow_permission_bypass:$?.allowPermissionBypass};return B$.from(this.client,await this.mutate((j)=>j.storage.approveHarnessPairing({path:{code:this.id},body:O})))}}class oO{client;id;constructor($,O){this.client=$;this.id=O}static byId($,O){return new oO($,O)}async claim($){let O=G(await this.client.storage.claimHarnessPairing({path:{pairing_id:this.id},body:{device_secret:$}}));return"harness"in O?{harness:B$.from(this.client,O.harness),token:O.token}:void 0}toJSON(){return{id:this.id}}}class L0{client;constructor($){this.client=$}byId($){return B$.byId(this.client,$)}async list(){return G(await this.client.storage.listHarnesses()).map(($)=>B$.from(this.client,$))}async me(){return B$.from(this.client,G(await this.client.storage.getSelfHarness()))}async deleteSelf(){G(await this.client.storage.deleteSelfHarness())}async agents(){return G(await this.client.storage.listHarnessAgents()).map(($)=>I$.byId(this.client,$.bot_id))}async sessions(){return G(await this.client.storage.listHarnessSessions()).map(($)=>i.byId(this.client,$.session_id))}pairing($){return RO.byId(this.client,$)}async createPairing($){let O=G(await this.client.storage.createHarnessPairing({body:$}));return{pairing:RO.byId(this.client,O.code),device:oO.byId(this.client,O.pairing_id),deviceSecret:O.device_secret,expiresAt:O.expires_at,pollIntervalSeconds:O.poll_interval_seconds}}}var Q7="00000001-0000-0000-0000-000000000002",J7="00000001-0000-0000-0002-000000000004";function V7($){let O=$.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/([1-9][0-9]*)\/?(?:[?#].*)?$/i);if(!O)throw Error(`Expected a GitHub pull request URL like https://github.com/owner/repo/pull/1, got ${$}`);return`${O[1]}/${O[2]}/pull/${O[3]}`}class _$ extends w{static byId($,O){return new _$($,O)}static async create($,O){let{documentId:j}=G(await $.storage.createTaskHandler({body:{taskName:O.name,markdown:O.markdown??null,projectId:O.project?.id??null,teamId:O.team?.id??null,shareWithTeam:O.shareWithTeam??!0}}));return new _$($,j)}static async forPullRequest($,O){let j=V7(O),{pullRequests:x}=G(await $.storage.getGithubPullRequestTasks({body:{githubKeys:[j]}}));return(x[0]?.taskIds??[]).map((S)=>new _$($,S))}async completed(){let{subType:$}=await this.detail.get();if($!=="task")return;let j=(await this.properties()).find((x)=>x.definition.id===Q7)?.value;return j?.type==="SelectOption"&&j.value.includes(J7)}webUrl(){return`${this.client.webAppUrl}/app/task/${this.id}`}static search=x$({filters:{document_filters:{sub_types:["task"],task_filters:{}}},type:"document",make:($,O)=>new _$($,O.document_id)})}class SO extends M{access=new o(this.client,"initiative",this);static byId($,O){return new SO($,O)}static from($,O){return new SO($,O.id,O)}async fetch(){return G(await this.client.storage.getInitiative({path:{initiative_id:this.id}}))}name=this.field("name");owner=this.mappedField("ownerId",($)=>P.byId(this.client,$));members=this.mappedField("memberIds",($)=>$.map((O)=>P.byId(this.client,O)));tasks=this.mappedField("taskIds",($)=>$.map((O)=>_$.byId(this.client,O)));accessLevel=this.field("userAccessLevel");sharePermissions=this.field("sharePermission");async update($){await this.mutate((O)=>O.storage.updateInitiative({path:{initiative_id:this.id},body:{name:$.name,memberIds:$.members?.map((j)=>j.id),sharePermission:$.sharePermission?n$($.sharePermission):void 0}}))}async delete(){await this.mutate(($)=>$.storage.deleteInitiative({path:{initiative_id:this.id}}))}}class N0{client;constructor($){this.client=$}byId($){return SO.byId(this.client,$)}async list(){return G(await this.client.storage.listInitiatives()).initiatives.map(($)=>SO.byId(this.client,$.id))}async create($){return SO.from(this.client,G(await this.client.storage.createInitiative({body:{name:$.name,description:$.description,memberIds:$.members?.map((O)=>O.id),shareWithTeam:$.shareWithTeam,propertyValues:$.propertyValues}})))}}class U0{client;constructor($){this.client=$}byId($){return H$.byId(this.client,$)}async create($){return H$.from(this.client,G(await this.client.storage.meetingCreate({body:$})))}async list(){return G(await this.client.storage.meetingList()).meetings.map(($)=>H$.from(this.client,$))}async active(){return G(await this.client.storage.meetingListActive()).meetings.map(($)=>H$.from(this.client,$))}async lookup($){return H$.from(this.client,G(await this.client.storage.meetingLookup({path:{token:$}})))}}class UO extends M{async fetch(){return G(await this.client.notification.getTypedNotificationById({path:{notification_id:this.id}}))}static byId($,O){return new UO($,O)}static from($,O){return new UO($,O.id,O)}static list($,O){return u(async(j)=>{let x=G(await $.notification.listTypedNotifications({query:{...O?.pageSize?{limit:O.pageSize}:{},...O?.states!==void 0?{states:O.states.join(",")}:{},...j?{cursor:j}:{}}}));return{items:x.items.map((S)=>UO.from($,S)),nextCursor:x.next_cursor}})}type=this.field("notification_event_type");createdAt=this.field("created_at");updatedAt=this.field("updated_at");state=this.field("state");done=this.mappedField("state",($)=>$==="done");sent=this.field("sent");viewedAt=this.field("viewed_at");deletedAt=this.field("deleted_at");owner=this.mappedField("owner_id",($)=>P.byId(this.client,$));sender=this.mappedField("sender_id",($)=>$?P.byId(this.client,$):void 0);metadata=this.field("notification_metadata");async seen(){return await this.state()!=="unseen"}async markSeen(){await this.mutate(($)=>$.notification.bulkMarkNotificationsSeen({body:{notificationIds:[this.id]}}))}async markDone(){await this.mutate(($)=>$.notification.bulkMarkNotificationsDone({body:{notificationIds:[this.id]}}))}async markUndone(){await this.mutate(($)=>$.notification.bulkMarkNotificationsUndone({body:{notificationIds:[this.id]}}))}async delete(){await this.mutate(($)=>$.notification.deleteUserNotificationV2({path:{notification_id:this.id}}))}}class z0{client;constructor($){this.client=$}byId($){return UO.byId(this.client,$)}list($){return UO.list(this.client,$)}async markSeen($){G(await this.client.notification.bulkMarkNotificationsSeen({body:{notificationIds:$}}))}async markDone($){G(await this.client.notification.bulkMarkNotificationsDone({body:{notificationIds:$}}))}async delete($){G(await this.client.notification.bulkDeleteUserNotificationsV2({body:{notificationIds:$}}))}async preferences(){return G(await this.client.notification.getNotificationTypePreferences())}async enableType($){G(await this.client.notification.enableNotificationType({path:{notification_event_type:$}}))}async disableType($){G(await this.client.notification.disableNotificationType({path:{notification_event_type:$}}))}}class I0{client;constructor($){this.client=$}async list(){let{data:$}=G(await this.client.storage.getPinsHandler({}));return $?.recent??[]}async add($,O){G(await this.client.storage.addPinHandler({path:{pinned_item_id:$.id},body:{pinType:$.entityType,pinIndex:O.index}}))}async remove($){G(await this.client.storage.removePinHandler({path:{pinned_item_id:$.id},body:{pinType:$.entityType}}))}async reorder($){G(await this.client.storage.reorderPinsHandler({body:$}))}}class T0{client;constructor($){this.client=$}byId($){return n.byId(this.client,$)}create($){return n.create(this.client,$)}search($,O){return n.search(this.client,$,O)}}class DO{client;definitionId;id;record;constructor($,O,j,x){this.client=$;this.definitionId=O;this.id=j;this.record=x}static from($,O,j){return new DO($,O,j.id,j)}get value(){return this.record.value}get color(){return this.record.color??void 0}get displayOrder(){return this.record.display_order}get createdAt(){return this.record.created_at}get updatedAt(){return this.record.updated_at}async update($){return this.record=G(await this.client.properties.updatePropertyOption({path:{definition_id:this.definitionId,option_id:this.id},body:$})),this}async delete(){G(await this.client.properties.deletePropertyOption({path:{definition_id:this.definitionId,option_id:this.id}}))}}class ZO{client;id;detail;constructor($,O,j){this.client=$;this.id=O;this.detail=new W$(()=>this.fetch(),j)}async fetch(){return G(await this.client.properties.getPropertyDefinition({path:{definition_id:this.id}}))}static async create($,O){let j=G(await $.properties.createPropertyDefinition({body:{display_name:O.displayName,scope:O.scope,data_type:O.dataType}}));return new ZO($,j.id,j)}static byId($,O){return new ZO($,O)}static fromRecord($,O){return new ZO($,O.id,O)}get displayName(){return this.detail.get().then(($)=>$.display_name)}get dataType(){return this.detail.get().then(($)=>$.data_type)}get owner(){return this.detail.get().then(($)=>$.owner)}get isSystem(){return this.detail.get().then(($)=>$.is_system)}get isMultiSelect(){return this.detail.get().then(($)=>$.is_multi_select)}get createdAt(){return this.detail.get().then(($)=>$.created_at)}get updatedAt(){return this.detail.get().then(($)=>$.updated_at)}async delete(){return G(await this.client.properties.deletePropertyDefinition({path:{definition_id:this.id}}))}async options(){return G(await this.client.properties.getPropertyOptions({path:{definition_id:this.id}})).map((O)=>DO.from(this.client,this.id,O))}async addOption($){let O=G(await this.client.properties.addPropertyOption({path:{definition_id:this.id},body:$}));return DO.from(this.client,this.id,O)}async updateOption($,O){let j=G(await this.client.properties.updatePropertyOption({path:{definition_id:this.id,option_id:$.id},body:O}));return DO.from(this.client,this.id,j)}async deleteOption($){return G(await this.client.properties.deletePropertyOption({path:{definition_id:this.id,option_id:$.id}}))}}class H0{client;constructor($){this.client=$}async list($){return G(await this.client.properties.listProperties({query:{scope:$?.scope??"all",...$?.forEntityType!==void 0?{for_entity_type:$.forEntityType}:{}}})).map((j)=>{let x="definition"in j?j.definition:j;return ZO.fromRecord(this.client,x)})}create($){return ZO.create(this.client,$)}definition($){return ZO.byId(this.client,$)}async bulkEntityProperties($){return G(await this.client.properties.getBulkEntityProperties({body:$}))}async updateEntityPropertyOptions($,O){return G(await this.client.properties.bulkUpdateEntitiesPropertyOptions({body:{entities:$.map((x)=>x.propertyReference()),property_id:O.property.id,...O.add?{add_option_ids:O.add.map((x)=>x.id)}:{},...O.remove?{remove_option_ids:O.remove.map((x)=>x.id)}:{}}})).results}async addEntityPropertyOption($){return G(await this.client.properties.addEntityPropertyOption({path:{...$.entity.propertyReference(),property_id:$.property.id,option_id:$.option.id}}))}async removeEntityPropertyOption($){return G(await this.client.properties.removeEntityPropertyOption({path:{...$.entity.propertyReference(),property_id:$.property.id,option_id:$.option.id}}))}async tags(){return G(await this.client.properties.listTags({}))}}function R0($){return c($).with({type:"cron"},(O)=>O).with({type:"events"},(O)=>({type:"events",filters:O.filters.map(({entities:j,...x})=>({...x,ids:j?.map((S)=>S.entity.id)}))})).exhaustive()}function W7($){return c($).with({type:"multiple"},(O)=>({type:"multiple",triggers:O.triggers.map(R0)})).with({type:"cron"},R0).with({type:"events"},R0).exhaustive()}class XO extends M{static byId($,O){return new XO($,O)}static from($,O){if(!O.id)throw Error("Routine response has no id");return new XO($,O.id,O)}async fetch(){return G(await this.client.scheduledAction.getScheduledAction({path:{id:this.id}}))}name=this.field("name");trigger=this.field("trigger");enabled=this.field("enabled");runVisibility=this.field("run_visibility");nextRunAt=this.field("next_run_at");revision=this.field("configuration_revision");owner=this.mappedField("owner",($)=>P.byId(this.client,$));async update($){await this.mutate((O)=>O.scheduledAction.updateScheduledAction({path:{id:this.id},body:_0($)}))}async setEnabled($){await this.mutate((O)=>O.scheduledAction.setScheduledActionEnabled({path:{id:this.id},body:{enabled:$}}))}async execute(){let $=G(await this.client.scheduledAction.executeScheduledActionNow({path:{id:this.id}}));if($.resource)return c($.resource).with({type:"agent"},(O)=>i.byId(this.client,O.id)).with({type:"chat"},(O)=>K$.byId(this.client,O.id)).exhaustive();return $.chat_id?K$.byId(this.client,$.chat_id):void 0}async history(){return G(await this.client.scheduledAction.listScheduledActionHistory({path:{id:this.id}})).map(($)=>new D0(this,$))}async delete(){await this.mutate(($)=>$.scheduledAction.deleteScheduledAction({path:{id:this.id}}))}}class D0{routine;record;constructor($,O){this.routine=$;this.record=O}get id(){return this.record.id??void 0}toJSON(){return{routine:this.routine.id,...this.record}}}function _0($){let O={prompt:$.prompt,user_prompt:$.userPrompt,model:$.model,agent:$.agent?{bot_id:$.agent.id}:void 0};return{kind:"Agent",name:$.name,enabled:$.enabled,run_visibility:$.runVisibility,trigger:W7($.trigger),task:{...O}}}class C0{client;constructor($){this.client=$}byId($){return XO.byId(this.client,$)}async create($){return XO.from(this.client,G(await this.client.scheduledAction.createScheduledAction({body:{..._0($),enabled:$.enabled??!0}})))}async list($){return G(await this.client.scheduledAction.listScheduledActions({query:{include_events:$?.includeEvents}})).map((O)=>XO.from(this.client,O))}}class P0{client;constructor($){this.client=$}byId($){return _$.byId(this.client,$)}create($){return _$.create(this.client,$)}forPullRequest($){return _$.forPullRequest(this.client,$)}search($,O){return _$.search(this.client,$,O)}}class M0{client;constructor($){this.client=$}byId($){return S$.byId(this.client,$)}current(){return S$.current(this.client)}list(){return S$.list(this.client)}}class A0{client;constructor($){this.client=$}async get($){return G(await this.client.unfurl.getUnfurl({query:{url:$}}))}async bulk($){return G(await this.client.unfurl.getUnfurlBulk({body:{url_list:$}}))}}class F0{client;constructor($){this.client=$}byId($){return P.byId(this.client,$)}me(){return P.me(this.client)}async search($){let O=$.trim().toLocaleLowerCase();if(!O)throw Error("A person search needs a name or email.");let j=await this.client.contacts.getContacts();if(j.response?.status===404)return[];let{contacts:x}=G(j),S=[...new Set(x)],K=[];for(let Z=0;Z[Q.id,Q]));for(let Q of X){let J=L.get(Q)??{id:Q};if(`${[J.first_name,J.last_name].filter(Boolean).join(" ")} ${Q}`.toLocaleLowerCase().includes(O))K.push(P.fromName(this.client,J))}}return K}}class KO extends M{signingSecret;constructor($,O,j,x){super($,O,j);this.signingSecret=x}async fetch(){return G(await this.client.storage.getWebhook({path:{webhook_id:this.id}}))}static async create($,O){let j=G(await $.storage.createWebhook({body:{endpoint_url:O.url,namespace:O.namespace,name:O.name,filters:O.filters,scope:O.scope??"user",...O.headers!==void 0?{headers:O.headers}:{}}}));return new KO($,j.id,j,j.signing_secret)}static byId($,O){return new KO($,O)}static fromRecord($,O){return new KO($,O.id,O)}name=this.field("name");namespace=this.field("namespace");endpointUrl=this.field("endpoint_url");status=this.field("status");isValid=this.field("is_valid");filters=this.field("filters");createdAt=this.field("created_at");workspaceId=this.field("workspace_id");createdBy=this.mappedField("created_by_user_id",($)=>P.byId(this.client,$));async scope(){let $=await this.detail.get();return $.workspace_id===$.created_by_user_id?"user":"team"}async update($){await this.mutate((O)=>O.storage.patchWebhook({path:{webhook_id:this.id},body:$}))}async rename($){await this.update({name:$})}async setUrl($){await this.update({endpoint_url:$})}async setFilters($){await this.update({filters:$})}async pause(){await this.update({status:"paused"})}async resume(){await this.update({status:"active"})}async delete(){await this.mutate(($)=>$.storage.deleteWebhook({path:{webhook_id:this.id}}))}async validate(){return this.mutate(($)=>$.storage.validateWebhook({path:{webhook_id:this.id}}))}}class E0{client;constructor($){this.client=$}create($){return KO.create(this.client,$)}byId($){return KO.byId(this.client,$)}async list(){let{webhooks:$}=G(await this.client.storage.listWebhooks());return $.map((O)=>KO.fromRecord(this.client,O))}}class _O extends M{static byId($,O){return new _O($,O)}static from($,O){return new _O($,O.id,O)}async fetch(){let{account:$}=G(await this.client.storage.linkedAccount());if($?.id!==this.id)throw new E("WhatsApp account is not available");return $}state=this.field("state");identity=this.field("identity_jid");owner=this.mappedField("user_id",($)=>P.byId(this.client,$));async refresh(){return this.detail.clear(),await this.detail.get(),this}}class C$ extends M{group;constructor($,O,j,x){super($,j,x);this.group=O}static byId($,O,j){return new C$($,O,j)}static from($,O,j){return new C$($,O,j.id,j)}get groupId(){return this.group.id}async fetch(){for await(let $ of this.group.messages({limit:100}))if($.id===this.id)return $.detail.get();throw new E("WhatsApp message is not available")}body=this.field("body");sender=this.field("sender");sentAt=this.field("sent_at");editedAt=this.field("edited_at");deleted=this.field("deleted");reactions=this.field("reactions");deliveryState=this.field("delivery_state");replyTo=this.mappedField("reply_to",($)=>$?C$.byId(this.client,this.group,$):void 0);attachments=this.mappedField("attachments",($)=>$.map((O)=>({ordinal:O.ordinal,state:O.state,fileName:O.file_name,mime:O.mime,size:O.size,document:O.document_id?w.byId(this.client,O.document_id):void 0})))}class YO{id;groupId;filename;kind;constructor($,O,j,x){this.id=$;this.groupId=O;this.filename=j;this.kind=x}static byId($,O){return new YO(O,$.id)}static from($,O){return new YO(O.upload_id,$.id,O.filename,O.kind)}toJSON(){return{id:this.id,groupId:this.groupId,filename:this.filename,kind:this.kind}}}class a$ extends M{group;constructor($,O,j,x){super($,j,x);this.group=O}static byId($,O,j){return new a$($,O,j)}static from($,O,j){return new a$($,O,j.id,j)}async fetch(){let O=G(await this.client.storage.linkedDelivery({path:{group_id:this.group.id},body:[this.id]})).find((j)=>j.id===this.id);if(!O)throw new E("WhatsApp send is not available");return O}state=this.field("state");errorCode=this.field("error_code");createdAt=this.field("created_at");message=this.mappedField("message_id",($)=>$?C$.byId(this.client,this.group,$):void 0);async request(){let{payload:$}=await this.detail.get(),O=$.message,j=c(O).returnType().with({type:"media"},(x)=>({type:"media",upload:YO.byId(this.group,x.upload_id),caption:x.caption??void 0})).with({type:"mutation"},(x)=>({type:"mutation",message:C$.byId(this.client,this.group,x.message_id),operation:x.operation,text:x.text??void 0})).with({type:"text"},(x)=>x).exhaustive();return{idempotencyKey:$.idempotency_key,message:j,replyTo:$.reply_to?C$.byId(this.client,this.group,$.reply_to):void 0}}async refresh(){return this.detail.clear(),await this.detail.get(),this}}class v$ extends z${entityType="whatsapp_group";static byId($,O){return new v$($,O)}async fetch(){return G(await this.client.storage.whatsappGroup({path:{group_id:this.id}}))}name=this.field("name");canSend=this.field("can_send");sendReason=this.field("send_reason");sendingIdentity=this.field("sending_identity");unreadCount=this.field("unread_count");backfill=this.field("backfill");limits=this.field("limits");roster=this.field("roster");owner=this.mappedField("user_id",($)=>P.byId(this.client,$));async*messages($={}){yield*u(async(O)=>{let j=G(await this.client.storage.whatsappGroupMessages({path:{group_id:this.id},query:{before:O,limit:$.limit}}));return{items:j.messages.map((x)=>C$.from(this.client,this,x)),nextCursor:j.next_cursor}})}async markRead($){this.assertGroup($.groupId),await this.mutate((O)=>O.storage.whatsappGroupRead({path:{group_id:this.id},body:{message_id:$.id}}))}async permissions(){return G(await this.client.storage.whatsappGroupPermissions({path:{group_id:this.id}}))}async updatePermissions($){await this.mutate((O)=>O.storage.whatsappGroupUpdatePermissions({path:{group_id:this.id},body:$}))}async detach(){await this.mutate(($)=>$.storage.linkedDetach({path:{group_id:this.id}}))}async upload($){let O=G(await this.client.storage.linkedUpload({path:{group_id:this.id},body:{file:$}}));return YO.from(this,O)}async reply($,O){if(O.replyTo)this.assertGroup(O.replyTo.groupId);let j=c($).returnType().with({type:"text"},(S)=>S).with({type:"media"},(S)=>{return this.assertGroup(S.upload.groupId),{type:"media",upload_id:S.upload.id,caption:S.caption}}).with({type:"mutation"},(S)=>{return this.assertGroup(S.message.groupId),{type:"mutation",message_id:S.message.id,operation:S.operation,text:S.text}}).exhaustive(),x=await this.mutate((S)=>S.storage.linkedReply({path:{group_id:this.id},body:{idempotency_key:O.idempotencyKey,message:j,reply_to:O.replyTo?.id}}));return a$.from(this.client,this,x)}async sends(){return G(await this.client.storage.whatsappGroupSends({path:{group_id:this.id}})).map(($)=>a$.from(this.client,this,$))}async delivery($){for(let O of $)this.assertGroup(O instanceof a$?O.group.id:O.groupId);return G(await this.client.storage.linkedDelivery({path:{group_id:this.id},body:$.map((O)=>O.id)})).map((O)=>a$.from(this.client,this,O))}async messageAttachments($){for(let O of $)this.assertGroup(O.groupId);return G(await this.client.storage.linkedMessageAttachments({path:{group_id:this.id},body:$.map((O)=>O.id)})).map((O)=>({message:C$.byId(this.client,this,O.message_id),attachments:O.linked_attachments.map((j)=>({ordinal:j.ordinal,state:j.state,fileName:j.file_name,mime:j.mime,size:j.size,document:j.document_id?w.byId(this.client,j.document_id):void 0}))}))}async refresh(){return this.detail.clear(),await this.detail.get(),this}assertGroup($){if($!==this.id)throw Error("WhatsApp reference belongs to another group")}}class CO extends M{static byId($,O){return new CO($,O)}static from($,O){return new CO($,O.jid,O)}async fetch(){let{chats:$}=G(await this.client.storage.linkedChats()),O=$.find((j)=>j.jid===this.id);if(!O)throw new E("WhatsApp chat is not available");return O}kind=this.field("kind");name=this.field("name");lastActivityAt=this.field("lastActivityAt");archived=this.field("archived");pinned=this.field("pinned");canAdd=this.field("canAdd");cannotAddReason=this.field("cannotAddReason");group=this.mappedField("group",($)=>$?v$.byId(this.client,$.id):void 0);shared=this.mappedField("shared",($)=>$?v$.byId(this.client,$.id):void 0);async adopt(){let $=await this.mutate((O)=>O.storage.linkedAdopt({body:{group_jid:this.id}}));return v$.byId(this.client,$.group_id)}async refresh(){return this.detail.clear(),await this.detail.get(),this}}class w0{client;constructor($){this.client=$}async account(){let{account:$}=G(await this.client.storage.linkedAccount());return $?_O.from(this.client,$):void 0}async groups(){return G(await this.client.storage.whatsappGroups()).map(($)=>v$.byId(this.client,$.id))}async chats(){return G(await this.client.storage.linkedChats()).chats.map(($)=>CO.from(this.client,$))}byId($){return v$.byId(this.client,$)}}function q0($){if("properties"in $.data)return $.data.properties;if($.tag==="calendarEvent")return $.data.extra.properties;return[]}function a1($,O,j){let x=[];for(let S of q0(O)){if(S.definition.data_type!=="TAG"||S.value?.type!=="SelectOption")continue;for(let K of S.value.value){let Z=j.get(K);if(Z&&!x.some((X)=>X.label===Z.label&&X.scope===Z.scope))x.push(Z)}}switch(O.tag){case"document":return{type:"document",id:O.data.id,entity:w.byId($,O.data.id),snapshot:O,tags:x,name:O.data.name,fileType:O.data.fileType,subType:O.data.subType?.type};case"chat":return{type:"ai_chat",id:O.data.id,entity:K$.byId($,O.data.id),snapshot:O,tags:x,name:O.data.name};case"project":return{type:"project",id:O.data.id,entity:n.byId($,O.data.id),snapshot:O,tags:x,name:O.data.name};case"emailThread":return{type:"email",id:O.data.id,entity:R$.byId($,O.data.id),snapshot:O,tags:x,subject:O.data.name,snippet:O.data.snippet,senderName:O.data.senderName,senderEmail:O.data.senderEmail,inboxVisible:O.data.inboxVisible,isRead:O.data.isRead,isDraft:O.data.isDraft};case"channel":return{type:"channel",id:O.data.channel.id,entity:b.byId($,O.data.channel.id),snapshot:O,tags:x,name:O.data.channel.name};case"channelThread":return{type:"channel_thread",id:O.data.id,entity:new h$($,O.data.channel_id,O.data.id),snapshot:O,tags:x,channelId:O.data.channel_id};case"call":return{type:"call",id:O.data.callId,entity:Q$.byId($,O.data.callId),snapshot:O,tags:x,createdBy:O.data.createdBy};case"calendarEvent":return{type:"calendar_event",id:O.data.id,entity:$$.byId($,O.data.id),snapshot:O,tags:x,title:O.data.title,status:O.data.status,location:O.data.location,conferenceUrl:O.data.conferenceUrl,conferenceProvider:O.data.conferenceProvider,time:O.data.time};case"foreignEntity":return{type:"foreign_entity",id:O.data.id,entity:xO.byId($,O.data.id),snapshot:O,tags:x,foreignEntityId:O.data.foreignEntityId,foreignEntitySource:O.data.foreignEntitySource,metadata:O.data.metadata};default:throw Error(`Unexpected workspace entity type: ${O.tag}`)}}var i1="00000000-0000-0000-0000-000000000000",f0={document:["df","id"],project:["pf","pid"],ai_chat:["cf","cid"],email:["ef","ThreadId"],channel:["chanf","ChannelId"],channel_thread:["cthf","ThreadId"],call:["callf","CallId"],calendar_event:["calf","id"],foreign_entity:["fef","id"]},Aj=($)=>$.replace(/[A-Z]/g,(O)=>O.toLowerCase()),n1=($,O)=>$==null?O:{"&":[$,O]};function b0($,O){if(!$||typeof $!=="object"||Array.isArray($))throw Error(`Invalid workspace ${O}: expected a filter expression.`);let j=Object.entries($),x=j[0];if(j.length!==1||x===void 0)throw Error(`Invalid workspace ${O}: use exactly one of l, &, |, !.`);let[S,K]=x;if(S==="l")return;if(S==="!"){b0(K,`${O}.!`);return}if(S!=="&"&&S!=="|")throw Error(`Invalid workspace ${O}: use l, &, | or !.`);if(!Array.isArray(K)||K.length!==2)throw Error(`Invalid workspace ${O}.${S}: expected exactly two expressions. Use {l: {...}} for a single condition; nest binary operators for three or more.`);K.forEach((Z,X)=>{b0(Z,`${O}.${S}[${X}]`)})}class Fj{client;tagSets;constructor($){this.client=$}sets(){return this.tagSets??=this.client.properties.listTags({}).then(G),this.tagSets}async tagMap(){let $=new Map;for(let O of await this.sets())for(let j of O.options)if(j.value.type==="string")$.set(j.id,{label:j.value.value,scope:O.scope==="user"?"personal":"team"});return $}async body($){for(let[S,K,Z]of[["sortBy",$.sortBy,["recently_viewed","recently_updated","recently_created"]],["emailPreset",$.emailPreset,["signal"]],["tagsMatch",$.tagsMatch,["any","all"]]])if(K!==void 0&&!Z.includes(K))throw Error(`Invalid workspace ${S}: ${K}`);if($.includeTypes!==void 0&&(!Array.isArray($.includeTypes)||$.includeTypes.some((S)=>!Object.hasOwn(f0,S))))throw Error(`Invalid workspace includeTypes. Supported values: ${Object.keys(f0).join(", ")}. Tasks use document with df: {l: {dst: 'task'}}.`);for(let S of["df","pf","cf","ef","chanf","cthf","callf","fef","calf","propf"])if($[S]!=null)b0($[S],S);if($.tags?.some((S)=>S.scope!==void 0&&S.scope!=="personal"&&S.scope!=="team"))throw Error("Invalid workspace tag scope");if($.limit!==void 0&&!Number.isFinite($.limit))throw Error("Workspace limit must be finite");let O={df:$.df,pf:$.pf,cf:$.cf,ef:$.ef,chanf:$.chanf,cthf:$.cthf,callf:$.callf,fef:$.fef,calf:$.calf,propf:$.propf,ccf:{l:{id:i1}},favorites_only:$.favoritesOnly,expand:!0,limit:Math.max(1,Math.min(500,Math.trunc($.limit??50))),sort_method:$.sortBy==="recently_viewed"?"viewed_at":$.sortBy==="recently_created"?"created_at":"updated_at",sort_direction:"desc",emailView:$.emailView??"inbox"};if($.emailPreset==="signal")O.ef={"&":[{l:{Importance:!0}},{l:{Shared:"exclude"}}]};let j=$.inbox?.trim();if(j){let{links:S}=G(await this.client.email.listLinks()),K=S.filter((X)=>Aj(X.email_address)===Aj(j)),Z=K[0];if(!Z)throw Error(`No connected inbox matches "${j}". Connected inboxes: ${S.map((X)=>X.email_address).join(", ")}.`);if(K.length>1)throw Error(`Multiple connected inboxes match "${j}"; cannot pick one.`);O.ef=n1(O.ef,{l:{Owner:Z.id}})}let x=$.includeTypes??($.emailPreset==="signal"?["email"]:void 0);if(x?.length){for(let[S,[K,Z]]of Object.entries(f0))if(!x.includes(S))O[K]={l:{[Z]:i1}}}if($.tags?.length){let S=(await this.sets()).flatMap((Y)=>{let L=Y.definition;if(!L)return[];return Y.options.flatMap((Q)=>Q.value.type==="string"?[{id:Q.id,definitionId:L.id,label:Q.value.value,scope:Y.scope==="user"?"personal":"team"}]:[])}),K=new Map;for(let Y of $.tags){let L=S.filter((Q)=>Aj(Q.label)===Aj(Y.label.trim())&&(!Y.scope||Q.scope===Y.scope));if(!L.length)throw Error(`Unknown tag "${Y.label}". Available tags: ${S.map((Q)=>`${Q.label} (${Q.scope})`).join(", ")}.`);if($.tagsMatch==="all"&&L.length>1)throw Error(`Tag "${Y.label}" is ambiguous; specify personal or team scope.`);for(let Q of L)K.set(Q.id,Q)}let X=[...K.values()].map((Y)=>({l:{pd:Y.definitionId,v:{so:Y.id}}})).reduce((Y,L)=>Y===void 0?L:{[$.tagsMatch==="all"?"&":"|"]:[Y,L]},void 0);O.propf=n1(O.propf,X)}return O}}class k0{client;constructor($){this.client=$}async page($={}){let O=new Fj(this.client);return this.fetch(O,await O.body($),$.cursor)}async*list($={}){let O=new Fj(this.client),j=await O.body($);yield*u((x)=>this.fetch(O,j,x))}async fetch($,O,j){let x=G(await this.client.storage.postItemsSoupAst({body:O,query:j?{cursor:j}:void 0})),K=x.items.some((Z)=>q0(Z).some((X)=>X.definition.data_type==="TAG"))?await $.tagMap():new Map;return{items:x.items.map((Z)=>a1(this.client,Z,K)),nextCursor:x.next_cursor,hasMore:x.next_cursor!=null}}}function s1({onRequest:$,onSseError:O,onSseEvent:j,responseTransformer:x,responseValidator:S,sseDefaultRetryDelay:K,sseMaxRetryAttempts:Z,sseMaxRetryDelay:X,sseSleepFn:Y,url:L,...Q}){let J,I=Y??((V)=>new Promise((B)=>setTimeout(B,V)));return{stream:async function*(){let V=K??3000,B=0,T=Q.signal??new AbortController().signal;while(!0){if(T.aborted)break;B++;let A=Q.headers instanceof Headers?Q.headers:new Headers(Q.headers);if(J!==void 0)A.set("Last-Event-ID",J);try{let D={redirect:"follow",...Q,body:Q.serializedBody,headers:A,signal:T},_=new Request(L,D);if($)_=await $(L,D);let N=await(Q.fetch??globalThis.fetch)(_);if(!N.ok)throw Error(`SSE failed: ${N.status} ${N.statusText}`);if(!N.body)throw Error("No body in SSE response");let H=N.body.pipeThrough(new TextDecoderStream).getReader(),U="",k=()=>{try{H.cancel()}catch{}};T.addEventListener("abort",k);try{while(!0){let{done:l,value:r}=await H.read();if(l)break;U+=r,U=U.replace(/\r\n?/g,`
+async function cO($,O,j){if(!Number.isInteger(O)||O<1)throw RangeError(`concurrency limit must be a positive integer, got ${O}`);let x=Array($.length),S=0,K=async()=>{while(S<$.length){let X=S;S+=1,x[X]=await j($[X])}},Z=Array.from({length:Math.min(O,$.length)},K);return await Promise.all(Z),x}class W${loader;value;inflight;constructor($,O){this.loader=$;this.value=O}get(){if(this.value!==void 0)return Promise.resolve(this.value);return this.inflight??=this.loader().then(($)=>{return this.value=$,$}),this.inflight}peek(){return this.value}clear(){this.value=void 0,this.inflight=void 0}}async function*u($){let O;do{let j=await $(O),x=j.nextCursor??void 0;if(O!==void 0&&x!==void 0&&JSON.stringify(x)===JSON.stringify(O))throw Error("Pagination cursor did not advance");yield*j.items,O=x}while(O!==void 0)}class f extends Error{constructor($){super($);this.name="MacroError"}}class GO extends f{status;data;constructor($,O){super(`Macro API error ${$}`);this.status=$;this.data=O;this.name="MacroApiError"}}class E extends f{constructor($){super($);this.name="MacroNotFoundError"}}function G($){if($.error!==void 0)throw new GO($.response?.status??0,$.error);return $.data}class M{client;id;detail;constructor($,O,j){this.client=$;this.id=O;this.detail=new W$(()=>this.fetch(),j)}field($){return async()=>(await this.detail.get())[$]??void 0}mappedField($,O){return async()=>O((await this.detail.get())[$]??void 0)}toJSON(){let $=this.detail.peek();return $===void 0?{id:this.id}:{id:this.id,detail:$}}async mutate($){let O=G(await $(this.client));return this.detail.clear(),O}}class z$ extends M{async permission(){return G(await this.client.storage.getEntityPermission({path:{entity_type:this.entityType,entity_id:this.id}}))}async favorite(){return G(await this.client.storage.addFavorite({body:{entityId:this.id,entityType:this.entityType}})),this}async unfavorite(){return G(await this.client.storage.removeFavoriteByEntity({path:{entity_type:this.entityType,entity_id:this.id}})),this}}class X$ extends z${propertyReference(){return{entity_type:this.propertyEntityType,entity_id:this.id}}async properties($){let{properties:O}=G(await this.client.properties.getEntityProperties({path:{entity_type:this.propertyEntityType,entity_id:this.id},query:$?.includeMetadata!==void 0?{include_metadata:$.includeMetadata}:void 0}));return O}async setProperty($,O){G(await this.client.properties.setEntityProperty({path:{...this.propertyReference(),property_id:$.id},body:{value:O??null}}))}async updatePropertyOptions($){return G(await this.client.properties.bulkUpdateEntityPropertyOptions({path:this.propertyReference(),body:{properties:$.map((O)=>({property_id:O.property.id,...O.add?{add_option_ids:O.add.map((j)=>j.id)}:{},...O.remove?{remove_option_ids:O.remove.map((j)=>j.id)}:{}}))}}))}async deleteProperty($){G(await this.client.properties.deleteEntityProperty({path:{entity_property_id:$.property.id}}))}}var w6={"<":"\\u003c",">":"\\u003e","&":"\\u0026"};function i$($,O){let j=JSON.stringify(O).replace(/[<>&]/g,(x)=>w6[x]??x);return`<${$}>${j}${$}>`}function q6($){return typeof $==="object"&&$!==null&&typeof $.toMention==="function"}var f6={toMention:()=>({tag:i$("m-group-mention",{groupAlias:"here"}),mention:{entity_type:"group",entity_id:"here"}})};function b6($,...O){let j="",x=[];for(let[S,K]of $.entries()){j+=K;let Z=O[S];if(Z===void 0)continue;if(q6(Z)){let X=Z.toMention();j+=X.tag,x.push(X.mention)}else j+=String(Z)}return{content:j,mentions:k6(x)}}function k6($){let O=new Set;return $.filter((j)=>{let x=`${j.entity_type}:${j.entity_id}`;if(O.has(x))return!1;return O.add(x),!0})}function O$($){return typeof $==="string"?{content:$,mentions:[]}:$}class B$ extends M{static byId($,O){return new B$($,O)}static from($,O){return new B$($,O.id,O)}async fetch(){let $=G(await this.client.storage.listHarnesses()).find((O)=>O.id===this.id);if(!$)throw new E(`Harness ${this.id} is unavailable`);return $}name=this.field("name");connected=this.field("connected");kind=this.field("kind");async delete(){await this.mutate(($)=>$.storage.deleteHarness({path:{harness_id:this.id}}))}}class I$ extends M{static byId($,O){return new I$($,O)}static from($,O){return new I$($,O.bot.id,O)}async fetch(){let $=G(await this.client.storage.listAgents()).find((O)=>O.bot.id===this.id);if(!$)throw new E(`Agent ${this.id} is unavailable`);return $}name=this.mappedField("bot",($)=>$.name);instructions=this.field("instructions");defaultModel=this.field("default_model");channels=this.mappedField("channel_ids",($)=>$.map((O)=>b.byId(this.client,O)));host=this.mappedField("harness_id",($)=>$?B$.byId(this.client,$):void 0);async update($){await this.mutate((O)=>O.storage.updateAgent({path:{agent_id:this.id},body:gx($)}))}}function gx($){let{channels:O,team:j,host:x,autoAcceptPermissions:S,avatarUrl:K,channelScope:Z,defaultModel:X,isCoding:Y,...L}=$;return{...L,channel_ids:O?.map((Q)=>Q.id),team_id:j?.id,harness_id:x?.id,auto_accept_permissions:S,avatar_url:K,channel_scope:Z,default_model:X,is_coding:Y}}async function Wj($,O,j){let{channelId:x,thread:S}=g6(O);if(await y6($)){let{id:Z}=G(await $.storage.entityMessageCreate({path:{parent_type:"channel",parent_id:x},body:{content:j.content,mentions:j.mentions,attachments:[],thread_id:S?.rootId??null,nonce:crypto.randomUUID()}}));return Z}if(S)throw new f("a user-owned bot can only post via the channel webhook endpoint, which "+"does not support threads — use a team-owned bot, or requestedAs(user), "+"to reply in a thread");let{message_id:K}=G(await $.storage.postChannelBotWebhook({path:{channel_id:x},body:{content:j.content},headers:{"x-macro-bot-scope":"user"}}));return K}function g6($){return"rootId"in $?{channelId:$.channelId,thread:$}:{channelId:$.id}}async function y6($){if($.authConfig.type!=="bot"||$.hasActingUser())return!0;let{owner:O}=await $.selfBot();return O?.type==="team"}class h${client;channelId;rootId;constructor($,O,j){this.client=$;this.channelId=O;this.rootId=j}toJSON(){return{channelId:this.channelId,rootId:this.rootId}}channel(){return b.byId(this.client,this.channelId)}root(){return e.byId(this.client,this.channelId,this.rootId)}async replies(){let{replies:$}=G(await this.client.storage.entityMessageGetThread({path:{parent_type:"channel",parent_id:this.channelId,id:this.rootId}}));return $.map((O)=>e.from(this.client,this.channelId,O))}async reply($){let O=await Wj(this.client,this,O$($));return e.byId(this.client,this.channelId,O)}}class e extends z${channelId;mentions;entityType="channel_message";constructor($,O,j,x=[],S){super($,j,S);this.channelId=O;this.mentions=x}get path(){return{parent_type:"channel",parent_id:this.channelId,id:this.id}}async fetch(){return G(await this.client.storage.entityMessageGetMessage({path:this.path}))}static byId($,O,j,x=[]){return new e($,O,j,x)}static from($,O,j){return new e($,O,j.id,j.mentions,j)}content=this.field("content");createdAt=this.field("created_at");editedAt=this.field("edited_at");updatedAt=this.field("updated_at");async author(){return P.byId(this.client,(await this.detail.get()).sender_id)}channel(){return b.byId(this.client,this.channelId)}thread(){return new h$(this.client,this.channelId,this.id)}async react($){return await this.mutate((O)=>O.storage.entityMessageReact({path:this.path,body:{add:!0,emoji:$}})),this}async unreact($){return await this.mutate((O)=>O.storage.entityMessageReact({path:this.path,body:{add:!1,emoji:$}})),this}async edit($){let{content:O,mentions:j}=O$($);return await this.mutate((x)=>x.storage.entityMessageEdit({path:this.path,body:{content:O,mentions:j}})),this}async delete(){await this.mutate(($)=>$.storage.entityMessageDeleteMessage({path:this.path}))}async reply($){return this.thread().reply($)}}class Bj{client;channel;record;constructor($,O,j){this.client=$;this.channel=O;this.record=j}agent(){return I$.byId(this.client,this.record.botId)}sessions(){return this.record.sessions.map(($)=>({session:i.byId(this.client,$.sessionId),isCurrent:$.isCurrent,createdAt:$.createdAt}))}turns(){return this.record.turns.map(($)=>new yx(this.client,this,$))}async startFresh($){G(await this.client.agentHarness.startFreshAgentConversation({path:{channel_id:this.channel.id,bot_id:this.record.botId},body:{sessionId:$.id}}))}toJSON(){return{channelId:this.channel.id,agentId:this.record.botId}}}class yx{client;conversation;record;constructor($,O,j){this.client=$;this.conversation=O;this.record=j}source(){return e.byId(this.client,this.conversation.channel.id,this.record.sourceMessageId)}session(){return i.byId(this.client,this.record.sessionId)}async retry($){G(await this.client.agentHarness.retryAgentConversationTurn({path:{channel_id:this.conversation.channel.id,bot_id:this.conversation.agent().id,source:this.record.sourceMessageId},body:{actionId:$}}))}toJSON(){return{channelId:this.conversation.channel.id,sourceMessageId:this.record.sourceMessageId}}}class L$ extends M{parent;toJSON(){return{...super.toJSON(),parentType:this.parent.type,parentId:this.parent.entity.id}}constructor($,O,j){super($,j);this.parent=O}static byId($,O,j){return new L$($,O,j)}get path(){return{parent_type:this.parent.type,parent_id:this.parent.entity.id,id:this.id}}async fetch(){return G(await this.client.storage.entityMessageGetThread({path:this.path})).state}resolved=this.field("resolved");async update($){let O={resolved:$.resolved,detach_anchor:$.detachAnchor,nonce:$.nonce};await this.mutate((j)=>j.storage.entityMessagePatchThread({path:this.path,body:O}))}async delete($){await this.mutate((O)=>O.storage.entityMessageDeleteThread({path:this.path,query:{nonce:$}}))}}async function w$($,O,j,x){for(let K of[...x?.messages??[],...x?.around?[x.around]:[]]){let Z="channelId"in K?{type:"channel",id:K.channelId}:("documentId"in K)?{type:"document",id:K.documentId}:("callId"in K)?{type:"call",id:K.callId}:K.parent;if(Z.type!==O.type||Z.id!==O.entity.id)throw Error("Timeline message belongs to another resource")}let S=G(await $.storage.messageTimelineEntries({path:{parent_type:O.type,parent_id:O.entity.id},query:{selection:x?JSON.stringify({cursor:x.cursor,around:x.around?.id,ids:x.messages?.map((K)=>K.id),activity_after:x.activityAfter,activity_before:x.activityBefore,anchored:x.anchored,direction:x.direction,include_deleted_threads:x.includeDeletedThreads,limit:x.limit}):void 0}}));return{entries:S.entries.map((K)=>{if(K.type==="activity")return{type:"activity",activity:{...K.activity,actor:P.byId($,K.activity.actor_id)}};return{type:"message",message:j(K.message)}}),nextCursor:S.next_cursor??void 0,previousCursor:S.previous_cursor??void 0}}var v6=50;function x$($){let{filters:O,type:j,make:x,includeCrm:S}=$;return(K,Z,X)=>{let Y=X?.searchOn??"name_content",L=X?.pageSize??v6;return u(async(Q)=>{let{results:J,next_cursor:I}=G(await K.search.unifiedSearch({body:{query:Z,match_type:"partial",search_on:Y,...O?{filters:O}:{},...S?{include_crm:!0}:{}},query:{page_size:L,...Q?{cursor:Q}:{}}}));return{items:J.filter((W)=>W.type===j).map((W)=>x(K,W)),nextCursor:I}})}}class QO{client;channel;record;constructor($,O,j){this.client=$;this.channel=O;this.record=j}get id(){return this.record.id}user(){return P.byId(this.client,this.record.user_id)}createdAt(){return this.record.created_at}async recordInteraction($){return new QO(this.client,this.channel,G(await this.client.storage.postActivity({body:{activity_type:$,channel_id:this.channel.id}})))}toJSON(){return this.record}}class b extends X${async recordActivity($){return new QO(this.client,this,G(await this.client.storage.postActivity({body:{activity_type:$,channel_id:this.id}})))}async setPicture($){await this.mutate((O)=>O.storage.setChannelPicture({path:{channel_id:this.id},body:{profile_picture_id:$?.id??null}}))}messageThreadById($){return L$.byId(this.client,{type:"channel",entity:this},$)}timelinePage($){return w$(this.client,{type:"channel",entity:this},(O)=>e.from(this.client,this.id,O),$)}async agentConversations(){return G(await this.client.agentHarness.listAgentConversations({path:{channel_id:this.id}})).conversations.map(($)=>new Bj(this.client,this,$))}entityType="channel";propertyEntityType="CHANNEL";async fetch(){return G(await this.client.storage.getChannel({path:{channel_id:this.id}}))}static byId($,O){return new b($,O)}static async dm($,O){let{channel_id:j}=G(await $.storage.getOrCreateDm({body:{recipient_id:O.id}}));return new b($,j)}static async private($,O){let{channel_id:j}=G(await $.storage.getOrCreatePrivate({body:{recipients:O.map((x)=>x.id)}}));return new b($,j)}static async create($,O){let{id:j}=G(await $.storage.createChannel({body:{channel_type:O.type,name:O.name??null,participants:(O.participants??[]).map((x)=>x.id),team_id:O.team?.id??null}}));return new b($,j)}name=this.field("channel_name");type=this.field("channel_type");async send($,O){let j=await Wj(this.client,O?.thread??this,O$($));return e.byId(this.client,this.id,j)}messages($){return this.timeline({limit:$?.pageSize})}messagesAfter($,O){return this.timeline({activity_after:$ instanceof Date?$.toISOString():$,limit:O?.pageSize})}timeline($){return u(async(O)=>{let j=G(await this.client.storage.messageTimeline({path:{parent_type:"channel",parent_id:this.id},query:{selection:JSON.stringify({...$,cursor:O})}}));return{items:j.items.map((x)=>e.from(this.client,this.id,x)),nextCursor:j.next_cursor}})}message($){return e.byId(this.client,this.id,$)}async rename($){await this.mutate((O)=>O.storage.patchChannel({path:{channel_id:this.id},body:{channel_name:$}}))}async delete(){await this.mutate(($)=>$.storage.deleteChannel({path:{channel_id:this.id}}))}async join(){await this.mutate(($)=>$.storage.joinChannel({path:{channel_id:this.id}}))}async joinCode(){let{join_code:$}=G(await this.client.storage.getChannelJoinLink({path:{channel_id:this.id}}));return $}async leave(){await this.mutate(($)=>$.storage.leaveChannel({path:{channel_id:this.id}}))}async participants(){return G(await this.client.storage.getChannelParticipants({path:{channel_id:this.id}}))}async addParticipants($){await this.mutate((O)=>O.storage.addParticipants({path:{channel_id:this.id},body:{participants:$.map((j)=>j.id)}}))}async removeParticipants($){await this.mutate((O)=>O.storage.removeParticipants({path:{channel_id:this.id},body:{participants:$.map((j)=>j.id)}}))}async typing($,O){G(await this.client.storage.entityMessageTyping({path:{parent_type:"channel",parent_id:this.id},body:{active:$==="start",thread_id:O?.thread?.rootId??null}}))}attachments($){return u(async(O)=>{let j=G(await this.client.storage.getChannelAttachments({path:{channel_id:this.id},query:{...$?.pageSize?{limit:$.pageSize}:{},...$?.type?{attachment_type:$.type}:{},...O?{cursor:O}:{}}}));return{items:j.items,nextCursor:j.next_cursor}})}webUrl(){return`${this.client.webAppUrl}/app/channel/${this.id}`}static search=x$({type:"channel",make:($,O)=>new b($,O.channel_id)})}class P extends X${entityType="user";propertyEntityType="USER";async fetch(){let{names:$}=G(await this.client.auth.getUserNamesWithEmail({body:{user_ids:[this.id]}})),O=$.find((j)=>j.id===this.id);if(!O)throw new E(`user ${this.id} not found`);return O}static byId($,O){return new P($,O)}static fromName($,O){return new P($,O.id,O)}static async me($){let{user_id:O}=G(await $.auth.getUserInfo());return new P($,O)}async name(){let $=await this.detail.get();return[$.first_name,$.last_name].filter(Boolean).join(" ")||void 0}email(){let $=this.id.startsWith("macro|")?this.id.slice(6):void 0;return $?.includes("@")?$:void 0}dm(){return b.dm(this.client,this)}toMention(){return{tag:i$("m-user-mention",{userId:this.id}),mention:{entity_type:"user",entity_id:this.id}}}}class o{client;entityType;entity;constructor($,O,j){this.client=$;this.entityType=O;this.entity=j}get path(){return{entity_type:this.entityType,entity_id:this.entity.id}}async state($){let O=G(await this.client.storage.accessRequestsState({path:this.path,query:{offset:$}}));return{accessLevel:O.accessLevel??void 0,canReview:O.canReview,owner:O.owner?P.byId(this.client,O.owner):void 0,pendingCount:O.pendingCount,supportedLevels:O.supportedLevels,nextOffset:O.nextOffset??void 0,mine:O.mine?zO.from(this.client,this,O.mine):void 0,requests:O.requests.map((j)=>zO.from(this.client,this,j))}}async submit($,O){return zO.from(this.client,this,G(await this.client.storage.accessRequestsSubmit({path:this.path,body:{requestedLevel:$,revision:O}})))}async cancel($){G(await this.client.storage.accessRequestsCancel({path:this.path,query:{revision:$}}))}async direct(){return G(await this.client.storage.accessRequestsDirect({path:this.path})).map(($)=>({user:P.byId(this.client,$.userId),accessLevel:$.accessLevel}))}async grant($,O){G(await this.client.storage.accessRequestsSetDirect({path:{...this.path,user_id:$.id},body:{accessLevel:O}}))}async remove($){G(await this.client.storage.accessRequestsRemoveDirect({path:{...this.path,user_id:$.id}}))}async resolve($,O){let j=$.access.toJSON();if(j.entity_type!==this.entityType||j.entity_id!==this.entity.id)throw Error("Access request belongs to another entity");return G(await this.client.storage.accessRequestsResolve({path:{...this.path,request_id:$.id},body:O}))}toJSON(){return this.path}}class zO extends M{access;resolvedRecord;constructor($,O,j){super($,j.id,j);this.access=O}static from($,O,j){return new zO($,O,j)}async fetch(){if(this.resolvedRecord)return this.resolvedRecord;let $;do{let O=await this.access.state($),j=[O.mine,...O.requests].find((x)=>x?.id===this.id);if(j)return j.detail.get();$=O.nextOffset}while($!==void 0);throw new E(`Access request ${this.id} is unavailable`)}revision=this.field("revision");status=this.field("status");requestedLevel=this.field("requestedLevel");requester=this.mappedField("requesterId",($)=>P.byId(this.client,$));async resolve($){this.resolvedRecord=await this.access.resolve(this,{revision:await this.revision(),grantedLevel:$}),this.detail.clear()}}function n$($){return{...$,channelSharePermissions:$.channelSharePermissions===void 0||$.channelSharePermissions===null?$.channelSharePermissions:$.channelSharePermissions.map((O)=>({channelId:O.channel.id,accessLevel:O.accessLevel,operation:O.operation}))}}class N$ extends M{parent;constructor($,O,j,x){super($,j,x);this.parent=O}static byId($,O,j){return new N$($,O,j)}static fromRecord($,O,j){return new N$($,O,j.id,j)}static async create($,O,j){return N$.createForSession($,O.session(),j,O)}static async createForSession($,O,j,x){let S=G(await $.agentHarness.createSessionReview({path:{session_id:O.id},body:{...j,executionId:x?.id??null}}));return N$.fromRecord($,O,S)}static async list($,O){return G(await $.agentHarness.listExecutionReviews({path:{session_id:O.session().id,execution_id:O.id}})).map((x)=>N$.fromRecord($,O.session(),x))}async fetch(){return G(await this.client.agentHarness.getSessionReview({path:{session_id:this.parent.id,review_id:this.id}}))}title=this.field("title");kind=this.field("kind");session(){return this.parent}async reference(){return{kind:await this.kind(),id:this.id}}async items(){return(await this.detail.get()).items.map(($)=>new vx($,{edit:(O)=>this.decide({action:"edit",item:$.id,draft:O}),submit:()=>this.decide({action:"submit",items:[$.id]}),dismiss:()=>this.decide({action:"dismiss",items:[$.id]})}))}async refresh(){return this.detail.clear(),await this.detail.get(),this}async submitAll(){await this.decide({action:"submit",items:[]})}async dismissAll(){await this.decide({action:"dismiss",items:[]})}async decide($){let O=(await this.detail.get()).revision;await this.mutate((j)=>j.agentHarness.decideSessionReview({path:{session_id:this.parent.id,review_id:this.id},body:{...$,revision:O}}))}}class vx{snapshot;actions;constructor($,O){this.snapshot=$;this.actions=O}get id(){return this.snapshot.id}get status(){return this.snapshot.status}get draft(){return this.snapshot.draft}get result(){return this.snapshot.result}async edit($){await this.actions.edit($)}async submit(){await this.actions.submit()}async dismiss(){await this.actions.dismiss()}}class uO extends M{parent;constructor($,O,j){super($,O);this.parent=j}static byId($,O,j){return new uO($,j,O)}async fetch(){return G(await this.client.agentHarness.getCodeExecution({path:{session_id:this.parent.id,execution_id:this.id}}))}reviews={create:($)=>N$.create(this.client,this,$),list:()=>N$.list(this.client,this)};session(){return this.parent}status=this.field("status");source=this.field("source");calls=this.field("calls");error=this.field("error");async result(){return(await this.detail.get()).result}async refresh(){return this.detail.clear(),await this.detail.get(),this}}class Lj{client;sessionId;dto;constructor($,O,j){this.client=$;this.sessionId=O;this.dto=j}static from($,O,j){return new Lj($,O,j)}get actionId(){return this.dto.actionId}get kind(){return this.dto.kind}get prompt(){return this.dto.prompt??void 0}get attachments(){return this.dto.attachments??[]}get actorUserId(){return this.dto.actorUserId??void 0}get createdAt(){return this.dto.createdAt}async edit($){return G(await this.client.agentHarness.editQueuedAction({path:{session_id:this.sessionId,action_id:this.actionId},body:{prompt:$}})),this.dto={...this.dto,prompt:$},this}async steer(){G(await this.client.agentHarness.steerQueuedAction({path:{session_id:this.sessionId,action_id:this.actionId}}))}async remove(){G(await this.client.agentHarness.removeQueuedAction({path:{session_id:this.sessionId,action_id:this.actionId}}))}}class dO{client;session;id;constructor($,O,j){this.client=$;this.session=O;this.id=j}static byId($,O,j){return new dO($,O,j)}async answer($){return G(await this.client.agentHarness.answerAgentSessionToolApproval({path:{session_id:this.session.id,approval_id:this.id},body:{answer:$}}))}toJSON(){return{id:this.id,sessionId:this.session.id}}}class i extends M{access=new o(this.client,"agent_session",this);toolApprovalById($){return dO.byId(this.client,this,$)}async sharePermissions(){return G(await this.client.agentHarness.getAgentSessionPermissions({path:{session_id:this.id}}))}async updateSharePermissions($){return this.mutate((O)=>O.agentHarness.updateAgentSessionPermissions({path:{session_id:this.id},body:n$($)}))}static byId($,O){return new i($,O)}static async createManaged($,O){let{session:j}=G(await $.agentHarness.createAgentSession({body:{prompt:O?.prompt,instructions:O?.instructions,model:O?.model,repoUrl:O?.repoUrl,repoBranch:O?.repoBranch}}));return new i($,j.id,j)}static async repositories($){let{repositories:O}=G(await $.agentHarness.listAgentRepositories());return O.map((j)=>({url:j.url,defaultBranch:j.defaultBranch??void 0}))}static async repositoryBranches($,O){let{branches:j}=G(await $.agentHarness.listAgentRepositoryBranches({query:{repoUrl:O}}));return j}async fetch(){return G(await this.client.agentHarness.getAgentSession({path:{session_id:this.id}}))}name=this.field("name");isArchived=this.field("isArchived");model=this.field("model");harness=this.field("harness");repoUrl=this.field("repoUrl");workspace=this.field("workspace");instructions=this.field("instructions");status=this.field("status");sandboxSize=this.field("sandboxSize");createdAt=this.field("createdAt");modifiedAt=this.field("modifiedAt");static async forPullRequest($,O){let{sessionIds:j}=G(await $.agentHarness.agentSessionsForPullRequest({body:{url:O}}));return j.map((x)=>i.byId($,x))}async pullRequests(){let{pullRequests:$}=G(await this.client.agentHarness.listAgentSessionPullRequests({path:{session_id:this.id}}));return $.map((O)=>({githubKey:O.githubKey,url:O.url,source:O.source,linkedBy:O.linkedBy?P.byId(this.client,O.linkedBy):void 0,createdAt:O.createdAt}))}async linkPullRequest($){G(await this.client.agentHarness.linkAgentSessionPullRequest({path:{session_id:this.id},body:{url:$}}))}async unlinkPullRequest($){G(await this.client.agentHarness.unlinkAgentSessionPullRequest({path:{session_id:this.id},query:{url:$}}))}async rename($){await this.mutate((O)=>O.agentHarness.renameAgentSession({path:{session_id:this.id},body:{name:$}}))}async archive(){await this.setArchived(!0)}async unarchive(){await this.setArchived(!1)}async setArchived($){await this.mutate((O)=>O.agentHarness.setAgentSessionArchived({path:{session_id:this.id},body:{isArchived:$}}))}async setSandboxSize($){let{size:O}=await this.mutate((j)=>j.agentHarness.putAgentSessionSandboxSize({path:{session_id:this.id},body:{size:$}}));return O}static async defaultSandboxSize($){return G(await $.agentHarness.getAgentSandboxSize()).size}static async setDefaultSandboxSize($,O){return G(await $.agentHarness.putAgentSandboxSize({body:{size:O}})).size}static async codingPreferences($){return G(await $.agentHarness.getAgentCodingPreferences())}static async setCodingPreferences($,O){return G(await $.agentHarness.putAgentCodingPreferences({body:O}))}async control($){return this.mutate((O)=>O.agentHarness.controlAgentSession({path:{session_id:this.id},body:$}))}prompt($,O){return this.control({type:"prompt",prompt:$,...O&&O.length>0?{attachments:O}:{}})}async queue(){let{entries:$}=G(await this.client.agentHarness.getAgentSessionQueue({path:{session_id:this.id}}));return $.map((O)=>Lj.from(this.client,this.id,O))}async changes(){return G(await this.client.agentHarness.getAgentSessionChanges({path:{session_id:this.id}}))}async changesPatch(){return G(await this.client.agentHarness.getAgentSessionChangesPatch({path:{session_id:this.id}})).patch}async refreshChanges(){return this.mutate(($)=>$.agentHarness.refreshAgentSessionChanges({path:{session_id:this.id}}))}async foldedLog(){let{bot:$,messages:O}=G(await this.client.agentHarness.getAgentSessionFoldedLog({path:{session_id:this.id}}));return{bot:$,messages:O}}async log(){return G(await this.client.agentHarness.getAgentSessionLog({path:{session_id:this.id}}))}reviews={create:($)=>N$.createForSession(this.client,this,$),byId:($)=>N$.byId(this.client,this,$)};codeExecutions={byId:($)=>uO.byId(this.client,this,$)};async delete(){await this.mutate(($)=>$.agentHarness.deleteAgentSession({path:{session_id:this.id}}))}}class hx{client;constructor($){this.client=$}byId($){return i.byId(this.client,$)}createManaged($){return i.createManaged(this.client,$)}repositories(){return i.repositories(this.client)}repositoryBranches($){return i.repositoryBranches(this.client,$)}forPullRequest($){return i.forPullRequest(this.client,$)}defaultSandboxSize(){return i.defaultSandboxSize(this.client)}setDefaultSandboxSize($){return i.setDefaultSandboxSize(this.client,$)}codingPreferences(){return i.codingPreferences(this.client)}setCodingPreferences($){return i.setCodingPreferences(this.client,$)}}class mx{client;constructor($){this.client=$}byId($){return I$.byId(this.client,$)}async list(){return G(await this.client.storage.listAgents()).map(($)=>I$.from(this.client,$))}async create($){return I$.from(this.client,G(await this.client.storage.createAgent({body:gx($)})))}}class cx{client;async usage($={}){let{users:O,...j}=$;return G(await this.client.cognition.getUsageHandler({body:{...j,include_users:O?.map((x)=>x.id)}}))}constructor($){this.client=$}async structuredCompletion($){return G(await this.client.cognition.structuredCompletion({body:$}))}async decide($){return G(await this.client.cognition.decisions({body:$}))}}class s$ extends M{static byId($,O){return new s$($,O)}static from($,O){return new s$($,O.id,{id:O.id,name:O.name,createdAt:O.createdAt})}async fetch(){let $=G(await this.client.storage.listUserApiKeys()).keys.find((O)=>O.id===this.id);if(!$)throw new E(`API key ${this.id} is unavailable`);return $}name=this.field("name");createdAt=this.field("createdAt");async delete(){await this.mutate(($)=>$.storage.deleteUserApiKey({path:{id:this.id}}))}}class ux{client;constructor($){this.client=$}byId($){return s$.byId(this.client,$)}async list(){return G(await this.client.storage.listUserApiKeys()).keys.map(($)=>s$.from(this.client,$))}async create($){let O=G(await this.client.storage.createUserApiKey({body:{name:$}}));return{key:s$.from(this.client,O),secret:O.key}}async tokenForEmail($){return G(await this.client.auth.macroApiToken({query:{email:$}}))}}class lO{client;constructor($){this.client=$}async me(){if(this.client.authConfig.type!=="bot")throw new f("bots.me() requires bot auth — a user API key has no bot identity");return G(await this.client.storage.getSelfBot())}async channels(){if(this.client.authConfig.type!=="bot")throw new f("bots.channels() requires bot auth — a user API key has no bot identity");let $=await this.me();return G(await this.client.storage.listBotChannels({path:{bot_id:$.id}}))}async myPrincipalId(){if(this.client.authConfig.type!=="bot")throw new f("bots.myPrincipalId() requires bot auth — a user API key has no bot identity");return this.client.myPrincipalId()}}class S$ extends M{async fetch(){let O=G(await this.client.auth.getUserTeams()).find((j)=>j.id===this.id);if(!O)throw new E(`team ${this.id} not found`);return O}static byId($,O){return new S$($,O)}static async current($){let O=G(await $.auth.getTeam());if(!O)throw new E("caller has no current team");return new S$($,O.team.id,O.team)}static async list($){return G(await $.auth.getUserTeams()).map((j)=>new S$($,j.id,j))}name=this.field("name");slug=this.field("slug");defaultLinkShare=this.field("default_link_share");owner=this.mappedField("owner_id",($)=>P.byId(this.client,$));async members(){let $=G(await this.client.auth.getTeam());if(!$||$.team.id!==this.id)throw new f("members are only available for the current team (see Team.current)");return $.members.map((O)=>({user:P.byId(this.client,O.user_id),role:O.role}))}}class T$ extends M{async fetch(){let{links:$}=G(await this.client.email.listLinks()),O=$.find((j)=>j.id===this.id);if(!O)throw new E(`email link ${this.id} not found`);return O}static byId($,O){return new T$($,O)}static async list($){let{links:O}=G(await $.email.listLinks());return O.map((j)=>new T$($,j.id,j))}emailAddress=this.field("email_address");provider=this.field("provider");isPrimary=this.field("is_primary");isSyncActive=this.field("is_sync_active");syncStatus=this.field("sync_status");needsReauth=this.field("needs_reauth");photoUrl=this.field("photo_url");async settingsOperations(){return G(await this.client.email.mailboxSettingsOperations({headers:{"X-Email-Link-Id":this.id}}))}}class m$ extends M{async setContributesToAvailability($){await this.mutate((O)=>O.calendar.setAvailabilityCalendar({path:{calendar_id:this.id},body:{contributesToAvailability:$}}))}async fetch(){let{calendars:$}=G(await this.client.calendar.listCalendars()),O=$.find((j)=>j.id===this.id);if(!O)throw new E(`calendar ${this.id} not found`);return O}static byId($,O){return new m$($,O)}static async list($){let{calendars:O}=G(await $.calendar.listCalendars());return O.map((j)=>new m$($,j.id,j))}name=this.field("name");emailAddress=this.field("emailAddress");isPrimary=this.field("isPrimary");isWritable=this.field("isWritable");isSubscription=this.field("isSubscription");color=this.field("color");syncError=this.field("syncError");emailLink=this.mappedField("emailLinkId",($)=>T$.byId(this.client,$))}class rO{client;id;constructor($,O){this.client=$;this.id=O}async status(){return G(await this.client.calendar.calendarReplacementStatus({path:{operation_id:this.id}}))}async confirm(){return G(await this.client.calendar.confirmCalendarReplacement({path:{operation_id:this.id}}))}async discard(){G(await this.client.calendar.discardCalendarReplacement({path:{operation_id:this.id}}))}}class $$ extends M{async notes($){return this.requireOwnOccurrence($),G(await this.client.calendar.eventNotes({path:{event_id:this.id},query:{occurrenceKey:$.occurrenceKey}})).map((j)=>q$.fromRecord(this.client,this,$,j))}async linkNote($,O,j={}){return this.requireOwnOccurrence(O),G(await this.client.calendar.linkEventNote({path:{event_id:this.id,document_id:$.id},query:{occurrenceKey:O.occurrenceKey},body:{teamId:j.team?.id??null}})),q$.forDocument(this.client,this,O,$)}requireOwnOccurrence($){if($.eventId!==this.id)throw new f("The occurrence belongs to a different event")}async fetch(){throw new f(`calendar event ${this.id} has no fetch-by-id endpoint; read the record returned by calendar.createEvent / event.update / event.rsvp instead`)}static byId($,O){return new $$($,O)}static fromRecord($,O){return new $$($,O.id,O)}title=this.field("title");description=this.field("description");location=this.field("location");time=this.field("time");status=this.field("status");attendees=this.field("attendees");transparency=this.field("transparency");isReadOnly=this.field("isReadOnly");recurrenceLines=this.field("recurrenceLines");guestPolicy=this.field("guestPolicy");viewerCan=this.field("viewerCan");calendar=this.mappedField("calendarId",($)=>$?m$.byId(this.client,$):void 0);record(){return this.detail.get()}async update($){let O=await this.mutate((j)=>j.calendar.updateCalendarEvent({path:{event_id:this.id},body:h6($)}));return $$.fromRecord(this.client,O)}async delete($={}){await this.mutate((O)=>O.calendar.deleteCalendarEvent({path:{event_id:this.id},query:{calendarId:$.calendar?.id,scope:$.scope,recurrenceId:$.recurrenceId,inbox:$.inbox}}))}async prepareReplacement($){let O=G(await this.client.calendar.prepareCalendarReplacement({path:{event_id:this.id},body:{removeConference:$.removeConference,recurrenceId:$.recurrenceId,calendarId:$.calendar?.id}}));return new rO(this.client,O.operationId)}async providerUrl($={}){return G(await this.client.calendar.calendarEventProviderUrl({path:{event_id:this.id},query:{recurrenceId:$.recurrenceId,calendarId:$.calendar?.id}})).url??void 0}async inviteGuests($){let O=await this.mutate((j)=>j.calendar.inviteCalendarEventGuests({path:{event_id:this.id},body:{guests:$.guests,scope:$.scope,recurrenceId:$.recurrenceId,calendarId:$.calendar?.id,inbox:$.inbox}}));return $$.fromRecord(this.client,O)}async addToCalendar($={}){let O=await this.mutate((j)=>j.calendar.copyCalendarEvent({path:{event_id:this.id},body:{calendarId:$.calendar?.id}}));return $$.fromRecord(this.client,O)}async ics(){return G(await this.client.storage.eventIcs({path:{event_id:this.id}}))}async rsvp($){let O=await this.mutate((j)=>j.calendar.rsvpCalendarEvent({path:{event_id:this.id},body:{response:$.response,scope:$.scope,recurrenceId:$.recurrenceId,calendarId:$.calendar?.id}}));return $$.fromRecord(this.client,O)}}function h6($){return{title:$.title,description:$.description,location:$.location,time:$.time,attendees:$.attendees,recurrenceLines:$.recurrenceLines,visibility:$.visibility,transparency:$.transparency,reminders:$.reminders,conference:$.conference,outOfOffice:$.outOfOffice,guestPolicy:$.guestPolicy,scope:$.scope,recurrenceId:$.recurrenceId,calendarId:$.calendar?.id,inbox:$.inbox}}class q$ extends M{event;occurrence;fromDocument;constructor($,O,j,x,S,K=!1){super($,x.id,S);this.event=O;this.occurrence=j;this.fromDocument=K;if(j.eventId!==O.id)throw new f("Choose an occurrence belonging to this event")}static fromDocumentLink($,O,j){return new q$($,$$.byId($,j.eventId),j,O,{documentId:O.id,name:j.documentName,team:j.team,canManage:!0},!0)}static forDocument($,O,j,x){return new q$($,O,j,x)}static fromRecord($,O,j,x){return new q$($,O,j,w.byId($,x.documentId),x)}async fetch(){if(this.fromDocument){let x=G(await this.client.calendar.documentNoteLinks({path:{document_id:this.id}})).find((S)=>S.eventId===this.event.id&&S.occurrenceKey===this.occurrence.occurrenceKey);if(!x)throw new E("This meeting link has been removed");return{documentId:this.id,name:x.documentName,team:x.team,canManage:!0}}let O=G(await this.client.calendar.eventNotes({path:{event_id:this.event.id},query:{occurrenceKey:this.occurrence.occurrenceKey}})).find((j)=>j.documentId===this.id);if(!O)throw new E("This document is no longer linked to the meeting");return O}document=this.mappedField("documentId",($)=>w.byId(this.client,$));name=this.field("name");team=this.mappedField("team",($)=>$?S$.byId(this.client,$.id):void 0);canManage=this.field("canManage");async setTeam($){await this.mutate((O)=>O.calendar.linkEventNote({path:{event_id:this.event.id,document_id:this.id},query:{occurrenceKey:this.occurrence.occurrenceKey},body:{teamId:$?.id??null}}))}async unlink(){await this.mutate(($)=>$.calendar.unlinkEventNote({path:{event_id:this.event.id,document_id:this.id},query:{occurrenceKey:this.occurrence.occurrenceKey}}))}}class n extends X${async sharePermissions(){return G(await this.client.storage.getProjectPermissionsV2({path:{id:this.id}}))}async accessLevel(){return G(await this.client.storage.getProjectUserAccessLevel({path:{id:this.id}}))}access=new o(this.client,"project",this);entityType="project";propertyEntityType="PROJECT";async fetch(){let{data:$}=G(await this.client.storage.getProjectHandler({path:{id:this.id}}));return $.projectMetadata}static byId($,O){return new n($,O)}static async create($,O){let{data:j}=G(await $.storage.createProjectHandler({body:{name:O.name,projectParentId:O.parent?.id??null}}));return new n($,j.id,j)}name=this.field("name");parent=this.mappedField("parentId",($)=>$?n.byId(this.client,$):void 0);createdAt=this.field("createdAt");async items(){let{data:$}=G(await this.client.storage.getProjectContentHandler({path:{id:this.id}}));return $.map((O)=>O.item)}async rename($){await this.mutate((O)=>O.storage.editProjectV2({path:{id:this.id},body:{name:$}}))}async delete($){await this.mutate((O)=>$?.permanent?O.storage.permanentlyDeleteProject({path:{id:this.id}}):O.storage.deleteProjectHandler({path:{id:this.id}}))}async restore(){await this.mutate(($)=>$.storage.revertDeleteProject({path:{id:this.id}}))}static search=x$({filters:{project_filters:{}},type:"project",make:($,O)=>new n($,O.id)})}class JO extends M{document;toJSON(){return{...super.toJSON(),documentId:this.document.id}}constructor($,O,j,x){super($,j,x);this.document=O}static byId($,O,j){return new JO($,O,j)}static from($,O,j){return new JO($,O,j.uuid,j)}async fetch(){let $=G(await this.client.storage.getDocumentAnchors({path:{document_id:this.document.id}})).data.find((O)=>O.uuid===this.id);if(!$)throw new E(`Annotation ${this.id} is unavailable`);return $}kind=this.field("anchorType");page=this.field("page");async edit($){if(await this.kind()!=="placeable")throw Error("Only placeable comment annotations can be edited");await this.mutate((O)=>O.storage.editAnchor({body:{...$,uuid:this.id,anchorType:"free-comment",fileType:"pdf"}}))}async delete(){if(await this.kind()!=="highlight")throw Error("Only standalone highlights can be deleted with this endpoint");await this.mutate(($)=>$.storage.deleteAnchor({body:{uuid:this.id,anchorType:"highlight",fileType:"pdf"}}))}}class c$ extends M{documentId;constructor($,O,j,x){super($,j,x);this.documentId=O}get path(){return{parent_type:"document",parent_id:this.documentId,id:this.id}}async fetch(){return G(await this.client.storage.entityMessageGetMessage({path:this.path}))}static byId($,O,j){return new c$($,O,j)}static from($,O,j){return new c$($,O,j.id,j)}text=this.field("content");createdAt=this.field("created_at");updatedAt=this.field("updated_at");async threadId(){return(await this.detail.get()).thread_id??this.id}document(){return w.byId(this.client,this.documentId)}async author(){return P.byId(this.client,(await this.detail.get()).sender_id)}async reply($){return this.document().comment($,{threadId:await this.threadId()})}async edit($){let{content:O,mentions:j}=O$($);return await this.mutate((x)=>x.storage.entityMessageEdit({path:this.path,body:{content:O,mentions:j}})),this}async delete(){await this.mutate(($)=>$.storage.entityMessageDeleteMessage({path:this.path}))}}class pO{client;document;id;detail;constructor($,O,j){this.client=$;this.document=O;this.id=j;this.detail=new W$(async()=>G(await this.client.storage.getDocumentVersion({path:{document_id:O.id,document_version_id:j}})).data)}static byId($,O,j){return new pO($,O,j)}read(){return this.detail.get()}toJSON(){return{id:this.id,documentId:this.document.id}}}var m6=8;class w extends X${async meetingNotes(){return G(await this.client.calendar.documentNoteLinks({path:{document_id:this.id}})).map((O)=>({note:q$.fromDocumentLink(this.client,this,O),eventTitle:O.eventTitle}))}async anchors(){return G(await this.client.storage.getDocumentAnchors({path:{document_id:this.id}})).data.map(($)=>JO.from(this.client,this,$))}async createHighlight($){return JO.from(this.client,this,G(await this.client.storage.createAnchor({path:{document_id:this.id},body:{...$,fileType:"pdf",anchorType:"highlight"}})))}version($){return pO.byId(this.client,this,$)}async sharePermissions(){return G(await this.client.storage.getDocumentPermissionsV2({path:{document_id:this.id}})).documentPermissions}async permissionsToken(){return G(await this.client.storage.getDocumentPermissionsToken({path:{document_id:this.id}})).token}async prepareSave($){return G(await this.client.storage.presaveDocumentHandler({path:{document_id:this.id},body:{newBom:$}})).data}async save($){return(await this.mutate((O)=>O.storage.saveDocumentHandler({path:{document_id:this.id},body:$}))).data}async simpleSave(){return(await this.mutate(($)=>$.storage.simpleSave({path:{document_id:this.id}}))).data}messageThreadById($){return L$.byId(this.client,{type:"document",entity:this},$)}timelinePage($){return w$(this.client,{type:"document",entity:this},(O)=>c$.from(this.client,this.id,O),$)}access=new o(this.client,"document",this);async fetch(){let{data:$}=G(await this.client.storage.getDocument({path:{document_id:this.id}}));return $.documentMetadata}static byId($,O){return new w($,O)}static async create($,O){let{documentId:j}=G(await $.storage.createMarkdownHandler({body:{documentName:O.name,markdown:O.markdown??null,projectId:O.project?.id??null}}));return new w($,j)}static async createSnippet($,O){let{documentId:j}=G(await $.storage.createSnippetHandler({body:{snippetName:O.name,markdown:O.markdown??null,projectId:O.project?.id??null}}));return new w($,j)}static async createSkill($,O){let{documentId:j}=G(await $.storage.createSkillHandler({body:{skillName:O.name,markdown:O.markdown??null,projectId:O.project?.id??null}}));return new w($,j)}static recent($,O){let j=O?.pageSize??50;return u(async(x)=>{let S=G(await $.storage.getUserDocumentsHandler({query:{limit:j,offset:x?Number(x):0,...O?.fileType!==void 0?{file_type:O.fileType}:{}}})),K=S.data?.next_offset;return{items:(S.data?.documents??[]).map((Z)=>new w($,Z.documentId)),nextCursor:K!=null?String(K):null}})}static async recentlyDeleted($){let{data:O}=G(await $.storage.recentlyDeleted({}));return O.items.filter((j)=>j.type==="document").map((j)=>new w($,j.id))}entityType="document";propertyEntityType="DOCUMENT";name=this.field("documentName");fileType=this.field("fileType");project=this.mappedField("projectId",($)=>$?n.byId(this.client,$):void 0);owner=this.mappedField("owner",($)=>P.byId(this.client,$));branchedFrom=this.mappedField("branchedFromId",($)=>$?w.byId(this.client,$):void 0);createdAt=this.field("createdAt");updatedAt=this.field("updatedAt");deletedAt=this.field("deletedAt");async content(){let{text:$}=G(await this.client.storage.getDocumentTextContent({path:{document_id:this.id}}));if($!==null)return $;let{presigned_url:O}=G(await this.client.storage.exportDocument({path:{document_id:this.id}})),j=await fetch(O);if(!j.ok)throw new GO(j.status,{message:"failed to download document content"});return j.text()}async rename($){await this.mutate((O)=>O.storage.editDocument({path:{document_id:this.id},body:{documentName:$}}))}async move($){await this.mutate((O)=>O.storage.editDocument({path:{document_id:this.id},body:{projectId:$?.id??""}}))}async delete($){await this.mutate((O)=>$?.permanent?O.storage.permanentlyDeleteDocument({path:{document_id:this.id}}):O.storage.deleteDocument({path:{document_id:this.id}}))}async restore(){await this.mutate(($)=>$.storage.revertDeleteDocument({path:{document_id:this.id}}))}async copy($){let{data:O}=G(await this.client.storage.copyDocument({path:{document_id:this.id},query:$.versionId!==void 0?{version_id:$.versionId}:void 0,body:{documentName:$.name}}));return new w(this.client,O.documentMetadata.documentId)}async comments(){let $={parent_type:"document",parent_id:this.id},O=u(async(x)=>{let S=G(await this.client.storage.messageTimeline({path:$,query:{selection:JSON.stringify({limit:100,cursor:x})}}));return{items:S.items,nextCursor:S.next_cursor}}),j=[];for await(let x of O)j.push(x);return cO(j,m6,async(x)=>{let{state:S,root:K,replies:Z}=G(await this.client.storage.entityMessageGetThread({path:{...$,id:x.id}}));return{thread:S,comments:[K,...Z].map((X)=>c$.from(this.client,this.id,X))}})}async comment($,O){let j=await this.mutate((x)=>x.storage.entityMessageCreate({path:{parent_type:"document",parent_id:this.id},body:{...O$($),thread_id:O?.threadId??null}}));return c$.from(this.client,this.id,j)}async shortId(){return G(await this.client.storage.getDocumentShortId({path:{document_id:this.id}}))}async teamShare(){return G(await this.client.storage.getDocumentTeamShare({path:{document_id:this.id}}))}async setTeamShare($){await this.mutate((O)=>O.storage.setDocumentTeamShare({path:{document_id:this.id},body:{shareWithTeam:$}}))}async githubPullRequests(){let{pullRequests:$}=G(await this.client.storage.getDocumentGithubPullRequests({path:{document_id:this.id}}));return $}async branchName(){let{branchName:$}=G(await this.client.storage.getDocumentBranchName({path:{document_id:this.id}}));return $}webUrl(){return`${this.client.webAppUrl}/app/md/${this.id}`}static search=x$({filters:{document_filters:{}},type:"document",make:($,O)=>new w($,O.document_id)});toMention(){let $=this.detail.peek();return{tag:i$("m-document-mention",{documentId:this.id,documentName:$?.documentName??"",blockName:$?.fileType??"md"}),mention:{entity_type:"document",entity_id:this.id}}}}class dx{client;async noteDocuments($=""){let O=G(await this.client.calendar.noteDocuments({query:{query:$}}));return{documents:O.documents.map((j)=>({document:w.byId(this.client,j.documentId),name:j.name})),teams:O.teams.map((j)=>({team:S$.byId(this.client,j.id),name:j.name}))}}async availabilityCalendars(){return G(await this.client.calendar.getAvailabilityCalendars()).calendars.map(($)=>({calendar:m$.byId(this.client,$.calendarId),name:$.name,isPrimary:$.isPrimary,contributesToAvailability:$.contributesToAvailability}))}async teamSharing(){return G(await this.client.calendar.getTeamSharing()).sharing}async setTeamSharing($){return G(await this.client.calendar.setTeamSharing({body:{sharing:$}})).sharing}occurrences($){let O;return u(async(j)=>{let x=G(await this.client.storage.listOccurrences({query:{...$,cursor:j}}));O??=u6(this.client);let S=await O;return{items:x.items.map((K)=>({event:$$.fromRecord(this.client,K.event),occurrence:K.occurrence,inbox:K.event.calendarId?S.get(K.event.calendarId):void 0,role:d6(K.event)})),nextCursor:x.nextCursor}})}async teamPage($){let O=G(await this.client.storage.listTeamCalendar({query:$}));return{items:O.items.map(({ownerId:j,...x})=>({...x,owner:P.byId(this.client,j)})),members:O.members.map((j)=>({user:P.byId(this.client,j.userId),sharing:j.sharing,coverage:j.coverage})),nextCursor:O.nextCursor??void 0}}async teamOutOfOffice($){let O=G(await this.client.storage.listTeamOutOfOffice({query:$}));return{items:O.items.map((j)=>({event:$$.byId(this.client,j.eventId),owner:P.byId(this.client,j.ownerId),occurrenceKey:j.occurrenceKey,time:j.time,title:j.title??void 0})),hasMore:O.hasMore}}constructor($){this.client=$}calendars(){return m$.list(this.client)}calendar($){return m$.byId(this.client,$)}event($){return $$.byId(this.client,$)}replacement($){return new rO(this.client,$)}async createEvent($){let O=G(await this.client.calendar.createCalendarEvent({body:c6($)}));return $$.fromRecord(this.client,O)}}function c6($){return{title:$.title,time:$.time,description:$.description,location:$.location,attendees:$.attendees,recurrenceLines:$.recurrenceLines,visibility:$.visibility,transparency:$.transparency,reminders:$.reminders,conference:$.conference,macroCall:$.macroCall,outOfOffice:$.outOfOffice,guestPolicy:$.guestPolicy,calendarId:$.calendar?.id,emailLinkId:$.emailLink?.id}}async function u6($){let{calendars:O}=G(await $.calendar.listCalendars());return new Map(O.map((j)=>[j.id,j.emailAddress]))}function d6($){let O=$.attendees.find((j)=>j.isSelf);if($.viewerCan.organize||O?.isOrganizer)return"organizer";return O?"guest":void 0}class H$ extends M{lookupToken;constructor($,O,j){super($,O,j);this.lookupToken=j?.shareToken}static byId($,O){return new H$($,O)}static from($,O){return new H$($,O.id,O)}async fetch(){if(this.lookupToken)return G(await this.client.storage.meetingLookup({path:{token:this.lookupToken}}));let $=G(await this.client.storage.meetingList()).meetings.find((O)=>O.id===this.id);if(!$)throw new E(`Meeting ${this.id} is unavailable`);return this.lookupToken=$.shareToken,$}title=this.field("title");scheduledStart=this.field("scheduledStart");scheduledEnd=this.field("scheduledEnd");channel=this.mappedField("channelId",($)=>$?b.byId(this.client,$):void 0);call=this.mappedField("callId",($)=>$?Q$.byId(this.client,$):void 0);async update($){await this.mutate((O)=>O.storage.meetingUpdate({path:{meeting_id:this.id},body:$}))}async cancel(){await this.mutate(($)=>$.storage.meetingCancel({path:{meeting_id:this.id}}))}async invitePermissions(){return G(await this.client.storage.meetingInvitePermissions({path:{token:(await this.detail.get()).shareToken}}))}async invite($){G(await this.client.storage.meetingInvite({path:{token:(await this.detail.get()).shareToken},body:{email:$}}))}async inviteUsers($){G(await this.client.storage.meetingInviteUsers({path:{token:(await this.detail.get()).shareToken},body:{userIds:$.map((O)=>O.id)}}))}async webUrl(){return`${this.client.webAppUrl}/app/meet/join/${encodeURIComponent((await this.detail.get()).shareToken)}`}}class u$ extends M{callId;constructor($,O,j,x){super($,j,x);this.callId=O}get path(){return{parent_type:"call",parent_id:this.callId,id:this.id}}async fetch(){return G(await this.client.storage.entityMessageGetMessage({path:this.path}))}static byId($,O,j){return new u$($,O,j)}static from($,O,j){return new u$($,O,j.id,j)}content=this.field("content");createdAt=this.field("created_at");updatedAt=this.field("updated_at");call(){return Q$.byId(this.client,this.callId)}async author(){return P.byId(this.client,(await this.detail.get()).sender_id)}async reply($){let O=G(await this.client.storage.entityMessageCreate({path:{parent_type:"call",parent_id:this.callId},body:{...O$($),thread_id:this.callId}}));return u$.from(this.client,this.callId,O)}async edit($){let{content:O,mentions:j}=O$($);return await this.mutate((x)=>x.storage.entityMessageEdit({path:this.path,body:{content:O,mentions:j}})),this}async delete(){await this.mutate(($)=>$.storage.entityMessageDeleteMessage({path:this.path}))}}class Q$ extends z${messageThreadById($){return L$.byId(this.client,{type:"call",entity:this},$)}timelinePage($){return w$(this.client,{type:"call",entity:this},(O)=>u$.from(this.client,this.id,O),$)}access=new o(this.client,"call",this);async meeting(){return H$.from(this.client,G(await this.client.storage.meetingShare({path:{call_id:this.id}})))}async editTranscript($){await this.mutate((O)=>O.storage.editCallTranscript({path:{call_id:this.id},body:{assignments:$}}))}async toggleTeamShare(){return this.mutate(($)=>$.storage.toggleShareWithTeam({path:{call_id:this.id}}))}entityType="call";async fetch(){return G(await this.client.storage.getCallRecord({path:{call_id:this.id}}))}static byId($,O){return new Q$($,O)}message($){return u$.byId(this.client,this.id,$)}name=this.field("customName");channel=this.mappedField("channelId",($)=>$?b.byId(this.client,$):void 0);channelName=this.field("channelName");creator=this.mappedField("createdBy",($)=>P.byId(this.client,$));isActive=this.field("isActive");status=this.field("status");roomName=this.field("roomName");startedAt=this.field("startedAt");endedAt=this.field("endedAt");durationMs=this.field("durationMs");summary=this.field("summary");transcript=this.field("transcript");participants=this.field("participants");guests=this.field("guests");teamShareAccessLevel=this.field("teamShareAccessLevel");shareWithTeam=this.field("shareWithTeam");recordingUrl=this.field("recordingUrl");recordingPreviewUrl=this.field("recordingPreviewUrl");recordingStartedAt=this.field("recordingStartedAt");async rename($){await this.mutate((O)=>O.storage.editCallRecord({path:{call_id:this.id},body:{customName:$??""}}))}async setTeamShare($){await this.mutate((O)=>O.storage.editCallRecord({path:{call_id:this.id},body:{sharePermission:{teamShareAccessLevel:$?"view":null}}}))}async delete(){await this.mutate(($)=>$.storage.deleteCallRecord({path:{call_id:this.id}}))}static search=x$({filters:{call_filters:{}},type:"call",make:($,O)=>new Q$($,O.call_id)})}class lx{client;constructor($){this.client=$}byId($){return Q$.byId(this.client,$)}search($,O){return Q$.search(this.client,$,O)}}class rx{client;constructor($){this.client=$}async list(){let{labels:$}=G(await this.client.storage.listChannelLabels());return $}async create($,O=[]){return G(await this.client.storage.createChannelLabel({body:{name:$,channelIds:O.map((j)=>j.id)}}))}async createSmartTag($,O){return G(await this.client.storage.createChannelLabel({body:{name:$,rule:O}}))}async previewSmartTag($){return G(await this.client.storage.previewSmartTag({body:$}))}async updateSmartTag($,O,j){return G(await this.client.storage.renameChannelLabel({path:{label_id:$.id},body:{name:O,rule:j}}))}async rename($,O){return G(await this.client.storage.renameChannelLabel({path:{label_id:$.id},body:{name:O}}))}async delete($){G(await this.client.storage.deleteChannelLabel({path:{label_id:$.id}}))}async setLabel($,O){G(await this.client.storage.setChannelLabel({path:{channel_id:$.id},body:{labelId:O?.id??null}}))}}class px{client;async activity(){return G(await this.client.storage.getActivity()).map(($)=>new QO(this.client,b.byId(this.client,$.channel_id),$))}labels;constructor($){this.client=$;this.labels=new rx($)}byId($){return b.byId(this.client,$)}async dm($){return b.dm(this.client,$)}async private($){return b.private(this.client,$)}async create($){return b.create(this.client,$)}async list(){let{items:$}=G(await this.client.storage.getChannels());return $.map((O)=>b.byId(this.client,O.id.toString()))}async*search($,O){if(!O?.searchOn||O.searchOn==="name"){let{items:j}=G(await this.client.storage.getChannels()),x=$.toLowerCase();for(let S of j)if(S.name?.toLowerCase().includes(x))yield b.byId(this.client,S.id.toString())}else yield*b.search(this.client,$,O)}}class aO{client;chat;id;message;constructor($,O,j,x){this.client=$;this.chat=O;this.id=j;this.message=x}static byId($,O,j,x){return new aO($,O,j,x)}async stop(){G(await this.client.cognition.stopChatStream({body:{chat_id:this.chat.id,stream_id:this.id}}))}toJSON(){return{id:this.id,chatId:this.chat.id,messageId:this.message.id}}}class iO extends M{chat;constructor($,O,j){super($,j);this.chat=O}static byId($,O,j){return new iO($,O,j)}async fetch(){let $=G(await this.client.cognition.getChat({path:{chat_id:this.chat.id}})).chat.messages.find((O)=>O.id===this.id);if(!$)throw new E(`Chat message ${this.id} is not yet available`);return $}content=this.field("content");role=this.field("role");async refresh(){this.detail.clear(),await this.detail.get()}toJSON(){return{...super.toJSON(),chatId:this.chat.id}}}class K$ extends X${async send($){let{attachments:O,additionalInstructions:j,...x}=$,S=await this.mutate((Z)=>Z.cognition.sendChatMessage({body:{...x,chat_id:this.id,additional_instructions:j,attachments:O?.map((X)=>({entity_id:X.entity.id,entity_type:X.type}))}})),K=K$.byId(this.client,S.chat_id);return aO.byId(this.client,K,S.stream_id,iO.byId(this.client,K,S.message_id))}access=new o(this.client,"chat",this);entityType="chat";propertyEntityType="CHAT";async fetch(){let{chat:$}=G(await this.client.cognition.getChat({path:{chat_id:this.id}}));return $}static byId($,O){return new K$($,O)}static async create($,O){let{id:j}=G(await $.cognition.createChat({body:{name:O?.name??null,projectId:O?.project?.id??null}}));return new K$($,j)}name=this.field("name");model=this.field("model");project=this.mappedField("projectId",($)=>$?n.byId(this.client,$):void 0);createdAt=this.field("createdAt");updatedAt=this.field("updatedAt");messages=this.field("messages");async rename($){await this.mutate((O)=>O.cognition.patchChat({path:{chat_id:this.id},body:{name:$}}))}async delete($){await this.mutate((O)=>$?.permanent?O.cognition.permanentlyDeleteChat({path:{chat_id:this.id}}):O.cognition.deleteChat({path:{chat_id:this.id}}))}async copy(){let{id:$}=await this.mutate((O)=>O.cognition.copyChat({path:{chat_id:this.id}}));return new K$(this.client,$)}async restore(){await this.mutate(($)=>$.cognition.revertDeleteChat({path:{chat_id:this.id}}))}async history(){let{conversation:$}=G(await this.client.cognition.getChatHistoryHandler({path:{chat_id:this.id}}));return $}webUrl(){return`${this.client.webAppUrl}/app/chat/${this.id}`}static search=x$({filters:{chat_filters:{}},type:"chat",make:($,O)=>new K$($,O.chat_id)});toMention(){return{tag:i$("m-document-mention",{documentId:this.id,documentName:this.detail.peek()?.name??"",blockName:"chat",blockParams:{}}),mention:{entity_type:"chat",entity_id:this.id}}}}class ax{client;constructor($){this.client=$}byId($){return K$.byId(this.client,$)}create($){return K$.create(this.client,$)}search($,O){return K$.search(this.client,$,O)}}var l6=8;class M$ extends M{parent;constructor($,O,j,x){super($,j,x);this.parent=O}get path(){return{parent_type:this.parent.type,parent_id:this.parent.id,id:this.id}}async fetch(){return G(await this.client.storage.entityMessageGetMessage({path:this.path}))}static byId($,O,j){return new M$($,O,j)}static from($,O,j){return new M$($,O,j.id,j)}text=this.field("content");createdAt=this.field("created_at");updatedAt=this.field("updated_at");deletedAt=this.field("deleted_at");async threadId(){return(await this.detail.get()).thread_id??this.id}async author(){return P.byId(this.client,(await this.detail.get()).sender_id)}async reply($){return nO(this.client,this.parent,$,{threadId:await this.threadId()})}async edit($){let{content:O,mentions:j}=O$($);return await this.mutate((x)=>x.storage.entityMessageEdit({path:this.path,body:{content:O,mentions:j}})),this}async delete(){await this.mutate(($)=>$.storage.entityMessageDeleteMessage({path:this.path}))}}async function Nj($,O){let j={parent_type:O.type,parent_id:O.id},x=u(async(K)=>{let Z=G(await $.storage.messageTimeline({path:j,query:{selection:JSON.stringify({limit:100,cursor:K})}}));return{items:Z.items,nextCursor:Z.next_cursor}}),S=[];for await(let K of x)S.push(K);return cO(S,l6,async(K)=>{let{state:Z,root:X,replies:Y}=G(await $.storage.entityMessageGetThread({path:{...j,id:K.id}}));return{thread:Z,comments:[X,...Y].map((L)=>M$.from($,O,L))}})}async function nO($,O,j,x){let S=G(await $.storage.entityMessageCreate({path:{parent_type:O.type,parent_id:O.id},body:{...O$(j),thread_id:x?.threadId??null}}));return M$.from($,O,S)}class f$ extends z${messageThreadById($){return L$.byId(this.client,{type:"crm_contact",entity:this},$)}timelinePage($){return w$(this.client,{type:"crm_contact",entity:this},(O)=>M$.from(this.client,{type:"crm_contact",id:this.id},O),$)}entityType="crm_contact";async fetch(){return G(await this.client.storage.getContact({path:{contact_id:this.id}}))}static byId($,O){return new f$($,O)}static from($,O){return new f$($,O.id,O)}name=this.field("name");email=this.field("email");company=this.mappedField("companyId",($)=>b$.byId(this.client,$));hidden=this.field("hidden");createdAt=this.field("createdAt");updatedAt=this.field("updatedAt");firstInteraction=this.field("firstInteraction");lastInteraction=this.field("lastInteraction");async hide(){await this.setHidden(!0)}async unhide(){await this.setHidden(!1)}async setHidden($){await this.mutate((O)=>O.storage.setContactHidden({path:{contact_id:this.id},body:{hidden:$}}))}async rename($){await this.mutate((O)=>O.storage.setCrmContactName({path:{contact_id:this.id},body:{name:$}}))}get commentParent(){return{type:"crm_contact",id:this.id}}async comments(){return Nj(this.client,this.commentParent)}async comment($,O){return nO(this.client,this.commentParent,$,O)}commentById($){return M$.byId(this.client,this.commentParent,$)}}class b$ extends X${messageThreadById($){return L$.byId(this.client,{type:"crm_company",entity:this},$)}timelinePage($){return w$(this.client,{type:"crm_company",entity:this},(O)=>M$.from(this.client,{type:"crm_company",id:this.id},O),$)}entityType="crm_company";propertyEntityType="COMPANY";async fetch(){return G(await this.client.storage.getCompany({path:{company_id:this.id}}))}static byId($,O){return new b$($,O)}static from($,O){return new b$($,O.id,O)}name=this.field("name");description=this.field("description");hidden=this.field("hidden");emailSync=this.field("emailSync");team=this.mappedField("teamId",($)=>S$.byId(this.client,$));createdAt=this.field("createdAt");updatedAt=this.field("updatedAt");async domain(){let{domains:$}=await this.detail.get();return $[0]?.domain}async domains(){let{domains:$}=await this.detail.get();return $.map((O)=>O.domain)}async contacts(){return G(await this.client.storage.listCompanyContacts({path:{company_id:this.id}})).map((O)=>f$.from(this.client,O))}static search=x$({filters:{crm_company_filters:{}},type:"company",includeCrm:!0,make:($,O)=>new b$($,O.id)});async hide(){await this.setHidden(!0)}async unhide(){await this.setHidden(!1)}async setHidden($){await this.mutate((O)=>O.storage.setCompanyHidden({path:{company_id:this.id},body:{hidden:$}}))}async setEmailSync($){await this.mutate((O)=>O.storage.setEmailSync({path:{company_id:this.id},body:{email_sync:$}}))}async rename($){await this.mutate((O)=>O.storage.setCrmCompanyName({path:{company_id:this.id},body:{name:$}}))}async createContact($){let O=await this.mutate((j)=>j.storage.createCrmContact({path:{company_id:this.id},body:$}));return f$.from(this.client,O)}get commentParent(){return{type:"crm_company",id:this.id}}async comments(){return Nj(this.client,this.commentParent)}async comment($,O){return nO(this.client,this.commentParent,$,O)}commentById($){return M$.byId(this.client,this.commentParent,$)}}class t$ extends M{tableDetail=new W$(async()=>G(await this.client.storage.getCrmPipelineTable({path:{id:this.id}})));static byId($,O){return new t$($,O)}static from($,O){return new t$($,O.id,O)}async fetch(){return G(await this.client.storage.getCrmPipeline({path:{id:this.id}}))}name=this.field("name");recordType=this.field("recordType");accessLevel=this.field("grant");sharing=this.field("sharing");owner=this.mappedField("userId",($)=>P.byId(this.client,$));team=this.mappedField("teamId",($)=>S$.byId(this.client,$));async rename($){await this.mutate((O)=>O.storage.renameCrmPipeline({path:{id:this.id},body:{name:$}}))}async setSharing($){if($===void 0)throw Error("Sharing is required");await this.mutate((O)=>O.storage.shareCrmPipeline({path:{id:this.id},body:{sharing:$}}))}async setTrashed($){await this.mutate((O)=>O.storage.trashCrmPipeline({path:{id:this.id},body:{trashed:$}}))}async sharePermissions(){return G(await this.client.storage.getCrmPipelinePermissions({path:{id:this.id}}))}async updateSharePermissions($){return this.mutate((O)=>O.storage.updateCrmPipelinePermissions({path:{id:this.id},body:n$($)}))}async table(){return Uj.from(this.client,this,await this.tableRecord())}tableRecord(){return this.tableDetail.get()}rows(){return u(async($)=>{let O=G(await this.client.storage.getCrmPipelineRows({path:{id:this.id},query:{after:$}}));return{items:O.rows.map((j)=>VO.from(this.client,this,j)),nextCursor:O.next}})}queryRows($={}){if($.rows?.some((O)=>O.pipeline.id!==this.id))throw Error("Rows belong to another pipeline");return u(async(O)=>{let j=G(await this.client.storage.queryCrmPipelineRows({path:{id:this.id},body:{after:O,query:$.query,rowIds:$.rows?.map((x)=>x.id)}}));return{items:j.rows.map((x)=>VO.from(this.client,this,x)),nextCursor:j.next}})}async applyOps($,O){if(O?.baseVersions?.some((S)=>S.table.pipeline.id!==this.id))throw Error("Table belongs to another pipeline");let j={ops:$,baseVersions:O?.baseVersions?Object.fromEntries(O.baseVersions.map((S)=>[S.table.id,S.version])):void 0},x=await this.mutate((S)=>S.storage.applyCrmPipelineOps({path:{id:this.id},body:j}));return this.tableDetail.clear(),x}}class Uj extends M{pipeline;toJSON(){return{...super.toJSON(),pipelineId:this.pipeline.id}}constructor($,O,j){super($,j.table.id,j);this.pipeline=O}static from($,O,j){return new Uj($,O,j)}fetch(){return this.pipeline.tableRecord()}async columns(){return(await this.pipeline.tableRecord()).columns.map(($)=>zj.from(this.client,this,$))}async version(){return(await this.pipeline.tableRecord()).table.version}rowById($){return VO.byId(this.client,this.pipeline,$)}rows(){return this.pipeline.rows()}}class VO extends M{pipeline;toJSON(){return{...super.toJSON(),pipelineId:this.pipeline.id}}constructor($,O,j,x){super($,j,x);this.pipeline=O}static byId($,O,j){return new VO($,O,j)}static from($,O,j){return new VO($,O,j.rowId,j)}async fetch(){for await(let $ of this.pipeline.queryRows({rows:[this]}))if($.id===this.id)return $.detail.get();throw new E(`Pipeline row ${this.id} is unavailable`)}cells=this.field("cells");async refresh(){this.detail.clear(),await this.detail.get()}}class zj extends M{table;toJSON(){return{...super.toJSON(),pipelineId:this.table.pipeline.id,tableId:this.table.id}}constructor($,O,j){super($,j.column.id,j);this.table=O}static from($,O,j){return new zj($,O,j)}async fetch(){let $=(await this.table.pipeline.tableRecord()).columns.find((O)=>O.column.id===this.id);if(!$)throw new E(`Pipeline column ${this.id} is unavailable`);return $}async writable(){return(await this.fetch()).writable}async name(){return(await this.fetch()).column.display_name??(await this.fetch()).definition.definition.display_name}}class ix{client;constructor($){this.client=$}byId($){return t$.byId(this.client,$)}async list(){return G(await this.client.storage.listCrmPipelines()).map(($)=>t$.from(this.client,$))}async create($){return t$.from(this.client,G(await this.client.storage.createCrmPipeline({body:$})))}}class nx{client;pipelines;constructor($){this.client=$;this.pipelines=new ix($)}companyById($){return b$.byId(this.client,$)}contactById($){return f$.byId(this.client,$)}async contactByEmail($){let{contact:O}=G(await this.client.storage.getContactByEmail({query:{email:$}}));return O?f$.from(this.client,O):void 0}async searchContacts($,O={}){let{contacts:j}=G(await this.client.storage.searchContacts({query:{query:$,limit:O.limit}}));return j.map((x)=>f$.from(this.client,x))}async createCompany($){return b$.from(this.client,G(await this.client.storage.createCrmCompany({body:$})))}async settings(){return G(await this.client.storage.getCrmTeamSettings())}async updateSettings($){return G(await this.client.storage.putCrmTeamSettings({body:$}))}async setStages($){return G(await this.client.storage.putCrmTeamStages({body:$}))}async resetStages(){G(await this.client.storage.resetCrmTeamStages())}searchCompanies($){return b$.search(this.client,$)}}var G$=Symbol.for("@ts-pattern/matcher"),v1=Symbol.for("@ts-pattern/isVariadic");var sx=($)=>Boolean($&&typeof $=="object"),Ij=($)=>$&&!!$[G$],s=($,O,j)=>{if(Ij($)){let x=$[G$](),{matched:S,selections:K}=x.match(O);return S&&K&&Object.keys(K).forEach((Z)=>j(Z,K[Z])),S}if(sx($)){if(!sx(O))return!1;if(Array.isArray($)){if(!Array.isArray(O))return!1;let x=[],S=[],K=[];for(let Z of $.keys()){let X=$[Z];Ij(X)&&X[v1]?K.push(X):K.length?S.push(X):x.push(X)}if(K.length){if(K.length>1)throw Error("Pattern error: Using `...P.array(...)` several times in a single pattern is not allowed.");if(O.lengths(L,Z[Q],j))&&S.every((L,Q)=>s(L,X[Q],j))&&(K.length===0||s(K[0],Y,j))}return $.length===O.length&&$.every((Z,X)=>s(Z,O[X],j))}return Reflect.ownKeys($).every((x)=>{let S=$[x];return((x in O)||Ij(K=S)&&K[G$]().matcherType==="optional")&&s(S,O[x],j);var K})}return Object.is(O,$)},Y$=($)=>{var O,j,x;return sx($)?Ij($)?(O=(j=(x=$[G$]()).getSelectionKeys)==null?void 0:j.call(x))!=null?O:[]:Array.isArray($)?sO($,Y$):sO(Object.values($),Y$):[]},sO=($,O)=>$.reduce((j,x)=>j.concat(O(x)),[]);function r6(...$){if($.length===1){let[O]=$;return(j)=>s(O,j,()=>{})}if($.length===2){let[O,j]=$;return s(O,j,()=>{})}throw Error(`isMatching wasn't given the right number of arguments: expected 1 or 2, received ${$.length}.`)}function t($){return Object.assign($,{optional:()=>ox($),and:(O)=>d($,O),or:(O)=>h1($,O),select:(O)=>O===void 0?tO($):tO(O,$)})}function tx($){return Object.assign(((O)=>Object.assign(O,{[Symbol.iterator](){let j=0,x=[{value:Object.assign(O,{[v1]:!0}),done:!1},{done:!0,value:void 0}];return{next:()=>{var S;return(S=x[j++])!=null?S:x.at(-1)}}}}))($),{optional:()=>tx(ox($)),select:(O)=>tx(O===void 0?tO($):tO(O,$))})}function ox($){return t({[G$]:()=>({match:(O)=>{let j={},x=(S,K)=>{j[S]=K};return O===void 0?(Y$($).forEach((S)=>x(S,void 0)),{matched:!0,selections:j}):{matched:s($,O,x),selections:j}},getSelectionKeys:()=>Y$($),matcherType:"optional"})})}var p6=($,O)=>{for(let j of $)if(!O(j))return!1;return!0},a6=($,O)=>{for(let[j,x]of $.entries())if(!O(x,j))return!1;return!0},i6=($,O)=>{let j=Reflect.ownKeys($);for(let x of j)if(!O(x,$[x]))return!1;return!0};function d(...$){return t({[G$]:()=>({match:(O)=>{let j={},x=(S,K)=>{j[S]=K};return{matched:$.every((S)=>s(S,O,x)),selections:j}},getSelectionKeys:()=>sO($,Y$),matcherType:"and"})})}function h1(...$){return t({[G$]:()=>({match:(O)=>{let j={},x=(S,K)=>{j[S]=K};return sO($,Y$).forEach((S)=>x(S,void 0)),{matched:$.some((S)=>s(S,O,x)),selections:j}},getSelectionKeys:()=>sO($,Y$),matcherType:"or"})})}function m($){return{[G$]:()=>({match:(O)=>({matched:Boolean($(O))})})}}function tO(...$){let O=typeof $[0]=="string"?$[0]:void 0,j=$.length===2?$[1]:typeof $[0]=="string"?void 0:$[0];return t({[G$]:()=>({match:(x)=>{let S={[O!=null?O:"@ts-pattern/anonymous-select-key"]:x};return{matched:j===void 0||s(j,x,(K,Z)=>{S[K]=Z}),selections:S}},getSelectionKeys:()=>[O!=null?O:"@ts-pattern/anonymous-select-key"].concat(j===void 0?[]:Y$(j))})})}function m1($){return!0}function k$($){return typeof $=="number"}function e$($){return typeof $=="string"}function o$($){return typeof $=="bigint"}var c1=t(m(m1)),n6=t(m(m1)),s6=c1,$O=($)=>Object.assign(t($),{startsWith:(O)=>{return $O(d($,(j=O,m((x)=>e$(x)&&x.startsWith(j)))));var j},endsWith:(O)=>{return $O(d($,(j=O,m((x)=>e$(x)&&x.endsWith(j)))));var j},minLength:(O)=>$O(d($,((j)=>m((x)=>e$(x)&&x.length>=j))(O))),length:(O)=>$O(d($,((j)=>m((x)=>e$(x)&&x.length===j))(O))),maxLength:(O)=>$O(d($,((j)=>m((x)=>e$(x)&&x.length<=j))(O))),includes:(O)=>{return $O(d($,(j=O,m((x)=>e$(x)&&x.includes(j)))));var j},regex:(O)=>{return $O(d($,(j=O,m((x)=>e$(x)&&Boolean(x.match(j))))));var j}}),y1=$O(m(e$)),g$=($)=>Object.assign(t($),{between:(O,j)=>g$(d($,((x,S)=>m((K)=>k$(K)&&x<=K&&S>=K))(O,j))),lt:(O)=>g$(d($,((j)=>m((x)=>k$(x)&&xg$(d($,((j)=>m((x)=>k$(x)&&x>j))(O))),lte:(O)=>g$(d($,((j)=>m((x)=>k$(x)&&x<=j))(O))),gte:(O)=>g$(d($,((j)=>m((x)=>k$(x)&&x>=j))(O))),int:()=>g$(d($,m((O)=>k$(O)&&Number.isInteger(O)))),finite:()=>g$(d($,m((O)=>k$(O)&&Number.isFinite(O)))),positive:()=>g$(d($,m((O)=>k$(O)&&O>0))),negative:()=>g$(d($,m((O)=>k$(O)&&O<0)))}),t6=g$(m(k$)),OO=($)=>Object.assign(t($),{between:(O,j)=>OO(d($,((x,S)=>m((K)=>o$(K)&&x<=K&&S>=K))(O,j))),lt:(O)=>OO(d($,((j)=>m((x)=>o$(x)&&xOO(d($,((j)=>m((x)=>o$(x)&&x>j))(O))),lte:(O)=>OO(d($,((j)=>m((x)=>o$(x)&&x<=j))(O))),gte:(O)=>OO(d($,((j)=>m((x)=>o$(x)&&x>=j))(O))),positive:()=>OO(d($,m((O)=>o$(O)&&O>0))),negative:()=>OO(d($,m((O)=>o$(O)&&O<0)))}),e6=OO(m(o$)),o6=t(m(function($){return typeof $=="boolean"})),$7=t(m(function($){return typeof $=="symbol"})),O7=t(m(function($){return $==null})),j7=t(m(function($){return $!=null})),Z$={__proto__:null,matcher:G$,optional:ox,array:function(...$){return tx({[G$]:()=>({match:(O)=>{if(!Array.isArray(O))return{matched:!1};if($.length===0)return{matched:!0};let j=$[0],x={};if(O.length===0)return Y$(j).forEach((K)=>{x[K]=[]}),{matched:!0,selections:x};let S=(K,Z)=>{x[K]=(x[K]||[]).concat([Z])};return{matched:O.every((K)=>s(j,K,S)),selections:x}},getSelectionKeys:()=>$.length===0?[]:Y$($[0])})})},set:function(...$){return t({[G$]:()=>({match:(O)=>{if(!(O instanceof Set))return{matched:!1};let j={};if(O.size===0)return{matched:!0,selections:j};if($.length===0)return{matched:!0};let x=(K,Z)=>{j[K]=(j[K]||[]).concat([Z])},S=$[0];return{matched:p6(O,(K)=>s(S,K,x)),selections:j}},getSelectionKeys:()=>$.length===0?[]:Y$($[0])})})},map:function(...$){return t({[G$]:()=>({match:(O)=>{if(!(O instanceof Map))return{matched:!1};let j={};if(O.size===0)return{matched:!0,selections:j};let x=(X,Y)=>{j[X]=(j[X]||[]).concat([Y])};if($.length===0)return{matched:!0};var S;if($.length===1)throw Error(`\`P.map\` wasn't given enough arguments. Expected (key, value), received ${(S=$[0])==null?void 0:S.toString()}`);let[K,Z]=$;return{matched:a6(O,(X,Y)=>{let L=s(K,Y,x),Q=s(Z,X,x);return L&&Q}),selections:j}},getSelectionKeys:()=>$.length===0?[]:[...Y$($[0]),...Y$($[1])]})})},record:function(...$){return t({[G$]:()=>({match:(O)=>{if(O===null||typeof O!="object"||Array.isArray(O))return{matched:!1};var j;if($.length===0)throw Error(`\`P.record\` wasn't given enough arguments. Expected (value) or (key, value), received ${(j=$[0])==null?void 0:j.toString()}`);let x={},S=(X,Y)=>{x[X]=(x[X]||[]).concat([Y])},[K,Z]=$.length===1?[y1,$[0]]:$;return{matched:i6(O,(X,Y)=>{let L=typeof X!="string"||Number.isNaN(Number(X))?null:Number(X),Q=L!==null&&s(K,L,S),J=s(K,X,S),I=s(Z,Y,S);return(J||Q)&&I}),selections:x}},getSelectionKeys:()=>$.length===0?[]:[...Y$($[0]),...Y$($[1])]})})},intersection:d,union:h1,not:function($){return t({[G$]:()=>({match:(O)=>({matched:!s($,O,()=>{})}),getSelectionKeys:()=>[],matcherType:"not"})})},when:m,select:tO,any:c1,unknown:n6,_:s6,string:y1,number:t6,bigint:e6,boolean:o6,symbol:$7,nullish:O7,nonNullable:j7,instanceOf:function($){return t(m(function(O){return(j)=>j instanceof O}($)))},shape:function($){return t(m(r6($)))}};class u1 extends Error{constructor($){let O;try{O=JSON.stringify($)}catch(j){O=$}super(`Pattern matching error: no pattern matches value ${O}`),this.input=void 0,this.input=$}}var ex={matched:!1,value:void 0};function c($){return new Tj($,ex)}class Tj{constructor($,O){this.input=void 0,this.state=void 0,this.input=$,this.state=O}with(...$){if(this.state.matched)return this;let O=$[$.length-1],j=[$[0]],x;$.length===3&&typeof $[1]=="function"?x=$[1]:$.length>2&&j.push(...$.slice(1,$.length-1));let S=!1,K={},Z=(Y,L)=>{S=!0,K[Y]=L},X=!j.some((Y)=>s(Y,this.input,Z))||x&&!Boolean(x(this.input))?ex:{matched:!0,value:O(S?"@ts-pattern/anonymous-select-key"in K?K["@ts-pattern/anonymous-select-key"]:K:this.input,this.input)};return new Tj(this.input,X)}when($,O){if(this.state.matched)return this;let j=Boolean($(this.input));return new Tj(this.input,j?{matched:!0,value:O(this.input,this.input)}:ex)}otherwise($){return this.state.matched?this.state.value:$(this.input)}exhaustive($=x7){return this.state.matched?this.state.value:$(this.input)}run(){return this.exhaustive()}returnType(){return this}narrow(){return this}}function x7($){throw new u1($)}var $0,S7=new Uint8Array(16);function Hj(){if(!$0){if(typeof crypto>"u"||!crypto.getRandomValues)throw Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");$0=crypto.getRandomValues.bind(crypto)}return $0(S7)}var j$=[];for(let $=0;$<256;++$)j$.push(($+256).toString(16).slice(1));function d1($,O=0){return(j$[$[O+0]]+j$[$[O+1]]+j$[$[O+2]]+j$[$[O+3]]+"-"+j$[$[O+4]]+j$[$[O+5]]+"-"+j$[$[O+6]]+j$[$[O+7]]+"-"+j$[$[O+8]]+j$[$[O+9]]+"-"+j$[$[O+10]]+j$[$[O+11]]+j$[$[O+12]]+j$[$[O+13]]+j$[$[O+14]]+j$[$[O+15]]).toLowerCase()}var O0={};function Z7($,O,j){let x;if($)x=l1($.random??$.rng?.()??Hj(),$.msecs,$.seq,O,j);else{let S=Date.now(),K=Hj();X7(O0,S,K),x=l1(K,O0.msecs,O0.seq,O,j)}return O??d1(x)}function X7($,O,j){if($.msecs??=-1/0,$.seq??=0,O>$.msecs)$.seq=j[6]<<23|j[7]<<16|j[8]<<8|j[9],$.msecs=O;else if($.seq=$.seq+1|0,$.seq===0)$.msecs++;return $}function l1($,O,j,x,S=0){if($.length<16)throw Error("Random bytes length must be >= 16");if(!x)x=new Uint8Array(16),S=0;else if(S<0||S+16>x.length)throw RangeError(`UUID byte range ${S}:${S+15} is out of buffer bounds`);return O??=Date.now(),j??=$[6]*127<<24|$[7]<<16|$[8]<<8|$[9],x[S++]=O/1099511627776&255,x[S++]=O/4294967296&255,x[S++]=O/16777216&255,x[S++]=O/65536&255,x[S++]=O/256&255,x[S++]=O&255,x[S++]=112|j>>>28&15,x[S++]=j>>>20&255,x[S++]=128|j>>>14&63,x[S++]=j>>>6&255,x[S++]=j<<2&255|$[10]&3,x[S++]=$[11],x[S++]=$[12],x[S++]=$[13],x[S++]=$[14],x[S++]=$[15],x}var d$=Z7;class A${table;id;constructor($,O){this.table=$;this.id=O}static byId($,O){return new A$($,O)}async detail(){let{columns:$}=await this.table.detail(),O=$.find((j)=>j.column.id===this.id);if(!O)throw new E(`column ${this.id} is not on table ${this.table.id}`);return O}async name(){let $=await this.detail();return $.column.display_name??$.definition.definition.display_name}async rename($){return await this.table.database.renameColumn(this,$),this}async inferType($){return await this.table.database.inferColumnType(this,$),this}changeType($){return this.table.database.changeColumnType(this,$)}convertIntoNewColumn($){return this.table.database.convertColumnIntoNewColumn(this,$)}casts(){return this.table.database.columnCasts(this)}async delete(){await this.table.database.deleteColumn(this)}async dataType(){return(await this.detail()).definition.definition.data_type}async isMultiSelect(){return(await this.detail()).definition.definition.is_multi_select}async position(){return(await this.detail()).column.position}async definition(){return(await this.detail()).definition}async config(){return(await this.detail()).column.config??void 0}async addOptions($){return await this.table.database.addColumnOptions(this,$),this}toJSON(){return{id:this.id,tableId:this.table.id}}}class F${table;id;constructor($,O){this.table=$;this.id=O}static byId($,O){return new F$($,O)}history(){return this.table.database.rowHistory(this)}toJSON(){return{id:this.id,tableId:this.table.id}}}class Rj{table;id;constructor($,O){this.table=$;this.id=O}static byId($,O){return new Rj($,O)}async detail(){let{views:$}=await this.table.detail(),O=$.find((j)=>j.id===this.id);if(!O)throw new E(`view ${this.id} is not on table ${this.table.id}`);return O}async name(){return(await this.detail()).name}async position(){return(await this.detail()).position}async layout(){return(await this.detail()).layout}async query(){return(await this.detail()).query}positions(){return this.table.database.viewPositions(this)}toJSON(){return{id:this.id,tableId:this.table.id}}}class J${database;id;constructor($,O){this.database=$;this.id=O}static byId($,O){return new J$($,O)}async detail(){let{tables:$}=await this.database.schema(),O=$.find((j)=>j.table.id===this.id);if(!O)throw new E(`table ${this.id} is not in database ${this.database.id}`);return O}async name(){return(await this.detail()).table.name}async rename($){return await this.database.renameTable(this,$),this}async version(){return(await this.detail()).table.version}async position(){return(await this.detail()).table.position}async columns(){let{columns:$}=await this.detail();return $.map((O)=>A$.byId(this,O.column.id))}addColumn($){return this.database.addColumn(this,$)}async reorderColumns($){return await this.database.reorderColumns(this,$),this}row($){return F$.byId(this,$)}async views(){let{views:$}=await this.detail();return $.map((O)=>Rj.byId(this,O.id))}createWebhook(){return this.database.createWebhook(this)}async delete(){await this.database.deleteTable(this)}toJSON(){return{id:this.id,databaseId:this.database.id}}}async function Dj($,O,j,x={}){let S=G(await $.storage.queryDatabases({body:{sql:j,databaseId:x.database?.id,baseVersions:Object.fromEntries((x.baseVersions??[]).map(({table:Q,version:J})=>[Q.id,J]))}})),K=new Map;for(let Q of[x.database,...(x.baseVersions??[]).map(({table:J})=>J.database)]){if(!Q)continue;let J=K.get(Q.id);if(J)J.add(Q);else K.set(Q.id,new Set([Q]))}let Z=(Q)=>{let J=K.get(Q)?.values().next().value;if(J)return J;let I=O($,Q);return K.set(Q,new Set([I])),I},X=(Q)=>{let J=S.tableDatabases[Q];if(!J)throw Error(`SQL response is missing the owner of table ${Q}`);return J$.byId(Z(J),Q)},Y=new Set(Object.keys(S.newVersions).map((Q)=>X(Q).database.id));if(S.statement.kind==="schema")Y.add(S.statement.databaseId);for(let Q of Y)for(let J of K.get(Q)??[])J.invalidateSchema();let L=c(S.statement).with({kind:"select"},(Q)=>Q).with({kind:"schema"},({databaseId:Q,summary:J})=>({kind:"schema",database:Z(Q),summary:J})).with({kind:"insert"},{kind:"update"},{kind:"delete"},({kind:Q,tableId:J,tableName:I})=>({kind:Q,table:X(J),tableName:I})).with({kind:"alterColumnType"},({tableId:Q,tableName:J,columnId:I,columnName:z,to:W})=>{let V=X(Q);return{kind:"alterColumnType",table:V,tableName:J,column:A$.byId(V,I),columnName:z,to:W}}).exhaustive();return{result:S.result?K7(S.result,X,S.tableDatabases):void 0,changesApplied:S.changesApplied,insertedRows:L.kind==="insert"?S.insertedRowIds.map((Q)=>F$.byId(L.table,Q)):[],newVersions:Object.entries(S.newVersions).map(([Q,J])=>({table:X(Q),version:J})),readVersions:Object.entries(S.readVersions).map(([Q,J])=>({table:X(Q),version:J})),truncatedTables:S.truncatedTables,statement:L}}function K7($,O,j){let x=$.sourceTable&&j[$.sourceTable]?O($.sourceTable):void 0;return{columns:$.columns.map(({relatedTable:S,...K})=>({...K,relatedTable:S&&j[S]?O(S):void 0})),rows:$.rows,sourceTable:x,rowHandles:x?$.rowIds.map((S)=>F$.byId(x,S)):[]}}class _j{table;id;createdBy;record;constructor($,O,j,x){this.table=$;this.id=O;this.createdBy=j;this.record=x}static fromRecord($,O,j){return new _j($,j.id,O,j)}get tokenPrefix(){return this.record.tokenPrefix}get createdAt(){return this.record.createdAt}delete(){return this.table.database.deleteWebhook(this)}toJSON(){return{id:this.id,tableId:this.table.id,tokenPrefix:this.tokenPrefix}}}class S0 extends GO{op;row;column;taken;constructor($,O){super($,O);this.name="MacroOpRefusedError",this.message=O.message,this.op=O.op,this.row=O.row,this.column=O.column,this.taken=O.taken}}function Cj($){return c($).with({type:"relation"},({table:O})=>({type:"relation",database:O.database.id,table:O.id})).with({type:Z$.not("relation")},(O)=>O).exhaustive()}var Y7={USER:"People",DOCUMENT:"Documents",TASK:"Tasks",COMPANY:"Companies",CONTACT:"Contacts",CALL_RECORD:"Calls",CHANNEL:"Channels",CHAT:"Chats",PROJECT:"Projects",THREAD:"Emails",CALENDAR_EVENT:"Events",INITIATIVE:"Initiatives"};function G7($){return c($).with({type:"text"},()=>"Text").with({type:"number"},()=>"Number").with({type:"boolean"},()=>"Checkbox").with({type:"date"},()=>"Date").with({type:"link"},()=>"URL").with({type:"select",multi:!0},()=>"Multi-select").with({type:"select",multi:!1},()=>"Select").with({type:"select_number",multi:!0},()=>"Multi-number select").with({type:"select_number",multi:!1},()=>"Number select").with({type:"tag"},()=>"Tag").with({type:"entity"},({target:O})=>Y7[O]).with({type:"relation"},()=>"Relation").exhaustive(()=>"Column")}function j0($){return $.map((O)=>({id:d$(),label:O}))}function r1($,O,j){return $?.kind===O&&"change"in $&&$.change.kind===j}function x0($){return c($).with({kind:"reorder_tables"},({kind:O})=>O).with({kind:Z$.union("table","column","rows","view")},({kind:O,change:j})=>`${O}.${j.kind}`).exhaustive(()=>"unknown")}function l$($,O,j){let[x]=$;if($.length!==1||!r1(x,O,j))throw new f(`expected one ${O}.${j} result, got ${$.map(x0).join(", ")||"none"}`);return x}class V$ extends M{access=new o(this.client,"database",this);async fetch(){return G(await this.client.storage.getDatabase({path:{id:this.id}}))}assertOwns($,O){if(O.id!==this.id)throw new f(`${$} does not belong to database ${this.id}`)}static byId($,O){return new V$($,O)}static async create($,O){let j=G(await $.storage.createDatabase({body:{name:O.name}}));return new V$($,j.id)}static async list($){return G(await $.storage.listDatabases()).map((j)=>new V$($,j.database.id))}query($,O){return Dj(this.client,V$.byId,$,{...O,database:this})}invalidateSchema(){this.detail.clear()}schema(){return this.detail.get()}name=this.mappedField("database",($)=>$.name);owner=this.mappedField("database",($)=>P.byId(this.client,$.owner_id));createdAt=this.mappedField("database",($)=>$.created_at);trashedAt=this.mappedField("database",($)=>$.trashed_at??void 0);grant=this.field("grant");tables=this.mappedField("tables",($)=>$.map((O)=>J$.byId(this,O.table.id)));async table($){let{tables:O}=await this.schema(),j=O.find((x)=>x.table.name===$);return j?J$.byId(this,j.table.id):void 0}async createTable($){let{table:O}=l$(await this.applyOps([{kind:"table",table:d$(),change:{kind:"create",name:$.name}}]),"table","created");return J$.byId(this,O)}async applyOps($,O={}){let j={ops:$};if(O.baseVersions!==void 0){for(let{table:S}of O.baseVersions)this.assertOwns(`table ${S.id}`,S.database);j.baseVersions=Object.fromEntries(O.baseVersions.map(({table:S,version:K})=>[S.id,K]))}let{results:x}=await this.mutate(async(S)=>{let K=await S.storage.applyDatabaseOps({path:{id:this.id},body:j});if(K.error!==void 0&&"op"in K.error)throw new S0(K.response?.status??0,K.error);return K});return x}async reorderTables($){for(let x of $)this.assertOwns(`table ${x.id}`,x.database);let O=await this.applyOps([{kind:"reorder_tables",order:$.map((x)=>x.id)}]),[j]=O;if(O.length!==1||j?.kind!=="reorder_tables")throw new f(`expected one reorder_tables result, got ${O.map(x0).join(", ")||"none"}`);return j.tables.map(({table:x})=>J$.byId(this,x))}async deleteTable($){this.assertOwns(`table ${$.id}`,$.database),l$(await this.applyOps([{kind:"table",table:$.id,change:{kind:"delete"}}]),"table","deleted")}async columnCasts($){return this.assertOwns(`column ${$.id}`,$.table.database),G(await this.client.storage.listDatabaseColumnCasts({path:{id:this.id,table_id:$.table.id,column_id:$.id}}))}async viewPositions($){this.assertOwns(`view ${$.id}`,$.table.database);let{positions:O}=G(await this.client.storage.getDatabaseViewPositions({path:{id:this.id,view_id:$.id}}));return O}async rowHistory($){this.assertOwns(`row ${$.id}`,$.table.database);let{changes:O}=G(await this.client.storage.getDatabaseRowHistory({path:{id:this.id,table_id:$.table.id,row_id:$.id}}));return O}async importTable($){let O=await this.mutate((j)=>j.storage.importDatabaseTable({path:{id:this.id},body:$}));return J$.byId(this,O.id)}webhookOf($){return _j.fromRecord(J$.byId(this,$.tableId),P.byId(this.client,$.createdBy),$)}async webhooks(){let{webhooks:$}=G(await this.client.storage.listDatabaseWebhooks({path:{id:this.id}}));return $.map((O)=>this.webhookOf(O))}async createWebhook($){this.assertOwns("table",$.database);let O=G(await this.client.storage.createDatabaseWebhook({path:{id:this.id},body:{tableId:$.id}}));return{webhook:this.webhookOf(O.webhook),token:O.token}}async deleteWebhook($){this.assertOwns("webhook",$.table.database),G(await this.client.storage.deleteDatabaseWebhook({path:{id:this.id,webhook_id:$.id}}))}async sharePermissions(){return G(await this.client.storage.getDatabasePermissions({path:{id:this.id}}))}updateSharePermissions($){return this.mutate((O)=>O.storage.updateDatabasePermissions({path:{id:this.id},body:$}))}async changeColumnType($,O){this.assertOwns(`column ${$.id}`,$.table.database);let j=await $.table.version();return l$(await this.applyOps([{kind:"column",table:$.table.id,column:$.id,change:{kind:"change_type",to:Cj(O.to)}}],{baseVersions:[{table:$.table,version:j}]}),"column","type_changed")}async columnConversion($,O){return this.assertOwns(`column ${$.id}`,$.table.database),G(await this.client.storage.convertDatabaseColumn({path:{id:this.id,table_id:$.table.id,column_id:$.id},body:{to:Cj(O)}}))}async convertColumnIntoNewColumn($,O){let j=await this.columnConversion($,O.to),x=O.name??`${await $.name()} (${G7(O.to)})`,S=d$(),K=[{kind:"column",table:$.table.id,column:S,change:{kind:"create",definition:{source:"new",name:x,type:Cj(O.to),options:j0(j.options)},after:$.id}}];if(j.cells.length>0)K.push({kind:"rows",table:$.table.id,change:{kind:"update",changes:{kind:"per_row",rows:j.cells.map(({row:X,value:Y})=>({row:X,cells:[{column:S,value:Y}]}))}}});let[Z]=await this.applyOps(K,{baseVersions:[{table:$.table,version:j.tableVersion}]});if(!r1(Z,"column","created"))throw new f(`expected a column.created result, got ${Z?x0(Z):"none"}`);return A$.byId($.table,Z.column)}async deleteColumn($){this.assertOwns(`column ${$.id}`,$.table.database);let O=await $.table.version(),{tableVersion:j}=l$(await this.applyOps([{kind:"column",table:$.table.id,column:$.id,change:{kind:"delete"}}],{baseVersions:[{table:$.table,version:O}]}),"column","deleted");return j}async reorderColumns($,O){this.assertOwns(`table ${$.id}`,$.database);for(let S of O)if(S.table.id!==$.id)throw new f(`column ${S.id} does not belong to table ${$.id}`);let j=await $.version(),{tableVersion:x}=l$(await this.applyOps([{kind:"table",table:$.id,change:{kind:"reorder_columns",order:O.map((S)=>S.id)}}],{baseVersions:[{table:$,version:j}]}),"table","columns_reordered");if(x===void 0)throw new f(`table ${$.id} reordered its columns without a version`);return x}async renameTable($,O){this.assertOwns(`table ${$.id}`,$.database);let j=await $.name(),{tableVersion:x}=l$(await this.applyOps([{kind:"table",table:$.id,change:{kind:"rename",name:O,previousName:j}}]),"table","renamed");if(x===void 0)throw new f(`table ${$.id} was renamed without a version`);return x}async renameColumn($,O){this.assertOwns(`column ${$.id}`,$.table.database);let j=await $.name(),{tableVersion:x}=l$(await this.applyOps([{kind:"column",table:$.table.id,column:$.id,change:{kind:"rename",name:O,previousName:j}}]),"column","renamed");return x}async inferColumnType($,O){return this.assertOwns(`column ${$.id}`,$.table.database),this.mutate((j)=>j.storage.inferDatabaseColumnType({path:{id:this.id,table_id:$.table.id,column_id:$.id},body:{dataType:O.dataType,baseVersion:O.baseVersion,...O.specificEntityType!==void 0?{specificEntityType:O.specificEntityType}:{}}}))}async addColumn($,O){if(this.assertOwns(`table ${$.id}`,$.database),O.after!==void 0&&O.after.table.id!==$.id)throw new f(`column ${O.after.id} does not belong to table ${$.id}`);let j="property"in O?{source:"existing",property:O.property.id}:{source:"new",name:O.name,type:Cj(O.type),...O.options!==void 0?{options:j0(O.options)}:{},...O.inferType!==void 0?{inferType:O.inferType}:{}},{column:x}=l$(await this.applyOps([{kind:"column",table:$.id,column:d$(),change:{kind:"create",definition:j,...O.after!==void 0?{after:O.after.id}:{}}}]),"column","created");return A$.byId($,x)}async addColumnOptions($,O){return this.assertOwns(`column ${$.id}`,$.table.database),l$(await this.applyOps([{kind:"column",table:$.table.id,column:$.id,change:{kind:"add_options",options:j0(O)}}]),"column","options_added")}}class Z0{client;constructor($){this.client=$}query($,O){return Dj(this.client,V$.byId,$,O)}byId($){return V$.byId(this.client,$)}create($){return V$.create(this.client,$)}list(){return V$.list(this.client)}}class Pj{id;name;constructor($,O){this.id=$;this.name=O}static async list($){let{skills:O}=G(await $.storage.getSystemSkillsHandler());return O.map((j)=>new Pj(j.id,j.name))}}class X0{client;async byTeamSlug($){let O=G(await this.client.storage.getDocumentByTeamSlug({path:{slug:$}}));if(typeof O!=="object"||O===null||!("data"in O)||typeof O.data!=="object"||O.data===null||!("documentMetadata"in O.data)||typeof O.data.documentMetadata!=="object"||O.data.documentMetadata===null||!("documentId"in O.data.documentMetadata)||typeof O.data.documentMetadata.documentId!=="string")throw Error("Invalid team task document response");return w.byId(this.client,O.data.documentMetadata.documentId)}async validatePermissionsToken($){let O=G(await this.client.storage.validateDocumentPermissionsToken({body:{token:$}}));return{document:w.byId(this.client,O.document_id),user:O.user_id?P.byId(this.client,O.user_id):void 0,accessLevel:O.access_level,expiresAt:O.exp,issuer:O.iss}}async createUpload($){let O={documentName:$.name,sha:$.sha,fileType:$.fileType,mimeType:$.mimeType,projectId:$.project?.id,teamId:$.team?.id,branchedFromId:$.branchedFrom?.id,emailAttachmentId:$.attachment?.id,isTask:$.isTask},j=G(await this.client.storage.createDocument({body:O})).data;return{document:w.byId(this.client,j.documentMetadata.documentId),uploadUrl:j.presignedUrl??void 0,contentType:j.contentType,content:j.documentMetadata.content}}constructor($){this.client=$}byId($){return w.byId(this.client,$)}create($){return w.create(this.client,$)}async createCanvas($){let O=G(await this.client.storage.createCanvasHandler({body:{documentName:$.name,projectId:$.project?.id}}));return w.byId(this.client,O.documentId)}createSnippet($){return w.createSnippet(this.client,$)}createSkill($){return w.createSkill(this.client,$)}systemSkills(){return Pj.list(this.client)}search($,O){return w.search(this.client,$,O)}recent($){return w.recent(this.client,$)}recentlyDeleted(){return w.recentlyDeleted(this.client)}}class WO extends M{async fetch(){let{attachment:$}=G(await this.client.email.getAttachment({path:{id:this.id}}));return $}static byId($,O){return new WO($,O)}static from($,O){return new WO($,O.db_id,O)}filename=this.field("filename");mimeType=this.field("mime_type");size=this.field("size_bytes");dataUrl=this.field("data_url");sfsId=this.field("sfs_id");contentId=this.field("content_id");async toDocument(){let{document_id:$}=G(await this.client.email.getAttachmentDocumentId({path:{id:this.id}}));return w.byId(this.client,$)}}class R$ extends X${access=new o(this.client,"email_thread",this);async followup(){return G(await this.client.storage.getEmailFollowup({path:{thread_id:this.id}}))}async setFollowup($){return G(await this.client.storage.setEmailFollowup({path:{thread_id:this.id},body:$}))}async calendarInvitations(){return G(await this.client.email.getThreadCalendarInvitations({path:{thread_id:this.id}}))}async addEventToCalendar($,O,j={}){let x=G(await this.client.email.copyThreadCalendarInvitation({path:{thread_id:this.id,message_id:$.id,invitation_id:O.id},body:{calendarId:j.calendar?.id}}));return $$.fromRecord(this.client,x)}async update($){await this.mutate((O)=>O.storage.editThreadV2({path:{thread_id:this.id},body:{projectId:$.project===void 0?void 0:$.project?.id??null,sharePermission:$.sharePermission?n$($.sharePermission):void 0}}))}entityType="email_thread";propertyEntityType="THREAD";async fetch(){let{thread:$}=G(await this.client.email.getThread({path:{thread_id:this.id},query:{limit:100}}));return $}static byId($,O){return new R$($,O)}createdAt=this.field("created_at");updatedAt=this.field("updated_at");isRead=this.field("is_read");inboxVisible=this.field("inbox_visible");accessLevel=this.field("access_level");providerId=this.field("provider_id");link=this.mappedField("link_id",($)=>T$.byId(this.client,$));project=this.mappedField("project_id",($)=>$?n.byId(this.client,$):void 0);latestInboundAt=this.field("latest_inbound_message_ts");latestOutboundAt=this.field("latest_outbound_message_ts");latestNonSpamAt=this.field("latest_non_spam_message_ts");async subject(){let{messages:$}=await this.detail.get();for(let O of $)if(O.subject)return O.subject;return}async participants(){let{messages:$}=await this.detail.get(),O=new Map;for(let j of $)for(let x of[j.from,...j.to,...j.cc,...j.bcc])if(x&&!O.has(x.email))O.set(x.email,x);return[...O.values()]}async messages($){return G(await this.client.email.getThreadMessagesHandler({path:{id:this.id},query:{...$?.since?{since:$.since}:{},...$?.limit?{limit:$.limit}:{}}})).map((j)=>D$.from(this.client,j))}async attachments(){let{messages:$}=await this.detail.get();return $.flatMap((O)=>O.attachments.map((j)=>WO.from(this.client,j)))}async reply($){let O=$.subject??await this.subject()??"",{message:j}=await this.mutate((x)=>x.email.sendMessage({body:{message:{thread_db_id:this.id,replying_to_id:$.replyingTo?.id??null,to:$.to,cc:$.cc??null,bcc:$.bcc??null,subject:O,body_text:$.bodyText}}}));return j.db_id?D$.byId(this.client,j.db_id):void 0}async archive($=!0){await this.mutate((O)=>O.email.archiveThread({path:{id:this.id},body:{value:$}}))}async updateState($){await this.mutate((O)=>O.email.updateThreadState({path:{id:this.id},body:$}))}async operations(){return G(await this.client.email.threadOperations({path:{id:this.id}}))}async markSeen(){await this.mutate(($)=>$.email.threadSeen({path:{id:this.id}}))}async addLabel($){await this.mutate((O)=>O.email.addRemoveThreadLabel({path:{id:this.id},body:{label_id:$.id,value:!0}}))}async removeLabel($){await this.mutate((O)=>O.email.addRemoveThreadLabel({path:{id:this.id},body:{label_id:$.id,value:!1}}))}async moveToProject($){await this.mutate((O)=>O.email.updateThreadProject({path:{thread_id:this.id},body:{projectId:$?.id??null}}))}static search=x$({filters:{email_filters:{}},type:"email",make:($,O)=>new R$($,O.thread_id)})}class D$ extends M{async fetch(){return G(await this.client.email.getMessage({path:{id:this.id}}))}static byId($,O){return new D$($,O)}static from($,O){return new D$($,O.db_id,O)}static async send($,O){let{message:j}=G(await $.email.sendMessage({body:{message:{to:O.to,cc:O.cc??null,bcc:O.bcc??null,subject:O.subject,body_text:O.bodyText}}}));return j.db_id?D$.byId($,j.db_id):void 0}subject=this.field("subject");from=this.field("from");to=this.field("to");cc=this.field("cc");bcc=this.field("bcc");body=this.field("body_parsed");sentAt=this.field("internal_date_ts");labels=this.field("labels");link=this.mappedField("link_id",($)=>T$.byId(this.client,$));async operationStatus(){return G(await this.client.email.messageOperationStatus({path:{id:this.id}}))}async resolveOperation($){await this.mutate((O)=>O.email.resolveMessageOperation({path:{id:this.id},body:$}))}async completeDraftAttachment($,O=0){let j=await this.link();await this.mutate((x)=>x.email.completeDraftAttachment({path:{id:this.id,attachment_id:$},query:{draft_generation:O},headers:{"X-Email-Link-Id":j.id}}))}async transferDraft($){let O=await this.detail.get();return this.mutate((j)=>j.email.transferDraft({path:{id:this.id},body:{...$,draft_generation:$.draft_generation??O.draft_generation??0},headers:{"X-Email-Link-Id":O.link_id}}))}async addLabel($){await this.mutate((O)=>O.email.addRemoveLabel({body:{label_id:$.id,message_ids:[this.id],value:!0}}))}async removeLabel($){await this.mutate((O)=>O.email.addRemoveLabel({body:{label_id:$.id,message_ids:[this.id],value:!1}}))}async thread(){return R$.byId(this.client,(await this.detail.get()).thread_db_id)}}class BO extends D${async draftContext(){let $=await this.detail.get();return{headers:{"X-Email-Link-Id":$.link_id},query:{draft_generation:$.draft_generation??0}}}static byId($,O){return new BO($,O)}draftGeneration=this.field("draft_generation");async update($){let{headers:O,query:j}=await this.draftContext();await this.mutate((x)=>x.email.createDraft({headers:O,body:{draft:{...K0($),db_id:this.id,draft_generation:$.draftGeneration??j.draft_generation}}}))}async delete(){let{headers:$,query:O}=await this.draftContext();await this.mutate((j)=>j.email.deleteDraft({path:{id:this.id},headers:$,query:O}))}async schedule($,O){let{headers:j,query:x}=await this.draftContext();await this.mutate((S)=>S.email.upsertScheduledMessage({path:{id:this.id},headers:j,query:x,body:{send_time:$,include_signature:O?.includeSignature}}))}async unschedule(){let{headers:$,query:O}=await this.draftContext();await this.mutate((j)=>j.email.deleteScheduledDraft({path:{message_id:this.id},headers:$,query:O}))}async addAttachment($){let O={file_name:$.fileName,sha:$.sha,size:$.size,upload_id:$.uploadId},{headers:j,query:x}=await this.draftContext(),S=await this.mutate((K)=>K.email.addDraftAttachment({path:{id:this.id},headers:j,query:x,body:O}));return{attachment:eO.byId(this.client,this,S.attachment_id,!1,x.draft_generation),uploadUrl:S.upload_url,contentType:S.content_type}}async forwardAttachment($){let{headers:O,query:j}=await this.draftContext(),x=await this.mutate((S)=>S.email.addForwardedAttachment({path:{id:this.id},headers:O,query:j,body:{attachment_id:$.id}}));return eO.byId(this.client,this,x.attachment_id,!0,j.draft_generation)}async removeAttachment($){if($.draft.id!==this.id)throw Error("Attachment belongs to another draft");let{headers:O}=await this.draftContext(),j={draft_generation:$.draftGeneration};if($.forwarded)await this.mutate((x)=>x.email.removeForwardedAttachment({path:{id:this.id,attachment_id:$.id},headers:O,query:j}));else await this.mutate((x)=>x.email.removeDraftAttachment({path:{id:this.id,attachment_id:$.id},headers:O,query:j}))}async verifyAttachment($){if($.draft.id!==this.id||$.forwarded)throw Error("Expected an uploaded attachment on this draft");await this.completeDraftAttachment($.id,$.draftGeneration)}}class eO{draft;id;forwarded;draftGeneration;constructor($,O,j,x){this.draft=$;this.id=O;this.forwarded=j;this.draftGeneration=x}static byId($,O,j,x=!1,S=0){return new eO(O,j,x,S)}complete(){return this.draft.verifyAttachment(this)}remove(){return this.draft.removeAttachment(this)}toJSON(){return{id:this.id,draftId:this.draft.id,forwarded:this.forwarded,draftGeneration:this.draftGeneration}}}function K0($){let{bodyText:O,bodyHtml:j,bodyMacro:x,headers:S,includeSignature:K,draftGeneration:Z,thread:X,replyingTo:Y,...L}=$;return{...L,body_text:O,body_html:j===void 0?void 0:btoa(Array.from(new TextEncoder().encode(j),(Q)=>String.fromCharCode(Q)).join("")).replaceAll("+","-").replaceAll("/","_").replaceAll("=",""),body_macro:x,headers_json:S,include_signature:K,draft_generation:Z,thread_db_id:X?.id,replying_to_id:Y?.id}}class LO extends M{inbox;constructor($,O,j,x){super($,O,j);this.inbox=x}get headers(){return this.inbox?{"X-Email-Link-Id":this.inbox.id}:void 0}static byId($,O,j){return new LO($,O,void 0,j)}static from($,O,j){return new LO($,O.id,O,j)}async fetch(){let $=G(await this.client.email.listEmailFilters({headers:this.headers})).filters.find((O)=>O.id===this.id);if(!$)throw new E(`Email filter ${this.id} is unavailable`);return $}emailAddress=this.field("email_address");emailDomain=this.field("email_domain");isImportant=this.field("is_important");async setImportant($){let O=await this.detail.get();await this.mutate((j)=>j.email.upsertEmailFilter({headers:this.headers,body:{email_address:O.email_address,email_domain:O.email_domain,is_important:$}}))}async delete(){await this.mutate(($)=>$.email.deleteEmailFilter({path:{id:this.id},headers:this.headers}))}}class jO extends M{async fetch(){let{labels:$}=G(await this.client.email.listLabels()),O=$.find((j)=>j.id===this.id);if(!O)throw new E(`label ${this.id} not found`);return O}static byId($,O){return new jO($,O)}static async list($){let{labels:O}=G(await $.email.listLabels());return O.map((j)=>new jO($,j.id,j))}static async create($,O){let{label:j}=G(await $.email.createLabel({body:{label_name:O}}));if(!j.id)throw new f("created label has no id");return new jO($,j.id)}async delete(){G(await this.client.email.deleteLabel({path:{id:this.id}}))}name=this.field("name");linkId=this.field("linkId");type=this.field("type");createdAt=this.field("createdAt");labelListVisibility=this.field("labelListVisibility");messageListVisibility=this.field("messageListVisibility")}class Y0{client;async createDraft($,O){let j=G(await this.client.email.createDraft({body:{draft:K0($)},headers:O?{"X-Email-Link-Id":O.id}:void 0}));if(!j.draft.db_id)throw Error("Draft response has no message id");return BO.byId(this.client,j.draft.db_id)}draftById($){return BO.byId(this.client,$)}async*scheduled($){let O=$?.pageSize??100;if(!Number.isInteger(O)||O<1)throw Error("pageSize must be positive");let j=Math.min(O,1000);for(let x=0;;x+=j){let S=G(await this.client.email.getScheduledMessages({query:{offset:x,limit:j},headers:$?.inbox?{"X-Email-Link-Id":$.inbox.id}:void 0}));for(let K of S.messages)yield BO.byId(this.client,K.db_id);if(S.messages.lengthLO.from(this.client,O,$))}async upsertFilter($,O){return LO.from(this.client,G(await this.client.email.upsertEmailFilter({headers:O?{"X-Email-Link-Id":O.id}:void 0,body:{email_address:$.emailAddress,email_domain:$.emailDomain,is_important:$.isImportant}})).filter,O)}async contacts(){return G(await this.client.email.listContacts()).contacts}reminders($){let{inboxes:O,...j}=$??{};return u(async(x)=>{let S=G(await this.client.storage.listEmailReminders({query:{...j,inboxIds:O?.map((K)=>K.id),cursor:x}}));return{items:S.items.map(({threadId:K,...Z})=>({...Z,thread:R$.byId(this.client,K)})),nextCursor:S.nextCursor}})}constructor($){this.client=$}byId($){return R$.byId(this.client,$)}message($){return D$.byId(this.client,$)}attachment($){return WO.byId(this.client,$)}link($){return T$.byId(this.client,$)}links(){return T$.list(this.client)}async connectionProviders(){return G(await this.client.auth.emailConnectionProviders())}async recoverDraftTransfer($){return G(await this.client.email.recoverDraftTransfer({path:{id:$}}))}send($){return D$.send(this.client,$)}search($,O){return R$.search(this.client,$,O)}inbox($){return u(async(O)=>{let j=G(await this.client.email.previewsInboxCursor({path:{view:"inbox"},query:{...$?.pageSize?{limit:$.pageSize}:{},...$?.sort?{sort_method:$.sort}:{},...O?{cursor:O}:{}}}));return{items:j.items.map((x)=>R$.byId(this.client,x.id)),nextCursor:j.next_cursor}})}label($){return jO.byId(this.client,$)}labels(){return jO.list(this.client)}createLabel($){return jO.create(this.client,$)}async deleteLabel($){G(await this.client.email.deleteLabel({path:{id:$.id}}))}async blockSender($){G(await this.client.email.blockSender({body:{email_address:$}}))}async unblockSender($){G(await this.client.email.unblockSender({body:{email_address:$}}))}async blockedSenders(){return G(await this.client.email.listBlockedSenders()).blocked_emails}}class IO{client;id;constructor($,O){this.client=$;this.id=O}static byId($,O){return new IO($,O)}async delete(){G(await this.client.storage.deleteEntityMention({path:{mention_id:this.id}}))}toJSON(){return{id:this.id}}}class G0{client;constructor($){this.client=$}byId($){return IO.byId(this.client,$)}async create($,O){let j=G(await this.client.storage.createEntityMention({body:{source_entity_id:$.entity.id,source_entity_type:$.type,entity_id:O.entity.id,entity_type:O.type}}));return IO.byId(this.client,j.id)}}class Q0{client;constructor($){this.client=$}async list($={}){let{favorites:O}=G(await this.client.storage.listFavorites({query:{entityType:$.entityTypes,entityId:$.entityIds}}));return O}async add($){return G(await this.client.storage.addFavorite({body:{entityId:$.id,entityType:$.entityType}}))}async remove($){G(await this.client.storage.removeFavoriteByEntity({path:{entity_type:$.entityType,entity_id:$.id}}))}async reorder($){G(await this.client.storage.reorderFavorites({body:{favorites:$}}))}}class xO extends z${entityType="foreign_entity";async fetch(){return G(await this.client.storage.getForeignEntity({path:{id:this.id}}))}static byId($,O){return new xO($,O)}static fromRecord($,O){return new xO($,O.id,O)}foreignEntityId=this.field("foreignEntityId");source=this.field("foreignEntitySource");storedForId=this.field("storedForId");storedForAuthEntity=this.field("storedForAuthEntity");createdAt=this.field("createdAt");updatedAt=this.field("updatedAt");async metadata(){return(await this.detail.get()).metadata}}function p1({login:$,...O}){return{...O,login:$??void 0}}class J0{client;constructor($){this.client=$}byId($){return xO.byId(this.client,$)}async bySource($,O){return xO.fromRecord(this.client,G(await this.client.storage.getForeignEntityBySource({path:{source:$,foreign_entity_id:O}})))}async githubPullRequestFacets(){let{repositories:$,authors:O,assignees:j,labels:x}=G(await this.client.storage.getGithubPullRequestFacets());return{repositories:$,authors:O.map(p1),assignees:j.map(p1),labels:x.map(({color:S,...K})=>({...K,color:S??void 0}))}}}class r${form;id;constructor($,O){this.form=$;this.id=O}static byId($,O){return new r$($,O)}async detail(){let O=(await this.form.schema()).sections.flatMap((j)=>j.kind==="questions"?j.questions:[]).find((j)=>j.id===this.id);if(!O)throw new E(`question ${this.id} is not on form ${this.form.id}`);return O}async title(){return(await this.detail()).title}async column(){return A$.byId(await this.form.table(),(await this.detail()).column)}async options(){return(await this.detail()).options.map(($)=>NO.byId(this,$.id))}}class NO{question;id;constructor($,O){this.question=$;this.id=O}static byId($,O){return new NO($,O)}async detail(){let $=(await this.question.detail()).options.find((O)=>O.id===this.id);if(!$)throw new E(`option ${this.id} is not on question ${this.question.id}`);return $}async label(){return(await this.detail()).label}async color(){return(await this.detail()).color??void 0}}class TO{form;id;detail;constructor($,O,j){this.form=$;this.id=O;this.detail=new W$(()=>this.load(),j)}async load(){let $=await this.form.myResponse();if($.response.id!==this.id)throw new E(`response ${this.id} is not your current response`);return $}static byId($,O,j){return new TO($,O,j)}async status(){return(await this.detail.get()).status}async row(){return(await this.detail.get()).row}async answers(){return(await this.detail.get()).answers}async booking(){return(await this.detail.get()).booking}async refresh(){let $=await this.load();return this.detail=new W$(()=>this.load(),$),this}}class p${form;id;constructor($,O){this.form=$;this.id=O}static byId($,O){return new p$($,O)}async detail(){let $=(await this.form.schema()).sections.find((O)=>O.id===this.id);if(!$)throw new E(`section ${this.id} is not on form ${this.form.id}`);return $}async title(){return(await this.detail()).title}async questions(){let $=await this.detail();return $.kind==="questions"?$.questions.map((O)=>r$.byId(this.form,O.id)):[]}}class Mj{entityType;id;constructor($,O){this.entityType=$;this.id=O}static byId($,O){return new Mj($,O)}}class y$ extends M{async fetch(){return G(await this.client.storage.getForm({path:{id:this.id}}))}static byId($,O){return new y$($,O)}static async create($,O){let j=G(await $.storage.createForm({body:{name:O.name,source:O.table?{kind:"table",databaseId:O.table.database.id,tableId:O.table.id}:{kind:"new"}}}));return new y$($,j.form.id,j)}static async list($,O){if(!O)return G(await $.storage.listAccessibleForms()).map(({form:S})=>new y$($,S.id));return G(await $.storage.listForms({query:{databaseId:O.id}})).map((x)=>new y$($,x.id))}schema(){return this.detail.get()}name=this.mappedField("form",($)=>$.name);description=this.mappedField("form",($)=>$.description);owner=this.mappedField("form",($)=>P.byId(this.client,$.ownerId));database=this.mappedField("form",($)=>V$.byId(this.client,$.databaseId));table=this.mappedField("form",($)=>J$.byId(V$.byId(this.client,$.databaseId),$.tableId));audience=this.mappedField("form",($)=>$.audience);status=this.mappedField("form",($)=>$.status);closesAt=this.mappedField("form",($)=>$.closesAt??void 0);access=this.field("access");async sections(){return(await this.detail.get()).sections.map(($)=>p$.byId(this,$.id))}async questions(){return(await this.detail.get()).sections.flatMap(($)=>$.kind==="questions"?$.questions.map((O)=>r$.byId(this,O.id)):[])}async update($){return await this.mutate((O)=>O.storage.updateForm({path:{id:this.id},body:$})),this}assertOwns($){if($.form.id!==this.id)throw new f(`question or section does not belong to form ${this.id}`)}async replaceLayout($){let O=await this.table(),j=(Z)=>{if(Z.table.id!==O.id||Z.table.database.id!==O.database.id)throw new f(`column does not belong to form ${this.id}'s table`);return Z.id},x=(Z)=>({conjunction:Z.conjunction,conditions:Z.conditions.map((X)=>c(X).with({kind:"group"},(Y)=>({kind:"group",...x(Y)})).with({kind:"condition"},(Y)=>({kind:"condition",column:j(Y.column),test:c(Y.test).with({kind:"options"},(L)=>({...L,options:L.options.map((Q)=>Q.id)})).with({kind:"entities"},(L)=>({...L,entities:L.entities.map((Q)=>Q.id)})).with({kind:Z$.union("presence","text","number","date","checkbox")},(L)=>L).exhaustive()})).exhaustive())}),S={sections:$.map((Z)=>{if(Z.section)this.assertOwns(Z.section);let X={id:Z.section?.id??d$(),title:Z.title,description:Z.description??""};return c(Z).with({kind:"booking"},(Y)=>({...X,kind:"booking",target:Y.target})).with({kind:"gate"},(Y)=>({...X,kind:"gate",rules:x(Y.rules),message:Y.message})).with({kind:"questions"},(Y)=>({...X,kind:"questions",questions:Y.questions.map((L)=>{if(L.question)this.assertOwns(L.question);return{id:L.question?.id??d$(),column:j(L.column),helpText:L.helpText??"",required:L.required??!1,widget:L.widget??null}})})).exhaustive()})},K=await this.mutate((Z)=>Z.storage.putFormLayout({path:{id:this.id},body:S}));return{form:this,publicationError:K.publicationError}}wireAnswers($){return $.map(({question:O,value:j})=>{this.assertOwns(O);let x=c(j).with({type:"rows"},(S)=>({type:"rows",value:S.value.map((K)=>K.id)})).with({type:"options"},(S)=>({type:"options",value:S.value.map((K)=>{if(K.question.id!==O.id||K.question.form.id!==this.id)throw new f("option does not belong to the answered question");return{id:K.id}})})).with({type:"entities"},(S)=>({type:"entities",value:S.value.map(({entityType:K,entity:Z})=>({entityType:K,entityId:Z.id}))})).with({type:Z$.union("text","number","boolean","date","link","clear")},(S)=>S).exhaustive();return{question:O.id,value:x}})}unlockedBooking($){return $?{...$,section:p$.byId(this,$.section)}:void 0}async outcome($,O){return c($).with({outcome:"stopped"},(j)=>({...j,section:p$.byId(this,j.section)})).with({outcome:"submitted"},async(j)=>{let x=F$.byId(await this.table(),j.row),S=this.unlockedBooking(j.booking);return{outcome:"submitted",response:TO.byId(this,j.response,{status:"submitted",row:x,answers:O,booking:S}),row:x,...S?{booking:S}:{}}}).exhaustive()}async submit($){let O={answers:this.wireAnswers($)};return this.outcome(G(await this.client.storage.submitFormResponse({path:{id:this.id},body:O})),$)}async editResponse($){let O={answers:this.wireAnswers($)};return this.outcome(G(await this.client.storage.editMyFormResponse({path:{id:this.id},body:O})),$)}async myResponse(){let $=G(await this.client.storage.getMyFormResponse({path:{id:this.id}})),O=await this.table(),j=await Promise.all($.answers.map(async(K)=>{let Z=r$.byId(this,K.question),X=await c(K.value).with({type:"options"},(Y)=>({type:"options",value:Y.value.map((L)=>{if(!("id"in L))throw new f("stored form answers must reference options by id");return NO.byId(Z,L.id)})})).with({type:"entities"},(Y)=>({type:"entities",value:Y.value.map((L)=>({entityType:L.entityType,entity:Mj.byId(L.entityType,L.entityId)}))})).with({type:"rows"},async(Y)=>{let L=await Z.detail();if(L.kind.type!=="relation")throw new f("row answer requires a relation question");let Q=J$.byId(V$.byId(this.client,L.kind.database),L.kind.table);return{type:"rows",value:Y.value.map((J)=>F$.byId(Q,J))}}).with({type:Z$.union("text","number","boolean","date","link","clear")},(Y)=>Y).exhaustive(()=>{throw new f(`answer to question ${Z.id} has a type this SDK doesn't know`)});return{question:Z,value:X}})),x=$.response.row?F$.byId(O,$.response.row):void 0,S=this.unlockedBooking($.booking);return{...S?{booking:S}:{},response:TO.byId(this,$.response.id,{status:$.response.status,row:x,answers:j,booking:S}),status:$.response.status,stoppedAtSection:$.response.stoppedAtSection?p$.byId(this,$.response.stoppedAtSection):void 0,row:x,submittedAt:$.response.submittedAt,updatedAt:$.response.updatedAt,answers:j}}async responseSummary(){let $=G(await this.client.storage.getFormResponseSummary({path:{id:this.id}}));return{...$,stoppedBySection:$.stoppedBySection.map((O)=>({section:p$.byId(this,O.section),count:O.count}))}}async tally(){return G(await this.client.storage.getFormTally({path:{id:this.id}})).questions.map((O)=>{let j=r$.byId(this,O.question);return{question:j,responses:O.responses,buckets:O.buckets.flatMap((x)=>c(x.value).with({kind:"option"},(S)=>[{count:x.count,value:{kind:"option",option:NO.byId(j,S.option)}}]).with({kind:"checkbox"},(S)=>[{count:x.count,value:S}]).exhaustive((S)=>{return console.warn("macro sdk: unknown form tally bucket",S),[]}))}})}async sharePermissions(){return G(await this.client.storage.getFormPermissions({path:{id:this.id}}))}updateSharePermissions($){return this.mutate((O)=>O.storage.updateFormPermissions({path:{id:this.id},body:$}))}webUrl(){return`${this.client.webAppUrl}/app/form/${this.id}`}respondUrl(){return`${this.client.webAppUrl}/app/form/${this.id}/respond`}}class V0{client;constructor($){this.client=$}byId($){return y$.byId(this.client,$)}create($){return y$.create(this.client,$)}list($){return y$.list(this.client,$)}}class HO extends M{static byId($,O){return new HO($,O)}async fetch(){return G(await this.client.storage.getGithubPullRequest({path:{id:this.id}}))}title=this.field("title");url=this.field("url");status=this.field("status");async changes(){let $=G(await this.client.storage.getGithubPullRequestChanges({path:{id:this.id}}));return{changeset:$.changeset?new W0(this.client,this,$.changeset):void 0,error:$.error??void 0}}}class W0{client;pullRequest;record;constructor($,O,j){this.client=$;this.pullRequest=O;this.record=j}get id(){return this.record.id}async patch(){return G(await this.client.storage.getGithubPullRequestChangesPatch({path:{id:this.pullRequest.id},query:{changeset:this.id}})).patch}toJSON(){return{pullRequestId:this.pullRequest.id,...this.record}}}class B0{client;constructor($){this.client=$}byId($){return HO.byId(this.client,$)}forEntity($){return HO.byId(this.client,$.id)}}class RO extends M{static byId($,O){return new RO($,O)}async fetch(){return G(await this.client.storage.getHarnessPairing({path:{code:this.id}}))}name=this.field("requested_name");expiresAt=this.field("expires_at");async approve($){let O={name:$?.name,team_id:$?.team?.id,allow_permission_bypass:$?.allowPermissionBypass};return B$.from(this.client,await this.mutate((j)=>j.storage.approveHarnessPairing({path:{code:this.id},body:O})))}}class oO{client;id;constructor($,O){this.client=$;this.id=O}static byId($,O){return new oO($,O)}async claim($){let O=G(await this.client.storage.claimHarnessPairing({path:{pairing_id:this.id},body:{device_secret:$}}));return"harness"in O?{harness:B$.from(this.client,O.harness),token:O.token}:void 0}toJSON(){return{id:this.id}}}class L0{client;constructor($){this.client=$}byId($){return B$.byId(this.client,$)}async list(){return G(await this.client.storage.listHarnesses()).map(($)=>B$.from(this.client,$))}async me(){return B$.from(this.client,G(await this.client.storage.getSelfHarness()))}async deleteSelf(){G(await this.client.storage.deleteSelfHarness())}async agents(){return G(await this.client.storage.listHarnessAgents()).map(($)=>I$.byId(this.client,$.bot_id))}async sessions(){return G(await this.client.storage.listHarnessSessions()).map(($)=>i.byId(this.client,$.session_id))}pairing($){return RO.byId(this.client,$)}async createPairing($){let O=G(await this.client.storage.createHarnessPairing({body:$}));return{pairing:RO.byId(this.client,O.code),device:oO.byId(this.client,O.pairing_id),deviceSecret:O.device_secret,expiresAt:O.expires_at,pollIntervalSeconds:O.poll_interval_seconds}}}var Q7="00000001-0000-0000-0000-000000000002",J7="00000001-0000-0000-0002-000000000004";function V7($){let O=$.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/([1-9][0-9]*)\/?(?:[?#].*)?$/i);if(!O)throw Error(`Expected a GitHub pull request URL like https://github.com/owner/repo/pull/1, got ${$}`);return`${O[1]}/${O[2]}/pull/${O[3]}`}class _$ extends w{static byId($,O){return new _$($,O)}static async create($,O){let{documentId:j}=G(await $.storage.createTaskHandler({body:{taskName:O.name,markdown:O.markdown??null,projectId:O.project?.id??null,teamId:O.team?.id??null,shareWithTeam:O.shareWithTeam??!0}}));return new _$($,j)}static async forPullRequest($,O){let j=V7(O),{pullRequests:x}=G(await $.storage.getGithubPullRequestTasks({body:{githubKeys:[j]}}));return(x[0]?.taskIds??[]).map((S)=>new _$($,S))}async completed(){let{subType:$}=await this.detail.get();if($!=="task")return;let j=(await this.properties()).find((x)=>x.definition.id===Q7)?.value;return j?.type==="SelectOption"&&j.value.includes(J7)}webUrl(){return`${this.client.webAppUrl}/app/task/${this.id}`}static search=x$({filters:{document_filters:{sub_types:["task"],task_filters:{}}},type:"document",make:($,O)=>new _$($,O.document_id)})}class SO extends M{access=new o(this.client,"initiative",this);static byId($,O){return new SO($,O)}static from($,O){return new SO($,O.id,O)}async fetch(){return G(await this.client.storage.getInitiative({path:{initiative_id:this.id}}))}name=this.field("name");owner=this.mappedField("ownerId",($)=>P.byId(this.client,$));members=this.mappedField("memberIds",($)=>$.map((O)=>P.byId(this.client,O)));tasks=this.mappedField("taskIds",($)=>$.map((O)=>_$.byId(this.client,O)));accessLevel=this.field("userAccessLevel");sharePermissions=this.field("sharePermission");async update($){await this.mutate((O)=>O.storage.updateInitiative({path:{initiative_id:this.id},body:{name:$.name,memberIds:$.members?.map((j)=>j.id),sharePermission:$.sharePermission?n$($.sharePermission):void 0}}))}async delete(){await this.mutate(($)=>$.storage.deleteInitiative({path:{initiative_id:this.id}}))}}class N0{client;constructor($){this.client=$}byId($){return SO.byId(this.client,$)}async list(){return G(await this.client.storage.listInitiatives()).initiatives.map(($)=>SO.byId(this.client,$.id))}async create($){return SO.from(this.client,G(await this.client.storage.createInitiative({body:{name:$.name,description:$.description,memberIds:$.members?.map((O)=>O.id),shareWithTeam:$.shareWithTeam,propertyValues:$.propertyValues}})))}}class U0{client;constructor($){this.client=$}byId($){return H$.byId(this.client,$)}async create($){return H$.from(this.client,G(await this.client.storage.meetingCreate({body:$})))}async list(){return G(await this.client.storage.meetingList()).meetings.map(($)=>H$.from(this.client,$))}async active(){return G(await this.client.storage.meetingListActive()).meetings.map(($)=>H$.from(this.client,$))}async lookup($){return H$.from(this.client,G(await this.client.storage.meetingLookup({path:{token:$}})))}}class UO extends M{async fetch(){return G(await this.client.notification.getTypedNotificationById({path:{notification_id:this.id}}))}static byId($,O){return new UO($,O)}static from($,O){return new UO($,O.id,O)}static list($,O){return u(async(j)=>{let x=G(await $.notification.listTypedNotifications({query:{...O?.pageSize?{limit:O.pageSize}:{},...O?.states!==void 0?{states:O.states.join(",")}:{},...j?{cursor:j}:{}}}));return{items:x.items.map((S)=>UO.from($,S)),nextCursor:x.next_cursor}})}type=this.field("notification_event_type");createdAt=this.field("created_at");updatedAt=this.field("updated_at");state=this.field("state");done=this.mappedField("state",($)=>$==="done");sent=this.field("sent");viewedAt=this.field("viewed_at");deletedAt=this.field("deleted_at");owner=this.mappedField("owner_id",($)=>P.byId(this.client,$));sender=this.mappedField("sender_id",($)=>$?P.byId(this.client,$):void 0);metadata=this.field("notification_metadata");async seen(){return await this.state()!=="unseen"}async markSeen(){await this.mutate(($)=>$.notification.bulkMarkNotificationsSeen({body:{notificationIds:[this.id]}}))}async markDone(){await this.mutate(($)=>$.notification.bulkMarkNotificationsDone({body:{notificationIds:[this.id]}}))}async markUndone(){await this.mutate(($)=>$.notification.bulkMarkNotificationsUndone({body:{notificationIds:[this.id]}}))}async delete(){await this.mutate(($)=>$.notification.deleteUserNotificationV2({path:{notification_id:this.id}}))}}class z0{client;constructor($){this.client=$}byId($){return UO.byId(this.client,$)}list($){return UO.list(this.client,$)}async markSeen($){G(await this.client.notification.bulkMarkNotificationsSeen({body:{notificationIds:$}}))}async markDone($){G(await this.client.notification.bulkMarkNotificationsDone({body:{notificationIds:$}}))}async delete($){G(await this.client.notification.bulkDeleteUserNotificationsV2({body:{notificationIds:$}}))}async preferences(){return G(await this.client.notification.getNotificationTypePreferences())}async enableType($){G(await this.client.notification.enableNotificationType({path:{notification_event_type:$}}))}async disableType($){G(await this.client.notification.disableNotificationType({path:{notification_event_type:$}}))}}class I0{client;constructor($){this.client=$}async list(){let{data:$}=G(await this.client.storage.getPinsHandler({}));return $?.recent??[]}async add($,O){G(await this.client.storage.addPinHandler({path:{pinned_item_id:$.id},body:{pinType:$.entityType,pinIndex:O.index}}))}async remove($){G(await this.client.storage.removePinHandler({path:{pinned_item_id:$.id},body:{pinType:$.entityType}}))}async reorder($){G(await this.client.storage.reorderPinsHandler({body:$}))}}class T0{client;constructor($){this.client=$}byId($){return n.byId(this.client,$)}create($){return n.create(this.client,$)}search($,O){return n.search(this.client,$,O)}}class DO{client;definitionId;id;record;constructor($,O,j,x){this.client=$;this.definitionId=O;this.id=j;this.record=x}static from($,O,j){return new DO($,O,j.id,j)}get value(){return this.record.value}get color(){return this.record.color??void 0}get displayOrder(){return this.record.display_order}get createdAt(){return this.record.created_at}get updatedAt(){return this.record.updated_at}async update($){return this.record=G(await this.client.properties.updatePropertyOption({path:{definition_id:this.definitionId,option_id:this.id},body:$})),this}async delete(){G(await this.client.properties.deletePropertyOption({path:{definition_id:this.definitionId,option_id:this.id}}))}}class ZO{client;id;detail;constructor($,O,j){this.client=$;this.id=O;this.detail=new W$(()=>this.fetch(),j)}async fetch(){return G(await this.client.properties.getPropertyDefinition({path:{definition_id:this.id}}))}static async create($,O){let j=G(await $.properties.createPropertyDefinition({body:{display_name:O.displayName,scope:O.scope,data_type:O.dataType}}));return new ZO($,j.id,j)}static byId($,O){return new ZO($,O)}static fromRecord($,O){return new ZO($,O.id,O)}get displayName(){return this.detail.get().then(($)=>$.display_name)}get dataType(){return this.detail.get().then(($)=>$.data_type)}get owner(){return this.detail.get().then(($)=>$.owner)}get isSystem(){return this.detail.get().then(($)=>$.is_system)}get isMultiSelect(){return this.detail.get().then(($)=>$.is_multi_select)}get createdAt(){return this.detail.get().then(($)=>$.created_at)}get updatedAt(){return this.detail.get().then(($)=>$.updated_at)}async delete(){return G(await this.client.properties.deletePropertyDefinition({path:{definition_id:this.id}}))}async options(){return G(await this.client.properties.getPropertyOptions({path:{definition_id:this.id}})).map((O)=>DO.from(this.client,this.id,O))}async addOption($){let O=G(await this.client.properties.addPropertyOption({path:{definition_id:this.id},body:$}));return DO.from(this.client,this.id,O)}async updateOption($,O){let j=G(await this.client.properties.updatePropertyOption({path:{definition_id:this.id,option_id:$.id},body:O}));return DO.from(this.client,this.id,j)}async deleteOption($){return G(await this.client.properties.deletePropertyOption({path:{definition_id:this.id,option_id:$.id}}))}}class H0{client;constructor($){this.client=$}async list($){return G(await this.client.properties.listProperties({query:{scope:$?.scope??"all",...$?.forEntityType!==void 0?{for_entity_type:$.forEntityType}:{}}})).map((j)=>{let x="definition"in j?j.definition:j;return ZO.fromRecord(this.client,x)})}create($){return ZO.create(this.client,$)}definition($){return ZO.byId(this.client,$)}async bulkEntityProperties($){return G(await this.client.properties.getBulkEntityProperties({body:$}))}async updateEntityPropertyOptions($,O){return G(await this.client.properties.bulkUpdateEntitiesPropertyOptions({body:{entities:$.map((x)=>x.propertyReference()),property_id:O.property.id,...O.add?{add_option_ids:O.add.map((x)=>x.id)}:{},...O.remove?{remove_option_ids:O.remove.map((x)=>x.id)}:{}}})).results}async addEntityPropertyOption($){return G(await this.client.properties.addEntityPropertyOption({path:{...$.entity.propertyReference(),property_id:$.property.id,option_id:$.option.id}}))}async removeEntityPropertyOption($){return G(await this.client.properties.removeEntityPropertyOption({path:{...$.entity.propertyReference(),property_id:$.property.id,option_id:$.option.id}}))}async tags(){return G(await this.client.properties.listTags({}))}}function R0($){return c($).with({type:"cron"},(O)=>O).with({type:"events"},(O)=>({type:"events",filters:O.filters.map(({entities:j,...x})=>({...x,ids:j?.map((S)=>S.entity.id)}))})).exhaustive()}function W7($){return c($).with({type:"multiple"},(O)=>({type:"multiple",triggers:O.triggers.map(R0)})).with({type:"cron"},R0).with({type:"events"},R0).exhaustive()}class XO extends M{static byId($,O){return new XO($,O)}static from($,O){if(!O.id)throw Error("Routine response has no id");return new XO($,O.id,O)}async fetch(){return G(await this.client.scheduledAction.getScheduledAction({path:{id:this.id}}))}name=this.field("name");trigger=this.field("trigger");enabled=this.field("enabled");runVisibility=this.field("run_visibility");nextRunAt=this.field("next_run_at");revision=this.field("configuration_revision");owner=this.mappedField("owner",($)=>P.byId(this.client,$));async update($){await this.mutate((O)=>O.scheduledAction.updateScheduledAction({path:{id:this.id},body:_0($)}))}async setEnabled($){await this.mutate((O)=>O.scheduledAction.setScheduledActionEnabled({path:{id:this.id},body:{enabled:$}}))}async execute(){let $=G(await this.client.scheduledAction.executeScheduledActionNow({path:{id:this.id}}));if($.resource)return c($.resource).with({type:"agent"},(O)=>i.byId(this.client,O.id)).with({type:"chat"},(O)=>K$.byId(this.client,O.id)).exhaustive();return $.chat_id?K$.byId(this.client,$.chat_id):void 0}async history(){return G(await this.client.scheduledAction.listScheduledActionHistory({path:{id:this.id}})).map(($)=>new D0(this,$))}async delete(){await this.mutate(($)=>$.scheduledAction.deleteScheduledAction({path:{id:this.id}}))}}class D0{routine;record;constructor($,O){this.routine=$;this.record=O}get id(){return this.record.id??void 0}toJSON(){return{routine:this.routine.id,...this.record}}}function _0($){let O={prompt:$.prompt,user_prompt:$.userPrompt,model:$.model,agent:$.agent?{bot_id:$.agent.id}:void 0};return{kind:"Agent",name:$.name,enabled:$.enabled,run_visibility:$.runVisibility,trigger:W7($.trigger),task:{...O}}}class C0{client;constructor($){this.client=$}byId($){return XO.byId(this.client,$)}async create($){return XO.from(this.client,G(await this.client.scheduledAction.createScheduledAction({body:{..._0($),enabled:$.enabled??!0}})))}async list($){return G(await this.client.scheduledAction.listScheduledActions({query:{include_events:$?.includeEvents}})).map((O)=>XO.from(this.client,O))}}class P0{client;constructor($){this.client=$}byId($){return _$.byId(this.client,$)}create($){return _$.create(this.client,$)}forPullRequest($){return _$.forPullRequest(this.client,$)}search($,O){return _$.search(this.client,$,O)}}class M0{client;constructor($){this.client=$}byId($){return S$.byId(this.client,$)}current(){return S$.current(this.client)}list(){return S$.list(this.client)}}class A0{client;constructor($){this.client=$}async get($){return G(await this.client.unfurl.getUnfurl({query:{url:$}}))}async bulk($){return G(await this.client.unfurl.getUnfurlBulk({body:{url_list:$}}))}}class F0{client;constructor($){this.client=$}byId($){return P.byId(this.client,$)}me(){return P.me(this.client)}async search($){let O=$.trim().toLocaleLowerCase();if(!O)throw Error("A person search needs a name or email.");let j=await this.client.contacts.getContacts();if(j.response?.status===404)return[];let{contacts:x}=G(j),S=[...new Set(x)],K=[];for(let Z=0;Z[Q.id,Q]));for(let Q of X){let J=L.get(Q)??{id:Q};if(`${[J.first_name,J.last_name].filter(Boolean).join(" ")} ${Q}`.toLocaleLowerCase().includes(O))K.push(P.fromName(this.client,J))}}return K}}class KO extends M{signingSecret;constructor($,O,j,x){super($,O,j);this.signingSecret=x}async fetch(){return G(await this.client.storage.getWebhook({path:{webhook_id:this.id}}))}static async create($,O){let j=G(await $.storage.createWebhook({body:{endpoint_url:O.url,namespace:O.namespace,name:O.name,filters:O.filters,scope:O.scope??"user",...O.headers!==void 0?{headers:O.headers}:{}}}));return new KO($,j.id,j,j.signing_secret)}static byId($,O){return new KO($,O)}static fromRecord($,O){return new KO($,O.id,O)}name=this.field("name");namespace=this.field("namespace");endpointUrl=this.field("endpoint_url");status=this.field("status");isValid=this.field("is_valid");filters=this.field("filters");createdAt=this.field("created_at");workspaceId=this.field("workspace_id");createdBy=this.mappedField("created_by_user_id",($)=>P.byId(this.client,$));async scope(){let $=await this.detail.get();return $.workspace_id===$.created_by_user_id?"user":"team"}async update($){await this.mutate((O)=>O.storage.patchWebhook({path:{webhook_id:this.id},body:$}))}async rename($){await this.update({name:$})}async setUrl($){await this.update({endpoint_url:$})}async setFilters($){await this.update({filters:$})}async pause(){await this.update({status:"paused"})}async resume(){await this.update({status:"active"})}async delete(){await this.mutate(($)=>$.storage.deleteWebhook({path:{webhook_id:this.id}}))}async validate(){return this.mutate(($)=>$.storage.validateWebhook({path:{webhook_id:this.id}}))}}class E0{client;constructor($){this.client=$}create($){return KO.create(this.client,$)}byId($){return KO.byId(this.client,$)}async list(){let{webhooks:$}=G(await this.client.storage.listWebhooks());return $.map((O)=>KO.fromRecord(this.client,O))}}class _O extends M{static byId($,O){return new _O($,O)}static from($,O){return new _O($,O.id,O)}async fetch(){let{account:$}=G(await this.client.storage.linkedAccount());if($?.id!==this.id)throw new E("WhatsApp account is not available");return $}state=this.field("state");identity=this.field("identity_jid");owner=this.mappedField("user_id",($)=>P.byId(this.client,$));async refresh(){return this.detail.clear(),await this.detail.get(),this}}class C$ extends M{group;constructor($,O,j,x){super($,j,x);this.group=O}static byId($,O,j){return new C$($,O,j)}static from($,O,j){return new C$($,O,j.id,j)}get groupId(){return this.group.id}async fetch(){for await(let $ of this.group.messages({limit:100}))if($.id===this.id)return $.detail.get();throw new E("WhatsApp message is not available")}body=this.field("body");sender=this.field("sender");sentAt=this.field("sent_at");editedAt=this.field("edited_at");deleted=this.field("deleted");reactions=this.field("reactions");deliveryState=this.field("delivery_state");kind=this.field("kind");forwarded=this.field("forwarded");mentions=this.field("mentions");rich=this.field("rich");replyTo=this.mappedField("reply_to",($)=>$?C$.byId(this.client,this.group,$):void 0);attachments=this.mappedField("attachments",($)=>$.map((O)=>({ordinal:O.ordinal,state:O.state,fileName:O.file_name,mime:O.mime,size:O.size,width:O.width??void 0,height:O.height??void 0,durationSeconds:O.duration_seconds??void 0,presentation:O.presentation??void 0,document:O.document_id?w.byId(this.client,O.document_id):void 0})))}class YO{id;groupId;filename;kind;constructor($,O,j,x){this.id=$;this.groupId=O;this.filename=j;this.kind=x}static byId($,O){return new YO(O,$.id)}static from($,O){return new YO(O.upload_id,$.id,O.filename,O.kind)}toJSON(){return{id:this.id,groupId:this.groupId,filename:this.filename,kind:this.kind}}}class a$ extends M{group;constructor($,O,j,x){super($,j,x);this.group=O}static byId($,O,j){return new a$($,O,j)}static from($,O,j){return new a$($,O,j.id,j)}async fetch(){let O=G(await this.client.storage.linkedDelivery({path:{group_id:this.group.id},body:[this.id]})).find((j)=>j.id===this.id);if(!O)throw new E("WhatsApp send is not available");return O}state=this.field("state");errorCode=this.field("error_code");createdAt=this.field("created_at");message=this.mappedField("message_id",($)=>$?C$.byId(this.client,this.group,$):void 0);async request(){let{payload:$}=await this.detail.get(),O=$.message,j=c(O).returnType().with({type:"media"},(x)=>({type:"media",upload:YO.byId(this.group,x.upload_id),caption:x.caption??void 0})).with({type:"mutation"},(x)=>({type:"mutation",message:C$.byId(this.client,this.group,x.message_id),operation:x.operation,text:x.text??void 0})).with({type:"text"},(x)=>({type:"text",text:x.text,mentions:x.mentions??[]})).exhaustive();return{idempotencyKey:$.idempotency_key,message:j,replyTo:$.reply_to?C$.byId(this.client,this.group,$.reply_to):void 0}}async refresh(){return this.detail.clear(),await this.detail.get(),this}}class v$ extends z${entityType="whatsapp_group";static byId($,O){return new v$($,O)}async fetch(){return G(await this.client.storage.whatsappGroup({path:{group_id:this.id}}))}name=this.field("name");canSend=this.field("can_send");sendReason=this.field("send_reason");sendingIdentity=this.field("sending_identity");unreadCount=this.field("unread_count");backfill=this.field("backfill");limits=this.field("limits");roster=this.field("roster");owner=this.mappedField("user_id",($)=>P.byId(this.client,$));async*messages($={}){yield*u(async(O)=>{let j=G(await this.client.storage.whatsappGroupMessages({path:{group_id:this.id},query:{before:O,limit:$.limit}}));return{items:j.messages.map((x)=>C$.from(this.client,this,x)),nextCursor:j.next_cursor}})}async markRead($){this.assertGroup($.groupId),await this.mutate((O)=>O.storage.whatsappGroupRead({path:{group_id:this.id},body:{message_id:$.id}}))}async permissions(){return G(await this.client.storage.whatsappGroupPermissions({path:{group_id:this.id}}))}async updatePermissions($){await this.mutate((O)=>O.storage.whatsappGroupUpdatePermissions({path:{group_id:this.id},body:$}))}async detach(){await this.mutate(($)=>$.storage.linkedDetach({path:{group_id:this.id}}))}async upload($){let O=G(await this.client.storage.linkedUpload({path:{group_id:this.id},body:{file:$}}));return YO.from(this,O)}async reply($,O){if(O.replyTo)this.assertGroup(O.replyTo.groupId);let j=c($).returnType().with({type:"text"},(S)=>({type:"text",text:S.text,mentions:S.mentions??[]})).with({type:"media"},(S)=>{return this.assertGroup(S.upload.groupId),{type:"media",upload_id:S.upload.id,caption:S.caption}}).with({type:"mutation"},(S)=>{return this.assertGroup(S.message.groupId),{type:"mutation",message_id:S.message.id,operation:S.operation,text:S.text}}).exhaustive(),x=await this.mutate((S)=>S.storage.linkedReply({path:{group_id:this.id},body:{idempotency_key:O.idempotencyKey,message:j,reply_to:O.replyTo?.id}}));return a$.from(this.client,this,x)}async sends(){return G(await this.client.storage.whatsappGroupSends({path:{group_id:this.id}})).map(($)=>a$.from(this.client,this,$))}async delivery($){for(let O of $)this.assertGroup(O instanceof a$?O.group.id:O.groupId);return G(await this.client.storage.linkedDelivery({path:{group_id:this.id},body:$.map((O)=>O.id)})).map((O)=>a$.from(this.client,this,O))}async messageAttachments($){for(let O of $)this.assertGroup(O.groupId);return G(await this.client.storage.linkedMessageAttachments({path:{group_id:this.id},body:$.map((O)=>O.id)})).map((O)=>({message:C$.byId(this.client,this,O.message_id),attachments:O.linked_attachments.map((j)=>({ordinal:j.ordinal,state:j.state,fileName:j.file_name,mime:j.mime,size:j.size,document:j.document_id?w.byId(this.client,j.document_id):void 0}))}))}async refresh(){return this.detail.clear(),await this.detail.get(),this}assertGroup($){if($!==this.id)throw Error("WhatsApp reference belongs to another group")}}class CO extends M{static byId($,O){return new CO($,O)}static from($,O){return new CO($,O.jid,O)}async fetch(){let{chats:$}=G(await this.client.storage.linkedChats()),O=$.find((j)=>j.jid===this.id);if(!O)throw new E("WhatsApp chat is not available");return O}kind=this.field("kind");name=this.field("name");lastActivityAt=this.field("lastActivityAt");archived=this.field("archived");pinned=this.field("pinned");canAdd=this.field("canAdd");cannotAddReason=this.field("cannotAddReason");group=this.mappedField("group",($)=>$?v$.byId(this.client,$.id):void 0);shared=this.mappedField("shared",($)=>$?v$.byId(this.client,$.id):void 0);async adopt(){let $=await this.mutate((O)=>O.storage.linkedAdopt({body:{group_jid:this.id}}));return v$.byId(this.client,$.group_id)}async refresh(){return this.detail.clear(),await this.detail.get(),this}}class w0{client;constructor($){this.client=$}async account(){let{account:$}=G(await this.client.storage.linkedAccount());return $?_O.from(this.client,$):void 0}async groups(){return G(await this.client.storage.whatsappGroups()).map(($)=>v$.byId(this.client,$.id))}async chats(){return G(await this.client.storage.linkedChats()).chats.map(($)=>CO.from(this.client,$))}byId($){return v$.byId(this.client,$)}}function q0($){if("properties"in $.data)return $.data.properties;if($.tag==="calendarEvent")return $.data.extra.properties;return[]}function a1($,O,j){let x=[];for(let S of q0(O)){if(S.definition.data_type!=="TAG"||S.value?.type!=="SelectOption")continue;for(let K of S.value.value){let Z=j.get(K);if(Z&&!x.some((X)=>X.label===Z.label&&X.scope===Z.scope))x.push(Z)}}switch(O.tag){case"document":return{type:"document",id:O.data.id,entity:w.byId($,O.data.id),snapshot:O,tags:x,name:O.data.name,fileType:O.data.fileType,subType:O.data.subType?.type};case"chat":return{type:"ai_chat",id:O.data.id,entity:K$.byId($,O.data.id),snapshot:O,tags:x,name:O.data.name};case"project":return{type:"project",id:O.data.id,entity:n.byId($,O.data.id),snapshot:O,tags:x,name:O.data.name};case"emailThread":return{type:"email",id:O.data.id,entity:R$.byId($,O.data.id),snapshot:O,tags:x,subject:O.data.name,snippet:O.data.snippet,senderName:O.data.senderName,senderEmail:O.data.senderEmail,inboxVisible:O.data.inboxVisible,isRead:O.data.isRead,isDraft:O.data.isDraft};case"channel":return{type:"channel",id:O.data.channel.id,entity:b.byId($,O.data.channel.id),snapshot:O,tags:x,name:O.data.channel.name};case"channelThread":return{type:"channel_thread",id:O.data.id,entity:new h$($,O.data.channel_id,O.data.id),snapshot:O,tags:x,channelId:O.data.channel_id};case"call":return{type:"call",id:O.data.callId,entity:Q$.byId($,O.data.callId),snapshot:O,tags:x,createdBy:O.data.createdBy};case"calendarEvent":return{type:"calendar_event",id:O.data.id,entity:$$.byId($,O.data.id),snapshot:O,tags:x,title:O.data.title,status:O.data.status,location:O.data.location,conferenceUrl:O.data.conferenceUrl,conferenceProvider:O.data.conferenceProvider,time:O.data.time};case"foreignEntity":return{type:"foreign_entity",id:O.data.id,entity:xO.byId($,O.data.id),snapshot:O,tags:x,foreignEntityId:O.data.foreignEntityId,foreignEntitySource:O.data.foreignEntitySource,metadata:O.data.metadata};default:throw Error(`Unexpected workspace entity type: ${O.tag}`)}}var i1="00000000-0000-0000-0000-000000000000",f0={document:["df","id"],project:["pf","pid"],ai_chat:["cf","cid"],email:["ef","ThreadId"],channel:["chanf","ChannelId"],channel_thread:["cthf","ThreadId"],call:["callf","CallId"],calendar_event:["calf","id"],foreign_entity:["fef","id"]},Aj=($)=>$.replace(/[A-Z]/g,(O)=>O.toLowerCase()),n1=($,O)=>$==null?O:{"&":[$,O]};function b0($,O){if(!$||typeof $!=="object"||Array.isArray($))throw Error(`Invalid workspace ${O}: expected a filter expression.`);let j=Object.entries($),x=j[0];if(j.length!==1||x===void 0)throw Error(`Invalid workspace ${O}: use exactly one of l, &, |, !.`);let[S,K]=x;if(S==="l")return;if(S==="!"){b0(K,`${O}.!`);return}if(S!=="&"&&S!=="|")throw Error(`Invalid workspace ${O}: use l, &, | or !.`);if(!Array.isArray(K)||K.length!==2)throw Error(`Invalid workspace ${O}.${S}: expected exactly two expressions. Use {l: {...}} for a single condition; nest binary operators for three or more.`);K.forEach((Z,X)=>{b0(Z,`${O}.${S}[${X}]`)})}class Fj{client;tagSets;constructor($){this.client=$}sets(){return this.tagSets??=this.client.properties.listTags({}).then(G),this.tagSets}async tagMap(){let $=new Map;for(let O of await this.sets())for(let j of O.options)if(j.value.type==="string")$.set(j.id,{label:j.value.value,scope:O.scope==="user"?"personal":"team"});return $}async body($){for(let[S,K,Z]of[["sortBy",$.sortBy,["recently_viewed","recently_updated","recently_created"]],["emailPreset",$.emailPreset,["signal"]],["tagsMatch",$.tagsMatch,["any","all"]]])if(K!==void 0&&!Z.includes(K))throw Error(`Invalid workspace ${S}: ${K}`);if($.includeTypes!==void 0&&(!Array.isArray($.includeTypes)||$.includeTypes.some((S)=>!Object.hasOwn(f0,S))))throw Error(`Invalid workspace includeTypes. Supported values: ${Object.keys(f0).join(", ")}. Tasks use document with df: {l: {dst: 'task'}}.`);for(let S of["df","pf","cf","ef","chanf","cthf","callf","fef","calf","propf"])if($[S]!=null)b0($[S],S);if($.tags?.some((S)=>S.scope!==void 0&&S.scope!=="personal"&&S.scope!=="team"))throw Error("Invalid workspace tag scope");if($.limit!==void 0&&!Number.isFinite($.limit))throw Error("Workspace limit must be finite");let O={df:$.df,pf:$.pf,cf:$.cf,ef:$.ef,chanf:$.chanf,cthf:$.cthf,callf:$.callf,fef:$.fef,calf:$.calf,propf:$.propf,ccf:{l:{id:i1}},favorites_only:$.favoritesOnly,expand:!0,limit:Math.max(1,Math.min(500,Math.trunc($.limit??50))),sort_method:$.sortBy==="recently_viewed"?"viewed_at":$.sortBy==="recently_created"?"created_at":"updated_at",sort_direction:"desc",emailView:$.emailView??"inbox"};if($.emailPreset==="signal")O.ef={"&":[{l:{Importance:!0}},{l:{Shared:"exclude"}}]};let j=$.inbox?.trim();if(j){let{links:S}=G(await this.client.email.listLinks()),K=S.filter((X)=>Aj(X.email_address)===Aj(j)),Z=K[0];if(!Z)throw Error(`No connected inbox matches "${j}". Connected inboxes: ${S.map((X)=>X.email_address).join(", ")}.`);if(K.length>1)throw Error(`Multiple connected inboxes match "${j}"; cannot pick one.`);O.ef=n1(O.ef,{l:{Owner:Z.id}})}let x=$.includeTypes??($.emailPreset==="signal"?["email"]:void 0);if(x?.length){for(let[S,[K,Z]]of Object.entries(f0))if(!x.includes(S))O[K]={l:{[Z]:i1}}}if($.tags?.length){let S=(await this.sets()).flatMap((Y)=>{let L=Y.definition;if(!L)return[];return Y.options.flatMap((Q)=>Q.value.type==="string"?[{id:Q.id,definitionId:L.id,label:Q.value.value,scope:Y.scope==="user"?"personal":"team"}]:[])}),K=new Map;for(let Y of $.tags){let L=S.filter((Q)=>Aj(Q.label)===Aj(Y.label.trim())&&(!Y.scope||Q.scope===Y.scope));if(!L.length)throw Error(`Unknown tag "${Y.label}". Available tags: ${S.map((Q)=>`${Q.label} (${Q.scope})`).join(", ")}.`);if($.tagsMatch==="all"&&L.length>1)throw Error(`Tag "${Y.label}" is ambiguous; specify personal or team scope.`);for(let Q of L)K.set(Q.id,Q)}let X=[...K.values()].map((Y)=>({l:{pd:Y.definitionId,v:{so:Y.id}}})).reduce((Y,L)=>Y===void 0?L:{[$.tagsMatch==="all"?"&":"|"]:[Y,L]},void 0);O.propf=n1(O.propf,X)}return O}}class k0{client;constructor($){this.client=$}async page($={}){let O=new Fj(this.client);return this.fetch(O,await O.body($),$.cursor)}async*list($={}){let O=new Fj(this.client),j=await O.body($);yield*u((x)=>this.fetch(O,j,x))}async fetch($,O,j){let x=G(await this.client.storage.postItemsSoupAst({body:O,query:j?{cursor:j}:void 0})),K=x.items.some((Z)=>q0(Z).some((X)=>X.definition.data_type==="TAG"))?await $.tagMap():new Map;return{items:x.items.map((Z)=>a1(this.client,Z,K)),nextCursor:x.next_cursor,hasMore:x.next_cursor!=null}}}function s1({onRequest:$,onSseError:O,onSseEvent:j,responseTransformer:x,responseValidator:S,sseDefaultRetryDelay:K,sseMaxRetryAttempts:Z,sseMaxRetryDelay:X,sseSleepFn:Y,url:L,...Q}){let J,I=Y??((V)=>new Promise((B)=>setTimeout(B,V)));return{stream:async function*(){let V=K??3000,B=0,T=Q.signal??new AbortController().signal;while(!0){if(T.aborted)break;B++;let A=Q.headers instanceof Headers?Q.headers:new Headers(Q.headers);if(J!==void 0)A.set("Last-Event-ID",J);try{let D={redirect:"follow",...Q,body:Q.serializedBody,headers:A,signal:T},_=new Request(L,D);if($)_=await $(L,D);let N=await(Q.fetch??globalThis.fetch)(_);if(!N.ok)throw Error(`SSE failed: ${N.status} ${N.statusText}`);if(!N.body)throw Error("No body in SSE response");let H=N.body.pipeThrough(new TextDecoderStream).getReader(),U="",k=()=>{try{H.cancel()}catch{}};T.addEventListener("abort",k);try{while(!0){let{done:l,value:r}=await H.read();if(l)break;U+=r,U=U.replace(/\r\n?/g,`
`);let g=U.split(`
`);U=g.pop()??"";for(let p of g){let a=p.split(`
diff --git a/crates/whatsapp_e2e/AGENTS.md b/crates/whatsapp_e2e/AGENTS.md
index 819ce4d8852..fe0acbd9413 100644
--- a/crates/whatsapp_e2e/AGENTS.md
+++ b/crates/whatsapp_e2e/AGENTS.md
@@ -24,7 +24,16 @@ database. Nothing talks to WhatsApp or AWS (the KMS is
loss or duplicates, drain handover, crash between enqueue and ingest, inbound
image download for import, outbound attachment, a send cut off after
submission is `unknown` and never resent, a send without an ack times out as
- `unknown`, and a real client's group events through the service's mapping.
+ `unknown`, a real client's group events through the service's mapping, and
+ WhatsApp renames and removals reaching Macro live (a rename WhatsApp made
+ is a system line without an actor).
+- Message types (`src/test/message_types.rs`): media dimensions, durations
+ and presentation (sticker, GIF, voice note), a media reply keeping its
+ quote, view-once as a placeholder without an attachment; mentions and
+ forwards, polls with a vote and a changed vote, places, contact cards,
+ events, pins and call logs as structured content; a mention sent from
+ Macro; group changes made by a member (added, left, renamed) as system
+ lines once with two observing accounts, never unread or notified.
## Checks
diff --git a/crates/whatsapp_e2e/src/test.rs b/crates/whatsapp_e2e/src/test.rs
index 495cf9b97a6..83c73330f6c 100644
--- a/crates/whatsapp_e2e/src/test.rs
+++ b/crates/whatsapp_e2e/src/test.rs
@@ -23,6 +23,8 @@ use whatsmeow::types::Jid;
use super::*;
+mod message_types;
+
const TIMEOUT: Duration = Duration::from_secs(60);
const ALICE: &str = "macro|alice@example.com";
const BOB: &str = "macro|bob@example.com";
@@ -326,6 +328,7 @@ impl World {
idempotency_key: Uuid::now_v7(),
message: ReplyContent::Text {
text: text.to_owned(),
+ mentions: vec![],
},
reply_to: None,
},
@@ -682,8 +685,8 @@ async fn a_phone_group_message_maps_to_one_inbound_text() {
));
}
}
- assert!(mapped[0].is_none(), "the key-only part is not content");
- let Some(Mapped::Inbound(event)) = &mapped[1] else {
+ assert!(mapped[0].is_empty(), "the key-only part is not content");
+ let [Mapped::Inbound(event)] = mapped[1].as_slice() else {
panic!("the text maps to an inbound event");
};
assert_eq!(event.group_jid, group.to_string());
@@ -870,6 +873,27 @@ async fn group_renames_and_removals_in_whatsapp_reach_macro_live(pool: PgPool) {
world
.expect(renamed, "the Macro group follows the WhatsApp rename")
.await;
+ // WhatsApp did not say who renamed it: a system line without an actor.
+ let line = wait_for_rows(&world.pool, TIMEOUT, || async {
+ sqlx::query!(
+ r#"SELECT rich->>'action' AS "action!", rich->>'value' AS value, (rich->>'byWhatsapp')::boolean AS "by_whatsapp!"
+ FROM whatsapp_group_message WHERE binding_id = $1 AND kind = 'system' AND message_id IS NOT NULL"#,
+ binding.id
+ )
+ .fetch_optional(&world.pool)
+ .await
+ .unwrap()
+ .map(|row| (row.action, row.value, row.by_whatsapp))
+ })
+ .await;
+ assert_eq!(
+ world.expect(line, "the rename is a system line").await,
+ (
+ "subject_changed".to_owned(),
+ Some("Escalations, renamed".to_owned()),
+ true
+ )
+ );
world.whatsapp.remove_from_group(&world.group, &world.alice);
let left = wait_for_rows(&world.pool, TIMEOUT, || async {
diff --git a/crates/whatsapp_e2e/src/test/message_types.rs b/crates/whatsapp_e2e/src/test/message_types.rs
new file mode 100644
index 00000000000..6c23e54a0c9
--- /dev/null
+++ b/crates/whatsapp_e2e/src/test/message_types.rs
@@ -0,0 +1,991 @@
+//! What phones send besides plain text, end to end: media with its shape,
+//! replies to media, mentions and forwards, polls and votes, places,
+//! contact cards, events, pins, calls, view-once media, and group changes
+//! as system lines.
+
+use std::sync::Mutex;
+
+use whatsapp_fake_server::GroupChange;
+use whatsapp_linked::domain::effects::EffectsService;
+use whatsapp_linked::domain::models::MediaPresentation;
+use whatsapp_linked::domain::ports::{GroupEffect, GroupPublisher};
+use whatsapp_linked::domain::rich::{
+ Call, CallOutcome, ContactCard, Contacts, Event, Location, SystemAction,
+};
+use whatsapp_linked::domain::timeline::{
+ WhatsAppGroupMessage, WhatsAppMention, WhatsAppMessageKind, WhatsAppPersonRef, WhatsAppPoll,
+ WhatsAppPollOption, WhatsAppRichContent, WhatsAppSystemLine,
+};
+use whatsmeow::download::MediaType;
+use whatsmeow::proto::wa_common::MessageKey;
+use whatsmeow::proto::wa_e2e::{
+ AudioMessage, CallLogMessage, ContactMessage, ContextInfo, EventMessage, ExtendedTextMessage,
+ FutureProofMessage, ImageMessage, LocationMessage, Message, PinInChatMessage, StickerMessage,
+ VideoMessage, call_log_message, pin_in_chat_message,
+};
+
+use super::*;
+
+/// The timeline entry for WhatsApp message `source`, as Alice reads it.
+async fn shown(world: &World, binding: &Binding, source: &str) -> WhatsAppGroupMessage {
+ let id = sqlx::query_scalar!(
+ "SELECT id FROM whatsapp_group_message WHERE binding_id = $1 AND source_id = $2",
+ binding.id,
+ source
+ )
+ .fetch_one(&world.pool)
+ .await
+ .unwrap();
+ world
+ .linked
+ .messages(&receipt::(ALICE, binding.id), None, 100)
+ .await
+ .unwrap()
+ .messages
+ .into_iter()
+ .find(|message| message.id == id)
+ .expect("the message is on the timeline")
+}
+
+/// Each stored attachment of one WhatsApp message: mime, width, height,
+/// duration and presentation.
+async fn attachment_shapes(
+ pool: &PgPool,
+ binding: Uuid,
+ source: &str,
+) -> Vec<(
+ Option,
+ Option,
+ Option,
+ Option,
+ Option,
+)> {
+ sqlx::query!(
+ "SELECT mime, width, height, duration_seconds, presentation FROM whatsapp_group_attachment
+ WHERE binding_id = $1 AND source_id = $2 ORDER BY ordinal",
+ binding,
+ source
+ )
+ .fetch_all(pool)
+ .await
+ .unwrap()
+ .into_iter()
+ .map(|row| {
+ (
+ row.mime,
+ row.width,
+ row.height,
+ row.duration_seconds,
+ row.presentation,
+ )
+ })
+ .collect()
+}
+
+#[sqlx::test(migrator = "MACRO_DB_MIGRATIONS")]
+async fn media_keeps_its_shape_and_a_media_reply_keeps_its_quote(pool: PgPool) {
+ let world = World::new(pool).await;
+ let replica = world.replica().await;
+ let (_, binding) = world.alice_adopted().await;
+
+ let question = world
+ .customer
+ .send_text(&world.group, "which box?")
+ .await
+ .unwrap();
+ assert_eq!(world.timeline(&binding, &question).await, ["which box?"]);
+
+ // A photo that replies to the question.
+ let photo = b"jpeg of the damaged box".repeat(40);
+ let uploaded = world
+ .customer
+ .upload_media(&photo, MediaType::Image)
+ .await
+ .unwrap();
+ let reply = world
+ .customer
+ .send_message(
+ &world.group,
+ Message {
+ image_message: Some(Box::new(ImageMessage {
+ url: Some(uploaded.url),
+ direct_path: Some(uploaded.direct_path),
+ media_key: Some(uploaded.media_key),
+ file_enc_sha256: Some(uploaded.file_enc_sha256),
+ file_sha256: Some(uploaded.file_sha256),
+ file_length: Some(uploaded.file_length),
+ mimetype: Some("image/jpeg".to_owned()),
+ caption: Some("this one".to_owned()),
+ width: Some(1200),
+ height: Some(900),
+ context_info: Some(Box::new(ContextInfo {
+ stanza_id: Some(question.clone()),
+ participant: Some(world.customer.lid().to_string()),
+ quoted_message: Some(Box::new(Message {
+ conversation: Some("which box?".to_owned()),
+ ..Message::default()
+ })),
+ ..ContextInfo::default()
+ })),
+ ..ImageMessage::default()
+ })),
+ ..Message::default()
+ },
+ )
+ .await
+ .unwrap();
+
+ let clip = b"mp4 of the unboxing".repeat(40);
+ let uploaded = world
+ .customer
+ .upload_media(&clip, MediaType::Video)
+ .await
+ .unwrap();
+ let video = world
+ .customer
+ .send_message(
+ &world.group,
+ Message {
+ video_message: Some(Box::new(VideoMessage {
+ url: Some(uploaded.url),
+ direct_path: Some(uploaded.direct_path),
+ media_key: Some(uploaded.media_key),
+ file_enc_sha256: Some(uploaded.file_enc_sha256),
+ file_sha256: Some(uploaded.file_sha256),
+ file_length: Some(uploaded.file_length),
+ mimetype: Some("video/mp4".to_owned()),
+ width: Some(1280),
+ height: Some(720),
+ seconds: Some(12),
+ ..VideoMessage::default()
+ })),
+ ..Message::default()
+ },
+ )
+ .await
+ .unwrap();
+
+ let loop_bytes = b"mp4 that loops".repeat(40);
+ let uploaded = world
+ .customer
+ .upload_media(&loop_bytes, MediaType::Video)
+ .await
+ .unwrap();
+ let gif = world
+ .customer
+ .send_message(
+ &world.group,
+ Message {
+ video_message: Some(Box::new(VideoMessage {
+ url: Some(uploaded.url),
+ direct_path: Some(uploaded.direct_path),
+ media_key: Some(uploaded.media_key),
+ file_enc_sha256: Some(uploaded.file_enc_sha256),
+ file_sha256: Some(uploaded.file_sha256),
+ file_length: Some(uploaded.file_length),
+ mimetype: Some("video/mp4".to_owned()),
+ width: Some(480),
+ height: Some(270),
+ seconds: Some(3),
+ gif_playback: Some(true),
+ ..VideoMessage::default()
+ })),
+ ..Message::default()
+ },
+ )
+ .await
+ .unwrap();
+
+ let sticker_bytes = b"webp sticker".repeat(40);
+ let uploaded = world
+ .customer
+ .upload_media(&sticker_bytes, MediaType::Image)
+ .await
+ .unwrap();
+ let sticker = world
+ .customer
+ .send_message(
+ &world.group,
+ Message {
+ sticker_message: Some(Box::new(StickerMessage {
+ url: Some(uploaded.url),
+ direct_path: Some(uploaded.direct_path),
+ media_key: Some(uploaded.media_key),
+ file_enc_sha256: Some(uploaded.file_enc_sha256),
+ file_sha256: Some(uploaded.file_sha256),
+ file_length: Some(uploaded.file_length),
+ mimetype: Some("image/webp".to_owned()),
+ width: Some(512),
+ height: Some(512),
+ ..StickerMessage::default()
+ })),
+ ..Message::default()
+ },
+ )
+ .await
+ .unwrap();
+
+ let voice_bytes = b"opus voice note".repeat(40);
+ let uploaded = world
+ .customer
+ .upload_media(&voice_bytes, MediaType::Audio)
+ .await
+ .unwrap();
+ let voice = world
+ .customer
+ .send_message(
+ &world.group,
+ Message {
+ audio_message: Some(Box::new(AudioMessage {
+ url: Some(uploaded.url),
+ direct_path: Some(uploaded.direct_path),
+ media_key: Some(uploaded.media_key),
+ file_enc_sha256: Some(uploaded.file_enc_sha256),
+ file_sha256: Some(uploaded.file_sha256),
+ file_length: Some(uploaded.file_length),
+ mimetype: Some("audio/ogg; codecs=opus".to_owned()),
+ seconds: Some(7),
+ ptt: Some(true),
+ ..AudioMessage::default()
+ })),
+ ..Message::default()
+ },
+ )
+ .await
+ .unwrap();
+
+ // View-once media is only ever shown on the phone.
+ let secret_bytes = b"jpeg seen once".repeat(40);
+ let uploaded = world
+ .customer
+ .upload_media(&secret_bytes, MediaType::Image)
+ .await
+ .unwrap();
+ let view_once = world
+ .customer
+ .send_message(
+ &world.group,
+ Message {
+ view_once_message: Some(Box::new(FutureProofMessage {
+ message: Some(Box::new(Message {
+ image_message: Some(Box::new(ImageMessage {
+ url: Some(uploaded.url),
+ direct_path: Some(uploaded.direct_path),
+ media_key: Some(uploaded.media_key),
+ file_enc_sha256: Some(uploaded.file_enc_sha256),
+ file_sha256: Some(uploaded.file_sha256),
+ file_length: Some(uploaded.file_length),
+ mimetype: Some("image/jpeg".to_owned()),
+ view_once: Some(true),
+ ..ImageMessage::default()
+ })),
+ ..Message::default()
+ })),
+ })),
+ ..Message::default()
+ },
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(world.timeline(&binding, &reply).await, ["this one"]);
+ for source in [&video, &gif, &sticker, &voice] {
+ assert_eq!(world.timeline(&binding, source).await, [""]);
+ }
+ assert_eq!(
+ world.timeline(&binding, &view_once).await,
+ ["View-once message \u{2014} open it on your phone"]
+ );
+
+ assert_eq!(
+ attachment_shapes(&world.pool, binding.id, &reply).await,
+ [(
+ Some("image/jpeg".to_owned()),
+ Some(1200),
+ Some(900),
+ None,
+ None
+ )]
+ );
+ assert_eq!(
+ attachment_shapes(&world.pool, binding.id, &video).await,
+ [(
+ Some("video/mp4".to_owned()),
+ Some(1280),
+ Some(720),
+ Some(12),
+ None
+ )]
+ );
+ assert_eq!(
+ attachment_shapes(&world.pool, binding.id, &gif).await,
+ [(
+ Some("video/mp4".to_owned()),
+ Some(480),
+ Some(270),
+ Some(3),
+ Some("gif".to_owned())
+ )]
+ );
+ assert_eq!(
+ attachment_shapes(&world.pool, binding.id, &sticker).await,
+ [(
+ Some("image/webp".to_owned()),
+ Some(512),
+ Some(512),
+ None,
+ Some("sticker".to_owned())
+ )]
+ );
+ assert_eq!(
+ attachment_shapes(&world.pool, binding.id, &voice).await,
+ [(
+ Some("audio/ogg".to_owned()),
+ None,
+ None,
+ Some(7),
+ Some("voice_note".to_owned())
+ )]
+ );
+ assert_eq!(
+ attachment_shapes(&world.pool, binding.id, &view_once).await,
+ []
+ );
+
+ // The timeline shows the quote and the shapes.
+ let question_shown = shown(&world, &binding, &question).await;
+ let reply_shown = shown(&world, &binding, &reply).await;
+ assert_eq!(reply_shown.reply_to, Some(question_shown.id));
+ assert_eq!(reply_shown.attachments.len(), 1);
+ assert_eq!(
+ (
+ reply_shown.attachments[0].width,
+ reply_shown.attachments[0].height
+ ),
+ (Some(1200), Some(900))
+ );
+ let voice_shown = shown(&world, &binding, &voice).await;
+ assert_eq!(voice_shown.attachments.len(), 1);
+ assert_eq!(voice_shown.attachments[0].duration_seconds, Some(7));
+ assert_eq!(
+ voice_shown.attachments[0].presentation,
+ Some(MediaPresentation::VoiceNote)
+ );
+ let sticker_shown = shown(&world, &binding, &sticker).await;
+ assert_eq!(
+ sticker_shown.attachments[0].presentation,
+ Some(MediaPresentation::Sticker)
+ );
+ let gif_shown = shown(&world, &binding, &gif).await;
+ assert_eq!(
+ gif_shown.attachments[0].presentation,
+ Some(MediaPresentation::Gif)
+ );
+ assert!(
+ shown(&world, &binding, &view_once)
+ .await
+ .attachments
+ .is_empty()
+ );
+
+ replica.drain(TIMEOUT).await.unwrap();
+}
+
+#[sqlx::test(migrator = "MACRO_DB_MIGRATIONS")]
+async fn polls_places_cards_events_mentions_pins_and_calls_arrive_structured(pool: PgPool) {
+ let world = World::new(pool).await;
+ let replica = world.replica().await;
+ let (_, binding) = world.alice_adopted().await;
+ let customer_lid = world.customer.lid().to_string();
+
+ let opener = world
+ .customer
+ .send_text(&world.group, "anyone free this week?")
+ .await
+ .unwrap();
+ assert_eq!(
+ world.timeline(&binding, &opener).await,
+ ["anyone free this week?"]
+ );
+
+ // Bob forwards a note that mentions the customer.
+ let mention_text = format!("@{} can you check this?", world.customer.lid().user);
+ let forwarded = world
+ .bob
+ .send_message(
+ &world.group,
+ Message {
+ extended_text_message: Some(Box::new(ExtendedTextMessage {
+ text: Some(mention_text.clone()),
+ context_info: Some(Box::new(ContextInfo {
+ mentioned_jid: vec![customer_lid.clone()],
+ is_forwarded: Some(true),
+ forwarding_score: Some(1),
+ ..ContextInfo::default()
+ })),
+ ..ExtendedTextMessage::default()
+ })),
+ ..Message::default()
+ },
+ )
+ .await
+ .unwrap();
+ assert_eq!(world.timeline(&binding, &forwarded).await, [mention_text]);
+ let forwarded_shown = shown(&world, &binding, &forwarded).await;
+ assert!(forwarded_shown.forwarded);
+ assert_eq!(
+ forwarded_shown.mentions,
+ [WhatsAppMention {
+ token: world.customer.lid().user,
+ person: WhatsAppPersonRef {
+ name: "Customer".to_owned(),
+ user_id: None,
+ },
+ }]
+ );
+
+ // A poll, then Bob's vote, then Bob changing it.
+ let poll = whatsmeow::msgsecret::build_poll_creation(
+ "Which day?",
+ &["Tuesday".to_owned(), "Wednesday".to_owned()],
+ 1,
+ );
+ let secret = poll
+ .message_context_info
+ .as_ref()
+ .and_then(|context| context.message_secret.clone())
+ .expect("a poll carries its message secret");
+ let poll_id = world
+ .customer
+ .send_message(&world.group, poll)
+ .await
+ .unwrap();
+ assert_eq!(world.timeline(&binding, &poll_id).await, [""]);
+ let unvoted = shown(&world, &binding, &poll_id).await;
+ assert_eq!(unvoted.kind, WhatsAppMessageKind::Message);
+ assert_eq!(
+ unvoted.rich,
+ Some(WhatsAppRichContent::Poll(WhatsAppPoll {
+ question: "Which day?".to_owned(),
+ options: vec![
+ WhatsAppPollOption {
+ name: "Tuesday".to_owned(),
+ votes: 0,
+ mine: false,
+ voters: vec![],
+ },
+ WhatsAppPollOption {
+ name: "Wednesday".to_owned(),
+ votes: 0,
+ mine: false,
+ voters: vec![],
+ },
+ ],
+ selectable_count: 1,
+ voters: 0,
+ }))
+ );
+
+ world
+ .bob
+ .vote(
+ &world.group,
+ &poll_id,
+ &world.customer.lid(),
+ &secret,
+ &["Tuesday"],
+ )
+ .await
+ .unwrap();
+ let bob = WhatsAppPersonRef {
+ name: "Bob".to_owned(),
+ user_id: None,
+ };
+ let voted = wait_for_rows(&world.pool, TIMEOUT, || async {
+ let shown = shown(&world, &binding, &poll_id).await;
+ let Some(WhatsAppRichContent::Poll(poll)) = shown.rich else {
+ return None;
+ };
+ (poll.voters == 1).then_some(poll)
+ })
+ .await;
+ let voted = world.expect(voted, "Bob's vote is tallied").await;
+ assert_eq!(
+ voted.options,
+ [
+ WhatsAppPollOption {
+ name: "Tuesday".to_owned(),
+ votes: 1,
+ mine: false,
+ voters: vec![bob.clone()],
+ },
+ WhatsAppPollOption {
+ name: "Wednesday".to_owned(),
+ votes: 0,
+ mine: false,
+ voters: vec![],
+ },
+ ]
+ );
+
+ world
+ .bob
+ .vote(
+ &world.group,
+ &poll_id,
+ &world.customer.lid(),
+ &secret,
+ &["Wednesday"],
+ )
+ .await
+ .unwrap();
+ let moved = wait_for_rows(&world.pool, TIMEOUT, || async {
+ let shown = shown(&world, &binding, &poll_id).await;
+ let Some(WhatsAppRichContent::Poll(poll)) = shown.rich else {
+ return None;
+ };
+ (poll.options[1].votes == 1).then_some(poll)
+ })
+ .await;
+ let moved = world.expect(moved, "Bob's changed vote moves").await;
+ assert_eq!(
+ moved,
+ WhatsAppPoll {
+ question: "Which day?".to_owned(),
+ options: vec![
+ WhatsAppPollOption {
+ name: "Tuesday".to_owned(),
+ votes: 0,
+ mine: false,
+ voters: vec![],
+ },
+ WhatsAppPollOption {
+ name: "Wednesday".to_owned(),
+ votes: 1,
+ mine: false,
+ voters: vec![bob.clone()],
+ },
+ ],
+ selectable_count: 1,
+ voters: 1,
+ }
+ );
+ let stored_votes =
+ sqlx::query_scalar!(r#"SELECT count(*) AS "count!" FROM whatsapp_group_poll_vote"#)
+ .fetch_one(&world.pool)
+ .await
+ .unwrap();
+ assert_eq!(stored_votes, 1, "one current vote per voter");
+
+ // A place, a contact card and an event.
+ let place = world
+ .customer
+ .send_message(
+ &world.group,
+ Message {
+ location_message: Some(Box::new(LocationMessage {
+ degrees_latitude: Some(52.52),
+ degrees_longitude: Some(13.405),
+ name: Some("Warehouse".to_owned()),
+ address: Some("Lagerstrasse 1, Berlin".to_owned()),
+ ..LocationMessage::default()
+ })),
+ ..Message::default()
+ },
+ )
+ .await
+ .unwrap();
+ let card = world
+ .customer
+ .send_message(
+ &world.group,
+ Message {
+ contact_message: Some(Box::new(ContactMessage {
+ display_name: Some("Support line".to_owned()),
+ vcard: Some(
+ "BEGIN:VCARD\nVERSION:3.0\nFN:Support line\nTEL;type=CELL;waid=15550100:+1 555 0100\nEND:VCARD"
+ .to_owned(),
+ ),
+ ..ContactMessage::default()
+ })),
+ ..Message::default()
+ },
+ )
+ .await
+ .unwrap();
+ let event = world
+ .customer
+ .send_message(
+ &world.group,
+ Message {
+ event_message: Some(Box::new(EventMessage {
+ name: Some("Pickup".to_owned()),
+ description: Some("Bring the label".to_owned()),
+ start_time: Some(1_798_790_400),
+ location: Some(Box::new(LocationMessage {
+ name: Some("Dock 4".to_owned()),
+ ..LocationMessage::default()
+ })),
+ ..EventMessage::default()
+ })),
+ ..Message::default()
+ },
+ )
+ .await
+ .unwrap();
+ for source in [&place, &card, &event] {
+ assert_eq!(world.timeline(&binding, source).await, [""]);
+ }
+ assert_eq!(
+ shown(&world, &binding, &place).await.rich,
+ Some(WhatsAppRichContent::Location(Location {
+ latitude: 52.52,
+ longitude: 13.405,
+ name: Some("Warehouse".to_owned()),
+ address: Some("Lagerstrasse 1, Berlin".to_owned()),
+ url: None,
+ live: false,
+ comment: None,
+ }))
+ );
+ assert_eq!(
+ shown(&world, &binding, &card).await.rich,
+ Some(WhatsAppRichContent::Contacts(Contacts {
+ contacts: vec![ContactCard {
+ name: "Support line".to_owned(),
+ phones: vec!["+1 555 0100".to_owned()],
+ }],
+ }))
+ );
+ assert_eq!(
+ shown(&world, &binding, &event).await.rich,
+ Some(WhatsAppRichContent::Event(Event {
+ name: "Pickup".to_owned(),
+ description: Some("Bring the label".to_owned()),
+ start_at: chrono::DateTime::from_timestamp(1_798_790_400, 0),
+ end_at: None,
+ location: Some("Dock 4".to_owned()),
+ join_link: None,
+ canceled: false,
+ }))
+ );
+
+ // Bob pins the opener; the customer's missed video call is logged.
+ let pin = world
+ .bob
+ .send_message(
+ &world.group,
+ Message {
+ pin_in_chat_message: Some(PinInChatMessage {
+ key: Some(MessageKey {
+ remote_jid: Some(world.group.to_string()),
+ from_me: Some(false),
+ id: Some(opener.clone()),
+ participant: Some(customer_lid.clone()),
+ }),
+ r#type: Some(pin_in_chat_message::Type::PinForAll as i32),
+ sender_timestamp_ms: Some(chrono::Utc::now().timestamp_millis()),
+ }),
+ ..Message::default()
+ },
+ )
+ .await
+ .unwrap();
+ let call = world
+ .customer
+ .send_message(
+ &world.group,
+ Message {
+ call_log_messsage: Some(CallLogMessage {
+ is_video: Some(true),
+ call_outcome: Some(call_log_message::CallOutcome::Missed as i32),
+ ..CallLogMessage::default()
+ }),
+ ..Message::default()
+ },
+ )
+ .await
+ .unwrap();
+ assert_eq!(world.timeline(&binding, &pin).await, [""]);
+ assert_eq!(world.timeline(&binding, &call).await, [""]);
+ let opener_shown = shown(&world, &binding, &opener).await;
+ let pin_shown = shown(&world, &binding, &pin).await;
+ assert_eq!(pin_shown.kind, WhatsAppMessageKind::System);
+ assert_eq!(pin_shown.reply_to, Some(opener_shown.id));
+ assert_eq!(
+ pin_shown.rich,
+ Some(WhatsAppRichContent::System(WhatsAppSystemLine {
+ action: SystemAction::MessagePinned,
+ actor: Some(bob.clone()),
+ participants: vec![],
+ value: None,
+ call: None,
+ }))
+ );
+ let call_shown = shown(&world, &binding, &call).await;
+ assert_eq!(call_shown.kind, WhatsAppMessageKind::System);
+ assert_eq!(
+ call_shown.rich,
+ Some(WhatsAppRichContent::System(WhatsAppSystemLine {
+ action: SystemAction::Call,
+ actor: Some(WhatsAppPersonRef {
+ name: "Customer".to_owned(),
+ user_id: None,
+ }),
+ participants: vec![],
+ value: None,
+ call: Some(Call {
+ video: true,
+ outcome: CallOutcome::Missed,
+ duration_seconds: None,
+ }),
+ }))
+ );
+ let kinds = sqlx::query!(
+ "SELECT source_id, kind FROM whatsapp_group_message WHERE binding_id = $1 AND source_id = ANY($2)",
+ binding.id,
+ &[pin.clone(), call.clone(), poll_id.clone()]
+ )
+ .fetch_all(&world.pool)
+ .await
+ .unwrap();
+ assert_eq!(kinds.len(), 3, "each lands once");
+ for row in kinds {
+ let expected = if row.source_id == poll_id {
+ "message"
+ } else {
+ "system"
+ };
+ assert_eq!(row.kind, expected, "{}", row.source_id);
+ }
+
+ // Alice mentions the customer from Macro.
+ let text = format!("@{} on it", world.customer.lid().user);
+ let send = world
+ .linked
+ .send_reply(
+ &receipt::(ALICE, binding.id),
+ &SendReply {
+ idempotency_key: Uuid::now_v7(),
+ message: ReplyContent::Text {
+ text: text.clone(),
+ mentions: vec![customer_lid.clone()],
+ },
+ reply_to: None,
+ },
+ SendAuthority::Human,
+ )
+ .await
+ .unwrap()
+ .id;
+ let received = world
+ .customer
+ .wait_for_message(TIMEOUT, |message| message.text() == Some(text.as_str()))
+ .await
+ .expect("the customer's phone receives the mention");
+ let context = received
+ .message
+ .extended_text_message
+ .as_ref()
+ .and_then(|extended| extended.context_info.as_deref())
+ .expect("a mention travels as extended text with context");
+ assert_eq!(context.mentioned_jid, [customer_lid]);
+ world
+ .wait_send(send, &["accepted", "sent", "delivered", "read"])
+ .await;
+
+ replica.drain(TIMEOUT).await.unwrap();
+}
+
+/// Records which messages the notification outbox publishes.
+#[derive(Default)]
+struct Published(Mutex>);
+
+#[async_trait]
+impl GroupPublisher for Published {
+ async fn publish(&self, effect: &GroupEffect) -> LinkedResult<()> {
+ self.0
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner)
+ .push(effect.message_id);
+ Ok(())
+ }
+}
+
+#[sqlx::test(migrator = "MACRO_DB_MIGRATIONS")]
+async fn group_changes_land_once_as_system_lines_that_never_notify(pool: PgPool) {
+ let world = World::new(pool).await;
+ let replica = world.replica().await;
+ world.pair(ALICE, &world.alice).await;
+ world.pair(BOB, &world.bob).await;
+ let binding = world.adopt(ALICE, &[BOB]).await;
+ world.observe(BOB, &binding).await;
+ let dana = world.whatsapp.add_user("15558880002", "Dana").await;
+
+ let announce = world
+ .customer
+ .send_text(&world.group, "adding Dana from the warehouse")
+ .await
+ .unwrap();
+ world
+ .whatsapp
+ .change_group(&world.group, Some(&world.customer), GroupChange::Add(&dana));
+ let hello = dana.send_text(&world.group, "hi, Dana here").await.unwrap();
+ world
+ .whatsapp
+ .change_group(&world.group, Some(&dana), GroupChange::Remove(&dana));
+ world.whatsapp.change_group(
+ &world.group,
+ Some(&world.customer),
+ GroupChange::Subject("Escalations, renamed".to_owned()),
+ );
+ let last = world
+ .customer
+ .send_text(&world.group, "Dana sorted it")
+ .await
+ .unwrap();
+ for (source, text) in [
+ (&announce, "adding Dana from the warehouse"),
+ (&hello, "hi, Dana here"),
+ (&last, "Dana sorted it"),
+ ] {
+ assert_eq!(world.timeline(&binding, source).await, [text]);
+ }
+ // Both accounts have seen everything up to the last message.
+ let observed = wait_for_rows(&world.pool, TIMEOUT, || async {
+ (observations(&world.pool, binding.id, &last).await == 2).then_some(())
+ })
+ .await;
+ world
+ .expect(observed, "both accounts observe the last message")
+ .await;
+
+ let lines = wait_for_rows(&world.pool, TIMEOUT, || async {
+ let lines = sqlx::query!(
+ r#"SELECT message_id AS "message_id!", source_id, rich->>'action' AS "action!" FROM whatsapp_group_message
+ WHERE binding_id = $1 AND kind = 'system' AND message_id IS NOT NULL ORDER BY source_at, rich->>'action'"#,
+ binding.id
+ )
+ .fetch_all(&world.pool)
+ .await
+ .unwrap();
+ (lines.len() >= 3).then_some(lines)
+ })
+ .await;
+ let lines = world
+ .expect(lines, "the three group changes become system lines")
+ .await;
+ let mut actions: Vec<&str> = lines.iter().map(|line| line.action.as_str()).collect();
+ actions.sort_unstable();
+ assert_eq!(
+ actions,
+ ["participants_added", "participants_left", "subject_changed"],
+ "each change lands once though two accounts saw it: {:?}",
+ lines
+ .iter()
+ .map(|line| (&line.action, &line.source_id))
+ .collect::>()
+ );
+
+ let page = world
+ .linked
+ .messages(&receipt::(ALICE, binding.id), None, 100)
+ .await
+ .unwrap()
+ .messages;
+ let customer = WhatsAppPersonRef {
+ name: "Customer".to_owned(),
+ user_id: None,
+ };
+ let dana_ref = WhatsAppPersonRef {
+ name: "Dana".to_owned(),
+ user_id: None,
+ };
+ let system: Vec = page
+ .iter()
+ .filter(|message| message.kind == WhatsAppMessageKind::System)
+ .filter_map(|message| match &message.rich {
+ Some(WhatsAppRichContent::System(line)) => Some(line.clone()),
+ _ => None,
+ })
+ .collect();
+ let line = |action: SystemAction| {
+ system
+ .iter()
+ .find(|line| line.action == action)
+ .cloned()
+ .unwrap_or_else(|| panic!("a {action:?} line in {system:?}"))
+ };
+ assert_eq!(
+ line(SystemAction::ParticipantsAdded),
+ WhatsAppSystemLine {
+ action: SystemAction::ParticipantsAdded,
+ actor: Some(customer.clone()),
+ participants: vec![dana_ref.clone()],
+ value: None,
+ call: None,
+ }
+ );
+ assert_eq!(
+ line(SystemAction::ParticipantsLeft),
+ WhatsAppSystemLine {
+ action: SystemAction::ParticipantsLeft,
+ actor: Some(dana_ref.clone()),
+ participants: vec![dana_ref.clone()],
+ value: None,
+ call: None,
+ }
+ );
+ assert_eq!(
+ line(SystemAction::SubjectChanged),
+ WhatsAppSystemLine {
+ action: SystemAction::SubjectChanged,
+ actor: Some(customer.clone()),
+ participants: vec![],
+ value: Some("Escalations, renamed".to_owned()),
+ call: None,
+ }
+ );
+
+ // Only the three texts are unread, and only they notify.
+ let summary = world
+ .linked
+ .adopted_groups(ALICE)
+ .await
+ .unwrap()
+ .into_iter()
+ .find(|group| group.id == binding.id)
+ .unwrap();
+ assert_eq!(summary.unread_count, 3, "system lines are never unread");
+ let published = Arc::new(Published::default());
+ let effects = EffectsService::new(
+ Arc::new(PgLinkedRepository::new(world.pool.clone())),
+ published.clone(),
+ );
+ while effects.publish_pending().await.unwrap() > 0 {}
+ let mut notified = published
+ .0
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner)
+ .clone();
+ notified.sort_unstable();
+ let texts = sqlx::query_scalar!(
+ r#"SELECT message_id AS "message_id!" FROM whatsapp_group_message
+ WHERE binding_id = $1 AND source_id = ANY($2) ORDER BY message_id"#,
+ binding.id,
+ &[announce.clone(), hello.clone(), last.clone()]
+ )
+ .fetch_all(&world.pool)
+ .await
+ .unwrap();
+ assert_eq!(notified, texts, "only the texts notify");
+ for line in &lines {
+ assert!(
+ !notified.contains(&line.message_id),
+ "{} notified",
+ line.source_id
+ );
+ }
+
+ replica.drain(TIMEOUT).await.unwrap();
+}
diff --git a/crates/whatsapp_e2e/src/watch_tables.sql b/crates/whatsapp_e2e/src/watch_tables.sql
index 5e7a6b76959..717f89da248 100644
--- a/crates/whatsapp_e2e/src/watch_tables.sql
+++ b/crates/whatsapp_e2e/src/watch_tables.sql
@@ -28,3 +28,5 @@ CREATE TRIGGER e2e_changed AFTER INSERT OR UPDATE OR DELETE ON whatsapp_group_at
FOR EACH STATEMENT EXECUTE FUNCTION e2e_notify_changed();
CREATE TRIGGER e2e_changed AFTER INSERT OR UPDATE OR DELETE ON whatsapp_chat
FOR EACH STATEMENT EXECUTE FUNCTION e2e_notify_changed();
+CREATE TRIGGER e2e_changed AFTER INSERT OR UPDATE OR DELETE ON whatsapp_group_poll_vote
+ FOR EACH STATEMENT EXECUTE FUNCTION e2e_notify_changed();
diff --git a/crates/whatsapp_fake_server/AGENTS.md b/crates/whatsapp_fake_server/AGENTS.md
index ac4f847e3e7..574664ab5ed 100644
--- a/crates/whatsapp_fake_server/AGENTS.md
+++ b/crates/whatsapp_fake_server/AGENTS.md
@@ -26,9 +26,13 @@ Lets the session service and its tests run end to end without WhatsApp.
## Start here
-- `src/lib.rs`: the `FakeWhatsApp` API (users, groups and their renames and
- removals as `w:gp2` notifications, kick, drop connection,
+- `src/lib.rs`: the `FakeWhatsApp` API (users, groups, kick, drop connection,
delivery counters, waiters).
+- `src/group_change.rs`: `change_group` adds, removes (a user removing
+ themselves left), renames, describes, promotes and demotes, and sends the
+ `w:gp2` notification (one id and `t` for every device) naming who did it as
+ `participant` / `participant_pn`; `rename_group` / `remove_from_group` name
+ no one, as changes WhatsApp makes.
- `src/state.rs`: routing, IQ handlers (encrypt, usync, w:g2, w:m), offline queues.
- `src/phone.rs`: fake phones: send/receive with real Signal, QR approval.
Phones remember chat messages and answer their linked devices' peer
@@ -36,6 +40,11 @@ Lets the session service and its tests run end to end without WhatsApp.
(`answer_history_requests(false)` is an offline phone).
`send_history_sync` / `chat_list()` send history-sync bundles inline.
`FakeWhatsApp::address_by_lid_only` drops `participant_pn` in a group.
+- `src/phone/content.rs`: `upload_media` (any attachment kind; build the
+ sticker, GIF, voice note or view-once message around the response and
+ `send_message` it) and `vote` (a poll vote encrypted with the poll's
+ message secret for the voter's LID; take the secret from the poll's
+ `message_context_info`, e.g. `whatsmeow::msgsecret::build_poll_creation`).
- `src/connection.rs`: handshake, login, `conflict replaced`, pairing.
## Checks
@@ -51,6 +60,8 @@ Lets the session service and its tests run end to end without WhatsApp.
(plain HTTP); phones upload and download with whatsmeow's own media crypto.
- Unhandled IQs get an empty `result`; add a handler in `State::handle_iq` when
the port starts parsing a response.
+- whatsmeow drops a `w:gp2` notification whose change element lacks a
+ required attribute (a `` needs `s_t`), with only an error log.
## Related
diff --git a/crates/whatsapp_fake_server/src/group_change.rs b/crates/whatsapp_fake_server/src/group_change.rs
new file mode 100644
index 00000000000..fbdbdb7069a
--- /dev/null
+++ b/crates/whatsapp_fake_server/src/group_change.rs
@@ -0,0 +1,158 @@
+//! Changes to a group's members and metadata, and the `w:gp2` notifications
+//! WhatsApp sends its members' devices about them.
+
+use whatsmeow::binary::Node;
+use whatsmeow::types::Jid;
+
+use crate::stanza::{bytes, children, jid, node, now, random_id, text};
+use crate::state::{DeviceAddress, State, device_jid};
+use crate::{FakePhone, FakeWhatsApp};
+
+/// One change to a group.
+pub enum GroupChange<'phone> {
+ /// Adds a user to the group.
+ Add(&'phone FakePhone),
+ /// Removes a user from the group; a user removing themselves left.
+ Remove(&'phone FakePhone),
+ /// Sets the subject.
+ Subject(String),
+ /// Sets the description.
+ Description(String),
+ /// Makes a member an admin.
+ Promote(&'phone FakePhone),
+ /// Removes a member as admin.
+ Demote(&'phone FakePhone),
+}
+
+impl FakeWhatsApp {
+ /// Applies a change to a group and notifies the members' devices (and an
+ /// added or removed user's). `by` is who made it, named on the
+ /// notification as WhatsApp does; `None` for a change WhatsApp made.
+ pub fn change_group(&self, group: &Jid, by: Option<&FakePhone>, change: GroupChange<'_>) {
+ let mut state = self.shared.lock();
+ let Some(stored) = state.groups.get(&group.user) else {
+ return;
+ };
+ let mut notified = stored.members.clone();
+ let mut members = stored.members.clone();
+ let mut admins = stored.admins.clone();
+ let mut subject = stored.subject.clone();
+ let mut description = stored.description.clone();
+ let actor = by
+ .and_then(|phone| state.users.get(phone.phone()))
+ .map(|user| {
+ (
+ device_jid(&user.lid, 0, true),
+ device_jid(&user.phone, 0, false),
+ )
+ });
+ let at = now();
+ let change = match change {
+ GroupChange::Add(phone) => {
+ if !members.iter().any(|member| member == phone.phone()) {
+ members.push(phone.phone().to_owned());
+ notified.push(phone.phone().to_owned());
+ }
+ participants_node("add", &state, phone)
+ }
+ GroupChange::Remove(phone) => {
+ members.retain(|member| member != phone.phone());
+ admins.retain(|admin| admin != phone.phone());
+ participants_node("remove", &state, phone)
+ }
+ GroupChange::Subject(new_subject) => {
+ let mut attrs = vec![("subject", text(&new_subject)), ("s_t", at.clone())];
+ if let Some((lid, phone)) = &actor {
+ attrs.push(("s_o", jid(lid)));
+ attrs.push(("s_o_pn", jid(phone)));
+ }
+ subject = new_subject;
+ node("subject", attrs, None)
+ }
+ GroupChange::Description(new_description) => {
+ let change = node(
+ "description",
+ vec![("id", text(random_id()))],
+ children(vec![node(
+ "body",
+ vec![],
+ bytes(new_description.as_bytes()),
+ )]),
+ );
+ description = Some(new_description);
+ change
+ }
+ GroupChange::Promote(phone) => {
+ admins.push(phone.phone().to_owned());
+ participants_node("promote", &state, phone)
+ }
+ GroupChange::Demote(phone) => {
+ admins.retain(|admin| admin != phone.phone());
+ participants_node("demote", &state, phone)
+ }
+ };
+ let mut attrs = vec![
+ ("from", jid(group)),
+ ("id", text(random_id())),
+ ("t", at),
+ ("type", text("w:gp2")),
+ ];
+ if let Some((lid, phone)) = &actor {
+ attrs.push(("participant", jid(lid)));
+ attrs.push(("participant_pn", jid(phone)));
+ }
+ let notification = node("notification", attrs, children(vec![change]));
+ if let Some(stored) = state.groups.get_mut(&group.user) {
+ stored.members = members;
+ stored.admins = admins;
+ stored.subject = subject;
+ stored.description = description;
+ }
+ let targets: Vec = notified
+ .iter()
+ .filter_map(|member| state.users.get(member))
+ .flat_map(|user| {
+ user.devices.keys().map(|device| DeviceAddress {
+ phone: user.phone.clone(),
+ device: *device,
+ })
+ })
+ .collect();
+ for target in targets {
+ state.deliver(&target, notification.clone());
+ }
+ drop(state);
+ self.shared.changed();
+ }
+
+ /// Renames a group, as WhatsApp does without naming who.
+ pub fn rename_group(&self, group: &Jid, subject: &str) {
+ self.change_group(group, None, GroupChange::Subject(subject.to_owned()));
+ }
+
+ /// Removes a phone's user from a group, as WhatsApp does without naming
+ /// who.
+ pub fn remove_from_group(&self, group: &Jid, phone: &FakePhone) {
+ self.change_group(group, None, GroupChange::Remove(phone));
+ }
+}
+
+/// ` ` for one user.
+fn participants_node(tag: &str, state: &State, phone: &FakePhone) -> Node {
+ let participant = state
+ .users
+ .get(phone.phone())
+ .map(|user| {
+ node(
+ "participant",
+ vec![
+ ("jid", jid(&device_jid(&user.lid, 0, true))),
+ ("phone_number", jid(&device_jid(&user.phone, 0, false))),
+ ],
+ None,
+ )
+ })
+ .into_iter()
+ .collect();
+ node(tag, vec![], children(participant))
+}
diff --git a/crates/whatsapp_fake_server/src/lib.rs b/crates/whatsapp_fake_server/src/lib.rs
index c11a54b4776..35d0d89a019 100644
--- a/crates/whatsapp_fake_server/src/lib.rs
+++ b/crates/whatsapp_fake_server/src/lib.rs
@@ -14,6 +14,7 @@
#![deny(missing_docs)]
mod connection;
+mod group_change;
mod media;
mod phone;
mod signal;
@@ -36,6 +37,7 @@ use whatsmeow::store::{MemoryStore, StoreError};
use whatsmeow::types::Jid;
use whatsmeow_signal::LibsignalCurveSignatures;
+pub use group_change::GroupChange;
pub use phone::{FakePhone, ReceivedMessage, ReceivedReceipt};
pub use signal::{TestSignal, test_root_key, test_signal};
@@ -90,6 +92,9 @@ pub enum SendError {
/// Uploading an attachment to the media host failed.
#[error("media upload: {0}")]
Upload(#[source] whatsmeow::upload::UploadError),
+ /// Encrypting with an earlier message's secret (a poll vote) failed.
+ #[error("message secret: {0}")]
+ MessageSecret(#[source] whatsmeow::errors::ClientError),
}
impl SendError {
@@ -344,6 +349,8 @@ impl FakeWhatsApp {
.iter()
.map(|phone| phone.phone().to_owned())
.collect(),
+ admins: Vec::new(),
+ description: None,
};
let group_node = state.group_node(&group);
let creator = state
@@ -396,46 +403,6 @@ impl FakeWhatsApp {
.insert(group.user.clone());
}
- /// Renames a group and notifies its members' linked devices.
- pub fn rename_group(&self, group: &Jid, subject: &str) {
- let mut state = self.shared.lock();
- let Some(stored) = state.groups.get_mut(&group.user) else {
- return;
- };
- stored.subject = subject.to_owned();
- let members = stored.members.clone();
- let change = node("subject", vec![("subject", text(subject))], None);
- notify_group(&mut state, group, &members, change);
- drop(state);
- self.shared.changed();
- }
-
- /// Removes a phone's user from a group and notifies the remaining
- /// members' and the removed user's linked devices.
- pub fn remove_from_group(&self, group: &Jid, phone: &FakePhone) {
- let mut state = self.shared.lock();
- let Some(stored) = state.groups.get_mut(&group.user) else {
- return;
- };
- let notified = stored.members.clone();
- stored.members.retain(|member| member != phone.phone());
- let Some(lid) = state.users.get(phone.phone()).map(|user| user.lid.clone()) else {
- return;
- };
- let change = node(
- "remove",
- vec![],
- children(vec![node(
- "participant",
- vec![("jid", jid(&device_jid(&lid, 0, true)))],
- None,
- )]),
- );
- notify_group(&mut state, group, ¬ified, change);
- drop(state);
- self.shared.changed();
- }
-
/// The linked devices of a user, as phone-number device JIDs.
pub fn linked_devices(&self, phone: &FakePhone) -> Vec {
let state = self.shared.lock();
@@ -629,36 +596,3 @@ impl FakeWhatsApp {
self.shared.tasks.wait().await;
}
}
-
-/// Delivers a `w:gp2` change notification about `group` to every device of
-/// the given members.
-fn notify_group(
- state: &mut State,
- group: &Jid,
- members: &[String],
- change: whatsmeow::binary::Node,
-) {
- let targets: Vec = members
- .iter()
- .filter_map(|member| state.users.get(member))
- .flat_map(|user| {
- user.devices.keys().map(|device| DeviceAddress {
- phone: user.phone.clone(),
- device: *device,
- })
- })
- .collect();
- for target in targets {
- let notification = node(
- "notification",
- vec![
- ("from", jid(group)),
- ("id", text(random_id())),
- ("t", now()),
- ("type", text("w:gp2")),
- ],
- children(vec![change.clone()]),
- );
- state.deliver(&target, notification);
- }
-}
diff --git a/crates/whatsapp_fake_server/src/phone.rs b/crates/whatsapp_fake_server/src/phone.rs
index dd366e4ceee..c252f4eebed 100644
--- a/crates/whatsapp_fake_server/src/phone.rs
+++ b/crates/whatsapp_fake_server/src/phone.rs
@@ -43,7 +43,6 @@ use whatsmeow::signal::{
};
use whatsmeow::store::Device;
use whatsmeow::types::{GROUP_SERVER, Jid, SERVER_JID};
-use whatsmeow::upload::upload;
use whatsmeow_signal::{LibsignalCurveSignatures, LibsignalProtocol};
use crate::connection::adv_hmac;
@@ -53,6 +52,8 @@ use crate::stanza::{
use crate::state::{DeviceAddress, PendingPair, device_jid, group_jid};
use crate::{PairError, SendError, Shared};
+mod content;
+
/// How long a phone waits for the client to sign a pair-success.
const PAIR_CONFIRMATION_TIMEOUT: Duration = Duration::from_secs(30);
@@ -244,14 +245,7 @@ impl FakePhone {
image: &[u8],
caption: &str,
) -> Result {
- let uploaded = upload(
- &self.media_http(),
- &self.media_conn(),
- image,
- MediaType::Image,
- )
- .await
- .map_err(SendError::Upload)?;
+ let uploaded = self.upload_media(image, MediaType::Image).await?;
let message = Message {
image_message: Some(Box::new(ImageMessage {
url: Some(uploaded.url),
@@ -1091,6 +1085,8 @@ impl FakePhone {
fn message_type(message: &Message) -> &'static str {
if message.reaction_message.is_some() {
"reaction"
+ } else if message.poll_creation_message.is_some() || message.poll_update_message.is_some() {
+ "poll"
} else if message.conversation.is_some() || message.extended_text_message.is_some() {
"text"
} else {
diff --git a/crates/whatsapp_fake_server/src/phone/content.rs b/crates/whatsapp_fake_server/src/phone/content.rs
new file mode 100644
index 00000000000..67865334056
--- /dev/null
+++ b/crates/whatsapp_fake_server/src/phone/content.rs
@@ -0,0 +1,79 @@
+//! What a phone sends besides plain messages: attachments uploaded to the
+//! media host, and poll votes encrypted with the poll's message secret.
+
+use prost::Message as _;
+use whatsmeow::download::MediaType;
+use whatsmeow::msgsecret::{MsgSecretType, encrypt_msg_secret_with, hash_poll_options};
+use whatsmeow::proto::wa_common::MessageKey;
+use whatsmeow::proto::wa_e2e::{Message, PollEncValue, PollUpdateMessage, PollVoteMessage};
+use whatsmeow::types::Jid;
+use whatsmeow::upload::{UploadResponse, upload};
+
+use super::FakePhone;
+use crate::SendError;
+
+impl FakePhone {
+ /// Encrypts an attachment as `media_type` and uploads it to the media
+ /// host; the response fills the attachment part of a message.
+ pub async fn upload_media(
+ &self,
+ plaintext: &[u8],
+ media_type: MediaType,
+ ) -> Result {
+ upload(
+ &self.media_http(),
+ &self.media_conn(),
+ plaintext,
+ media_type,
+ )
+ .await
+ .map_err(SendError::Upload)
+ }
+
+ /// Votes on a poll in a group, choosing `options` (none withdraws the
+ /// vote), as WhatsApp does: the vote is encrypted with the poll's message
+ /// secret for this voter. `poll_author` is the poll sender's LID.
+ /// Returns the vote's message ID.
+ pub async fn vote(
+ &self,
+ chat: &Jid,
+ poll_id: &str,
+ poll_author: &Jid,
+ poll_secret: &[u8],
+ options: &[&str],
+ ) -> Result {
+ let names: Vec = options.iter().map(|option| (*option).to_owned()).collect();
+ let vote = PollVoteMessage {
+ selected_options: hash_poll_options(&names),
+ };
+ let iv: [u8; 12] = rand::random();
+ let ciphertext = encrypt_msg_secret_with(
+ &self.lid(),
+ poll_author,
+ poll_id,
+ MsgSecretType::PollVote,
+ poll_secret,
+ &iv,
+ &vote.encode_to_vec(),
+ )
+ .map_err(SendError::MessageSecret)?;
+ let update = Message {
+ poll_update_message: Some(PollUpdateMessage {
+ poll_creation_message_key: Some(MessageKey {
+ remote_jid: Some(chat.to_string()),
+ from_me: Some(false),
+ id: Some(poll_id.to_owned()),
+ participant: Some(poll_author.to_non_ad().to_string()),
+ }),
+ vote: Some(PollEncValue {
+ enc_payload: Some(ciphertext),
+ enc_iv: Some(iv.to_vec()),
+ }),
+ sender_timestamp_ms: Some(chrono::Utc::now().timestamp_millis()),
+ ..PollUpdateMessage::default()
+ }),
+ ..Message::default()
+ };
+ self.send_message(chat, update).await
+ }
+}
diff --git a/crates/whatsapp_fake_server/src/state.rs b/crates/whatsapp_fake_server/src/state.rs
index 6df3f09255f..ea99411fd90 100644
--- a/crates/whatsapp_fake_server/src/state.rs
+++ b/crates/whatsapp_fake_server/src/state.rs
@@ -75,6 +75,10 @@ pub(crate) struct Group {
pub(crate) created: u64,
/// Members by phone number, in join order.
pub(crate) members: Vec,
+ /// Members made admins (the creator is always the super admin).
+ pub(crate) admins: Vec,
+ /// The group description, if set.
+ pub(crate) description: Option,
}
/// A connection that is pairing a new device.
@@ -274,10 +278,23 @@ impl State {
];
if user.phone == group.creator {
attrs.push(("type", text("superadmin")));
+ } else if group.admins.contains(&user.phone) {
+ attrs.push(("type", text("admin")));
}
node("participant", attrs, None)
})
- .collect();
+ .collect::>();
+ let description = group.description.iter().map(|description| {
+ node(
+ "description",
+ vec![
+ ("id", text(format!("{:X}", group.created))),
+ ("t", text(group.created.to_string())),
+ ],
+ children(vec![node("body", vec![], bytes(description.as_bytes()))]),
+ )
+ });
+ let group_children = participants.into_iter().chain(description).collect();
let creator_lid = self
.users
.get(&group.creator)
@@ -296,7 +313,7 @@ impl State {
("addressing_mode", text("lid")),
("size", text(group.members.len().to_string())),
],
- children(participants),
+ children(group_children),
)
}
diff --git a/crates/whatsapp_fake_server/src/test.rs b/crates/whatsapp_fake_server/src/test.rs
index 339fe499617..2a069179f2a 100644
--- a/crates/whatsapp_fake_server/src/test.rs
+++ b/crates/whatsapp_fake_server/src/test.rs
@@ -398,3 +398,263 @@ async fn images_travel_both_ways_through_the_media_host() {
.expect("bob's phone receives the image");
assert_eq!(bob.download_image(&received).await.unwrap(), reply);
}
+
+#[tokio::test]
+async fn group_changes_name_who_made_them() {
+ let server = FakeWhatsApp::start().await.unwrap();
+ let alice = server.add_user("15550000001", "Alice").await;
+ let bob = server.add_user("15550000002", "Bob").await;
+ let carol = server.add_user("15550000003", "Carol").await;
+ let dana = server.add_user("15550000004", "Dana").await;
+ let mut linked = link(&server, &alice).await;
+ let group = server.create_group("Team", &[&bob, &alice, &carol]);
+
+ server.change_group(&group, Some(&bob), GroupChange::Add(&dana));
+ let Event::GroupInfo(added) = next_event(
+ &mut linked.events,
+ |event| matches!(event, Event::GroupInfo(change) if !change.join.is_empty()),
+ )
+ .await
+ else {
+ unreachable!("matched a group change");
+ };
+ assert_eq!(added.jid, group);
+ assert_eq!(added.sender, Some(bob.lid()));
+ assert_eq!(added.sender_pn, Some(bob.jid()));
+ assert_eq!(added.join, vec![dana.lid()]);
+ dana.send_text(&group, "hi, Dana here")
+ .await
+ .expect("an added member can send to the group");
+ next_text(&mut linked.events, "hi, Dana here").await;
+
+ server.change_group(&group, Some(&dana), GroupChange::Remove(&dana));
+ let Event::GroupInfo(left) = next_event(
+ &mut linked.events,
+ |event| matches!(event, Event::GroupInfo(change) if !change.leave.is_empty()),
+ )
+ .await
+ else {
+ unreachable!("matched a group change");
+ };
+ assert_eq!(left.sender, Some(dana.lid()));
+ assert_eq!(left.leave, vec![dana.lid()]);
+
+ server.change_group(
+ &group,
+ Some(&carol),
+ GroupChange::Subject("Team, renamed".to_owned()),
+ );
+ let Event::GroupInfo(renamed) = next_event(
+ &mut linked.events,
+ |event| matches!(event, Event::GroupInfo(change) if change.name.is_some()),
+ )
+ .await
+ else {
+ unreachable!("matched a group change");
+ };
+ assert_eq!(renamed.sender, Some(carol.lid()));
+ assert_eq!(renamed.name.unwrap().name, "Team, renamed");
+
+ server.change_group(
+ &group,
+ Some(&bob),
+ GroupChange::Description("Where the team talks".to_owned()),
+ );
+ let Event::GroupInfo(described) = next_event(
+ &mut linked.events,
+ |event| matches!(event, Event::GroupInfo(change) if change.topic.is_some()),
+ )
+ .await
+ else {
+ unreachable!("matched a group change");
+ };
+ assert_eq!(described.topic.unwrap().topic, "Where the team talks");
+
+ server.change_group(&group, Some(&bob), GroupChange::Promote(&carol));
+ let Event::GroupInfo(promoted) = next_event(
+ &mut linked.events,
+ |event| matches!(event, Event::GroupInfo(change) if !change.promote.is_empty()),
+ )
+ .await
+ else {
+ unreachable!("matched a group change");
+ };
+ assert_eq!(promoted.promote, vec![carol.lid()]);
+
+ let info = linked.client.get_group_info(&group).await.unwrap();
+ assert_eq!(info.name.name, "Team, renamed");
+ assert_eq!(info.topic.topic, "Where the team talks");
+ let members: Vec<(String, bool)> = info
+ .participants
+ .iter()
+ .map(|participant| (participant.jid.user.clone(), participant.is_admin))
+ .collect();
+ assert_eq!(
+ members,
+ vec![
+ (bob.lid().user, true),
+ (alice.lid().user, false),
+ (carol.lid().user, true),
+ ]
+ );
+
+ server.change_group(&group, Some(&bob), GroupChange::Demote(&carol));
+ let Event::GroupInfo(demoted) = next_event(
+ &mut linked.events,
+ |event| matches!(event, Event::GroupInfo(change) if !change.demote.is_empty()),
+ )
+ .await
+ else {
+ unreachable!("matched a group change");
+ };
+ assert_eq!(demoted.demote, vec![carol.lid()]);
+
+ // A change WhatsApp makes names no one.
+ server.rename_group(&group, "Team, again");
+ let Event::GroupInfo(anonymous) = next_event(
+ &mut linked.events,
+ |event| matches!(event, Event::GroupInfo(change) if change.name.is_some()),
+ )
+ .await
+ else {
+ unreachable!("matched a group change");
+ };
+ assert_eq!(anonymous.sender, None);
+}
+
+#[tokio::test]
+async fn a_poll_vote_decrypts_with_the_polls_secret() {
+ let server = FakeWhatsApp::start().await.unwrap();
+ let alice = server.add_user("15550000001", "Alice").await;
+ let bob = server.add_user("15550000002", "Bob").await;
+ let carol = server.add_user("15550000003", "Carol").await;
+ let mut linked = link(&server, &alice).await;
+ let group = server.create_group("Lunch", &[&bob, &alice, &carol]);
+
+ let poll = whatsmeow::msgsecret::build_poll_creation(
+ "Where to?",
+ &["Pizza".to_owned(), "Sushi".to_owned()],
+ 1,
+ );
+ let secret = poll
+ .message_context_info
+ .as_ref()
+ .and_then(|context| context.message_secret.clone())
+ .expect("a poll carries its message secret");
+ let poll_id = bob.send_message(&group, poll).await.unwrap();
+ next_event(&mut linked.events, |event| {
+ matches!(event, Event::Message(message) if message.info.id == poll_id
+ && message.message.as_ref().is_some_and(|message| message.poll_creation_message.is_some()))
+ })
+ .await;
+
+ let vote_id = carol
+ .vote(&group, &poll_id, &bob.lid(), &secret, &["Sushi"])
+ .await
+ .unwrap();
+ let Event::Message(vote) = next_event(&mut linked.events, |event| {
+ matches!(event, Event::Message(message) if message.info.id == vote_id
+ && message.message.as_ref().is_some_and(|message| message.poll_update_message.is_some()))
+ })
+ .await
+ else {
+ unreachable!("matched a message event");
+ };
+ let decrypted = linked.client.decrypt_poll_vote(&vote).await.unwrap();
+ assert_eq!(
+ decrypted.selected_options,
+ whatsmeow::msgsecret::hash_poll_options(&["Sushi".to_owned()])
+ );
+}
+
+#[tokio::test]
+async fn stickers_and_voice_notes_download_like_images() {
+ let server = FakeWhatsApp::start().await.unwrap();
+ let alice = server.add_user("15550000001", "Alice").await;
+ let bob = server.add_user("15550000002", "Bob").await;
+ let mut linked = link(&server, &alice).await;
+ let group = server.create_group("Media", &[&alice, &bob]);
+
+ let sticker = b"webp sticker frames".repeat(30);
+ let uploaded = bob
+ .upload_media(&sticker, whatsmeow::download::MediaType::Image)
+ .await
+ .unwrap();
+ let sent = bob
+ .send_message(
+ &group,
+ whatsmeow::proto::wa_e2e::Message {
+ sticker_message: Some(Box::new(whatsmeow::proto::wa_e2e::StickerMessage {
+ url: Some(uploaded.url),
+ direct_path: Some(uploaded.direct_path),
+ media_key: Some(uploaded.media_key),
+ file_enc_sha256: Some(uploaded.file_enc_sha256),
+ file_sha256: Some(uploaded.file_sha256),
+ file_length: Some(uploaded.file_length),
+ mimetype: Some("image/webp".to_owned()),
+ width: Some(512),
+ height: Some(512),
+ ..Default::default()
+ })),
+ ..Default::default()
+ },
+ )
+ .await
+ .unwrap();
+ let Event::Message(message) = next_event(&mut linked.events, |event| {
+ matches!(event, Event::Message(message) if message.info.id == sent
+ && message.message.as_ref().is_some_and(|message| message.sticker_message.is_some()))
+ })
+ .await
+ else {
+ unreachable!("matched a message event");
+ };
+ let part = message
+ .message
+ .as_ref()
+ .and_then(|message| message.sticker_message.as_deref())
+ .unwrap();
+ assert_eq!(linked.client.download(part).await.unwrap(), sticker);
+
+ let voice = b"opus voice note".repeat(30);
+ let uploaded = bob
+ .upload_media(&voice, whatsmeow::download::MediaType::Audio)
+ .await
+ .unwrap();
+ let sent = bob
+ .send_message(
+ &group,
+ whatsmeow::proto::wa_e2e::Message {
+ audio_message: Some(Box::new(whatsmeow::proto::wa_e2e::AudioMessage {
+ url: Some(uploaded.url),
+ direct_path: Some(uploaded.direct_path),
+ media_key: Some(uploaded.media_key),
+ file_enc_sha256: Some(uploaded.file_enc_sha256),
+ file_sha256: Some(uploaded.file_sha256),
+ file_length: Some(uploaded.file_length),
+ mimetype: Some("audio/ogg; codecs=opus".to_owned()),
+ ptt: Some(true),
+ seconds: Some(4),
+ ..Default::default()
+ })),
+ ..Default::default()
+ },
+ )
+ .await
+ .unwrap();
+ let Event::Message(message) = next_event(&mut linked.events, |event| {
+ matches!(event, Event::Message(message) if message.info.id == sent
+ && message.message.as_ref().is_some_and(|message| message.audio_message.is_some()))
+ })
+ .await
+ else {
+ unreachable!("matched a message event");
+ };
+ let part = message
+ .message
+ .as_ref()
+ .and_then(|message| message.audio_message.as_deref())
+ .unwrap();
+ assert_eq!(part.ptt, Some(true));
+ assert_eq!(linked.client.download(part).await.unwrap(), voice);
+}
diff --git a/crates/whatsapp_linked/AGENTS.md b/crates/whatsapp_linked/AGENTS.md
index 2fa15d2b26a..4ac50071598 100644
--- a/crates/whatsapp_linked/AGENTS.md
+++ b/crates/whatsapp_linked/AGENTS.md
@@ -21,6 +21,14 @@ rows and `NOTIFY` (the `outbound::pg` notify helper).
first (`resolve_sender_in`): WhatsApp's from-me flag means the account's own
identity; a LID-only sender takes its phone number from the account's roster
pair. Only what is still LID-only is quarantined.
+- `src/domain/rich.rs`: stored structured content (`RichContent`: poll, poll
+ vote, location, contact cards, event, system line) in
+ `whatsapp_group_message.rich`; `kind = 'system'` rows never notify or count
+ as unread. `src/domain/timeline/view.rs` resolves ids in mentions, system
+ lines and votes to names and tallies polls. Poll votes are stored per voter
+ in `whatsapp_group_poll_vote` through the reaction path of `reserve_in`. A
+ re-ingested message fills in `rich`/`mentions`/`forwarded` an earlier copy
+ lacked (re-importing history upgrades old placeholders).
- `src/outbound/pg/chats.rs`: the account's chat list (`whatsapp_chat`,
`GET /chats` via `LinkedService::chats`) and importing earlier messages of
added groups (`backfill_*` columns on `whatsapp_group`; limits
@@ -66,8 +74,9 @@ rows and `NOTIFY` (the `outbound::pg` notify helper).
- `src/domain/timeline.rs` and `src/domain/service/timeline.rs` own list/detail,
timeline, unread and sharing use cases. Group HTTP paths are
`/meta/linked/whatsapp-groups/{group_id}`. Soup/search are deferred.
-- Store messages/reactions/attachments in `whatsapp_group_message`,
- `whatsapp_group_reaction`, and `whatsapp_group_attachment`; never native comms tables.
+- Store messages/reactions/attachments/poll votes in `whatsapp_group_message`,
+ `whatsapp_group_reaction`, `whatsapp_group_attachment` and
+ `whatsapp_group_poll_vote`; never native comms tables.
- Attachment bytes cross Postgres (`whatsapp_media_blob`, ≤ 32 MiB) because the
session service has no document storage; DSS imports and deletes them.
`events::Media::locator` is sealed by the session service; never read it.
diff --git a/crates/whatsapp_linked/Cargo.toml b/crates/whatsapp_linked/Cargo.toml
index 45124b21b9a..11872a45f4e 100644
--- a/crates/whatsapp_linked/Cargo.toml
+++ b/crates/whatsapp_linked/Cargo.toml
@@ -59,6 +59,7 @@ serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
sqlx = { workspace = true, optional = true }
+strum = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-opentelemetry = { workspace = true, optional = true }
diff --git a/crates/whatsapp_linked/src/domain.rs b/crates/whatsapp_linked/src/domain.rs
index cf584f1ba88..5736903a7e4 100644
--- a/crates/whatsapp_linked/src/domain.rs
+++ b/crates/whatsapp_linked/src/domain.rs
@@ -2,6 +2,8 @@
pub mod events;
/// Account, binding and send models shared with the web app.
pub mod models;
+/// Polls, locations, contact cards, events and system lines.
+pub mod rich;
/// Native notification outbox.
#[cfg(feature = "ports")]
diff --git a/crates/whatsapp_linked/src/domain/events.rs b/crates/whatsapp_linked/src/domain/events.rs
index 8f6e7a55eae..51f5a35520e 100644
--- a/crates/whatsapp_linked/src/domain/events.rs
+++ b/crates/whatsapp_linked/src/domain/events.rs
@@ -1,7 +1,8 @@
//! Types exchanged in-process with the WhatsApp session service. They carry no
//! whatsmeow types: the session service maps protocol events onto them.
-use super::models::{BackfillState, ChatKind, Identity, PairingState};
+use super::models::{BackfillState, ChatKind, Identity, MediaPresentation, PairingState};
+pub use super::rich::RichContent;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@@ -94,6 +95,38 @@ pub enum MessageKind {
Delete,
/// A shape Macro does not import; `text` is a readable placeholder.
Unsupported,
+ /// A poll, location, contact card or event: `rich` holds it, `text` any
+ /// caption.
+ Rich,
+ /// Something that happened in the group (`rich` is a system line); never
+ /// notifies or counts as unread.
+ System,
+ /// A vote on the poll `target` (`rich` is the vote).
+ PollVote,
+}
+
+impl MessageKind {
+ /// Snake-case name, as serialized.
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Text => "text",
+ Self::Reaction => "reaction",
+ Self::Edit => "edit",
+ Self::Delete => "delete",
+ Self::Unsupported => "unsupported",
+ Self::Rich => "rich",
+ Self::System => "system",
+ Self::PollVote => "poll_vote",
+ }
+ }
+
+ /// A new timeline entry rather than a change to an existing one.
+ pub fn is_entry(self) -> bool {
+ matches!(
+ self,
+ Self::Text | Self::Unsupported | Self::Rich | Self::System
+ )
+ }
}
/// Source message reference for quotes and mutations.
@@ -120,10 +153,22 @@ pub struct Media {
/// returned unchanged by `claim_media_imports`.
#[serde(default)]
pub locator: Vec,
+ /// Pixel size of an image, sticker or video, as WhatsApp declared it.
+ #[serde(default)]
+ pub width: Option,
+ /// Pixel height, as declared.
+ #[serde(default)]
+ pub height: Option,
+ /// Length of audio or video in whole seconds, as declared.
+ #[serde(default)]
+ pub duration_seconds: Option,
+ /// How WhatsApp shows it, when not as a plain file of its kind.
+ #[serde(default)]
+ pub presentation: Option,
}
/// One normalized group message or revision.
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Message {
/// WhatsApp message id.
pub source_id: String,
@@ -145,6 +190,16 @@ pub struct Message {
pub target: Option,
/// Attachments.
pub media: Vec,
+ /// People the text @-mentions, as WhatsApp ids; the text carries
+ /// `@` for each.
+ #[serde(default)]
+ pub mentions: Vec,
+ /// Forwarded from another chat.
+ #[serde(default)]
+ pub forwarded: bool,
+ /// Structured content of rich, system and poll-vote messages.
+ #[serde(default)]
+ pub rich: Option,
}
/// One participant's receipt, never proof of group-wide delivery.
@@ -154,13 +209,33 @@ pub struct Receipt {
pub source_ids: Vec,
/// Participant who sent the receipt.
pub participant_jid: String,
- /// Receipt category, such as `delivered` or `read`.
- pub state: String,
+ /// What the participant reported.
+ pub state: ReceiptState,
+}
+
+/// What a participant's receipt reports; other WhatsApp receipt types (retry,
+/// from the account's own devices, server errors) are not kept.
+#[derive(
+ Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::AsRefStr, strum::EnumString,
+)]
+#[serde(rename_all = "snake_case")]
+#[strum(serialize_all = "snake_case")]
+pub enum ReceiptState {
+ /// Reached the participant's device.
+ Delivered,
+ /// The participant saw it.
+ Read,
+ /// The participant played the voice note or opened the view-once media.
+ Played,
}
/// What happened in the group.
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
+#[expect(
+ clippy::large_enum_variant,
+ reason = "one event per WhatsApp message, built and moved once, never held in bulk"
+)]
pub enum InboundKind {
/// A message, reaction, edit or delete.
Message(Message),
@@ -172,7 +247,7 @@ pub enum InboundKind {
/// One event from an observed group. `event_id` must be stable across
/// redelivery of the same protocol event.
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InboundEvent {
/// Stable id for deferral and quarantine.
pub event_id: Uuid,
@@ -208,6 +283,9 @@ pub enum OutgoingContent {
text: String,
/// Quoted message.
quote: Option,
+ /// Mentioned participants (WhatsApp ids); the text carries
+ /// `@` for each.
+ mentions: Vec,
},
/// Reaction; empty emoji removes it.
Reaction {
diff --git a/crates/whatsapp_linked/src/domain/media.rs b/crates/whatsapp_linked/src/domain/media.rs
index 1acab45ceb7..90f964e03fa 100644
--- a/crates/whatsapp_linked/src/domain/media.rs
+++ b/crates/whatsapp_linked/src/domain/media.rs
@@ -58,8 +58,8 @@ impl MediaService {
Ok(imported)
}
- /// Delete staged uploads that were never sent.
- pub async fn expire_uploads(&self) -> LinkedResult {
- self.repository.expire_uploads().await
+ /// Delete staged uploads that were never sent and old participant receipts.
+ pub async fn expire_retained(&self) -> LinkedResult {
+ self.repository.expire_retained().await
}
}
diff --git a/crates/whatsapp_linked/src/domain/models.rs b/crates/whatsapp_linked/src/domain/models.rs
index 055e29ba0de..9f3f1a75c66 100644
--- a/crates/whatsapp_linked/src/domain/models.rs
+++ b/crates/whatsapp_linked/src/domain/models.rs
@@ -534,6 +534,11 @@ pub enum ReplyContent {
Text {
/// UTF-8 text, at most [`TEXT_LIMIT_BYTES`] bytes.
text: String,
+ /// Group participants the text @-mentions, by their WhatsApp id
+ /// (`jid` of a roster participant); the text must contain
+ /// `@` for each.
+ #[serde(default)]
+ mentions: Vec,
},
/// One attachment staged through the attachments route.
Media {
@@ -675,6 +680,41 @@ impl MediaKind {
}
}
+/// How WhatsApp shows an attachment, when not as a plain file of its kind.
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
+#[cfg_attr(feature = "schema", schema(as = LinkedMediaPresentation))]
+pub enum MediaPresentation {
+ /// A sticker: a small image (WebP, possibly animated), no caption.
+ Sticker,
+ /// A short video WhatsApp plays as a looping, muted GIF.
+ Gif,
+ /// A voice note recorded in WhatsApp (push to talk).
+ VoiceNote,
+}
+
+impl MediaPresentation {
+ /// Stored form.
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Sticker => "sticker",
+ Self::Gif => "gif",
+ Self::VoiceNote => "voice_note",
+ }
+ }
+
+ /// Parses the stored form.
+ pub fn parse(value: &str) -> Option {
+ Some(match value {
+ "sticker" => Self::Sticker,
+ "gif" => Self::Gif,
+ "voice_note" => Self::VoiceNote,
+ _ => return None,
+ })
+ }
+}
+
/// Largest attachment imported or sent, in bytes.
pub const MEDIA_LIMIT_BYTES: usize = 32 * 1024 * 1024;
@@ -724,6 +764,14 @@ pub struct LinkedAttachment {
pub size: u64,
/// The private document, once ready.
pub document_id: Option,
+ /// Pixel width of an image, sticker or video, when WhatsApp declared it.
+ pub width: Option,
+ /// Pixel height, when declared.
+ pub height: Option,
+ /// Length of audio or video in whole seconds, when declared.
+ pub duration_seconds: Option,
+ /// Sticker, GIF or voice note; `null` for an ordinary file of its kind.
+ pub presentation: Option,
}
/// The attachments of one linked group message.
@@ -737,6 +785,11 @@ pub struct MessageAttachments {
pub linked_attachments: Vec,
}
+/// Most people one text send may @-mention.
+pub const MENTION_LIMIT: usize = 64;
+
+/// Days a participant's receipt is kept.
+pub const RECEIPT_RETENTION_DAYS: i32 = 30;
/// Maximum UTF-8 bytes in one text send.
pub const TEXT_LIMIT_BYTES: usize = 16 * 1024;
/// Edits and deletes are offered for this long after the original send.
diff --git a/crates/whatsapp_linked/src/domain/ports.rs b/crates/whatsapp_linked/src/domain/ports.rs
index e94026e3eb0..1d342b2e22d 100644
--- a/crates/whatsapp_linked/src/domain/ports.rs
+++ b/crates/whatsapp_linked/src/domain/ports.rs
@@ -430,12 +430,14 @@ pub struct GroupEffect {
pub origin: Origin,
/// Verified Macro user behind a phone-origin message, never notified of it.
pub associated_user: Option,
+ /// A system line (added, left, renamed…), never a notification.
+ pub system: bool,
}
impl GroupEffect {
/// Only a newly received live message creates a notification.
pub fn should_notify(&self) -> bool {
- self.first_publication && self.origin != Origin::History
+ self.first_publication && self.origin != Origin::History && !self.system
}
}
@@ -515,8 +517,9 @@ pub trait MediaRepository: Send + Sync {
async fn import_stored(&self, import: &DocumentImport) -> LinkedResult<()>;
/// Retry later, or give up and show a notice on the message.
async fn import_failed(&self, import: &DocumentImport, permanent: bool) -> LinkedResult<()>;
- /// Delete staged uploads nobody sent.
- async fn expire_uploads(&self) -> LinkedResult;
+ /// Delete staged uploads nobody sent and participant receipts older than
+ /// [`RECEIPT_RETENTION_DAYS`](crate::domain::models::RECEIPT_RETENTION_DAYS).
+ async fn expire_retained(&self) -> LinkedResult;
}
/// Private document storage owned by the documents domain.
diff --git a/crates/whatsapp_linked/src/domain/rich.rs b/crates/whatsapp_linked/src/domain/rich.rs
new file mode 100644
index 00000000000..f4e47f2f3aa
--- /dev/null
+++ b/crates/whatsapp_linked/src/domain/rich.rs
@@ -0,0 +1,243 @@
+//! Structured content of WhatsApp messages that are more than text: polls,
+//! locations, contact cards, events and system lines. Stored as JSON on the
+//! group message (`whatsapp_group_message.rich`) in this form; the timeline
+//! resolves the WhatsApp ids in it to names when it is read.
+
+use chrono::{DateTime, Utc};
+use serde::{Deserialize, Serialize};
+
+/// What a message carries besides its text.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(tag = "type", rename_all = "snake_case")]
+pub enum RichContent {
+ /// A poll; votes are stored per voter and tallied when read.
+ Poll(Poll),
+ /// A vote on a poll (the message's target): the options chosen now.
+ /// Never stored on a message.
+ PollVote(PollVote),
+ /// A place, or a live location as of when it was shared.
+ Location(Location),
+ /// One or more contact cards.
+ Contacts(Contacts),
+ /// A WhatsApp event (calendar invite).
+ Event(Event),
+ /// Something that happened in the group rather than something said.
+ System(SystemLine),
+}
+
+/// A poll as created.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Poll {
+ /// The question.
+ pub question: String,
+ /// The options, in order.
+ pub options: Vec,
+ /// How many options one voter may choose; 0 means any number.
+ pub selectable_count: u32,
+}
+
+/// One option of a poll.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PollOption {
+ /// Its text.
+ pub name: String,
+ /// SHA-256 of the text, hex: how votes name it.
+ pub hash: String,
+}
+
+/// A voter's current choice.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PollVote {
+ /// Hashes (hex) of the options chosen; empty withdraws the vote.
+ pub selected: Vec,
+}
+
+/// A shared place.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
+#[cfg_attr(feature = "schema", schema(as = WhatsAppLocation))]
+pub struct Location {
+ /// Degrees.
+ pub latitude: f64,
+ /// Degrees.
+ pub longitude: f64,
+ /// Place name.
+ #[serde(default)]
+ pub name: Option,
+ /// Street address.
+ #[serde(default)]
+ pub address: Option,
+ /// Link the sender attached.
+ #[serde(default)]
+ pub url: Option,
+ /// Shared as a live location (this is where it started).
+ #[serde(default)]
+ pub live: bool,
+ /// Caption or comment.
+ #[serde(default)]
+ pub comment: Option,
+}
+
+/// Contact cards.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
+#[cfg_attr(feature = "schema", schema(as = WhatsAppContacts))]
+pub struct Contacts {
+ /// The cards, in order.
+ pub contacts: Vec,
+}
+
+/// One contact card, from its vCard.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
+#[cfg_attr(feature = "schema", schema(as = WhatsAppContactCard))]
+pub struct ContactCard {
+ /// Display name.
+ pub name: String,
+ /// Phone numbers as written on the card.
+ pub phones: Vec,
+}
+
+/// A WhatsApp event.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
+#[cfg_attr(feature = "schema", schema(as = WhatsAppEvent))]
+pub struct Event {
+ /// Title.
+ pub name: String,
+ /// Description.
+ #[serde(default)]
+ pub description: Option,
+ /// Start.
+ #[serde(default)]
+ pub start_at: Option>,
+ /// End.
+ #[serde(default)]
+ pub end_at: Option>,
+ /// Place name or address.
+ #[serde(default)]
+ pub location: Option,
+ /// Call link for an online event.
+ #[serde(default)]
+ pub join_link: Option,
+ /// Canceled by its organizer.
+ #[serde(default)]
+ pub canceled: bool,
+}
+
+/// Something that happened in the group. The message's sender is who did it,
+/// unless WhatsApp did not say (then the sender is the group itself).
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SystemLine {
+ /// What happened.
+ pub action: SystemAction,
+ /// The people it happened to (added, removed, promoted…), as WhatsApp ids.
+ #[serde(default)]
+ pub participants: Vec,
+ /// The new subject or description, or the disappearing-messages timer in
+ /// seconds ("0" is off).
+ #[serde(default)]
+ pub value: Option,
+ /// For calls: what kind and how it went.
+ #[serde(default)]
+ pub call: Option,
+ /// WhatsApp did not say who did it; the message's sender is the group.
+ #[serde(default)]
+ pub by_whatsapp: bool,
+}
+
+/// Kinds of system lines.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
+#[cfg_attr(feature = "schema", schema(as = WhatsAppSystemAction))]
+pub enum SystemAction {
+ /// The group was created.
+ GroupCreated,
+ /// The sender added `participants`.
+ ParticipantsAdded,
+ /// The sender removed `participants`.
+ ParticipantsRemoved,
+ /// `participants` left.
+ ParticipantsLeft,
+ /// `participants` joined with an invite link.
+ ParticipantsJoined,
+ /// The sender made `participants` admins.
+ ParticipantsPromoted,
+ /// The sender removed `participants` as admins.
+ ParticipantsDemoted,
+ /// The sender changed the subject to `value`.
+ SubjectChanged,
+ /// The sender changed the group icon.
+ IconChanged,
+ /// The sender changed the description to `value` (absent: removed it).
+ DescriptionChanged,
+ /// The sender set disappearing messages to `value` seconds ("0": off).
+ DisappearingChanged,
+ /// The sender pinned the message the line replies to.
+ MessagePinned,
+ /// The sender unpinned the message the line replies to.
+ MessageUnpinned,
+ /// A call in the group (`call` says which).
+ Call,
+}
+
+/// A call in the group.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
+#[cfg_attr(feature = "schema", schema(as = WhatsAppCall))]
+pub struct Call {
+ /// Video rather than voice.
+ pub video: bool,
+ /// How it went for the account that saw it.
+ pub outcome: CallOutcome,
+ /// Length, when it connected.
+ #[serde(default)]
+ pub duration_seconds: Option,
+}
+
+/// How a call went.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
+#[cfg_attr(feature = "schema", schema(as = WhatsAppCallOutcome))]
+pub enum CallOutcome {
+ /// Answered.
+ Answered,
+ /// Not answered.
+ Missed,
+ /// Declined.
+ Rejected,
+ /// Failed or still going when reported.
+ Other,
+}
+
+/// Longest link kept from a sender's location or event.
+const WEB_LINK_LIMIT: usize = 2048;
+
+/// A sender-supplied link that is safe to open: `http` or `https` only, so a
+/// `javascript:` or `file:` link from anyone in the chat never runs.
+pub fn web_link(value: &str) -> Option {
+ let value = value.trim();
+ let scheme_ok = ["http://", "https://"].iter().any(|scheme| {
+ value
+ .get(..scheme.len())
+ .is_some_and(|prefix| prefix.eq_ignore_ascii_case(scheme))
+ });
+ (scheme_ok
+ && value.len() <= WEB_LINK_LIMIT
+ && !value.chars().any(|c| c.is_control() || c.is_whitespace()))
+ .then(|| value.to_owned())
+}
+
+#[cfg(test)]
+mod test;
diff --git a/crates/whatsapp_linked/src/domain/rich/test.rs b/crates/whatsapp_linked/src/domain/rich/test.rs
new file mode 100644
index 00000000000..c9f75092bbd
--- /dev/null
+++ b/crates/whatsapp_linked/src/domain/rich/test.rs
@@ -0,0 +1,26 @@
+use super::*;
+
+#[test]
+fn only_http_and_https_links_are_kept() {
+ for (link, kept) in [
+ ("https://maps.example.com/place?id=1", true),
+ (" HTTP://example.com ", true),
+ ("javascript:alert(document.cookie)", false),
+ ("JavaScript:alert(1)", false),
+ ("data:text/html,", false),
+ ("file:///etc/passwd", false),
+ ("//evil.example.com", false),
+ ("https://exa mple.com", false),
+ ("https://example.com/\u{0}", false),
+ ("", false),
+ ] {
+ assert_eq!(web_link(link).is_some(), kept, "{link}");
+ }
+ assert_eq!(
+ web_link(&format!(
+ "https://example.com/{}",
+ "a".repeat(WEB_LINK_LIMIT)
+ )),
+ None
+ );
+}
diff --git a/crates/whatsapp_linked/src/domain/service.rs b/crates/whatsapp_linked/src/domain/service.rs
index 37595024fc3..9a49ebb1591 100644
--- a/crates/whatsapp_linked/src/domain/service.rs
+++ b/crates/whatsapp_linked/src/domain/service.rs
@@ -710,12 +710,36 @@ fn validate_text(text: &str) -> LinkedResult<()> {
Ok(())
}
+/// Each mention is a person's WhatsApp id that the text names as
+/// `@`, the way WhatsApp renders it.
+fn validate_mentions(text: &str, mentions: &[String]) -> LinkedResult<()> {
+ if mentions.len() > MENTION_LIMIT {
+ return Err(LinkedError::Invalid("too many mentions in one message").into());
+ }
+ for mention in mentions {
+ let user = mention
+ .strip_suffix("@s.whatsapp.net")
+ .or_else(|| mention.strip_suffix("@lid"))
+ .filter(|user| !user.is_empty() && user.bytes().all(|byte| byte.is_ascii_digit()))
+ .ok_or(LinkedError::Invalid(
+ "a mention is not a WhatsApp participant",
+ ))?;
+ if !text.contains(&format!("@{user}")) {
+ return Err(LinkedError::Invalid("a mentioned person is not named in the text").into());
+ }
+ }
+ Ok(())
+}
+
pub(crate) fn validate_request(request: &SendReply) -> LinkedResult<()> {
if request.idempotency_key.is_nil() {
return Err(LinkedError::Invalid("a stable send intent key is required").into());
}
match &request.message {
- ReplyContent::Text { text } => validate_text(text),
+ ReplyContent::Text { text, mentions } => {
+ validate_text(text)?;
+ validate_mentions(text, mentions)
+ }
ReplyContent::Media { caption: None, .. } => Ok(()),
ReplyContent::Media {
caption: Some(caption),
diff --git a/crates/whatsapp_linked/src/domain/service/test.rs b/crates/whatsapp_linked/src/domain/service/test.rs
index 4279bd0842d..6102d829cb7 100644
--- a/crates/whatsapp_linked/src/domain/service/test.rs
+++ b/crates/whatsapp_linked/src/domain/service/test.rs
@@ -123,7 +123,10 @@ fn requests_are_bounded_and_operations_carry_the_right_text() {
let key = Uuid::from_u128(9);
let message = Uuid::from_u128(10);
let valid = [
- ReplyContent::Text { text: "hi".into() },
+ ReplyContent::Text {
+ text: "hi".into(),
+ mentions: vec![],
+ },
ReplyContent::Mutation {
message_id: message,
operation: Mutation::Reaction,
@@ -149,9 +152,13 @@ fn requests_are_bounded_and_operations_carry_the_right_text() {
.unwrap();
}
let invalid = [
- ReplyContent::Text { text: " ".into() },
+ ReplyContent::Text {
+ text: " ".into(),
+ mentions: vec![],
+ },
ReplyContent::Text {
text: "x".repeat(TEXT_LIMIT_BYTES + 1),
+ mentions: vec![],
},
ReplyContent::Mutation {
message_id: message,
@@ -177,7 +184,10 @@ fn requests_are_bounded_and_operations_carry_the_right_text() {
assert!(
validate_request(&SendReply {
idempotency_key: Uuid::nil(),
- message: ReplyContent::Text { text: "hi".into() },
+ message: ReplyContent::Text {
+ text: "hi".into(),
+ mentions: vec![]
+ },
reply_to: None,
})
.is_err()
diff --git a/crates/whatsapp_linked/src/domain/session.rs b/crates/whatsapp_linked/src/domain/session.rs
index 06344462f38..bbb8b10d3ee 100644
--- a/crates/whatsapp_linked/src/domain/session.rs
+++ b/crates/whatsapp_linked/src/domain/session.rs
@@ -425,8 +425,6 @@ pub(crate) fn validate_event(event: &InboundEvent) -> LinkedResult<()> {
.iter()
.any(|id| id.is_empty() || id.len() > 256)
|| !is_person_jid(&receipt.participant_jid)
- || receipt.state.is_empty()
- || receipt.state.len() > 64
{
return Err(LinkedError::Invalid("invalid receipt").into());
}
@@ -434,9 +432,14 @@ pub(crate) fn validate_event(event: &InboundEvent) -> LinkedResult<()> {
}
InboundKind::Message(message) => {
let sender = &message.sender;
+ // A group change WhatsApp made without naming who did it is the
+ // group's own.
+ let group_itself = message.kind == MessageKind::System
+ && sender.jid == event.group_jid
+ && sender.alternate_jid.is_none();
if message.source_id.is_empty()
|| message.source_id.len() > 256
- || !is_person_jid(&sender.jid)
+ || !(is_person_jid(&sender.jid) || group_itself)
|| sender
.alternate_jid
.as_deref()
@@ -453,11 +456,32 @@ pub(crate) fn validate_event(event: &InboundEvent) -> LinkedResult<()> {
}
let needs_target = matches!(
message.kind,
- MessageKind::Reaction | MessageKind::Edit | MessageKind::Delete
+ MessageKind::Reaction
+ | MessageKind::Edit
+ | MessageKind::Delete
+ | MessageKind::PollVote
);
if needs_target && message.target.is_none() {
return Err(LinkedError::Invalid("message operation without a target").into());
}
+ let rich_fits = match (message.kind, &message.rich) {
+ (MessageKind::System, Some(RichContent::System(_)))
+ | (MessageKind::PollVote, Some(RichContent::PollVote(_)))
+ | (
+ MessageKind::Rich,
+ Some(
+ RichContent::Poll(_)
+ | RichContent::Location(_)
+ | RichContent::Contacts(_)
+ | RichContent::Event(_),
+ ),
+ ) => true,
+ (MessageKind::System | MessageKind::PollVote | MessageKind::Rich, _) => false,
+ (_, rich) => rich.is_none(),
+ };
+ if !rich_fits || message.mentions.len() > 256 {
+ return Err(LinkedError::Invalid("invalid message content").into());
+ }
// Only the verified author edits or deletes. Admin deletions need
// separate role evidence and are not imported.
if matches!(message.kind, MessageKind::Edit | MessageKind::Delete)
diff --git a/crates/whatsapp_linked/src/domain/session/test.rs b/crates/whatsapp_linked/src/domain/session/test.rs
index 90c808f7d43..a6d6241845c 100644
--- a/crates/whatsapp_linked/src/domain/session/test.rs
+++ b/crates/whatsapp_linked/src/domain/session/test.rs
@@ -25,6 +25,9 @@ fn message(kind: MessageKind, sender: &str, target: Option<&str>) -> InboundEven
sender_jid: jid.into(),
}),
media: vec![],
+ mentions: vec![],
+ forwarded: false,
+ rich: None,
}),
}
}
@@ -67,3 +70,47 @@ fn malformed_events_are_rejected_before_storage() {
}
assert!(validate_event(&huge).is_err());
}
+
+fn system_line(sender: &str) -> InboundEvent {
+ let mut event = message(MessageKind::System, sender, None);
+ if let InboundKind::Message(m) = &mut event.kind {
+ m.sender.alternate_jid = None;
+ m.rich = Some(RichContent::System(crate::domain::rich::SystemLine {
+ action: crate::domain::rich::SystemAction::SubjectChanged,
+ participants: vec![],
+ value: Some("Launch".into()),
+ call: None,
+ by_whatsapp: true,
+ }));
+ }
+ event
+}
+
+#[test]
+fn only_a_system_line_may_come_from_the_group_itself_and_content_must_fit_the_kind() {
+ let group = "120363000000000001@g.us";
+ validate_event(&system_line(group)).unwrap();
+ assert!(
+ validate_event(&message(MessageKind::Text, group, None)).is_err(),
+ "a message is always from a person"
+ );
+ let mut no_line = system_line(group);
+ if let InboundKind::Message(m) = &mut no_line.kind {
+ m.rich = None;
+ }
+ assert!(validate_event(&no_line).is_err());
+ let mut text_with_rich = system_line("15550000001@s.whatsapp.net");
+ if let InboundKind::Message(m) = &mut text_with_rich.kind {
+ m.kind = MessageKind::Text;
+ }
+ assert!(validate_event(&text_with_rich).is_err());
+ assert!(
+ validate_event(&message(
+ MessageKind::PollVote,
+ "15550000001@s.whatsapp.net",
+ None
+ ))
+ .is_err(),
+ "a vote names its poll"
+ );
+}
diff --git a/crates/whatsapp_linked/src/domain/timeline.rs b/crates/whatsapp_linked/src/domain/timeline.rs
index 0e020d7fafd..98f72f16f7b 100644
--- a/crates/whatsapp_linked/src/domain/timeline.rs
+++ b/crates/whatsapp_linked/src/domain/timeline.rs
@@ -6,6 +6,9 @@ use models_permissions::share_permission::{SharePermissionV2, UpdateSharePermiss
use serde::{Deserialize, Serialize};
use uuid::Uuid;
+/// Names, tallies and system lines as the timeline shows them.
+pub mod view;
+
/// Adopted group visible through ordinary entity sharing.
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
@@ -94,7 +97,117 @@ pub struct WhatsAppGroupMessage {
pub attachments: Vec,
/// Delivery state for the caller's deliberate send only.
pub delivery_state: Option,
+ /// A message, or a system line.
+ pub kind: WhatsAppMessageKind,
+ /// Forwarded from another chat.
+ pub forwarded: bool,
+ /// People the body @-mentions.
+ pub mentions: Vec,
+ /// Poll, location, contact cards, event or system line.
+ pub rich: Option,
+}
+/// What a timeline entry is.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "snake_case")]
+#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
+pub enum WhatsAppMessageKind {
+ /// Something someone sent.
+ Message,
+ /// Something that happened in the group (`rich` is a system line): shown
+ /// as a centered line, never notified or counted as unread.
+ System,
+}
+
+/// A person named in a message: a Macro user when verified, else their
+/// WhatsApp name, else their phone number.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "camelCase")]
+#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
+pub struct WhatsAppPersonRef {
+ /// Display name, never authentication evidence.
+ pub name: String,
+ /// Verified Macro user, if any.
+ pub user_id: Option,
+}
+
+/// An @-mention in a message body.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "camelCase")]
+#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
+pub struct WhatsAppMention {
+ /// What follows the `@` in the body (the digits of the person's WhatsApp
+ /// id); replace `@` with the name.
+ pub token: String,
+ /// Who it is.
+ #[serde(flatten)]
+ pub person: WhatsAppPersonRef,
+}
+
+/// One option of a poll with its votes.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "camelCase")]
+#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
+pub struct WhatsAppPollOption {
+ /// Option text.
+ pub name: String,
+ /// People who chose it.
+ pub votes: i64,
+ /// The caller chose it.
+ pub mine: bool,
+ /// Who chose it.
+ pub voters: Vec,
+}
+
+/// A poll, read-only in Macro.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "camelCase")]
+#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
+pub struct WhatsAppPoll {
+ /// The question.
+ pub question: String,
+ /// Options in order, with tallies.
+ pub options: Vec,
+ /// How many options one person may choose; 0 means any number.
+ pub selectable_count: u32,
+ /// People who voted for anything.
+ pub voters: i64,
}
+
+/// Something that happened in the group.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "camelCase")]
+#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
+pub struct WhatsAppSystemLine {
+ /// What happened.
+ pub action: super::rich::SystemAction,
+ /// Who did it; absent when WhatsApp did not say.
+ pub actor: Option,
+ /// Who it happened to (added, removed, promoted, …).
+ pub participants: Vec,
+ /// New subject or description; disappearing-messages timer in seconds
+ /// (`"0"` is off).
+ pub value: Option,
+ /// For `call`: video or voice, and how it went.
+ pub call: Option,
+}
+
+/// Structured content of a message besides its body.
+#[derive(Debug, Clone, PartialEq, Serialize)]
+#[serde(tag = "type", rename_all = "snake_case")]
+#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
+pub enum WhatsAppRichContent {
+ /// A poll with current votes.
+ Poll(WhatsAppPoll),
+ /// A place (or the start of a live location); link to a maps URL.
+ Location(super::rich::Location),
+ /// Contact cards.
+ Contacts(super::rich::Contacts),
+ /// A WhatsApp event.
+ Event(super::rich::Event),
+ /// A system line.
+ System(WhatsAppSystemLine),
+}
+
/// Newest-first timeline page; `before` is the last returned message id.
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
diff --git a/crates/whatsapp_linked/src/domain/timeline/view.rs b/crates/whatsapp_linked/src/domain/timeline/view.rs
new file mode 100644
index 00000000000..c4e05707b0a
--- /dev/null
+++ b/crates/whatsapp_linked/src/domain/timeline/view.rs
@@ -0,0 +1,141 @@
+//! How stored rich content, mentions and votes read in the timeline: WhatsApp
+//! ids become people's names, votes become tallies.
+
+use super::*;
+use crate::domain::rich::{Event, Location, PollOption, RichContent, SystemLine, web_link};
+use std::collections::HashMap;
+
+#[cfg(test)]
+mod test;
+
+/// Who WhatsApp ids are, as far as the group knows: people seen in it (with
+/// their verified Macro user) and the roster's names.
+#[derive(Debug, Default)]
+pub struct People {
+ by_jid: HashMap,
+}
+
+impl People {
+ /// Learn a person's name; the first name given for an id wins, so pass
+ /// the better sources first.
+ pub fn learn(&mut self, jid: &str, person: WhatsAppPersonRef) {
+ self.by_jid.entry(jid.to_owned()).or_insert(person);
+ }
+
+ /// The person behind an id: their name when known, else their phone
+ /// number, else a generic name.
+ pub fn get(&self, jid: &str) -> WhatsAppPersonRef {
+ if let Some(person) = self.by_jid.get(jid) {
+ return person.clone();
+ }
+ WhatsAppPersonRef {
+ name: jid
+ .strip_suffix("@s.whatsapp.net")
+ .map(|phone| format!("+{phone}"))
+ .unwrap_or_else(|| UNKNOWN_PERSON.to_owned()),
+ user_id: None,
+ }
+ }
+}
+
+/// Name for someone WhatsApp only knows by a private id.
+pub const UNKNOWN_PERSON: &str = "WhatsApp participant";
+
+/// One person's current vote on a poll.
+#[derive(Debug, Clone)]
+pub struct StoredVote {
+ /// The poll message.
+ pub message_id: Uuid,
+ /// Hashes of the options chosen.
+ pub selected: Vec,
+ /// Who voted.
+ pub voter: WhatsAppPersonRef,
+}
+
+/// The mentions of a body, by name.
+pub fn mentions_view(mentions: &[String], people: &People) -> Vec {
+ mentions
+ .iter()
+ .filter_map(|jid| {
+ let (token, _) = jid.split_once('@')?;
+ Some(WhatsAppMention {
+ token: token.to_owned(),
+ person: people.get(jid),
+ })
+ })
+ .collect()
+}
+
+fn poll_option(option: &PollOption, votes: &[&StoredVote], viewer: &str) -> WhatsAppPollOption {
+ let voters: Vec = votes
+ .iter()
+ .filter(|vote| vote.selected.contains(&option.hash))
+ .map(|vote| vote.voter.clone())
+ .collect();
+ WhatsAppPollOption {
+ name: option.name.clone(),
+ votes: voters.len() as i64,
+ mine: voters
+ .iter()
+ .any(|voter| voter.user_id.as_deref() == Some(viewer)),
+ voters,
+ }
+}
+
+fn system_view(
+ line: SystemLine,
+ sender: &WhatsAppPersonRef,
+ people: &People,
+) -> WhatsAppSystemLine {
+ WhatsAppSystemLine {
+ action: line.action,
+ actor: (!line.by_whatsapp).then(|| sender.clone()),
+ participants: line
+ .participants
+ .iter()
+ .map(|jid| people.get(jid))
+ .collect(),
+ value: line.value,
+ call: line.call,
+ }
+}
+
+/// The timeline form of stored rich content. `votes` are the votes on this
+/// message; `viewer` decides `mine`.
+pub fn rich_view(
+ rich: RichContent,
+ sender: &WhatsAppPersonRef,
+ votes: &[&StoredVote],
+ viewer: &str,
+ people: &People,
+) -> Option {
+ Some(match rich {
+ RichContent::Poll(poll) => WhatsAppRichContent::Poll(WhatsAppPoll {
+ options: poll
+ .options
+ .iter()
+ .map(|option| poll_option(option, votes, viewer))
+ .collect(),
+ question: poll.question,
+ selectable_count: poll.selectable_count,
+ voters: votes
+ .iter()
+ .filter(|vote| !vote.selected.is_empty())
+ .count() as i64,
+ }),
+ // Links were sender-supplied; ones stored before they were checked
+ // are checked again.
+ RichContent::Location(location) => WhatsAppRichContent::Location(Location {
+ url: location.url.as_deref().and_then(web_link),
+ ..location
+ }),
+ RichContent::Contacts(contacts) => WhatsAppRichContent::Contacts(contacts),
+ RichContent::Event(event) => WhatsAppRichContent::Event(Event {
+ join_link: event.join_link.as_deref().and_then(web_link),
+ ..event
+ }),
+ RichContent::System(line) => WhatsAppRichContent::System(system_view(line, sender, people)),
+ // Votes are applied to their poll, never shown on their own.
+ RichContent::PollVote(_) => return None,
+ })
+}
diff --git a/crates/whatsapp_linked/src/domain/timeline/view/test.rs b/crates/whatsapp_linked/src/domain/timeline/view/test.rs
new file mode 100644
index 00000000000..5ed80404b5a
--- /dev/null
+++ b/crates/whatsapp_linked/src/domain/timeline/view/test.rs
@@ -0,0 +1,194 @@
+use super::*;
+use crate::domain::rich::{Call, CallOutcome, Poll, PollOption, SystemAction};
+
+fn person(name: &str, user: Option<&str>) -> WhatsAppPersonRef {
+ WhatsAppPersonRef {
+ name: name.to_owned(),
+ user_id: user.map(str::to_owned),
+ }
+}
+
+#[test]
+fn mentions_name_people_seen_in_the_group_else_their_number() {
+ let mut people = People::default();
+ people.learn(
+ "15550001111@s.whatsapp.net",
+ person("Ana", Some("macro|ana@example.com")),
+ );
+
+ let mentions = mentions_view(
+ &[
+ "15550001111@s.whatsapp.net".to_owned(),
+ "15550002222@s.whatsapp.net".to_owned(),
+ "987654321@lid".to_owned(),
+ ],
+ &people,
+ );
+
+ assert_eq!(
+ mentions,
+ vec![
+ WhatsAppMention {
+ token: "15550001111".to_owned(),
+ person: person("Ana", Some("macro|ana@example.com")),
+ },
+ WhatsAppMention {
+ token: "15550002222".to_owned(),
+ person: person("+15550002222", None),
+ },
+ WhatsAppMention {
+ token: "987654321".to_owned(),
+ person: person(UNKNOWN_PERSON, None),
+ },
+ ]
+ );
+}
+
+#[test]
+fn a_poll_tallies_current_votes_per_option_and_marks_the_viewers() {
+ let poll = RichContent::Poll(Poll {
+ question: "Lunch?".to_owned(),
+ options: vec![
+ PollOption {
+ name: "Pizza".to_owned(),
+ hash: "aa".to_owned(),
+ },
+ PollOption {
+ name: "Sushi".to_owned(),
+ hash: "bb".to_owned(),
+ },
+ ],
+ selectable_count: 1,
+ });
+ let message = Uuid::from_u128(1);
+ let ana = StoredVote {
+ message_id: message,
+ selected: vec!["aa".to_owned()],
+ voter: person("Ana", Some("macro|ana@example.com")),
+ };
+ let ben = StoredVote {
+ message_id: message,
+ selected: vec!["aa".to_owned()],
+ voter: person("Ben", None),
+ };
+
+ let view = rich_view(
+ poll,
+ &person("Ana", None),
+ &[&ana, &ben],
+ "macro|ana@example.com",
+ &People::default(),
+ );
+
+ assert_eq!(
+ view,
+ Some(WhatsAppRichContent::Poll(WhatsAppPoll {
+ question: "Lunch?".to_owned(),
+ options: vec![
+ WhatsAppPollOption {
+ name: "Pizza".to_owned(),
+ votes: 2,
+ mine: true,
+ voters: vec![
+ person("Ana", Some("macro|ana@example.com")),
+ person("Ben", None)
+ ],
+ },
+ WhatsAppPollOption {
+ name: "Sushi".to_owned(),
+ votes: 0,
+ mine: false,
+ voters: vec![],
+ },
+ ],
+ selectable_count: 1,
+ voters: 2,
+ }))
+ );
+}
+
+#[test]
+fn a_system_line_names_who_did_it_and_to_whom() {
+ let mut people = People::default();
+ people.learn("15550002222@s.whatsapp.net", person("Ben", None));
+ let added = RichContent::System(SystemLine {
+ action: SystemAction::ParticipantsAdded,
+ participants: vec![
+ "15550002222@s.whatsapp.net".to_owned(),
+ "15550003333@s.whatsapp.net".to_owned(),
+ ],
+ value: None,
+ call: None,
+ by_whatsapp: false,
+ });
+ let renamed_by_whatsapp = RichContent::System(SystemLine {
+ action: SystemAction::SubjectChanged,
+ participants: vec![],
+ value: Some("Launch".to_owned()),
+ call: None,
+ by_whatsapp: true,
+ });
+ let call = RichContent::System(SystemLine {
+ action: SystemAction::Call,
+ participants: vec![],
+ value: None,
+ call: Some(Call {
+ video: true,
+ outcome: CallOutcome::Missed,
+ duration_seconds: None,
+ }),
+ by_whatsapp: false,
+ });
+ let ana = person("Ana", None);
+
+ assert_eq!(
+ rich_view(added, &ana, &[], "viewer", &people),
+ Some(WhatsAppRichContent::System(WhatsAppSystemLine {
+ action: SystemAction::ParticipantsAdded,
+ actor: Some(ana.clone()),
+ participants: vec![person("Ben", None), person("+15550003333", None)],
+ value: None,
+ call: None,
+ }))
+ );
+ assert_eq!(
+ rich_view(renamed_by_whatsapp, &ana, &[], "viewer", &people),
+ Some(WhatsAppRichContent::System(WhatsAppSystemLine {
+ action: SystemAction::SubjectChanged,
+ actor: None,
+ participants: vec![],
+ value: Some("Launch".to_owned()),
+ call: None,
+ }))
+ );
+ let Some(WhatsAppRichContent::System(line)) = rich_view(call, &ana, &[], "viewer", &people)
+ else {
+ panic!("expected a system line");
+ };
+ assert_eq!(
+ line.call,
+ Some(Call {
+ video: true,
+ outcome: CallOutcome::Missed,
+ duration_seconds: None,
+ })
+ );
+}
+
+#[test]
+fn a_poll_vote_is_never_shown_on_its_own() {
+ let vote = RichContent::PollVote(crate::domain::rich::PollVote {
+ selected: vec!["aa".to_owned()],
+ });
+
+ assert_eq!(
+ rich_view(
+ vote,
+ &person("Ana", None),
+ &[],
+ "viewer",
+ &People::default()
+ ),
+ None
+ );
+}
diff --git a/crates/whatsapp_linked/src/outbound/pg/bindings.rs b/crates/whatsapp_linked/src/outbound/pg/bindings.rs
index 5b4e8d4a2ee..3b0bfea681c 100644
--- a/crates/whatsapp_linked/src/outbound/pg/bindings.rs
+++ b/crates/whatsapp_linked/src/outbound/pg/bindings.rs
@@ -120,7 +120,8 @@ impl BindingRepository for PgLinkedRepository {
messages: &[Uuid],
) -> LinkedResult> {
let rows = sqlx::query!(
- r#"SELECT m.message_id AS "message_id!", a.ordinal, a.state, a.filename, a.mime, a.size, a.document_id
+ r#"SELECT m.message_id AS "message_id!", a.ordinal, a.state, a.filename, a.mime, a.size, a.document_id,
+ a.width, a.height, a.duration_seconds, a.presentation
FROM whatsapp_group_attachment a
JOIN whatsapp_group_message m ON m.binding_id = a.binding_id AND m.person_id = a.person_id AND m.source_id = a.source_id
WHERE a.binding_id = $1 AND m.message_id = ANY($2) AND a.state <> 'deleted'
@@ -147,6 +148,13 @@ impl BindingRepository for PgLinkedRepository {
mime,
size: row.size.unwrap_or(0).max(0) as u64,
document_id: (state == AttachmentState::Ready).then_some(row.document_id),
+ width: row.width.map(|value| value.max(0) as u32),
+ height: row.height.map(|value| value.max(0) as u32),
+ duration_seconds: row.duration_seconds.map(|value| value.max(0) as u32),
+ presentation: row
+ .presentation
+ .as_deref()
+ .and_then(MediaPresentation::parse),
};
match result.last_mut() {
Some(last) if last.message_id == row.message_id => {
diff --git a/crates/whatsapp_linked/src/outbound/pg/effects.rs b/crates/whatsapp_linked/src/outbound/pg/effects.rs
index e07f9fb5e44..72173b642df 100644
--- a/crates/whatsapp_linked/src/outbound/pg/effects.rs
+++ b/crates/whatsapp_linked/src/outbound/pg/effects.rs
@@ -10,7 +10,7 @@ use async_trait::async_trait;
impl EffectsRepository for PgLinkedRepository {
async fn pending_effects(&self, limit: i64) -> LinkedResult> {
sqlx::query!(
- r#"SELECT b.id AS group_id, m.message_id AS "message_id!", m.effect_revision, m.published_revision, m.origin, m.content_revision, m.deleted_at, p.user_id
+ r#"SELECT b.id AS group_id, m.message_id AS "message_id!", m.effect_revision, m.published_revision, m.origin, m.content_revision, m.deleted_at, p.user_id, m.kind
FROM whatsapp_group_message m
JOIN whatsapp_group b ON b.id = m.binding_id
JOIN whatsapp_external_person p ON p.id = m.person_id
@@ -35,6 +35,7 @@ impl EffectsRepository for PgLinkedRepository {
_ => return Err(LinkedError::Unavailable.into()),
},
associated_user: row.user_id,
+ system: row.kind == "system",
})
})
.collect()
diff --git a/crates/whatsapp_linked/src/outbound/pg/groups.rs b/crates/whatsapp_linked/src/outbound/pg/groups.rs
index d8554342604..fa580588341 100644
--- a/crates/whatsapp_linked/src/outbound/pg/groups.rs
+++ b/crates/whatsapp_linked/src/outbound/pg/groups.rs
@@ -356,7 +356,7 @@ impl GroupRepository for PgLinkedRepository {
}
async fn binding_roster(&self, binding: &Binding) -> LinkedResult> {
- sqlx::query_scalar!(
+ let roster = sqlx::query_scalar!(
r#"SELECT r.metadata FROM whatsapp_group_roster r
JOIN whatsapp_group_membership m ON m.account_id = r.account_id
WHERE m.binding_id = $1 AND m.consent_ended_at IS NULL AND r.group_jid = $2
@@ -367,8 +367,51 @@ impl GroupRepository for PgLinkedRepository {
.fetch_optional(&self.pool)
.await
.map_err(failure)?
- .map(decode)
- .transpose()
+ .map(decode::)
+ .transpose()?;
+ let Some(mut roster) = roster else {
+ return Ok(None);
+ };
+ // WhatsApp's group metadata has no names; people who wrote in the
+ // group are known by their WhatsApp name.
+ let jids: Vec = roster
+ .participants
+ .iter()
+ .filter(|participant| participant.identity.display_name.is_none())
+ .flat_map(|participant| {
+ [
+ Some(participant.identity.jid.clone()),
+ participant.identity.alternate_jid.clone(),
+ ]
+ })
+ .flatten()
+ .collect();
+ if jids.is_empty() {
+ return Ok(Some(roster));
+ }
+ let names = sqlx::query!(
+ r#"SELECT a.jid, p.display_name AS "display_name!" FROM whatsapp_identity_alias a
+ JOIN whatsapp_external_person p ON p.id = a.person_id
+ WHERE a.binding_id = $1 AND a.jid = ANY($2) AND p.display_name IS NOT NULL"#,
+ binding.id,
+ &jids
+ )
+ .fetch_all(&self.pool)
+ .await
+ .map_err(failure)?;
+ for participant in &mut roster.participants {
+ let identity = &mut participant.identity;
+ if identity.display_name.is_none() {
+ identity.display_name = names
+ .iter()
+ .find(|row| {
+ row.jid == identity.jid
+ || identity.alternate_jid.as_deref() == Some(row.jid.as_str())
+ })
+ .map(|row| row.display_name.clone());
+ }
+ }
+ Ok(Some(roster))
}
async fn account_chats(&self, account: Uuid) -> LinkedResult> {
diff --git a/crates/whatsapp_linked/src/outbound/pg/ingest.rs b/crates/whatsapp_linked/src/outbound/pg/ingest.rs
index 30267ab92c5..8726ee55035 100644
--- a/crates/whatsapp_linked/src/outbound/pg/ingest.rs
+++ b/crates/whatsapp_linked/src/outbound/pg/ingest.rs
@@ -46,6 +46,10 @@ pub(super) enum Reserve {
Skip,
}
+/// Namespace of each group's stand-in person: the author of system lines
+/// WhatsApp reported without naming anyone.
+const GROUP_PERSON_NAMESPACE: Uuid = Uuid::from_u128(0x2f7c_9a41_5d0e_4b83_9c16_7e2a_b04d_51f8);
+
/// How far before adoption or consent a message may be stamped and still be
/// imported. WhatsApp stamps messages in whole seconds by its own clock, which
/// can differ from ours; comparing exactly drops messages sent in the same
@@ -113,16 +117,26 @@ pub(super) async fn reserve_in(
return Ok(Reserve::Skip);
}
let sender = &message.sender;
- let mut aliases = vec![sender.jid.clone()];
- aliases.extend(sender.alternate_jid.clone());
- let people = sqlx::query_scalar!(
- "SELECT DISTINCT person_id FROM whatsapp_identity_alias WHERE binding_id = $1 AND jid = ANY($2)",
- binding.id,
- &aliases
- )
- .fetch_all(&mut **tx)
- .await
- .map_err(failure)?;
+ // A group change WhatsApp made without naming anyone belongs to the
+ // group's own stand-in person, which has no alias.
+ let group_itself = sender.jid == binding.group_jid;
+ let mut aliases = vec![];
+ if !group_itself {
+ aliases.push(sender.jid.clone());
+ aliases.extend(sender.alternate_jid.clone());
+ }
+ let people = if group_itself {
+ vec![Uuid::new_v5(&GROUP_PERSON_NAMESPACE, binding.id.as_bytes())]
+ } else {
+ sqlx::query_scalar!(
+ "SELECT DISTINCT person_id FROM whatsapp_identity_alias WHERE binding_id = $1 AND jid = ANY($2)",
+ binding.id,
+ &aliases
+ )
+ .fetch_all(&mut **tx)
+ .await
+ .map_err(failure)?
+ };
// A LID alone cannot be matched to a phone-number alias already seen.
if people.len() > 1
|| (people.is_empty() && sender.jid.ends_with("@lid") && sender.alternate_jid.is_none())
@@ -163,7 +177,9 @@ pub(super) async fn reserve_in(
.await
.map_err(failure)?;
}
- if message.kind == MessageKind::Reaction {
+ // Reactions and poll votes belong to the person who sent them, on the
+ // target's message.
+ if matches!(message.kind, MessageKind::Reaction | MessageKind::PollVote) {
let target = message
.target
.as_ref()
@@ -238,9 +254,24 @@ pub(super) async fn reserve_in(
let deleted = (message.kind == MessageKind::Delete).then_some(event.source_at);
let content = (message.kind == MessageKind::Edit).then(|| message.text.clone());
let content_revision = if revision { message.revision.max(1) } else { 0 };
+ let entry_kind = if message.kind == MessageKind::System {
+ "system"
+ } else {
+ "message"
+ };
+ let rich = message
+ .rich
+ .as_ref()
+ .map(serde_json::to_value)
+ .transpose()
+ .map_err(failure)?;
+ // A redelivered or re-imported message fills in what an earlier copy
+ // lacked (an older importer stored polls and the like as placeholders);
+ // revisions carry none of it and keep what is stored.
sqlx::query!(
- r#"INSERT INTO whatsapp_group_message (id, binding_id, person_id, source_id, origin, source_at, content_revision, deleted_at, revision_content, revised_at)
- VALUES ($11, $1, $2, $3, $4, $5, $6, $7, $8, $9)
+ r#"INSERT INTO whatsapp_group_message (id, binding_id, person_id, source_id, origin, source_at, content_revision, deleted_at, revision_content, revised_at,
+ kind, rich, mentions, forwarded)
+ VALUES ($11, $1, $2, $3, $4, $5, $6, $7, $8, $9, $12, $13, $14, $15)
ON CONFLICT (binding_id, person_id, source_id) DO UPDATE SET
deleted_at = COALESCE(whatsapp_group_message.deleted_at, excluded.deleted_at),
revision_content = CASE WHEN excluded.content_revision > whatsapp_group_message.content_revision
@@ -248,7 +279,11 @@ pub(super) async fn reserve_in(
revised_at = CASE WHEN excluded.content_revision > whatsapp_group_message.content_revision
THEN excluded.revised_at ELSE whatsapp_group_message.revised_at END,
content_revision = GREATEST(whatsapp_group_message.content_revision, excluded.content_revision),
- source_at = CASE WHEN $10 THEN whatsapp_group_message.source_at ELSE excluded.source_at END"#,
+ source_at = CASE WHEN $10 THEN whatsapp_group_message.source_at ELSE excluded.source_at END,
+ kind = CASE WHEN $10 THEN whatsapp_group_message.kind ELSE excluded.kind END,
+ rich = CASE WHEN $10 THEN whatsapp_group_message.rich ELSE COALESCE(excluded.rich, whatsapp_group_message.rich) END,
+ mentions = CASE WHEN $10 OR cardinality(excluded.mentions) = 0 THEN whatsapp_group_message.mentions ELSE excluded.mentions END,
+ forwarded = whatsapp_group_message.forwarded OR excluded.forwarded"#,
binding.id,
person,
source,
@@ -259,7 +294,11 @@ pub(super) async fn reserve_in(
content,
revision.then_some(event.source_at),
revision,
- send.as_ref().map_or_else(Uuid::now_v7, |s| s.id)
+ send.as_ref().map_or_else(Uuid::now_v7, |s| s.id),
+ entry_kind,
+ rich,
+ &message.mentions,
+ message.forwarded
)
.execute(&mut **tx)
.await
@@ -306,6 +345,13 @@ pub(super) async fn reserve_in(
})))
}
+/// A declared pixel size, when it is one.
+fn dimension(value: Option) -> Option {
+ value
+ .filter(|value| *value > 0)
+ .map(|value| value.min(i32::MAX as u32) as i32)
+}
+
/// One reservation per attachment ordinal. Every observer offers its sealed
/// locator; a pending reservation takes the newest one so a working observer
/// can download it. Unsupported types fail at once.
@@ -321,8 +367,9 @@ async fn reserve_attachments_in(
"failed"
};
sqlx::query!(
- r#"INSERT INTO whatsapp_group_attachment (binding_id, person_id, source_id, ordinal, document_id, sha256, state, account_id, locator, mime, filename, size)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
+ r#"INSERT INTO whatsapp_group_attachment (binding_id, person_id, source_id, ordinal, document_id, sha256, state, account_id, locator, mime, filename, size,
+ width, height, duration_seconds, presentation)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
ON CONFLICT (binding_id, person_id, source_id, ordinal) DO UPDATE SET account_id = excluded.account_id, locator = excluded.locator
WHERE whatsapp_group_attachment.state = 'pending'
AND (whatsapp_group_attachment.claimed_until IS NULL OR whatsapp_group_attachment.claimed_until < now())"#,
@@ -337,7 +384,11 @@ async fn reserve_attachments_in(
&media.locator,
media.mime,
media.filename.chars().take(255).collect::(),
- media.size.min(i64::MAX as u64) as i64
+ media.size.min(i64::MAX as u64) as i64,
+ dimension(media.width),
+ dimension(media.height),
+ media.duration_seconds.map(|seconds| seconds.min(i32::MAX as u32) as i32),
+ media.presentation.map(MediaPresentation::as_str)
)
.execute(&mut **tx)
.await
@@ -353,6 +404,32 @@ pub(super) async fn persist_in(
message: &Message,
r: &Reservation,
) -> LinkedResult {
+ if message.kind == MessageKind::PollVote {
+ let (Some(actor), Some(RichContent::PollVote(vote))) = (r.reaction_actor, &message.rich)
+ else {
+ return Err(LinkedError::Invalid("poll vote without a choice").into());
+ };
+ return Ok(sqlx::query!(
+ r#"INSERT INTO whatsapp_group_poll_vote (message_id, person_id, selected, user_id, source_id, revision, updated_at)
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
+ ON CONFLICT (message_id, person_id) DO UPDATE SET selected = excluded.selected, user_id = excluded.user_id,
+ source_id = excluded.source_id, revision = excluded.revision, updated_at = excluded.updated_at
+ WHERE (excluded.revision, excluded.updated_at, excluded.source_id)
+ > (whatsapp_group_poll_vote.revision, whatsapp_group_poll_vote.updated_at, whatsapp_group_poll_vote.source_id)"#,
+ r.message,
+ actor,
+ &vote.selected,
+ r.associated_user,
+ message.source_id,
+ message.revision,
+ event.source_at
+ )
+ .execute(&mut **tx)
+ .await
+ .map_err(failure)?
+ .rows_affected()
+ > 0);
+ }
if let Some(actor) = r.reaction_actor {
return Ok(sqlx::query!(r#"INSERT INTO whatsapp_group_reaction (message_id,person_id,emoji,user_id,source_id,revision,updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7)
@@ -561,29 +638,28 @@ async fn apply_in(
Ok(IngestOutcome::Applied)
}
InboundKind::Receipt(receipt) => {
- let mut stored = 0;
- for source in &receipt.source_ids {
- stored += sqlx::query!(
- r#"INSERT INTO whatsapp_participant_receipt (account_id, binding_id, source_id, participant_jid, receipt_state, source_at)
- SELECT $1, b.id, $4, $5, $6, $7 FROM whatsapp_group b JOIN whatsapp_group_membership m ON m.binding_id = b.id
- WHERE b.group_jid = $2 AND m.account_id = $1 AND m.generation = $3 AND m.consent_ended_at IS NULL
- AND whatsapp_group_can_view(b.id, $8)
- ON CONFLICT (account_id, binding_id, source_id, participant_jid, receipt_state)
- DO UPDATE SET source_at = GREATEST(whatsapp_participant_receipt.source_at, excluded.source_at)"#,
- account.id,
- event.group_jid,
- account.generation,
- source,
- receipt.participant_jid,
- receipt.state,
- event.source_at,
- account.user_id
- )
- .execute(&mut **tx)
- .await
- .map_err(failure)?
- .rows_affected();
- }
+ let stored = sqlx::query!(
+ r#"INSERT INTO whatsapp_participant_receipt (account_id, binding_id, source_id, participant_jid, receipt_state, source_at)
+ SELECT $1, b.id, source.id, $5, $6, $7
+ FROM whatsapp_group b JOIN whatsapp_group_membership m ON m.binding_id = b.id
+ CROSS JOIN (SELECT DISTINCT unnest($4::text[]) AS id) source
+ WHERE b.group_jid = $2 AND m.account_id = $1 AND m.generation = $3 AND m.consent_ended_at IS NULL
+ AND whatsapp_group_can_view(b.id, $8)
+ ON CONFLICT (account_id, binding_id, source_id, participant_jid, receipt_state)
+ DO UPDATE SET source_at = GREATEST(whatsapp_participant_receipt.source_at, excluded.source_at)"#,
+ account.id,
+ event.group_jid,
+ account.generation,
+ &receipt.source_ids,
+ receipt.participant_jid,
+ receipt.state.as_ref(),
+ event.source_at,
+ account.user_id
+ )
+ .execute(&mut **tx)
+ .await
+ .map_err(failure)?
+ .rows_affected();
Ok(if stored > 0 {
IngestOutcome::Applied
} else {
diff --git a/crates/whatsapp_linked/src/outbound/pg/media.rs b/crates/whatsapp_linked/src/outbound/pg/media.rs
index b9211a5e65d..07def6dbc13 100644
--- a/crates/whatsapp_linked/src/outbound/pg/media.rs
+++ b/crates/whatsapp_linked/src/outbound/pg/media.rs
@@ -316,14 +316,23 @@ impl MediaRepository for PgLinkedRepository {
tx.commit().await.map_err(failure)
}
- async fn expire_uploads(&self) -> LinkedResult {
- Ok(sqlx::query!(
+ async fn expire_retained(&self) -> LinkedResult {
+ let uploads = sqlx::query!(
r#"DELETE FROM whatsapp_media_blob b WHERE b.created_at < now() - interval '1 day'
AND NOT EXISTS (SELECT 1 FROM whatsapp_group_attachment a WHERE a.document_id = b.document_id)"#
)
.execute(&self.pool)
.await
.map_err(failure)?
- .rows_affected())
+ .rows_affected();
+ let receipts = sqlx::query!(
+ "DELETE FROM whatsapp_participant_receipt WHERE source_at < now() - make_interval(days => $1)",
+ crate::domain::models::RECEIPT_RETENTION_DAYS
+ )
+ .execute(&self.pool)
+ .await
+ .map_err(failure)?
+ .rows_affected();
+ Ok(uploads + receipts)
}
}
diff --git a/crates/whatsapp_linked/src/outbound/pg/sends.rs b/crates/whatsapp_linked/src/outbound/pg/sends.rs
index 1390a5e9097..fd64294c147 100644
--- a/crates/whatsapp_linked/src/outbound/pg/sends.rs
+++ b/crates/whatsapp_linked/src/outbound/pg/sends.rs
@@ -380,12 +380,13 @@ async fn content_in(
},
})
}
- ReplyContent::Text { text } => Some(OutgoingContent::Text {
+ ReplyContent::Text { text, mentions } => Some(OutgoingContent::Text {
text: text.clone(),
quote: match request.reply_to {
Some(quoted) => target_in(tx, binding, quoted, identity).await?,
None => None,
},
+ mentions: mentions.clone(),
}),
ReplyContent::Mutation {
message_id,
@@ -627,6 +628,10 @@ impl PgLinkedRepository {
.try_into()
.map_err(|_| LinkedError::Unavailable)?,
locator: vec![],
+ width: None,
+ height: None,
+ duration_seconds: None,
+ presentation: None,
});
upload = Some(*upload_id);
(
@@ -640,7 +645,7 @@ impl PgLinkedRepository {
},
)
}
- ReplyContent::Text { text } => (
+ ReplyContent::Text { text, .. } => (
MessageKind::Text,
text.clone(),
match request.reply_to {
@@ -694,6 +699,12 @@ impl PgLinkedRepository {
revision: server_at.timestamp_millis(),
target,
media,
+ mentions: match &request.message {
+ ReplyContent::Text { mentions, .. } => mentions.clone(),
+ _ => vec![],
+ },
+ forwarded: false,
+ rich: None,
}),
};
let InboundKind::Message(message) = &event.kind else {
diff --git a/crates/whatsapp_linked/src/outbound/pg/test.rs b/crates/whatsapp_linked/src/outbound/pg/test.rs
index 4ce67727c9d..6481bf0dd20 100644
--- a/crates/whatsapp_linked/src/outbound/pg/test.rs
+++ b/crates/whatsapp_linked/src/outbound/pg/test.rs
@@ -21,6 +21,7 @@ mod changes;
mod concurrency;
mod media;
mod pairing;
+mod rich;
mod timeline;
const GROUP: &str = "120363000000000001@g.us";
@@ -175,6 +176,9 @@ fn text(event: u128, sender: &str, source: &str, body: &str) -> InboundEvent {
revision: 0,
target: None,
media: vec![],
+ mentions: vec![],
+ forwarded: false,
+ rich: None,
}),
}
}
@@ -521,6 +525,9 @@ async fn a_reaction_before_its_message_is_deferred_and_replayed(pool: PgPool) {
sender_jid: CUSTOMER_JID.into(),
}),
media: vec![],
+ mentions: vec![],
+ forwarded: false,
+ rich: None,
}),
..text(1, BOB_JID, "unused", "")
};
@@ -578,6 +585,9 @@ async fn edits_apply_in_source_order_and_deletes_are_permanent(pool: PgPool) {
sender_jid: CUSTOMER_JID.into(),
}),
media: vec![],
+ mentions: vec![],
+ forwarded: false,
+ rich: None,
}),
..text(event, CUSTOMER_JID, "unused", "")
};
@@ -620,6 +630,7 @@ async fn a_dispatched_send_becomes_unknown_after_takeover_and_settles_from_its_e
idempotency_key: Uuid::now_v7(),
message: ReplyContent::Text {
text: "On it".into(),
+ mentions: vec![],
},
reply_to: None,
};
@@ -694,7 +705,8 @@ async fn a_dispatched_send_becomes_unknown_after_takeover_and_settles_from_its_e
claimed[0].content,
OutgoingContent::Text {
text: "On it".into(),
- quote: None
+ quote: None,
+ mentions: vec![],
}
);
session
@@ -760,6 +772,7 @@ async fn an_accepted_send_writes_the_group_message_and_its_echo_converges(pool:
idempotency_key: Uuid::now_v7(),
message: ReplyContent::Text {
text: "Answer".into(),
+ mentions: vec![],
},
reply_to: Some(question),
},
@@ -779,7 +792,8 @@ async fn an_accepted_send_writes_the_group_message_and_its_echo_converges(pool:
quote: Some(Target {
source_id: "3EB0Q".into(),
sender_jid: CUSTOMER_JID.into()
- })
+ }),
+ mentions: vec![],
}
);
session
@@ -865,6 +879,7 @@ async fn disconnecting_fails_undispatched_sends_and_stops_observation(pool: PgPo
idempotency_key: Uuid::now_v7(),
message: ReplyContent::Text {
text: "later".into(),
+ mentions: vec![],
},
reply_to: None,
},
@@ -942,7 +957,10 @@ async fn stored_group_facts_authorize_adoption_and_sending_however_old(pool: PgP
&receipt::(ALICE, binding.id),
&SendReply {
idempotency_key: Uuid::now_v7(),
- message: ReplyContent::Text { text: "hi".into() },
+ message: ReplyContent::Text {
+ text: "hi".into(),
+ mentions: vec![],
+ },
reply_to: None,
},
SendAuthority::Human,
@@ -1031,6 +1049,7 @@ async fn rate_limited_send_never_creates_a_dispatchable_intent(pool: PgPool) {
idempotency_key: Uuid::now_v7(),
message: ReplyContent::Text {
text: "Rate limited".into(),
+ mentions: vec![],
},
reply_to: None,
};
diff --git a/crates/whatsapp_linked/src/outbound/pg/test/changes.rs b/crates/whatsapp_linked/src/outbound/pg/test/changes.rs
index e4f1c74e778..e0255d805b5 100644
--- a/crates/whatsapp_linked/src/outbound/pg/test/changes.rs
+++ b/crates/whatsapp_linked/src/outbound/pg/test/changes.rs
@@ -86,7 +86,10 @@ async fn messages_and_send_outcomes_signal_their_group(pool: PgPool) {
&receipt::(ALICE, binding.id),
&SendReply {
idempotency_key: Uuid::now_v7(),
- message: ReplyContent::Text { text: "hi".into() },
+ message: ReplyContent::Text {
+ text: "hi".into(),
+ mentions: vec![],
+ },
reply_to: None,
},
SendAuthority::Human,
@@ -123,6 +126,7 @@ async fn a_retried_send_returns_the_original_whatever_happened_since(pool: PgPoo
idempotency_key: Uuid::now_v7(),
message: ReplyContent::Text {
text: "once".into(),
+ mentions: vec![],
},
reply_to: None,
};
@@ -152,6 +156,7 @@ async fn a_retried_send_returns_the_original_whatever_happened_since(pool: PgPoo
let changed = SendReply {
message: ReplyContent::Text {
text: "twice".into(),
+ mentions: vec![],
},
..request
};
diff --git a/crates/whatsapp_linked/src/outbound/pg/test/concurrency.rs b/crates/whatsapp_linked/src/outbound/pg/test/concurrency.rs
index 1d8b0b882b9..14b79e46277 100644
--- a/crates/whatsapp_linked/src/outbound/pg/test/concurrency.rs
+++ b/crates/whatsapp_linked/src/outbound/pg/test/concurrency.rs
@@ -13,6 +13,7 @@ async fn sends_and_roster_refreshes_share_a_consistent_lock_order(pool: PgPool)
idempotency_key: Uuid::now_v7(),
message: ReplyContent::Text {
text: format!("Concurrent send {index}"),
+ mentions: vec![],
},
reply_to: None,
};
diff --git a/crates/whatsapp_linked/src/outbound/pg/test/media.rs b/crates/whatsapp_linked/src/outbound/pg/test/media.rs
index 76cbe39bebd..33b865bdad5 100644
--- a/crates/whatsapp_linked/src/outbound/pg/test/media.rs
+++ b/crates/whatsapp_linked/src/outbound/pg/test/media.rs
@@ -39,6 +39,10 @@ fn photo(event: u128, sender: &str, source: &str, bytes: &[u8]) -> InboundEvent
size: bytes.len() as u64,
sha256: Sha256::digest(bytes).into(),
locator: b"sealed locator".to_vec(),
+ width: Some(800),
+ height: Some(600),
+ duration_seconds: None,
+ presentation: None,
}];
}
event
@@ -172,6 +176,10 @@ async fn an_inbound_photo_is_downloaded_once_and_becomes_a_private_document(pool
mime: "image/jpeg".into(),
size: bytes.len() as u64,
document_id: Some(stored[0].0),
+ width: Some(800),
+ height: Some(600),
+ duration_seconds: None,
+ presentation: None,
}
);
// Imported documents inherit ordinary group shares dynamically.
diff --git a/crates/whatsapp_linked/src/outbound/pg/test/rich.rs b/crates/whatsapp_linked/src/outbound/pg/test/rich.rs
new file mode 100644
index 00000000000..73693d5a0d5
--- /dev/null
+++ b/crates/whatsapp_linked/src/outbound/pg/test/rich.rs
@@ -0,0 +1,271 @@
+use super::*;
+use crate::domain::rich::{
+ Location, Poll, PollOption, PollVote, RichContent, SystemAction, SystemLine,
+};
+use crate::domain::timeline::{
+ WhatsAppMessageKind, WhatsAppPersonRef, WhatsAppPollOption, WhatsAppRichContent,
+};
+
+fn with(
+ mut event: InboundEvent,
+ kind: MessageKind,
+ rich: Option,
+ target: Option,
+) -> InboundEvent {
+ if let InboundKind::Message(message) = &mut event.kind {
+ message.kind = kind;
+ message.rich = rich;
+ message.target = target;
+ }
+ event
+}
+
+fn person(name: &str, user: Option<&str>) -> WhatsAppPersonRef {
+ WhatsAppPersonRef {
+ name: name.to_owned(),
+ user_id: user.map(str::to_owned),
+ }
+}
+
+#[sqlx::test(migrator = "MACRO_DB_MIGRATIONS")]
+async fn a_system_line_names_people_and_never_notifies_or_counts_as_unread(pool: PgPool) {
+ let (linked, session, repository, binding, alice, _) = adopted(&pool).await;
+ let added = with(
+ text(30, CUSTOMER_JID, "SYSTEM-ADD", ""),
+ MessageKind::System,
+ Some(RichContent::System(SystemLine {
+ action: SystemAction::ParticipantsAdded,
+ participants: vec![BOB_JID.to_owned()],
+ value: None,
+ call: None,
+ by_whatsapp: false,
+ })),
+ None,
+ );
+
+ session.ingest_event(alice, &added).await.unwrap();
+
+ let messages = linked
+ .messages(&receipt::(BOB, binding.id), None, 50)
+ .await
+ .unwrap()
+ .messages;
+ assert_eq!(messages.len(), 1);
+ assert_eq!(messages[0].kind, WhatsAppMessageKind::System);
+ let Some(WhatsAppRichContent::System(line)) = &messages[0].rich else {
+ panic!("expected a system line");
+ };
+ assert_eq!(line.action, SystemAction::ParticipantsAdded);
+ assert_eq!(line.actor, Some(person("Customer", None)));
+ assert_eq!(line.participants, vec![person("+15550000002", None)]);
+ assert_eq!(
+ repository
+ .summary(binding.id, BOB)
+ .await
+ .unwrap()
+ .unread_count,
+ 0
+ );
+ let effects = repository.pending_effects(10).await.unwrap();
+ assert_eq!(effects.len(), 1);
+ assert!(effects[0].system);
+ assert!(!effects[0].should_notify());
+}
+
+fn lunch() -> RichContent {
+ RichContent::Poll(Poll {
+ question: "Lunch?".to_owned(),
+ options: vec![
+ PollOption {
+ name: "Pizza".to_owned(),
+ hash: "aa".to_owned(),
+ },
+ PollOption {
+ name: "Sushi".to_owned(),
+ hash: "bb".to_owned(),
+ },
+ ],
+ selectable_count: 1,
+ })
+}
+
+fn vote(event: u128, voter: &str, source: &str, revision: i64, selected: &[&str]) -> InboundEvent {
+ let mut event = with(
+ text(event, voter, source, ""),
+ MessageKind::PollVote,
+ Some(RichContent::PollVote(PollVote {
+ selected: selected.iter().map(|hash| (*hash).to_owned()).collect(),
+ })),
+ Some(Target {
+ source_id: "POLL".to_owned(),
+ sender_jid: CUSTOMER_JID.to_owned(),
+ }),
+ );
+ if let InboundKind::Message(message) = &mut event.kind {
+ message.revision = revision;
+ message.sender.display_name = None;
+ }
+ event
+}
+
+async fn tallies(linked: &LinkedService, group: Uuid) -> Vec {
+ let messages = linked
+ .messages(&receipt::(ALICE, group), None, 50)
+ .await
+ .unwrap()
+ .messages;
+ let Some(WhatsAppRichContent::Poll(poll)) = &messages[0].rich else {
+ panic!("expected a poll");
+ };
+ poll.options.clone()
+}
+
+#[sqlx::test(migrator = "MACRO_DB_MIGRATIONS")]
+async fn poll_votes_tally_per_voter_and_the_latest_vote_wins(pool: PgPool) {
+ let (linked, session, _, binding, alice, _) = adopted(&pool).await;
+ // A vote that arrives before its poll waits for it.
+ assert_eq!(
+ session
+ .ingest_event(alice, &vote(41, ALICE_JID, "VOTE-1", 1_000, &["aa"]))
+ .await
+ .unwrap(),
+ IngestOutcome::Deferred
+ );
+ session
+ .ingest_event(
+ alice,
+ &with(
+ text(40, CUSTOMER_JID, "POLL", ""),
+ MessageKind::Rich,
+ Some(lunch()),
+ None,
+ ),
+ )
+ .await
+ .unwrap();
+ session
+ .ingest_event(alice, &vote(41, ALICE_JID, "VOTE-1", 1_000, &["aa"]))
+ .await
+ .unwrap();
+ session
+ .ingest_event(alice, &vote(42, CUSTOMER_JID, "VOTE-2", 1_000, &["aa"]))
+ .await
+ .unwrap();
+
+ let options = tallies(&linked, binding.id).await;
+ assert_eq!(options[0].votes, 2);
+ assert!(options[0].mine, "Alice's own number voted");
+ assert_eq!(options[1].votes, 0);
+
+ // The customer changes their mind; a late copy of the old vote is ignored.
+ session
+ .ingest_event(alice, &vote(43, CUSTOMER_JID, "VOTE-3", 2_000, &["bb"]))
+ .await
+ .unwrap();
+ session
+ .ingest_event(alice, &vote(44, CUSTOMER_JID, "VOTE-2", 1_000, &["aa"]))
+ .await
+ .unwrap();
+
+ let options = tallies(&linked, binding.id).await;
+ assert_eq!(options[0].votes, 1);
+ assert_eq!(options[1].votes, 1);
+ assert_eq!(options[1].voters, vec![person("Customer", None)]);
+}
+
+#[sqlx::test(migrator = "MACRO_DB_MIGRATIONS")]
+async fn a_re_imported_placeholder_gains_its_content_and_mentions_read_as_names(pool: PgPool) {
+ let (linked, session, _, binding, alice, _) = adopted(&pool).await;
+ let mut placeholder = text(
+ 50,
+ CUSTOMER_JID,
+ "PLACE",
+ "Sent something Macro can't show yet",
+ );
+ if let InboundKind::Message(message) = &mut placeholder.kind {
+ message.kind = MessageKind::Unsupported;
+ }
+ session.ingest_event(alice, &placeholder).await.unwrap();
+ let mut location = with(
+ text(51, CUSTOMER_JID, "PLACE", ""),
+ MessageKind::Rich,
+ Some(RichContent::Location(Location {
+ latitude: 40.7,
+ longitude: -74.0,
+ name: Some("Office".to_owned()),
+ address: None,
+ url: None,
+ live: false,
+ comment: None,
+ })),
+ None,
+ );
+ if let InboundKind::Message(message) = &mut location.kind {
+ message.history = true;
+ }
+ let mut mention = text(52, ALICE_JID, "MENTION", "ping @15558880001");
+ if let InboundKind::Message(message) = &mut mention.kind {
+ message.mentions = vec![CUSTOMER_JID.to_owned()];
+ message.forwarded = true;
+ }
+
+ session.ingest_event(alice, &location).await.unwrap();
+ session.ingest_event(alice, &mention).await.unwrap();
+
+ let messages = linked
+ .messages(&receipt::(ALICE, binding.id), None, 50)
+ .await
+ .unwrap()
+ .messages;
+ let place = messages.iter().find(|m| m.body.is_empty()).unwrap();
+ let Some(WhatsAppRichContent::Location(location)) = &place.rich else {
+ panic!("the placeholder became the location");
+ };
+ assert_eq!(location.name.as_deref(), Some("Office"));
+ let ping = messages
+ .iter()
+ .find(|m| m.body == "ping @15558880001")
+ .unwrap();
+ assert!(ping.forwarded);
+ assert_eq!(ping.mentions.len(), 1);
+ assert_eq!(ping.mentions[0].token, "15558880001");
+ assert_eq!(ping.mentions[0].person, person("Customer", None));
+}
+
+#[sqlx::test(migrator = "MACRO_DB_MIGRATIONS")]
+async fn a_change_whatsapp_made_without_naming_anyone_is_the_groups_own_line(pool: PgPool) {
+ let (linked, session, _, binding, alice, _) = adopted(&pool).await;
+ let mut renamed = with(
+ text(60, GROUP, "SYSTEM-RENAME", ""),
+ MessageKind::System,
+ Some(RichContent::System(SystemLine {
+ action: SystemAction::SubjectChanged,
+ participants: vec![],
+ value: Some("Launch".to_owned()),
+ call: None,
+ by_whatsapp: true,
+ })),
+ None,
+ );
+ if let InboundKind::Message(message) = &mut renamed.kind {
+ message.sender.display_name = None;
+ }
+
+ assert_eq!(
+ session.ingest_event(alice, &renamed).await.unwrap(),
+ IngestOutcome::Applied
+ );
+ session.ingest_event(alice, &renamed).await.unwrap();
+
+ let messages = linked
+ .messages(&receipt::(ALICE, binding.id), None, 50)
+ .await
+ .unwrap()
+ .messages;
+ assert_eq!(messages.len(), 1, "redelivery converges on one line");
+ let Some(WhatsAppRichContent::System(line)) = &messages[0].rich else {
+ panic!("expected a system line");
+ };
+ assert_eq!(line.actor, None);
+ assert_eq!(line.value.as_deref(), Some("Launch"));
+}
diff --git a/crates/whatsapp_linked/src/outbound/pg/test/timeline.rs b/crates/whatsapp_linked/src/outbound/pg/test/timeline.rs
index a2486b3e262..5e61ee65523 100644
--- a/crates/whatsapp_linked/src/outbound/pg/test/timeline.rs
+++ b/crates/whatsapp_linked/src/outbound/pg/test/timeline.rs
@@ -91,6 +91,7 @@ async fn shared_viewer_without_whatsapp_reads_history_and_unread_but_cannot_send
idempotency_key: Uuid::now_v7(),
message: ReplyContent::Text {
text: "cannot send".into(),
+ mentions: vec![],
},
reply_to: None,
};
@@ -421,6 +422,7 @@ async fn shared_sender_uses_own_roster_without_adopting_again(pool: PgPool) {
idempotency_key: Uuid::now_v7(),
message: ReplyContent::Text {
text: "from Bob's own connection".into(),
+ mentions: vec![],
},
reply_to: None,
},
diff --git a/crates/whatsapp_linked/src/outbound/pg/timeline.rs b/crates/whatsapp_linked/src/outbound/pg/timeline.rs
index 74ed930e7c0..d240fcc9009 100644
--- a/crates/whatsapp_linked/src/outbound/pg/timeline.rs
+++ b/crates/whatsapp_linked/src/outbound/pg/timeline.rs
@@ -1,6 +1,13 @@
//! Queries over standalone WhatsApp entities, messages and read cursors.
use super::*;
-use crate::domain::{ports::BindingRepository, timeline::*};
+use crate::domain::{
+ ports::BindingRepository,
+ rich::RichContent,
+ timeline::{
+ view::{self, People, StoredVote},
+ *,
+ },
+};
use async_trait::async_trait;
use models_permissions::share_permission::{
LinkShare, SharePermissionV2, UpdateSharePermissionRequestV2, access_level::AccessLevel,
@@ -13,12 +20,91 @@ fn backfill(state: Option<&str>, imported: i32) -> Option {
})
}
+impl PgLinkedRepository {
+ /// Names for WhatsApp ids in a group: people seen in it first (their
+ /// WhatsApp name and verified Macro user), then the owner's roster.
+ async fn people(&self, binding: &Binding, jids: &[String]) -> LinkedResult {
+ let mut people = People::default();
+ if jids.is_empty() {
+ return Ok(people);
+ }
+ let seen = sqlx::query!(
+ r#"SELECT a.jid, p.user_id, p.display_name FROM whatsapp_identity_alias a
+ JOIN whatsapp_external_person p ON p.id = a.person_id
+ WHERE a.binding_id = $1 AND a.jid = ANY($2)"#,
+ binding.id,
+ jids
+ )
+ .fetch_all(&self.pool)
+ .await
+ .map_err(failure)?;
+ let roster = sqlx::query_scalar!(
+ r#"SELECT r.metadata FROM whatsapp_group_roster r
+ JOIN whatsapp_linked_account a ON a.id = r.account_id
+ JOIN whatsapp_group g ON g.user_id = a.user_id AND g.group_jid = r.group_jid
+ WHERE g.id = $1"#,
+ binding.id
+ )
+ .fetch_optional(&self.pool)
+ .await
+ .map_err(failure)?
+ .map(decode::)
+ .transpose()?;
+ for row in seen {
+ let roster_name = roster.as_ref().and_then(|roster| {
+ roster
+ .participants
+ .iter()
+ .find(|participant| {
+ participant.identity.jid == row.jid
+ || participant.identity.alternate_jid.as_deref() == Some(&row.jid)
+ })
+ .and_then(|participant| participant.identity.label())
+ });
+ let name = row
+ .display_name
+ .filter(|name| !name.trim().is_empty())
+ .or(roster_name);
+ if let Some(name) = name {
+ people.learn(
+ &row.jid,
+ WhatsAppPersonRef {
+ name,
+ user_id: row.user_id,
+ },
+ );
+ }
+ }
+ for participant in roster.iter().flat_map(|roster| &roster.participants) {
+ let Some(name) = participant.identity.label() else {
+ continue;
+ };
+ for jid in [
+ Some(&participant.identity.jid),
+ participant.identity.alternate_jid.as_ref(),
+ ]
+ .into_iter()
+ .flatten()
+ {
+ people.learn(
+ jid,
+ WhatsAppPersonRef {
+ name: name.clone(),
+ user_id: None,
+ },
+ );
+ }
+ }
+ Ok(people)
+ }
+}
+
#[async_trait]
impl TimelineRepository for PgLinkedRepository {
async fn visible_groups(&self, user: &str) -> LinkedResult> {
let rows = sqlx::query!(r#"SELECT g.id,g.name,g.user_id,g.group_jid,g.state,g.adopted_at,g.backfill_state,g.backfill_imported,
(SELECT count(*) FROM whatsapp_group_message m WHERE m.binding_id=g.id AND m.message_id IS NOT NULL
- AND m.deleted_at IS NULL AND m.origin<>'history'
+ AND m.deleted_at IS NULL AND m.origin<>'history' AND m.kind<>'system'
AND NOT EXISTS(SELECT 1 FROM whatsapp_external_person p WHERE p.id=m.person_id AND p.user_id=$1)
AND (r.message_id IS NULL OR (m.source_at,m.id)>(r.read_at,r.message_id))) AS "unread_count!",
(SELECT max(source_at) FROM whatsapp_group_message WHERE binding_id=g.id AND message_id IS NOT NULL) AS last_message_at,
@@ -62,7 +148,7 @@ impl TimelineRepository for PgLinkedRepository {
async fn summary(&self, group: Uuid, user: &str) -> LinkedResult {
let row = sqlx::query!(r#"SELECT g.id,g.name,g.user_id,g.backfill_state,g.backfill_imported,
(SELECT count(*) FROM whatsapp_group_message m WHERE m.binding_id = g.id AND m.message_id IS NOT NULL
- AND m.deleted_at IS NULL AND m.origin <> 'history'
+ AND m.deleted_at IS NULL AND m.origin <> 'history' AND m.kind <> 'system'
AND NOT EXISTS (SELECT 1 FROM whatsapp_external_person p WHERE p.id = m.person_id AND p.user_id = $2)
AND (r.message_id IS NULL OR (m.source_at,m.id) > (r.read_at,r.message_id))) AS "unread_count!",
(SELECT max(source_at) FROM whatsapp_group_message WHERE binding_id = g.id AND message_id IS NOT NULL) AS last_message_at
@@ -87,7 +173,7 @@ impl TimelineRepository for PgLinkedRepository {
limit: i64,
) -> LinkedResult {
let rows = sqlx::query!(r#"SELECT m.id,m.person_id,p.user_id,COALESCE(p.display_name,'WhatsApp participant') AS "name!",
- m.body,m.reply_to,m.source_at,m.ingested_at,m.revised_at,m.deleted_at,
+ m.body,m.reply_to,m.source_at,m.ingested_at,m.revised_at,m.deleted_at,m.kind,m.rich,m.mentions,m.forwarded,
(SELECT state FROM whatsapp_send s WHERE s.message_id = m.id AND s.user_id = $2 ORDER BY s.created_at DESC LIMIT 1) AS delivery
FROM whatsapp_group_message m JOIN whatsapp_external_person p ON p.id = m.person_id
WHERE m.binding_id = $1 AND m.message_id IS NOT NULL
@@ -99,10 +185,58 @@ impl TimelineRepository for PgLinkedRepository {
let binding = self.binding(group).await?.ok_or(LinkedError::NotFound)?;
let ids: Vec<_> = rows.iter().take(limit as usize).map(|r| r.id).collect();
let attachments = self.message_attachments(&binding, &ids).await?;
+ let mut rich = Vec::with_capacity(ids.len());
+ let mut wanted = vec![];
+ for row in rows.iter().take(limit as usize) {
+ let content: Option = match (&row.deleted_at, &row.rich) {
+ (None, Some(value)) => Some(decode(value.clone())?),
+ _ => None,
+ };
+ if let Some(RichContent::System(line)) = &content {
+ wanted.extend(line.participants.iter().cloned());
+ }
+ wanted.extend(row.mentions.iter().cloned());
+ rich.push(content);
+ }
+ let people = self.people(&binding, &wanted).await?;
+ let polls: Vec = rows
+ .iter()
+ .zip(&rich)
+ .filter(|(_, content)| matches!(content, Some(RichContent::Poll(_))))
+ .map(|(row, _)| row.id)
+ .collect();
+ let votes: Vec = sqlx::query!(
+ r#"SELECT v.message_id, v.selected, p.user_id, COALESCE(p.display_name, 'WhatsApp participant') AS "name!"
+ FROM whatsapp_group_poll_vote v JOIN whatsapp_external_person p ON p.id = v.person_id
+ WHERE v.message_id = ANY($1) AND cardinality(v.selected) > 0
+ ORDER BY v.updated_at"#,
+ &polls
+ )
+ .fetch_all(&self.pool)
+ .await
+ .map_err(failure)?
+ .into_iter()
+ .map(|row| StoredVote {
+ message_id: row.message_id,
+ selected: row.selected,
+ voter: WhatsAppPersonRef {
+ name: row.name,
+ user_id: row.user_id,
+ },
+ })
+ .collect();
let reactions=sqlx::query!(r#"SELECT message_id,emoji,count(*) AS "count!",COALESCE(bool_or(user_id=$2),false) AS "mine!"
FROM whatsapp_group_reaction WHERE message_id=ANY($1) AND emoji<>'' GROUP BY message_id,emoji ORDER BY message_id,emoji"#,&ids,user)
.fetch_all(&self.pool).await.map_err(failure)?;
- for row in rows.into_iter().take(limit as usize) {
+ for (row, content) in rows.into_iter().take(limit as usize).zip(rich) {
+ let sender = WhatsAppPersonRef {
+ name: row.name.clone(),
+ user_id: row.user_id.clone(),
+ };
+ let message_votes: Vec<&StoredVote> = votes
+ .iter()
+ .filter(|vote| vote.message_id == row.id)
+ .collect();
let grouped = reactions
.iter()
.filter(|r| r.message_id == row.id)
@@ -136,6 +270,20 @@ impl TimelineRepository for PgLinkedRepository {
.map(|a| a.linked_attachments.clone())
.unwrap_or_default(),
delivery_state: row.delivery.as_deref().and_then(DeliveryState::parse),
+ kind: if row.kind == "system" {
+ WhatsAppMessageKind::System
+ } else {
+ WhatsAppMessageKind::Message
+ },
+ forwarded: row.forwarded,
+ mentions: if row.deleted_at.is_some() {
+ vec![]
+ } else {
+ view::mentions_view(&row.mentions, &people)
+ },
+ rich: content.and_then(|content| {
+ view::rich_view(content, &sender, &message_votes, user, &people)
+ }),
});
}
let next_cursor = more.then(|| messages.last().map(|m| m.id)).flatten();
diff --git a/docs/agent-guide/external.md b/docs/agent-guide/external.md
index e5b73b3f2d4..c79870b104b 100644
--- a/docs/agent-guide/external.md
+++ b/docs/agent-guide/external.md
@@ -67,16 +67,31 @@ The timeline pages newest-first from
replies, grouped reactions, edit/delete flags, attachment states, and delivery
for the caller's own sends. Entering the view marks messages read through `/read`.
Ready images and videos show inline in Chat's media grid (click opens the
-viewer), voice notes and audio in an inline player; their bytes come from the
-imported document's presigned URL. Other ready files show as document chips;
-other states show loading, unavailable or unsupported. Senders show their
-WhatsApp name. The header has the group name, a **N members** button that
+viewer), sized from WhatsApp's declared width/height before they load so the
+timeline opens at the bottom. Stickers show small and unframed, GIFs loop muted
+in place, voice notes read **Voice note · 0:42** above an inline player; bytes
+come from the imported document's presigned URL. Other ready files show as
+document chips; other states show loading, unavailable or unsupported. Senders
+show their WhatsApp name. `@` in a body shows the person: a verified
+Macro user as Chat's mention chip, anyone else their WhatsApp name in bold.
+Bare URLs are links with Chat's hover preview. **Forwarded** marks forwarded
+messages. Polls (read-only tallies, "Vote in WhatsApp"), locations (**Open in
+Maps**, no map tiles), contact cards and WhatsApp events render as small cards
+(`data-whatsapp-card="poll|location|contacts|event"`). Group changes, pins and
+calls (`kind: "system"`) are one-line system rows like Chat's
+(`data-system-activity=""`, e.g. "Ana added Ben", "Missed group video
+call"): no avatar, no actions, never unread. View-once and unknown types keep
+DSS's placeholder text. The header has the group name, a **N members** button that
toggles the WhatsApp roster, and **Share**. The layout supports phone-width panes.
The composer placeholder names the caller's own sending phone ("Message Test 11
as +1…"); otherwise the reason replies are unavailable shows in its place. Sending requires view access, the caller's own connected account,
and current membership that permits posting. A shared second member can send
-from their number without owning or re-adopting the group. Text/media/replies,
+from their number without owning or re-adopting the group. Typing `@` in the
+composer offers only the group's WhatsApp participants (not the sender, agents,
+Macro items or `@here`); a picked person is sent as `@` with
+`mentions: [jid]`, and a mentioning text follows files instead of captioning
+them. Text/media/replies,
reactions, and edits/deletes of the caller's own recent messages use
`POST /meta/linked/whatsapp-groups//sends`; files stage through `/attachments`.
The detail's `limits` supplies allowed MIME types, bytes and the edit window.
diff --git a/docs/architecture/whatsapp-linked-devices.md b/docs/architecture/whatsapp-linked-devices.md
index bf77cc47ff4..916329740a7 100644
--- a/docs/architecture/whatsapp-linked-devices.md
+++ b/docs/architecture/whatsapp-linked-devices.md
@@ -132,6 +132,23 @@ ambiguous are quarantined and retried. Missing reaction/revision targets
are deferred. Edits follow source revision order; deletes are permanent tombstones.
Reactions and media live in `whatsapp_group_reaction` and `whatsapp_group_attachment`.
+Besides text and media, a message may carry structured content in
+`whatsapp_group_message.rich` (`whatsapp_linked::domain::rich::RichContent`):
+polls, locations, contact cards and WhatsApp events, plus `mentions` (WhatsApp
+ids the text @-mentions) and `forwarded`. Poll votes are decrypted by the
+replica with the poll's message secret and kept per voter in
+`whatsapp_group_poll_vote` (latest revision wins); history pages bring them
+already decrypted. System lines (`kind = 'system'`) record what happened in a
+group: live `w:gp2` notifications (subject, description, disappearing timer,
+members added, removed, left, joined by link, promoted, demoted; each gets a
+stable synthetic source id), group icon changes, pins, call logs and the stub
+messages of history pages (including group creation). They never notify and
+never count as unread. The timeline resolves the ids in mentions, system lines
+and votes to names (people seen in the group, then the roster, then the phone
+number). View-once media and shapes Macro does not read stay readable
+placeholders. Attachments keep WhatsApp's declared width, height and length,
+and whether WhatsApp shows them as a sticker, GIF or voice note.
+
`GET /meta/linked/whatsapp-groups` lists shared groups with viewer-specific unread
counts and sending eligibility. Detail and messages live under
`/meta/linked/whatsapp-groups/{group_id}`. Messages page newest-first using stable
@@ -144,7 +161,7 @@ DSS drains a durable notification outbox through the generic notification ingres
channel members, and known link viewers who have read the group, except the verified sender. Link
viewers must still have access when recipients are resolved; unknown link visitors cannot be
enumerated. Notifications use the message id for idempotency and are suppressed
-for history, replays, edits and reactions. They link to the standalone group entity.
+for history, system lines, replays, edits, reactions and poll votes. They link to the standalone group entity.
GraphQL notification targets use the canonical entity enum while groups remain outside Soup.
## Sending
@@ -183,6 +200,9 @@ bytes are never rewritten. Failures retry with backoff and then expose an
Outbound: the attachments route stages one file per send in the blob table;
`ReplyContent::Media` names it, the replica fetches it with `outbound_media`,
and on acceptance it becomes the message's document, owned by the sender.
+A text send may @-mention participants (`ReplyContent::Text.mentions`, their
+WhatsApp ids; the text names each as `@`); the replica sends them as
+the message's `mentionedJid`.
## Passkey pairing
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/README.md b/packages/sdk-docs/reference/generated/storage/types.gen/README.md
index af906151dbf..533f972410b 100644
--- a/packages/sdk-docs/reference/generated/storage/types.gen/README.md
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/README.md
@@ -1384,6 +1384,7 @@
- [LinkedGroupsResponses](type-aliases/LinkedGroupsResponses.md)
- [LinkedIdentity](type-aliases/LinkedIdentity.md)
- [LinkedMediaKind](type-aliases/LinkedMediaKind.md)
+- [LinkedMediaPresentation](type-aliases/LinkedMediaPresentation.md)
- [LinkedMessageAttachments](type-aliases/LinkedMessageAttachments.md)
- [LinkedMessageAttachmentsData](type-aliases/LinkedMessageAttachmentsData.md)
- [LinkedMessageAttachmentsResponse](type-aliases/LinkedMessageAttachmentsResponse.md)
@@ -2251,6 +2252,11 @@
- [WebhookScope](type-aliases/WebhookScope.md)
- [WebhookStatus](type-aliases/WebhookStatus.md)
- [WebhookValidationTestEvent](type-aliases/WebhookValidationTestEvent.md)
+- [WhatsAppCall](type-aliases/WhatsAppCall.md)
+- [WhatsAppCallOutcome](type-aliases/WhatsAppCallOutcome.md)
+- [WhatsAppContactCard](type-aliases/WhatsAppContactCard.md)
+- [WhatsAppContacts](type-aliases/WhatsAppContacts.md)
+- [WhatsAppEvent](type-aliases/WhatsAppEvent.md)
- [WhatsappGroupData](type-aliases/WhatsappGroupData.md)
- [WhatsAppGroupDetail](type-aliases/WhatsAppGroupDetail.md)
- [WhatsAppGroupMessage](type-aliases/WhatsAppGroupMessage.md)
@@ -2278,6 +2284,15 @@
- [WhatsappGroupUpdatePermissionsData](type-aliases/WhatsappGroupUpdatePermissionsData.md)
- [WhatsappGroupUpdatePermissionsResponse](type-aliases/WhatsappGroupUpdatePermissionsResponse.md)
- [WhatsappGroupUpdatePermissionsResponses](type-aliases/WhatsappGroupUpdatePermissionsResponses.md)
+- [WhatsAppLocation](type-aliases/WhatsAppLocation.md)
+- [WhatsAppMention](type-aliases/WhatsAppMention.md)
+- [WhatsAppMessageKind](type-aliases/WhatsAppMessageKind.md)
+- [WhatsAppPersonRef](type-aliases/WhatsAppPersonRef.md)
+- [WhatsAppPoll](type-aliases/WhatsAppPoll.md)
+- [WhatsAppPollOption](type-aliases/WhatsAppPollOption.md)
+- [WhatsAppRichContent](type-aliases/WhatsAppRichContent.md)
+- [WhatsAppSystemAction](type-aliases/WhatsAppSystemAction.md)
+- [WhatsAppSystemLine](type-aliases/WhatsAppSystemLine.md)
- [Widget](type-aliases/Widget.md)
- [WithCallId](type-aliases/WithCallId.md)
- [WithChannelId](type-aliases/WithChannelId.md)
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/LinkedAttachment.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/LinkedAttachment.md
index d6c728e8be0..8481457f4b7 100644
--- a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/LinkedAttachment.md
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/LinkedAttachment.md
@@ -1,6 +1,6 @@
# Type Alias: LinkedAttachment
-> **LinkedAttachment** = \{ `document_id?`: `string` \| `null`; `file_name`: `string`; `mime`: `string`; `ordinal`: `number`; `size`: `number`; `state`: [`LinkedAttachmentState`](LinkedAttachmentState.md); \}
+> **LinkedAttachment** = \{ `document_id?`: `string` \| `null`; `duration_seconds?`: `number` \| `null`; `file_name`: `string`; `height?`: `number` \| `null`; `mime`: `string`; `ordinal`: `number`; `presentation?`: `null` \| [`LinkedMediaPresentation`](LinkedMediaPresentation.md); `size`: `number`; `state`: [`LinkedAttachmentState`](LinkedAttachmentState.md); `width?`: `number` \| `null`; \}
One attachment of a linked message.
@@ -9,8 +9,12 @@ One attachment of a linked message.
| Property | Type | Description |
| ------ | ------ | ------ |
| `document_id?` | `string` \| `null` | The private document, once ready. |
+| `duration_seconds?` | `number` \| `null` | Length of audio or video in whole seconds, when declared. |
| `file_name` | `string` | Display filename. |
+| `height?` | `number` \| `null` | Pixel height, when declared. |
| `mime` | `string` | Declared MIME type. |
| `ordinal` | `number` | Position within the message. |
+| `presentation?` | `null` \| [`LinkedMediaPresentation`](LinkedMediaPresentation.md) | - |
| `size` | `number` | Declared size in bytes. |
| `state` | [`LinkedAttachmentState`](LinkedAttachmentState.md) | Current state. |
+| `width?` | `number` \| `null` | Pixel width of an image, sticker or video, when WhatsApp declared it. |
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/LinkedMediaPresentation.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/LinkedMediaPresentation.md
new file mode 100644
index 00000000000..50c73109b6b
--- /dev/null
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/LinkedMediaPresentation.md
@@ -0,0 +1,5 @@
+# Type Alias: LinkedMediaPresentation
+
+> **LinkedMediaPresentation** = `"sticker"` \| `"gif"` \| `"voice_note"`
+
+How WhatsApp shows an attachment, when not as a plain file of its kind.
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/LinkedReplyContent.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/LinkedReplyContent.md
index d4eeff140f5..81fa116f664 100644
--- a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/LinkedReplyContent.md
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/LinkedReplyContent.md
@@ -1,6 +1,6 @@
# Type Alias: LinkedReplyContent
-> **LinkedReplyContent** = \{ `text`: `string`; `type`: `"text"`; \} \| \{ `caption?`: `string` \| `null`; `type`: `"media"`; `upload_id`: `string`; \} \| \{ `message_id`: `string`; `operation`: [`LinkedMutation`](LinkedMutation.md); `text?`: `string` \| `null`; `type`: `"mutation"`; \}
+> **LinkedReplyContent** = \{ `mentions?`: `string`[]; `text`: `string`; `type`: `"text"`; \} \| \{ `caption?`: `string` \| `null`; `type`: `"media"`; `upload_id`: `string`; \} \| \{ `message_id`: `string`; `operation`: [`LinkedMutation`](LinkedMutation.md); `text?`: `string` \| `null`; `type`: `"mutation"`; \}
Content of a deliberate send.
@@ -8,10 +8,11 @@ Content of a deliberate send.
### Type Literal
-\{ `text`: `string`; `type`: `"text"`; \}
+\{ `mentions?`: `string`[]; `text`: `string`; `type`: `"text"`; \}
| Name | Type | Description |
| ------ | ------ | ------ |
+| `mentions?` | `string`[] | Group participants the text @-mentions, by their WhatsApp id (`jid` of a roster participant); the text must contain `@` for each. |
| `text` | `string` | UTF-8 text, at most [`TEXT_LIMIT_BYTES`] bytes. |
| `type` | `"text"` | - |
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppCall.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppCall.md
new file mode 100644
index 00000000000..486414c10d3
--- /dev/null
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppCall.md
@@ -0,0 +1,13 @@
+# Type Alias: WhatsAppCall
+
+> **WhatsAppCall** = \{ `durationSeconds?`: `number` \| `null`; `outcome`: [`WhatsAppCallOutcome`](WhatsAppCallOutcome.md); `video`: `boolean`; \}
+
+A call in the group.
+
+## Properties
+
+| Property | Type | Description |
+| ------ | ------ | ------ |
+| `durationSeconds?` | `number` \| `null` | Length, when it connected. |
+| `outcome` | [`WhatsAppCallOutcome`](WhatsAppCallOutcome.md) | How it went for the account that saw it. |
+| `video` | `boolean` | Video rather than voice. |
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppCallOutcome.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppCallOutcome.md
new file mode 100644
index 00000000000..916410fe58d
--- /dev/null
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppCallOutcome.md
@@ -0,0 +1,5 @@
+# Type Alias: WhatsAppCallOutcome
+
+> **WhatsAppCallOutcome** = `"answered"` \| `"missed"` \| `"rejected"` \| `"other"`
+
+How a call went.
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppContactCard.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppContactCard.md
new file mode 100644
index 00000000000..abd0b94906e
--- /dev/null
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppContactCard.md
@@ -0,0 +1,12 @@
+# Type Alias: WhatsAppContactCard
+
+> **WhatsAppContactCard** = \{ `name`: `string`; `phones`: `string`[]; \}
+
+One contact card, from its vCard.
+
+## Properties
+
+| Property | Type | Description |
+| ------ | ------ | ------ |
+| `name` | `string` | Display name. |
+| `phones` | `string`[] | Phone numbers as written on the card. |
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppContacts.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppContacts.md
new file mode 100644
index 00000000000..6f0b8c7d7d1
--- /dev/null
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppContacts.md
@@ -0,0 +1,11 @@
+# Type Alias: WhatsAppContacts
+
+> **WhatsAppContacts** = \{ `contacts`: [`WhatsAppContactCard`](WhatsAppContactCard.md)[]; \}
+
+Contact cards.
+
+## Properties
+
+| Property | Type | Description |
+| ------ | ------ | ------ |
+| `contacts` | [`WhatsAppContactCard`](WhatsAppContactCard.md)[] | The cards, in order. |
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppEvent.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppEvent.md
new file mode 100644
index 00000000000..e688ff5e51f
--- /dev/null
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppEvent.md
@@ -0,0 +1,17 @@
+# Type Alias: WhatsAppEvent
+
+> **WhatsAppEvent** = \{ `canceled?`: `boolean`; `description?`: `string` \| `null`; `endAt?`: `string` \| `null`; `joinLink?`: `string` \| `null`; `location?`: `string` \| `null`; `name`: `string`; `startAt?`: `string` \| `null`; \}
+
+A WhatsApp event.
+
+## Properties
+
+| Property | Type | Description |
+| ------ | ------ | ------ |
+| `canceled?` | `boolean` | Canceled by its organizer. |
+| `description?` | `string` \| `null` | Description. |
+| `endAt?` | `string` \| `null` | End. |
+| `joinLink?` | `string` \| `null` | Call link for an online event. |
+| `location?` | `string` \| `null` | Place name or address. |
+| `name` | `string` | Title. |
+| `startAt?` | `string` \| `null` | Start. |
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppGroupMessage.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppGroupMessage.md
index 5619e97f9f6..7e4e9c6dfdb 100644
--- a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppGroupMessage.md
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppGroupMessage.md
@@ -1,6 +1,6 @@
# Type Alias: WhatsAppGroupMessage
-> **WhatsAppGroupMessage** = \{ `attachments`: [`LinkedAttachment`](LinkedAttachment.md)[]; `body`: `string`; `deleted`: `boolean`; `delivery_state?`: `null` \| [`LinkedDeliveryState`](LinkedDeliveryState.md); `edited_at?`: `string` \| `null`; `id`: `string`; `reactions`: [`WhatsAppGroupReaction`](WhatsAppGroupReaction.md)[]; `received_at`: `string`; `reply_to?`: `string` \| `null`; `sender`: [`WhatsAppGroupSender`](WhatsAppGroupSender.md); `sent_at`: `string`; \}
+> **WhatsAppGroupMessage** = \{ `attachments`: [`LinkedAttachment`](LinkedAttachment.md)[]; `body`: `string`; `deleted`: `boolean`; `delivery_state?`: `null` \| [`LinkedDeliveryState`](LinkedDeliveryState.md); `edited_at?`: `string` \| `null`; `forwarded`: `boolean`; `id`: `string`; `kind`: [`WhatsAppMessageKind`](WhatsAppMessageKind.md); `mentions`: [`WhatsAppMention`](WhatsAppMention.md)[]; `reactions`: [`WhatsAppGroupReaction`](WhatsAppGroupReaction.md)[]; `received_at`: `string`; `reply_to?`: `string` \| `null`; `rich?`: `null` \| [`WhatsAppRichContent`](WhatsAppRichContent.md); `sender`: [`WhatsAppGroupSender`](WhatsAppGroupSender.md); `sent_at`: `string`; \}
One message in an adopted group.
@@ -13,9 +13,13 @@ One message in an adopted group.
| `deleted` | `boolean` | Deleted for everyone. |
| `delivery_state?` | `null` \| [`LinkedDeliveryState`](LinkedDeliveryState.md) | - |
| `edited_at?` | `string` \| `null` | Last edit timestamp. |
+| `forwarded` | `boolean` | Forwarded from another chat. |
| `id` | `string` | Stable message id. |
+| `kind` | [`WhatsAppMessageKind`](WhatsAppMessageKind.md) | A message, or a system line. |
+| `mentions` | [`WhatsAppMention`](WhatsAppMention.md)[] | People the body @-mentions. |
| `reactions` | [`WhatsAppGroupReaction`](WhatsAppGroupReaction.md)[] | Reactions grouped by emoji. |
| `received_at` | `string` | Timestamp first received by Macro. |
| `reply_to?` | `string` \| `null` | Message quoted by this one. |
+| `rich?` | `null` \| [`WhatsAppRichContent`](WhatsAppRichContent.md) | - |
| `sender` | [`WhatsAppGroupSender`](WhatsAppGroupSender.md) | Sender. |
| `sent_at` | `string` | Timestamp assigned by WhatsApp. |
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppLocation.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppLocation.md
new file mode 100644
index 00000000000..c582f0cde1e
--- /dev/null
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppLocation.md
@@ -0,0 +1,17 @@
+# Type Alias: WhatsAppLocation
+
+> **WhatsAppLocation** = \{ `address?`: `string` \| `null`; `comment?`: `string` \| `null`; `latitude`: `number`; `live?`: `boolean`; `longitude`: `number`; `name?`: `string` \| `null`; `url?`: `string` \| `null`; \}
+
+A shared place.
+
+## Properties
+
+| Property | Type | Description |
+| ------ | ------ | ------ |
+| `address?` | `string` \| `null` | Street address. |
+| `comment?` | `string` \| `null` | Caption or comment. |
+| `latitude` | `number` | Degrees. |
+| `live?` | `boolean` | Shared as a live location (this is where it started). |
+| `longitude` | `number` | Degrees. |
+| `name?` | `string` \| `null` | Place name. |
+| `url?` | `string` \| `null` | Link the sender attached. |
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppMention.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppMention.md
new file mode 100644
index 00000000000..ac2b0f61751
--- /dev/null
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppMention.md
@@ -0,0 +1,11 @@
+# Type Alias: WhatsAppMention
+
+> **WhatsAppMention** = [`WhatsAppPersonRef`](WhatsAppPersonRef.md) & \{ `token`: `string`; \}
+
+An @-mention in a message body.
+
+## Type Declaration
+
+| Name | Type | Description |
+| ------ | ------ | ------ |
+| `token` | `string` | What follows the `@` in the body (the digits of the person's WhatsApp id); replace `@` with the name. |
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppMessageKind.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppMessageKind.md
new file mode 100644
index 00000000000..82078230a3c
--- /dev/null
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppMessageKind.md
@@ -0,0 +1,5 @@
+# Type Alias: WhatsAppMessageKind
+
+> **WhatsAppMessageKind** = `"message"` \| `"system"`
+
+What a timeline entry is.
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppPersonRef.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppPersonRef.md
new file mode 100644
index 00000000000..0528ba34a33
--- /dev/null
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppPersonRef.md
@@ -0,0 +1,13 @@
+# Type Alias: WhatsAppPersonRef
+
+> **WhatsAppPersonRef** = \{ `name`: `string`; `userId?`: `string` \| `null`; \}
+
+A person named in a message: a Macro user when verified, else their
+WhatsApp name, else their phone number.
+
+## Properties
+
+| Property | Type | Description |
+| ------ | ------ | ------ |
+| `name` | `string` | Display name, never authentication evidence. |
+| `userId?` | `string` \| `null` | Verified Macro user, if any. |
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppPoll.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppPoll.md
new file mode 100644
index 00000000000..ebd5431efa1
--- /dev/null
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppPoll.md
@@ -0,0 +1,14 @@
+# Type Alias: WhatsAppPoll
+
+> **WhatsAppPoll** = \{ `options`: [`WhatsAppPollOption`](WhatsAppPollOption.md)[]; `question`: `string`; `selectableCount`: `number`; `voters`: `number`; \}
+
+A poll, read-only in Macro.
+
+## Properties
+
+| Property | Type | Description |
+| ------ | ------ | ------ |
+| `options` | [`WhatsAppPollOption`](WhatsAppPollOption.md)[] | Options in order, with tallies. |
+| `question` | `string` | The question. |
+| `selectableCount` | `number` | How many options one person may choose; 0 means any number. |
+| `voters` | `number` | People who voted for anything. |
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppPollOption.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppPollOption.md
new file mode 100644
index 00000000000..3a271280c41
--- /dev/null
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppPollOption.md
@@ -0,0 +1,14 @@
+# Type Alias: WhatsAppPollOption
+
+> **WhatsAppPollOption** = \{ `mine`: `boolean`; `name`: `string`; `voters`: [`WhatsAppPersonRef`](WhatsAppPersonRef.md)[]; `votes`: `number`; \}
+
+One option of a poll with its votes.
+
+## Properties
+
+| Property | Type | Description |
+| ------ | ------ | ------ |
+| `mine` | `boolean` | The caller chose it. |
+| `name` | `string` | Option text. |
+| `voters` | [`WhatsAppPersonRef`](WhatsAppPersonRef.md)[] | Who chose it. |
+| `votes` | `number` | People who chose it. |
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppRichContent.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppRichContent.md
new file mode 100644
index 00000000000..d98ce6f599f
--- /dev/null
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppRichContent.md
@@ -0,0 +1,5 @@
+# Type Alias: WhatsAppRichContent
+
+> **WhatsAppRichContent** = [`WhatsAppPoll`](WhatsAppPoll.md) & \{ `type`: `"poll"`; \} \| [`WhatsAppLocation`](WhatsAppLocation.md) & \{ `type`: `"location"`; \} \| [`WhatsAppContacts`](WhatsAppContacts.md) & \{ `type`: `"contacts"`; \} \| [`WhatsAppEvent`](WhatsAppEvent.md) & \{ `type`: `"event"`; \} \| [`WhatsAppSystemLine`](WhatsAppSystemLine.md) & \{ `type`: `"system"`; \}
+
+Structured content of a message besides its body.
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppSystemAction.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppSystemAction.md
new file mode 100644
index 00000000000..6ea2717ad27
--- /dev/null
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppSystemAction.md
@@ -0,0 +1,5 @@
+# Type Alias: WhatsAppSystemAction
+
+> **WhatsAppSystemAction** = `"group_created"` \| `"participants_added"` \| `"participants_removed"` \| `"participants_left"` \| `"participants_joined"` \| `"participants_promoted"` \| `"participants_demoted"` \| `"subject_changed"` \| `"icon_changed"` \| `"description_changed"` \| `"disappearing_changed"` \| `"message_pinned"` \| `"message_unpinned"` \| `"call"`
+
+Kinds of system lines.
diff --git a/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppSystemLine.md b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppSystemLine.md
new file mode 100644
index 00000000000..a156d9ece8e
--- /dev/null
+++ b/packages/sdk-docs/reference/generated/storage/types.gen/type-aliases/WhatsAppSystemLine.md
@@ -0,0 +1,15 @@
+# Type Alias: WhatsAppSystemLine
+
+> **WhatsAppSystemLine** = \{ `action`: [`WhatsAppSystemAction`](WhatsAppSystemAction.md); `actor?`: `null` \| [`WhatsAppPersonRef`](WhatsAppPersonRef.md); `call?`: `null` \| [`WhatsAppCall`](WhatsAppCall.md); `participants`: [`WhatsAppPersonRef`](WhatsAppPersonRef.md)[]; `value?`: `string` \| `null`; \}
+
+Something that happened in the group.
+
+## Properties
+
+| Property | Type | Description |
+| ------ | ------ | ------ |
+| `action` | [`WhatsAppSystemAction`](WhatsAppSystemAction.md) | What happened. |
+| `actor?` | `null` \| [`WhatsAppPersonRef`](WhatsAppPersonRef.md) | - |
+| `call?` | `null` \| [`WhatsAppCall`](WhatsAppCall.md) | - |
+| `participants` | [`WhatsAppPersonRef`](WhatsAppPersonRef.md)[] | Who it happened to (added, removed, promoted, …). |
+| `value?` | `string` \| `null` | New subject or description; disappearing-messages timer in seconds (`"0"` is off). |
diff --git a/packages/sdk-docs/reference/src/entities/whatsapp/group/type-aliases/WhatsAppReply.md b/packages/sdk-docs/reference/src/entities/whatsapp/group/type-aliases/WhatsAppReply.md
index 9c20a7b72c5..51beda92d8b 100644
--- a/packages/sdk-docs/reference/src/entities/whatsapp/group/type-aliases/WhatsAppReply.md
+++ b/packages/sdk-docs/reference/src/entities/whatsapp/group/type-aliases/WhatsAppReply.md
@@ -1,5 +1,29 @@
# Type Alias: WhatsAppReply
-> **WhatsAppReply** = \{ `text`: `string`; `type`: `"text"`; \} \| \{ `caption?`: `string`; `type`: `"media"`; `upload`: [`WhatsAppUpload`](../../upload/classes/WhatsAppUpload.md); \} \| \{ `message`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); `operation`: `"reaction"` \| `"edit"` \| `"delete"`; `text?`: `string`; `type`: `"mutation"`; \}
+> **WhatsAppReply** = \{ `mentions?`: `string`[]; `text`: `string`; `type`: `"text"`; \} \| \{ `caption?`: `string`; `type`: `"media"`; `upload`: [`WhatsAppUpload`](../../upload/classes/WhatsAppUpload.md); \} \| \{ `message`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); `operation`: `"reaction"` \| `"edit"` \| `"delete"`; `text?`: `string`; `type`: `"mutation"`; \}
Deliberate content from the credential owner's own WhatsApp account.
+
+## Union Members
+
+### Type Literal
+
+\{ `mentions?`: `string`[]; `text`: `string`; `type`: `"text"`; \}
+
+| Name | Type | Description |
+| ------ | ------ | ------ |
+| `mentions?` | `string`[] | WhatsApp ids of mentioned participants (`roster.participants[].jid`); the text names each as `@`. |
+| `text` | `string` | - |
+| `type` | `"text"` | - |
+
+***
+
+### Type Literal
+
+\{ `caption?`: `string`; `type`: `"media"`; `upload`: [`WhatsAppUpload`](../../upload/classes/WhatsAppUpload.md); \}
+
+***
+
+### Type Literal
+
+\{ `message`: [`WhatsAppMessage`](../../message/classes/WhatsAppMessage.md); `operation`: `"reaction"` \| `"edit"` \| `"delete"`; `text?`: `string`; `type`: `"mutation"`; \}
diff --git a/packages/sdk-docs/reference/src/entities/whatsapp/message/classes/WhatsAppMessage.md b/packages/sdk-docs/reference/src/entities/whatsapp/message/classes/WhatsAppMessage.md
index e35ed77045e..5539192bd56 100644
--- a/packages/sdk-docs/reference/src/entities/whatsapp/message/classes/WhatsAppMessage.md
+++ b/packages/sdk-docs/reference/src/entities/whatsapp/message/classes/WhatsAppMessage.md
@@ -10,14 +10,18 @@ Message in an independently shared WhatsApp group.
| Property | Modifier | Type | Description | Inherited from |
| ------ | ------ | ------ | ------ | ------ |
-| `attachments` | `readonly` | () => `Promise`\<\{ `document`: [`Document`](../../../documents/document/classes/Document.md) \| `undefined`; `fileName`: `string`; `mime`: `string`; `ordinal`: `number`; `size`: `number`; `state`: [`LinkedAttachmentState`](../../../../../generated/storage/types.gen/type-aliases/LinkedAttachmentState.md); \}[]\> | Attachment state and imported document handles. | - |
+| `attachments` | `readonly` | () => `Promise`\<\{ `document`: [`Document`](../../../documents/document/classes/Document.md) \| `undefined`; `durationSeconds`: `number` \| `undefined`; `fileName`: `string`; `height`: `number` \| `undefined`; `mime`: `string`; `ordinal`: `number`; `presentation`: [`LinkedMediaPresentation`](../../../../../generated/storage/types.gen/type-aliases/LinkedMediaPresentation.md) \| `undefined`; `size`: `number`; `state`: [`LinkedAttachmentState`](../../../../../generated/storage/types.gen/type-aliases/LinkedAttachmentState.md); `width`: `number` \| `undefined`; \}[]\> | Attachment state and imported document handles. | - |
| `body` | `readonly` | () => `Promise`\<`string`\> | Plain WhatsApp text. | - |
| `deleted` | `readonly` | () => `Promise`\<`boolean`\> | Deleted for everyone. | - |
| `deliveryState` | `readonly` | () => `Promise`\<`NonNullable`\ \| `undefined`\> | Caller-owned send's delivery state, when present. | - |
| `editedAt` | `readonly` | () => `Promise`\<`string` \| `undefined`\> | Last edit time. | - |
+| `forwarded` | `readonly` | () => `Promise`\<`boolean`\> | Forwarded from another chat. | - |
| `id` | `public readonly` | `string` | - | [`MacroEntity`](../../../entity/classes/MacroEntity.md).[`id`](../../../entity/classes/MacroEntity.md#property-id) |
+| `kind` | `readonly` | () => `Promise`\<[`WhatsAppMessageKind`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppMessageKind.md)\> | `message`, or `system` for something that happened in the group (see `rich`). | - |
+| `mentions` | `readonly` | () => `Promise`\<[`WhatsAppMention`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppMention.md)[]\> | People the body @-mentions: `@` in the body is this person. | - |
| `reactions` | `readonly` | () => `Promise`\<[`WhatsAppGroupReaction`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupReaction.md)[]\> | Grouped emoji counts and caller's own reaction. | - |
| `replyTo` | `readonly` | () => `Promise`\<`WhatsAppMessage` \| `undefined`\> | Quoted message in this group. | - |
+| `rich` | `readonly` | () => `Promise`\<`NonNullable`\ \| `undefined`\> | Poll (with tallies), location, contact cards, event or system line. | - |
| `sender` | `readonly` | () => `Promise`\<[`WhatsAppGroupSender`](../../../../../generated/storage/types.gen/type-aliases/WhatsAppGroupSender.md)\> | Protocol-verified sender association and display name. | - |
| `sentAt` | `readonly` | () => `Promise`\<`string`\> | WhatsApp timestamp. | - |
diff --git a/packages/sdk/generated/storage/index.ts b/packages/sdk/generated/storage/index.ts
index d06a8aad049..083965cda61 100644
--- a/packages/sdk/generated/storage/index.ts
+++ b/packages/sdk/generated/storage/index.ts
@@ -1,4 +1,4 @@
// This file is auto-generated by @hey-api/openapi-ts
export { type Options, Sdk } from './sdk.gen';
-export type { AccessiblePipeline, AccessLevel, AccessRequest, AccessRequestsCancelData, AccessRequestsCancelErrors, AccessRequestsCancelResponses, AccessRequestsDirectData, AccessRequestsDirectErrors, AccessRequestsDirectResponse, AccessRequestsDirectResponses, AccessRequestsRemoveDirectData, AccessRequestsRemoveDirectErrors, AccessRequestsRemoveDirectResponses, AccessRequestsResolveData, AccessRequestsResolveErrors, AccessRequestsResolveResponse, AccessRequestsResolveResponses, AccessRequestsSetDirectData, AccessRequestsSetDirectErrors, AccessRequestsSetDirectResponses, AccessRequestsStateData, AccessRequestsStateErrors, AccessRequestsStateResponse, AccessRequestsStateResponses, AccessRequestsSubmitData, AccessRequestsSubmitErrors, AccessRequestsSubmitResponse, AccessRequestsSubmitResponses, AccessRequestState, ActiveCallsResponse, ActiveCallSummary, ActiveMeeting, ActiveMeetingsResponse, ActivityType, AddFavoriteData, AddFavoriteError, AddFavoriteErrors, AddFavoriteRequest, AddFavoriteResponse, AddFavoriteResponses, AddParticipantsData, AddParticipantsError, AddParticipantsErrors, AddParticipantsRequest, AddParticipantsResponses, AddPinHandlerData, AddPinHandlerError, AddPinHandlerErrors, AddPinHandlerResponse, AddPinHandlerResponses, AddPinRequest, AdoptedGroup, Agent, AgentActionId, AgentChannelScope, AgentDmProfile, AgentMcpServer, AgentMcpServers, AgentPullRequestState, AgentSessionFilters, AgentSessionLifecycleEvent, AgentSessionSource, AgentSessionVisibility, AgentTyping, AgentTypingPhase, Anchor, AnchorId, AnchorResponse, AnnotationIncrementalUpdate, Answer, ApiActivity, ApiAttachmentChannelReference, ApiAttachmentCommentReference, ApiAttachmentEntityReference, ApiAttachmentGenericReference, ApiChannelAttachment, ApiChannelAttachmentsPage, ApiChannelDetail, ApiChannelListMessage, ApiChannelListPage, ApiChannelListParticipant, ApiChannelListType, ApiChannelParticipant, ApiChannelWithLatest, ApiEntityFilterAst, ApiGroupByField, ApiGroupMeta, ApiParticipantListRole, ApiParticipantRole, ApiPropertyEntityType, AppliedOps, ApplyCrmPipelineOpsData, ApplyCrmPipelineOpsResponse, ApplyCrmPipelineOpsResponses, ApplyDatabaseOpsData, ApplyDatabaseOpsError, ApplyDatabaseOpsErrors, ApplyDatabaseOpsResponse, ApplyDatabaseOpsResponses, ApplyOpsRequest, ApplyOpsResponse, ApproveHarnessPairingData, ApproveHarnessPairingError, ApproveHarnessPairingErrors, ApproveHarnessPairingResponse, ApproveHarnessPairingResponses, ApprovePairingRequest, AttachmentChange, AttendeeResponseStatus, Audience, Awareness, AwarenessRelay, BasicDocument, BasicDocumentSubType, BomPart, BookingTarget, Bot, BotChannel, BotChannelType, BotId, BotKind, BotOwner, BotOwnerProfile, BotSenderProfile, BotToken, BranchNameResponse, BTreeMap, BulkWakeupRequest, BulkWakeupResponse, BulkWakeupSyncServiceDocumentsData, BulkWakeupSyncServiceDocumentsErrors, BulkWakeupSyncServiceDocumentsResponse, BulkWakeupSyncServiceDocumentsResponses, CalendarAttendee, CalendarEvent, CalendarEventFilters, CalendarEventSourceContent, CalendarJoinRequest, CalendarJoinRequestApiError, CalendarJoinRequestErrorCode, CalendarJoinRequestsResponse, CalendarJoinRequestStatus, CalendarMentionEvent, CalendarMentionPreviewItem, CalendarMentionPreviewKind, CalendarMentionPreviewRequest, CalendarMentionPreviewRequestItem, CalendarMentionPreviewResponse, CalendarMentionViewerCopy, CalendarOccurrence, CalendarOccurrenceItem, CalendarOccurrenceResponse, CalendarSyncStatus, CallActiveResponse, CallFilters, CallProvider, CallRecord, CallRecordGuest, CallRecordParticipant, CallRecordPreview, CallRecordPreviewData, CallRecordSource, CallRecordTranscriptSegment, CallStatus, CallTokenResponse, CallWebhookData, CallWebhookError, CallWebhookErrors, CallWebhookResponses, CancelSlackImportData, CancelSlackImportErrors, CancelSlackImportResponse, CancelSlackImportResponses, CardPosition, CastVerdict, CellValue, CellWrite, ChangedFileDto, ChangesetDto, ChangesetSourceDto, Channel, ChannelCreatedMetadata, ChannelDeletedMetadata, ChannelFilters, ChannelJoinCodeResponse, ChannelLabel, ChannelLabelRule, ChannelLabelsList, ChannelMessage, ChannelParticipant, ChannelParticipantAddedMetadata, ChannelParticipantRemovedMetadata, ChannelPictureChangedMetadata, ChannelPreview, ChannelPreviewData, ChannelSender, ChannelSharePermission, ChannelThreadFilters, ChannelTopicEvent, ChannelType, ChannelUpdatedMetadata, ChannelWebhookRequest, ChannelWebhookResponse, ChannelWithParticipants, Chat, ChatFilters, CheckActiveCallData, CheckActiveCallError, CheckActiveCallErrors, CheckActiveCallResponse, CheckActiveCallResponses, ClaimedPairing, ClaimHarnessPairingData, ClaimHarnessPairingError, ClaimHarnessPairingErrors, ClaimHarnessPairingResponse, ClaimHarnessPairingResponses, ClaimPairingRequest, ClientOptions, CloudStorageItemType, CollaborateFormData, CollaborateFormError, CollaborateFormErrors, CollaborateFormResponse, CollaborateFormResponses, CollabSurfaceResponse, CollabSurfaceTokenResponse, Column, ColumnCast, ColumnChange, ColumnChangeKind, ColumnConfig, ColumnConversion, ColumnConversionRequest, ColumnDetail, ColumnId, ColumnKind, ColumnProtection, ColumnResult, CommandFailure, CommandRejectedMetadata, CommittedChange, CompleteSlackImportUploadsData, CompleteSlackImportUploadsErrors, CompleteSlackImportUploadsResponse, CompleteSlackImportUploadsResponses, CompleteUploads, ConferenceProvider, Conjunction, ConversationId, ConversationKind, ConversationMetadata, ConversationProgress, ConversationSeal, ConversationStatus, ConvertDatabaseColumnData, ConvertDatabaseColumnError, ConvertDatabaseColumnErrors, ConvertDatabaseColumnResponse, ConvertDatabaseColumnResponses, ConvertedCell, CopyDocumentData, CopyDocumentError, CopyDocumentErrors, CopyDocumentQueryParams, CopyDocumentRequest, CopyDocumentResponse, CopyDocumentResponse2, CopyDocumentResponses, CountedReaction, CreateAgentData, CreateAgentError, CreateAgentErrors, CreateAgentRequest, CreateAgentResponse, CreateAgentResponses, CreateAnchorData, CreateAnchorError, CreateAnchorErrors, CreateAnchorResponse, CreateAnchorResponses, CreateBotRequest, CreateBulkDocumentResponse, CreateBulkDocumentResponseData, CreateCanvasDocumentRequest, CreateCanvasDocumentResponse, CreateCanvasHandlerData, CreateCanvasHandlerError, CreateCanvasHandlerErrors, CreateCanvasHandlerResponse, CreateCanvasHandlerResponses, CreateChannelData, CreateChannelError, CreateChannelErrors, CreateChannelLabelData, CreateChannelLabelError, CreateChannelLabelErrors, CreateChannelLabelRequest, CreateChannelLabelResponse, CreateChannelLabelResponses, CreateChannelRequest, CreateChannelResponse, CreateChannelResponse2, CreateChannelResponses, CreateChannelScopedBotData, CreateChannelScopedBotError, CreateChannelScopedBotErrors, CreateChannelScopedBotRequest, CreateChannelScopedBotResponse, CreateChannelScopedBotResponse2, CreateChannelScopedBotResponses, CreateCollabSurfaceTokenData, CreateCollabSurfaceTokenError, CreateCollabSurfaceTokenErrors, CreateCollabSurfaceTokenResponse, CreateCollabSurfaceTokenResponses, CreateCrmCompanyData, CreateCrmCompanyError, CreateCrmCompanyErrors, CreateCrmCompanyRequest, CreateCrmCompanyResponse, CreateCrmCompanyResponses, CreateCrmContactData, CreateCrmContactError, CreateCrmContactErrors, CreateCrmContactRequest, CreateCrmContactResponse, CreateCrmContactResponses, CreateCrmPipelineData, CreateCrmPipelineResponse, CreateCrmPipelineResponses, CreateDatabaseData, CreateDatabaseError, CreateDatabaseErrors, CreateDatabaseRequest, CreateDatabaseResponse, CreateDatabaseResponses, CreateDatabaseWebhookData, CreateDatabaseWebhookError, CreateDatabaseWebhookErrors, CreateDatabaseWebhookRequest, CreateDatabaseWebhookResponse, CreateDatabaseWebhookResponses, CreatedDatabaseWebhookResponse, CreateDocumentData, CreateDocumentError, CreateDocumentErrors, CreateDocumentRequest, CreateDocumentResponse, CreateDocumentResponseData, CreateDocumentResponses, CreatedPairing, CreatedUserApiKey, CreateEntityMentionData, CreateEntityMentionError, CreateEntityMentionErrors, CreateEntityMentionRequest, CreateEntityMentionResponse, CreateEntityMentionResponse2, CreateEntityMentionResponses, CreateForm, CreateFormData, CreateFormError, CreateFormErrors, CreateFormResponse, CreateFormResponses, CreateHarnessPairingData, CreateHarnessPairingError, CreateHarnessPairingErrors, CreateHarnessPairingResponse, CreateHarnessPairingResponses, CreateImport, CreateInitiativeData, CreateInitiativeError, CreateInitiativeErrors, CreateInitiativeRequest, CreateInitiativeResponse, CreateInitiativeResponses, CreateInstructionsDocumentResponse, CreateInstructionsHandlerData, CreateInstructionsHandlerError, CreateInstructionsHandlerErrors, CreateInstructionsHandlerResponse, CreateInstructionsHandlerResponses, CreateMarkdownDocumentRequest, CreateMarkdownDocumentResponse, CreateMarkdownHandlerData, CreateMarkdownHandlerError, CreateMarkdownHandlerErrors, CreateMarkdownHandlerResponse, CreateMarkdownHandlerResponses, CreateMeetingRequest, CreatePairingRequest, CreatePipeline, CreateProjectHandlerData, CreateProjectHandlerError, CreateProjectHandlerErrors, CreateProjectHandlerResponse, CreateProjectHandlerResponses, CreateProjectRequest, CreateProjectResponse, CreateSkillHandlerData, CreateSkillHandlerError, CreateSkillHandlerErrors, CreateSkillHandlerResponse, CreateSkillHandlerResponses, CreateSkillRequest, CreateSkillResponse, CreateSlackImportData, CreateSlackImportErrors, CreateSlackImportResponse, CreateSlackImportResponses, CreateSnippetHandlerData, CreateSnippetHandlerError, CreateSnippetHandlerErrors, CreateSnippetHandlerResponse, CreateSnippetHandlerResponses, CreateSnippetRequest, CreateSnippetResponse, CreateTaskHandlerData, CreateTaskHandlerError, CreateTaskHandlerErrors, CreateTaskHandlerResponse, CreateTaskHandlerResponses, CreateTaskRequest, CreateTaskResponse, CreateToken, CreateUnthreadedAnchorRequest, CreateUnthreadedAnchorResponse, CreateUnthreadedPdfAnchorRequest, CreateUserApiKeyData, CreateUserApiKeyError, CreateUserApiKeyErrors, CreateUserApiKeyRequest, CreateUserApiKeyResponse, CreateUserApiKeyResponses, CreateViewHandlerData, CreateViewHandlerError, CreateViewHandlerErrors, CreateViewHandlerResponse, CreateViewHandlerResponses, CreateViewRequest, CreateWebhookData, CreateWebhookError, CreateWebhookErrors, CreateWebhookRequest, CreateWebhookResponse, CreateWebhookResponse2, CreateWebhookResponses, CrmAssociation, CrmCompanyFilters, CrmCompanyResponse, CrmContactResponse, CrmDomainResponse, CrmPermissionRole, CrmStageInput, CrmStageResponse, CrmStagesResponse, CrmTeamSettingsResponse, CustomSpeakerAssignment, Database, DatabaseDetail, DatabaseId, DatabaseOp, DatabaseQueryRequest, DatabaseView, DatabaseWebhookDeliveryResponse, DatabaseWebhookPayloadProblem, DatabaseWebhookResponse, DataType, DateOperator, DeclineCallData, DeclineCallError, DeclineCallErrors, DeclineCallResponse, DeclineCallResponses, DeclineRingData, DeclineRingError, DeclineRingErrors, DeclineRingResponse, DeclineRingResponses, DeleteAnchorData, DeleteAnchorError, DeleteAnchorErrors, DeleteAnchorResponse, DeleteAnchorResponses, DeleteCallRecordData, DeleteCallRecordError, DeleteCallRecordErrors, DeleteCallRecordResponse, DeleteCallRecordResponses, DeleteChannelData, DeleteChannelError, DeleteChannelErrors, DeleteChannelLabelData, DeleteChannelLabelError, DeleteChannelLabelErrors, DeleteChannelLabelResponse, DeleteChannelLabelResponses, DeleteChannelResponse, DeleteChannelResponses, DeleteCollabSurfaceData, DeleteCollabSurfaceError, DeleteCollabSurfaceErrors, DeleteCollabSurfaceResponse, DeleteCollabSurfaceResponses, DeleteDatabaseWebhookData, DeleteDatabaseWebhookError, DeleteDatabaseWebhookErrors, DeleteDatabaseWebhookResponse, DeleteDatabaseWebhookResponses, DeleteDocumentData, DeleteDocumentError, DeleteDocumentErrors, DeleteDocumentResponse, DeleteDocumentResponses, DeleteEntityMentionData, DeleteEntityMentionError, DeleteEntityMentionErrors, DeleteEntityMentionResponse, DeleteEntityMentionResponse2, DeleteEntityMentionResponses, DeleteHarnessData, DeleteHarnessError, DeleteHarnessErrors, DeleteHarnessResponse, DeleteHarnessResponses, DeleteHistoryHandlerData, DeleteHistoryHandlerError, DeleteHistoryHandlerErrors, DeleteHistoryHandlerResponse, DeleteHistoryHandlerResponses, DeleteInitiativeData, DeleteInitiativeError, DeleteInitiativeErrors, DeleteInitiativeResponse, DeleteInitiativeResponses, DeleteProjectHandlerData, DeleteProjectHandlerError, DeleteProjectHandlerErrors, DeleteProjectHandlerResponse, DeleteProjectHandlerResponses, DeleteSelfHarnessData, DeleteSelfHarnessError, DeleteSelfHarnessErrors, DeleteSelfHarnessResponse, DeleteSelfHarnessResponses, DeleteUnthreadedAnchorRequest, DeleteUnthreadedAnchorResponse, DeleteUnthreadedPdfAnchorRequest, DeleteUserApiKeyData, DeleteUserApiKeyError, DeleteUserApiKeyErrors, DeleteUserApiKeyResponse, DeleteUserApiKeyResponses, DeleteUserDocumentViewLocationData, DeleteUserDocumentViewLocationError, DeleteUserDocumentViewLocationErrors, DeleteUserDocumentViewLocationResponse, DeleteUserDocumentViewLocationResponses, DeleteViewHandlerData, DeleteViewHandlerError, DeleteViewHandlerErrors, DeleteViewHandlerResponses, DeleteWebhookData, DeleteWebhookError, DeleteWebhookErrors, DeleteWebhookResponse, DeleteWebhookResponses, DeliverDatabaseWebhookData, DeliverDatabaseWebhookError, DeliverDatabaseWebhookErrors, DeliverDatabaseWebhookResponse, DeliverDatabaseWebhookResponses, DirectAccess, DocumentBasic, DocumentContent, DocumentContentLocation, DocumentContentState, DocumentContentUploadedMetadata, DocumentCopiedMetadata, DocumentCreatedMetadata, DocumentDeletedMetadata, DocumentFilters, DocumentId, DocumentInteractionMetadata, DocumentMetadata, DocumentMetadataWithContent, DocumentPermissionsToken, DocumentPermissionsTokenRequest, DocumentPermissionsTokenResponse, DocumentPreview, DocumentPreviewData, DocumentPreviewDataSubType, DocumentPurgedMetadata, DocumentResponse, DocumentResponseMetadata, DocumentResponseMetadataWithContent, DocumentStorageServiceApiVersion, DocumentSubType, DocumentSyncContentUpdatedMetadata, DocumentSyncEditor, DocumentTeamShareResponse, DocumentTextContent, DocumentTopicEvent, DocumentUpdatedMetadata, EditAnchorData, EditAnchorError, EditAnchorErrors, EditAnchorRequest, EditAnchorResponse, EditAnchorResponse2, EditAnchorResponses, EditCallRecordData, EditCallRecordError, EditCallRecordErrors, EditCallRecordRequest, EditCallRecordResponse, EditCallRecordResponses, EditCallTranscriptData, EditCallTranscriptError, EditCallTranscriptErrors, EditCallTranscriptRequest, EditCallTranscriptResponse, EditCallTranscriptResponses, EditDocumentData, EditDocumentError, EditDocumentErrors, EditDocumentResponse, EditDocumentResponse2, EditDocumentResponses, EditDocumentServiceArgs, EditMyFormResponseData, EditMyFormResponseError, EditMyFormResponseErrors, EditMyFormResponseResponse, EditMyFormResponseResponses, EditPdfAnchorRequest, EditPdfPlaceableCommentAnchorRequest, EditProjectV2Data, EditProjectV2Error, EditProjectV2Errors, EditProjectV2Response, EditProjectV2Responses, EditThreadV2Data, EditThreadV2Error, EditThreadV2Errors, EditThreadV2Response, EditThreadV2Responses, EmailFilters, EmailFollowup, EmailFollowupCommand, EmailFollowupResponse, EmailReminderCondition, EmailReminderPage, EmailReminderSummary, EmptyResponse, EnsureCollabSurfaceData, EnsureCollabSurfaceError, EnsureCollabSurfaceErrors, EnsureCollabSurfaceRequest, EnsureCollabSurfaceResponse, EnsureCollabSurfaceResponses, EnsureStarterHandlerData, EnsureStarterHandlerErrors, EnsureStarterHandlerResponse, EnsureStarterHandlerResponses, EntityFilters, EntityKind, EntityMessageCreateData, EntityMessageCreateResponse, EntityMessageCreateResponses, EntityMessageDeleteMessageData, EntityMessageDeleteMessageResponse, EntityMessageDeleteMessageResponses, EntityMessageDeleteThreadData, EntityMessageDeleteThreadResponse, EntityMessageDeleteThreadResponses, EntityMessageEditData, EntityMessageEditResponse, EntityMessageEditResponses, EntityMessageGetMessageData, EntityMessageGetMessageResponse, EntityMessageGetMessageResponses, EntityMessageGetThreadData, EntityMessageGetThreadResponse, EntityMessageGetThreadResponses, EntityMessageLegacyData, EntityMessageLegacyResponse, EntityMessageLegacyResponses, EntityMessagePatchThreadData, EntityMessagePatchThreadResponse, EntityMessagePatchThreadResponses, EntityMessageReactData, EntityMessageReactResponse, EntityMessageReactResponses, EntityMessageTypingData, EntityMessageTypingResponse, EntityMessageTypingResponses, EntityPermission, EntityPermissionResponse, EntityRef, EntityReference, EntityType, ErrorResponse, EventIcsData, EventIcsErrors, EventIcsResponse, EventIcsResponses, EventReminderOverride, EventReminders, EventStatus, EventTime, EventTransparency, EventType, EventVisibility, ExcludedDefaultView, ExcludeDefaultViewHandlerData, ExcludeDefaultViewHandlerError, ExcludeDefaultViewHandlerErrors, ExcludeDefaultViewHandlerResponses, ExcludeDefaultViewRequest, ExportDocumentData, ExportDocumentError, ExportDocumentErrors, ExportDocumentResponse, ExportDocumentResponse2, ExportDocumentResponses, Favorite, FavoriteEntityRef, FavoritesList, FileChangeKindDto, FileSystemNodeWithIds, FileType, FileTypeUpdate, FilterCondition, FilterGroup, FilterNode, FilterTest, FinalizeSlackImportData, FinalizeSlackImportErrors, FinalizeSlackImportResponse, FinalizeSlackImportResponses, FolderItem, FollowupState, ForeignEntity, ForeignEntityFilters, Form, FormAccess, FormCollaboration, FormDetail, FormErrorCode, FormErrorResponse, FormLayout, FormPublicationProblem, FormQuestionDetail, FormResponse, FormSection, FormSectionDetail, FormSource, FormStatus, FormTally, Formula, GenericErrorResponse, GenericResponse, GenericSuccessResponse, GetActiveCallsData, GetActiveCallsError, GetActiveCallsErrors, GetActiveCallsResponse, GetActiveCallsResponses, GetActivityData, GetActivityError, GetActivityErrors, GetActivityResponse, GetActivityResponses, GetAttachmentReferencesData, GetAttachmentReferencesError, GetAttachmentReferencesErrors, GetAttachmentReferencesResponse, GetAttachmentReferencesResponse2, GetAttachmentReferencesResponses, GetBatchCallRecordPreviewData, GetBatchCallRecordPreviewError, GetBatchCallRecordPreviewErrors, GetBatchCallRecordPreviewRequest, GetBatchCallRecordPreviewResponse, GetBatchCallRecordPreviewResponse2, GetBatchCallRecordPreviewResponses, GetBatchChannelPreviewData, GetBatchChannelPreviewError, GetBatchChannelPreviewErrors, GetBatchChannelPreviewRequest, GetBatchChannelPreviewResponse, GetBatchChannelPreviewResponse2, GetBatchChannelPreviewResponses, GetBatchPreviewHandlerData, GetBatchPreviewHandlerError, GetBatchPreviewHandlerErrors, GetBatchPreviewHandlerResponse, GetBatchPreviewHandlerResponses, GetBatchPreviewRequest, GetBatchPreviewResponse, GetBatchProjectPreviewData, GetBatchProjectPreviewError, GetBatchProjectPreviewErrors, GetBatchProjectPreviewRequest, GetBatchProjectPreviewResponse, GetBatchProjectPreviewResponse2, GetBatchProjectPreviewResponses, GetBotOwnerProfilesData, GetBotOwnerProfilesError, GetBotOwnerProfilesErrors, GetBotOwnerProfilesResponse, GetBotOwnerProfilesResponses, GetCallRecordData, GetCallRecordError, GetCallRecordErrors, GetCallRecordResponse, GetCallRecordResponses, GetChannelAttachmentsData, GetChannelAttachmentsError, GetChannelAttachmentsErrors, GetChannelAttachmentsResponse, GetChannelAttachmentsResponses, GetChannelData, GetChannelError, GetChannelErrors, GetChannelJoinLinkData, GetChannelJoinLinkError, GetChannelJoinLinkErrors, GetChannelJoinLinkResponse, GetChannelJoinLinkResponses, GetChannelParticipantsData, GetChannelParticipantsError, GetChannelParticipantsErrors, GetChannelParticipantsResponse, GetChannelParticipantsResponses, GetChannelResponse, GetChannelResponses, GetChannelsData, GetChannelsError, GetChannelsErrors, GetChannelsResponse, GetChannelsResponses, GetCollabSurfaceData, GetCollabSurfaceError, GetCollabSurfaceErrors, GetCollabSurfaceResponse, GetCollabSurfaceResponses, GetCompanyData, GetCompanyError, GetCompanyErrors, GetCompanyResponse, GetCompanyResponses, GetContactByEmailData, GetContactByEmailError, GetContactByEmailErrors, GetContactByEmailResponse, GetContactByEmailResponse2, GetContactByEmailResponses, GetContactData, GetContactError, GetContactErrors, GetContactResponse, GetContactResponses, GetCrmPipelineData, GetCrmPipelinePermissionsData, GetCrmPipelinePermissionsResponse, GetCrmPipelinePermissionsResponses, GetCrmPipelineResponse, GetCrmPipelineResponses, GetCrmPipelineRowsData, GetCrmPipelineRowsResponse, GetCrmPipelineRowsResponses, GetCrmPipelineTableData, GetCrmPipelineTableResponse, GetCrmPipelineTableResponses, GetCrmTeamSettingsData, GetCrmTeamSettingsError, GetCrmTeamSettingsErrors, GetCrmTeamSettingsResponse, GetCrmTeamSettingsResponses, GetDatabaseData, GetDatabaseError, GetDatabaseErrors, GetDatabasePermissionsData, GetDatabasePermissionsError, GetDatabasePermissionsErrors, GetDatabasePermissionsResponse, GetDatabasePermissionsResponses, GetDatabaseQueryData, GetDatabaseQueryError, GetDatabaseQueryErrors, GetDatabaseQueryResponse, GetDatabaseQueryResponses, GetDatabaseResponse, GetDatabaseResponses, GetDatabaseRowHistoryData, GetDatabaseRowHistoryError, GetDatabaseRowHistoryErrors, GetDatabaseRowHistoryResponse, GetDatabaseRowHistoryResponses, GetDatabaseTableChangesData, GetDatabaseTableChangesError, GetDatabaseTableChangesErrors, GetDatabaseTableChangesResponse, GetDatabaseTableChangesResponses, GetDatabaseViewPositionsData, GetDatabaseViewPositionsError, GetDatabaseViewPositionsErrors, GetDatabaseViewPositionsResponse, GetDatabaseViewPositionsResponses, GetDocumentAnchorsData, GetDocumentAnchorsError, GetDocumentAnchorsErrors, GetDocumentAnchorsResponse, GetDocumentAnchorsResponses, GetDocumentBranchNameData, GetDocumentBranchNameError, GetDocumentBranchNameErrors, GetDocumentBranchNameResponse, GetDocumentBranchNameResponses, GetDocumentByTeamSlugData, GetDocumentByTeamSlugError, GetDocumentByTeamSlugErrors, GetDocumentByTeamSlugResponse, GetDocumentByTeamSlugResponses, GetDocumentData, GetDocumentError, GetDocumentErrors, GetDocumentGithubPullRequestsData, GetDocumentGithubPullRequestsError, GetDocumentGithubPullRequestsErrors, GetDocumentGithubPullRequestsResponse, GetDocumentGithubPullRequestsResponses, GetDocumentKeyResponse, GetDocumentKeyResponseData, GetDocumentListHandlerData, GetDocumentListHandlerError, GetDocumentListHandlerErrors, GetDocumentListHandlerResponse, GetDocumentListHandlerResponses, GetDocumentListResult, GetDocumentLocationV3Data, GetDocumentLocationV3Error, GetDocumentLocationV3Errors, GetDocumentLocationV3Response, GetDocumentLocationV3Responses, GetDocumentPermissionsResponseDataV2, GetDocumentPermissionsTokenData, GetDocumentPermissionsTokenError, GetDocumentPermissionsTokenErrors, GetDocumentPermissionsTokenResponse, GetDocumentPermissionsTokenResponses, GetDocumentPermissionsV2Data, GetDocumentPermissionsV2Error, GetDocumentPermissionsV2Errors, GetDocumentPermissionsV2Response, GetDocumentPermissionsV2Responses, GetDocumentProcessingResult, GetDocumentProcessingResultData, GetDocumentProcessingResultError, GetDocumentProcessingResultErrors, GetDocumentProcessingResultResponse, GetDocumentProcessingResultResponse2, GetDocumentProcessingResultResponses, GetDocumentResponse, GetDocumentResponse2, GetDocumentResponseData, GetDocumentResponses, GetDocumentSearchResponse, GetDocumentShortIdData, GetDocumentShortIdError, GetDocumentShortIdErrors, GetDocumentShortIdResponse, GetDocumentShortIdResponses, GetDocumentsResponse, GetDocumentTeamShareData, GetDocumentTeamShareError, GetDocumentTeamShareErrors, GetDocumentTeamShareResponse, GetDocumentTeamShareResponses, GetDocumentTextContentData, GetDocumentTextContentErrors, GetDocumentTextContentResponse, GetDocumentTextContentResponses, GetDocumentUserAccessLevelResponse, GetDocumentVersionData, GetDocumentVersionError, GetDocumentVersionErrors, GetDocumentVersionResponse, GetDocumentVersionResponses, GetDocumentViewsHandlerData, GetDocumentViewsHandlerError, GetDocumentViewsHandlerErrors, GetDocumentViewsHandlerResponse, GetDocumentViewsHandlerResponses, GetEmailFollowupData, GetEmailFollowupError, GetEmailFollowupErrors, GetEmailFollowupResponse, GetEmailFollowupResponses, GetEntityPermissionData, GetEntityPermissionErrors, GetEntityPermissionResponse, GetEntityPermissionResponses, GetForeignEntityBySourceData, GetForeignEntityBySourceError, GetForeignEntityBySourceErrors, GetForeignEntityBySourceResponse, GetForeignEntityBySourceResponses, GetForeignEntityData, GetForeignEntityError, GetForeignEntityErrors, GetForeignEntityResponse, GetForeignEntityResponses, GetFormData, GetFormError, GetFormErrors, GetFormPermissionsData, GetFormPermissionsError, GetFormPermissionsErrors, GetFormPermissionsResponse, GetFormPermissionsResponses, GetFormResponse, GetFormResponses, GetFormResponseSummaryData, GetFormResponseSummaryError, GetFormResponseSummaryErrors, GetFormResponseSummaryResponse, GetFormResponseSummaryResponses, GetFormTallyData, GetFormTallyError, GetFormTallyErrors, GetFormTallyResponse, GetFormTallyResponses, GetGithubPullRequestChangesData, GetGithubPullRequestChangesError, GetGithubPullRequestChangesErrors, GetGithubPullRequestChangesPatchData, GetGithubPullRequestChangesPatchError, GetGithubPullRequestChangesPatchErrors, GetGithubPullRequestChangesPatchResponse, GetGithubPullRequestChangesPatchResponses, GetGithubPullRequestChangesResponse, GetGithubPullRequestChangesResponses, GetGithubPullRequestData, GetGithubPullRequestError, GetGithubPullRequestErrors, GetGithubPullRequestFacetsData, GetGithubPullRequestFacetsError, GetGithubPullRequestFacetsErrors, GetGithubPullRequestFacetsResponse, GetGithubPullRequestFacetsResponses, GetGithubPullRequestResponse, GetGithubPullRequestResponses, GetGithubPullRequestTasksData, GetGithubPullRequestTasksError, GetGithubPullRequestTasksErrors, GetGithubPullRequestTasksResponse, GetGithubPullRequestTasksResponses, GetHarnessPairingData, GetHarnessPairingError, GetHarnessPairingErrors, GetHarnessPairingResponse, GetHarnessPairingResponses, GetHistoryHandlerData, GetHistoryHandlerError, GetHistoryHandlerErrors, GetHistoryHandlerResponse, GetHistoryHandlerResponses, GetInitiativeData, GetInitiativeError, GetInitiativeErrors, GetInitiativeResponse, GetInitiativeResponses, GetInstructionsDocumentResponse, GetInstructionsHandlerData, GetInstructionsHandlerError, GetInstructionsHandlerErrors, GetInstructionsHandlerResponse, GetInstructionsHandlerResponses, GetItemsSoupData, GetItemsSoupError, GetItemsSoupErrors, GetItemsSoupResponse, GetItemsSoupResponses, GetLocationHandlerData, GetLocationHandlerError, GetLocationHandlerErrors, GetLocationHandlerResponse, GetLocationHandlerResponses, GetMyFormResponseData, GetMyFormResponseError, GetMyFormResponseErrors, GetMyFormResponseResponse, GetMyFormResponseResponses, GetOrCreateAction, GetOrCreateCallData, GetOrCreateCallError, GetOrCreateCallErrors, GetOrCreateCallResponse, GetOrCreateCallResponses, GetOrCreateChannelResponse, GetOrCreateDmData, GetOrCreateDmError, GetOrCreateDmErrors, GetOrCreateDmRequest, GetOrCreateDmResponse, GetOrCreateDmResponses, GetOrCreatePrivateData, GetOrCreatePrivateError, GetOrCreatePrivateErrors, GetOrCreatePrivateRequest, GetOrCreatePrivateResponse, GetOrCreatePrivateResponses, GetPendingProjectsHandlerData, GetPendingProjectsHandlerError, GetPendingProjectsHandlerErrors, GetPendingProjectsHandlerResponse, GetPendingProjectsHandlerResponses, GetPinsHandlerData, GetPinsHandlerError, GetPinsHandlerErrors, GetPinsHandlerResponse, GetPinsHandlerResponses, GetPinsResponse, GetProjectContentHandlerData, GetProjectContentHandlerError, GetProjectContentHandlerErrors, GetProjectContentHandlerResponse, GetProjectContentHandlerResponses, GetProjectContentResponse, GetProjectHandlerData, GetProjectHandlerError, GetProjectHandlerErrors, GetProjectHandlerResponse, GetProjectHandlerResponses, GetProjectPermissionsV2Data, GetProjectPermissionsV2Error, GetProjectPermissionsV2Errors, GetProjectPermissionsV2Response, GetProjectPermissionsV2Responses, GetProjectResponse, GetProjectResponseData, GetProjectsHandlerData, GetProjectsHandlerError, GetProjectsHandlerErrors, GetProjectsHandlerResponse, GetProjectsHandlerResponses, GetProjectsResponse, GetProjectUserAccessLevelData, GetProjectUserAccessLevelError, GetProjectUserAccessLevelErrors, GetProjectUserAccessLevelResponse, GetProjectUserAccessLevelResponses, GetRingStatusData, GetRingStatusError, GetRingStatusErrors, GetRingStatusResponse, GetRingStatusResponses, GetSelfBotData, GetSelfBotError, GetSelfBotErrors, GetSelfBotResponse, GetSelfBotResponses, GetSelfHarnessData, GetSelfHarnessError, GetSelfHarnessErrors, GetSelfHarnessResponse, GetSelfHarnessResponses, GetSlackImportData, GetSlackImportErrors, GetSlackImportResponse, GetSlackImportResponses, GetSystemSkillsHandlerData, GetSystemSkillsHandlerError, GetSystemSkillsHandlerErrors, GetSystemSkillsHandlerResponse, GetSystemSkillsHandlerResponses, GetUserDocumentsHandlerData, GetUserDocumentsHandlerError, GetUserDocumentsHandlerErrors, GetUserDocumentsHandlerResponse, GetUserDocumentsHandlerResponses, GetUserDocumentViewLocationData, GetUserDocumentViewLocationError, GetUserDocumentViewLocationErrors, GetUserDocumentViewLocationResponse, GetUserDocumentViewLocationResponses, GetUserHistoryResponse, GetViewsHandlerData, GetViewsHandlerError, GetViewsHandlerErrors, GetViewsHandlerResponse, GetViewsHandlerResponses, GetWebhookData, GetWebhookError, GetWebhookErrors, GetWebhookResponse, GetWebhookResponses, GithubLabelFacet, GithubPullRequest, GithubPullRequestChangesPatchResponse, GithubPullRequestChangesResponse, GithubPullRequestCheckRun, GithubPullRequestComment, GithubPullRequestFacets, GithubPullRequestLabel, GithubPullRequestReview, GithubPullRequestReviewDecision, GithubPullRequestReviewState, GithubPullRequestsResponse, GithubPullRequestTasks, GithubPullRequestTasksRequest, GithubPullRequestTasksResponse, GithubPullRequestUser, GithubRepositoryFacet, GithubUserFacet, GitRefDto, GroupedInitialParams, GroupedPageParams, GroupedSoupGroupPage, GroupedSoupInitialPage, GroupedSoupPage, GroupedSoupSort, GuestCallChatPostData, GuestCallChatPostErrors, GuestCallChatPostResponse, GuestCallChatPostResponses, GuestCallChatReadData, GuestCallChatReadErrors, GuestCallChatReadResponse, GuestCallChatReadResponses, GuestChatMessage, GuestChatPage, GuestChatPost, GuestId, GuestJoinRequest, GuestPolicy, HandlerData, HandlerError, HandlerErrors, HandlerResponse, HandlerResponses, Harness, HarnessAgent, HarnessId, HarnessOwner, HarnessSession, HashMap, HealthHandlerData, HealthHandlerResponse, HealthHandlerResponses, HighlightType, ImportCounters, ImportDatabaseTableData, ImportDatabaseTableError, ImportDatabaseTableErrors, ImportDatabaseTableResponse, ImportDatabaseTableResponses, ImportedAuthor, ImportError, ImportLimits, ImportPage, ImportProgress, ImportTable, ImportWarning, InferColumnTypeOutcome, InferColumnTypeRequest, InferDatabaseColumnTypeData, InferDatabaseColumnTypeError, InferDatabaseColumnTypeErrors, InferDatabaseColumnTypeResponse, InferDatabaseColumnTypeResponses, InFlightTurnSummary, IngestTranscriptData, IngestTranscriptError, IngestTranscriptErrors, IngestTranscriptResponses, InitializeUserDocumentsData, InitializeUserDocumentsError, InitializeUserDocumentsErrors, InitializeUserDocumentsResponse, InitializeUserDocumentsResponses, InitialPropertyValue, InitiativeDetail, InitiativeFilters, InitiativeId, InitiativeList, InitiativeSummary, InputReceivedMetadata, InstallSyncData, InstallSyncErrors, InteractionReason, InvalidDatabaseWebhookPayloadResponse, InviteMeetingRequest, InviteMeetingUsersRequest, Item, ItemWithUserAccessLevel, JobId, JobProcessingResultHandlerData, JobProcessingResultHandlerError, JobProcessingResultHandlerErrors, JobProcessingResultHandlerResponse, JobProcessingResultHandlerResponses, JobStatus, JoinChannelByCodeData, JoinChannelByCodeError, JoinChannelByCodeErrors, JoinChannelByCodeResponses, JoinChannelData, JoinChannelError, JoinChannelErrors, JoinChannelResponses, KeySegment, Lane, LaneKey, LatestMessage, LayoutProblem, LeaveCallResponse, LeaveChannelData, LeaveChannelError, LeaveChannelErrors, LeaveChannelResponses, LeaveOrEndCallData, LeaveOrEndCallError, LeaveOrEndCallErrors, LeaveOrEndCallResponse, LeaveOrEndCallResponses, LinkedAccount, LinkedAccountData, LinkedAccountResponse, LinkedAccountResponses, LinkedAccountState, LinkedAccountView, LinkedAdoptData, LinkedAdoptGroup, LinkedAdoptResponse, LinkedAdoptResponses, LinkedAttachment, LinkedAttachmentState, LinkedBackfill, LinkedBackfillState, LinkedCancelPairingData, LinkedCancelPairingResponse, LinkedCancelPairingResponses, LinkedCapabilities, LinkedChat, LinkedChatKind, LinkedChats, LinkedChatsData, LinkedChatsResponse, LinkedChatsResponses, LinkedConfigurationData, LinkedConfigurationResponse, LinkedConfigurationResponses, LinkedConnect, LinkedConnectData, LinkedConnectResponse, LinkedConnectResponses, LinkedDeliveryData, LinkedDeliveryResponse, LinkedDeliveryResponses, LinkedDeliveryState, LinkedDetachData, LinkedDetachResponse, LinkedDetachResponses, LinkedDisconnect, LinkedDisconnectData, LinkedDisconnectRequest, LinkedDisconnectResponse, LinkedDisconnectResponses, LinkedFeatures, LinkedGroupInfo, LinkedGroupReference, LinkedGroupsData, LinkedGroupsResponse, LinkedGroupsResponses, LinkedIdentity, LinkedMediaKind, LinkedMessageAttachments, LinkedMessageAttachmentsData, LinkedMessageAttachmentsResponse, LinkedMessageAttachmentsResponses, LinkedMutation, LinkedPairing, LinkedPairingData, LinkedPairingResponse, LinkedPairingResponses, LinkedPairingState, LinkedPairState, LinkedParticipant, LinkedPasskeyData, LinkedPasskeyResponse, LinkedPasskeyResponses, LinkedReplyBlock, LinkedReplyContent, LinkedReplyData, LinkedReplyErrors, LinkedReplyResponse, LinkedReplyResponses, LinkedSendProgress, LinkedSendReply, LinkedSharedGroupReference, LinkedUploadData, LinkedUploadedAttachment, LinkedUploadRequest, LinkedUploadResponse, LinkedUploadResponses, LinkShare, ListAccessibleFormsData, ListAccessibleFormsError, ListAccessibleFormsErrors, ListAccessibleFormsResponse, ListAccessibleFormsResponses, ListAgentsData, ListAgentsError, ListAgentsErrors, ListAgentsResponse, ListAgentsResponses, ListBotChannelsData, ListBotChannelsError, ListBotChannelsErrors, ListBotChannelsResponse, ListBotChannelsResponses, ListChannelLabelsData, ListChannelLabelsError, ListChannelLabelsErrors, ListChannelLabelsResponse, ListChannelLabelsResponses, ListCompanyContactsData, ListCompanyContactsError, ListCompanyContactsErrors, ListCompanyContactsResponse, ListCompanyContactsResponses, ListCrmPipelinesData, ListCrmPipelinesResponse, ListCrmPipelinesResponses, ListDatabaseColumnCastsData, ListDatabaseColumnCastsError, ListDatabaseColumnCastsErrors, ListDatabaseColumnCastsResponse, ListDatabaseColumnCastsResponses, ListDatabasesData, ListDatabasesError, ListDatabasesErrors, ListDatabasesResponse, ListDatabasesResponses, ListDatabaseWebhooksData, ListDatabaseWebhooksError, ListDatabaseWebhooksErrors, ListDatabaseWebhooksResponse, ListDatabaseWebhooksResponse2, ListDatabaseWebhooksResponses, ListedDatabase, ListedForm, ListEmailRemindersData, ListEmailRemindersError, ListEmailRemindersErrors, ListEmailRemindersResponse, ListEmailRemindersResponses, ListFavoritesData, ListFavoritesError, ListFavoritesErrors, ListFavoritesResponse, ListFavoritesResponses, ListFormsData, ListFormsError, ListFormsErrors, ListFormsResponse, ListFormsResponses, ListHarnessAgentsData, ListHarnessAgentsError, ListHarnessAgentsErrors, ListHarnessAgentsResponse, ListHarnessAgentsResponses, ListHarnessesData, ListHarnessesError, ListHarnessesErrors, ListHarnessesResponse, ListHarnessesResponses, ListHarnessSessionsData, ListHarnessSessionsError, ListHarnessSessionsErrors, ListHarnessSessionsResponse, ListHarnessSessionsResponses, ListInitiativesData, ListInitiativesError, ListInitiativesErrors, ListInitiativesResponse, ListInitiativesResponses, ListJoinRequestsData, ListJoinRequestsError, ListJoinRequestsErrors, ListJoinRequestsResponse, ListJoinRequestsResponses, ListOccurrencesData, ListOccurrencesErrors, ListOccurrencesResponse, ListOccurrencesResponses, ListSlackImportsData, ListSlackImportsResponse, ListSlackImportsResponses, ListTeamCalendarData, ListTeamCalendarResponse, ListTeamCalendarResponses, ListTeamOutOfOfficeData, ListTeamOutOfOfficeErrors, ListTeamOutOfOfficeResponse, ListTeamOutOfOfficeResponses, ListUserApiKeysData, ListUserApiKeysError, ListUserApiKeysErrors, ListUserApiKeysResponse, ListUserApiKeysResponses, ListWebhooksData, ListWebhooksError, ListWebhooksErrors, ListWebhooksResponse, ListWebhooksResponse2, ListWebhooksResponses, LocationResponseData, LocationResponseV3, MacroUserIdStr, MarkRead, Meeting, MeetingCancelData, MeetingCancelError, MeetingCancelErrors, MeetingCancelPreparationData, MeetingCancelPreparationError, MeetingCancelPreparationErrors, MeetingCancelPreparationResponse, MeetingCancelPreparationResponses, MeetingCancelResponse, MeetingCancelResponses, MeetingCreateData, MeetingCreateError, MeetingCreateErrors, MeetingCreateResponse, MeetingCreateResponses, MeetingGuestJoinData, MeetingGuestJoinError, MeetingGuestJoinErrors, MeetingGuestJoinResponse, MeetingGuestJoinResponses, MeetingGuestParticipantsData, MeetingGuestParticipantsError, MeetingGuestParticipantsErrors, MeetingGuestParticipantsResponse, MeetingGuestParticipantsResponses, MeetingInviteData, MeetingInviteError, MeetingInviteErrors, MeetingInvitePermissions, MeetingInvitePermissionsData, MeetingInvitePermissionsError, MeetingInvitePermissionsErrors, MeetingInvitePermissionsResponse, MeetingInvitePermissionsResponses, MeetingInviteResponse, MeetingInviteResponses, MeetingInviteUsersData, MeetingInviteUsersError, MeetingInviteUsersErrors, MeetingInviteUsersResponse, MeetingInviteUsersResponses, MeetingJoinData, MeetingJoinError, MeetingJoinErrors, MeetingJoinResponse, MeetingJoinResponses, MeetingLeaveData, MeetingLeaveError, MeetingLeaveErrors, MeetingLeaveResponse, MeetingLeaveResponses, MeetingListActiveData, MeetingListActiveError, MeetingListActiveErrors, MeetingListActiveResponse, MeetingListActiveResponses, MeetingListData, MeetingListError, MeetingListErrors, MeetingListResponse, MeetingListResponses, MeetingLookupData, MeetingLookupError, MeetingLookupErrors, MeetingLookupResponse, MeetingLookupResponses, MeetingParticipant, MeetingParticipants, MeetingParticipantsData, MeetingParticipantsError, MeetingParticipantsErrors, MeetingParticipantsResponse, MeetingParticipantsResponses, MeetingPreparation, MeetingPrepareData, MeetingPrepareError, MeetingPrepareErrors, MeetingPrepareResponse, MeetingPrepareResponses, MeetingShareData, MeetingShareError, MeetingShareErrors, MeetingShareResponse, MeetingShareResponses, MeetingsResponse, MeetingToken, MeetingUpdateData, MeetingUpdateError, MeetingUpdateErrors, MeetingUpdateResponse, MeetingUpdateResponses, MentionPreviewsData, MentionPreviewsErrors, MentionPreviewsResponse, MentionPreviewsResponses, Message, MessageAttachment, MessageAttachmentCreatedMetadata, MessageAttachmentRemovedMetadata, MessageChange, MessageCursor, MessageDeletedMetadata, MessageDirection, MessageEvent, MessageEventAttachment, MessageListItem, MessageMentionedMetadata, MessagePage, MessageParent, MessagePatch, MessagePatchedMetadata, MessagePostedMetadata, MessageThread, MessageThreadPreview, MessageTimelineData, MessageTimelineEntriesData, MessageTimelineEntriesResponse, MessageTimelineEntriesResponses, MessageTimelineEntry, MessageTimelinePage, MessageTimelineQuery, MessageTimelineResponse, MessageTimelineResponses, MessageTopicEvent, MyResponse, NewAttachment, NewColumn, NewOption, NewThreadAnchor, NewView, NotificationFilters, NotificationState, NumberOperator, OpBatch, Operator, OpRefusalResponse, OpResult, OptionId, OptionRef, PairingDetails, Params, ParticipantRole, PatchBotRequest, PatchChannelData, PatchChannelError, PatchChannelErrors, PatchChannelRequest, PatchChannelResponse, PatchChannelResponses, PatchMessageNotificationPolicy, PatchProjectRequestV2, PatchThreadRequestV2, PatchViewHandlerData, PatchViewHandlerError, PatchViewHandlerErrors, PatchViewHandlerResponses, PatchWebhookData, PatchWebhookError, PatchWebhookErrors, PatchWebhookRequest, PatchWebhookResponse, PatchWebhookResponses, PdfAnchor, PdfAnchorId, PdfHighlightAnchor, PdfHighlightAnchorRequest, PdfHighlightRect, PdfHighlightRectAnchorRequest, PdfPlaceableCommentAnchor, PendingClaimResponse, PermanentlyDeleteDocumentData, PermanentlyDeleteDocumentError, PermanentlyDeleteDocumentErrors, PermanentlyDeleteDocumentResponse, PermanentlyDeleteDocumentResponses, PermanentlyDeleteProjectData, PermanentlyDeleteProjectError, PermanentlyDeleteProjectErrors, PermanentlyDeleteProjectResponse, PermanentlyDeleteProjectResponses, PinnedItem, PinRequest, Pipeline, PipelineRecordType, PipelineSharing, PostActivityData, PostActivityError, PostActivityErrors, PostActivityRequest, PostActivityResponse, PostActivityResponses, PostChannelBotWebhookData, PostChannelBotWebhookError, PostChannelBotWebhookErrors, PostChannelBotWebhookResponse, PostChannelBotWebhookResponses, PostGroupedSoupAstGroupPageRequest, PostGroupedSoupAstInitialRequest, PostGroupedSoupAstRequest, PostItemsSoupAstData, PostItemsSoupAstError, PostItemsSoupAstErrors, PostItemsSoupAstGroupedData, PostItemsSoupAstGroupedError, PostItemsSoupAstGroupedErrors, PostItemsSoupAstGroupedResponse, PostItemsSoupAstGroupedResponses, PostItemsSoupAstResponse, PostItemsSoupAstResponses, PostItemsSoupData, PostItemsSoupError, PostItemsSoupErrors, PostItemsSoupResponse, PostItemsSoupResponses, PostMessage, PostMessageNotificationPolicy, PostSoupAstRequest, PostSoupRequest, PresaveDocumentHandlerData, PresaveDocumentHandlerError, PresaveDocumentHandlerErrors, PresaveDocumentHandlerResponse, PresaveDocumentHandlerResponses, PreSaveDocumentRequest, PreSaveDocumentResponse, PreSaveDocumentResponseData, PresenceOperator, PresignedUrl, PreviewSmartTagData, PreviewSmartTagError, PreviewSmartTagErrors, PreviewSmartTagResponse, PreviewSmartTagResponses, Project, ProjectDeleteResponseData, ProjectDocumentStatus, ProjectFilters, ProjectPreview, ProjectPreviewData, PropertyDefinition, PropertyDefinitionWithOptions, PropertyFilter, PropertyInput, PropertyOption, PropertyOptionValue, PropertyOwner, PropertyValue, PutCrmTeamSettingsData, PutCrmTeamSettingsError, PutCrmTeamSettingsErrors, PutCrmTeamSettingsResponse, PutCrmTeamSettingsResponses, PutCrmTeamStagesData, PutCrmTeamStagesError, PutCrmTeamStagesErrors, PutCrmTeamStagesResponse, PutCrmTeamStagesResponses, PutFormLayoutData, PutFormLayoutError, PutFormLayoutErrors, PutFormLayoutResponse, PutFormLayoutResponses, QueryCrmPipelineRowsData, QueryCrmPipelineRowsResponse, QueryCrmPipelineRowsResponses, QueryDatabasesData, QueryDatabasesError, QueryDatabasesErrors, QueryDatabasesResponse, QueryDatabasesResponses, QueryDefinition, QuestionLayout, QuestionOption, QuestionTally, ReactionInput, RecentlyDeletedData, RecentlyDeletedError, RecentlyDeletedErrors, RecentlyDeletedResponse, RecentlyDeletedResponseData, RecentlyDeletedResponses, RegisterSlackImportUploadsData, RegisterSlackImportUploadsErrors, RegisterSlackImportUploadsResponse, RegisterSlackImportUploadsResponses, RegisterUploads, RemoveBotFromChannelByBotData, RemoveBotFromChannelByBotError, RemoveBotFromChannelByBotErrors, RemoveBotFromChannelByBotResponse, RemoveBotFromChannelByBotResponses, RemoveFavoriteByEntityData, RemoveFavoriteByEntityError, RemoveFavoriteByEntityErrors, RemoveFavoriteByEntityResponses, RemoveParticipantsData, RemoveParticipantsError, RemoveParticipantsErrors, RemoveParticipantsRequest, RemoveParticipantsResponses, RemovePinHandlerData, RemovePinHandlerError, RemovePinHandlerErrors, RemovePinHandlerResponse, RemovePinHandlerResponses, RenameChannelLabelData, RenameChannelLabelError, RenameChannelLabelErrors, RenameChannelLabelRequest, RenameChannelLabelResponse, RenameChannelLabelResponses, RenameCrmPipelineData, RenameCrmPipelineResponse, RenameCrmPipelineResponses, RenamePipeline, ReorderFavoritesData, ReorderFavoritesError, ReorderFavoritesErrors, ReorderFavoritesRequest, ReorderFavoritesResponses, ReorderPinRequest, ReorderPinsHandlerData, ReorderPinsHandlerError, ReorderPinsHandlerErrors, ReorderPinsHandlerResponse, ReorderPinsHandlerResponses, ReplaceCrmStagesRequest, RequestAccess, RequestedHarnessScope, RequestedLayout, RequestStatus, RequestToJoinData, RequestToJoinError, RequestToJoinErrors, RequestToJoinResponse, RequestToJoinResponses, ResetCrmTeamStagesData, ResetCrmTeamStagesError, ResetCrmTeamStagesErrors, ResetCrmTeamStagesResponse, ResetCrmTeamStagesResponses, ResolveRequest, ResponseStatus, ResponseSummary, ResultColumn, ResultSet, RevertDeleteDocumentData, RevertDeleteDocumentError, RevertDeleteDocumentErrors, RevertDeleteDocumentResponse, RevertDeleteDocumentResponses, RevertDeleteProjectData, RevertDeleteProjectError, RevertDeleteProjectErrors, RevertDeleteProjectResponse, RevertDeleteProjectResponses, RingStatus, RingStatusResponse, RowChange, RowChangeKind, RowChanges, RowHistoryEntry, RowHistoryResponse, RowId, RowsChange, RowsResult, S3ObjectInfo, SaveBomPart, SaveDatabaseQueryData, SaveDatabaseQueryError, SaveDatabaseQueryErrors, SaveDatabaseQueryResponse, SaveDatabaseQueryResponses, SaveDocumentHandlerData, SaveDocumentHandlerError, SaveDocumentHandlerErrors, SaveDocumentHandlerResponse, SaveDocumentHandlerResponses, SaveDocumentRequest, SaveDocumentResponse, SaveDocumentResponseData, SavedQuery, SaveQueryRequest, SearchContactsData, SearchContactsError, SearchContactsErrors, SearchContactsResponse, SearchContactsResponse2, SearchContactsResponses, SearchState, SectionCount, SessionDeletedMetadata, SessionIdentity, SessionMentionedMetadata, SessionOpenedMetadata, SessionRenamedMetadata, SessionSettledMetadata, SessionSource, SessionStoppedMetadata, SetChannelLabelData, SetChannelLabelError, SetChannelLabelErrors, SetChannelLabelRequest, SetChannelLabelResponse, SetChannelLabelResponses, SetChannelPictureData, SetChannelPictureError, SetChannelPictureErrors, SetChannelPictureRequest, SetChannelPictureResponse, SetChannelPictureResponses, SetCompanyHiddenData, SetCompanyHiddenError, SetCompanyHiddenErrors, SetCompanyHiddenRequest, SetCompanyHiddenResponse, SetCompanyHiddenResponses, SetCompanyNameRequest, SetContactHiddenData, SetContactHiddenError, SetContactHiddenErrors, SetContactHiddenRequest, SetContactHiddenResponse, SetContactHiddenResponses, SetContactNameRequest, SetCrmCompanyNameData, SetCrmCompanyNameError, SetCrmCompanyNameErrors, SetCrmCompanyNameResponse, SetCrmCompanyNameResponses, SetCrmContactNameData, SetCrmContactNameError, SetCrmContactNameErrors, SetCrmContactNameResponse, SetCrmContactNameResponses, SetDirectAccess, SetDocumentTeamShareData, SetDocumentTeamShareError, SetDocumentTeamShareErrors, SetDocumentTeamShareRequest, SetDocumentTeamShareResponse, SetDocumentTeamShareResponses, SetEmailFollowupData, SetEmailFollowupError, SetEmailFollowupErrors, SetEmailFollowupResponse, SetEmailFollowupResponses, SetEmailSyncData, SetEmailSyncError, SetEmailSyncErrors, SetEmailSyncRequest, SetEmailSyncResponse, SetEmailSyncResponses, SetOperator, SetPropertyValue, Sha256Digest, ShareCrmPipelineData, ShareCrmPipelineResponse, ShareCrmPipelineResponses, ShareDatabaseAwarenessData, ShareDatabaseAwarenessError, ShareDatabaseAwarenessErrors, ShareDatabaseAwarenessResponse, ShareDatabaseAwarenessResponses, SharedEmailFilter, ShareEntity, ShareEntityType, SharePermissionV2, SharePipeline, ShortIdResponse, SimpleMention, SimpleSaveData, SimpleSaveError, SimpleSaveErrors, SimpleSaveResponse, SimpleSaveResponses, SkippedCell, SlackCompleteRequest, SlackCreateRequest, SlackRegisterRequest, SlackTimestamp, SlackUserId, SmartTagChannelMatch, SmartTagPreview, SortDirection, SortKey, SoupAgentSessionSoupPropertiesField, SoupApiItem, SoupApiSort, SoupApiSortDirection, SoupAttachment, SoupCalendarEventSoupPropertiesField, SoupCalendarEventTime, SoupCallRecordGuest, SoupCallRecordParticipant, SoupCallRecordSoupPropertiesField, SoupChannel, SoupChannelThread, SoupChatSoupPropertiesField, SoupContact, SoupCountedReaction, SoupCrmCompanySoupPropertiesField, SoupCrmContactSoupPropertiesField, SoupCrmDomain, SoupDatabaseRowSoupPropertiesField, SoupDocumentSoupPropertiesField, SoupDocumentSubType, SoupEmailThreadPreview, SoupEnrichedEmailThreadPreviewSoupPropertiesField, SoupForeignEntity, SoupInitiativeSoupPropertiesField, SoupItem, SoupLabel, SoupLabelListVisibility, SoupLabelType, SoupMessageAttachment, SoupMessageListVisibility, SoupMessageSender, SoupMessageSenderType, SoupPage, SoupProjectSoupPropertiesField, SoupPropertiesField, SoupProperty, SoupThreadInfo, SoupThreadReply, SourceBinding, SourceId, SourceIdentity, SqlCell, SqlEntityKind, SqlOutcome, SqlOutcomeKind, SqlSelectOption, SqlStatement, StarterDatabase, StarterDocumentsResponse, StorageRow, StorageRows, StorageRowsQuery, StoredGithubPullRequest, StreamEventsData, StreamEventsError, StreamEventsErrors, StreamEventsResponse, StreamEventsResponses, String, Submission, SubmissionOutcome, SubmitFormResponseData, SubmitFormResponseError, SubmitFormResponseErrors, SubmitFormResponseResponse, SubmitFormResponseResponses, SuccessResponse, SurfaceState, SyncServiceVersionId, SystemSkillSummary, Table, TableChange, TableChanged, TableChanges, TableDetail, TableId, TableResult, TableVersion, TagFilterMode, TakenId, TallyBucket, TallyValue, TaskFilters, TeamCalendarContent, TeamCalendarCoverage, TeamCalendarDetails, TeamCalendarItem, TeamCalendarMember, TeamCalendarPage, TeamCalendarSharing, TeamOutOfOfficeItem, TeamOutOfOfficeResponse, TeamRole, TextOperator, ThreadAnchor, ThreadOrigin, ThreadPatch, ThreadState, TimelineActivity, ToggleShareWithTeamData, ToggleShareWithTeamError, ToggleShareWithTeamErrors, ToggleShareWithTeamResponse, ToggleShareWithTeamResponses, TouchedColumn, TouchedRow, TranscribeDictationData, TranscribeDictationError, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeResponse, TranscriptSegmentRequest, TrashCrmPipelineData, TrashCrmPipelineResponse, TrashCrmPipelineResponses, TrashPipeline, TurnEndedMetadata, TurnStartedMetadata, TurnSummary, TypedSuccessResponse, TypedSuccessResponseGetDocumentResponseData, TypingInput, UndoChangeResponse, UndoDatabaseChangeData, UndoDatabaseChangeError, UndoDatabaseChangeErrors, UndoDatabaseChangeResponse, UndoDatabaseChangeResponses, UndoOutcome, UndoRefusal, UnlockedBooking, UpdateAgentData, UpdateAgentError, UpdateAgentErrors, UpdateAgentRequest, UpdateAgentResponse, UpdateAgentResponses, UpdateChannelSharePermission, UpdateCrmPipelinePermissionsData, UpdateCrmPipelinePermissionsResponse, UpdateCrmPipelinePermissionsResponses, UpdateCrmTeamSettingsRequest, UpdateDatabasePermissionsData, UpdateDatabasePermissionsError, UpdateDatabasePermissionsErrors, UpdateDatabasePermissionsResponse, UpdateDatabasePermissionsResponses, UpdateForm, UpdateFormData, UpdateFormError, UpdateFormErrors, UpdateFormPermissionsData, UpdateFormPermissionsError, UpdateFormPermissionsErrors, UpdateFormPermissionsResponse, UpdateFormPermissionsResponses, UpdateFormResponse, UpdateFormResponses, UpdateInitiativeData, UpdateInitiativeError, UpdateInitiativeErrors, UpdateInitiativeRequest, UpdateInitiativeResponse, UpdateInitiativeResponses, UpdateMeetingRequest, UpdateOperation, UpdateSharePermissionRequestV2, UploadDescriptor, UploadDocumentStatus, UploadExtractFolderHandlerData, UploadExtractFolderHandlerError, UploadExtractFolderHandlerErrors, UploadExtractFolderHandlerResponse, UploadExtractFolderHandlerResponses, UploadExtractFolderRequest, UploadFolderHandlerData, UploadFolderHandlerError, UploadFolderHandlerErrors, UploadFolderHandlerResponse, UploadFolderHandlerResponses, UploadFolderRequest, UploadGrant, UploadId, UpsertHistoryHandlerData, UpsertHistoryHandlerError, UpsertHistoryHandlerErrors, UpsertHistoryHandlerResponse, UpsertHistoryHandlerResponses, UpsertUserDocumentViewLocationData, UpsertUserDocumentViewLocationError, UpsertUserDocumentViewLocationErrors, UpsertUserDocumentViewLocationRequest, UpsertUserDocumentViewLocationResponse, UpsertUserDocumentViewLocationResponses, UserApiKeyId, UserApiKeyInfo, UserApiKeysList, UserDocumentsResponse, UserDocumentViewLocation, UserDocumentViewLocationResponse, UserPinsResponse, UserViewsResponse, ValidateDocumentPermissionsTokenData, ValidateDocumentPermissionsTokenError, ValidateDocumentPermissionsTokenErrors, ValidateDocumentPermissionsTokenResponse, ValidateDocumentPermissionsTokenResponses, ValidateWebhookData, ValidateWebhookError, ValidateWebhookErrors, ValidateWebhookResponse, ValidateWebhookResponse2, ValidateWebhookResponses, Vec, VersionedTable, View, ViewChange, ViewColumn, ViewerCapabilities, ViewLayout, ViewPatch, ViewPosition, ViewPositionsResponse, ViewQuery, ViewResult, ViewsResponse, WaitingForInputMetadata, Webhook, WebhookEvent, WebhookFilter, WebhookScope, WebhookStatus, WebhookValidationTestEvent, WhatsappGroupData, WhatsAppGroupDetail, WhatsAppGroupMessage, WhatsAppGroupMessages, WhatsappGroupMessagesData, WhatsappGroupMessagesResponse, WhatsappGroupMessagesResponses, WhatsappGroupPermissionsData, WhatsappGroupPermissionsResponse, WhatsappGroupPermissionsResponses, WhatsAppGroupReaction, WhatsappGroupReadData, WhatsappGroupReadResponse, WhatsappGroupReadResponses, WhatsappGroupResponse, WhatsappGroupResponses, WhatsappGroupsData, WhatsAppGroupSender, WhatsappGroupSendsData, WhatsappGroupSendsResponse, WhatsappGroupSendsResponses, WhatsappGroupsResponse, WhatsappGroupsResponses, WhatsAppGroupSummary, WhatsappGroupUpdatePermissionsData, WhatsappGroupUpdatePermissionsResponse, WhatsappGroupUpdatePermissionsResponses, Widget, WithCallId, WithChannelId, WithDocumentId, WithProjectId } from './types.gen';
+export type { AccessiblePipeline, AccessLevel, AccessRequest, AccessRequestsCancelData, AccessRequestsCancelErrors, AccessRequestsCancelResponses, AccessRequestsDirectData, AccessRequestsDirectErrors, AccessRequestsDirectResponse, AccessRequestsDirectResponses, AccessRequestsRemoveDirectData, AccessRequestsRemoveDirectErrors, AccessRequestsRemoveDirectResponses, AccessRequestsResolveData, AccessRequestsResolveErrors, AccessRequestsResolveResponse, AccessRequestsResolveResponses, AccessRequestsSetDirectData, AccessRequestsSetDirectErrors, AccessRequestsSetDirectResponses, AccessRequestsStateData, AccessRequestsStateErrors, AccessRequestsStateResponse, AccessRequestsStateResponses, AccessRequestsSubmitData, AccessRequestsSubmitErrors, AccessRequestsSubmitResponse, AccessRequestsSubmitResponses, AccessRequestState, ActiveCallsResponse, ActiveCallSummary, ActiveMeeting, ActiveMeetingsResponse, ActivityType, AddFavoriteData, AddFavoriteError, AddFavoriteErrors, AddFavoriteRequest, AddFavoriteResponse, AddFavoriteResponses, AddParticipantsData, AddParticipantsError, AddParticipantsErrors, AddParticipantsRequest, AddParticipantsResponses, AddPinHandlerData, AddPinHandlerError, AddPinHandlerErrors, AddPinHandlerResponse, AddPinHandlerResponses, AddPinRequest, AdoptedGroup, Agent, AgentActionId, AgentChannelScope, AgentDmProfile, AgentMcpServer, AgentMcpServers, AgentPullRequestState, AgentSessionFilters, AgentSessionLifecycleEvent, AgentSessionSource, AgentSessionVisibility, AgentTyping, AgentTypingPhase, Anchor, AnchorId, AnchorResponse, AnnotationIncrementalUpdate, Answer, ApiActivity, ApiAttachmentChannelReference, ApiAttachmentCommentReference, ApiAttachmentEntityReference, ApiAttachmentGenericReference, ApiChannelAttachment, ApiChannelAttachmentsPage, ApiChannelDetail, ApiChannelListMessage, ApiChannelListPage, ApiChannelListParticipant, ApiChannelListType, ApiChannelParticipant, ApiChannelWithLatest, ApiEntityFilterAst, ApiGroupByField, ApiGroupMeta, ApiParticipantListRole, ApiParticipantRole, ApiPropertyEntityType, AppliedOps, ApplyCrmPipelineOpsData, ApplyCrmPipelineOpsResponse, ApplyCrmPipelineOpsResponses, ApplyDatabaseOpsData, ApplyDatabaseOpsError, ApplyDatabaseOpsErrors, ApplyDatabaseOpsResponse, ApplyDatabaseOpsResponses, ApplyOpsRequest, ApplyOpsResponse, ApproveHarnessPairingData, ApproveHarnessPairingError, ApproveHarnessPairingErrors, ApproveHarnessPairingResponse, ApproveHarnessPairingResponses, ApprovePairingRequest, AttachmentChange, AttendeeResponseStatus, Audience, Awareness, AwarenessRelay, BasicDocument, BasicDocumentSubType, BomPart, BookingTarget, Bot, BotChannel, BotChannelType, BotId, BotKind, BotOwner, BotOwnerProfile, BotSenderProfile, BotToken, BranchNameResponse, BTreeMap, BulkWakeupRequest, BulkWakeupResponse, BulkWakeupSyncServiceDocumentsData, BulkWakeupSyncServiceDocumentsErrors, BulkWakeupSyncServiceDocumentsResponse, BulkWakeupSyncServiceDocumentsResponses, CalendarAttendee, CalendarEvent, CalendarEventFilters, CalendarEventSourceContent, CalendarJoinRequest, CalendarJoinRequestApiError, CalendarJoinRequestErrorCode, CalendarJoinRequestsResponse, CalendarJoinRequestStatus, CalendarMentionEvent, CalendarMentionPreviewItem, CalendarMentionPreviewKind, CalendarMentionPreviewRequest, CalendarMentionPreviewRequestItem, CalendarMentionPreviewResponse, CalendarMentionViewerCopy, CalendarOccurrence, CalendarOccurrenceItem, CalendarOccurrenceResponse, CalendarSyncStatus, CallActiveResponse, CallFilters, CallProvider, CallRecord, CallRecordGuest, CallRecordParticipant, CallRecordPreview, CallRecordPreviewData, CallRecordSource, CallRecordTranscriptSegment, CallStatus, CallTokenResponse, CallWebhookData, CallWebhookError, CallWebhookErrors, CallWebhookResponses, CancelSlackImportData, CancelSlackImportErrors, CancelSlackImportResponse, CancelSlackImportResponses, CardPosition, CastVerdict, CellValue, CellWrite, ChangedFileDto, ChangesetDto, ChangesetSourceDto, Channel, ChannelCreatedMetadata, ChannelDeletedMetadata, ChannelFilters, ChannelJoinCodeResponse, ChannelLabel, ChannelLabelRule, ChannelLabelsList, ChannelMessage, ChannelParticipant, ChannelParticipantAddedMetadata, ChannelParticipantRemovedMetadata, ChannelPictureChangedMetadata, ChannelPreview, ChannelPreviewData, ChannelSender, ChannelSharePermission, ChannelThreadFilters, ChannelTopicEvent, ChannelType, ChannelUpdatedMetadata, ChannelWebhookRequest, ChannelWebhookResponse, ChannelWithParticipants, Chat, ChatFilters, CheckActiveCallData, CheckActiveCallError, CheckActiveCallErrors, CheckActiveCallResponse, CheckActiveCallResponses, ClaimedPairing, ClaimHarnessPairingData, ClaimHarnessPairingError, ClaimHarnessPairingErrors, ClaimHarnessPairingResponse, ClaimHarnessPairingResponses, ClaimPairingRequest, ClientOptions, CloudStorageItemType, CollaborateFormData, CollaborateFormError, CollaborateFormErrors, CollaborateFormResponse, CollaborateFormResponses, CollabSurfaceResponse, CollabSurfaceTokenResponse, Column, ColumnCast, ColumnChange, ColumnChangeKind, ColumnConfig, ColumnConversion, ColumnConversionRequest, ColumnDetail, ColumnId, ColumnKind, ColumnProtection, ColumnResult, CommandFailure, CommandRejectedMetadata, CommittedChange, CompleteSlackImportUploadsData, CompleteSlackImportUploadsErrors, CompleteSlackImportUploadsResponse, CompleteSlackImportUploadsResponses, CompleteUploads, ConferenceProvider, Conjunction, ConversationId, ConversationKind, ConversationMetadata, ConversationProgress, ConversationSeal, ConversationStatus, ConvertDatabaseColumnData, ConvertDatabaseColumnError, ConvertDatabaseColumnErrors, ConvertDatabaseColumnResponse, ConvertDatabaseColumnResponses, ConvertedCell, CopyDocumentData, CopyDocumentError, CopyDocumentErrors, CopyDocumentQueryParams, CopyDocumentRequest, CopyDocumentResponse, CopyDocumentResponse2, CopyDocumentResponses, CountedReaction, CreateAgentData, CreateAgentError, CreateAgentErrors, CreateAgentRequest, CreateAgentResponse, CreateAgentResponses, CreateAnchorData, CreateAnchorError, CreateAnchorErrors, CreateAnchorResponse, CreateAnchorResponses, CreateBotRequest, CreateBulkDocumentResponse, CreateBulkDocumentResponseData, CreateCanvasDocumentRequest, CreateCanvasDocumentResponse, CreateCanvasHandlerData, CreateCanvasHandlerError, CreateCanvasHandlerErrors, CreateCanvasHandlerResponse, CreateCanvasHandlerResponses, CreateChannelData, CreateChannelError, CreateChannelErrors, CreateChannelLabelData, CreateChannelLabelError, CreateChannelLabelErrors, CreateChannelLabelRequest, CreateChannelLabelResponse, CreateChannelLabelResponses, CreateChannelRequest, CreateChannelResponse, CreateChannelResponse2, CreateChannelResponses, CreateChannelScopedBotData, CreateChannelScopedBotError, CreateChannelScopedBotErrors, CreateChannelScopedBotRequest, CreateChannelScopedBotResponse, CreateChannelScopedBotResponse2, CreateChannelScopedBotResponses, CreateCollabSurfaceTokenData, CreateCollabSurfaceTokenError, CreateCollabSurfaceTokenErrors, CreateCollabSurfaceTokenResponse, CreateCollabSurfaceTokenResponses, CreateCrmCompanyData, CreateCrmCompanyError, CreateCrmCompanyErrors, CreateCrmCompanyRequest, CreateCrmCompanyResponse, CreateCrmCompanyResponses, CreateCrmContactData, CreateCrmContactError, CreateCrmContactErrors, CreateCrmContactRequest, CreateCrmContactResponse, CreateCrmContactResponses, CreateCrmPipelineData, CreateCrmPipelineResponse, CreateCrmPipelineResponses, CreateDatabaseData, CreateDatabaseError, CreateDatabaseErrors, CreateDatabaseRequest, CreateDatabaseResponse, CreateDatabaseResponses, CreateDatabaseWebhookData, CreateDatabaseWebhookError, CreateDatabaseWebhookErrors, CreateDatabaseWebhookRequest, CreateDatabaseWebhookResponse, CreateDatabaseWebhookResponses, CreatedDatabaseWebhookResponse, CreateDocumentData, CreateDocumentError, CreateDocumentErrors, CreateDocumentRequest, CreateDocumentResponse, CreateDocumentResponseData, CreateDocumentResponses, CreatedPairing, CreatedUserApiKey, CreateEntityMentionData, CreateEntityMentionError, CreateEntityMentionErrors, CreateEntityMentionRequest, CreateEntityMentionResponse, CreateEntityMentionResponse2, CreateEntityMentionResponses, CreateForm, CreateFormData, CreateFormError, CreateFormErrors, CreateFormResponse, CreateFormResponses, CreateHarnessPairingData, CreateHarnessPairingError, CreateHarnessPairingErrors, CreateHarnessPairingResponse, CreateHarnessPairingResponses, CreateImport, CreateInitiativeData, CreateInitiativeError, CreateInitiativeErrors, CreateInitiativeRequest, CreateInitiativeResponse, CreateInitiativeResponses, CreateInstructionsDocumentResponse, CreateInstructionsHandlerData, CreateInstructionsHandlerError, CreateInstructionsHandlerErrors, CreateInstructionsHandlerResponse, CreateInstructionsHandlerResponses, CreateMarkdownDocumentRequest, CreateMarkdownDocumentResponse, CreateMarkdownHandlerData, CreateMarkdownHandlerError, CreateMarkdownHandlerErrors, CreateMarkdownHandlerResponse, CreateMarkdownHandlerResponses, CreateMeetingRequest, CreatePairingRequest, CreatePipeline, CreateProjectHandlerData, CreateProjectHandlerError, CreateProjectHandlerErrors, CreateProjectHandlerResponse, CreateProjectHandlerResponses, CreateProjectRequest, CreateProjectResponse, CreateSkillHandlerData, CreateSkillHandlerError, CreateSkillHandlerErrors, CreateSkillHandlerResponse, CreateSkillHandlerResponses, CreateSkillRequest, CreateSkillResponse, CreateSlackImportData, CreateSlackImportErrors, CreateSlackImportResponse, CreateSlackImportResponses, CreateSnippetHandlerData, CreateSnippetHandlerError, CreateSnippetHandlerErrors, CreateSnippetHandlerResponse, CreateSnippetHandlerResponses, CreateSnippetRequest, CreateSnippetResponse, CreateTaskHandlerData, CreateTaskHandlerError, CreateTaskHandlerErrors, CreateTaskHandlerResponse, CreateTaskHandlerResponses, CreateTaskRequest, CreateTaskResponse, CreateToken, CreateUnthreadedAnchorRequest, CreateUnthreadedAnchorResponse, CreateUnthreadedPdfAnchorRequest, CreateUserApiKeyData, CreateUserApiKeyError, CreateUserApiKeyErrors, CreateUserApiKeyRequest, CreateUserApiKeyResponse, CreateUserApiKeyResponses, CreateViewHandlerData, CreateViewHandlerError, CreateViewHandlerErrors, CreateViewHandlerResponse, CreateViewHandlerResponses, CreateViewRequest, CreateWebhookData, CreateWebhookError, CreateWebhookErrors, CreateWebhookRequest, CreateWebhookResponse, CreateWebhookResponse2, CreateWebhookResponses, CrmAssociation, CrmCompanyFilters, CrmCompanyResponse, CrmContactResponse, CrmDomainResponse, CrmPermissionRole, CrmStageInput, CrmStageResponse, CrmStagesResponse, CrmTeamSettingsResponse, CustomSpeakerAssignment, Database, DatabaseDetail, DatabaseId, DatabaseOp, DatabaseQueryRequest, DatabaseView, DatabaseWebhookDeliveryResponse, DatabaseWebhookPayloadProblem, DatabaseWebhookResponse, DataType, DateOperator, DeclineCallData, DeclineCallError, DeclineCallErrors, DeclineCallResponse, DeclineCallResponses, DeclineRingData, DeclineRingError, DeclineRingErrors, DeclineRingResponse, DeclineRingResponses, DeleteAnchorData, DeleteAnchorError, DeleteAnchorErrors, DeleteAnchorResponse, DeleteAnchorResponses, DeleteCallRecordData, DeleteCallRecordError, DeleteCallRecordErrors, DeleteCallRecordResponse, DeleteCallRecordResponses, DeleteChannelData, DeleteChannelError, DeleteChannelErrors, DeleteChannelLabelData, DeleteChannelLabelError, DeleteChannelLabelErrors, DeleteChannelLabelResponse, DeleteChannelLabelResponses, DeleteChannelResponse, DeleteChannelResponses, DeleteCollabSurfaceData, DeleteCollabSurfaceError, DeleteCollabSurfaceErrors, DeleteCollabSurfaceResponse, DeleteCollabSurfaceResponses, DeleteDatabaseWebhookData, DeleteDatabaseWebhookError, DeleteDatabaseWebhookErrors, DeleteDatabaseWebhookResponse, DeleteDatabaseWebhookResponses, DeleteDocumentData, DeleteDocumentError, DeleteDocumentErrors, DeleteDocumentResponse, DeleteDocumentResponses, DeleteEntityMentionData, DeleteEntityMentionError, DeleteEntityMentionErrors, DeleteEntityMentionResponse, DeleteEntityMentionResponse2, DeleteEntityMentionResponses, DeleteHarnessData, DeleteHarnessError, DeleteHarnessErrors, DeleteHarnessResponse, DeleteHarnessResponses, DeleteHistoryHandlerData, DeleteHistoryHandlerError, DeleteHistoryHandlerErrors, DeleteHistoryHandlerResponse, DeleteHistoryHandlerResponses, DeleteInitiativeData, DeleteInitiativeError, DeleteInitiativeErrors, DeleteInitiativeResponse, DeleteInitiativeResponses, DeleteProjectHandlerData, DeleteProjectHandlerError, DeleteProjectHandlerErrors, DeleteProjectHandlerResponse, DeleteProjectHandlerResponses, DeleteSelfHarnessData, DeleteSelfHarnessError, DeleteSelfHarnessErrors, DeleteSelfHarnessResponse, DeleteSelfHarnessResponses, DeleteUnthreadedAnchorRequest, DeleteUnthreadedAnchorResponse, DeleteUnthreadedPdfAnchorRequest, DeleteUserApiKeyData, DeleteUserApiKeyError, DeleteUserApiKeyErrors, DeleteUserApiKeyResponse, DeleteUserApiKeyResponses, DeleteUserDocumentViewLocationData, DeleteUserDocumentViewLocationError, DeleteUserDocumentViewLocationErrors, DeleteUserDocumentViewLocationResponse, DeleteUserDocumentViewLocationResponses, DeleteViewHandlerData, DeleteViewHandlerError, DeleteViewHandlerErrors, DeleteViewHandlerResponses, DeleteWebhookData, DeleteWebhookError, DeleteWebhookErrors, DeleteWebhookResponse, DeleteWebhookResponses, DeliverDatabaseWebhookData, DeliverDatabaseWebhookError, DeliverDatabaseWebhookErrors, DeliverDatabaseWebhookResponse, DeliverDatabaseWebhookResponses, DirectAccess, DocumentBasic, DocumentContent, DocumentContentLocation, DocumentContentState, DocumentContentUploadedMetadata, DocumentCopiedMetadata, DocumentCreatedMetadata, DocumentDeletedMetadata, DocumentFilters, DocumentId, DocumentInteractionMetadata, DocumentMetadata, DocumentMetadataWithContent, DocumentPermissionsToken, DocumentPermissionsTokenRequest, DocumentPermissionsTokenResponse, DocumentPreview, DocumentPreviewData, DocumentPreviewDataSubType, DocumentPurgedMetadata, DocumentResponse, DocumentResponseMetadata, DocumentResponseMetadataWithContent, DocumentStorageServiceApiVersion, DocumentSubType, DocumentSyncContentUpdatedMetadata, DocumentSyncEditor, DocumentTeamShareResponse, DocumentTextContent, DocumentTopicEvent, DocumentUpdatedMetadata, EditAnchorData, EditAnchorError, EditAnchorErrors, EditAnchorRequest, EditAnchorResponse, EditAnchorResponse2, EditAnchorResponses, EditCallRecordData, EditCallRecordError, EditCallRecordErrors, EditCallRecordRequest, EditCallRecordResponse, EditCallRecordResponses, EditCallTranscriptData, EditCallTranscriptError, EditCallTranscriptErrors, EditCallTranscriptRequest, EditCallTranscriptResponse, EditCallTranscriptResponses, EditDocumentData, EditDocumentError, EditDocumentErrors, EditDocumentResponse, EditDocumentResponse2, EditDocumentResponses, EditDocumentServiceArgs, EditMyFormResponseData, EditMyFormResponseError, EditMyFormResponseErrors, EditMyFormResponseResponse, EditMyFormResponseResponses, EditPdfAnchorRequest, EditPdfPlaceableCommentAnchorRequest, EditProjectV2Data, EditProjectV2Error, EditProjectV2Errors, EditProjectV2Response, EditProjectV2Responses, EditThreadV2Data, EditThreadV2Error, EditThreadV2Errors, EditThreadV2Response, EditThreadV2Responses, EmailFilters, EmailFollowup, EmailFollowupCommand, EmailFollowupResponse, EmailReminderCondition, EmailReminderPage, EmailReminderSummary, EmptyResponse, EnsureCollabSurfaceData, EnsureCollabSurfaceError, EnsureCollabSurfaceErrors, EnsureCollabSurfaceRequest, EnsureCollabSurfaceResponse, EnsureCollabSurfaceResponses, EnsureStarterHandlerData, EnsureStarterHandlerErrors, EnsureStarterHandlerResponse, EnsureStarterHandlerResponses, EntityFilters, EntityKind, EntityMessageCreateData, EntityMessageCreateResponse, EntityMessageCreateResponses, EntityMessageDeleteMessageData, EntityMessageDeleteMessageResponse, EntityMessageDeleteMessageResponses, EntityMessageDeleteThreadData, EntityMessageDeleteThreadResponse, EntityMessageDeleteThreadResponses, EntityMessageEditData, EntityMessageEditResponse, EntityMessageEditResponses, EntityMessageGetMessageData, EntityMessageGetMessageResponse, EntityMessageGetMessageResponses, EntityMessageGetThreadData, EntityMessageGetThreadResponse, EntityMessageGetThreadResponses, EntityMessageLegacyData, EntityMessageLegacyResponse, EntityMessageLegacyResponses, EntityMessagePatchThreadData, EntityMessagePatchThreadResponse, EntityMessagePatchThreadResponses, EntityMessageReactData, EntityMessageReactResponse, EntityMessageReactResponses, EntityMessageTypingData, EntityMessageTypingResponse, EntityMessageTypingResponses, EntityPermission, EntityPermissionResponse, EntityRef, EntityReference, EntityType, ErrorResponse, EventIcsData, EventIcsErrors, EventIcsResponse, EventIcsResponses, EventReminderOverride, EventReminders, EventStatus, EventTime, EventTransparency, EventType, EventVisibility, ExcludedDefaultView, ExcludeDefaultViewHandlerData, ExcludeDefaultViewHandlerError, ExcludeDefaultViewHandlerErrors, ExcludeDefaultViewHandlerResponses, ExcludeDefaultViewRequest, ExportDocumentData, ExportDocumentError, ExportDocumentErrors, ExportDocumentResponse, ExportDocumentResponse2, ExportDocumentResponses, Favorite, FavoriteEntityRef, FavoritesList, FileChangeKindDto, FileSystemNodeWithIds, FileType, FileTypeUpdate, FilterCondition, FilterGroup, FilterNode, FilterTest, FinalizeSlackImportData, FinalizeSlackImportErrors, FinalizeSlackImportResponse, FinalizeSlackImportResponses, FolderItem, FollowupState, ForeignEntity, ForeignEntityFilters, Form, FormAccess, FormCollaboration, FormDetail, FormErrorCode, FormErrorResponse, FormLayout, FormPublicationProblem, FormQuestionDetail, FormResponse, FormSection, FormSectionDetail, FormSource, FormStatus, FormTally, Formula, GenericErrorResponse, GenericResponse, GenericSuccessResponse, GetActiveCallsData, GetActiveCallsError, GetActiveCallsErrors, GetActiveCallsResponse, GetActiveCallsResponses, GetActivityData, GetActivityError, GetActivityErrors, GetActivityResponse, GetActivityResponses, GetAttachmentReferencesData, GetAttachmentReferencesError, GetAttachmentReferencesErrors, GetAttachmentReferencesResponse, GetAttachmentReferencesResponse2, GetAttachmentReferencesResponses, GetBatchCallRecordPreviewData, GetBatchCallRecordPreviewError, GetBatchCallRecordPreviewErrors, GetBatchCallRecordPreviewRequest, GetBatchCallRecordPreviewResponse, GetBatchCallRecordPreviewResponse2, GetBatchCallRecordPreviewResponses, GetBatchChannelPreviewData, GetBatchChannelPreviewError, GetBatchChannelPreviewErrors, GetBatchChannelPreviewRequest, GetBatchChannelPreviewResponse, GetBatchChannelPreviewResponse2, GetBatchChannelPreviewResponses, GetBatchPreviewHandlerData, GetBatchPreviewHandlerError, GetBatchPreviewHandlerErrors, GetBatchPreviewHandlerResponse, GetBatchPreviewHandlerResponses, GetBatchPreviewRequest, GetBatchPreviewResponse, GetBatchProjectPreviewData, GetBatchProjectPreviewError, GetBatchProjectPreviewErrors, GetBatchProjectPreviewRequest, GetBatchProjectPreviewResponse, GetBatchProjectPreviewResponse2, GetBatchProjectPreviewResponses, GetBotOwnerProfilesData, GetBotOwnerProfilesError, GetBotOwnerProfilesErrors, GetBotOwnerProfilesResponse, GetBotOwnerProfilesResponses, GetCallRecordData, GetCallRecordError, GetCallRecordErrors, GetCallRecordResponse, GetCallRecordResponses, GetChannelAttachmentsData, GetChannelAttachmentsError, GetChannelAttachmentsErrors, GetChannelAttachmentsResponse, GetChannelAttachmentsResponses, GetChannelData, GetChannelError, GetChannelErrors, GetChannelJoinLinkData, GetChannelJoinLinkError, GetChannelJoinLinkErrors, GetChannelJoinLinkResponse, GetChannelJoinLinkResponses, GetChannelParticipantsData, GetChannelParticipantsError, GetChannelParticipantsErrors, GetChannelParticipantsResponse, GetChannelParticipantsResponses, GetChannelResponse, GetChannelResponses, GetChannelsData, GetChannelsError, GetChannelsErrors, GetChannelsResponse, GetChannelsResponses, GetCollabSurfaceData, GetCollabSurfaceError, GetCollabSurfaceErrors, GetCollabSurfaceResponse, GetCollabSurfaceResponses, GetCompanyData, GetCompanyError, GetCompanyErrors, GetCompanyResponse, GetCompanyResponses, GetContactByEmailData, GetContactByEmailError, GetContactByEmailErrors, GetContactByEmailResponse, GetContactByEmailResponse2, GetContactByEmailResponses, GetContactData, GetContactError, GetContactErrors, GetContactResponse, GetContactResponses, GetCrmPipelineData, GetCrmPipelinePermissionsData, GetCrmPipelinePermissionsResponse, GetCrmPipelinePermissionsResponses, GetCrmPipelineResponse, GetCrmPipelineResponses, GetCrmPipelineRowsData, GetCrmPipelineRowsResponse, GetCrmPipelineRowsResponses, GetCrmPipelineTableData, GetCrmPipelineTableResponse, GetCrmPipelineTableResponses, GetCrmTeamSettingsData, GetCrmTeamSettingsError, GetCrmTeamSettingsErrors, GetCrmTeamSettingsResponse, GetCrmTeamSettingsResponses, GetDatabaseData, GetDatabaseError, GetDatabaseErrors, GetDatabasePermissionsData, GetDatabasePermissionsError, GetDatabasePermissionsErrors, GetDatabasePermissionsResponse, GetDatabasePermissionsResponses, GetDatabaseQueryData, GetDatabaseQueryError, GetDatabaseQueryErrors, GetDatabaseQueryResponse, GetDatabaseQueryResponses, GetDatabaseResponse, GetDatabaseResponses, GetDatabaseRowHistoryData, GetDatabaseRowHistoryError, GetDatabaseRowHistoryErrors, GetDatabaseRowHistoryResponse, GetDatabaseRowHistoryResponses, GetDatabaseTableChangesData, GetDatabaseTableChangesError, GetDatabaseTableChangesErrors, GetDatabaseTableChangesResponse, GetDatabaseTableChangesResponses, GetDatabaseViewPositionsData, GetDatabaseViewPositionsError, GetDatabaseViewPositionsErrors, GetDatabaseViewPositionsResponse, GetDatabaseViewPositionsResponses, GetDocumentAnchorsData, GetDocumentAnchorsError, GetDocumentAnchorsErrors, GetDocumentAnchorsResponse, GetDocumentAnchorsResponses, GetDocumentBranchNameData, GetDocumentBranchNameError, GetDocumentBranchNameErrors, GetDocumentBranchNameResponse, GetDocumentBranchNameResponses, GetDocumentByTeamSlugData, GetDocumentByTeamSlugError, GetDocumentByTeamSlugErrors, GetDocumentByTeamSlugResponse, GetDocumentByTeamSlugResponses, GetDocumentData, GetDocumentError, GetDocumentErrors, GetDocumentGithubPullRequestsData, GetDocumentGithubPullRequestsError, GetDocumentGithubPullRequestsErrors, GetDocumentGithubPullRequestsResponse, GetDocumentGithubPullRequestsResponses, GetDocumentKeyResponse, GetDocumentKeyResponseData, GetDocumentListHandlerData, GetDocumentListHandlerError, GetDocumentListHandlerErrors, GetDocumentListHandlerResponse, GetDocumentListHandlerResponses, GetDocumentListResult, GetDocumentLocationV3Data, GetDocumentLocationV3Error, GetDocumentLocationV3Errors, GetDocumentLocationV3Response, GetDocumentLocationV3Responses, GetDocumentPermissionsResponseDataV2, GetDocumentPermissionsTokenData, GetDocumentPermissionsTokenError, GetDocumentPermissionsTokenErrors, GetDocumentPermissionsTokenResponse, GetDocumentPermissionsTokenResponses, GetDocumentPermissionsV2Data, GetDocumentPermissionsV2Error, GetDocumentPermissionsV2Errors, GetDocumentPermissionsV2Response, GetDocumentPermissionsV2Responses, GetDocumentProcessingResult, GetDocumentProcessingResultData, GetDocumentProcessingResultError, GetDocumentProcessingResultErrors, GetDocumentProcessingResultResponse, GetDocumentProcessingResultResponse2, GetDocumentProcessingResultResponses, GetDocumentResponse, GetDocumentResponse2, GetDocumentResponseData, GetDocumentResponses, GetDocumentSearchResponse, GetDocumentShortIdData, GetDocumentShortIdError, GetDocumentShortIdErrors, GetDocumentShortIdResponse, GetDocumentShortIdResponses, GetDocumentsResponse, GetDocumentTeamShareData, GetDocumentTeamShareError, GetDocumentTeamShareErrors, GetDocumentTeamShareResponse, GetDocumentTeamShareResponses, GetDocumentTextContentData, GetDocumentTextContentErrors, GetDocumentTextContentResponse, GetDocumentTextContentResponses, GetDocumentUserAccessLevelResponse, GetDocumentVersionData, GetDocumentVersionError, GetDocumentVersionErrors, GetDocumentVersionResponse, GetDocumentVersionResponses, GetDocumentViewsHandlerData, GetDocumentViewsHandlerError, GetDocumentViewsHandlerErrors, GetDocumentViewsHandlerResponse, GetDocumentViewsHandlerResponses, GetEmailFollowupData, GetEmailFollowupError, GetEmailFollowupErrors, GetEmailFollowupResponse, GetEmailFollowupResponses, GetEntityPermissionData, GetEntityPermissionErrors, GetEntityPermissionResponse, GetEntityPermissionResponses, GetForeignEntityBySourceData, GetForeignEntityBySourceError, GetForeignEntityBySourceErrors, GetForeignEntityBySourceResponse, GetForeignEntityBySourceResponses, GetForeignEntityData, GetForeignEntityError, GetForeignEntityErrors, GetForeignEntityResponse, GetForeignEntityResponses, GetFormData, GetFormError, GetFormErrors, GetFormPermissionsData, GetFormPermissionsError, GetFormPermissionsErrors, GetFormPermissionsResponse, GetFormPermissionsResponses, GetFormResponse, GetFormResponses, GetFormResponseSummaryData, GetFormResponseSummaryError, GetFormResponseSummaryErrors, GetFormResponseSummaryResponse, GetFormResponseSummaryResponses, GetFormTallyData, GetFormTallyError, GetFormTallyErrors, GetFormTallyResponse, GetFormTallyResponses, GetGithubPullRequestChangesData, GetGithubPullRequestChangesError, GetGithubPullRequestChangesErrors, GetGithubPullRequestChangesPatchData, GetGithubPullRequestChangesPatchError, GetGithubPullRequestChangesPatchErrors, GetGithubPullRequestChangesPatchResponse, GetGithubPullRequestChangesPatchResponses, GetGithubPullRequestChangesResponse, GetGithubPullRequestChangesResponses, GetGithubPullRequestData, GetGithubPullRequestError, GetGithubPullRequestErrors, GetGithubPullRequestFacetsData, GetGithubPullRequestFacetsError, GetGithubPullRequestFacetsErrors, GetGithubPullRequestFacetsResponse, GetGithubPullRequestFacetsResponses, GetGithubPullRequestResponse, GetGithubPullRequestResponses, GetGithubPullRequestTasksData, GetGithubPullRequestTasksError, GetGithubPullRequestTasksErrors, GetGithubPullRequestTasksResponse, GetGithubPullRequestTasksResponses, GetHarnessPairingData, GetHarnessPairingError, GetHarnessPairingErrors, GetHarnessPairingResponse, GetHarnessPairingResponses, GetHistoryHandlerData, GetHistoryHandlerError, GetHistoryHandlerErrors, GetHistoryHandlerResponse, GetHistoryHandlerResponses, GetInitiativeData, GetInitiativeError, GetInitiativeErrors, GetInitiativeResponse, GetInitiativeResponses, GetInstructionsDocumentResponse, GetInstructionsHandlerData, GetInstructionsHandlerError, GetInstructionsHandlerErrors, GetInstructionsHandlerResponse, GetInstructionsHandlerResponses, GetItemsSoupData, GetItemsSoupError, GetItemsSoupErrors, GetItemsSoupResponse, GetItemsSoupResponses, GetLocationHandlerData, GetLocationHandlerError, GetLocationHandlerErrors, GetLocationHandlerResponse, GetLocationHandlerResponses, GetMyFormResponseData, GetMyFormResponseError, GetMyFormResponseErrors, GetMyFormResponseResponse, GetMyFormResponseResponses, GetOrCreateAction, GetOrCreateCallData, GetOrCreateCallError, GetOrCreateCallErrors, GetOrCreateCallResponse, GetOrCreateCallResponses, GetOrCreateChannelResponse, GetOrCreateDmData, GetOrCreateDmError, GetOrCreateDmErrors, GetOrCreateDmRequest, GetOrCreateDmResponse, GetOrCreateDmResponses, GetOrCreatePrivateData, GetOrCreatePrivateError, GetOrCreatePrivateErrors, GetOrCreatePrivateRequest, GetOrCreatePrivateResponse, GetOrCreatePrivateResponses, GetPendingProjectsHandlerData, GetPendingProjectsHandlerError, GetPendingProjectsHandlerErrors, GetPendingProjectsHandlerResponse, GetPendingProjectsHandlerResponses, GetPinsHandlerData, GetPinsHandlerError, GetPinsHandlerErrors, GetPinsHandlerResponse, GetPinsHandlerResponses, GetPinsResponse, GetProjectContentHandlerData, GetProjectContentHandlerError, GetProjectContentHandlerErrors, GetProjectContentHandlerResponse, GetProjectContentHandlerResponses, GetProjectContentResponse, GetProjectHandlerData, GetProjectHandlerError, GetProjectHandlerErrors, GetProjectHandlerResponse, GetProjectHandlerResponses, GetProjectPermissionsV2Data, GetProjectPermissionsV2Error, GetProjectPermissionsV2Errors, GetProjectPermissionsV2Response, GetProjectPermissionsV2Responses, GetProjectResponse, GetProjectResponseData, GetProjectsHandlerData, GetProjectsHandlerError, GetProjectsHandlerErrors, GetProjectsHandlerResponse, GetProjectsHandlerResponses, GetProjectsResponse, GetProjectUserAccessLevelData, GetProjectUserAccessLevelError, GetProjectUserAccessLevelErrors, GetProjectUserAccessLevelResponse, GetProjectUserAccessLevelResponses, GetRingStatusData, GetRingStatusError, GetRingStatusErrors, GetRingStatusResponse, GetRingStatusResponses, GetSelfBotData, GetSelfBotError, GetSelfBotErrors, GetSelfBotResponse, GetSelfBotResponses, GetSelfHarnessData, GetSelfHarnessError, GetSelfHarnessErrors, GetSelfHarnessResponse, GetSelfHarnessResponses, GetSlackImportData, GetSlackImportErrors, GetSlackImportResponse, GetSlackImportResponses, GetSystemSkillsHandlerData, GetSystemSkillsHandlerError, GetSystemSkillsHandlerErrors, GetSystemSkillsHandlerResponse, GetSystemSkillsHandlerResponses, GetUserDocumentsHandlerData, GetUserDocumentsHandlerError, GetUserDocumentsHandlerErrors, GetUserDocumentsHandlerResponse, GetUserDocumentsHandlerResponses, GetUserDocumentViewLocationData, GetUserDocumentViewLocationError, GetUserDocumentViewLocationErrors, GetUserDocumentViewLocationResponse, GetUserDocumentViewLocationResponses, GetUserHistoryResponse, GetViewsHandlerData, GetViewsHandlerError, GetViewsHandlerErrors, GetViewsHandlerResponse, GetViewsHandlerResponses, GetWebhookData, GetWebhookError, GetWebhookErrors, GetWebhookResponse, GetWebhookResponses, GithubLabelFacet, GithubPullRequest, GithubPullRequestChangesPatchResponse, GithubPullRequestChangesResponse, GithubPullRequestCheckRun, GithubPullRequestComment, GithubPullRequestFacets, GithubPullRequestLabel, GithubPullRequestReview, GithubPullRequestReviewDecision, GithubPullRequestReviewState, GithubPullRequestsResponse, GithubPullRequestTasks, GithubPullRequestTasksRequest, GithubPullRequestTasksResponse, GithubPullRequestUser, GithubRepositoryFacet, GithubUserFacet, GitRefDto, GroupedInitialParams, GroupedPageParams, GroupedSoupGroupPage, GroupedSoupInitialPage, GroupedSoupPage, GroupedSoupSort, GuestCallChatPostData, GuestCallChatPostErrors, GuestCallChatPostResponse, GuestCallChatPostResponses, GuestCallChatReadData, GuestCallChatReadErrors, GuestCallChatReadResponse, GuestCallChatReadResponses, GuestChatMessage, GuestChatPage, GuestChatPost, GuestId, GuestJoinRequest, GuestPolicy, HandlerData, HandlerError, HandlerErrors, HandlerResponse, HandlerResponses, Harness, HarnessAgent, HarnessId, HarnessOwner, HarnessSession, HashMap, HealthHandlerData, HealthHandlerResponse, HealthHandlerResponses, HighlightType, ImportCounters, ImportDatabaseTableData, ImportDatabaseTableError, ImportDatabaseTableErrors, ImportDatabaseTableResponse, ImportDatabaseTableResponses, ImportedAuthor, ImportError, ImportLimits, ImportPage, ImportProgress, ImportTable, ImportWarning, InferColumnTypeOutcome, InferColumnTypeRequest, InferDatabaseColumnTypeData, InferDatabaseColumnTypeError, InferDatabaseColumnTypeErrors, InferDatabaseColumnTypeResponse, InferDatabaseColumnTypeResponses, InFlightTurnSummary, IngestTranscriptData, IngestTranscriptError, IngestTranscriptErrors, IngestTranscriptResponses, InitializeUserDocumentsData, InitializeUserDocumentsError, InitializeUserDocumentsErrors, InitializeUserDocumentsResponse, InitializeUserDocumentsResponses, InitialPropertyValue, InitiativeDetail, InitiativeFilters, InitiativeId, InitiativeList, InitiativeSummary, InputReceivedMetadata, InstallSyncData, InstallSyncErrors, InteractionReason, InvalidDatabaseWebhookPayloadResponse, InviteMeetingRequest, InviteMeetingUsersRequest, Item, ItemWithUserAccessLevel, JobId, JobProcessingResultHandlerData, JobProcessingResultHandlerError, JobProcessingResultHandlerErrors, JobProcessingResultHandlerResponse, JobProcessingResultHandlerResponses, JobStatus, JoinChannelByCodeData, JoinChannelByCodeError, JoinChannelByCodeErrors, JoinChannelByCodeResponses, JoinChannelData, JoinChannelError, JoinChannelErrors, JoinChannelResponses, KeySegment, Lane, LaneKey, LatestMessage, LayoutProblem, LeaveCallResponse, LeaveChannelData, LeaveChannelError, LeaveChannelErrors, LeaveChannelResponses, LeaveOrEndCallData, LeaveOrEndCallError, LeaveOrEndCallErrors, LeaveOrEndCallResponse, LeaveOrEndCallResponses, LinkedAccount, LinkedAccountData, LinkedAccountResponse, LinkedAccountResponses, LinkedAccountState, LinkedAccountView, LinkedAdoptData, LinkedAdoptGroup, LinkedAdoptResponse, LinkedAdoptResponses, LinkedAttachment, LinkedAttachmentState, LinkedBackfill, LinkedBackfillState, LinkedCancelPairingData, LinkedCancelPairingResponse, LinkedCancelPairingResponses, LinkedCapabilities, LinkedChat, LinkedChatKind, LinkedChats, LinkedChatsData, LinkedChatsResponse, LinkedChatsResponses, LinkedConfigurationData, LinkedConfigurationResponse, LinkedConfigurationResponses, LinkedConnect, LinkedConnectData, LinkedConnectResponse, LinkedConnectResponses, LinkedDeliveryData, LinkedDeliveryResponse, LinkedDeliveryResponses, LinkedDeliveryState, LinkedDetachData, LinkedDetachResponse, LinkedDetachResponses, LinkedDisconnect, LinkedDisconnectData, LinkedDisconnectRequest, LinkedDisconnectResponse, LinkedDisconnectResponses, LinkedFeatures, LinkedGroupInfo, LinkedGroupReference, LinkedGroupsData, LinkedGroupsResponse, LinkedGroupsResponses, LinkedIdentity, LinkedMediaKind, LinkedMediaPresentation, LinkedMessageAttachments, LinkedMessageAttachmentsData, LinkedMessageAttachmentsResponse, LinkedMessageAttachmentsResponses, LinkedMutation, LinkedPairing, LinkedPairingData, LinkedPairingResponse, LinkedPairingResponses, LinkedPairingState, LinkedPairState, LinkedParticipant, LinkedPasskeyData, LinkedPasskeyResponse, LinkedPasskeyResponses, LinkedReplyBlock, LinkedReplyContent, LinkedReplyData, LinkedReplyErrors, LinkedReplyResponse, LinkedReplyResponses, LinkedSendProgress, LinkedSendReply, LinkedSharedGroupReference, LinkedUploadData, LinkedUploadedAttachment, LinkedUploadRequest, LinkedUploadResponse, LinkedUploadResponses, LinkShare, ListAccessibleFormsData, ListAccessibleFormsError, ListAccessibleFormsErrors, ListAccessibleFormsResponse, ListAccessibleFormsResponses, ListAgentsData, ListAgentsError, ListAgentsErrors, ListAgentsResponse, ListAgentsResponses, ListBotChannelsData, ListBotChannelsError, ListBotChannelsErrors, ListBotChannelsResponse, ListBotChannelsResponses, ListChannelLabelsData, ListChannelLabelsError, ListChannelLabelsErrors, ListChannelLabelsResponse, ListChannelLabelsResponses, ListCompanyContactsData, ListCompanyContactsError, ListCompanyContactsErrors, ListCompanyContactsResponse, ListCompanyContactsResponses, ListCrmPipelinesData, ListCrmPipelinesResponse, ListCrmPipelinesResponses, ListDatabaseColumnCastsData, ListDatabaseColumnCastsError, ListDatabaseColumnCastsErrors, ListDatabaseColumnCastsResponse, ListDatabaseColumnCastsResponses, ListDatabasesData, ListDatabasesError, ListDatabasesErrors, ListDatabasesResponse, ListDatabasesResponses, ListDatabaseWebhooksData, ListDatabaseWebhooksError, ListDatabaseWebhooksErrors, ListDatabaseWebhooksResponse, ListDatabaseWebhooksResponse2, ListDatabaseWebhooksResponses, ListedDatabase, ListedForm, ListEmailRemindersData, ListEmailRemindersError, ListEmailRemindersErrors, ListEmailRemindersResponse, ListEmailRemindersResponses, ListFavoritesData, ListFavoritesError, ListFavoritesErrors, ListFavoritesResponse, ListFavoritesResponses, ListFormsData, ListFormsError, ListFormsErrors, ListFormsResponse, ListFormsResponses, ListHarnessAgentsData, ListHarnessAgentsError, ListHarnessAgentsErrors, ListHarnessAgentsResponse, ListHarnessAgentsResponses, ListHarnessesData, ListHarnessesError, ListHarnessesErrors, ListHarnessesResponse, ListHarnessesResponses, ListHarnessSessionsData, ListHarnessSessionsError, ListHarnessSessionsErrors, ListHarnessSessionsResponse, ListHarnessSessionsResponses, ListInitiativesData, ListInitiativesError, ListInitiativesErrors, ListInitiativesResponse, ListInitiativesResponses, ListJoinRequestsData, ListJoinRequestsError, ListJoinRequestsErrors, ListJoinRequestsResponse, ListJoinRequestsResponses, ListOccurrencesData, ListOccurrencesErrors, ListOccurrencesResponse, ListOccurrencesResponses, ListSlackImportsData, ListSlackImportsResponse, ListSlackImportsResponses, ListTeamCalendarData, ListTeamCalendarResponse, ListTeamCalendarResponses, ListTeamOutOfOfficeData, ListTeamOutOfOfficeErrors, ListTeamOutOfOfficeResponse, ListTeamOutOfOfficeResponses, ListUserApiKeysData, ListUserApiKeysError, ListUserApiKeysErrors, ListUserApiKeysResponse, ListUserApiKeysResponses, ListWebhooksData, ListWebhooksError, ListWebhooksErrors, ListWebhooksResponse, ListWebhooksResponse2, ListWebhooksResponses, LocationResponseData, LocationResponseV3, MacroUserIdStr, MarkRead, Meeting, MeetingCancelData, MeetingCancelError, MeetingCancelErrors, MeetingCancelPreparationData, MeetingCancelPreparationError, MeetingCancelPreparationErrors, MeetingCancelPreparationResponse, MeetingCancelPreparationResponses, MeetingCancelResponse, MeetingCancelResponses, MeetingCreateData, MeetingCreateError, MeetingCreateErrors, MeetingCreateResponse, MeetingCreateResponses, MeetingGuestJoinData, MeetingGuestJoinError, MeetingGuestJoinErrors, MeetingGuestJoinResponse, MeetingGuestJoinResponses, MeetingGuestParticipantsData, MeetingGuestParticipantsError, MeetingGuestParticipantsErrors, MeetingGuestParticipantsResponse, MeetingGuestParticipantsResponses, MeetingInviteData, MeetingInviteError, MeetingInviteErrors, MeetingInvitePermissions, MeetingInvitePermissionsData, MeetingInvitePermissionsError, MeetingInvitePermissionsErrors, MeetingInvitePermissionsResponse, MeetingInvitePermissionsResponses, MeetingInviteResponse, MeetingInviteResponses, MeetingInviteUsersData, MeetingInviteUsersError, MeetingInviteUsersErrors, MeetingInviteUsersResponse, MeetingInviteUsersResponses, MeetingJoinData, MeetingJoinError, MeetingJoinErrors, MeetingJoinResponse, MeetingJoinResponses, MeetingLeaveData, MeetingLeaveError, MeetingLeaveErrors, MeetingLeaveResponse, MeetingLeaveResponses, MeetingListActiveData, MeetingListActiveError, MeetingListActiveErrors, MeetingListActiveResponse, MeetingListActiveResponses, MeetingListData, MeetingListError, MeetingListErrors, MeetingListResponse, MeetingListResponses, MeetingLookupData, MeetingLookupError, MeetingLookupErrors, MeetingLookupResponse, MeetingLookupResponses, MeetingParticipant, MeetingParticipants, MeetingParticipantsData, MeetingParticipantsError, MeetingParticipantsErrors, MeetingParticipantsResponse, MeetingParticipantsResponses, MeetingPreparation, MeetingPrepareData, MeetingPrepareError, MeetingPrepareErrors, MeetingPrepareResponse, MeetingPrepareResponses, MeetingShareData, MeetingShareError, MeetingShareErrors, MeetingShareResponse, MeetingShareResponses, MeetingsResponse, MeetingToken, MeetingUpdateData, MeetingUpdateError, MeetingUpdateErrors, MeetingUpdateResponse, MeetingUpdateResponses, MentionPreviewsData, MentionPreviewsErrors, MentionPreviewsResponse, MentionPreviewsResponses, Message, MessageAttachment, MessageAttachmentCreatedMetadata, MessageAttachmentRemovedMetadata, MessageChange, MessageCursor, MessageDeletedMetadata, MessageDirection, MessageEvent, MessageEventAttachment, MessageListItem, MessageMentionedMetadata, MessagePage, MessageParent, MessagePatch, MessagePatchedMetadata, MessagePostedMetadata, MessageThread, MessageThreadPreview, MessageTimelineData, MessageTimelineEntriesData, MessageTimelineEntriesResponse, MessageTimelineEntriesResponses, MessageTimelineEntry, MessageTimelinePage, MessageTimelineQuery, MessageTimelineResponse, MessageTimelineResponses, MessageTopicEvent, MyResponse, NewAttachment, NewColumn, NewOption, NewThreadAnchor, NewView, NotificationFilters, NotificationState, NumberOperator, OpBatch, Operator, OpRefusalResponse, OpResult, OptionId, OptionRef, PairingDetails, Params, ParticipantRole, PatchBotRequest, PatchChannelData, PatchChannelError, PatchChannelErrors, PatchChannelRequest, PatchChannelResponse, PatchChannelResponses, PatchMessageNotificationPolicy, PatchProjectRequestV2, PatchThreadRequestV2, PatchViewHandlerData, PatchViewHandlerError, PatchViewHandlerErrors, PatchViewHandlerResponses, PatchWebhookData, PatchWebhookError, PatchWebhookErrors, PatchWebhookRequest, PatchWebhookResponse, PatchWebhookResponses, PdfAnchor, PdfAnchorId, PdfHighlightAnchor, PdfHighlightAnchorRequest, PdfHighlightRect, PdfHighlightRectAnchorRequest, PdfPlaceableCommentAnchor, PendingClaimResponse, PermanentlyDeleteDocumentData, PermanentlyDeleteDocumentError, PermanentlyDeleteDocumentErrors, PermanentlyDeleteDocumentResponse, PermanentlyDeleteDocumentResponses, PermanentlyDeleteProjectData, PermanentlyDeleteProjectError, PermanentlyDeleteProjectErrors, PermanentlyDeleteProjectResponse, PermanentlyDeleteProjectResponses, PinnedItem, PinRequest, Pipeline, PipelineRecordType, PipelineSharing, PostActivityData, PostActivityError, PostActivityErrors, PostActivityRequest, PostActivityResponse, PostActivityResponses, PostChannelBotWebhookData, PostChannelBotWebhookError, PostChannelBotWebhookErrors, PostChannelBotWebhookResponse, PostChannelBotWebhookResponses, PostGroupedSoupAstGroupPageRequest, PostGroupedSoupAstInitialRequest, PostGroupedSoupAstRequest, PostItemsSoupAstData, PostItemsSoupAstError, PostItemsSoupAstErrors, PostItemsSoupAstGroupedData, PostItemsSoupAstGroupedError, PostItemsSoupAstGroupedErrors, PostItemsSoupAstGroupedResponse, PostItemsSoupAstGroupedResponses, PostItemsSoupAstResponse, PostItemsSoupAstResponses, PostItemsSoupData, PostItemsSoupError, PostItemsSoupErrors, PostItemsSoupResponse, PostItemsSoupResponses, PostMessage, PostMessageNotificationPolicy, PostSoupAstRequest, PostSoupRequest, PresaveDocumentHandlerData, PresaveDocumentHandlerError, PresaveDocumentHandlerErrors, PresaveDocumentHandlerResponse, PresaveDocumentHandlerResponses, PreSaveDocumentRequest, PreSaveDocumentResponse, PreSaveDocumentResponseData, PresenceOperator, PresignedUrl, PreviewSmartTagData, PreviewSmartTagError, PreviewSmartTagErrors, PreviewSmartTagResponse, PreviewSmartTagResponses, Project, ProjectDeleteResponseData, ProjectDocumentStatus, ProjectFilters, ProjectPreview, ProjectPreviewData, PropertyDefinition, PropertyDefinitionWithOptions, PropertyFilter, PropertyInput, PropertyOption, PropertyOptionValue, PropertyOwner, PropertyValue, PutCrmTeamSettingsData, PutCrmTeamSettingsError, PutCrmTeamSettingsErrors, PutCrmTeamSettingsResponse, PutCrmTeamSettingsResponses, PutCrmTeamStagesData, PutCrmTeamStagesError, PutCrmTeamStagesErrors, PutCrmTeamStagesResponse, PutCrmTeamStagesResponses, PutFormLayoutData, PutFormLayoutError, PutFormLayoutErrors, PutFormLayoutResponse, PutFormLayoutResponses, QueryCrmPipelineRowsData, QueryCrmPipelineRowsResponse, QueryCrmPipelineRowsResponses, QueryDatabasesData, QueryDatabasesError, QueryDatabasesErrors, QueryDatabasesResponse, QueryDatabasesResponses, QueryDefinition, QuestionLayout, QuestionOption, QuestionTally, ReactionInput, RecentlyDeletedData, RecentlyDeletedError, RecentlyDeletedErrors, RecentlyDeletedResponse, RecentlyDeletedResponseData, RecentlyDeletedResponses, RegisterSlackImportUploadsData, RegisterSlackImportUploadsErrors, RegisterSlackImportUploadsResponse, RegisterSlackImportUploadsResponses, RegisterUploads, RemoveBotFromChannelByBotData, RemoveBotFromChannelByBotError, RemoveBotFromChannelByBotErrors, RemoveBotFromChannelByBotResponse, RemoveBotFromChannelByBotResponses, RemoveFavoriteByEntityData, RemoveFavoriteByEntityError, RemoveFavoriteByEntityErrors, RemoveFavoriteByEntityResponses, RemoveParticipantsData, RemoveParticipantsError, RemoveParticipantsErrors, RemoveParticipantsRequest, RemoveParticipantsResponses, RemovePinHandlerData, RemovePinHandlerError, RemovePinHandlerErrors, RemovePinHandlerResponse, RemovePinHandlerResponses, RenameChannelLabelData, RenameChannelLabelError, RenameChannelLabelErrors, RenameChannelLabelRequest, RenameChannelLabelResponse, RenameChannelLabelResponses, RenameCrmPipelineData, RenameCrmPipelineResponse, RenameCrmPipelineResponses, RenamePipeline, ReorderFavoritesData, ReorderFavoritesError, ReorderFavoritesErrors, ReorderFavoritesRequest, ReorderFavoritesResponses, ReorderPinRequest, ReorderPinsHandlerData, ReorderPinsHandlerError, ReorderPinsHandlerErrors, ReorderPinsHandlerResponse, ReorderPinsHandlerResponses, ReplaceCrmStagesRequest, RequestAccess, RequestedHarnessScope, RequestedLayout, RequestStatus, RequestToJoinData, RequestToJoinError, RequestToJoinErrors, RequestToJoinResponse, RequestToJoinResponses, ResetCrmTeamStagesData, ResetCrmTeamStagesError, ResetCrmTeamStagesErrors, ResetCrmTeamStagesResponse, ResetCrmTeamStagesResponses, ResolveRequest, ResponseStatus, ResponseSummary, ResultColumn, ResultSet, RevertDeleteDocumentData, RevertDeleteDocumentError, RevertDeleteDocumentErrors, RevertDeleteDocumentResponse, RevertDeleteDocumentResponses, RevertDeleteProjectData, RevertDeleteProjectError, RevertDeleteProjectErrors, RevertDeleteProjectResponse, RevertDeleteProjectResponses, RingStatus, RingStatusResponse, RowChange, RowChangeKind, RowChanges, RowHistoryEntry, RowHistoryResponse, RowId, RowsChange, RowsResult, S3ObjectInfo, SaveBomPart, SaveDatabaseQueryData, SaveDatabaseQueryError, SaveDatabaseQueryErrors, SaveDatabaseQueryResponse, SaveDatabaseQueryResponses, SaveDocumentHandlerData, SaveDocumentHandlerError, SaveDocumentHandlerErrors, SaveDocumentHandlerResponse, SaveDocumentHandlerResponses, SaveDocumentRequest, SaveDocumentResponse, SaveDocumentResponseData, SavedQuery, SaveQueryRequest, SearchContactsData, SearchContactsError, SearchContactsErrors, SearchContactsResponse, SearchContactsResponse2, SearchContactsResponses, SearchState, SectionCount, SessionDeletedMetadata, SessionIdentity, SessionMentionedMetadata, SessionOpenedMetadata, SessionRenamedMetadata, SessionSettledMetadata, SessionSource, SessionStoppedMetadata, SetChannelLabelData, SetChannelLabelError, SetChannelLabelErrors, SetChannelLabelRequest, SetChannelLabelResponse, SetChannelLabelResponses, SetChannelPictureData, SetChannelPictureError, SetChannelPictureErrors, SetChannelPictureRequest, SetChannelPictureResponse, SetChannelPictureResponses, SetCompanyHiddenData, SetCompanyHiddenError, SetCompanyHiddenErrors, SetCompanyHiddenRequest, SetCompanyHiddenResponse, SetCompanyHiddenResponses, SetCompanyNameRequest, SetContactHiddenData, SetContactHiddenError, SetContactHiddenErrors, SetContactHiddenRequest, SetContactHiddenResponse, SetContactHiddenResponses, SetContactNameRequest, SetCrmCompanyNameData, SetCrmCompanyNameError, SetCrmCompanyNameErrors, SetCrmCompanyNameResponse, SetCrmCompanyNameResponses, SetCrmContactNameData, SetCrmContactNameError, SetCrmContactNameErrors, SetCrmContactNameResponse, SetCrmContactNameResponses, SetDirectAccess, SetDocumentTeamShareData, SetDocumentTeamShareError, SetDocumentTeamShareErrors, SetDocumentTeamShareRequest, SetDocumentTeamShareResponse, SetDocumentTeamShareResponses, SetEmailFollowupData, SetEmailFollowupError, SetEmailFollowupErrors, SetEmailFollowupResponse, SetEmailFollowupResponses, SetEmailSyncData, SetEmailSyncError, SetEmailSyncErrors, SetEmailSyncRequest, SetEmailSyncResponse, SetEmailSyncResponses, SetOperator, SetPropertyValue, Sha256Digest, ShareCrmPipelineData, ShareCrmPipelineResponse, ShareCrmPipelineResponses, ShareDatabaseAwarenessData, ShareDatabaseAwarenessError, ShareDatabaseAwarenessErrors, ShareDatabaseAwarenessResponse, ShareDatabaseAwarenessResponses, SharedEmailFilter, ShareEntity, ShareEntityType, SharePermissionV2, SharePipeline, ShortIdResponse, SimpleMention, SimpleSaveData, SimpleSaveError, SimpleSaveErrors, SimpleSaveResponse, SimpleSaveResponses, SkippedCell, SlackCompleteRequest, SlackCreateRequest, SlackRegisterRequest, SlackTimestamp, SlackUserId, SmartTagChannelMatch, SmartTagPreview, SortDirection, SortKey, SoupAgentSessionSoupPropertiesField, SoupApiItem, SoupApiSort, SoupApiSortDirection, SoupAttachment, SoupCalendarEventSoupPropertiesField, SoupCalendarEventTime, SoupCallRecordGuest, SoupCallRecordParticipant, SoupCallRecordSoupPropertiesField, SoupChannel, SoupChannelThread, SoupChatSoupPropertiesField, SoupContact, SoupCountedReaction, SoupCrmCompanySoupPropertiesField, SoupCrmContactSoupPropertiesField, SoupCrmDomain, SoupDatabaseRowSoupPropertiesField, SoupDocumentSoupPropertiesField, SoupDocumentSubType, SoupEmailThreadPreview, SoupEnrichedEmailThreadPreviewSoupPropertiesField, SoupForeignEntity, SoupInitiativeSoupPropertiesField, SoupItem, SoupLabel, SoupLabelListVisibility, SoupLabelType, SoupMessageAttachment, SoupMessageListVisibility, SoupMessageSender, SoupMessageSenderType, SoupPage, SoupProjectSoupPropertiesField, SoupPropertiesField, SoupProperty, SoupThreadInfo, SoupThreadReply, SourceBinding, SourceId, SourceIdentity, SqlCell, SqlEntityKind, SqlOutcome, SqlOutcomeKind, SqlSelectOption, SqlStatement, StarterDatabase, StarterDocumentsResponse, StorageRow, StorageRows, StorageRowsQuery, StoredGithubPullRequest, StreamEventsData, StreamEventsError, StreamEventsErrors, StreamEventsResponse, StreamEventsResponses, String, Submission, SubmissionOutcome, SubmitFormResponseData, SubmitFormResponseError, SubmitFormResponseErrors, SubmitFormResponseResponse, SubmitFormResponseResponses, SuccessResponse, SurfaceState, SyncServiceVersionId, SystemSkillSummary, Table, TableChange, TableChanged, TableChanges, TableDetail, TableId, TableResult, TableVersion, TagFilterMode, TakenId, TallyBucket, TallyValue, TaskFilters, TeamCalendarContent, TeamCalendarCoverage, TeamCalendarDetails, TeamCalendarItem, TeamCalendarMember, TeamCalendarPage, TeamCalendarSharing, TeamOutOfOfficeItem, TeamOutOfOfficeResponse, TeamRole, TextOperator, ThreadAnchor, ThreadOrigin, ThreadPatch, ThreadState, TimelineActivity, ToggleShareWithTeamData, ToggleShareWithTeamError, ToggleShareWithTeamErrors, ToggleShareWithTeamResponse, ToggleShareWithTeamResponses, TouchedColumn, TouchedRow, TranscribeDictationData, TranscribeDictationError, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeResponse, TranscriptSegmentRequest, TrashCrmPipelineData, TrashCrmPipelineResponse, TrashCrmPipelineResponses, TrashPipeline, TurnEndedMetadata, TurnStartedMetadata, TurnSummary, TypedSuccessResponse, TypedSuccessResponseGetDocumentResponseData, TypingInput, UndoChangeResponse, UndoDatabaseChangeData, UndoDatabaseChangeError, UndoDatabaseChangeErrors, UndoDatabaseChangeResponse, UndoDatabaseChangeResponses, UndoOutcome, UndoRefusal, UnlockedBooking, UpdateAgentData, UpdateAgentError, UpdateAgentErrors, UpdateAgentRequest, UpdateAgentResponse, UpdateAgentResponses, UpdateChannelSharePermission, UpdateCrmPipelinePermissionsData, UpdateCrmPipelinePermissionsResponse, UpdateCrmPipelinePermissionsResponses, UpdateCrmTeamSettingsRequest, UpdateDatabasePermissionsData, UpdateDatabasePermissionsError, UpdateDatabasePermissionsErrors, UpdateDatabasePermissionsResponse, UpdateDatabasePermissionsResponses, UpdateForm, UpdateFormData, UpdateFormError, UpdateFormErrors, UpdateFormPermissionsData, UpdateFormPermissionsError, UpdateFormPermissionsErrors, UpdateFormPermissionsResponse, UpdateFormPermissionsResponses, UpdateFormResponse, UpdateFormResponses, UpdateInitiativeData, UpdateInitiativeError, UpdateInitiativeErrors, UpdateInitiativeRequest, UpdateInitiativeResponse, UpdateInitiativeResponses, UpdateMeetingRequest, UpdateOperation, UpdateSharePermissionRequestV2, UploadDescriptor, UploadDocumentStatus, UploadExtractFolderHandlerData, UploadExtractFolderHandlerError, UploadExtractFolderHandlerErrors, UploadExtractFolderHandlerResponse, UploadExtractFolderHandlerResponses, UploadExtractFolderRequest, UploadFolderHandlerData, UploadFolderHandlerError, UploadFolderHandlerErrors, UploadFolderHandlerResponse, UploadFolderHandlerResponses, UploadFolderRequest, UploadGrant, UploadId, UpsertHistoryHandlerData, UpsertHistoryHandlerError, UpsertHistoryHandlerErrors, UpsertHistoryHandlerResponse, UpsertHistoryHandlerResponses, UpsertUserDocumentViewLocationData, UpsertUserDocumentViewLocationError, UpsertUserDocumentViewLocationErrors, UpsertUserDocumentViewLocationRequest, UpsertUserDocumentViewLocationResponse, UpsertUserDocumentViewLocationResponses, UserApiKeyId, UserApiKeyInfo, UserApiKeysList, UserDocumentsResponse, UserDocumentViewLocation, UserDocumentViewLocationResponse, UserPinsResponse, UserViewsResponse, ValidateDocumentPermissionsTokenData, ValidateDocumentPermissionsTokenError, ValidateDocumentPermissionsTokenErrors, ValidateDocumentPermissionsTokenResponse, ValidateDocumentPermissionsTokenResponses, ValidateWebhookData, ValidateWebhookError, ValidateWebhookErrors, ValidateWebhookResponse, ValidateWebhookResponse2, ValidateWebhookResponses, Vec, VersionedTable, View, ViewChange, ViewColumn, ViewerCapabilities, ViewLayout, ViewPatch, ViewPosition, ViewPositionsResponse, ViewQuery, ViewResult, ViewsResponse, WaitingForInputMetadata, Webhook, WebhookEvent, WebhookFilter, WebhookScope, WebhookStatus, WebhookValidationTestEvent, WhatsAppCall, WhatsAppCallOutcome, WhatsAppContactCard, WhatsAppContacts, WhatsAppEvent, WhatsappGroupData, WhatsAppGroupDetail, WhatsAppGroupMessage, WhatsAppGroupMessages, WhatsappGroupMessagesData, WhatsappGroupMessagesResponse, WhatsappGroupMessagesResponses, WhatsappGroupPermissionsData, WhatsappGroupPermissionsResponse, WhatsappGroupPermissionsResponses, WhatsAppGroupReaction, WhatsappGroupReadData, WhatsappGroupReadResponse, WhatsappGroupReadResponses, WhatsappGroupResponse, WhatsappGroupResponses, WhatsappGroupsData, WhatsAppGroupSender, WhatsappGroupSendsData, WhatsappGroupSendsResponse, WhatsappGroupSendsResponses, WhatsappGroupsResponse, WhatsappGroupsResponses, WhatsAppGroupSummary, WhatsappGroupUpdatePermissionsData, WhatsappGroupUpdatePermissionsResponse, WhatsappGroupUpdatePermissionsResponses, WhatsAppLocation, WhatsAppMention, WhatsAppMessageKind, WhatsAppPersonRef, WhatsAppPoll, WhatsAppPollOption, WhatsAppRichContent, WhatsAppSystemAction, WhatsAppSystemLine, Widget, WithCallId, WithChannelId, WithDocumentId, WithProjectId } from './types.gen';
diff --git a/packages/sdk/generated/storage/types.gen.ts b/packages/sdk/generated/storage/types.gen.ts
index f327f5d1234..cee4ef35ea6 100644
--- a/packages/sdk/generated/storage/types.gen.ts
+++ b/packages/sdk/generated/storage/types.gen.ts
@@ -9000,10 +9000,18 @@ export type LinkedAttachment = {
* The private document, once ready.
*/
document_id?: string | null;
+ /**
+ * Length of audio or video in whole seconds, when declared.
+ */
+ duration_seconds?: number | null;
/**
* Display filename.
*/
file_name: string;
+ /**
+ * Pixel height, when declared.
+ */
+ height?: number | null;
/**
* Declared MIME type.
*/
@@ -9012,6 +9020,7 @@ export type LinkedAttachment = {
* Position within the message.
*/
ordinal: number;
+ presentation?: null | LinkedMediaPresentation;
/**
* Declared size in bytes.
*/
@@ -9020,6 +9029,10 @@ export type LinkedAttachment = {
* Current state.
*/
state: LinkedAttachmentState;
+ /**
+ * Pixel width of an image, sticker or video, when WhatsApp declared it.
+ */
+ width?: number | null;
};
/**
@@ -9262,6 +9275,11 @@ export type LinkedIdentity = {
*/
export type LinkedMediaKind = 'image' | 'audio' | 'video' | 'document';
+/**
+ * How WhatsApp shows an attachment, when not as a plain file of its kind.
+ */
+export type LinkedMediaPresentation = 'sticker' | 'gif' | 'voice_note';
+
/**
* The attachments of one linked group message.
*/
@@ -9361,6 +9379,12 @@ export type LinkedReplyBlock = 'sending_disabled' | 'no_account' | 'disconnected
* Content of a deliberate send.
*/
export type LinkedReplyContent = {
+ /**
+ * Group participants the text @-mentions, by their WhatsApp id
+ * (`jid` of a roster participant); the text must contain
+ * `@` for each.
+ */
+ mentions?: Array;
/**
* UTF-8 text, at most [`TEXT_LIMIT_BYTES`] bytes.
*/
@@ -16061,6 +16085,87 @@ export type WebhookValidationTestEvent = {
webhook_id: String;
};
+/**
+ * A call in the group.
+ */
+export type WhatsAppCall = {
+ /**
+ * Length, when it connected.
+ */
+ durationSeconds?: number | null;
+ /**
+ * How it went for the account that saw it.
+ */
+ outcome: WhatsAppCallOutcome;
+ /**
+ * Video rather than voice.
+ */
+ video: boolean;
+};
+
+/**
+ * How a call went.
+ */
+export type WhatsAppCallOutcome = 'answered' | 'missed' | 'rejected' | 'other';
+
+/**
+ * One contact card, from its vCard.
+ */
+export type WhatsAppContactCard = {
+ /**
+ * Display name.
+ */
+ name: string;
+ /**
+ * Phone numbers as written on the card.
+ */
+ phones: Array;
+};
+
+/**
+ * Contact cards.
+ */
+export type WhatsAppContacts = {
+ /**
+ * The cards, in order.
+ */
+ contacts: Array;
+};
+
+/**
+ * A WhatsApp event.
+ */
+export type WhatsAppEvent = {
+ /**
+ * Canceled by its organizer.
+ */
+ canceled?: boolean;
+ /**
+ * Description.
+ */
+ description?: string | null;
+ /**
+ * End.
+ */
+ endAt?: string | null;
+ /**
+ * Call link for an online event.
+ */
+ joinLink?: string | null;
+ /**
+ * Place name or address.
+ */
+ location?: string | null;
+ /**
+ * Title.
+ */
+ name: string;
+ /**
+ * Start.
+ */
+ startAt?: string | null;
+};
+
/**
* Full group detail and current viewer capabilities.
*/
@@ -16105,10 +16210,22 @@ export type WhatsAppGroupMessage = {
* Last edit timestamp.
*/
edited_at?: string | null;
+ /**
+ * Forwarded from another chat.
+ */
+ forwarded: boolean;
/**
* Stable message id.
*/
id: string;
+ /**
+ * A message, or a system line.
+ */
+ kind: WhatsAppMessageKind;
+ /**
+ * People the body @-mentions.
+ */
+ mentions: Array;
/**
* Reactions grouped by emoji.
*/
@@ -16121,6 +16238,7 @@ export type WhatsAppGroupMessage = {
* Message quoted by this one.
*/
reply_to?: string | null;
+ rich?: null | WhatsAppRichContent;
/**
* Sender.
*/
@@ -16213,6 +16331,156 @@ export type WhatsAppGroupSummary = {
user_id: string;
};
+/**
+ * A shared place.
+ */
+export type WhatsAppLocation = {
+ /**
+ * Street address.
+ */
+ address?: string | null;
+ /**
+ * Caption or comment.
+ */
+ comment?: string | null;
+ /**
+ * Degrees.
+ */
+ latitude: number;
+ /**
+ * Shared as a live location (this is where it started).
+ */
+ live?: boolean;
+ /**
+ * Degrees.
+ */
+ longitude: number;
+ /**
+ * Place name.
+ */
+ name?: string | null;
+ /**
+ * Link the sender attached.
+ */
+ url?: string | null;
+};
+
+/**
+ * An @-mention in a message body.
+ */
+export type WhatsAppMention = WhatsAppPersonRef & {
+ /**
+ * What follows the `@` in the body (the digits of the person's WhatsApp
+ * id); replace `@` with the name.
+ */
+ token: string;
+};
+
+/**
+ * What a timeline entry is.
+ */
+export type WhatsAppMessageKind = 'message' | 'system';
+
+/**
+ * A person named in a message: a Macro user when verified, else their
+ * WhatsApp name, else their phone number.
+ */
+export type WhatsAppPersonRef = {
+ /**
+ * Display name, never authentication evidence.
+ */
+ name: string;
+ /**
+ * Verified Macro user, if any.
+ */
+ userId?: string | null;
+};
+
+/**
+ * A poll, read-only in Macro.
+ */
+export type WhatsAppPoll = {
+ /**
+ * Options in order, with tallies.
+ */
+ options: Array;
+ /**
+ * The question.
+ */
+ question: string;
+ /**
+ * How many options one person may choose; 0 means any number.
+ */
+ selectableCount: number;
+ /**
+ * People who voted for anything.
+ */
+ voters: number;
+};
+
+/**
+ * One option of a poll with its votes.
+ */
+export type WhatsAppPollOption = {
+ /**
+ * The caller chose it.
+ */
+ mine: boolean;
+ /**
+ * Option text.
+ */
+ name: string;
+ /**
+ * Who chose it.
+ */
+ voters: Array;
+ /**
+ * People who chose it.
+ */
+ votes: number;
+};
+
+/**
+ * Structured content of a message besides its body.
+ */
+export type WhatsAppRichContent = (WhatsAppPoll & {
+ type: 'poll';
+}) | (WhatsAppLocation & {
+ type: 'location';
+}) | (WhatsAppContacts & {
+ type: 'contacts';
+}) | (WhatsAppEvent & {
+ type: 'event';
+}) | (WhatsAppSystemLine & {
+ type: 'system';
+});
+
+/**
+ * Kinds of system lines.
+ */
+export type WhatsAppSystemAction = 'group_created' | 'participants_added' | 'participants_removed' | 'participants_left' | 'participants_joined' | 'participants_promoted' | 'participants_demoted' | 'subject_changed' | 'icon_changed' | 'description_changed' | 'disappearing_changed' | 'message_pinned' | 'message_unpinned' | 'call';
+
+/**
+ * Something that happened in the group.
+ */
+export type WhatsAppSystemLine = {
+ /**
+ * What happened.
+ */
+ action: WhatsAppSystemAction;
+ actor?: null | WhatsAppPersonRef;
+ call?: null | WhatsAppCall;
+ /**
+ * Who it happened to (added, removed, promoted, …).
+ */
+ participants: Array;
+ /**
+ * New subject or description; disappearing-messages timer in seconds
+ * (`"0"` is off).
+ */
+ value?: string | null;
+};
+
/**
* How a question is asked. Each column kind takes a few, the first its
* default; kinds asked one way only (numbers, checkboxes, entity and row
diff --git a/packages/sdk/specs/storage.json b/packages/sdk/specs/storage.json
index 063bea67be5..40ef0d2c478 100644
--- a/packages/sdk/specs/storage.json
+++ b/packages/sdk/specs/storage.json
@@ -33379,10 +33379,22 @@
"format": "uuid",
"description": "The private document, once ready."
},
+ "duration_seconds": {
+ "type": ["integer", "null"],
+ "format": "int32",
+ "description": "Length of audio or video in whole seconds, when declared.",
+ "minimum": 0
+ },
"file_name": {
"type": "string",
"description": "Display filename."
},
+ "height": {
+ "type": ["integer", "null"],
+ "format": "int32",
+ "description": "Pixel height, when declared.",
+ "minimum": 0
+ },
"mime": {
"type": "string",
"description": "Declared MIME type."
@@ -33392,6 +33404,17 @@
"format": "int32",
"description": "Position within the message."
},
+ "presentation": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/components/schemas/LinkedMediaPresentation",
+ "description": "Sticker, GIF or voice note; `null` for an ordinary file of its kind."
+ }
+ ]
+ },
"size": {
"type": "integer",
"format": "int64",
@@ -33401,6 +33424,12 @@
"state": {
"$ref": "#/components/schemas/LinkedAttachmentState",
"description": "Current state."
+ },
+ "width": {
+ "type": ["integer", "null"],
+ "format": "int32",
+ "description": "Pixel width of an image, sticker or video, when WhatsApp declared it.",
+ "minimum": 0
}
}
},
@@ -33739,6 +33768,11 @@
"description": "Attachment categories WhatsApp distinguishes.",
"enum": ["image", "audio", "video", "document"]
},
+ "LinkedMediaPresentation": {
+ "type": "string",
+ "description": "How WhatsApp shows an attachment, when not as a plain file of its kind.",
+ "enum": ["sticker", "gif", "voice_note"]
+ },
"LinkedMessageAttachments": {
"type": "object",
"description": "The attachments of one linked group message.",
@@ -33872,6 +33906,13 @@
"description": "Plain text.",
"required": ["text", "type"],
"properties": {
+ "mentions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Group participants the text @-mentions, by their WhatsApp id\n(`jid` of a roster participant); the text must contain\n`@` for each."
+ },
"text": {
"type": "string",
"description": "UTF-8 text, at most [`TEXT_LIMIT_BYTES`] bytes."
@@ -44540,6 +44581,101 @@
}
}
},
+ "WhatsAppCall": {
+ "type": "object",
+ "description": "A call in the group.",
+ "required": ["video", "outcome"],
+ "properties": {
+ "durationSeconds": {
+ "type": ["integer", "null"],
+ "format": "int32",
+ "description": "Length, when it connected.",
+ "minimum": 0
+ },
+ "outcome": {
+ "$ref": "#/components/schemas/WhatsAppCallOutcome",
+ "description": "How it went for the account that saw it."
+ },
+ "video": {
+ "type": "boolean",
+ "description": "Video rather than voice."
+ }
+ }
+ },
+ "WhatsAppCallOutcome": {
+ "type": "string",
+ "description": "How a call went.",
+ "enum": ["answered", "missed", "rejected", "other"]
+ },
+ "WhatsAppContactCard": {
+ "type": "object",
+ "description": "One contact card, from its vCard.",
+ "required": ["name", "phones"],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Display name."
+ },
+ "phones": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Phone numbers as written on the card."
+ }
+ }
+ },
+ "WhatsAppContacts": {
+ "type": "object",
+ "description": "Contact cards.",
+ "required": ["contacts"],
+ "properties": {
+ "contacts": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WhatsAppContactCard"
+ },
+ "description": "The cards, in order."
+ }
+ }
+ },
+ "WhatsAppEvent": {
+ "type": "object",
+ "description": "A WhatsApp event.",
+ "required": ["name"],
+ "properties": {
+ "canceled": {
+ "type": "boolean",
+ "description": "Canceled by its organizer."
+ },
+ "description": {
+ "type": ["string", "null"],
+ "description": "Description."
+ },
+ "endAt": {
+ "type": ["string", "null"],
+ "format": "date-time",
+ "description": "End."
+ },
+ "joinLink": {
+ "type": ["string", "null"],
+ "description": "Call link for an online event."
+ },
+ "location": {
+ "type": ["string", "null"],
+ "description": "Place name or address."
+ },
+ "name": {
+ "type": "string",
+ "description": "Title."
+ },
+ "startAt": {
+ "type": ["string", "null"],
+ "format": "date-time",
+ "description": "Start."
+ }
+ }
+ },
"WhatsAppGroupDetail": {
"allOf": [
{
@@ -44593,7 +44729,10 @@
"received_at",
"deleted",
"reactions",
- "attachments"
+ "attachments",
+ "kind",
+ "forwarded",
+ "mentions"
],
"properties": {
"attachments": {
@@ -44627,11 +44766,26 @@
"format": "date-time",
"description": "Last edit timestamp."
},
+ "forwarded": {
+ "type": "boolean",
+ "description": "Forwarded from another chat."
+ },
"id": {
"type": "string",
"format": "uuid",
"description": "Stable message id."
},
+ "kind": {
+ "$ref": "#/components/schemas/WhatsAppMessageKind",
+ "description": "A message, or a system line."
+ },
+ "mentions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WhatsAppMention"
+ },
+ "description": "People the body @-mentions."
+ },
"reactions": {
"type": "array",
"items": {
@@ -44649,6 +44803,17 @@
"format": "uuid",
"description": "Message quoted by this one."
},
+ "rich": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/components/schemas/WhatsAppRichContent",
+ "description": "Poll, location, contact cards, event or system line."
+ }
+ ]
+ },
"sender": {
"$ref": "#/components/schemas/WhatsAppGroupSender",
"description": "Sender."
@@ -44775,6 +44940,302 @@
}
}
},
+ "WhatsAppLocation": {
+ "type": "object",
+ "description": "A shared place.",
+ "required": ["latitude", "longitude"],
+ "properties": {
+ "address": {
+ "type": ["string", "null"],
+ "description": "Street address."
+ },
+ "comment": {
+ "type": ["string", "null"],
+ "description": "Caption or comment."
+ },
+ "latitude": {
+ "type": "number",
+ "format": "double",
+ "description": "Degrees."
+ },
+ "live": {
+ "type": "boolean",
+ "description": "Shared as a live location (this is where it started)."
+ },
+ "longitude": {
+ "type": "number",
+ "format": "double",
+ "description": "Degrees."
+ },
+ "name": {
+ "type": ["string", "null"],
+ "description": "Place name."
+ },
+ "url": {
+ "type": ["string", "null"],
+ "description": "Link the sender attached."
+ }
+ }
+ },
+ "WhatsAppMention": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/WhatsAppPersonRef",
+ "description": "Who it is."
+ },
+ {
+ "type": "object",
+ "required": ["token"],
+ "properties": {
+ "token": {
+ "type": "string",
+ "description": "What follows the `@` in the body (the digits of the person's WhatsApp\nid); replace `@` with the name."
+ }
+ }
+ }
+ ],
+ "description": "An @-mention in a message body."
+ },
+ "WhatsAppMessageKind": {
+ "type": "string",
+ "description": "What a timeline entry is.",
+ "enum": ["message", "system"]
+ },
+ "WhatsAppPersonRef": {
+ "type": "object",
+ "description": "A person named in a message: a Macro user when verified, else their\nWhatsApp name, else their phone number.",
+ "required": ["name"],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Display name, never authentication evidence."
+ },
+ "userId": {
+ "type": ["string", "null"],
+ "description": "Verified Macro user, if any."
+ }
+ }
+ },
+ "WhatsAppPoll": {
+ "type": "object",
+ "description": "A poll, read-only in Macro.",
+ "required": ["question", "options", "selectableCount", "voters"],
+ "properties": {
+ "options": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WhatsAppPollOption"
+ },
+ "description": "Options in order, with tallies."
+ },
+ "question": {
+ "type": "string",
+ "description": "The question."
+ },
+ "selectableCount": {
+ "type": "integer",
+ "format": "int32",
+ "description": "How many options one person may choose; 0 means any number.",
+ "minimum": 0
+ },
+ "voters": {
+ "type": "integer",
+ "format": "int64",
+ "description": "People who voted for anything."
+ }
+ }
+ },
+ "WhatsAppPollOption": {
+ "type": "object",
+ "description": "One option of a poll with its votes.",
+ "required": ["name", "votes", "mine", "voters"],
+ "properties": {
+ "mine": {
+ "type": "boolean",
+ "description": "The caller chose it."
+ },
+ "name": {
+ "type": "string",
+ "description": "Option text."
+ },
+ "voters": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WhatsAppPersonRef"
+ },
+ "description": "Who chose it."
+ },
+ "votes": {
+ "type": "integer",
+ "format": "int64",
+ "description": "People who chose it."
+ }
+ }
+ },
+ "WhatsAppRichContent": {
+ "oneOf": [
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/WhatsAppPoll",
+ "description": "A poll with current votes."
+ },
+ {
+ "type": "object",
+ "required": ["type"],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["poll"]
+ }
+ }
+ }
+ ],
+ "description": "A poll with current votes."
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/WhatsAppLocation",
+ "description": "A place (or the start of a live location); link to a maps URL."
+ },
+ {
+ "type": "object",
+ "required": ["type"],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["location"]
+ }
+ }
+ }
+ ],
+ "description": "A place (or the start of a live location); link to a maps URL."
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/WhatsAppContacts",
+ "description": "Contact cards."
+ },
+ {
+ "type": "object",
+ "required": ["type"],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["contacts"]
+ }
+ }
+ }
+ ],
+ "description": "Contact cards."
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/WhatsAppEvent",
+ "description": "A WhatsApp event."
+ },
+ {
+ "type": "object",
+ "required": ["type"],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["event"]
+ }
+ }
+ }
+ ],
+ "description": "A WhatsApp event."
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/WhatsAppSystemLine",
+ "description": "A system line."
+ },
+ {
+ "type": "object",
+ "required": ["type"],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["system"]
+ }
+ }
+ }
+ ],
+ "description": "A system line."
+ }
+ ],
+ "description": "Structured content of a message besides its body."
+ },
+ "WhatsAppSystemAction": {
+ "type": "string",
+ "description": "Kinds of system lines.",
+ "enum": [
+ "group_created",
+ "participants_added",
+ "participants_removed",
+ "participants_left",
+ "participants_joined",
+ "participants_promoted",
+ "participants_demoted",
+ "subject_changed",
+ "icon_changed",
+ "description_changed",
+ "disappearing_changed",
+ "message_pinned",
+ "message_unpinned",
+ "call"
+ ]
+ },
+ "WhatsAppSystemLine": {
+ "type": "object",
+ "description": "Something that happened in the group.",
+ "required": ["action", "participants"],
+ "properties": {
+ "action": {
+ "$ref": "#/components/schemas/WhatsAppSystemAction",
+ "description": "What happened."
+ },
+ "actor": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/components/schemas/WhatsAppPersonRef",
+ "description": "Who did it; absent when WhatsApp did not say."
+ }
+ ]
+ },
+ "call": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/components/schemas/WhatsAppCall",
+ "description": "For `call`: video or voice, and how it went."
+ }
+ ]
+ },
+ "participants": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WhatsAppPersonRef"
+ },
+ "description": "Who it happened to (added, removed, promoted, …)."
+ },
+ "value": {
+ "type": ["string", "null"],
+ "description": "New subject or description; disappearing-messages timer in seconds\n(`\"0\"` is off)."
+ }
+ }
+ },
"Widget": {
"type": "string",
"description": "How a question is asked. Each column kind takes a few, the first its\ndefault; kinds asked one way only (numbers, checkboxes, entity and row\npickers) take none.",
diff --git a/packages/sdk/src/entities/whatsapp/group.ts b/packages/sdk/src/entities/whatsapp/group.ts
index 42bf533e2c9..f912fe35ef2 100644
--- a/packages/sdk/src/entities/whatsapp/group.ts
+++ b/packages/sdk/src/entities/whatsapp/group.ts
@@ -15,7 +15,12 @@ import { WhatsAppUpload } from './upload';
/** Deliberate content from the credential owner's own WhatsApp account. */
export type WhatsAppReply =
- | { type: 'text'; text: string }
+ | {
+ type: 'text';
+ text: string;
+ /** WhatsApp ids of mentioned participants (`roster.participants[].jid`); the text names each as `@`. */
+ mentions?: string[];
+ }
| { type: 'media'; upload: WhatsAppUpload; caption?: string }
| {
type: 'mutation';
@@ -142,7 +147,11 @@ export class WhatsAppGroup extends FavoritableEntity {
if (options.replyTo) this.assertGroup(options.replyTo.groupId);
const message = match(content)
.returnType()
- .with({ type: 'text' }, (value) => value)
+ .with({ type: 'text' }, (value) => ({
+ type: 'text',
+ text: value.text,
+ mentions: value.mentions ?? [],
+ }))
.with({ type: 'media' }, (value) => {
this.assertGroup(value.upload.groupId);
return {
diff --git a/packages/sdk/src/entities/whatsapp/message.ts b/packages/sdk/src/entities/whatsapp/message.ts
index cfb6fbeacf0..975adb30b1e 100644
--- a/packages/sdk/src/entities/whatsapp/message.ts
+++ b/packages/sdk/src/entities/whatsapp/message.ts
@@ -60,6 +60,14 @@ export class WhatsAppMessage extends MacroEntity {
readonly reactions = this.field('reactions');
/** Caller-owned send's delivery state, when present. */
readonly deliveryState = this.field('delivery_state');
+ /** `message`, or `system` for something that happened in the group (see `rich`). */
+ readonly kind = this.field('kind');
+ /** Forwarded from another chat. */
+ readonly forwarded = this.field('forwarded');
+ /** People the body @-mentions: `@` in the body is this person. */
+ readonly mentions = this.field('mentions');
+ /** Poll (with tallies), location, contact cards, event or system line. */
+ readonly rich = this.field('rich');
/** Quoted message in this group. */
readonly replyTo: () => Promise =
this.mappedField('reply_to', (id) =>
@@ -73,6 +81,10 @@ export class WhatsAppMessage extends MacroEntity {
fileName: record.file_name,
mime: record.mime,
size: record.size,
+ width: record.width ?? undefined,
+ height: record.height ?? undefined,
+ durationSeconds: record.duration_seconds ?? undefined,
+ presentation: record.presentation ?? undefined,
document: record.document_id
? Document.byId(this.client, record.document_id)
: undefined,
diff --git a/packages/sdk/src/entities/whatsapp/send.ts b/packages/sdk/src/entities/whatsapp/send.ts
index 28f636725ff..989ae584193 100644
--- a/packages/sdk/src/entities/whatsapp/send.ts
+++ b/packages/sdk/src/entities/whatsapp/send.ts
@@ -84,7 +84,11 @@ export class WhatsAppSend extends MacroEntity {
operation: wire.operation,
text: wire.text ?? undefined,
}))
- .with({ type: 'text' }, (wire) => wire)
+ .with({ type: 'text' }, (wire) => ({
+ type: 'text',
+ text: wire.text,
+ mentions: wire.mentions ?? [],
+ }))
.exhaustive();
return {
idempotencyKey: payload.idempotency_key,
diff --git a/packages/sdk/tests/whatsapp.test.ts b/packages/sdk/tests/whatsapp.test.ts
index 66d5c22876d..c2e0400a59c 100644
--- a/packages/sdk/tests/whatsapp.test.ts
+++ b/packages/sdk/tests/whatsapp.test.ts
@@ -234,7 +234,7 @@ test('owner-token send preserves intent and unknown delivery is read without res
expect(await sent.state()).toBe('unknown');
expect(await requests[0].json()).toEqual({
idempotency_key: 'stable',
- message: { type: 'text', text: 'hello' },
+ message: { type: 'text', text: 'hello', mentions: [] },
});
await sent.refresh();
expect(await sent.state()).toBe('unknown');
@@ -288,3 +288,23 @@ test('mutations and replies accept only standalone WhatsApp messages', async ()
},
});
});
+
+test('a text reply names the participants it mentions by their WhatsApp ids', async () => {
+ const { group, requests } = fixture(() => Response.json(progress));
+ await group.reply(
+ {
+ type: 'text',
+ text: 'hi @15550001111',
+ mentions: ['15550001111@s.whatsapp.net'],
+ },
+ { idempotencyKey: 'mention' },
+ );
+ expect(await requests[0].json()).toEqual({
+ idempotency_key: 'mention',
+ message: {
+ type: 'text',
+ text: 'hi @15550001111',
+ mentions: ['15550001111@s.whatsapp.net'],
+ },
+ });
+});
diff --git a/services/document_storage_service/src/whatsapp_linked.rs b/services/document_storage_service/src/whatsapp_linked.rs
index ad101f6a289..ce9eb54ecad 100644
--- a/services/document_storage_service/src/whatsapp_linked.rs
+++ b/services/document_storage_service/src/whatsapp_linked.rs
@@ -206,10 +206,10 @@ async fn drain(effects: &EffectsService, media: &MediaService) {
}
}
}
- if let Err(error) = media.expire_uploads().await {
+ if let Err(error) = media.expire_retained().await {
tracing::warn!(
code = error.current_context().code(),
- "Expiring WhatsApp uploads will retry"
+ "Expiring WhatsApp uploads and receipts will retry"
);
}
loop {
diff --git a/services/whatsapp_session_service/AGENTS.md b/services/whatsapp_session_service/AGENTS.md
index 181a07b14ae..40771f69c7c 100644
--- a/services/whatsapp_session_service/AGENTS.md
+++ b/services/whatsapp_session_service/AGENTS.md
@@ -57,6 +57,15 @@ current, ingests standalone `whatsapp_group` traffic through `whatsapp_linked`'s
a miss always waits for a load that started after the event arrived
(single-flight), so an adoption that committed just before is seen. Media locators (direct path, media key, hashes) are sealed with
`PgWhatsAppStore::seal("media-locator", …)` before they leave the process.
+- Message types (`events.rs` + `events/rich.rs`): text, media (with declared
+ size/length; stickers, GIFs and PTT voice notes marked), quotes on any
+ content part, mentions, forwarding, polls, locations, contact cards (vCard
+ name and numbers), events; pins and call logs become system lines, as do
+ live group notifications (`map_event` returns the roster refresh plus one
+ system line per change), group icon changes and history stub messages.
+ Poll votes are decrypted in `Forwarder::poll_vote` with the poll's stored
+ message secret (undecryptable ones are acked and dropped). View-once media
+ is a placeholder.
- Chat list and history: `outbound/whatsapp.rs` `Forwarder` turns history
sync bundles, live messages (any chat) and contact/archive/pin events into
`ChatUpdate`s (`events.rs`: `history_chats`, `message_chat`,
@@ -96,7 +105,9 @@ Spans: `whatsapp.account.{lease,release,rescan,connect}`,
`whatsapp.inbound.ingest`, `whatsapp.media.import`, `whatsapp.send.dispatch`
(parented on the trace of the request that recorded the send),
`whatsapp.groups.{refresh,event}`, `whatsapp.signal.receive`, `whatsapp.drain`,
-`whatsapp.history.{sync,page}`, `whatsapp.backfill.request`. Log events read by `infra/stacks/monitoring/monitors/
+`whatsapp.history.{sync,page}`, `whatsapp.backfill.request`
+(`whatsapp.inbound.ingest` records the message `kind`; `whatsapp.history.page`
+counts what a page stored, could not parse and skipped, by stub type). Log events read by `infra/stacks/monitoring/monitors/
whatsapp-session.ts` (`telemetry_event` = `whatsapp_session_health`,
`whatsapp_session_reconnect`, `whatsapp_send_outcome`,
`whatsapp_session_drain`): keep their field names. Opaque ids only; never
@@ -132,7 +143,7 @@ JIDs, phone numbers, QR codes or message text.
never wait on the runner (session events go through an unbounded channel).
- Every path that acknowledges an event without storing it logs a debug
event `whatsapp.inbound.skipped` with a `reason` (`not_group_content`,
- `group_not_adopted`, `invalid_event`). Once stored, the domain may still
+ `group_not_adopted`, `invalid_event`, `undecryptable_poll_vote`). Once stored, the domain may still
drop an event as not observed (consent boundary).
- whatsmeow's media, send and history futures are large: the adapter and the
runner box them, or nesting them overflows a 2 MiB stack in debug builds
diff --git a/services/whatsapp_session_service/Cargo.toml b/services/whatsapp_session_service/Cargo.toml
index 9a37c014f81..f610f4f3731 100644
--- a/services/whatsapp_session_service/Cargo.toml
+++ b/services/whatsapp_session_service/Cargo.toml
@@ -27,6 +27,7 @@ macro_env_var = { path = "../../crates/macro_env_var" }
macro_entrypoint = { path = "../../crates/macro_entrypoint" }
macro_env = { path = "../../crates/macro_env" }
macro_service_urls = { path = "../../crates/macro_service_urls" }
+nom = { workspace = true }
rootcause = { workspace = true }
rustls = { workspace = true }
serde = { workspace = true }
diff --git a/services/whatsapp_session_service/src/domain/account/test.rs b/services/whatsapp_session_service/src/domain/account/test.rs
index f4b8ff4b1ac..c3698dc86f2 100644
--- a/services/whatsapp_session_service/src/domain/account/test.rs
+++ b/services/whatsapp_session_service/src/domain/account/test.rs
@@ -541,6 +541,9 @@ fn group_text(event: u128) -> InboundEvent {
revision: 0,
target: None,
media: vec![],
+ mentions: vec![],
+ forwarded: false,
+ rich: None,
}),
}
}
diff --git a/services/whatsapp_session_service/src/domain/inbound.rs b/services/whatsapp_session_service/src/domain/inbound.rs
index 3ee0678f3cc..da9cf228299 100644
--- a/services/whatsapp_session_service/src/domain/inbound.rs
+++ b/services/whatsapp_session_service/src/domain/inbound.rs
@@ -9,7 +9,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
use tokio::sync::{Mutex, Notify};
use tokio_util::sync::CancellationToken;
use tracing::Instrument;
-use whatsapp_linked::domain::events::{InboundEvent, SessionScope};
+use whatsapp_linked::domain::events::{InboundEvent, InboundKind, SessionScope};
use whatsapp_linked::domain::models::LinkedError;
use whatsapp_linked::domain::ports::LinkedResult;
use whatsapp_linked::domain::session::SessionService;
@@ -153,6 +153,11 @@ impl InboundSink for InboundGate {
"whatsapp.inbound.ingest",
account_id = %self.scope.account_id,
event_id = %event.event_id,
+ kind = match &event.kind {
+ InboundKind::Message(message) => message.kind.as_str(),
+ InboundKind::Receipt(_) => "receipt",
+ InboundKind::Left => "left",
+ },
observed,
outcome = tracing::field::Empty,
);
diff --git a/services/whatsapp_session_service/src/domain/inbound/test.rs b/services/whatsapp_session_service/src/domain/inbound/test.rs
index cb0ca31e37a..5060eeedc7c 100644
--- a/services/whatsapp_session_service/src/domain/inbound/test.rs
+++ b/services/whatsapp_session_service/src/domain/inbound/test.rs
@@ -61,6 +61,9 @@ fn text(event: u128) -> InboundEvent {
revision: 0,
target: None,
media: vec![],
+ mentions: vec![],
+ forwarded: false,
+ rich: None,
}),
}
}
diff --git a/services/whatsapp_session_service/src/outbound/whatsapp.rs b/services/whatsapp_session_service/src/outbound/whatsapp.rs
index 01a499bcc85..e4ed745b4de 100644
--- a/services/whatsapp_session_service/src/outbound/whatsapp.rs
+++ b/services/whatsapp_session_service/src/outbound/whatsapp.rs
@@ -264,6 +264,9 @@ impl Forwarder {
messages = page.messages,
complete = page.complete,
stored = tracing::field::Empty,
+ unreadable = tracing::field::Empty,
+ skipped = tracing::field::Empty,
+ skipped_stub_types = tracing::field::Empty,
outcome = tracing::field::Empty,
);
async {
@@ -271,34 +274,61 @@ impl Forwarder {
return;
};
let mut stored = 0usize;
+ let mut unreadable: Vec = vec![];
+ // Messages Macro does not import, by WhatsApp stub type (0: not
+ // a stub), so a page that stores less than it got is explained.
+ let mut skipped: Vec = vec![];
let mut kept = true;
for message in &conversation.messages {
let Some(web) = message.message.clone() else {
continue;
};
+ let stub_type = web.message_stub_type.unwrap_or(0);
let parsed = match Box::pin(client.parse_web_message(&chat, web)).await {
Ok(parsed) => parsed,
Err(error) => {
- tracing::debug!(error = %error, "Skipping an unreadable history message");
+ unreadable.push(stub_type);
+ tracing::debug!(error = %error, stub_type, "Skipping an unreadable history message");
continue;
}
};
- let Some(mut event) = events::map_history_message(
+ let now = Utc::now();
+ let Some(event) = events::map_history_message(
self.account,
&parsed,
- Utc::now(),
+ now,
&|locator: &MediaLocator| self.seal(locator),
) else {
+ skipped.push(stub_type);
continue;
};
- events::complete_sender(self.store.as_ref(), &mut event).await;
- if Box::pin(self.inbound.accept(&event)).await {
+ // The phone sends a poll's votes with it, already decrypted;
+ // they apply after the poll.
+ let votes = events::history_poll_votes(self.account, &parsed, now);
+ let mut message_kept = true;
+ for mut event in std::iter::once(event).chain(votes) {
+ events::complete_sender(self.store.as_ref(), &mut event).await;
+ message_kept &= Box::pin(self.inbound.accept(&event)).await;
+ }
+ if message_kept {
stored += 1;
} else {
kept = false;
}
}
- tracing::Span::current().record("stored", stored);
+ let span = tracing::Span::current();
+ span.record("stored", stored);
+ span.record("unreadable", unreadable.len());
+ span.record("skipped", skipped.len());
+ span.record("skipped_stub_types", format!("{skipped:?}"));
+ tracing::info!(
+ stored,
+ unreadable = unreadable.len(),
+ unreadable_stub_types = ?unreadable,
+ skipped = skipped.len(),
+ skipped_stub_types = ?skipped,
+ "WhatsApp history page received"
+ );
if !kept {
tracing::Span::current().record("outcome", "not_stored");
tracing::warn!("A history page could not be stored; it will be requested again");
@@ -312,6 +342,35 @@ impl Forwarder {
}
}
+impl Forwarder {
+ /// A poll vote, decrypted with the poll's message secret. `None` when the
+ /// message is not a vote; a vote that cannot be decrypted (the poll was
+ /// sent before this device saw it) is acknowledged and dropped.
+ async fn poll_vote(&self, message: &whatsmeow::types::events::Message) -> Option {
+ message.message.as_ref()?.poll_update_message.as_ref()?;
+ let client = self.client.upgrade()?;
+ let selected = match client.decrypt_poll_vote(message).await {
+ Ok(vote) => vote.selected_options,
+ Err(error) => {
+ tracing::debug!(
+ name: "whatsapp.inbound.skipped",
+ account_id = %self.account,
+ reason = "undecryptable_poll_vote",
+ error = %error,
+ "Acknowledged a WhatsApp poll vote without storing it"
+ );
+ return Some(true);
+ }
+ };
+ let Some(mut event) = events::map_poll_vote(self.account, message, &selected, Utc::now())
+ else {
+ return Some(true);
+ };
+ events::complete_sender(self.store.as_ref(), &mut event).await;
+ Some(self.inbound.accept(&event).await)
+ }
+}
+
// Object safe (`Arc`), hence `async_trait`.
#[async_trait]
impl EventHandler for Forwarder {
@@ -330,28 +389,34 @@ impl EventHandler for Forwarder {
Box::pin(self.chats(events::chat_setting(event).into_iter().collect())).await;
}
}
+ if let Event::Message(message) = event
+ && let Some(vote) = Box::pin(self.poll_vote(message)).await
+ {
+ return vote;
+ }
let seal = |locator: &MediaLocator| self.seal(locator);
- match events::map_event(self.account, event, Utc::now(), &seal) {
- Some(Mapped::Inbound(mut event)) => {
- events::complete_sender(self.store.as_ref(), &mut event).await;
- self.inbound.accept(&event).await
- }
- Some(Mapped::Session(event)) => {
- let _ = self.events.send(event);
- true
- }
- None => {
- if matches!(event, Event::Message(_)) {
- tracing::debug!(
- name: "whatsapp.inbound.skipped",
- account_id = %self.account,
- reason = "not_group_content",
- "Acknowledged a WhatsApp message without storing it"
- );
+ let mapped = events::map_event(self.account, event, Utc::now(), &seal);
+ if mapped.is_empty() && matches!(event, Event::Message(_)) {
+ tracing::debug!(
+ name: "whatsapp.inbound.skipped",
+ account_id = %self.account,
+ reason = "not_group_content",
+ "Acknowledged a WhatsApp message without storing it"
+ );
+ }
+ let mut stored = true;
+ for mapped in mapped {
+ match mapped {
+ Mapped::Inbound(mut event) => {
+ events::complete_sender(self.store.as_ref(), &mut event).await;
+ stored &= self.inbound.accept(&event).await;
+ }
+ Mapped::Session(event) => {
+ let _ = self.events.send(event);
}
- true
}
}
+ stored
}
}
@@ -530,21 +595,30 @@ impl WhatsmeowSession {
media: Option<&UploadedMedia>,
) -> Result {
Ok(match content {
- OutgoingContent::Text { text, quote: None } => wa_e2e::Message {
+ OutgoingContent::Text {
+ text,
+ quote: None,
+ mentions,
+ } if mentions.is_empty() => wa_e2e::Message {
conversation: Some(text.clone()),
..Default::default()
},
OutgoingContent::Text {
text,
- quote: Some(quote),
- } => wa_e2e::Message {
- extended_text_message: Some(Box::new(wa_e2e::ExtendedTextMessage {
- text: Some(text.clone()),
- context_info: quote_context(Some(quote)),
+ quote,
+ mentions,
+ } => {
+ let mut context_info = quote_context(quote.as_ref()).unwrap_or_default();
+ context_info.mentioned_jid = mentions.clone();
+ wa_e2e::Message {
+ extended_text_message: Some(Box::new(wa_e2e::ExtendedTextMessage {
+ text: Some(text.clone()),
+ context_info: Some(context_info),
+ ..Default::default()
+ })),
..Default::default()
- })),
- ..Default::default()
- },
+ }
+ }
OutgoingContent::Media {
kind,
mime,
diff --git a/services/whatsapp_session_service/src/outbound/whatsapp/events.rs b/services/whatsapp_session_service/src/outbound/whatsapp/events.rs
index e33f319c3f1..d52f4432e2b 100644
--- a/services/whatsapp_session_service/src/outbound/whatsapp/events.rs
+++ b/services/whatsapp_session_service/src/outbound/whatsapp/events.rs
@@ -7,9 +7,10 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid;
use whatsapp_linked::domain::events::{
ChatUpdate, HistoryPage, InboundEvent, InboundKind, Media, Message, MessageAnchor, MessageKind,
- Receipt, Target,
+ Receipt, ReceiptState, RichContent, Target,
};
-use whatsapp_linked::domain::models::{ChatKind, Identity};
+use whatsapp_linked::domain::models::{ChatKind, Identity, MediaPresentation};
+use whatsapp_linked::domain::rich::SystemLine;
use whatsmeow::Event;
use whatsmeow::download::{DownloadableMessage, MediaType};
use whatsmeow::proto::wa_common::MessageKey;
@@ -19,10 +20,12 @@ use whatsmeow::proto::wa_history_sync::conversation::EndOfHistoryTransferType;
use whatsmeow::proto::wa_history_sync::{Conversation, HistorySync, Pushname};
use whatsmeow::proto::wa_web::WebMessageInfo;
use whatsmeow::store::{ContactStore, LidStore};
+use whatsmeow::types::events::GroupInfo;
use whatsmeow::types::{DEFAULT_USER_SERVER, GROUP_SERVER, HIDDEN_USER_SERVER, Jid};
use crate::domain::ports::SessionEvent;
+mod rich;
#[cfg(test)]
mod test;
@@ -32,6 +35,9 @@ const EVENT_NAMESPACE: Uuid = Uuid::from_u128(0x6c1e_4f0a_93d2_4b7e_8a55_0f3c_2d
/// Readable stand-in for message shapes Macro does not import.
const UNSUPPORTED_TEXT: &str = "Sent something Macro can't show yet";
+/// Stand-in for view-once media, which WhatsApp shows only on the phone.
+const VIEW_ONCE_TEXT: &str = "View-once message \u{2014} open it on your phone";
+
/// What a whatsmeow event means for the account.
#[derive(Debug)]
pub enum Mapped {
@@ -83,37 +89,58 @@ impl LocatorMediaType {
}
}
-/// The meaning of a whatsmeow event, if the account cares about it. `seal`
-/// turns a [`MediaLocator`] into the opaque bytes stored with the attachment;
-/// `None` drops the attachment to a placeholder.
+/// What a whatsmeow event means for the account, if anything. `seal` turns
+/// a [`MediaLocator`] into the opaque bytes stored with the attachment;
+/// `None` drops the attachment to a placeholder. Poll votes are mapped by
+/// [`map_poll_vote`] once decrypted.
pub fn map_event(
account: Uuid,
event: &Event,
now: DateTime,
seal: &dyn Fn(&MediaLocator) -> Option>,
-) -> Option {
- Some(match event {
+) -> Vec {
+ let inbound = |event: InboundEvent| Mapped::Inbound(Box::new(event));
+ match event {
// A message the phone sent again on request is an earlier message.
- Event::Message(message) => Mapped::Inbound(Box::new(map_message(
+ Event::Message(message) => map_message(
account,
message,
now,
seal,
!message.unavailable_request_id.is_empty(),
- )?)),
- Event::Receipt(receipt) => Mapped::Inbound(Box::new(map_receipt(account, receipt, now)?)),
- Event::JoinedGroup(joined) => Mapped::Session(SessionEvent::GroupChanged {
+ )
+ .map(inbound)
+ .into_iter()
+ .collect(),
+ Event::Receipt(receipt) => map_receipt(account, receipt, now)
+ .map(inbound)
+ .into_iter()
+ .collect(),
+ Event::JoinedGroup(joined) => vec![Mapped::Session(SessionEvent::GroupChanged {
group_jid: joined.group_info.jid.to_non_ad().to_string(),
info: Some(Box::new(super::group_info(joined.group_info.clone(), now))),
- }),
+ })],
Event::GroupInfo(change) if change.jid.server == GROUP_SERVER => {
- Mapped::Session(SessionEvent::GroupChanged {
+ let mut mapped = vec![Mapped::Session(SessionEvent::GroupChanged {
group_jid: change.jid.to_non_ad().to_string(),
info: None,
- })
+ })];
+ mapped.extend(
+ map_group_info(account, change, now)
+ .into_iter()
+ .map(inbound),
+ );
+ mapped
}
- event => Mapped::Session(map_session(event)?),
- })
+ Event::Picture(picture) => map_group_picture(account, picture, now)
+ .map(inbound)
+ .into_iter()
+ .collect(),
+ event => map_session(event)
+ .map(Mapped::Session)
+ .into_iter()
+ .collect(),
+ }
}
fn map_session(event: &Event) -> Option {
@@ -212,15 +239,55 @@ fn carries_only_keys(message: &wa_e2e::Message) -> bool {
content == wa_e2e::Message::default()
}
-fn text_of(message: &wa_e2e::Message) -> Option<(String, Option<&wa_e2e::ContextInfo>)> {
+fn text_of(message: &wa_e2e::Message) -> Option {
if let Some(text) = &message.conversation {
- return Some((text.clone(), None));
+ return Some(text.clone());
}
let extended = message.extended_text_message.as_ref()?;
- Some((
- extended.text.clone().unwrap_or_default(),
- extended.context_info.as_deref(),
- ))
+ Some(extended.text.clone().unwrap_or_default())
+}
+
+/// The context of a message's content part: what it quotes, whom it
+/// mentions, whether it was forwarded. Every content type carries its own.
+fn context_of(message: &wa_e2e::Message) -> Option<&wa_e2e::ContextInfo> {
+ [
+ message
+ .extended_text_message
+ .as_ref()
+ .and_then(|part| part.context_info.as_deref()),
+ message
+ .image_message
+ .as_ref()
+ .and_then(|part| part.context_info.as_deref()),
+ message
+ .video_message
+ .as_ref()
+ .and_then(|part| part.context_info.as_deref()),
+ message
+ .audio_message
+ .as_ref()
+ .and_then(|part| part.context_info.as_deref()),
+ message
+ .document_message
+ .as_ref()
+ .and_then(|part| part.context_info.as_deref()),
+ message
+ .sticker_message
+ .as_ref()
+ .and_then(|part| part.context_info.as_deref()),
+ ]
+ .into_iter()
+ .flatten()
+ .next()
+}
+
+/// The message a reply quotes.
+fn quote_of(context: Option<&wa_e2e::ContextInfo>) -> Option {
+ let context = context?;
+ Some(Target {
+ source_id: context.stanza_id.clone()?,
+ sender_jid: person_text(context.participant.as_deref()?)?,
+ })
}
/// One attachment part of a message, with its caption.
@@ -231,6 +298,10 @@ struct Attachment<'part> {
filename: Option<&'part str>,
size: Option,
caption: Option<&'part str>,
+ width: Option,
+ height: Option,
+ seconds: Option,
+ presentation: Option,
}
fn attachment_of(message: &wa_e2e::Message) -> Option> {
@@ -242,6 +313,24 @@ fn attachment_of(message: &wa_e2e::Message) -> Option> {
filename: None,
size: image.file_length,
caption: image.caption.as_deref(),
+ width: image.width,
+ height: image.height,
+ seconds: None,
+ presentation: None,
+ });
+ }
+ if let Some(sticker) = &message.sticker_message {
+ return Some(Attachment {
+ part: sticker.as_ref(),
+ media_type: LocatorMediaType::Image,
+ mime: sticker.mimetype.as_deref().or(Some("image/webp")),
+ filename: None,
+ size: sticker.file_length,
+ caption: None,
+ width: sticker.width,
+ height: sticker.height,
+ seconds: None,
+ presentation: Some(MediaPresentation::Sticker),
});
}
if let Some(video) = &message.video_message {
@@ -252,6 +341,13 @@ fn attachment_of(message: &wa_e2e::Message) -> Option> {
filename: None,
size: video.file_length,
caption: video.caption.as_deref(),
+ width: video.width,
+ height: video.height,
+ seconds: video.seconds,
+ presentation: video
+ .gif_playback
+ .unwrap_or(false)
+ .then_some(MediaPresentation::Gif),
});
}
if let Some(audio) = &message.audio_message {
@@ -262,6 +358,13 @@ fn attachment_of(message: &wa_e2e::Message) -> Option> {
filename: None,
size: audio.file_length,
caption: None,
+ width: None,
+ height: None,
+ seconds: audio.seconds,
+ presentation: audio
+ .ptt
+ .unwrap_or(false)
+ .then_some(MediaPresentation::VoiceNote),
});
}
let document = message.document_message.as_ref()?;
@@ -272,6 +375,10 @@ fn attachment_of(message: &wa_e2e::Message) -> Option> {
filename: document.file_name.as_deref(),
size: document.file_length,
caption: document.caption.as_deref(),
+ width: None,
+ height: None,
+ seconds: None,
+ presentation: None,
})
}
@@ -296,6 +403,7 @@ fn media_of(
_ => {
let extension = mime.rsplit('/').next().unwrap_or("bin");
let stem = match attachment.media_type {
+ LocatorMediaType::Image if attachment.presentation.is_some() => "sticker",
LocatorMediaType::Image => "image",
LocatorMediaType::Video => "video",
LocatorMediaType::Audio => "audio",
@@ -317,6 +425,10 @@ fn media_of(
size: attachment.size?,
sha256,
locator,
+ width: attachment.width.filter(|width| *width > 0),
+ height: attachment.height.filter(|height| *height > 0),
+ duration_seconds: attachment.seconds,
+ presentation: attachment.presentation,
})
}
@@ -343,117 +455,417 @@ fn map_message(
return None;
}
let author = person(&info.sender)?;
- let sender = Identity {
- jid: author.clone(),
- alternate_jid: person(&info.sender_alt),
- display_name: (!info.push_name.is_empty()).then(|| info.push_name.clone()),
- };
let source_at = info.timestamp.unwrap_or(now);
- let message = event.message.as_ref()?;
- if carries_only_keys(message) {
- return None;
+ let content = match event.message.as_ref() {
+ Some(message) => {
+ if carries_only_keys(message) || message.poll_update_message.is_some() {
+ return None;
+ }
+ message_content(message, event.is_view_once, &author, source_at, seal)?
+ }
+ // A history page keeps what happened in the group as stub messages.
+ None => {
+ let line = event.source_web_msg.as_ref().and_then(rich::stub_line)?;
+ let target = event
+ .source_web_msg
+ .as_ref()
+ .and_then(|web| web.pin_in_chat.as_ref()?.key.as_ref())
+ .and_then(|key| target(key, &author));
+ Content {
+ kind: MessageKind::System,
+ text: String::new(),
+ target,
+ revision: 0,
+ media: vec![],
+ rich: Some(RichContent::System(line)),
+ context: None,
+ }
+ }
+ };
+ Some(inbound_message(
+ account, event, &author, now, history, content,
+ ))
+}
+
+/// What one message says, before it is addressed to its group.
+struct Content<'message> {
+ kind: MessageKind,
+ text: String,
+ target: Option,
+ revision: i64,
+ media: Vec,
+ rich: Option,
+ context: Option<&'message wa_e2e::ContextInfo>,
+}
+
+impl Content<'_> {
+ fn new(kind: MessageKind, text: String, target: Option, revision: i64) -> Self {
+ Self {
+ kind,
+ text,
+ target,
+ revision,
+ media: vec![],
+ rich: None,
+ context: None,
+ }
+ }
+
+ fn unsupported(text: &str) -> Self {
+ Self::new(MessageKind::Unsupported, text.to_owned(), None, 0)
}
- let mut media = Vec::new();
- let (kind, text, target, revision) = if let Some(protocol) = &message.protocol_message {
+}
+
+fn message_content<'message>(
+ message: &'message wa_e2e::Message,
+ view_once: bool,
+ author: &str,
+ source_at: DateTime,
+ seal: &dyn Fn(&MediaLocator) -> Option>,
+) -> Option> {
+ if let Some(protocol) = &message.protocol_message {
let key = protocol.key.as_ref()?;
let edit_target = Target {
source_id: key.id.clone()?,
- sender_jid: author.clone(),
+ sender_jid: author.to_owned(),
};
- match protocol
+ return match protocol
.r#type
.and_then(|value| protocol_message::Type::try_from(value).ok())
{
- Some(protocol_message::Type::Revoke) => {
- (MessageKind::Delete, String::new(), Some(edit_target), 0)
- }
+ Some(protocol_message::Type::Revoke) => Some(Content::new(
+ MessageKind::Delete,
+ String::new(),
+ Some(edit_target),
+ 0,
+ )),
Some(protocol_message::Type::MessageEdit) => {
- let (text, _) = protocol.edited_message.as_deref().and_then(text_of)?;
+ let text = protocol.edited_message.as_deref().and_then(text_of)?;
let revision = protocol
.timestamp_ms
.unwrap_or_else(|| source_at.timestamp_millis());
- (MessageKind::Edit, text, Some(edit_target), revision)
+ Some(Content::new(
+ MessageKind::Edit,
+ text,
+ Some(edit_target),
+ revision,
+ ))
}
// Key shares, history notifications and the like are not group content.
- _ => return None,
- }
- } else if let Some(reaction) = &message.reaction_message {
- let target = target(reaction.key.as_ref()?, &author)?;
- let revision = reaction.sender_timestamp_ms.unwrap_or(0);
- (
+ _ => None,
+ };
+ }
+ if let Some(reaction) = &message.reaction_message {
+ let target = target(reaction.key.as_ref()?, author)?;
+ return Some(Content::new(
MessageKind::Reaction,
reaction.text.clone().unwrap_or_default(),
Some(target),
- revision,
- )
- } else if let Some(attachment) = attachment_of(message) {
- match media_of(&attachment, seal) {
- Some(part) => {
- media.push(part);
- (
+ reaction.sender_timestamp_ms.unwrap_or(0),
+ ));
+ }
+ // Shown once on the phone; Macro never keeps it.
+ if view_once {
+ return Some(Content::unsupported(VIEW_ONCE_TEXT));
+ }
+ if let Some(pin) = message.pin_in_chat_message.as_ref() {
+ let line = rich::pin_line(pin)?;
+ return Some(Content {
+ rich: Some(RichContent::System(line)),
+ ..Content::new(
+ MessageKind::System,
+ String::new(),
+ target(pin.key.as_ref()?, author),
+ 0,
+ )
+ });
+ }
+ if let Some(call) = message.call_log_messsage.as_ref() {
+ return Some(Content {
+ rich: Some(RichContent::System(rich::call_line(call))),
+ ..Content::new(MessageKind::System, String::new(), None, 0)
+ });
+ }
+ if let Some(attachment) = attachment_of(message) {
+ let context = context_of(message);
+ return Some(match media_of(&attachment, seal) {
+ Some(part) => Content {
+ media: vec![part],
+ context,
+ ..Content::new(
MessageKind::Text,
attachment.caption.unwrap_or_default().to_owned(),
- None,
+ quote_of(context),
0,
)
- }
- None => (
- MessageKind::Unsupported,
- UNSUPPORTED_TEXT.to_owned(),
- None,
- 0,
- ),
- }
- } else if let Some((text, context)) = text_of(message) {
- let quote = context.and_then(|context| {
- Some(Target {
- source_id: context.stanza_id.clone()?,
- sender_jid: person_text(context.participant.as_deref()?)?,
- })
+ },
+ None => Content::unsupported(UNSUPPORTED_TEXT),
});
- (MessageKind::Text, text, quote, 0)
- } else {
- (
- MessageKind::Unsupported,
- UNSUPPORTED_TEXT.to_owned(),
- None,
- 0,
- )
- };
- let kind_name = match kind {
- MessageKind::Text => "text",
- MessageKind::Reaction => "reaction",
- MessageKind::Edit => "edit",
- MessageKind::Delete => "delete",
- MessageKind::Unsupported => "unsupported",
+ }
+ if let Some(rich_content) = rich::rich_of(message) {
+ let context = rich::rich_context(message);
+ return Some(Content {
+ rich: Some(rich_content),
+ context,
+ ..Content::new(MessageKind::Rich, String::new(), quote_of(context), 0)
+ });
+ }
+ if let Some(text) = text_of(message) {
+ let context = context_of(message);
+ return Some(Content {
+ context,
+ ..Content::new(MessageKind::Text, text, quote_of(context), 0)
+ });
+ }
+ Some(Content::unsupported(UNSUPPORTED_TEXT))
+}
+
+fn inbound_message(
+ account: Uuid,
+ event: &whatsmeow::types::events::Message,
+ author: &str,
+ now: DateTime,
+ history: bool,
+ content: Content<'_>,
+) -> InboundEvent {
+ let info = &event.info;
+ let sender = Identity {
+ jid: author.to_owned(),
+ alternate_jid: person(&info.sender_alt),
+ display_name: (!info.push_name.is_empty()).then(|| info.push_name.clone()),
};
- let revision_text = revision.to_string();
- Some(InboundEvent {
+ let revision_text = content.revision.to_string();
+ let mentions = content
+ .context
+ .map(|context| {
+ context
+ .mentioned_jid
+ .iter()
+ .filter_map(|jid| person_text(jid))
+ .collect()
+ })
+ .unwrap_or_default();
+ InboundEvent {
event_id: event_id(
account,
&[
"message",
&info.chat.to_string(),
- &author,
+ author,
info.id.as_str(),
- kind_name,
+ content.kind.as_str(),
&revision_text,
],
),
group_jid: info.chat.to_non_ad().to_string(),
- source_at,
+ source_at: info.timestamp.unwrap_or(now),
observed_at: now,
kind: InboundKind::Message(Message {
source_id: info.id.to_string(),
sender,
- kind,
- text,
+ kind: content.kind,
+ text: content.text,
from_me: info.is_from_me,
history,
- revision,
- target,
- media,
+ revision: content.revision,
+ target: content.target,
+ media: content.media,
+ mentions,
+ forwarded: content
+ .context
+ .is_some_and(|context| context.is_forwarded.unwrap_or(false)),
+ rich: content.rich,
}),
+ }
+}
+
+/// A decrypted vote on a poll in a group: the options the voter chose now.
+pub fn map_poll_vote(
+ account: Uuid,
+ event: &whatsmeow::types::events::Message,
+ selected: &[Vec],
+ now: DateTime,
+) -> Option {
+ let info = &event.info;
+ if info.chat.server != GROUP_SERVER {
+ return None;
+ }
+ let author = person(&info.sender)?;
+ let update = event.message.as_ref()?.poll_update_message.as_ref()?;
+ let poll = target(update.poll_creation_message_key.as_ref()?, &author)?;
+ let content = Content {
+ rich: Some(rich::poll_vote(selected)),
+ ..Content::new(
+ MessageKind::PollVote,
+ String::new(),
+ Some(poll),
+ update.sender_timestamp_ms.unwrap_or(0),
+ )
+ };
+ Some(inbound_message(
+ account,
+ event,
+ &author,
+ now,
+ !event.unavailable_request_id.is_empty(),
+ content,
+ ))
+}
+
+/// The votes a history page carries on a poll message (already decrypted by
+/// the phone), one event per voter.
+pub fn history_poll_votes(
+ account: Uuid,
+ event: &whatsmeow::types::events::Message,
+ now: DateTime,
+) -> Vec {
+ let Some(web) = event.source_web_msg.as_ref() else {
+ return vec![];
+ };
+ let Some(poll_author) = person(&event.info.sender) else {
+ return vec![];
+ };
+ let poll = Target {
+ source_id: event.info.id.clone(),
+ sender_jid: poll_author,
+ };
+ web.poll_updates
+ .iter()
+ .filter_map(|update| {
+ let key = update.poll_update_message_key.as_ref()?;
+ let voter = if key.from_me.unwrap_or(false) {
+ person(&event.info.sender).filter(|_| event.info.is_from_me)?
+ } else {
+ person_text(key.participant.as_deref()?)?
+ };
+ let vote = update.vote.as_ref()?;
+ let source_id = key.id.clone()?;
+ let revision = update.sender_timestamp_ms.unwrap_or(0);
+ let source_at = DateTime::from_timestamp_millis(revision).unwrap_or(now);
+ Some(InboundEvent {
+ event_id: event_id(
+ account,
+ &[
+ "message",
+ &event.info.chat.to_string(),
+ &voter,
+ &source_id,
+ MessageKind::PollVote.as_str(),
+ &revision.to_string(),
+ ],
+ ),
+ group_jid: event.info.chat.to_non_ad().to_string(),
+ source_at,
+ observed_at: now,
+ kind: InboundKind::Message(Message {
+ source_id,
+ sender: Identity {
+ jid: voter,
+ alternate_jid: None,
+ display_name: None,
+ },
+ from_me: key.from_me.unwrap_or(false),
+ kind: MessageKind::PollVote,
+ text: String::new(),
+ history: true,
+ revision,
+ target: Some(poll.clone()),
+ media: vec![],
+ mentions: vec![],
+ forwarded: false,
+ rich: Some(rich::poll_vote(&vote.selected_options)),
+ }),
+ })
+ })
+ .collect()
+}
+
+/// A system line from a group notification, which has no message id: the
+/// line gets a stable one from what it says.
+fn system_event(
+ account: Uuid,
+ group: &Jid,
+ actor: Option<&Jid>,
+ actor_pn: Option<&Jid>,
+ at: Option>,
+ mut line: SystemLine,
+ now: DateTime,
+) -> InboundEvent {
+ let group = group.to_non_ad().to_string();
+ let actor = actor.and_then(person);
+ line.by_whatsapp = actor.is_none();
+ let source_at = at.unwrap_or(now);
+ // The same change seen by several accounts is one line: its id comes
+ // from what it says, never from who saw it.
+ let said = [
+ "system",
+ &group,
+ &source_at.timestamp().to_string(),
+ &serde_json::to_string(&line).unwrap_or_default(),
+ ]
+ .join("\n");
+ let source = Uuid::new_v5(&EVENT_NAMESPACE, said.as_bytes());
+ InboundEvent {
+ event_id: event_id(account, &[&said]),
+ source_at,
+ observed_at: now,
+ kind: InboundKind::Message(Message {
+ source_id: format!("SYSTEM-{}", source.simple()).to_uppercase(),
+ sender: Identity {
+ jid: actor.unwrap_or_else(|| group.clone()),
+ alternate_jid: actor_pn.and_then(person),
+ display_name: None,
+ },
+ from_me: false,
+ kind: MessageKind::System,
+ text: String::new(),
+ history: false,
+ revision: 0,
+ target: None,
+ media: vec![],
+ mentions: vec![],
+ forwarded: false,
+ rich: Some(RichContent::System(line)),
+ }),
+ group_jid: group,
+ }
+}
+
+/// The system lines of a live group notification, each its own event.
+pub fn map_group_info(account: Uuid, change: &GroupInfo, now: DateTime) -> Vec {
+ let actor = change.sender.as_ref().and_then(person);
+ rich::group_info_lines(change, actor.as_deref())
+ .into_iter()
+ .map(|line| {
+ system_event(
+ account,
+ &change.jid,
+ change.sender.as_ref(),
+ change.sender_pn.as_ref(),
+ change.timestamp,
+ line,
+ now,
+ )
+ })
+ .collect()
+}
+
+/// A new (or removed) group icon, as a system line.
+pub fn map_group_picture(
+ account: Uuid,
+ picture: &whatsmeow::types::events::Picture,
+ now: DateTime,
+) -> Option {
+ (picture.jid.server == GROUP_SERVER).then(|| {
+ system_event(
+ account,
+ &picture.jid,
+ (!picture.author.is_empty()).then_some(&picture.author),
+ None,
+ picture.timestamp,
+ rich::icon_line(),
+ now,
+ )
})
}
@@ -468,8 +880,10 @@ fn map_receipt(
}
let participant = person(&source.sender)?;
let state = match receipt.r#type.as_str() {
- "" => "delivered",
- other => other,
+ "" => ReceiptState::Delivered,
+ "read" => ReceiptState::Read,
+ "played" => ReceiptState::Played,
+ _ => return None,
};
let source_ids: Vec