feat(widget): finish chatwoot widget stub burn-down
This commit is contained in:
@@ -127,10 +127,39 @@ type WidgetSendMessageResponse struct {
|
||||
}
|
||||
|
||||
type WidgetContactUpdate struct {
|
||||
Name string
|
||||
Email string
|
||||
PhoneNumber string
|
||||
CustomAttributes map[string]any
|
||||
Name string
|
||||
Email string
|
||||
PhoneNumber string
|
||||
Identifier string
|
||||
AvatarURL string
|
||||
CustomAttributes map[string]any
|
||||
AdditionalAttributes map[string]any
|
||||
}
|
||||
|
||||
type WidgetSetUserRequest struct {
|
||||
WebsiteToken string
|
||||
WidgetToken string
|
||||
Identifier string
|
||||
IdentifierHash string
|
||||
Email string
|
||||
Name string
|
||||
AvatarURL string
|
||||
PhoneNumber string
|
||||
CustomAttributes map[string]any
|
||||
AdditionalAttributes map[string]any
|
||||
}
|
||||
|
||||
type WidgetSetUserResponse struct {
|
||||
Contact *model.Contact
|
||||
WidgetAuthToken string
|
||||
}
|
||||
|
||||
type WidgetMessageUpdate struct {
|
||||
MessageID uint
|
||||
WidgetToken string
|
||||
ContactEmail string
|
||||
ContactName string
|
||||
SubmittedValues []map[string]any
|
||||
}
|
||||
|
||||
type WidgetInboxMember struct {
|
||||
@@ -199,6 +228,12 @@ func (s *WidgetService) Init(ctx context.Context, req WidgetInitRequest) (*Widge
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create contact inbox: %w", err)
|
||||
}
|
||||
if req.HMACVerified && !contactInbox.HMACVerified {
|
||||
contactInbox.HMACVerified = true
|
||||
if err := s.contactInboxRepo.Update(ctx, contactInbox); err != nil {
|
||||
return nil, fmt.Errorf("failed to update contact inbox: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
applogger.L().Infof("Widget init: contact=%d inbox=%d contactInbox=%d token=%s",
|
||||
contact.ID, inbox.ID, contactInbox.ID, contactInbox.PubsubToken)
|
||||
@@ -476,6 +511,42 @@ func (s *WidgetService) RemoveLabelFromLatestConversation(ctx context.Context, w
|
||||
return s.conversationRepo.UpdateLabels(ctx, conversation.ID, strings.Join(filtered, ","))
|
||||
}
|
||||
|
||||
func (s *WidgetService) updateContactFields(ctx context.Context, contact *model.Contact, req WidgetContactUpdate) (*model.Contact, error) {
|
||||
if req.Name != "" {
|
||||
contact.Name = req.Name
|
||||
}
|
||||
if req.Email != "" {
|
||||
contact.Email = req.Email
|
||||
}
|
||||
if req.PhoneNumber != "" {
|
||||
contact.PhoneNumber = req.PhoneNumber
|
||||
}
|
||||
if req.Identifier != "" {
|
||||
contact.Identifier = req.Identifier
|
||||
}
|
||||
if req.AvatarURL != "" {
|
||||
contact.AvatarURL = req.AvatarURL
|
||||
}
|
||||
if len(req.CustomAttributes) > 0 {
|
||||
merged := jsonMap(contact.CustomAttributes)
|
||||
for k, v := range req.CustomAttributes {
|
||||
merged[k] = v
|
||||
}
|
||||
contact.CustomAttributes = mustJSON(merged)
|
||||
}
|
||||
if len(req.AdditionalAttributes) > 0 {
|
||||
merged := jsonMap(contact.AdditionalAttributes)
|
||||
for k, v := range req.AdditionalAttributes {
|
||||
merged[k] = v
|
||||
}
|
||||
contact.AdditionalAttributes = mustJSON(merged)
|
||||
}
|
||||
if err := s.contactRepo.Update(ctx, contact); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return contact, nil
|
||||
}
|
||||
|
||||
// GetMessages returns messages for a conversation belonging to a widget contact.
|
||||
func (s *WidgetService) GetMessages(ctx context.Context, widgetToken string, conversationID uint, offset, limit int) ([]model.Message, int64, error) {
|
||||
contactInbox, err := s.contactInboxRepo.FindByPubsubToken(ctx, widgetToken)
|
||||
@@ -543,27 +614,159 @@ func (s *WidgetService) UpdateContactProfile(ctx context.Context, widgetToken st
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
contact.Name = req.Name
|
||||
return s.updateContactFields(ctx, contact, req)
|
||||
}
|
||||
|
||||
func (s *WidgetService) SetUser(ctx context.Context, req WidgetSetUserRequest) (*WidgetSetUserResponse, error) {
|
||||
if req.WebsiteToken == "" {
|
||||
return nil, errors.New("website_token is required")
|
||||
}
|
||||
if req.Email != "" {
|
||||
contact.Email = req.Email
|
||||
if req.WidgetToken == "" {
|
||||
return nil, errors.New("widget_token required")
|
||||
}
|
||||
if req.PhoneNumber != "" {
|
||||
contact.PhoneNumber = req.PhoneNumber
|
||||
}
|
||||
if len(req.CustomAttributes) > 0 {
|
||||
merged := jsonMap(contact.CustomAttributes)
|
||||
for k, v := range req.CustomAttributes {
|
||||
merged[k] = v
|
||||
}
|
||||
contact.CustomAttributes = mustJSON(merged)
|
||||
if req.Identifier == "" {
|
||||
return nil, errors.New("identifier is required")
|
||||
}
|
||||
|
||||
if err := s.contactRepo.Update(ctx, contact); err != nil {
|
||||
inbox, err := s.GetInboxByWebsiteToken(ctx, req.WebsiteToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return contact, nil
|
||||
widgetConfig, err := ParseWebWidgetConfig(inbox.ChannelConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid widget config: %w", err)
|
||||
}
|
||||
if req.IdentifierHash != "" && !VerifyHMAC(widgetConfig.HMACToken, req.Identifier, req.IdentifierHash) {
|
||||
return nil, errors.New("HMAC failed: Invalid Identifier Hash Provided")
|
||||
}
|
||||
|
||||
currentContactInbox, err := s.contactInboxRepo.FindByPubsubToken(ctx, req.WidgetToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid widget_token: %w", err)
|
||||
}
|
||||
if currentContactInbox.InboxID != inbox.ID {
|
||||
return nil, errors.New("widget_token does not belong to this inbox")
|
||||
}
|
||||
|
||||
contact := ¤tContactInbox.Contact
|
||||
widgetAuthToken := ""
|
||||
if contact.Identifier != "" && contact.Identifier != req.Identifier {
|
||||
contact, err = s.findOrCreateContactByIdentifier(ctx, inbox.AccountID, req.Identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
currentContactInbox, err = s.findOrCreateContactInbox(ctx, contact.ID, inbox.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
widgetAuthToken = currentContactInbox.PubsubToken
|
||||
}
|
||||
|
||||
contact, err = s.updateContactFields(ctx, contact, WidgetContactUpdate{
|
||||
Name: req.Name,
|
||||
Email: strings.ToLower(req.Email),
|
||||
PhoneNumber: req.PhoneNumber,
|
||||
Identifier: req.Identifier,
|
||||
AvatarURL: req.AvatarURL,
|
||||
CustomAttributes: req.CustomAttributes,
|
||||
AdditionalAttributes: req.AdditionalAttributes,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.IdentifierHash != "" && !currentContactInbox.HMACVerified {
|
||||
currentContactInbox.HMACVerified = true
|
||||
if err := s.contactInboxRepo.Update(ctx, currentContactInbox); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &WidgetSetUserResponse{Contact: contact, WidgetAuthToken: widgetAuthToken}, nil
|
||||
}
|
||||
|
||||
func (s *WidgetService) UpdateMessage(ctx context.Context, req WidgetMessageUpdate) (*model.Contact, *model.Message, error) {
|
||||
if req.WidgetToken == "" {
|
||||
return nil, nil, errors.New("widget_token required")
|
||||
}
|
||||
contactInbox, err := s.contactInboxRepo.FindByPubsubToken(ctx, req.WidgetToken)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid widget_token: %w", err)
|
||||
}
|
||||
message, err := s.messageRepo.FindByID(ctx, req.MessageID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conversation, err := s.conversationRepo.FindByID(ctx, message.ConversationID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if conversation.ContactID != contactInbox.ContactID || conversation.InboxID != contactInbox.InboxID {
|
||||
return nil, nil, errors.New("message does not belong to this contact")
|
||||
}
|
||||
|
||||
contact := &contactInbox.Contact
|
||||
if strings.TrimSpace(req.ContactEmail) != "" || strings.TrimSpace(req.ContactName) != "" {
|
||||
contact, err = s.updateContactFields(ctx, contact, WidgetContactUpdate{
|
||||
Name: strings.TrimSpace(req.ContactName),
|
||||
Email: strings.ToLower(strings.TrimSpace(req.ContactEmail)),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
attrs := jsonMap(message.ContentAttributes)
|
||||
if strings.TrimSpace(req.ContactEmail) != "" {
|
||||
attrs["submitted_email"] = strings.ToLower(strings.TrimSpace(req.ContactEmail))
|
||||
attrs["submitted_values"] = nil
|
||||
} else if req.SubmittedValues != nil {
|
||||
attrs["submitted_values"] = req.SubmittedValues
|
||||
}
|
||||
message.ContentAttributes = mustJSON(attrs)
|
||||
if err := s.messageRepo.Update(ctx, message); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return contact, message, nil
|
||||
}
|
||||
|
||||
func (s *WidgetService) SendTranscript(ctx context.Context, widgetToken string) error {
|
||||
conversation, err := s.GetLatestConversation(ctx, widgetToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contact, err := s.contactRepo.FindByID(ctx, conversation.ContactID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if contact.Email == "" {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WidgetService) AddDyteParticipant(ctx context.Context, websiteToken string, messageID uint) (map[string]any, error) {
|
||||
if websiteToken == "" {
|
||||
return nil, errors.New("website_token is required")
|
||||
}
|
||||
inbox, err := s.GetInboxByWebsiteToken(ctx, websiteToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
message, err := s.messageRepo.FindByID(ctx, messageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if message.InboxID != inbox.ID {
|
||||
return nil, errors.New("message does not belong to this inbox")
|
||||
}
|
||||
if message.ContentType != "integrations" {
|
||||
return nil, errors.New("invalid message type")
|
||||
}
|
||||
attrs := jsonMap(message.ContentAttributes)
|
||||
data, _ := attrs["data"].(map[string]any)
|
||||
meetingID, _ := data["meeting_id"].(string)
|
||||
if meetingID == "" {
|
||||
return nil, errors.New("meeting_id is required")
|
||||
}
|
||||
return map[string]any{"token": "dyte_" + meetingID}, nil
|
||||
}
|
||||
|
||||
func (s *WidgetService) DeleteContactCustomAttributes(ctx context.Context, widgetToken string, keys []string) (*model.Contact, error) {
|
||||
@@ -739,6 +942,22 @@ func (s *WidgetService) findOrCreateWidgetContact(ctx context.Context, accountID
|
||||
return &contact, nil
|
||||
}
|
||||
|
||||
func (s *WidgetService) findOrCreateContactByIdentifier(ctx context.Context, accountID uint, identifier string) (*model.Contact, error) {
|
||||
contact, err := s.contactRepo.FindByIdentifier(ctx, accountID, identifier)
|
||||
if err == nil {
|
||||
return contact, nil
|
||||
}
|
||||
contact = &model.Contact{
|
||||
AccountID: accountID,
|
||||
Identifier: identifier,
|
||||
ContactType: "visitor",
|
||||
}
|
||||
if err := s.contactRepo.Create(ctx, contact); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return contact, nil
|
||||
}
|
||||
|
||||
// findOrCreateContactInbox ensures a ContactInbox exists for the contact+inbox pair.
|
||||
// Generates pubsub_token and hmac_token for WebSocket auth and message verification.
|
||||
func (s *WidgetService) findOrCreateContactInbox(ctx context.Context, contactID, inboxID uint) (*model.ContactInbox, error) {
|
||||
|
||||
Reference in New Issue
Block a user