Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Fixes #256 with Thread-Aware Message Retrieval #266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Joezanini wants to merge 1 commit into main
base: main
Choose a base branch
Loading
from fix/issue-265-root-message
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions examples/thread_example.py
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Example demonstrating the new thread-aware message retrieval functionality.

This example shows how to use the new thread utilities to collect thread messages
in both 1:1 conversations and spaces, addressing the issues described in #256.

Copyright (c) 2016-2024 Cisco and/or its affiliates.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

import os
import sys

# Add the src directory to the path so we can import webexpythonsdk
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

import webexpythonsdk
from webexpythonsdk.thread_utils import collect_thread_text_and_attachments


def main():
"""Main example function."""
# Initialize the Webex API
# You'll need to set your access token as an environment variable
access_token = os.getenv("WEBEX_ACCESS_TOKEN")
if not access_token:
print("Please set WEBEX_ACCESS_TOKEN environment variable")
return

api = webexpythonsdk.WebexAPI(access_token=access_token)

print("Webex Thread-Aware Message Retrieval Example")
print("=" * 50)

# Example 1: Using the new thread-aware API methods directly
print("\n1. Using thread-aware API methods:")
print("-" * 30)

# This would be a message object from a webhook or API call
# For demonstration, we'll create a mock message
class MockMessage:
def __init__(self, message_id, parent_id, room_id, room_type, text):
self.id = message_id
self.parentId = parent_id
self.roomId = room_id
self.roomType = room_type
self.text = text
self.personId = "person123"
self.created = "2024年01月01日T10:00:00Z"

# Example message from a space (group room)
space_message = MockMessage(
message_id="msg123",
parent_id="parent456",
room_id="room789",
room_type="group",
text="This is a reply in a space thread",
)

try:
# Get thread context using the new API method
thread_context = api.messages.get_thread_context(space_message)

print(f"Room Type: {thread_context['room_type']}")
print(f"Is Thread: {thread_context['is_thread']}")
print(f"Reply Count: {thread_context['reply_count']}")
print(f"Thread Messages: {len(thread_context['thread_messages'])}")

if thread_context["error"]:
print(f"Error: {thread_context['error']}")
else:
print("Thread retrieved successfully!")

except Exception as e:
print(f"Error retrieving thread context: {e}")

# Example 2: Using the utility function (drop-in replacement)
print("\n2. Using the utility function:")
print("-" * 30)

try:
# This is the drop-in replacement for the user's original function
thread_text, attachments = collect_thread_text_and_attachments(
api, space_message
)

print(f"Thread Text Length: {len(thread_text)} characters")
print(f"Attachments: {len(attachments)}")
print(f"Thread Text Preview: {thread_text[:100]}...")

except Exception as e:
print(f"Error using utility function: {e}")

# Example 3: Handling different room types
print("\n3. Handling different room types:")
print("-" * 30)

# Direct room message
direct_message = MockMessage(
message_id="msg456",
parent_id="parent789",
room_id="room123",
room_type="direct",
text="This is a reply in a 1:1 conversation",
)

try:
# Check room type
is_direct = api.messages._is_direct_room(direct_message)
is_group = api.messages._is_group_room(direct_message)

print(f"Message is from direct room: {is_direct}")
print(f"Message is from group room: {is_group}")

except Exception as e:
print(f"Error checking room type: {e}")

print("\nExample completed!")
print("\nTo use this in your bot:")
print(
"1. Replace your existing collect_thread_text_and_attachments function"
)
print(
"2. Import: from webexpythonsdk.thread_utils import collect_thread_text_and_attachments"
)
print(
"3. Call: thread_text, attachments = collect_thread_text_and_attachments(api, msg)"
)


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions src/webexpythonsdk/__init__.py
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
WebhookEvent,
)
from .models.simple import simple_data_factory, SimpleDataModel
from .thread_utils import collect_thread_text_and_attachments
from .utils import WebexDateTime


Expand Down
180 changes: 180 additions & 0 deletions src/webexpythonsdk/api/messages.py
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -394,3 +394,183 @@ def update(self, messageId=None, roomId=None, text=None, markdown=None):

# Add edit() as an alias to the update() method for backward compatibility
edit = update

def _is_direct_room(self, message):
"""Determine if a message is from a direct (1:1) room.

Args:
message: Message object with roomType property

Returns:
bool: True if the message is from a direct room, False otherwise
"""
if hasattr(message, "roomType"):
return message.roomType == "direct"
return False

