Skip to content

Commit 9f0dd16

Browse files
committed
refactor: add standardize data class
1 parent 733a669 commit 9f0dd16

6 files changed

Lines changed: 127 additions & 86 deletions

File tree

raven/omni_channel_chat/doctype/omni_channel_chat_provider/provider/base_provider.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from werkzeug.wrappers import Response
55

6+
from raven.omni_channel_chat.models.messages import StdMessage
67
from raven.omni_channel_chat.omni_channel_raven_connector import OmniChannelRavenConnector
78

89
if TYPE_CHECKING:
@@ -18,7 +19,7 @@ def __init__(self, config: "OmniChannelChatProvider"):
1819
self.provider_config = config
1920
self.provider_config.decode_password_field()
2021

21-
def push_message_to_raven(self, messages: list[dict]) -> None:
22+
def push_message_to_raven(self, messages: list[StdMessage]) -> None:
2223
handler = OmniChannelRavenConnector(provider=self)
2324
for message in messages:
2425
handler.receive_from_provider(message)
@@ -49,13 +50,13 @@ def send_message(self, user_id: str, message: dict) -> None:
4950
"""Send an outbound message (push, not reply)."""
5051

5152
@abstractmethod
52-
def event_mapper(self, event: ProviderWebhookEvent) -> dict | None:
53-
"""Map a provider-specific webhook event into a standardized event. Return None to skip."""
53+
def event_mapper(self, event: ProviderWebhookEvent) -> StdMessage | None:
54+
"""Map a provider-specific webhook event into a standardized message. Return None to skip."""
5455

5556
@abstractmethod
56-
def standardize_events(self, events: list[ProviderWebhookEvent]) -> list[dict]:
57-
"""Standardize a list of provider-specific webhook events into standardized events."""
57+
def standardize_events(self, events: list[ProviderWebhookEvent]) -> list[StdMessage]:
58+
"""Standardize a list of provider-specific webhook events into StdMessage instances."""
5859

5960
@abstractmethod
60-
def extract_messages(self, body: bytes, headers: dict) -> list[dict]:
61+
def extract_messages(self, body: bytes, headers: dict) -> list[StdMessage]:
6162
"""Parse the raw webhook body into standardized messages."""

raven/omni_channel_chat/doctype/omni_channel_chat_provider/provider/facebook_provider.py

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
from raven.omni_channel_chat.doctype.omni_channel_chat_provider.provider import (
1111
Provider,
1212
)
13+
from raven.omni_channel_chat.models.messages import (
14+
FileMessage,
15+
ImageMessage,
16+
StdMessage,
17+
TextMessage,
18+
)
1319

1420
# A "messaging event" dict from the Facebook webhook payload
1521
FacebookMessagingEvent = dict[str, Any]
@@ -111,20 +117,17 @@ def _download_attachment(self, url: str, default_name: str) -> tuple[bytes, str]
111117
file_name = content_disposition.split("filename=")[-1].strip('" ')
112118
return response.content, file_name
113119

114-
def event_mapper(self, event: FacebookMessagingEvent) -> dict | None:
120+
def event_mapper(self, event: FacebookMessagingEvent) -> StdMessage | None:
115121
message = event.get("message")
116122
if message is None:
117123
return None
118124

119125
mid = message.get("mid")
126+
user_id = event["sender"]["id"]
127+
metadata = {"mid": mid}
120128

121129
if "text" in message:
122-
return {
123-
"provider": self.provider_config.provider,
124-
"user_id": event["sender"]["id"],
125-
"message": {"type": "Text", "text": message["text"]},
126-
"message_metadata": {"mid": mid},
127-
}
130+
return TextMessage(user_id=user_id, metadata=metadata, text=message["text"])
128131

