Skip to content

feat(gchat): deliver files via multipart media upload endpoint#112

Open
tony-chinchill-ai wants to merge 1 commit into
Chinchill-AI:mainfrom
tony-chinchill-ai:tony/gchat-file-upload
Open

feat(gchat): deliver files via multipart media upload endpoint#112
tony-chinchill-ai wants to merge 1 commit into
Chinchill-AI:mainfrom
tony-chinchill-ai:tony/gchat-file-upload

Conversation

@tony-chinchill-ai

@tony-chinchill-ai tony-chinchill-ai commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds _gchat_media_upload() to GoogleChatAdapter: POSTs each file to https://chat.googleapis.com/upload/v1/{space}/messages?uploadType=multipart (one request per file -- the GChat upload API does not support batching)
  • Wires into post_message(): uploads files first, then returns early when the message carries no text or card (mirrors the Slack adapter pattern at slack/adapter.py:3261-3277)
  • Per-file errors are logged and swallowed so a transport hiccup cannot kill a turn whose text response already landed

Test plan

  • TestGChatMediaUpload::test_post_message_sends_one_multipart_upload_per_file -- two files produce two POSTs to /upload/v1/ with uploadType=multipart
  • TestGChatMediaUpload::test_files_only_post_message_does_not_emit_text_message -- files-only message returns early without posting to the standard messages endpoint
  • TestGChatMediaUpload::test_post_message_file_upload_failure_does_not_raise -- network error during upload is swallowed; post_message does not raise
  • Full GChat adapter test suite: 28/28 passing (uv run pytest tests/test_google_chat_adapter.py)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • File uploads to Google Chat are now supported through multipart media upload requests, allowing attachments in chat messages.
  • Tests

    • Added test coverage for Google Chat file upload functionality, including multipart request handling and error resilience.

Review Change Stack

Add _gchat_media_upload() to GoogleChatAdapter: POSTs each file as a
multipart/related request to the GChat media upload endpoint
(upload/v1/.../messages?uploadType=multipart). One request per file --
the GChat upload API does not support batching.

Wire into post_message(): uploads files first, then returns early when
the message has no text or card (matching the Slack adapter pattern).
Per-file errors are logged and swallowed so a transport hiccup cannot
kill a turn whose text response already landed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

GoogleChatAdapter now supports file attachments by implementing multipart media uploads to Google Chat. A new _gchat_media_upload helper processes files one-by-one, and post_message detects files-only messages to return early. Tests verify per-file uploads, query parameters, files-only flow, and error resilience.

Changes

Google Chat File Upload Support

Layer / File(s) Summary
Media upload implementation and integration
src/chat_sdk/adapters/google_chat/adapter.py
Imports FileUpload type, implements _gchat_media_upload helper for per-file multipart media uploads with optional thread metadata, and updates post_message to upload extracted files and handle files-only payloads without making standard API calls.
Media upload test coverage
tests/test_google_chat_adapter.py
Test helper with fake async HTTP session; test cases verify one multipart upload request per file with uploadType=multipart query param, files-only message exit after upload, and exception swallowing on transport errors.

Sequence Diagram

sequenceDiagram
  participant Client
  participant post_message
  participant extract_files
  participant _gchat_media_upload
  participant GChatAPI
  
  Client->>post_message: post_message(message)
  post_message->>extract_files: extract_files(message)
  extract_files-->>post_message: files list
  alt files present
    loop per file
      post_message->>_gchat_media_upload: upload file with optional thread metadata
      _gchat_media_upload->>GChatAPI: POST /upload/v1/ multipart
      GChatAPI-->>_gchat_media_upload: response
      _gchat_media_upload-->>post_message: log success/failure
    end
    alt files-only (no text/card)
      post_message-->>Client: RawMessage({files})
    else has text or card
      post_message->>GChatAPI: POST standard card/text API
      GChatAPI-->>post_message: response
      post_message-->>Client: result
    end
  else no files
    post_message->>GChatAPI: POST standard card/text API
    GChatAPI-->>post_message: response
    post_message-->>Client: result
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit bounces with glee,
Files now upload to Google Chat, you see!
Multipart magic, thread-aware and bright,
Per-file uploads done just right! 📎✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding multipart media upload functionality to deliver files to Google Chat, which is the primary feature added in this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements file upload support for Google Chat by introducing a multipart media upload method (_gchat_media_upload) and integrating it into post_message. It also adds corresponding unit tests. The review feedback suggests improving the robustness of the text detection logic (has_text) to correctly handle dictionary-based messages and avoid truthiness issues with is not None checks. Additionally, it recommends uploading files concurrently using asyncio.gather to improve performance, alongside ensuring explicit is not None checks for optional parameters.

