Skip to content

Commit a6b3e3c

Browse files
committed
group message with MS Teams
1 parent d277b07 commit a6b3e3c

File tree

2 files changed

+124
-3
lines changed

2 files changed

+124
-3
lines changed

notify/providers/teams/teams.py

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict, Optional, Union, Any
1+
from typing import Dict, List, Optional, Union, Any
22
import json
33
import uuid
44
import base64
@@ -184,7 +184,27 @@ async def _render_(
184184
# TODO: using Jinja Templates on Teams Cards.
185185
return payload
186186

187-
async def _send_(self, to: Actor, message: Union[str, Any], **kwargs) -> Any:
187+
async def send_to_group(
188+
self,
189+
recipient: List[Actor],
190+
message: Union[str, Any],
191+
**kwargs
192+
) -> Any:
193+
"""send_to_group.
194+
Send message to Microsoft Teams channel using an incoming webhook.
195+
"""
196+
return await self._send_(
197+
to=recipient,
198+
message=message,
199+
**kwargs
200+
)
201+
202+
async def _send_(
203+
self,
204+
to: Union[Actor, List[Actor], TeamsChannel, TeamsChat, TeamsWebhook],
205+
message: Union[str, Any],
206+
**kwargs
207+
) -> Any:
188208
"""_send_.
189209
Send message to Microsoft Teams channel using an incoming webhook.
190210
"""
@@ -223,6 +243,10 @@ async def _send_(self, to: Actor, message: Union[str, Any], **kwargs) -> Any:
223243
result = await self.send_direct_message(
224244
to, msg
225245
)
246+
elif isinstance(to, list) and all(isinstance(r, Actor) for r in to):
247+
result = await self.send_group_direct_message(
248+
to, msg, topic=kwargs.get('topic')
249+
)
226250
else:
227251
raise NotifyException(
228252
"Invalid Recipient Object: Need an string or a TeamsChannel Object"
@@ -466,6 +490,36 @@ async def _create_chat(self, owner, user_id: str) -> str:
466490
result = await self._graph.chats.post(request_body)
467491
return result.id
468492

493+
async def _create_group_chat(
494+
self,
495+
member_ids: list[str],
496+
topic: Optional[str] = None
497+
) -> str:
498+
"""
499+
Create a group chat between the delegated account and N users.
500+
member_ids should include self._owner_id as well.
501+
"""
502+
members = [
503+
AadUserConversationMember(
504+
odata_type="#microsoft.graph.aadUserConversationMember",
505+
roles=["owner"],
506+
additional_data={
507+
# Mejor usar v1.0 en lugar de /beta
508+
"user@odata.bind": f"https://graph.microsoft.com/v1.0/users('{user_id}')"
509+
},
510+
)
511+
for user_id in member_ids
512+
]
513+
514+
request_body = Chat(
515+
chat_type=ChatType.Group,
516+
topic=topic,
517+
members=members,
518+
)
519+
520+
result = await self._graph.chats.post(request_body)
521+
return result.id
522+
469523
async def _get_chat(self, user_id: str) -> str:
470524
"""
471525
Create a new chat with the specified user or return if it already exists.
@@ -494,6 +548,45 @@ async def _get_chat(self, user_id: str) -> str:
494548
return chat.id
495549
return None
496550

551+
async def _get_group_chat(
552+
self,
553+
member_ids: list[str],
554+
restricted: bool = True
555+
) -> Optional[str]:
556+
"""
557+
Find an existing group chat that contains *at least* all member_ids.
558+
"""
559+
560+
query_params = ChatsRequestBuilder.ChatsRequestBuilderGetQueryParameters(
561+
filter="chatType eq 'group'",
562+
expand=["members"],
563+
)
564+
565+
request_configuration = RequestConfiguration(
566+
query_parameters=query_params,
567+
)
568+
569+
chats = await self._graph.chats.get(
570+
request_configuration=request_configuration
571+
)
572+
if not chats.value:
573+
return None
574+
575+
members_set = set(member_ids)
576+
print('::: Members Set > ', members_set)
577+
578+
for chat in chats.value:
579+
if not chat.members:
580+
continue
581+
chat_member_ids = {m.user_id for m in chat.members if m.user_id}
582+
if restricted:
583+
if chat_member_ids == members_set:
584+
return chat.id
585+
else:
586+
if members_set.issubset(chat_member_ids):
587+
return chat.id
588+
return None
589+
497590
async def get_teams_user(self, email: str) -> Dict[str, Any]:
498591
"""
499592
Retrieve a user from Microsoft Graph by email, returns the user object (JSON).
@@ -519,3 +612,31 @@ async def get_teams_user(self, email: str) -> Dict[str, Any]:
519612
self.logger.error(
520613
f"Failed to retrieve user info for {email}: {e}"
521614
)
615+
616+
async def send_group_direct_message(
617+
self,
618+
recipients: list[Actor],
619+
message: Dict[str, Any],
620+
topic: Optional[str] = None
621+
):
622+
"""
623+
Send a message to a group chat between the delegated account and 2+ users.
624+
"""
625+
626+
if not self._owner_id:
627+
raise NotifyException(
628+
"send_group_direct_message requires as_user=True so _owner_id is set."
629+
)
630+
631+
# Resolve the user ids for all recipients
632+
user_ids: list[str] = []
633+
for recipient in recipients:
634+
user = await self.get_teams_user(recipient.account.address)
635+
user_ids.append(user.id)
636+
637+
# Todos los miembros = owner + usuarios
638+
member_ids = [self._owner_id, *user_ids]
639+
# Buscar chat existente o crear nuevo
640+
chat_id = await self._get_group_chat(member_ids) or await self._create_group_chat(member_ids, topic=topic)
641+
# Enviar mensaje usando tu lógica actual
642+
return await self.send_message_to_chat(chat_id, message)

notify/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"Library for send notifications. "
66
"simple but powerful asyncio-based library for sending messages and notifications."
77
)
8-
__version__ = "1.5.1"
8+
__version__ = "1.5.2"
99
__url__ = "https://github.com/phenobarbital/async-notify"
1010
__copyright__ = "Copyright (c) 2020-2024 Jesus Lara"
1111
__author__ = "Jesus Lara"

0 commit comments

Comments
 (0)