feat(public): implement chatwoot public inbox flow
This commit is contained in:
@@ -735,6 +735,202 @@ func (h *WidgetHandler) AddDyteParticipantToMeeting(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) PublicInboxShow(c *gin.Context) {
|
||||
inbox, identityValidation, err := h.widgetService.PublicGetInbox(c.Request.Context(), c.Param("inbox_id"))
|
||||
if err != nil {
|
||||
c.JSON(widgetErrorStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
payload := publicInboxPayload(inbox)
|
||||
payload["identifier"] = c.Param("inbox_id")
|
||||
payload["identity_validation_enabled"] = identityValidation
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) PublicCreateContact(c *gin.Context) {
|
||||
req, err := bindPublicContactRequest(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
resp, err := h.widgetService.PublicCreateContact(c.Request.Context(), c.Param("inbox_id"), req)
|
||||
if err != nil {
|
||||
c.JSON(widgetErrorStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, publicContactPayload(resp.ContactInbox, resp.Contact))
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) PublicGetContact(c *gin.Context) {
|
||||
resp, err := h.widgetService.PublicGetContact(c.Request.Context(), c.Param("inbox_id"), c.Param("contact_id"))
|
||||
if err != nil {
|
||||
c.JSON(widgetErrorStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, publicContactPayload(resp.ContactInbox, resp.Contact))
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) PublicUpdateContact(c *gin.Context) {
|
||||
req, err := bindPublicContactRequest(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
resp, err := h.widgetService.PublicUpdateContact(c.Request.Context(), c.Param("inbox_id"), c.Param("contact_id"), req)
|
||||
if err != nil {
|
||||
c.JSON(widgetErrorStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, publicContactPayload(resp.ContactInbox, resp.Contact))
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) PublicListConversations(c *gin.Context) {
|
||||
conversations, err := h.widgetService.PublicListConversations(c.Request.Context(), c.Param("inbox_id"), c.Param("contact_id"))
|
||||
if err != nil {
|
||||
c.JSON(widgetErrorStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
payload := make([]gin.H, 0, len(conversations))
|
||||
for _, conversation := range conversations {
|
||||
payload = append(payload, publicConversationPayload(conversation, nil))
|
||||
}
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) PublicCreateConversation(c *gin.Context) {
|
||||
var req struct {
|
||||
CustomAttributes map[string]any `json:"custom_attributes"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil && c.Request.ContentLength != 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
conversation, err := h.widgetService.PublicCreateConversation(c.Request.Context(), c.Param("inbox_id"), c.Param("contact_id"), service.PublicConversationRequest{CustomAttributes: req.CustomAttributes})
|
||||
if err != nil {
|
||||
c.JSON(widgetErrorStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, publicConversationPayload(*conversation, nil))
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) PublicGetConversation(c *gin.Context) {
|
||||
conversationID, ok := publicUintParam(c, "conversation_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
conversation, err := h.widgetService.PublicGetConversation(c.Request.Context(), c.Param("inbox_id"), c.Param("contact_id"), conversationID)
|
||||
if err != nil {
|
||||
c.JSON(widgetErrorStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
messages, _, _, _ := h.widgetService.PublicListMessages(c.Request.Context(), c.Param("inbox_id"), c.Param("contact_id"), conversationID, 0, 100)
|
||||
c.JSON(http.StatusOK, publicConversationPayload(*conversation, messages))
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) PublicToggleStatus(c *gin.Context) {
|
||||
conversationID, ok := publicUintParam(c, "conversation_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
conversation, err := h.widgetService.PublicToggleStatus(c.Request.Context(), c.Param("inbox_id"), c.Param("contact_id"), conversationID)
|
||||
if err != nil {
|
||||
c.JSON(widgetErrorStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, publicConversationPayload(*conversation, nil))
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) PublicToggleTyping(c *gin.Context) {
|
||||
conversationID, ok := publicUintParam(c, "conversation_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
TypingStatus string `json:"typing_status"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
if req.TypingStatus != "on" && req.TypingStatus != "off" {
|
||||
c.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
if err := h.widgetService.PublicToggleTyping(c.Request.Context(), c.Param("inbox_id"), c.Param("contact_id"), conversationID, req.TypingStatus != "off"); err != nil {
|
||||
c.JSON(widgetErrorStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) PublicUpdateLastSeen(c *gin.Context) {
|
||||
conversationID, ok := publicUintParam(c, "conversation_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := h.widgetService.PublicUpdateLastSeen(c.Request.Context(), c.Param("inbox_id"), c.Param("contact_id"), conversationID); err != nil {
|
||||
c.JSON(widgetErrorStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) PublicListMessages(c *gin.Context) {
|
||||
conversationID, ok := publicUintParam(c, "conversation_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "25"))
|
||||
messages, _, conversation, err := h.widgetService.PublicListMessages(c.Request.Context(), c.Param("inbox_id"), c.Param("contact_id"), conversationID, offset, limit)
|
||||
if err != nil {
|
||||
c.JSON(widgetErrorStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
payload := make([]gin.H, 0, len(messages))
|
||||
for _, message := range messages {
|
||||
payload = append(payload, publicMessagePayload(message, *conversation))
|
||||
}
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) PublicCreateMessage(c *gin.Context) {
|
||||
conversationID, ok := publicUintParam(c, "conversation_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
req, err := bindPublicMessageRequest(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
message, conversation, err := h.widgetService.PublicCreateMessage(c.Request.Context(), c.Param("inbox_id"), c.Param("contact_id"), conversationID, req)
|
||||
if err != nil {
|
||||
c.JSON(widgetErrorStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, publicMessagePayload(*message, *conversation))
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) PublicUpdateMessage(c *gin.Context) {
|
||||
conversationID, ok := publicUintParam(c, "conversation_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageID, ok := publicUintParam(c, "message_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
req, err := bindPublicMessageRequest(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
message, conversation, err := h.widgetService.PublicUpdateMessage(c.Request.Context(), c.Param("inbox_id"), c.Param("contact_id"), conversationID, messageID, req)
|
||||
if err != nil {
|
||||
c.JSON(widgetErrorStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, publicMessagePayload(*message, *conversation))
|
||||
}
|
||||
|
||||
// SubmitOfflineMessage handles a visitor submitting a message when agents are offline.
|
||||
// POST /widget/offline_message
|
||||
// Reference: Chatwoot widget SDK — when business_hours are disabled and no agents online,
|
||||
@@ -863,6 +1059,137 @@ func widgetContactFullPayload(contact *model.Contact) gin.H {
|
||||
}
|
||||
}
|
||||
|
||||
func bindPublicContactRequest(c *gin.Context) (service.PublicContactRequest, error) {
|
||||
var body struct {
|
||||
SourceID string `json:"source_id"`
|
||||
Identifier string `json:"identifier"`
|
||||
IdentifierHash string `json:"identifier_hash"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
PhoneNumber string `json:"phone_number"`
|
||||
CustomAttributes map[string]any `json:"custom_attributes"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil && c.Request.ContentLength != 0 {
|
||||
return service.PublicContactRequest{}, err
|
||||
}
|
||||
if body.SourceID == "" {
|
||||
body.SourceID = c.Query("source_id")
|
||||
}
|
||||
if body.Identifier == "" {
|
||||
body.Identifier = c.Query("identifier")
|
||||
}
|
||||
if body.IdentifierHash == "" {
|
||||
body.IdentifierHash = c.Query("identifier_hash")
|
||||
}
|
||||
return service.PublicContactRequest{
|
||||
SourceID: body.SourceID,
|
||||
Identifier: body.Identifier,
|
||||
IdentifierHash: body.IdentifierHash,
|
||||
Email: body.Email,
|
||||
Name: body.Name,
|
||||
AvatarURL: body.AvatarURL,
|
||||
PhoneNumber: body.PhoneNumber,
|
||||
CustomAttributes: body.CustomAttributes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func bindPublicMessageRequest(c *gin.Context) (service.PublicMessageRequest, error) {
|
||||
var body struct {
|
||||
Content string `json:"content"`
|
||||
EchoID string `json:"echo_id"`
|
||||
SubmittedValues []map[string]any `json:"submitted_values"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
return service.PublicMessageRequest{}, err
|
||||
}
|
||||
return service.PublicMessageRequest{
|
||||
Content: body.Content,
|
||||
EchoID: body.EchoID,
|
||||
SubmittedValues: body.SubmittedValues,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func publicInboxPayload(inbox *model.Inbox) gin.H {
|
||||
workingHours := map[string]any{}
|
||||
return gin.H{
|
||||
"name": inbox.Name,
|
||||
"timezone": inbox.Timezone,
|
||||
"working_hours": workingHours,
|
||||
"working_hours_enabled": inbox.WorkingHoursEnabled,
|
||||
"csat_survey_enabled": inbox.CsatSurveyEnabled,
|
||||
"greeting_enabled": inbox.GreetingEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
func publicContactPayload(contactInbox *model.ContactInbox, contact *model.Contact) gin.H {
|
||||
return gin.H{
|
||||
"source_id": contactInbox.SourceID,
|
||||
"pubsub_token": contactInbox.PubsubToken,
|
||||
"id": contact.ID,
|
||||
"name": contact.Name,
|
||||
"email": contact.Email,
|
||||
"phone_number": contact.PhoneNumber,
|
||||
}
|
||||
}
|
||||
|
||||
func publicConversationPayload(conversation model.Conversation, messages []model.Message) gin.H {
|
||||
payload := gin.H{
|
||||
"id": publicDisplayID(conversation),
|
||||
"uuid": conversation.UUID,
|
||||
"inbox_id": conversation.InboxID,
|
||||
"contact_last_seen_at": publicUnix(conversation.ContactLastSeenAt),
|
||||
"status": conversation.Status,
|
||||
"agent_last_seen_at": publicUnix(conversation.AgentLastSeenAt),
|
||||
"contact": gin.H{"id": conversation.ContactID},
|
||||
}
|
||||
messagePayloads := make([]gin.H, 0, len(messages))
|
||||
for _, message := range messages {
|
||||
messagePayloads = append(messagePayloads, publicMessagePayload(message, conversation))
|
||||
}
|
||||
payload["messages"] = messagePayloads
|
||||
return payload
|
||||
}
|
||||
|
||||
func publicMessagePayload(message model.Message, conversation model.Conversation) gin.H {
|
||||
payload := gin.H{
|
||||
"id": message.ID,
|
||||
"content": message.Content,
|
||||
"message_type": message.MessageType,
|
||||
"content_type": message.ContentType,
|
||||
"content_attributes": message.ContentAttributes,
|
||||
"created_at": message.CreatedAt.Unix(),
|
||||
"conversation_id": publicDisplayID(conversation),
|
||||
}
|
||||
if message.SenderID != nil {
|
||||
payload["sender"] = gin.H{"id": *message.SenderID, "type": message.SenderType}
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func publicUintParam(c *gin.Context, name string) (uint, bool) {
|
||||
value, err := strconv.ParseUint(c.Param(name), 10, 64)
|
||||
if err != nil || value == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": name + " must be a positive integer"})
|
||||
return 0, false
|
||||
}
|
||||
return uint(value), true
|
||||
}
|
||||
|
||||
func publicDisplayID(conversation model.Conversation) uint {
|
||||
if conversation.DisplayID != nil && *conversation.DisplayID != 0 {
|
||||
return *conversation.DisplayID
|
||||
}
|
||||
return conversation.ID
|
||||
}
|
||||
|
||||
func publicUnix(value *int64) int64 {
|
||||
if value == nil {
|
||||
return 0
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func widgetErrorStatus(err error) int {
|
||||
msg := err.Error()
|
||||
if strings.Contains(msg, "invalid widget_token") || strings.Contains(msg, "HMAC failed") {
|
||||
|
||||
Reference in New Issue
Block a user