Skip to content

Conversation

@mldangelo
Copy link
Member

Summary

  • Add real-time message polling to crab slack chat so incoming messages are displayed as they arrive
  • Fix the empty polling block that previously did nothing
  • Track message timestamps to avoid showing duplicate messages

Test plan

  • Start a chat session with crab slack chat @someone
  • Send a test message from the terminal
  • Have the other person reply (or use Slack app to send yourself a DM)
  • Verify new messages appear automatically within ~5 seconds
  • Verify own sent messages don't appear twice
  • Verify Ctrl+C exits cleanly

🤖 Generated with Claude Code

Implement message polling in slack_chat() so incoming messages are
displayed as they arrive. Previously, the polling block was empty.

- Add slack_get_latest_ts() to get newest message timestamp
- Add slack_poll_new_messages() to fetch messages since a timestamp
- Track last_ts after displaying initial messages
- Poll every 5 seconds and display new messages in chronological order
- Update last_ts after sending to avoid showing own messages twice

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@MrFlounder MrFlounder left a comment

Choose a reason for hiding this comment

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

double check the current pull command to see if that duplicate your effort. If not then feel free to merge

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds real-time polling to crab slack chat so inbound Slack messages appear automatically during an interactive chat session, using a last-seen timestamp baseline to avoid duplicates.

Changes:

  • Added helpers to fetch the latest message timestamp and to poll Slack history for messages newer than a stored timestamp.
  • Implemented a periodic polling loop in slack_chat to display new messages as they arrive.
  • Updated the timestamp baseline after sending messages to reduce duplicate display.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

src/crabcode Outdated
Comment on lines 2581 to 2583
curl -s -H "Authorization: Bearer $token" \
"https://slack.com/api/conversations.history?channel=$channel_id&limit=1" \
| jq -r '.messages[0].ts // "0"'
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

slack_get_latest_ts doesn’t validate Slack’s {ok:false, error:...} responses. On auth/rate-limit errors this will currently return "0" (or "null"), which can cause the polling baseline to reset and re-display recent history. Consider parsing .ok/.error (like slack_read_history) and returning non-zero on failure so callers can handle it explicitly.

Suggested change
curl -s -H "Authorization: Bearer $token" \
"https://slack.com/api/conversations.history?channel=$channel_id&limit=1" \
| jq -r '.messages[0].ts // "0"'
local response=$(curl -s -H "Authorization: Bearer $token" \
"https://slack.com/api/conversations.history?channel=$channel_id&limit=1")
local ok=$(echo "$response" | jq -r '.ok')
if [ "$ok" != "true" ]; then
# Do not return a bogus "0" timestamp on Slack API errors
return 1
fi
echo "$response" | jq -r '.messages[0].ts // "0"'

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed in 885182d - Added .ok validation to slack_get_latest_ts. Now returns non-zero on API errors, and callers handle this gracefully.

src/crabcode Outdated
echo -ne "\r\033[K"

# Poll for new messages
local new_messages=$(slack_poll_new_messages "$channel_id" "$last_ts")
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

Because the script runs with set -e, this command substitution will terminate the entire chat loop if slack_poll_new_messages returns non-zero (e.g. Slack returns ok:false due to ratelimited). Wrap the call in an if new_messages=$(...); then ... else ... fi (or use || true with explicit handling) so transient polling failures don’t exit the session.

Suggested change
local new_messages=$(slack_poll_new_messages "$channel_id" "$last_ts")
local new_messages=""
if ! new_messages=$(slack_poll_new_messages "$channel_id" "$last_ts"); then
# Avoid exiting the chat loop on transient Slack API errors (e.g. rate limiting)
last_check=$now
continue
fi

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed in 885182d - Wrapped polling call in if ! new_messages=$(...) to handle errors gracefully without crashing the chat loop under set -e.

src/crabcode Outdated
Comment on lines 2671 to 2672
# Update last_ts to avoid re-displaying our own message
last_ts=$(slack_get_latest_ts "$channel_id")
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

Updating last_ts by calling slack_get_latest_ts after sending can skip messages: if another user posts between chat.postMessage and this history fetch, the “latest” ts may advance past an unseen incoming message, and the next poll will never display it. Prefer capturing the sent message ts from the chat.postMessage response and advancing last_ts to that value (or otherwise ensure you don’t jump past unseen messages).

Suggested change
# Update last_ts to avoid re-displaying our own message
last_ts=$(slack_get_latest_ts "$channel_id")
# Note: last_ts is updated when polling new messages to avoid skipping any

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed in 885182d - Removed the post-send last_ts update. Now last_ts is only updated when messages are displayed in the polling loop, ensuring no messages are skipped.

src/crabcode Outdated
Comment on lines 2592 to 2595
# Get messages newer than since_ts
local response=$(curl -s -H "Authorization: Bearer $token" \
"https://slack.com/api/conversations.history?channel=$channel_id&oldest=$since_ts&limit=20")

Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

Polling uses a fixed limit=20. If more than 20 messages arrive between polls (busy channels, long pauses), older messages in that interval will be dropped because conversations.history returns only the most recent page. Consider paginating using has_more/response_metadata.next_cursor until all messages since since_ts are drained (or increase the limit and document the tradeoff).

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed in 885182d - Increased limit from 20 to 100. For most use cases this should be sufficient. Full pagination could be added later if needed for very high-traffic channels.

src/crabcode Outdated
Comment on lines 2691 to 2703
local text=$(echo "$msg" | jq -r '.text')
local bot_name=$(echo "$msg" | jq -r '.bot_profile.name // empty')

local sender
if [ -n "$bot_name" ]; then
sender="$bot_name"
elif [ -n "$user_id" ]; then
sender=$(slack_get_username_cached "$user_id")
else
sender="unknown"
fi

echo -e "${GRAY}[$time]${NC} ${CYAN}$sender${NC}: $text"
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

Slack message text is read from the API and passed directly into echo -e without any sanitization, which allows remote users to inject terminal escape/control sequences. A malicious Slack user could send a message containing ANSI or control codes that, when rendered, can clear the screen, forge prompts, or otherwise manipulate the terminal output shown to the operator. To mitigate this, strip or escape non-printable/control characters from text before printing, or otherwise ensure only safe printable characters are written to the terminal.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed in 885182d - Added comprehensive sanitization:

  • Removes CSI sequences (colors, cursor control, screen manipulation)
  • Removes OSC sequences (title/window manipulation)
  • Strips control characters (bell, backspace, etc.)

Tested against multiple injection payloads including color codes, title injection, screen clear, and cursor movement attacks.

- Add Slack API error validation to slack_get_latest_ts
- Handle polling errors gracefully to prevent chat loop crashes with set -e
- Remove post-send last_ts update to avoid skipping messages
- Increase poll limit from 20 to 100 for busy channels
- Sanitize message text to prevent terminal escape sequence injection
  - Removes CSI sequences (colors, cursor, screen control)
  - Removes OSC sequences (title manipulation)
  - Strips control characters (bell, backspace, etc.)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@mldangelo mldangelo force-pushed the feat/slack-chat-polling branch from 814e7d1 to 885182d Compare January 26, 2026 08:20
@mldangelo
Copy link
Member Author

Checked for duplicate functionality - no overlap found. slack read is a one-time history fetch, while slack chat provides interactive real-time polling. Different use cases.

All Copilot review comments have been addressed in commit 885182d. Ready for re-review.

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.

3 participants