added media fixes, search, push updates, pwa, metrics. preparing release 1.0
CI / Go (build + vet + test) (push) Canceled after 0s
CI / Web (build + lint) (push) Canceled after 0s

This commit is contained in:
vantablackTea
2026-09-27 23:26:15 +03:00
parent 855a62755c
commit 6fc6d977bf
59 changed files with 6823 additions and 113 deletions
+55
View File
@@ -181,4 +181,59 @@ func TestIntegration(t *testing.T) {
if err != nil || !belongs {
t.Fatalf("MessageBelongsToConversation = %v, %v; want true", belongs, err)
}
// Search
hits, err := db.SearchMessages(ctx, u1.ID, "hell", 10)
if err != nil {
t.Fatalf("SearchMessages: %v", err)
}
if len(hits) != 1 || hits[0].ID != msg.ID {
t.Fatalf("SearchMessages = %+v, want single hit for message %d", hits, msg.ID)
}
if hits[0].ConversationType != "dm" {
t.Fatalf("SearchMessages conversation type = %q, want dm", hits[0].ConversationType)
}
if hits, _ := db.SearchMessages(ctx, u1.ID, "", 10); hits != nil {
t.Fatalf("SearchMessages empty query = %v, want nil", hits)
}
if hits, _ := db.SearchMessages(ctx, u1.ID, "zzz-not-found", 10); len(hits) != 0 {
t.Fatalf("SearchMessages no-match = %d hits, want 0", len(hits))
}
// Push subscriptions
sub, err := db.CreatePushSubscription(ctx, PushSubscription{
UserID: u1.ID,
Endpoint: "https://push.example/abc",
P256dh: "p256dh-key",
Auth: "auth-key",
})
if err != nil {
t.Fatalf("CreatePushSubscription: %v", err)
}
sub2, err := db.CreatePushSubscription(ctx, PushSubscription{
UserID: u1.ID,
Endpoint: "https://push.example/abc",
P256dh: "p256dh-key2",
Auth: "auth-key2",
})
if err != nil {
t.Fatalf("CreatePushSubscription upsert: %v", err)
}
if sub2.ID != sub.ID {
t.Fatalf("upsert created a new row: %d vs %d", sub2.ID, sub.ID)
}
subs, err := db.ListPushSubscriptionsForUser(ctx, u1.ID)
if err != nil || len(subs) != 1 {
t.Fatalf("ListPushSubscriptionsForUser = %v, %v; want 1", subs, err)
}
if subs[0].P256dh != "p256dh-key2" {
t.Fatalf("upsert did not update keys: %q", subs[0].P256dh)
}
if err := db.DeletePushSubscription(ctx, u1.ID, sub.Endpoint); err != nil {
t.Fatalf("DeletePushSubscription: %v", err)
}
subs, _ = db.ListPushSubscriptionsForUser(ctx, u1.ID)
if len(subs) != 0 {
t.Fatalf("after delete = %d subs, want 0", len(subs))
}
}
@@ -0,0 +1,10 @@
-- 0005_push.sql
CREATE TABLE IF NOT EXISTS push_subscriptions (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
endpoint TEXT NOT NULL UNIQUE,
p256dh TEXT NOT NULL,
auth TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_push_user ON push_subscriptions(user_id);
+61
View File
@@ -0,0 +1,61 @@
package store
import (
"context"
"time"
)
// PushSubscription is a browser Web Push endpoint registered by a user.
type PushSubscription struct {
ID int64
UserID int64
Endpoint string
P256dh string
Auth string
CreatedAt time.Time
}
// CreatePushSubscription stores a subscription, replacing any previous one
// bound to the same endpoint.
func (s *Store) CreatePushSubscription(ctx context.Context, sub PushSubscription) (*PushSubscription, error) {
err := s.pool.QueryRow(ctx, `
INSERT INTO push_subscriptions(user_id, endpoint, p256dh, auth)
VALUES($1, $2, $3, $4)
ON CONFLICT (endpoint) DO UPDATE SET user_id = EXCLUDED.user_id, p256dh = EXCLUDED.p256dh, auth = EXCLUDED.auth
RETURNING id, created_at
`, sub.UserID, sub.Endpoint, sub.P256dh, sub.Auth).Scan(&sub.ID, &sub.CreatedAt)
if err != nil {
return nil, err
}
return &sub, nil
}
// DeletePushSubscription removes a subscription by endpoint and owner.
func (s *Store) DeletePushSubscription(ctx context.Context, userID int64, endpoint string) error {
_, err := s.pool.Exec(ctx, `
DELETE FROM push_subscriptions WHERE user_id=$1 AND endpoint=$2
`, userID, endpoint)
return err
}
// ListPushSubscriptionsForUser returns all endpoints registered by a user.
func (s *Store) ListPushSubscriptionsForUser(ctx context.Context, userID int64) ([]PushSubscription, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, user_id, endpoint, p256dh, auth, created_at
FROM push_subscriptions WHERE user_id=$1
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var subs []PushSubscription
for rows.Next() {
var sub PushSubscription
if err := rows.Scan(&sub.ID, &sub.UserID, &sub.Endpoint, &sub.P256dh, &sub.Auth, &sub.CreatedAt); err != nil {
return nil, err
}
subs = append(subs, sub)
}
return subs, nil
}
+50
View File
@@ -0,0 +1,50 @@
package store
import (
"context"
)
// MessageHit is a search result: a message scoped to the member's
// conversations, with its conversation context.
type MessageHit struct {
Message
ConversationType string
ConversationName string
}
// SearchMessages finds messages matching query in conversations the user
// belongs to, newest first. ILIKE substring matching keeps the search
// dependency-free at family scale; pg_trgm is the documented scaling path.
func (s *Store) SearchMessages(ctx context.Context, userID int64, query string, limit int) ([]MessageHit, error) {
if query == "" {
return nil, nil
}
pattern := "%" + query + "%"
rows, err := s.pool.Query(ctx, `
SELECT m.id, m.conversation_id, m.sender_id, m.body, m.media_id, m.created_at, m.edited_at, m.deleted_at,
c.type, c.name
FROM messages m
JOIN conversation_members cm ON cm.conversation_id = m.conversation_id AND cm.user_id = $1
JOIN conversations c ON c.id = m.conversation_id
WHERE m.body ILIKE $2
ORDER BY m.id DESC
LIMIT $3
`, userID, pattern, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var hits []MessageHit
for rows.Next() {
var h MessageHit
if err := rows.Scan(
&h.ID, &h.ConversationID, &h.SenderID, &h.Body, &h.MediaID, &h.CreatedAt, &h.EditedAt, &h.DeletedAt,
&h.ConversationType, &h.ConversationName,
); err != nil {
return nil, err
}
hits = append(hits, h)
}
return hits, nil
}
+13
View File
@@ -65,3 +65,16 @@ func (s *Store) RevokeAllSessions(ctx context.Context, userID int64) error {
`, userID)
return err
}
// CleanupSessions removes expired sessions and revocations older than a week.
func (s *Store) CleanupSessions(ctx context.Context) (int64, error) {
tag, err := s.pool.Exec(ctx, `
DELETE FROM sessions
WHERE expires_at < now()
OR (revoked_at IS NOT NULL AND revoked_at < now() - interval '7 days')
`)
if err != nil {
return 0, err
}
return tag.RowsAffected(), nil
}