ci / api / api (push) Canceled after 0s
ci / module boundaries / boundaries (push) Canceled after 0s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Canceled after 11m29s
ci / db / db (push) Canceled after 11m22s
ci / mobile / mobile (Metro-бандл Android) (push) Canceled after 6m43s
ci / recommender / recommender (push) Canceled after 6m47s
ci / ts / ts (packages + web) (push) Canceled after 1s
194 lines
5.8 KiB
Go
194 lines
5.8 KiB
Go
package social
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"pipu/api/internal/api/gen"
|
|
"pipu/api/internal/platform/apperr"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
func validEmoji(s string) bool {
|
|
switch s {
|
|
case "❤️", "👍", "🔥", "😂", "🎵", "👏":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
func (h *Handler) GetChatMeta(ctx context.Context, r gen.GetChatMetaRequestObject) (gen.GetChatMetaResponseObject, error) {
|
|
id, e := userID(ctx)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
tx, e := pairTx(ctx, h.store.pool, id, r.PeerId)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
if e = allowed(ctx, tx, id, r.PeerId); e != nil {
|
|
return nil, e
|
|
}
|
|
var typing bool
|
|
e = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM chat_typing WHERE user_id=$1 AND peer_id=$2 AND expires_at>now())`, r.PeerId, id).Scan(&typing)
|
|
return gen.GetChatMeta200JSONResponse{Typing: typing}, e
|
|
}
|
|
func (h *Handler) SetChatTyping(ctx context.Context, r gen.SetChatTypingRequestObject) (gen.SetChatTypingResponseObject, error) {
|
|
id, e := userID(ctx)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
if r.Body == nil {
|
|
return nil, invalid()
|
|
}
|
|
tx, e := pairTx(ctx, h.store.pool, id, r.PeerId)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
if e = allowed(ctx, tx, id, r.PeerId); e != nil {
|
|
return nil, e
|
|
}
|
|
if r.Body.Typing {
|
|
_, e = tx.Exec(ctx, `INSERT INTO chat_typing(user_id,peer_id,expires_at) VALUES($1,$2,now()+interval '6 seconds') ON CONFLICT(user_id,peer_id) DO UPDATE SET expires_at=EXCLUDED.expires_at`, id, r.PeerId)
|
|
} else {
|
|
_, e = tx.Exec(ctx, `DELETE FROM chat_typing WHERE user_id=$1 AND peer_id=$2`, id, r.PeerId)
|
|
}
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
return gen.SetChatTyping204Response{}, tx.Commit(ctx)
|
|
}
|
|
func (h *Handler) UpdateChatMessage(ctx context.Context, r gen.UpdateChatMessageRequestObject) (gen.UpdateChatMessageResponseObject, error) {
|
|
id, e := userID(ctx)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
if r.Body == nil {
|
|
return nil, invalid()
|
|
}
|
|
e = h.store.updateMessage(ctx, id, r.PeerId, r.MessageId, *r.Body)
|
|
return gen.UpdateChatMessage204Response{}, e
|
|
}
|
|
func (s *Store) updateMessage(ctx context.Context, id, peer, message uuid.UUID, a gen.UpdateChatMessageJSONRequestBody) error {
|
|
tx, e := pairTx(ctx, s.pool, id, peer)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
if e = allowed(ctx, tx, id, peer); e != nil {
|
|
return e
|
|
}
|
|
var sender uuid.UUID
|
|
var kind string
|
|
var deleted bool
|
|
e = tx.QueryRow(ctx, `SELECT sender_id,attachment_kind,deleted_at IS NOT NULL FROM direct_messages WHERE id=$1 AND LEAST(sender_id,recipient_id)=LEAST($2::uuid,$3::uuid) AND GREATEST(sender_id,recipient_id)=GREATEST($2::uuid,$3::uuid) FOR UPDATE`, message, id, peer).Scan(&sender, &kind, &deleted)
|
|
if errors.Is(e, pgx.ErrNoRows) {
|
|
return apperr.NotFound("social.message_conflict", "message unavailable")
|
|
}
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if a.Action != "react" && sender != id {
|
|
return denied()
|
|
}
|
|
if deleted {
|
|
if a.Action == "delete" {
|
|
return tx.Commit(ctx)
|
|
}
|
|
return apperr.Conflict("social.message_conflict", "message deleted")
|
|
}
|
|
switch a.Action {
|
|
case "edit":
|
|
if a.Text == nil || utf8.RuneCountInString(*a.Text) > 4000 || (kind == "none" && strings.TrimSpace(*a.Text) == "") {
|
|
return invalid()
|
|
}
|
|
_, e = tx.Exec(ctx, `UPDATE direct_messages SET text=$2,edited_at=now() WHERE id=$1`, message, strings.TrimSpace(*a.Text))
|
|
case "delete":
|
|
_, e = tx.Exec(ctx, `UPDATE direct_messages SET text='[deleted]',attachment_kind='none',attachment_id=NULL,deleted_at=now() WHERE id=$1`, message)
|
|
if e == nil {
|
|
_, e = tx.Exec(ctx, `DELETE FROM message_reactions WHERE message_id=$1`, message)
|
|
}
|
|
if e == nil {
|
|
_, e = tx.Exec(ctx, `UPDATE social_notifications SET read_at=COALESCE(read_at,now()) WHERE message_seq=(SELECT seq FROM direct_messages WHERE id=$1)`, message)
|
|
}
|
|
case "react":
|
|
if a.Emoji == nil || !validEmoji(string(*a.Emoji)) {
|
|
return invalid()
|
|
}
|
|
result, err := tx.Exec(ctx, `DELETE FROM message_reactions WHERE message_id=$1 AND user_id=$2 AND emoji=$3`, message, id, *a.Emoji)
|
|
e = err
|
|
if e == nil && result.RowsAffected() == 0 {
|
|
_, e = tx.Exec(ctx, `INSERT INTO message_reactions(message_id,user_id,emoji) VALUES($1,$2,$3)`, message, id, *a.Emoji)
|
|
}
|
|
default:
|
|
return invalid()
|
|
}
|
|
if e != nil {
|
|
return e
|
|
}
|
|
return tx.Commit(ctx)
|
|
}
|
|
func enrichMessages(ctx context.Context, tx pgx.Tx, viewer uuid.UUID, messages []gen.DirectMessage) error {
|
|
if len(messages) == 0 {
|
|
return nil
|
|
}
|
|
ids := make([]uuid.UUID, 0, len(messages))
|
|
replies := []uuid.UUID{}
|
|
positions := map[uuid.UUID]int{}
|
|
for i, m := range messages {
|
|
ids = append(ids, m.Id)
|
|
positions[m.Id] = i
|
|
if m.ReplyTo != nil {
|
|
replies = append(replies, *m.ReplyTo)
|
|
}
|
|
empty := []gen.MessageReaction{}
|
|
messages[i].Reactions = &empty
|
|
}
|
|
rows, e := tx.Query(ctx, `SELECT message_id,emoji,count(*),bool_or(user_id=$2) FROM message_reactions WHERE message_id=ANY($1::uuid[]) GROUP BY message_id,emoji ORDER BY emoji`, ids, viewer)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
for rows.Next() {
|
|
var id uuid.UUID
|
|
var r gen.MessageReaction
|
|
if e = rows.Scan(&id, &r.Emoji, &r.Count, &r.Mine); e != nil {
|
|
rows.Close()
|
|
return e
|
|
}
|
|
i := positions[id]
|
|
*messages[i].Reactions = append(*messages[i].Reactions, r)
|
|
}
|
|
e = rows.Err()
|
|
rows.Close()
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if len(replies) == 0 {
|
|
return nil
|
|
}
|
|
rows, e = tx.Query(ctx, `SELECT id,CASE WHEN deleted_at IS NULL THEN left(text,160) ELSE '' END FROM direct_messages WHERE id=ANY($1::uuid[])`, replies)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
defer rows.Close()
|
|
previews := map[uuid.UUID]string{}
|
|
for rows.Next() {
|
|
var id uuid.UUID
|
|
var text string
|
|
if e = rows.Scan(&id, &text); e != nil {
|
|
return e
|
|
}
|
|
previews[id] = text
|
|
}
|
|
for i, m := range messages {
|
|
if m.ReplyTo != nil {
|
|
p := previews[*m.ReplyTo]
|
|
messages[i].ReplyPreview = &p
|
|
}
|
|
}
|
|
return rows.Err()
|
|
}
|