129132
for attachment in message.get("attachments") or []:
130133
att_type = attachment.get("type")
@@ -133,32 +136,26 @@ def event_mapper(self, event: FacebookMessagingEvent) -> dict | None:
133136
continue
134137
if att_type == "image":
135138
content, file_name = self._download_attachment(url, f"{mid or 'image'}.jpg")
136-
return {
137-
"provider": self.provider_config.provider,
138-
"user_id": event["sender"]["id"],
139-
"message": {"type": "Image", "file_name": file_name, "file_content": content},
140-
"message_metadata": {"mid": mid},
141-
}
139+
return ImageMessage(
140+
user_id=user_id, metadata=metadata, file_name=file_name, file_content=content
141+
)
142142
if att_type in ("file", "document"):
143143
content, file_name = self._download_attachment(url, mid or "file")
144-
return {
145-
"provider": self.provider_config.provider,
146-
"user_id": event["sender"]["id"],
147-
"message": {"type": "File", "file_name": file_name, "file_content": content},
148-
"message_metadata": {"mid": mid},
149-
}
144+
return FileMessage(
145+
user_id=user_id, metadata=metadata, file_name=file_name, file_content=content
146+
)
150147

151148
return None
152149

153-
def standardize_events(self, events: list[FacebookMessagingEvent]) -> list[dict]:
154-
std_events: list[dict] = []
150+
def standardize_events(self, events: list[FacebookMessagingEvent]) -> list[StdMessage]:
151+
std_events: list[StdMessage] = []
155152
for event in events:
156153
std_event = self.event_mapper(event)
157154
if std_event:
158155
std_events.append(std_event)
159156
return std_events
160157

161-
def extract_messages(self, body: bytes, headers: dict) -> list[dict]:
158+
def extract_messages(self, body: bytes, headers: dict) -> list[StdMessage]:
162159
signature = headers.get("X-Hub-Signature-256", "") or headers.get(
163160
"x-hub-signature-256", ""
164161
)

raven/omni_channel_chat/doctype/omni_channel_chat_provider/provider/instagram_provider.py

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
from raven.omni_channel_chat.doctype.omni_channel_chat_provider.provider import (
1111
Provider,
1212
)
13+
from raven.omni_channel_chat.models.messages import (
14+
FileMessage,
15+
ImageMessage,
16+
StdMessage,
17+
TextMessage,
18+
)
1319

1420
InstagramMessagingEvent = dict[str, Any]
1521

@@ -110,20 +116,17 @@ def _download_attachment(self, url: str, default_name: str) -> tuple[bytes, str]
110116
file_name = content_disposition.split("filename=")[-1].strip('" ')
111117
return response.content, file_name
112118

113-
def event_mapper(self, event: InstagramMessagingEvent) -> dict | None:
119+
def event_mapper(self, event: InstagramMessagingEvent) -> StdMessage | None:
114120
message = event.get("message")
115121
if message is None:
116122
return None
117123

118124
mid = message.get("mid")
125+
user_id = event["sender"]["id"]
126+
metadata = {"mid": mid}
119127

120128
if "text" in message:
121-
return {
122-
"provider": self.provider_config.provider,
123-
"user_id": event["sender"]["id"],
124-
"message": {"type": "Text", "text": message["text"]},
125-
"message_metadata": {"mid": mid},
126-
}
129+
return TextMessage(user_id=user_id, metadata=metadata, text=message["text"])
127130

128131
for attachment in message.get("attachments") or []:
129132
att_type = attachment.get("type")
@@ -132,32 +135,26 @@ def event_mapper(self, event: InstagramMessagingEvent) -> dict | None:
132135
continue
133136
if att_type == "image":
134137
content, file_name = self._download_attachment(url, f"{mid or 'image'}.jpg")
135-
return {
136-
"provider": self.provider_config.provider,
137-
"user_id": event["sender"]["id"],
138-
"message": {"type": "Image", "file_name": file_name, "file_content": content},
139-
"message_metadata": {"mid": mid},
140-
}
138+
return ImageMessage(
139+
user_id=user_id, metadata=metadata, file_name=file_name, file_content=content
140+
)
141141
if att_type in ("file", "video", "audio"):
142142
content, file_name = self._download_attachment(url, mid or "file")
143-
return {
144-
"provider": self.provider_config.provider,
145-
"user_id": event["sender"]["id"],
146-
"message": {"type": "File", "file_name": file_name, "file_content": content},
147-
"message_metadata": {"mid": mid},
148-
}
143+
return FileMessage(
144+
user_id=user_id, metadata=metadata, file_name=file_name, file_content=content
145+
)
149146

