Add complete database schema and ORM models
Schema (002_tables.sql): - users: accounts with nick, password hash, timestamps - auth_tokens: per-device tokens with expiry, linked to users - channels: chat rooms with topic and mode flags - channel_members: membership with per-user modes (+o, +v) - messages: channel/DM history with structured JSON meta - message_queue: per-user pending delivery queue - sessions: server-held session state with idle timeout - server_links: federation peer configuration Models (internal/models/): - All models embed Base for database access - Relation methods on models: User.Channels(), User.QueuedMessages(), Channel.Members(), Channel.RecentMessages(), ChannelMember.User(), ChannelMember.Channel(), AuthToken.User(), Session.User() - IDs are UUIDs (TEXT), not auto-increment integers - JSON tags use camelCase per lint rules All tables verified: migrations apply cleanly, 0 lint issues.
This commit is contained in:
@@ -11,7 +11,6 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.eeqj.de/sneak/chat/internal/config"
|
"git.eeqj.de/sneak/chat/internal/config"
|
||||||
"git.eeqj.de/sneak/chat/internal/logger"
|
"git.eeqj.de/sneak/chat/internal/logger"
|
||||||
@@ -51,19 +50,9 @@ func (s *Database) GetDB() *sql.DB {
|
|||||||
return s.db
|
return s.db
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChannel creates a Channel model instance with the db reference injected.
|
// Hydrate injects the database reference into any model that embeds Base.
|
||||||
func (s *Database) NewChannel(id int64, name, topic, modes string, createdAt, updatedAt time.Time) *models.Channel {
|
func (s *Database) Hydrate(m interface{ SetDB(d models.DB) }) {
|
||||||
c := &models.Channel{
|
m.SetDB(s)
|
||||||
ID: id,
|
|
||||||
Name: name,
|
|
||||||
Topic: topic,
|
|
||||||
Modes: modes,
|
|
||||||
CreatedAt: createdAt,
|
|
||||||
UpdatedAt: updatedAt,
|
|
||||||
}
|
|
||||||
c.SetDB(s)
|
|
||||||
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Database instance and registers lifecycle hooks.
|
// New creates a new Database instance and registers lifecycle hooks.
|
||||||
|
|||||||
@@ -1,8 +1,85 @@
|
|||||||
CREATE TABLE IF NOT EXISTS channels (
|
-- Users: accounts and authentication
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
name TEXT NOT NULL UNIQUE,
|
id TEXT PRIMARY KEY, -- UUID
|
||||||
topic TEXT NOT NULL DEFAULT '',
|
nick TEXT NOT NULL UNIQUE,
|
||||||
modes TEXT NOT NULL DEFAULT '',
|
password_hash TEXT NOT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_seen_at DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Auth tokens: one user can have multiple active tokens (multiple devices)
|
||||||
|
CREATE TABLE IF NOT EXISTS auth_tokens (
|
||||||
|
token TEXT PRIMARY KEY, -- random token string
|
||||||
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at DATETIME, -- NULL = no expiry
|
||||||
|
last_used_at DATETIME
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_auth_tokens_user_id ON auth_tokens(user_id);
|
||||||
|
|
||||||
|
-- Channels: chat rooms
|
||||||
|
CREATE TABLE IF NOT EXISTS channels (
|
||||||
|
id TEXT PRIMARY KEY, -- UUID
|
||||||
|
name TEXT NOT NULL UNIQUE, -- #general, etc.
|
||||||
|
topic TEXT NOT NULL DEFAULT '',
|
||||||
|
modes TEXT NOT NULL DEFAULT '', -- +i, +m, +s, +t, +n
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Channel members: who is in which channel, with per-user modes
|
||||||
|
CREATE TABLE IF NOT EXISTS channel_members (
|
||||||
|
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||||
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
modes TEXT NOT NULL DEFAULT '', -- +o (operator), +v (voice)
|
||||||
|
joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (channel_id, user_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_channel_members_user_id ON channel_members(user_id);
|
||||||
|
|
||||||
|
-- Messages: channel and DM history (rotated per MAX_HISTORY)
|
||||||
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
id TEXT PRIMARY KEY, -- UUID
|
||||||
|
ts DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
from_user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
from_nick TEXT NOT NULL, -- denormalized for history
|
||||||
|
target TEXT NOT NULL, -- #channel name or user UUID for DMs
|
||||||
|
type TEXT NOT NULL DEFAULT 'message', -- message, action, notice, join, part, quit, topic, mode, nick, system
|
||||||
|
body TEXT NOT NULL DEFAULT '',
|
||||||
|
meta TEXT NOT NULL DEFAULT '{}', -- JSON extensible metadata
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_target_ts ON messages(target, ts);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_from_user ON messages(from_user_id);
|
||||||
|
|
||||||
|
-- Message queue: per-user pending delivery (unread messages)
|
||||||
|
CREATE TABLE IF NOT EXISTS message_queue (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
||||||
|
queued_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(user_id, message_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_message_queue_user_id ON message_queue(user_id, queued_at);
|
||||||
|
|
||||||
|
-- Sessions: server-held session state
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
id TEXT PRIMARY KEY, -- UUID
|
||||||
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_active_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at DATETIME -- idle timeout
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
|
||||||
|
|
||||||
|
-- Server links: federation peer configuration
|
||||||
|
CREATE TABLE IF NOT EXISTS server_links (
|
||||||
|
id TEXT PRIMARY KEY, -- UUID
|
||||||
|
name TEXT NOT NULL UNIQUE, -- human-readable peer name
|
||||||
|
url TEXT NOT NULL, -- base URL of peer server
|
||||||
|
shared_key_hash TEXT NOT NULL, -- hashed shared secret
|
||||||
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_seen_at DATETIME
|
||||||
);
|
);
|
||||||
|
|||||||
37
internal/models/auth_token.go
Normal file
37
internal/models/auth_token.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthToken represents an authentication token for a user session.
|
||||||
|
type AuthToken struct {
|
||||||
|
Base
|
||||||
|
|
||||||
|
Token string `json:"-"`
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
||||||
|
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// User returns the user who owns this token.
|
||||||
|
func (t *AuthToken) User(ctx context.Context) (*User, error) {
|
||||||
|
u := &User{}
|
||||||
|
u.SetDB(t.db)
|
||||||
|
|
||||||
|
err := t.GetDB().QueryRowContext(ctx, `
|
||||||
|
SELECT id, nick, password_hash, created_at, updated_at, last_seen_at
|
||||||
|
FROM users WHERE id = ?`,
|
||||||
|
t.UserID,
|
||||||
|
).Scan(
|
||||||
|
&u.ID, &u.Nick, &u.PasswordHash,
|
||||||
|
&u.CreatedAt, &u.UpdatedAt, &u.LastSeenAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -8,10 +9,88 @@ import (
|
|||||||
type Channel struct {
|
type Channel struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
ID int64 `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Topic string `json:"topic"`
|
Topic string `json:"topic"`
|
||||||
Modes string `json:"modes"`
|
Modes string `json:"modes"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Members returns all users who are members of this channel.
|
||||||
|
func (c *Channel) Members(ctx context.Context) ([]*ChannelMember, error) {
|
||||||
|
rows, err := c.GetDB().QueryContext(ctx, `
|
||||||
|
SELECT cm.channel_id, cm.user_id, cm.modes, cm.joined_at,
|
||||||
|
u.nick
|
||||||
|
FROM channel_members cm
|
||||||
|
JOIN users u ON u.id = cm.user_id
|
||||||
|
WHERE cm.channel_id = ?
|
||||||
|
ORDER BY cm.joined_at`,
|
||||||
|
c.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
var members []*ChannelMember
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
m := &ChannelMember{}
|
||||||
|
m.SetDB(c.db)
|
||||||
|
|
||||||
|
err = rows.Scan(
|
||||||
|
&m.ChannelID, &m.UserID, &m.Modes,
|
||||||
|
&m.JoinedAt, &m.Nick,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
members = append(members, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return members, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecentMessages returns the most recent messages in this channel.
|
||||||
|
func (c *Channel) RecentMessages(
|
||||||
|
ctx context.Context,
|
||||||
|
limit int,
|
||||||
|
) ([]*Message, error) {
|
||||||
|
rows, err := c.GetDB().QueryContext(ctx, `
|
||||||
|
SELECT id, ts, from_user_id, from_nick,
|
||||||
|
target, type, body, meta, created_at
|
||||||
|
FROM messages
|
||||||
|
WHERE target = ?
|
||||||
|
ORDER BY ts DESC
|
||||||
|
LIMIT ?`,
|
||||||
|
c.Name, limit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
var messages []*Message
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
msg := &Message{}
|
||||||
|
msg.SetDB(c.db)
|
||||||
|
|
||||||
|
err = rows.Scan(
|
||||||
|
&msg.ID, &msg.Timestamp, &msg.FromUserID,
|
||||||
|
&msg.FromNick, &msg.Target, &msg.Type,
|
||||||
|
&msg.Body, &msg.Meta, &msg.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
messages = append(messages, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages, rows.Err()
|
||||||
|
}
|
||||||
|
|||||||
57
internal/models/channel_member.go
Normal file
57
internal/models/channel_member.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChannelMember represents a user's membership in a channel.
|
||||||
|
type ChannelMember struct {
|
||||||
|
Base
|
||||||
|
|
||||||
|
ChannelID string `json:"channelId"`
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
Modes string `json:"modes"`
|
||||||
|
JoinedAt time.Time `json:"joinedAt"`
|
||||||
|
Nick string `json:"nick"` // denormalized from users table
|
||||||
|
}
|
||||||
|
|
||||||
|
// User returns the full User for this membership.
|
||||||
|
func (cm *ChannelMember) User(ctx context.Context) (*User, error) {
|
||||||
|
u := &User{}
|
||||||
|
u.SetDB(cm.db)
|
||||||
|
|
||||||
|
err := cm.GetDB().QueryRowContext(ctx, `
|
||||||
|
SELECT id, nick, password_hash, created_at, updated_at, last_seen_at
|
||||||
|
FROM users WHERE id = ?`,
|
||||||
|
cm.UserID,
|
||||||
|
).Scan(
|
||||||
|
&u.ID, &u.Nick, &u.PasswordHash,
|
||||||
|
&u.CreatedAt, &u.UpdatedAt, &u.LastSeenAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel returns the full Channel for this membership.
|
||||||
|
func (cm *ChannelMember) Channel(ctx context.Context) (*Channel, error) {
|
||||||
|
c := &Channel{}
|
||||||
|
c.SetDB(cm.db)
|
||||||
|
|
||||||
|
err := cm.GetDB().QueryRowContext(ctx, `
|
||||||
|
SELECT id, name, topic, modes, created_at, updated_at
|
||||||
|
FROM channels WHERE id = ?`,
|
||||||
|
cm.ChannelID,
|
||||||
|
).Scan(
|
||||||
|
&c.ID, &c.Name, &c.Topic, &c.Modes,
|
||||||
|
&c.CreatedAt, &c.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
20
internal/models/message.go
Normal file
20
internal/models/message.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message represents a chat message (channel or DM).
|
||||||
|
type Message struct {
|
||||||
|
Base
|
||||||
|
|
||||||
|
ID string `json:"id"`
|
||||||
|
Timestamp time.Time `json:"ts"`
|
||||||
|
FromUserID string `json:"fromUserId"`
|
||||||
|
FromNick string `json:"from"`
|
||||||
|
Target string `json:"to"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Meta string `json:"meta"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
}
|
||||||
15
internal/models/message_queue.go
Normal file
15
internal/models/message_queue.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MessageQueueEntry represents a pending message delivery for a user.
|
||||||
|
type MessageQueueEntry struct {
|
||||||
|
Base
|
||||||
|
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
MessageID string `json:"messageId"`
|
||||||
|
QueuedAt time.Time `json:"queuedAt"`
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
// Package models defines the data models used by the chat application.
|
// Package models defines the data models used by the chat application.
|
||||||
|
// All model structs embed Base, which provides database access for
|
||||||
|
// relation-fetching methods directly on model instances.
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import "database/sql"
|
import "database/sql"
|
||||||
|
|
||||||
// DB is the interface that models use to query relations.
|
// DB is the interface that models use to query the database.
|
||||||
// This avoids a circular import with the db package.
|
// This avoids a circular import with the db package.
|
||||||
type DB interface {
|
type DB interface {
|
||||||
GetDB() *sql.DB
|
GetDB() *sql.DB
|
||||||
@@ -18,3 +20,8 @@ type Base struct {
|
|||||||
func (b *Base) SetDB(d DB) {
|
func (b *Base) SetDB(d DB) {
|
||||||
b.db = d
|
b.db = d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDB returns the database interface for use in model methods.
|
||||||
|
func (b *Base) GetDB() *sql.DB {
|
||||||
|
return b.db.GetDB()
|
||||||
|
}
|
||||||
|
|||||||
18
internal/models/server_link.go
Normal file
18
internal/models/server_link.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServerLink represents a federation peer server configuration.
|
||||||
|
type ServerLink struct {
|
||||||
|
Base
|
||||||
|
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
SharedKeyHash string `json:"-"`
|
||||||
|
IsActive bool `json:"isActive"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
LastSeenAt *time.Time `json:"lastSeenAt,omitempty"`
|
||||||
|
}
|
||||||
37
internal/models/session.go
Normal file
37
internal/models/session.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Session represents a server-held user session.
|
||||||
|
type Session struct {
|
||||||
|
Base
|
||||||
|
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
LastActiveAt time.Time `json:"lastActiveAt"`
|
||||||
|
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// User returns the user who owns this session.
|
||||||
|
func (s *Session) User(ctx context.Context) (*User, error) {
|
||||||
|
u := &User{}
|
||||||
|
u.SetDB(s.db)
|
||||||
|
|
||||||
|
err := s.GetDB().QueryRowContext(ctx, `
|
||||||
|
SELECT id, nick, password_hash, created_at, updated_at, last_seen_at
|
||||||
|
FROM users WHERE id = ?`,
|
||||||
|
s.UserID,
|
||||||
|
).Scan(
|
||||||
|
&u.ID, &u.Nick, &u.PasswordHash,
|
||||||
|
&u.CreatedAt, &u.UpdatedAt, &u.LastSeenAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
92
internal/models/user.go
Normal file
92
internal/models/user.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User represents a registered user account.
|
||||||
|
type User struct {
|
||||||
|
Base
|
||||||
|
|
||||||
|
ID string `json:"id"`
|
||||||
|
Nick string `json:"nick"`
|
||||||
|
PasswordHash string `json:"-"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
LastSeenAt *time.Time `json:"lastSeenAt,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channels returns all channels the user is a member of.
|
||||||
|
func (u *User) Channels(ctx context.Context) ([]*Channel, error) {
|
||||||
|
rows, err := u.GetDB().QueryContext(ctx, `
|
||||||
|
SELECT c.id, c.name, c.topic, c.modes, c.created_at, c.updated_at
|
||||||
|
FROM channels c
|
||||||
|
JOIN channel_members cm ON cm.channel_id = c.id
|
||||||
|
WHERE cm.user_id = ?
|
||||||
|
ORDER BY c.name`,
|
||||||
|
u.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
var channels []*Channel
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
c := &Channel{}
|
||||||
|
c.SetDB(u.db)
|
||||||
|
|
||||||
|
err = rows.Scan(
|
||||||
|
&c.ID, &c.Name, &c.Topic, &c.Modes,
|
||||||
|
&c.CreatedAt, &c.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
channels = append(channels, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueuedMessages returns undelivered messages for this user.
|
||||||
|
func (u *User) QueuedMessages(ctx context.Context) ([]*Message, error) {
|
||||||
|
rows, err := u.GetDB().QueryContext(ctx, `
|
||||||
|
SELECT m.id, m.ts, m.from_user_id, m.from_nick,
|
||||||
|
m.target, m.type, m.body, m.meta, m.created_at
|
||||||
|
FROM messages m
|
||||||
|
JOIN message_queue mq ON mq.message_id = m.id
|
||||||
|
WHERE mq.user_id = ?
|
||||||
|
ORDER BY mq.queued_at ASC`,
|
||||||
|
u.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
var messages []*Message
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
msg := &Message{}
|
||||||
|
msg.SetDB(u.db)
|
||||||
|
|
||||||
|
err = rows.Scan(
|
||||||
|
&msg.ID, &msg.Timestamp, &msg.FromUserID,
|
||||||
|
&msg.FromNick, &msg.Target, &msg.Type,
|
||||||
|
&msg.Body, &msg.Meta, &msg.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
messages = append(messages, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages, rows.Err()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user