fix: new incoming messages having 0 attachments in API responses, upload attachments first before sending out websocket updates.

- refactor: stringutils package remove unncessary `string` in function names.
- minor refactors.
This commit is contained in:
Abhinav Raut
2025-02-08 18:04:02 +05:30
parent 37070770bf
commit 356a206110
13 changed files with 52 additions and 44 deletions

View File

@@ -26,7 +26,7 @@ func handleOIDCLogin(r *fastglue.Request) error {
}
// Set a state and save it in the session, to prevent CSRF attacks.
state, err := stringutil.RandomAlNumString(32)
state, err := stringutil.RandomAlphanumeric(32)
if err != nil {
app.lo.Error("error generating state", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error generating state.", nil, envelope.GeneralError)

View File

@@ -133,7 +133,7 @@ func handleMediaUpload(r *fastglue.Request) error {
}
// Insert in DB.
media, err := app.media.Insert(disposition, srcFileName, srcContentType, "" /**content_id**/, linkedModel, uuid.String(), 0, int(srcFileSize), meta)
media, err := app.media.Insert(disposition, srcFileName, srcContentType, "" /**content_id**/, null.NewString(linkedModel, linkedModel != ""), uuid.String(), null.Int{} /**model_id**/, int(srcFileSize), meta)
if err != nil {
cleanUp = true
app.lo.Error("error inserting metadata into database", "error", err)

View File

@@ -51,7 +51,7 @@ func handleGetMessages(r *fastglue.Request) error {
for j := range messages[i].Attachments {
messages[i].Attachments[j].URL = app.media.GetURL(messages[i].Attachments[j].UUID)
}
messages[i].HideCSAT()
messages[i].CensorCSATContent()
}
return r.SendEnvelope(envelope.PageResults{
Total: total,
@@ -86,7 +86,8 @@ func handleGetMessage(r *fastglue.Request) error {
return sendErrorEnvelope(r, err)
}
message.HideCSAT()
// Redact CSAT survey link
message.CensorCSATContent()
for j := range message.Attachments {
message.Attachments[j].URL = app.media.GetURL(message.Attachments[j].UUID)
@@ -145,14 +146,14 @@ func handleSendMessage(r *fastglue.Request) error {
if err := r.Decode(&req, "json"); err != nil {
app.lo.Error("error unmarshalling message request", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "error decoding request", nil, "")
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "error decoding request", nil, envelope.GeneralError)
}
for _, id := range req.Attachments {
m, err := app.media.Get(id)
if err != nil {
app.lo.Error("error fetching media", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error fetching media", nil, "")
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error fetching media", nil, envelope.GeneralError)
}
media = append(media, m)
}

View File

@@ -142,7 +142,12 @@ func handleUpdateCurrentUser(r *fastglue.Request) error {
// Reset ptr.
file.Seek(0, 0)
media, err := app.media.UploadAndInsert(srcFileName, srcContentType, "" /**content_id**/, mmodels.ModelUser, user.ID, file, int(srcFileSize), null.NewString("", false) /**disposition**/, []byte("{}") /**meta**/)
linkedModel := null.StringFrom(mmodels.ModelUser)
linkedID := null.IntFrom(user.ID)
disposition := null.NewString("", false)
contentID := ""
meta := []byte("{}")
media, err := app.media.UploadAndInsert(srcFileName, srcContentType, contentID, linkedModel, linkedID, file, int(srcFileSize), disposition, meta)
if err != nil {
app.lo.Error("error uploading file", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error uploading file", nil, envelope.GeneralError)

View File

@@ -329,7 +329,7 @@ func (a *Auth) DestroySession(r *fastglue.Request) error {
// generateCSRFToken creates a random base64 encoded str.
func generateCSRFToken() (string, error) {
b, err := stringutil.RandomAlNumString(30)
b, err := stringutil.RandomAlphanumeric(32)
if err != nil {
return "", err
}

View File

@@ -107,7 +107,7 @@ type mediaStore interface {
GetByModel(id int, model string) ([]mmodels.Media, error)
ContentIDExists(contentID string) (bool, error)
Upload(fileName, contentType string, content io.ReadSeeker) (string, error)
UploadAndInsert(fileName, contentType, contentID, modelType string, modelID int, content io.ReadSeeker, fileSize int, disposition null.String, meta []byte) (mmodels.Media, error)
UploadAndInsert(fileName, contentType, contentID string, modelType null.String, modelID null.Int, content io.ReadSeeker, fileSize int, disposition null.String, meta []byte) (mmodels.Media, error)
}
type inboxStore interface {

View File

@@ -294,11 +294,10 @@ func (m *Manager) SendPrivateNote(media []mmodels.Media, senderID int, conversat
// SendReply inserts a reply message in a conversation.
func (m *Manager) SendReply(media []mmodels.Media, senderID int, conversationUUID, content string, cc, bcc []string, meta map[string]interface{}) error {
// Strip empty strings bc and cc.
cc = stringutil.RemoveEmptyStrings(cc)
bcc = stringutil.RemoveEmptyStrings(bcc)
cc = stringutil.RemoveEmpty(cc)
bcc = stringutil.RemoveEmpty(bcc)
// Set cc and bcc in meta.
// Save cc and bcc as JSON in meta.
if len(cc) > 0 {
meta["cc"] = cc
}
@@ -331,11 +330,12 @@ func (m *Manager) InsertMessage(message *models.Message) error {
message.Status = MessageStatusSent
}
// Handle empty meta.
if message.Meta == "" || message.Meta == "null" {
message.Meta = "{}"
}
// Generate text content.
// Convert HTML content to text for search.
message.TextContent = stringutil.HTML2Text(message.Content)
// Insert Message.
@@ -355,7 +355,7 @@ func (m *Manager) InsertMessage(message *models.Message) error {
return err
}
// Update conversation last message details in conversation.
// Update conversation last message details in conversation metadata.
m.UpdateConversationLastMessage(message.ConversationID, message.ConversationUUID, message.TextContent, message.CreatedAt)
// Broadcast new message.
@@ -494,17 +494,17 @@ func (m *Manager) processIncomingMessage(in models.IncomingMessage) error {
return err
}
// Insert message.
if err = m.InsertMessage(&in.Message); err != nil {
return err
}
// Upload message attachments.
if err := m.uploadMessageAttachments(&in.Message); err != nil {
// Log error but continue processing.
m.lo.Error("error uploading message attachments", "message_source_id", in.Message.SourceID, "error", err)
}
// Insert message.
if err = m.InsertMessage(&in.Message); err != nil {
return err
}
// Evaluate automation rules for this conversation.
if isNewConversation {
m.automation.EvaluateNewConversationRules(in.Message.ConversationUUID)
@@ -615,17 +615,18 @@ func (m *Manager) uploadMessageAttachments(message *models.Message) []error {
// Sanitize filename and upload.
attachment.Name = stringutil.SanitizeFilename(attachment.Name)
reader := bytes.NewReader(attachment.Content)
attachReader := bytes.NewReader(attachment.Content)
media, err := m.mediaStore.UploadAndInsert(
attachment.Name,
attachment.ContentType,
attachment.ContentID,
mmodels.ModelMessages,
message.ID,
reader,
/** Linking media to message happens later **/
null.String{}, /** modelType */
null.Int{}, /** modelID **/
attachReader,
attachment.Size,
null.StringFrom(attachment.Disposition),
[]byte("{}"),
[]byte("{}"), /** meta **/
)
if err != nil {
uploadErr = append(uploadErr, err)
@@ -640,6 +641,7 @@ func (m *Manager) uploadMessageAttachments(message *models.Message) []error {
m.lo.Error("error uploading thumbnail", "error", err)
}
}
message.Media = append(message.Media, media)
}
return uploadErr
}

View File

@@ -121,8 +121,8 @@ type Message struct {
Total int `db:"total" json:"-"`
}
// HideCSAT hides the CSAT message.
func (m *Message) HideCSAT() {
// CensorCSATContent redacts the content of a CSAT message to prevent leaking the CSAT survey public link.
func (m *Message) CensorCSATContent() {
var meta map[string]interface{}
if err := json.Unmarshal([]byte(m.Meta), &meta); err != nil {
return

View File

@@ -73,7 +73,7 @@ type queries struct {
}
// UploadAndInsert uploads file on storage and inserts an entry in db.
func (m *Manager) UploadAndInsert(srcFilename, contentType, contentID, modelType string, modelID int, content io.ReadSeeker, fileSize int, disposition null.String, meta []byte) (models.Media, error) {
func (m *Manager) UploadAndInsert(srcFilename, contentType, contentID string, modelType null.String, modelID null.Int, content io.ReadSeeker, fileSize int, disposition null.String, meta []byte) (models.Media, error) {
var uuid = uuid.New()
_, err := m.Upload(uuid.String(), contentType, content)
if err != nil {
@@ -99,7 +99,7 @@ func (m *Manager) Upload(fileName, contentType string, content io.ReadSeeker) (s
}
// Insert inserts media details into the database and returns the inserted media record.
func (m *Manager) Insert(disposition null.String, fileName, contentType, contentID, modelType, uuid string, modelID int, fileSize int, meta []byte) (models.Media, error) {
func (m *Manager) Insert(disposition null.String, fileName, contentType, contentID string, modelType null.String, uuid string, modelID null.Int, fileSize int, meta []byte) (models.Media, error) {
var id int
if err := m.queries.Insert.QueryRow(m.store.Name(), fileName, contentType, fileSize, meta, modelID, modelType, disposition, contentID, uuid).Scan(&id); err != nil {
m.lo.Error("error inserting media", "error", err)
@@ -155,8 +155,8 @@ func (m *Manager) GetURL(name string) string {
// Attach associates a media file with a specific model by its ID and model name.
func (m *Manager) Attach(id int, model string, modelID int) error {
if _, err := m.queries.Attach.Exec(id, model, modelID); err != nil {
m.lo.Error("error attaching media to model", "model", model, "model_id", modelID, "error", err)
return err
m.lo.Error("error attaching media to model", "model", model, "model_id", modelID, "media_id", id, "error", err)
return fmt.Errorf("attaching media;%d to model:%s model_id:%d: %w", id, model, modelID, err)
}
return nil
}

View File

@@ -41,8 +41,8 @@ func SanitizeFilename(fName string) string {
return filepath.Base(name)
}
// RandomAlNumString generates a random alphanumeric string of length n.
func RandomAlNumString(n int) (string, error) {
// RandomAlphanumeric generates a random alphanumeric string of length n.
func RandomAlphanumeric(n int) (string, error) {
const dictionary = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
bytes := make([]byte, n)
@@ -58,8 +58,8 @@ func RandomAlNumString(n int) (string, error) {
return string(bytes), nil
}
// RandomNumericString generates a random numeric string of length n.
func RandomNumericString(n int) (string, error) {
// RandomNumeric generates a random numeric string of length n.
func RandomNumeric(n int) (string, error) {
const dictionary = "0123456789"
bytes := make([]byte, n)
@@ -84,8 +84,8 @@ func GetPathFromURL(u string) (string, error) {
return parsedURL.Path, nil
}
// RemoveEmptyStrings removes empty strings from a slice of strings.
func RemoveEmptyStrings(s []string) []string {
// RemoveEmpty removes empty strings from a slice of strings.
func RemoveEmpty(s []string) []string {
var r []string
for _, str := range s {
if str != "" {
@@ -93,4 +93,4 @@ func RemoveEmptyStrings(s []string) []string {
}
}
return r
}
}

View File

@@ -13,7 +13,7 @@ const (
// Built-in templates names stored in the database.
TmplConversationAssigned = "Conversation assigned"
// Built-in templates in fetched from memory stored in `static` directory.
// Built-in templates fetched from memory stored in `static` directory.
TmplResetPassword = "reset-password"
TmplWelcome = "welcome"
@@ -29,7 +29,7 @@ func (m *Manager) RenderWithBaseTemplate(data any, content string) (string, erro
defaultTmpl, err := m.getDefaultOutgoingEmailTemplate()
if err != nil {
if err == ErrTemplateNotFound {
m.lo.Warn("default outgoing email template not found, rendering content without base")
m.lo.Warn("default outgoing email template not found, rendering content any template")
return content, nil
}
return "", err
@@ -95,7 +95,7 @@ func (m *Manager) RenderNamedTemplate(name string, data any) (string, string, er
defaultTmpl, err := m.getDefaultOutgoingEmailTemplate()
if err != nil {
if err == ErrTemplateNotFound {
m.lo.Warn("default outgoing email template not found, rendering content without base")
m.lo.Warn("default outgoing email template not found, rendering content any template")
content, err := executeContentTemplate(tmpl.Body)
if err != nil {
return "", "", err

View File

@@ -250,7 +250,7 @@ func (u *Manager) GetEmail(id int) (string, error) {
// SetResetPasswordToken sets a reset password token for a user and returns the token.
func (u *Manager) SetResetPasswordToken(id int) (string, error) {
token, err := stringutil.RandomAlNumString(32)
token, err := stringutil.RandomAlphanumeric(32)
if err != nil {
u.lo.Error("error generating reset password token", "error", err)
return "", envelope.NewError(envelope.GeneralError, "Error generating reset password token", nil)
@@ -300,7 +300,7 @@ func (u *Manager) verifyPassword(pwd []byte, pwdHash string) error {
// generatePassword generates a random password and returns its bcrypt hash.
func (u *Manager) generatePassword() ([]byte, error) {
password, _ := stringutil.RandomAlNumString(70)
password, _ := stringutil.RandomAlphanumeric(70)
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
u.lo.Error("error generating bcrypt password", "error", err)

View File

@@ -304,10 +304,10 @@ CREATE TABLE media (
store "media_store" NOT NULL,
filename TEXT NOT NULL,
content_type TEXT NOT NULL,
content_id TEXT NULL,
model_id INT NULL,
model_type TEXT NULL,
disposition media_disposition NULL,
content_id TEXT NULL,
"size" INT NULL,
meta jsonb DEFAULT '{}'::jsonb NOT NULL,
CONSTRAINT constraint_media_on_filename CHECK (length(filename) <= 1000),