Temp commit for notification
This commit is contained in:
parent
c0986b10bf
commit
1510010091
@ -16,4 +16,4 @@ EXPOSE 8005
|
|||||||
|
|
||||||
|
|
||||||
# Run the application using the start script
|
# Run the application using the start script
|
||||||
CMD ["uvicorn", "app.central_storage.webapi.main:app", "--reload", "--port=8005", "--host=0.0.0.0", "--log-level", "warning"]
|
CMD ["uvicorn", "app.central_storage.webapi.main:app", "--reload", "--port=8005", "--host=0.0.0.0"]
|
||||||
|
|||||||
@ -1,11 +1,37 @@
|
|||||||
from app.notification.backend.business.notification_manager import (
|
from typing import Dict
|
||||||
NotificationManager,
|
from app.notification.backend.business.notification_manager import NotificationManager
|
||||||
)
|
from app.notification.backend.models.constants import NotificationChannel
|
||||||
|
|
||||||
|
|
||||||
class NotificationHub:
|
class NotificationHub:
|
||||||
def __init__(
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def enqueue_notification(
|
||||||
self,
|
self,
|
||||||
|
sender_id: str, # Added sender_id here
|
||||||
|
channels: list[str], # Accept multiple channels as a list of strings
|
||||||
|
receiver_id: str,
|
||||||
|
subject: str,
|
||||||
|
event: str,
|
||||||
|
properties: Dict,
|
||||||
):
|
):
|
||||||
self.notification_manager = NotificationManager()
|
# Convert string channels to NotificationChannel enums
|
||||||
return
|
notification_channels = []
|
||||||
|
for channel_str in channels:
|
||||||
|
try:
|
||||||
|
notification_channels.append(NotificationChannel[channel_str.upper()])
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError(f"Unsupported notification channel: {channel_str}")
|
||||||
|
|
||||||
|
# Initialize NotificationManager with sender_id
|
||||||
|
notification_manager = NotificationManager(sender_id=sender_id)
|
||||||
|
|
||||||
|
# Call the enqueue_notification method in NotificationManager
|
||||||
|
return await notification_manager.enqueue_notification(
|
||||||
|
channels=notification_channels,
|
||||||
|
receiver_id=receiver_id,
|
||||||
|
subject=subject,
|
||||||
|
event=event,
|
||||||
|
properties=properties,
|
||||||
|
)
|
||||||
|
|||||||
@ -1,10 +1,261 @@
|
|||||||
|
from typing import Dict
|
||||||
from app.notification.backend.services.sms_service import SmsService
|
from app.notification.backend.services.sms_service import SmsService
|
||||||
from app.notification.backend.services.in_app_notif_service import InAppNotifService
|
from app.notification.backend.services.in_app_notif_service import InAppNotifService
|
||||||
from app.notification.backend.services.email_service import EmailService
|
from app.notification.backend.services.email_service import EmailService
|
||||||
|
from app.notification.backend.services.notification_publisher_service import (
|
||||||
|
NotificationPublisherService,
|
||||||
|
)
|
||||||
|
from app.notification.backend.models.constants import (
|
||||||
|
NotificationChannel,
|
||||||
|
NotificationMessage,
|
||||||
|
SystemNotifications,
|
||||||
|
)
|
||||||
|
import threading
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional, Type
|
||||||
|
from types import TracebackType
|
||||||
|
from infra.models.constants import UserRegion
|
||||||
|
from app.notification.backend.services.notification_publisher_service import (
|
||||||
|
NotificationPublisherService,
|
||||||
|
)
|
||||||
|
from app.notification.common.config.app_settings import app_settings
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
class NotificationManager:
|
class NotificationManager:
|
||||||
def __init__(self) -> None:
|
notification_queues: dict[NotificationChannel, NotificationPublisherService] = None
|
||||||
|
instance_counter = 0
|
||||||
|
instance_counter_lock = threading.Lock()
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
sender_id: str, # Require sender_id in the constructor
|
||||||
|
) -> None:
|
||||||
|
self.notification_publisher_service = NotificationPublisherService()
|
||||||
self.sms_service = SmsService()
|
self.sms_service = SmsService()
|
||||||
self.in_app_notif_service = InAppNotifService()
|
self.in_app_notif_service = InAppNotifService()
|
||||||
self.email_service = EmailService()
|
self.email_service = EmailService()
|
||||||
|
self.sender_id = sender_id
|
||||||
|
self.notification_queues = NotificationManager.__create_notification_queues__()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __increment_instance_counter__() -> int:
|
||||||
|
with NotificationManager.instance_counter_lock:
|
||||||
|
NotificationManager.instance_counter += 1
|
||||||
|
return NotificationManager.instance_counter
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __decrement_instance_counter__() -> int:
|
||||||
|
with NotificationManager.instance_counter_lock:
|
||||||
|
NotificationManager.instance_counter -= 1
|
||||||
|
return NotificationManager.instance_counter
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __create_notification_queues__() -> None:
|
||||||
|
if not NotificationManager.notification_queues:
|
||||||
|
NotificationManager.notification_queues = {}
|
||||||
|
for channel in NotificationChannel:
|
||||||
|
NotificationManager.notification_queues[channel] = (
|
||||||
|
NotificationPublisherService(channel=channel)
|
||||||
|
)
|
||||||
|
return NotificationManager.notification_queues
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
if NotificationManager.__increment_instance_counter__() == 1:
|
||||||
|
for channel in NotificationManager.notification_queues:
|
||||||
|
await NotificationManager.notification_queues[channel].bind()
|
||||||
|
|
||||||
|
async def __aexit__(
|
||||||
|
self,
|
||||||
|
exctype: Optional[Type[BaseException]],
|
||||||
|
excinst: Optional[BaseException],
|
||||||
|
exctb: Optional[TracebackType],
|
||||||
|
):
|
||||||
|
if NotificationManager.__decrement_instance_counter__() == 0:
|
||||||
|
for channel in NotificationManager.notification_queues:
|
||||||
|
await NotificationManager.notification_queues[channel].close()
|
||||||
|
|
||||||
|
async def __publish_notification__(
|
||||||
|
self,
|
||||||
|
channel: NotificationChannel,
|
||||||
|
message: NotificationMessage,
|
||||||
|
) -> None:
|
||||||
|
message.properties["publish_time"] = datetime.now(timezone.utc).isoformat()
|
||||||
|
await self.notification_queues[channel].publish(
|
||||||
|
message=message.model_dump_json()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def __generate_message_from_subject_and_event__(
|
||||||
|
self,
|
||||||
|
subject: str,
|
||||||
|
event: str,
|
||||||
|
properties: dict,
|
||||||
|
region: Optional[UserRegion] = None,
|
||||||
|
) -> str:
|
||||||
|
# leverage the information in properties to enrich the message.
|
||||||
|
message_subject = None
|
||||||
|
message = None
|
||||||
|
if subject.lower() == "payment":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Default region to be international if not set
|
||||||
|
if region is None:
|
||||||
|
region = UserRegion.OTHER
|
||||||
|
|
||||||
|
message_subject = SystemNotifications[region][subject.lower()][event.lower()][
|
||||||
|
"message_subject"
|
||||||
|
]
|
||||||
|
message = SystemNotifications[region][subject.lower()][event.lower()]["message"]
|
||||||
|
|
||||||
|
if event.lower() == "authentication":
|
||||||
|
message = message.format(properties["auth_code"])
|
||||||
|
if not message:
|
||||||
|
raise RuntimeError("unsupported event:{}".format(event))
|
||||||
|
return message, message_subject
|
||||||
|
|
||||||
|
async def send_in_app_notification(
|
||||||
|
self, receiver_id: str, subject: str, event: str, properties: dict = None
|
||||||
|
) -> None:
|
||||||
|
await self.__publish_notification__(
|
||||||
|
channel=NotificationChannel.IN_APP,
|
||||||
|
message=NotificationMessage(
|
||||||
|
sender_id=app_settings.SYSTEM_USER_ID,
|
||||||
|
receiver_id=receiver_id,
|
||||||
|
subject=subject,
|
||||||
|
event=event,
|
||||||
|
properties=properties,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send_chat_message_notification(
|
||||||
|
self, receiver_id: str, subject: str, event: str, properties: dict = None
|
||||||
|
) -> None:
|
||||||
|
(
|
||||||
|
content_text,
|
||||||
|
content_subject,
|
||||||
|
) = await self.__generate_message_from_subject_and_event__(
|
||||||
|
subject=subject, event=event, properties=properties
|
||||||
|
)
|
||||||
|
properties_dict = {
|
||||||
|
"content_text": content_text,
|
||||||
|
"content_subject": content_subject,
|
||||||
|
"receiver_type": "user", # or 'conversation'
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.__publish_notification__(
|
||||||
|
channel=NotificationChannel.CHAT_MESSAGE,
|
||||||
|
message=NotificationMessage(
|
||||||
|
sender_id=app_settings.SYSTEM_USER_ID,
|
||||||
|
receiver_id=receiver_id,
|
||||||
|
subject=subject,
|
||||||
|
event=event,
|
||||||
|
properties=(
|
||||||
|
{**properties_dict, **properties} if properties else properties_dict
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send_email_notification(
|
||||||
|
self,
|
||||||
|
receiver_id: str,
|
||||||
|
subject: str,
|
||||||
|
event: str,
|
||||||
|
properties: dict = None,
|
||||||
|
region: Optional[UserRegion] = None,
|
||||||
|
) -> None:
|
||||||
|
(
|
||||||
|
content_text,
|
||||||
|
content_subject,
|
||||||
|
) = await self.__generate_message_from_subject_and_event__(
|
||||||
|
subject=subject, event=event, properties=properties, region=region
|
||||||
|
)
|
||||||
|
properties_dict = {
|
||||||
|
"content_text": content_text,
|
||||||
|
"content_subject": content_subject,
|
||||||
|
"sender_email": app_settings.EMAIL_FROM,
|
||||||
|
"receiver_type": "email",
|
||||||
|
}
|
||||||
|
await self.__publish_notification__(
|
||||||
|
channel=NotificationChannel.EMAIL,
|
||||||
|
message=NotificationMessage(
|
||||||
|
sender_id=app_settings.SYSTEM_USER_ID,
|
||||||
|
receiver_id=receiver_id,
|
||||||
|
subject=subject,
|
||||||
|
event=event,
|
||||||
|
properties=(
|
||||||
|
properties_dict.update(properties)
|
||||||
|
if not properties
|
||||||
|
else properties_dict
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send_sms_notification(
|
||||||
|
self, receiver_id: str, subject: str, event: str, properties: dict = None
|
||||||
|
) -> None:
|
||||||
|
(
|
||||||
|
content_text,
|
||||||
|
content_subject,
|
||||||
|
) = await self.__generate_message_from_subject_and_event__(
|
||||||
|
subject=subject, event=event, properties=properties
|
||||||
|
)
|
||||||
|
properties_dict = {
|
||||||
|
"content_text": content_text,
|
||||||
|
"content_subject": content_subject,
|
||||||
|
"sender_mobile": app_settings.SMS_FROM,
|
||||||
|
"receiver_type": "mobile",
|
||||||
|
}
|
||||||
|
await self.__publish_notification__(
|
||||||
|
channel=NotificationChannel.SMS,
|
||||||
|
message=NotificationMessage(
|
||||||
|
sender_id=app_settings.SYSTEM_USER_ID,
|
||||||
|
receiver_id=receiver_id,
|
||||||
|
subject=subject,
|
||||||
|
event=event,
|
||||||
|
properties=(
|
||||||
|
properties_dict.update(properties)
|
||||||
|
if not properties
|
||||||
|
else properties_dict
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def enqueue_notification(
|
||||||
|
self,
|
||||||
|
channels: list[NotificationChannel],
|
||||||
|
receiver_id: str,
|
||||||
|
subject: str,
|
||||||
|
event: str,
|
||||||
|
properties: dict = None,
|
||||||
|
) -> None:
|
||||||
|
for channel in channels:
|
||||||
|
if channel == NotificationChannel.CHAT_MESSAGE:
|
||||||
|
await self.send_chat_message_notification(
|
||||||
|
receiver_id=receiver_id,
|
||||||
|
subject=subject,
|
||||||
|
event=event,
|
||||||
|
properties=properties,
|
||||||
|
)
|
||||||
|
elif channel == NotificationChannel.IN_APP:
|
||||||
|
await self.send_in_app_notification(
|
||||||
|
receiver_id=receiver_id,
|
||||||
|
subject=subject,
|
||||||
|
event=event,
|
||||||
|
properties=properties,
|
||||||
|
)
|
||||||
|
elif channel == NotificationChannel.EMAIL:
|
||||||
|
await self.send_email_notification(
|
||||||
|
receiver_id=receiver_id,
|
||||||
|
subject=subject,
|
||||||
|
event=event,
|
||||||
|
properties=properties,
|
||||||
|
)
|
||||||
|
elif channel == NotificationChannel.SMS:
|
||||||
|
await self.send_sms_notification(
|
||||||
|
receiver_id=receiver_id,
|
||||||
|
subject=subject,
|
||||||
|
event=event,
|
||||||
|
properties=properties,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Unsupported notification channel: {channel}")
|
||||||
|
|||||||
0
app/notification/backend/infra/rabbitmq/__init__.py
Normal file
0
app/notification/backend/infra/rabbitmq/__init__.py
Normal file
64
app/notification/backend/infra/rabbitmq/async_client.py
Normal file
64
app/notification/backend/infra/rabbitmq/async_client.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
from app.notification.common.config.app_settings import app_settings
|
||||||
|
from infra.log.module_logger import ModuleLogger
|
||||||
|
import asyncio
|
||||||
|
from asyncio import AbstractEventLoop
|
||||||
|
import aio_pika
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncMQClient:
|
||||||
|
exchange_name_format = "freeleaps.notification.exchange.{}"
|
||||||
|
exchange_type = "direct"
|
||||||
|
|
||||||
|
def __init__(self, channel_name: str) -> None:
|
||||||
|
self.exchange_name_format = AsyncMQClient.exchange_name_format
|
||||||
|
self.channel_name = channel_name
|
||||||
|
self.exchange_type = AsyncMQClient.exchange_type
|
||||||
|
self.exchange_name = self.exchange_name_format.format(self.channel_name)
|
||||||
|
self.process_callable = None
|
||||||
|
self.routing_key = self.channel_name
|
||||||
|
self.module_logger = ModuleLogger(sender_id="AsyncMQClient")
|
||||||
|
|
||||||
|
async def bind(self, max_retries=10, event_loop: AbstractEventLoop = None):
|
||||||
|
retry_count = 0
|
||||||
|
retry_interval = 1 # Start with a 1-second interval
|
||||||
|
|
||||||
|
while retry_count < max_retries:
|
||||||
|
try:
|
||||||
|
self.connection = await aio_pika.connect_robust(
|
||||||
|
host=app_settings.RABBITMQ_HOST,
|
||||||
|
port=int(app_settings.RABBITMQ_PORT),
|
||||||
|
loop=event_loop,
|
||||||
|
)
|
||||||
|
self.channel = await self.connection.channel()
|
||||||
|
self.exchange = await self.channel.declare_exchange(
|
||||||
|
name=self.exchange_name, type="direct", auto_delete=False
|
||||||
|
)
|
||||||
|
# Declare and bind queue if it's not set by a specific client
|
||||||
|
|
||||||
|
self.queue = await self.channel.declare_queue(
|
||||||
|
name=None, exclusive=True, auto_delete=True, durable=False
|
||||||
|
)
|
||||||
|
await self.queue.bind(
|
||||||
|
exchange=self.exchange, routing_key=self.routing_key
|
||||||
|
)
|
||||||
|
break # Exit loop once connected
|
||||||
|
except Exception as e:
|
||||||
|
await self.module_logger.log_exception(
|
||||||
|
exception=e,
|
||||||
|
text=f"Reconnection attempt {retry_count + 1}/{max_retries} failed: {e}",
|
||||||
|
)
|
||||||
|
await asyncio.sleep(retry_interval)
|
||||||
|
retry_interval = min(
|
||||||
|
retry_interval * 2, 60
|
||||||
|
) # Exponential backoff, up to 60s max
|
||||||
|
retry_count += 1
|
||||||
|
|
||||||
|
if retry_count >= max_retries:
|
||||||
|
raise ConnectionError(
|
||||||
|
"Unable to connect to RabbitMQ after multiple retries."
|
||||||
|
)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""Unbind the queue and close the connection gracefully."""
|
||||||
|
await self.queue.unbind(self.exchange, self.routing_key)
|
||||||
|
await self.connection.close()
|
||||||
50
app/notification/backend/infra/rabbitmq/async_publisher.py
Normal file
50
app/notification/backend/infra/rabbitmq/async_publisher.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from infra.log.module_logger import ModuleLogger
|
||||||
|
from .async_client import AsyncMQClient
|
||||||
|
import aio_pika
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncMQPublisher(AsyncMQClient):
|
||||||
|
def __init__(self, channel_name: str) -> None:
|
||||||
|
super().__init__(channel_name=channel_name)
|
||||||
|
self.module_logger = ModuleLogger(sender_id="AsyncMQPublisher")
|
||||||
|
|
||||||
|
async def publish(self, message: str | object, max_retries: int = 3):
|
||||||
|
retries = 0
|
||||||
|
while retries < max_retries:
|
||||||
|
try:
|
||||||
|
if not hasattr(self, "exchange") or self.exchange is None:
|
||||||
|
# Ensure the exchange is bound before publishing
|
||||||
|
await self.bind()
|
||||||
|
|
||||||
|
await self.exchange.publish(
|
||||||
|
aio_pika.Message(
|
||||||
|
bytes(
|
||||||
|
(
|
||||||
|
message
|
||||||
|
if isinstance(message, str)
|
||||||
|
else json.dumps(message)
|
||||||
|
),
|
||||||
|
"utf-8",
|
||||||
|
),
|
||||||
|
content_type="text/plain",
|
||||||
|
),
|
||||||
|
self.routing_key,
|
||||||
|
)
|
||||||
|
return # Exit after successful publish
|
||||||
|
except aio_pika.exceptions.ChannelInvalidStateError as e:
|
||||||
|
retries += 1
|
||||||
|
await self.module_logger.log_exception(
|
||||||
|
exception=e,
|
||||||
|
text=f"Attempting reconnect and retry {retries}/{max_retries} "
|
||||||
|
"for publish ran into ChannelInvalidStateError",
|
||||||
|
)
|
||||||
|
await asyncio.sleep(2) # Short delay before retrying
|
||||||
|
await self.bind() # Rebind to re-establish connection/channel
|
||||||
|
|
||||||
|
# Log final failure if retries are exhausted
|
||||||
|
await self.module_logger.log_exception(
|
||||||
|
exception=ConnectionError("Unable to publish after max retries."),
|
||||||
|
text=f"Publish failed after {max_retries} retries.",
|
||||||
|
)
|
||||||
85
app/notification/backend/infra/rabbitmq/async_subscriber.py
Normal file
85
app/notification/backend/infra/rabbitmq/async_subscriber.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
from asyncio import AbstractEventLoop
|
||||||
|
from infra.log.module_logger import ModuleLogger
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from .async_client import AsyncMQClient
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncMQSubscriber(AsyncMQClient):
|
||||||
|
def __init__(self, channel_name: str) -> None:
|
||||||
|
super().__init__(channel_name=channel_name)
|
||||||
|
self.process_callable = None
|
||||||
|
self.routing_key = self.channel_name
|
||||||
|
self.consumer_callbacks = {}
|
||||||
|
self.consumer_callbacks_lock = asyncio.Lock() # Async lock for async context
|
||||||
|
self.module_logger = ModuleLogger(sender_id="AsyncMQSubscriber")
|
||||||
|
|
||||||
|
async def process_incoming_message(self, message):
|
||||||
|
"""Processing incoming message from RabbitMQ"""
|
||||||
|
await message.ack()
|
||||||
|
body = message.body
|
||||||
|
if body:
|
||||||
|
async with self.consumer_callbacks_lock: # Use async lock for safe concurrent access
|
||||||
|
for registry_key, callback_info in self.consumer_callbacks.items():
|
||||||
|
try:
|
||||||
|
await callback_info["method"](
|
||||||
|
registry_key, json.loads(body), callback_info["args"]
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
# Log each exception that occurs within callback processing
|
||||||
|
await self.module_logger.log_exception(
|
||||||
|
exception=err,
|
||||||
|
text=f"Error processing message for consumer '{registry_key}'",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def subscribe(self, max_retries=10, event_loop: AbstractEventLoop = None):
|
||||||
|
"""Attempts to bind and consume messages, with retry mechanism."""
|
||||||
|
retries = 0
|
||||||
|
while retries < max_retries:
|
||||||
|
try:
|
||||||
|
await self.bind(max_retries=5, event_loop=event_loop)
|
||||||
|
await self.queue.consume(
|
||||||
|
no_ack=False, exclusive=True, callback=self.process_incoming_message
|
||||||
|
)
|
||||||
|
break # Exit loop if subscription is successful
|
||||||
|
except Exception as e:
|
||||||
|
await self.module_logger.log_exception(
|
||||||
|
exception=e,
|
||||||
|
text=f"Failed to subscribe at {retries} time, will retry",
|
||||||
|
)
|
||||||
|
retries += 1
|
||||||
|
await asyncio.sleep(5) # Delay before retrying
|
||||||
|
else:
|
||||||
|
await self.module_logger.log_exception(
|
||||||
|
exception=ConnectionError(
|
||||||
|
f"Exceeded max retries ({max_retries}) for subscription."
|
||||||
|
),
|
||||||
|
text=f"Subscription failed for {self.channel_name} after {max_retries} attempts.",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def register_consumer(
|
||||||
|
self,
|
||||||
|
registry_key: str, # a unique string to identify the callback
|
||||||
|
callback_method,
|
||||||
|
args: dict,
|
||||||
|
):
|
||||||
|
"""Register a consumer callback with a unique key."""
|
||||||
|
async with self.consumer_callbacks_lock:
|
||||||
|
self.consumer_callbacks[registry_key] = {
|
||||||
|
"method": callback_method,
|
||||||
|
"args": args,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def unregister_consumer(
|
||||||
|
self,
|
||||||
|
registry_key: str, # a unique string to identify the callback
|
||||||
|
):
|
||||||
|
"""Unregister a consumer callback by its key."""
|
||||||
|
async with self.consumer_callbacks_lock:
|
||||||
|
if registry_key in self.consumer_callbacks:
|
||||||
|
del self.consumer_callbacks[registry_key]
|
||||||
|
|
||||||
|
async def clear_all_consumers(self):
|
||||||
|
"""Unregister all consumer callbacks."""
|
||||||
|
async with self.consumer_callbacks_lock:
|
||||||
|
self.consumer_callbacks.clear()
|
||||||
127
app/notification/backend/models/constants.py
Normal file
127
app/notification/backend/models/constants.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from infra.models.constants import UserRegion
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationChannel(Enum):
|
||||||
|
IN_APP = 0
|
||||||
|
CHAT_MESSAGE = 1
|
||||||
|
EMAIL = 2
|
||||||
|
SMS = 3
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationMessage(BaseModel):
|
||||||
|
sender_id: str
|
||||||
|
receiver_id: str
|
||||||
|
subject: str
|
||||||
|
event: str
|
||||||
|
properties: dict
|
||||||
|
|
||||||
|
|
||||||
|
SystemNotifications = {
|
||||||
|
UserRegion.OTHER: {
|
||||||
|
"request": { # Subject
|
||||||
|
"quoted": { # Event
|
||||||
|
"message_subject": "update for your request",
|
||||||
|
"message": "There is a quote for your request",
|
||||||
|
},
|
||||||
|
"unquoted": {
|
||||||
|
"message_subject": "update for your request",
|
||||||
|
"message": "A quote for your request has been withdrawed",
|
||||||
|
},
|
||||||
|
"invite": {
|
||||||
|
"message_subject": "You are invited",
|
||||||
|
"message": "You are invited to provide proposal for a request.",
|
||||||
|
},
|
||||||
|
"chat_created": {
|
||||||
|
"message_subject": "new chat created",
|
||||||
|
"message": "This chat group is created for the project.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"quote": { # Subject
|
||||||
|
"accepted": { # Event
|
||||||
|
"message_subject": "update for your quote",
|
||||||
|
"message": "Your quote has been accepted",
|
||||||
|
},
|
||||||
|
"rejected": {
|
||||||
|
"message_subject": "update for your quote",
|
||||||
|
"message": "Your quote has been rejected",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"milestone": { # Subject
|
||||||
|
"state-change": { # Event
|
||||||
|
"message_subject": "update for your milestone",
|
||||||
|
"message": "Your project milestone has a state change.",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": { # Subject
|
||||||
|
"authentication": { # Event
|
||||||
|
"message_subject": "Freeleaps Support",
|
||||||
|
"message": "The auth code is: {} . \r\nDo not share this to anyone.",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mobile": { # Subject
|
||||||
|
"authentication": { # Event
|
||||||
|
"message_subject": "Freeleaps Support",
|
||||||
|
"message": "The auth code is: {} . \r\nDo not share this to anyone.",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message": { # Subject
|
||||||
|
"new": { # Event
|
||||||
|
"message_subject": "new messages",
|
||||||
|
"message": "There are new messages.",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UserRegion.ZH_CN: {
|
||||||
|
"request": { # Subject
|
||||||
|
"quoted": { # Event
|
||||||
|
"message_subject": "你的请求有更新",
|
||||||
|
"message": "这里有一条关于你的请求的报价",
|
||||||
|
},
|
||||||
|
"unquoted": {
|
||||||
|
"message_subject": "你的请求有更新",
|
||||||
|
"message": "一条你的请求的报价被拒绝",
|
||||||
|
},
|
||||||
|
"invite": {
|
||||||
|
"message_subject": "有人邀请你",
|
||||||
|
"message": "你被邀请为一个请求提供报价",
|
||||||
|
},
|
||||||
|
"chat_created": {
|
||||||
|
"message_subject": "创建了聊天群组",
|
||||||
|
"message": "本项目的聊天群组以及建立完成.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"quote": { # Subject
|
||||||
|
"accepted": {
|
||||||
|
"message_subject": "你的报价有更新",
|
||||||
|
"message": "你的报价已被接受",
|
||||||
|
}, # Event
|
||||||
|
"rejected": {
|
||||||
|
"message_subject": "你的报价有更新",
|
||||||
|
"message": "你的报价被拒绝",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"milestone": { # Subject
|
||||||
|
"state-change": { # Event
|
||||||
|
"message_subject": "你的里程碑有更新",
|
||||||
|
"message": "你的项目里程碑发生了状态改变",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": { # Subject
|
||||||
|
"authentication": { # Event
|
||||||
|
"message_subject": "自由跳跃技术支持",
|
||||||
|
"message": "安全认证码为: {} . \r\n切勿分享给他人",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mobile": { # Subject
|
||||||
|
"authentication": { # Event
|
||||||
|
"message_subject": "自由跳跃技术支持",
|
||||||
|
"message": "安全认证码为: {} . \r\n切勿分享给他人",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message": { # Subject
|
||||||
|
"new": {"message_subject": "新消息", "message": "你有新的会话消息"} # Event
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
from app.notification.backend.models.constants import NotificationChannel
|
||||||
|
from backend.infra.rabbitmq.async_publisher import AsyncMQPublisher
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationPublisherService:
|
||||||
|
def __init__(self, channel: NotificationChannel) -> None:
|
||||||
|
self.mq_client = AsyncMQPublisher(channel.name)
|
||||||
|
|
||||||
|
async def bind(self):
|
||||||
|
await self.mq_client.bind(max_retries=5)
|
||||||
|
|
||||||
|
async def publish(self, message: str | bytes):
|
||||||
|
await self.mq_client.publish(message=message)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
await self.mq_client.close()
|
||||||
@ -5,10 +5,12 @@ from pydantic_settings import BaseSettings
|
|||||||
class AppSettings(BaseSettings):
|
class AppSettings(BaseSettings):
|
||||||
NAME: str = "notification"
|
NAME: str = "notification"
|
||||||
|
|
||||||
AZURE_STORAGE_DOCUMENT_API_ENDPOINT: str = (
|
RABBITMQ_HOST: str = "rabbitmq"
|
||||||
"https://freeleaps1document.blob.core.windows.net/"
|
RABBITMQ_PORT: int = 5672
|
||||||
)
|
|
||||||
AZURE_STORAGE_DOCUMENT_API_KEY: str = ""
|
SYSTEM_USER_ID: str = ""
|
||||||
|
SMS_FROM: str = ""
|
||||||
|
EMAIL_FROM: str = ""
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = ".myapp.env"
|
env_file = ".myapp.env"
|
||||||
|
|||||||
50
app/notification/webapi/routes/send_notification.py
Normal file
50
app/notification/webapi/routes/send_notification.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
from app.notification.backend.application.notification_hub import NotificationHub
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from app.notification.backend.models.constants import NotificationChannel
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
# Define the request body schema
|
||||||
|
class NotificationRequest(BaseModel):
|
||||||
|
sender_id: str
|
||||||
|
channels: list[str]
|
||||||
|
receiver_id: str
|
||||||
|
subject: str
|
||||||
|
event: str
|
||||||
|
properties: Dict
|
||||||
|
|
||||||
|
|
||||||
|
# Web API
|
||||||
|
# Send notification to user for a certain channel
|
||||||
|
@router.post(
|
||||||
|
"/enqueue_notification",
|
||||||
|
operation_id="enqueue_notification",
|
||||||
|
summary="Send notification to user for a certain channel",
|
||||||
|
description="Send notification to user for a channel (e.g., sms, email, in-app, etc.)",
|
||||||
|
response_description="Success/failure response in processing the notification send request",
|
||||||
|
)
|
||||||
|
# API route to enqueue notifications
|
||||||
|
@router.post("/enqueue_notification")
|
||||||
|
async def enqueue_notification(request: NotificationRequest):
|
||||||
|
try:
|
||||||
|
notification_hub = NotificationHub()
|
||||||
|
await notification_hub.enqueue_notification(
|
||||||
|
sender_id=request.sender_id, # Pass sender_id
|
||||||
|
channels=request.channels,
|
||||||
|
receiver_id=request.receiver_id,
|
||||||
|
subject=request.subject,
|
||||||
|
event=request.event,
|
||||||
|
properties=request.properties,
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
content={"message": "Notifications queued successfully."}, status_code=200
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to enqueue notification")
|
||||||
@ -1,17 +1,17 @@
|
|||||||
services:
|
services:
|
||||||
rabbitmq:
|
# rabbitmq:
|
||||||
image: rabbitmq:3-management
|
# image: rabbitmq:3-management
|
||||||
ports:
|
# ports:
|
||||||
- "5672:5672" # RabbitMQ communication port
|
# - "5672:5672" # RabbitMQ communication port
|
||||||
- "15672:15672" # RabbitMQ management port
|
# - "15672:15672" # RabbitMQ management port
|
||||||
networks:
|
# networks:
|
||||||
- freeleaps_service_hub_network
|
# - freeleaps_service_hub_network
|
||||||
healthcheck:
|
# healthcheck:
|
||||||
test: [ "CMD", "rabbitmq-diagnostics", "ping" ]
|
# test: [ "CMD", "rabbitmq-diagnostics", "ping" ]
|
||||||
interval: 30s
|
# interval: 30s
|
||||||
retries: 5
|
# retries: 5
|
||||||
start_period: 10s
|
# start_period: 10s
|
||||||
timeout: 10s
|
# timeout: 10s
|
||||||
|
|
||||||
central_storage:
|
central_storage:
|
||||||
build:
|
build:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user