150147
return None
151148

152-
def standardize_events(self, events: list[InstagramMessagingEvent]) -> list[dict]:
153-
std_events: list[dict] = []
149+
def standardize_events(self, events: list[InstagramMessagingEvent]) -> list[StdMessage]:
150+
std_events: list[StdMessage] = []
154151
for event in events:
155152
std_event = self.event_mapper(event)
156153
if std_event:
157154
std_events.append(std_event)
158155
return std_events
159156

160-
def extract_messages(self, body: bytes, headers: dict) -> list[dict]:
157+
def extract_messages(self, body: bytes, headers: dict) -> list[StdMessage]:
161158
signature = headers.get("X-Hub-Signature-256", "") or headers.get(
162159
"x-hub-signature-256", ""
163160
)

raven/omni_channel_chat/doctype/omni_channel_chat_provider/provider/line_provider.py

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@
2323
from linebot.v3.webhooks import FileMessageContent, ImageMessageContent, TextMessageContent
2424
from linebot.v3.webhooks import MessageEvent as LineMessageEvent
2525

26+
from raven.omni_channel_chat.models.messages import (
27+
FileMessage,
28+
ImageMessage,
29+
StdMessage,
30+
TextMessage as StdTextMessage,
31+
)
32+
2633
from . import Provider
2734

2835

@@ -121,56 +128,44 @@ def _download_line_content(self, message_id: str) -> bytes:
121128
with ApiClient(self.config) as api_client:
122129
return bytes(MessagingApiBlob(api_client).get_message_content(message_id))
123130

124-
def event_mapper(self, event: LineEvent) -> dict | None:
131+
def event_mapper(self, event: LineEvent) -> StdMessage | None:
125132
if not isinstance(event, LineMessageEvent):
126133
return None
127134

128135
msg = event.message
129136
metadata = {"message_id": msg.id, "reply_token": event.reply_token}
137+
user_id = event.source.user_id
130138

131139
if isinstance(msg, TextMessageContent):
132-
return {
133-
"provider": self.provider_config.provider,
134-
"user_id": event.source.user_id,
135-
"message": {"type": "Text", "text": msg.text},
136-
"message_metadata": metadata,
137-
}
140+
return StdTextMessage(user_id=user_id, metadata=metadata, text=msg.text)
138141

139142
if isinstance(msg, ImageMessageContent):
140-
return {
141-
"provider": self.provider_config.provider,
142-
"user_id": event.source.user_id,
143-
"message": {
144-
"type": "Image",
145-
"file_name": f"{msg.id}.jpg",
146-
"file_content": self._download_line_content(msg.id),
147-
},
148-
"message_metadata": metadata,
149-
}
143+
return ImageMessage(
144+
user_id=user_id,
145+
metadata=metadata,
146+
file_name=f"{msg.id}.jpg",
147+
file_content=self._download_line_content(msg.id),
148+
)
150149

151150
if isinstance(msg, FileMessageContent):
152-
return {
153-
"provider": self.provider_config.provider,
154-
"user_id": event.source.user_id,
155-
"message": {
156-
"type": "File",
157-
"file_name": msg.file_name,
158-
"file_content": self._download_line_content(msg.id),
159-
},
160-
"message_metadata": metadata,
161-
}
151+
return FileMessage(
152+
user_id=user_id,
153+
metadata=metadata,
154+
file_name=msg.file_name,
155+
file_content=self._download_line_content(msg.id),
156+
)
162157

163158
return None
164159

165-
def standardize_events(self, events: list[LineEvent]) -> list[dict]:
166-
std_events: list[dict] = []
160+
def standardize_events(self, events: list[LineEvent]) -> list[StdMessage]:
161+
std_events: list[StdMessage] = []
167162
for event in events:
168163
std_event = self.event_mapper(event)
169164
if std_event:
170165
std_events.append(std_event)
171166
return std_events
172167

173-
def extract_messages(self, body: bytes, headers: dict) -> list[dict]:
168+
def extract_messages(self, body: bytes, headers: dict) -> list[StdMessage]:
174169
signature = headers.get("x-line-signature", "") or headers.get("X-Line-Signature", "")
175170
try:
176171
events = self.parser.parse(body=body.decode(), signature=signature, as_payload=False)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from abc import ABC, abstractmethod
2+
from dataclasses import dataclass
3+
4+
5+
@dataclass
6+
class StdMessage(ABC):
7+
user_id: str
8+
metadata: dict
9+
10+
@abstractmethod
11+
def to_raven(self) -> dict:
12+
pass
13+
14+
15+
@dataclass
16+
class TextMessage(StdMessage):
17+
text: str
18+
19+
def to_raven(self):
20+
return {
21+
"type": "Text",
22+
"text": self.text,
23+
}
24+
25+
26+
@dataclass
27+
class FileMessage(StdMessage):
28+
file_name: str
29+
file_content: bytes
30+
31+
def to_raven(self):
32+
return {
33+
"type": "File",
34+
"file_name": self.file_name,
35+
"file_content": self.file_content,
36+
}
37+
38+
39+
@dataclass
40+
class ImageMessage(StdMessage):
41+
file_name: str
42+
file_content: bytes
43+
44+
def to_raven(self):
45+
return {
46+
"type": "Image",
47+
"file_name": self.file_name,
48+
"file_content": self.file_content,
49+
}

raven/omni_channel_chat/omni_channel_raven_connector.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import frappe
44
from frappe.utils import get_url
55

6+
from raven.omni_channel_chat.models.messages import StdMessage
7+
68
if TYPE_CHECKING:
79
from frappe.core.doctype.user.user import User
810

@@ -151,33 +153,33 @@ def handle_webhook(self, body: bytes, headers: dict) -> None:
151153
for message in messages:
152154
self.receive_from_provider(message)
153155

154-
def receive_from_provider(self, message: dict) -> "RavenChannel":
156+
def receive_from_provider(self, message: StdMessage) -> "RavenChannel":
155157
"""Inbound: turn a provider webhook payload into a Raven message.
156158
157159
Creates the Frappe user, Raven user, and channel on first contact,
158160
then appends the message to the channel.
159161
160162
Returns the Raven channel the message was posted to.
161163
"""
162-
user = self._get_or_create_customer_user(user_id=message["user_id"])
164+
user = self._get_or_create_customer_user(user_id=message.user_id)
163165
frappe.set_user(user.name)
164166

165-
raven_user = self._get_or_create_raven_user(user=user, user_id=message["user_id"])
167+
raven_user = self._get_or_create_raven_user(user=user, user_id=message.user_id)
166168
raven_channel = self._get_or_create_channel(raven_user=raven_user)
167169
self._save_inbound_message(raven_channel=raven_channel, message=message)
168170

169171
return raven_channel
170172

171-
def _save_inbound_message(self, *, raven_channel: "RavenChannel", message: dict) -> None:
172-
msg = message["message"]
173+
def _save_inbound_message(self, *, raven_channel: "RavenChannel", message: StdMessage) -> None:
174+
msg = message.to_raven()
173175
doc = frappe.new_doc(doctype="Raven Message")
174176
doc.update(
175177
{
176178
"channel_id": raven_channel.name,
177179
"message_type": msg["type"],
178180
"is_customer_message": True,
179181
"owner": raven_channel.customer_user,
180-
"omni_channel_msg_meta": message.get("message_metadata"),
182+
"omni_channel_msg_meta": message.metadata,
181183
}
182184
)
183185
if msg["type"] == "Text":

0 commit comments

Comments
 (0)