def _is_group_room(self, message):
"""Determine if a message is from a group room (space).

Args:
message: Message object with roomType property

Returns:
bool: True if the message is from a group room, False otherwise
"""
if hasattr(message, "roomType"):
return message.roomType == "group"
return False

def get_thread_messages(self, message, max_scan=500):
"""Retrieve all messages in a thread, including the root message.

This method provides a robust way to collect thread messages that works
for both 1:1 conversations and spaces, handling the different permission
models and API limitations.

Args:
message: The message object to get the thread for
max_scan (int): Maximum number of messages to scan when searching for parent

Returns:
tuple: (thread_messages, root_message, error_message)
- thread_messages: List of all messages in the thread (oldest to newest)
- root_message: The root message of the thread (or None if not found)
- error_message: Error description if any issues occurred
"""
thread_messages = []
root_message = None
error_message = None

parent_id = getattr(message, "parentId", None)
room_id = getattr(message, "roomId", None)

if not parent_id or not room_id:
# Not a threaded message, return just this message
return [message], None, None

try:
# Strategy 1: Try to get the parent message directly
try:
root_message = self.get(parent_id)
thread_messages.append(root_message)
except Exception:
# Direct retrieval failed, try alternative strategies
if self._is_direct_room(message):
# For direct rooms, try list_direct with parentId
try:
direct_messages = list(
self.list_direct(
personId=getattr(message, "toPersonId", None),
personEmail=getattr(
message, "toPersonEmail", None
),
parentId=parent_id,
max=100,
)
)
if direct_messages:
root_message = direct_messages[0]
thread_messages.extend(direct_messages)
except Exception:
pass
else:
# For group rooms, try scanning recent messages
try:
scanned = 0
for msg in self.list(roomId=room_id, max=100):
scanned += 1
if getattr(msg, "id", None) == parent_id:
root_message = msg
thread_messages.append(msg)
break
if scanned >= max_scan:
break
except Exception:
pass

if not root_message:
error_message = f"Could not retrieve parent message {parent_id}. Bot may have joined after thread started or lacks permission."

# Strategy 2: Get all replies in the thread
try:
if self._is_direct_room(message):
# For direct rooms, use list_direct
replies = list(
self.list_direct(
personId=getattr(message, "toPersonId", None),
personEmail=getattr(
message, "toPersonEmail", None
),
parentId=parent_id,
max=100,
)
)
else:
# For group rooms, use list
replies = list(
self.list(roomId=room_id, parentId=parent_id, max=100)
)

# Add replies to thread messages, avoiding duplicates
existing_ids = {
getattr(m, "id", None) for m in thread_messages
}
for reply in replies:
reply_id = getattr(reply, "id", None)
if reply_id and reply_id not in existing_ids:
thread_messages.append(reply)
existing_ids.add(reply_id)

except Exception as e:
if not error_message:
error_message = (
f"Could not retrieve thread replies: {str(e)}"
)

# Strategy 3: Ensure the original message is included
original_id = getattr(message, "id", None)
if original_id and not any(
getattr(m, "id", None) == original_id for m in thread_messages
):
thread_messages.append(message)

# Sort messages by creation time (oldest to newest)
thread_messages.sort(key=lambda m: getattr(m, "created", ""))

except Exception as e:
error_message = f"Unexpected error retrieving thread: {str(e)}"

return thread_messages, root_message, error_message

def get_thread_context(self, message, max_scan=500):
"""Get thread context including root message and all replies.

This is a convenience method that returns a structured result with
thread information, making it easy to work with thread data.

Args:
message: The message object to get thread context for
max_scan (int): Maximum number of messages to scan when searching for parent

Returns:
dict: Dictionary containing:
- "thread_messages": List of all messages in thread (oldest to newest)
- "root_message": The root message of the thread
- "reply_count": Number of replies in the thread
- "is_thread": Boolean indicating if this is a threaded conversation
- "error": Error message if any issues occurred
- "room_type": Type of room (direct/group)
"""
thread_messages, root_message, error = self.get_thread_messages(
message, max_scan
)

return {
"thread_messages": thread_messages,
"root_message": root_message,
"reply_count": len(thread_messages) - 1 if root_message else 0,
"is_thread": getattr(message, "parentId", None) is not None,
"error": error,
"room_type": getattr(message, "roomType", "unknown"),
}
Loading

AltStyle によって変換されたページ (->オリジナル) /