Comment on lines +1419 to 1424
has_text = (
isinstance(message, str)
or (hasattr(message, "raw") and getattr(message, "raw", None))
or (hasattr(message, "markdown") and getattr(message, "markdown", None))
or (hasattr(message, "ast") and getattr(message, "ast", None))
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If message is passed as a dict (which is supported and handled by extract_files and extract_card), the has_text check will evaluate to False because it only checks attributes using hasattr and getattr. This causes any text content in a dict-based message to be silently dropped when files are present, returning early with only the files uploaded. Adding a check for dictionary keys ensures robust behavior. Additionally, use is not None checks instead of truthiness checks to avoid silently ignoring falsy but valid values (like empty strings).

Suggested change
has_text = (
isinstance(message, str)
or (hasattr(message, "raw") and getattr(message, "raw", None))
or (hasattr(message, "markdown") and getattr(message, "markdown", None))
or (hasattr(message, "ast") and getattr(message, "ast", None))
)
has_text = (
isinstance(message, str)
or (isinstance(message, dict) and any(message.get(k) is not None for k in ("raw", "markdown", "ast")))
or (hasattr(message, "raw") and getattr(message, "raw", None) is not None)
or (hasattr(message, "markdown") and getattr(message, "markdown", None) is not None)
or (hasattr(message, "ast") and getattr(message, "ast", None) is not None)
)
References
  1. When checking for optional values that can be falsy but valid (e.g., 0, empty string, empty list), use is not None instead of a truthiness check to avoid silently ignoring them.

Comment on lines +458 to +498
for file in files:
try:
boundary = f"boundary_{_random_id()}"
metadata: dict[str, Any] = {"text": label}
if thread_name:
metadata["thread"] = {"name": thread_name}

body = (
f"--{boundary}\r\n"
f"Content-Type: application/json; charset=UTF-8\r\n\r\n"
f"{json.dumps(metadata)}\r\n"
f"--{boundary}\r\n"
f"Content-Type: {file.mime_type or 'application/octet-stream'}\r\n\r\n"
).encode() + file.data + f"\r\n--{boundary}--".encode()

async with session.request(
"POST",
upload_url,
data=body,
params={"uploadType": "multipart"},
headers={
"Authorization": f"Bearer {token}",
"Content-Type": f"multipart/related; boundary={boundary}",
},
) as response:
if response.status >= 400:
error_text = await response.text()
self._logger.error(
f"GChat media upload failed for {file.filename}",
{"status": response.status, "error": error_text},
)
else:
self._logger.debug(
"GChat media upload succeeded",
{"filename": file.filename},
)
except Exception:
self._logger.error(
f"GChat media upload failed for {file.filename}",
{"exc_info": True},
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Uploading files sequentially can be slow, especially when multiple files are attached. Since Google Chat does not support batching, we can perform the uploads concurrently using asyncio.gather to improve performance and efficiency. Additionally, ensure we use explicit is not None checks for optional values like thread_name and mime_type to avoid issues with falsy but valid values.

        async def _upload_single_file(file: FileUpload) -> None:
            try:
                boundary = f"boundary_{_random_id()}"
                metadata: dict[str, Any] = {"text": label}
                if thread_name is not None:
                    metadata["thread"] = {"name": thread_name}

                body = (
                    f"--{boundary}\r\n"
                    f"Content-Type: application/json; charset=UTF-8\r\n\r\n"
                    f"{json.dumps(metadata)}\r\n"
                    f"--{boundary}\r\n"
                    f"Content-Type: {file.mime_type if file.mime_type is not None else 'application/octet-stream'}\r\n\r\n"
                ).encode() + file.data + f"\r\n--{boundary}--".encode()

                async with session.request(
                    "POST",
                    upload_url,
                    data=body,
                    params={"uploadType": "multipart"},
                    headers={
                        "Authorization": f"Bearer {token}",
                        "Content-Type": f"multipart/related; boundary={boundary}",
                    },
                ) as response:
                    if response.status >= 400:
                        error_text = await response.text()
                        self._logger.error(
                            f"GChat media upload failed for {file.filename}",
                            {"status": response.status, "error": error_text},
                        )
                    else:
                        self._logger.debug(
                            "GChat media upload succeeded",
                            {"filename": file.filename},
                        )
            except Exception:
                self._logger.error(
                    f"GChat media upload failed for {file.filename}",
                    {"exc_info": True},
                )

        await asyncio.gather(*(_upload_single_file(file) for file in files))
References
  1. When checking for optional values that can be falsy but valid (e.g., 0, empty string, empty list), use is not None instead of a truthiness check to avoid silently ignoring them.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e1e21a849

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


token = await self._get_access_token()
session = await self._get_http_session()
upload_url = f"https://chat.googleapis.com/upload/v1/{space_name}/messages"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the attachments upload flow

When any file is posted, this builds https://chat.googleapis.com/upload/v1/{space}/messages, but the Google Chat media API upload URI is /upload/v1/{parent=spaces/*}/attachments:upload and it returns an attachmentDataRef that must be passed in a subsequent spaces.messages.create attachment field. As written, file uploads go to the wrong resource and the response is ignored, so file-only messages can return success while no attachment is delivered.

Useful? React with 👍 / 👎.

Comment on lines +454 to +455
token = await self._get_access_token()
session = await self._get_http_session()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Authenticate media uploads as a user

For the default service-account/ADC bot configuration, _get_access_token() mints a token using GCHAT_SCOPES (chat.bot, read/reaction scopes), but Google Chat media upload requires user authentication with chat.messages.create or chat.messages. In those deployments every file upload gets a 403 and is swallowed by the new error handling, so users see a successful post with missing files.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/chat_sdk/adapters/google_chat/adapter.py (1)

1425-1434: 💤 Low value

Redundant extract_card call.

extract_card(message) is called at line 1425 and again at line 1434. Since the function has no side effects, the second call is wasteful.

♻️ Remove redundant call
                 has_text = (
                     isinstance(message, str)
                     or (hasattr(message, "raw") and getattr(message, "raw", None))
                     or (hasattr(message, "markdown") and getattr(message, "markdown", None))
                     or (hasattr(message, "ast") and getattr(message, "ast", None))
                 )
                 card = extract_card(message)
                 if not (has_text or card):
                     return RawMessage(
                         id=f"file-{int(time.time() * 1000)}",
                         thread_id=thread_id,
                         raw={"files": files},
                     )
 
-            # Check if message contains a card
-            card = extract_card(message)
-
-            if card:
+            if card:  # already computed above when files were present

Note: This fix is incomplete since card is only computed inside the if files: block. You'd need to move card = extract_card(message) outside that block to avoid the redundant call in all paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/chat_sdk/adapters/google_chat/adapter.py` around lines 1425 - 1434, The
code calls extract_card(message) twice (once inside the files-handling branch
and again after it), which is redundant; compute card once before the files
conditional by moving the extract_card(message) call above the block that checks
files, remove the later duplicate call, and ensure all uses reference the single
variable card (look for the extract_card function and the card variable in the
files-handling branch and the subsequent "Check if message contains a card"
section).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/chat_sdk/adapters/google_chat/adapter.py`:
- Line 71: The file src/chat_sdk/adapters/google_chat/adapter.py failed ruff
formatting; run the formatter (e.g., `ruff format
src/chat_sdk/adapters/google_chat/adapter.py`) or apply the equivalent styling
changes so the import list (including FileUpload) and surrounding code comply
with ruff rules; ensure the import line containing FileUpload and the adapter
module's formatting are fixed and then re-run `ruff format --check` to verify.

In `@tests/test_google_chat_adapter.py`:
- Around line 390-421: Run ruff format on the test file and add the missing
newline at EOF: run `ruff format tests/test_google_chat_adapter.py` to fix
formatting in the test_post_message_file_upload_failure_does_not_raise test
(which references GoogleChatThreadId, encode_thread_id, PostableMarkdown,
FileUpload, adapter._get_access_token, adapter._get_http_session, and
adapter.post_message) and ensure the file ends with a trailing newline after the
last line.

---

Nitpick comments:
In `@src/chat_sdk/adapters/google_chat/adapter.py`:
- Around line 1425-1434: The code calls extract_card(message) twice (once inside
the files-handling branch and again after it), which is redundant; compute card
once before the files conditional by moving the extract_card(message) call above
the block that checks files, remove the later duplicate call, and ensure all
uses reference the single variable card (look for the extract_card function and
the card variable in the files-handling branch and the subsequent "Check if
message contains a card" section).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 78c83cd4-a174-4c9e-963c-76df9c48339e

📥 Commits

Reviewing files that changed from the base of the PR and between f1a1d4f and 8e1e21a.

📒 Files selected for processing (2)
  • src/chat_sdk/adapters/google_chat/adapter.py
  • tests/test_google_chat_adapter.py

EphemeralMessage,
FetchOptions,
FetchResult,
FileUpload,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Pipeline failure: ruff formatting required.

The CI pipeline reports that ruff format --check failed for this file. Run ruff format src/chat_sdk/adapters/google_chat/adapter.py to fix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/chat_sdk/adapters/google_chat/adapter.py` at line 71, The file
src/chat_sdk/adapters/google_chat/adapter.py failed ruff formatting; run the
formatter (e.g., `ruff format src/chat_sdk/adapters/google_chat/adapter.py`) or
apply the equivalent styling changes so the import list (including FileUpload)
and surrounding code comply with ruff rules; ensure the import line containing
FileUpload and the adapter module's formatting are fixed and then re-run `ruff
format --check` to verify.

Comment on lines +390 to +421
@pytest.mark.asyncio
async def test_post_message_file_upload_failure_does_not_raise(self):
"""A file upload network error must not propagate out of post_message --
a transport hiccup cannot kill a turn whose text response already landed."""
from unittest.mock import AsyncMock

from chat_sdk.adapters.google_chat.thread_utils import GoogleChatThreadId, encode_thread_id
from chat_sdk.types import FileUpload, PostableMarkdown

adapter = _make_adapter()
adapter._get_access_token = AsyncMock(return_value="tok") # type: ignore[method-assign]

class _FailResponse:
async def __aenter__(self):
raise RuntimeError("network error")

async def __aexit__(self, *args):
pass

class _FailSession:
def request(self, method: str, url: str, **kwargs) -> _FailResponse:
return _FailResponse()

adapter._get_http_session = AsyncMock(return_value=_FailSession()) # type: ignore[method-assign]

thread_id = encode_thread_id(GoogleChatThreadId(space_name="spaces/TEST"))
message = PostableMarkdown(
markdown="",
files=[FileUpload(data=b"x", filename="file.txt", mime_type="text/plain")],
)

await adapter.post_message(thread_id, message)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Pipeline failure: ruff formatting required & missing trailing newline.

The CI reports ruff format --check failed. Run ruff format tests/test_google_chat_adapter.py and ensure the file ends with a newline after line 421.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_google_chat_adapter.py` around lines 390 - 421, Run ruff format on
the test file and add the missing newline at EOF: run `ruff format
tests/test_google_chat_adapter.py` to fix formatting in the
test_post_message_file_upload_failure_does_not_raise test (which references
GoogleChatThreadId, encode_thread_id, PostableMarkdown, FileUpload,
adapter._get_access_token, adapter._get_http_session, and adapter.post_message)
and ensure the file ends with a trailing newline after the last line.

patrick-chinchill added a commit that referenced this pull request Jun 19, 2026
CHANGELOG:
- Drop false gchat file-delivery bullet (PR #112 never merged; upstream
  itself does not implement gchat outbound file delivery)
- Fix Slack DM block-action citation to merged PR #137 (supersedes #133)
- Re-point 0.4.27.1 vehicle from unmerged #120 to tag v0.4.27.1 + #117
- Drop numberless Twilio 'scaffolding PR' (only #142 exists)

UPSTREAM_SYNC:
- Reframe Google Chat outbound file delivery as parity (upstream also
  logs 'not yet supported' + media.upload TODO, index.ts:1282-1289)
- Correct Teams cert-auth row: TS is not 'Supported' — config throws at
  startup in both SDKs (types.ts:31, config.ts:13); parity, issue #58
- Add Messenger get_user row: upstream has no getUser; raising stub is
  parity, Graph-API impl tracked as #132
patrick-chinchill added a commit that referenced this pull request Jun 19, 2026
…PR 4/4) (#146)

* chore(release): cut 0.4.30 — Teams SDK migration + 4.30.0 parity (#93 PR 4/4)

Final PR of the Teams SDK migration (issue #93). PRs 1-3 (inbound+auth,
outbound, native streaming) are merged; this cuts the 0.4.30 release.

- Version bump: pyproject 0.4.29 -> 0.4.30; UPSTREAM_PARITY "4.29.0" -> "4.30.0".
- Fidelity re-pin chat@4.29.0 -> chat@4.30.0 in lint.yml +
  verify_test_fidelity.py (docstring, default parity fallback, clone hint).
  packages/chat/src is byte-for-byte identical between the two tags, so zero
  new test ports: strict fidelity stays 100% (732/732, 0 missing) against
  chat@4.30.0. Baseline regenerated (ts_parity -> chat@4.30.0; the recorded
  total_ts_tests literal 731 -> 732 corrects a stale count from the merged
  adapter waves, not the re-pin — the count is identical against both tags).
- Docs: project-instructions version map + fidelity pin; README status line;
  CHANGELOG 0.4.30 entry (Twilio adapter, Telegram streaming, Slack subpaths,
  WhatsApp/Slack/gchat fixes, Teams #93 PRs 1-4); UPSTREAM_SYNC.md parity
  header + the Teams deferral row flipped to delivered.
- Version-label normalization: malformed `adapter-teams@chat@4.30.0` and loose
  `adapter-teams@4.30.0` -> `@chat-adapter/teams@4.30.0` in adapter.py (5),
  bridge.py (1), UPSTREAM_SYNC.md (4). Comment/doc-only.

Does NOT tag/publish — the release is a separate maintainer-gated step (live
Teams 429 streaming check + PyPI authorization).

* docs(release): clarify 0.4.30 core-parity wording + finish label normalization

Review fast-follow for PR 4. The CHANGELOG now states the mapped core is
content-identical *between the chat@4.29.0 and chat@4.30.0 upstream tags*
(verified: thread.ts/types.ts/thread.test.ts and the full packages/chat/src
tree are byte-identical) — the prior 'unchanged from 4.29.0' phrasing was
accurate but misread as a claim about our code. Also sweeps the two
remaining old-style `adapter-teams@chat@4.30.0` labels in the Teams test
docstrings to the canonical `@chat-adapter/teams@4.30.0` npm tag,
completing the normalization the PR's scope called for. No logic change.

* docs(release): document 0.4.30 parity-audit wave + remaining exceptions

CHANGELOG: add 'Pre-existing parity gaps closed (4.30 audit)' subsection
covering the 7 ported fixes (PRs #147-#150) and a documented-exceptions
note (Linear agent-sessions #151 deferred to 4.31 / #152; adapter-web and
the GitHub/Linear native-client + message.subject halves stay Known
Non-Parity).

UPSTREAM_SYNC:
- file-mapping table: add state-ioredis -> redis.py (IoRedisStateAdapter)
  and state-pg -> postgres.py rows (both ported + tested, were missing).
- Known Non-Parity (platform gaps): add Linear agent-sessions row (#151)
  and an adapter-web row that splits the portable server-side WebAdapter
  (deferred) from the genuinely browser-only client subpaths, correcting
  the earlier over-broad 'browser-only; no Python runtime' note.
- Correct the Google Chat file-uploads row: inbound attachment parsing is
  fully implemented (_create_attachment), so the gap is outbound file
  delivery only (post_message still logs 'not yet supported').

* docs(release): correct phantom-feature framing before PyPI cut

CHANGELOG:
- Drop false gchat file-delivery bullet (PR #112 never merged; upstream
  itself does not implement gchat outbound file delivery)
- Fix Slack DM block-action citation to merged PR #137 (supersedes #133)
- Re-point 0.4.27.1 vehicle from unmerged #120 to tag v0.4.27.1 + #117
- Drop numberless Twilio 'scaffolding PR' (only #142 exists)

UPSTREAM_SYNC:
- Reframe Google Chat outbound file delivery as parity (upstream also
  logs 'not yet supported' + media.upload TODO, index.ts:1282-1289)
- Correct Teams cert-auth row: TS is not 'Supported' — config throws at
  startup in both SDKs (types.ts:31, config.ts:13); parity, issue #58
- Add Messenger get_user row: upstream has no getUser; raising stub is
  parity, Graph-API impl tracked as #132

* docs(changelog): clarify 0.4.30 audit-gap count (8 closed + 1 deferred = 9)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant