package db import ( "database/sql" "encoding/json" "fmt" "sort" "strings" "time" "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" "tangled.org/core/appview/models" "tangled.org/core/orm" ) func PutComment(tx *sql.Tx, c *models.Comment, references []syntax.ATURI) (bool, error) { var bodyBlobs, replyToUri, replyToCid *string if len(c.Body.Blobs) > 0 { encoded, err := json.Marshal(c.Body.Blobs) if err != nil { return false, fmt.Errorf("encoding blobs to json: %w", err) } encodedStr := string(encoded) bodyBlobs = &encodedStr } if c.ReplyTo != nil { replyToUri = &c.ReplyTo.Uri replyToCid = &c.ReplyTo.Cid } result, err := tx.Exec( // users can change the 'created' date. // skip update entirely if cid is unchanged. `insert into comments ( did, collection, rkey, cid, subject_uri, subject_cid, body_text, body_original, body_blobs, created, reply_to_uri, reply_to_cid, pull_round_idx ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) on conflict(did, collection, rkey) do update set cid = excluded.cid, subject_uri = excluded.subject_uri, subject_cid = excluded.subject_cid, body_text = excluded.body_text, body_original = excluded.body_original, body_blobs = excluded.body_blobs, created = excluded.created, reply_to_uri = excluded.reply_to_uri, reply_to_cid = excluded.reply_to_cid, pull_round_idx = excluded.pull_round_idx, edited = ? where comments.cid is not excluded.cid`, c.Did, c.Collection, c.Rkey, c.Cid, c.Subject.Uri, c.Subject.Cid, c.Body.Text, c.Body.Original, bodyBlobs, c.Created.Format(time.RFC3339), replyToUri, replyToCid, c.PullRoundIdx, time.Now().Format(time.RFC3339), ) if err != nil { return false, err } c.Id, err = result.LastInsertId() if err != nil { return false, err } affected, err := result.RowsAffected() if err != nil { return false, err } if affected < 1 { return false, nil } // update references when comment is updated if err := putReferences(tx, c.AtUri(), references); err != nil { return false, fmt.Errorf("put reference_links: %w", err) } return true, nil } // PurgeComments actually purges a comment row from db instead of marking it as "deleted" func PurgeComments(e Execer, filters ...orm.Filter) error { var conditions []string var args []any for _, filter := range filters { conditions = append(conditions, filter.Condition()) args = append(args, filter.Arg()...) } whereClause := "" if conditions != nil { whereClause = " where " + strings.Join(conditions, " and ") } _, err := e.Exec(fmt.Sprintf(`delete from comments %s`, whereClause), args...) return err } func DeleteComments(e Execer, filters ...orm.Filter) error { var conditions []string var args []any for _, filter := range filters { conditions = append(conditions, filter.Condition()) args = append(args, filter.Arg()...) } whereClause := "" if conditions != nil { whereClause = " where " + strings.Join(conditions, " and ") } query := fmt.Sprintf( `update comments set body_text = "", body_original = null, body_blobs = null, deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause, ) _, err := e.Exec(query, args...) return err } func GetComment(e Execer, filters ...orm.Filter) (models.Comment, error) { comments, err := GetComments(e, filters...) if err != nil { return models.Comment{}, err } if len(comments) != 1 { return models.Comment{}, fmt.Errorf("expected 1 comment, got %d", len(comments)) } return comments[0], nil } func GetComments(e Execer, filters ...orm.Filter) ([]models.Comment, error) { var comments []models.Comment var conditions []string var args []any for _, filter := range filters { conditions = append(conditions, filter.Condition()) args = append(args, filter.Arg()...) } whereClause := "" if conditions != nil { whereClause = " where " + strings.Join(conditions, " and ") } query := fmt.Sprintf(` select id, did, collection, rkey, cid, subject_uri, subject_cid, body_text, body_original, body_blobs, created, reply_to_uri, reply_to_cid, pull_round_idx, edited, deleted from comments %s `, whereClause) rows, err := e.Query(query, args...) if err != nil { return nil, err } defer rows.Close() for rows.Next() { var comment models.Comment var created string var cid, bodyBlobs, replyToUri, replyToCid, edited, deleted sql.Null[string] err := rows.Scan( &comment.Id, &comment.Did, &comment.Collection, &comment.Rkey, &cid, &comment.Subject.Uri, &comment.Subject.Cid, &comment.Body.Text, &comment.Body.Original, &bodyBlobs, &created, &replyToUri, &replyToCid, &comment.PullRoundIdx, &edited, &deleted, ) if err != nil { return nil, err } if cid.Valid && cid.V != "" { comment.Cid = syntax.CID(cid.V) } if bodyBlobs.Valid && bodyBlobs.V != "" { if err := json.Unmarshal([]byte(bodyBlobs.V), &comment.Body.Blobs); err != nil { return nil, fmt.Errorf("decoding blobs: %w", err) } } if t, err := time.Parse(time.RFC3339, created); err == nil { comment.Created = t } if replyToUri.Valid && replyToCid.Valid { comment.ReplyTo = &atproto.RepoStrongRef{ Uri: replyToUri.V, Cid: replyToCid.V, } } if edited.Valid { if t, err := time.Parse(time.RFC3339, edited.V); err == nil { comment.Edited = &t } } if deleted.Valid { if t, err := time.Parse(time.RFC3339, deleted.V); err == nil { comment.Deleted = &t } } comments = append(comments, comment) } if err := rows.Err(); err != nil { return nil, err } sort.Slice(comments, func(i, j int) bool { return comments[i].Created.Before(comments[j].Created) }) return comments, nil }