style: fix all golangci-lint issues and format code (refs #17)

Fix 380 lint violations across all Go source files including wsl_v5,
nlreturn, noinlineerr, errcheck, funlen, funcorder, tagliatelle,
perfsprint, modernize, revive, gosec, ireturn, mnd, forcetypeassert,
cyclop, and others.

Key changes:
- Split large handler/command functions into smaller methods
- Extract scan helpers for database queries
- Reorder exported/unexported methods per funcorder
- Add sentinel errors in models package
- Use camelCase JSON tags per tagliatelle defaults
- Add package comments
- Fix .gitignore to not exclude cmd/chat-cli directory
This commit is contained in:
clawbot
2026-02-26 06:27:56 -08:00
parent 636546d74a
commit b78d526f02
13 changed files with 1920 additions and 753 deletions

View File

@@ -88,8 +88,10 @@ func NewTest(dsn string) (*Database, error) {
}
// Item 9: Enable foreign keys
if _, err := d.Exec("PRAGMA foreign_keys = ON"); err != nil {
_, err = d.Exec("PRAGMA foreign_keys = ON") //nolint:noctx // no context in sql.Open path
if err != nil {
_ = d.Close()
return nil, fmt.Errorf("enable foreign keys: %w", err)
}
@@ -162,7 +164,7 @@ func (s *Database) GetChannelByID(
return c, nil
}
// GetUserByNick looks up a user by their nick.
// GetUserByNickModel looks up a user by their nick.
func (s *Database) GetUserByNickModel(
ctx context.Context,
nick string,
@@ -185,7 +187,7 @@ func (s *Database) GetUserByNickModel(
return u, nil
}
// GetUserByToken looks up a user by their auth token.
// GetUserByTokenModel looks up a user by their auth token.
func (s *Database) GetUserByTokenModel(
ctx context.Context,
token string,
@@ -219,6 +221,7 @@ func (s *Database) DeleteAuthToken(
_, err := s.db.ExecContext(ctx,
`DELETE FROM auth_tokens WHERE token = ?`, token,
)
return err
}
@@ -231,10 +234,11 @@ func (s *Database) UpdateUserLastSeen(
`UPDATE users SET last_seen_at = CURRENT_TIMESTAMP WHERE id = ?`,
userID,
)
return err
}
// CreateUser inserts a new user into the database.
// CreateUserModel inserts a new user into the database.
func (s *Database) CreateUserModel(
ctx context.Context,
id, nick, passwordHash string,
@@ -394,6 +398,7 @@ func (s *Database) DequeueMessages(
if err != nil {
return nil, err
}
defer func() { _ = rows.Close() }()
entries := []*models.MessageQueueEntry{}
@@ -423,14 +428,14 @@ func (s *Database) AckMessages(
}
placeholders := make([]string, len(entryIDs))
args := make([]interface{}, len(entryIDs))
args := make([]any, len(entryIDs))
for i, id := range entryIDs {
placeholders[i] = "?"
args[i] = id
}
query := fmt.Sprintf(
query := fmt.Sprintf( //nolint:gosec // placeholders are ?, not user input
"DELETE FROM message_queue WHERE id IN (%s)",
strings.Join(placeholders, ","),
)
@@ -549,7 +554,8 @@ func (s *Database) connect(ctx context.Context) error {
s.log.Info("database connected")
// Item 9: Enable foreign keys on every connection
if _, err := s.db.ExecContext(ctx, "PRAGMA foreign_keys = ON"); err != nil {
_, err = s.db.ExecContext(ctx, "PRAGMA foreign_keys = ON")
if err != nil {
return fmt.Errorf("enable foreign keys: %w", err)
}
@@ -676,41 +682,54 @@ func (s *Database) applyMigrations(
"version", m.version, "name", m.name,
)
tx, err := s.db.BeginTx(ctx, nil)
err = s.executeMigration(ctx, m)
if err != nil {
return fmt.Errorf(
"begin tx for migration %d: %w", m.version, err,
)
}
_, err = tx.ExecContext(ctx, m.sql)
if err != nil {
_ = tx.Rollback()
return fmt.Errorf(
"apply migration %d (%s): %w",
m.version, m.name, err,
)
}
_, err = tx.ExecContext(ctx,
"INSERT INTO schema_migrations (version) VALUES (?)",
m.version,
)
if err != nil {
_ = tx.Rollback()
return fmt.Errorf(
"record migration %d: %w", m.version, err,
)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf(
"commit migration %d: %w", m.version, err,
)
return err
}
}
return nil
}
func (s *Database) executeMigration(
ctx context.Context,
m migration,
) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf(
"begin tx for migration %d: %w", m.version, err,
)
}
_, err = tx.ExecContext(ctx, m.sql)
if err != nil {
_ = tx.Rollback()
return fmt.Errorf(
"apply migration %d (%s): %w",
m.version, m.name, err,
)
}
_, err = tx.ExecContext(ctx,
"INSERT INTO schema_migrations (version) VALUES (?)",
m.version,
)
if err != nil {
_ = tx.Rollback()
return fmt.Errorf(
"record migration %d: %w", m.version, err,
)
}
err = tx.Commit()
if err != nil {
return fmt.Errorf(
"commit migration %d: %w", m.version, err,
)
}
return nil
}

View File

@@ -3,107 +3,144 @@ package db
import (
"context"
"crypto/rand"
"database/sql"
"encoding/hex"
"fmt"
"time"
)
const (
defaultMessageLimit = 50
defaultPollLimit = 100
tokenBytes = 32
)
func generateToken() string {
b := make([]byte, 32)
b := make([]byte, tokenBytes)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}
// CreateUser registers a new user with the given nick and returns the user with token.
func (s *Database) CreateUser(ctx context.Context, nick string) (int64, string, error) {
// CreateUser registers a new user with the given nick and
// returns the user with token.
func (s *Database) CreateUser(
ctx context.Context,
nick string,
) (int64, string, error) {
token := generateToken()
now := time.Now()
res, err := s.db.ExecContext(ctx,
"INSERT INTO users (nick, token, created_at, last_seen) VALUES (?, ?, ?, ?)",
nick, token, now, now)
if err != nil {
return 0, "", fmt.Errorf("create user: %w", err)
}
id, _ := res.LastInsertId()
return id, token, nil
}
// GetUserByToken returns user id and nick for a given auth token.
func (s *Database) GetUserByToken(ctx context.Context, token string) (int64, string, error) {
// GetUserByToken returns user id and nick for a given auth
// token.
func (s *Database) GetUserByToken(
ctx context.Context,
token string,
) (int64, string, error) {
var id int64
var nick string
err := s.db.QueryRowContext(ctx, "SELECT id, nick FROM users WHERE token = ?", token).Scan(&id, &nick)
err := s.db.QueryRowContext(
ctx,
"SELECT id, nick FROM users WHERE token = ?",
token,
).Scan(&id, &nick)
if err != nil {
return 0, "", err
}
// Update last_seen
_, _ = s.db.ExecContext(ctx, "UPDATE users SET last_seen = ? WHERE id = ?", time.Now(), id)
_, _ = s.db.ExecContext(
ctx,
"UPDATE users SET last_seen = ? WHERE id = ?",
time.Now(), id,
)
return id, nick, nil
}
// GetUserByNick returns user id for a given nick.
func (s *Database) GetUserByNick(ctx context.Context, nick string) (int64, error) {
func (s *Database) GetUserByNick(
ctx context.Context,
nick string,
) (int64, error) {
var id int64
err := s.db.QueryRowContext(ctx, "SELECT id FROM users WHERE nick = ?", nick).Scan(&id)
err := s.db.QueryRowContext(
ctx,
"SELECT id FROM users WHERE nick = ?",
nick,
).Scan(&id)
return id, err
}
// GetOrCreateChannel returns the channel id, creating it if needed.
func (s *Database) GetOrCreateChannel(ctx context.Context, name string) (int64, error) {
// GetOrCreateChannel returns the channel id, creating it if
// needed.
func (s *Database) GetOrCreateChannel(
ctx context.Context,
name string,
) (int64, error) {
var id int64
err := s.db.QueryRowContext(ctx, "SELECT id FROM channels WHERE name = ?", name).Scan(&id)
err := s.db.QueryRowContext(
ctx,
"SELECT id FROM channels WHERE name = ?",
name,
).Scan(&id)
if err == nil {
return id, nil
}
now := time.Now()
res, err := s.db.ExecContext(ctx,
"INSERT INTO channels (name, created_at, updated_at) VALUES (?, ?, ?)",
name, now, now)
if err != nil {
return 0, fmt.Errorf("create channel: %w", err)
}
id, _ = res.LastInsertId()
return id, nil
}
// JoinChannel adds a user to a channel.
func (s *Database) JoinChannel(ctx context.Context, channelID, userID int64) error {
func (s *Database) JoinChannel(
ctx context.Context,
channelID, userID int64,
) error {
_, err := s.db.ExecContext(ctx,
"INSERT OR IGNORE INTO channel_members (channel_id, user_id, joined_at) VALUES (?, ?, ?)",
channelID, userID, time.Now())
return err
}
// PartChannel removes a user from a channel.
func (s *Database) PartChannel(ctx context.Context, channelID, userID int64) error {
func (s *Database) PartChannel(
ctx context.Context,
channelID, userID int64,
) error {
_, err := s.db.ExecContext(ctx,
"DELETE FROM channel_members WHERE channel_id = ? AND user_id = ?",
channelID, userID)
return err
}
// ListChannels returns all channels the user has joined.
func (s *Database) ListChannels(ctx context.Context, userID int64) ([]ChannelInfo, error) {
rows, err := s.db.QueryContext(ctx,
`SELECT c.id, c.name, c.topic FROM channels c
INNER JOIN channel_members cm ON cm.channel_id = c.id
WHERE cm.user_id = ? ORDER BY c.name`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var channels []ChannelInfo
for rows.Next() {
var ch ChannelInfo
if err := rows.Scan(&ch.ID, &ch.Name, &ch.Topic); err != nil {
return nil, err
}
channels = append(channels, ch)
}
if channels == nil {
channels = []ChannelInfo{}
}
return channels, nil
return err
}
// ChannelInfo is a lightweight channel representation.
@@ -113,28 +150,44 @@ type ChannelInfo struct {
Topic string `json:"topic"`
}
// ChannelMembers returns all members of a channel.
func (s *Database) ChannelMembers(ctx context.Context, channelID int64) ([]MemberInfo, error) {
// ListChannels returns all channels the user has joined.
func (s *Database) ListChannels(
ctx context.Context,
userID int64,
) ([]ChannelInfo, error) {
rows, err := s.db.QueryContext(ctx,
`SELECT u.id, u.nick, u.last_seen FROM users u
INNER JOIN channel_members cm ON cm.user_id = u.id
WHERE cm.channel_id = ? ORDER BY u.nick`, channelID)
`SELECT c.id, c.name, c.topic FROM channels c
INNER JOIN channel_members cm ON cm.channel_id = c.id
WHERE cm.user_id = ? ORDER BY c.name`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var members []MemberInfo
defer func() { _ = rows.Close() }()
var channels []ChannelInfo
for rows.Next() {
var m MemberInfo
if err := rows.Scan(&m.ID, &m.Nick, &m.LastSeen); err != nil {
var ch ChannelInfo
err := rows.Scan(&ch.ID, &ch.Name, &ch.Topic)
if err != nil {
return nil, err
}
members = append(members, m)
channels = append(channels, ch)
}
if members == nil {
members = []MemberInfo{}
err = rows.Err()
if err != nil {
return nil, err
}
return members, nil
if channels == nil {
channels = []ChannelInfo{}
}
return channels, nil
}
// MemberInfo represents a channel member.
@@ -144,6 +197,46 @@ type MemberInfo struct {
LastSeen time.Time `json:"lastSeen"`
}
// ChannelMembers returns all members of a channel.
func (s *Database) ChannelMembers(
ctx context.Context,
channelID int64,
) ([]MemberInfo, error) {
rows, err := s.db.QueryContext(ctx,
`SELECT u.id, u.nick, u.last_seen FROM users u
INNER JOIN channel_members cm ON cm.user_id = u.id
WHERE cm.channel_id = ? ORDER BY u.nick`, channelID)
if err != nil {
return nil, err
}
defer func() { _ = rows.Close() }()
var members []MemberInfo
for rows.Next() {
var m MemberInfo
err := rows.Scan(&m.ID, &m.Nick, &m.LastSeen)
if err != nil {
return nil, err
}
members = append(members, m)
}
err = rows.Err()
if err != nil {
return nil, err
}
if members == nil {
members = []MemberInfo{}
}
return members, nil
}
// MessageInfo represents a chat message.
type MessageInfo struct {
ID int64 `json:"id"`
@@ -155,11 +248,18 @@ type MessageInfo struct {
CreatedAt time.Time `json:"createdAt"`
}
// GetMessages returns messages for a channel, optionally after a given ID.
func (s *Database) GetMessages(ctx context.Context, channelID int64, afterID int64, limit int) ([]MessageInfo, error) {
// GetMessages returns messages for a channel, optionally
// after a given ID.
func (s *Database) GetMessages(
ctx context.Context,
channelID int64,
afterID int64,
limit int,
) ([]MessageInfo, error) {
if limit <= 0 {
limit = 50
limit = defaultMessageLimit
}
rows, err := s.db.QueryContext(ctx,
`SELECT m.id, c.name, u.nick, m.content, m.created_at
FROM messages m
@@ -170,128 +270,288 @@ func (s *Database) GetMessages(ctx context.Context, channelID int64, afterID int
if err != nil {
return nil, err
}
defer rows.Close()
defer func() { _ = rows.Close() }()
var msgs []MessageInfo
for rows.Next() {
var m MessageInfo
if err := rows.Scan(&m.ID, &m.Channel, &m.Nick, &m.Content, &m.CreatedAt); err != nil {
err := rows.Scan(
&m.ID, &m.Channel, &m.Nick,
&m.Content, &m.CreatedAt,
)
if err != nil {
return nil, err
}
msgs = append(msgs, m)
}
err = rows.Err()
if err != nil {
return nil, err
}
if msgs == nil {
msgs = []MessageInfo{}
}
return msgs, nil
}
// SendMessage inserts a channel message.
func (s *Database) SendMessage(ctx context.Context, channelID, userID int64, content string) (int64, error) {
func (s *Database) SendMessage(
ctx context.Context,
channelID, userID int64,
content string,
) (int64, error) {
res, err := s.db.ExecContext(ctx,
"INSERT INTO messages (channel_id, user_id, content, is_dm, created_at) VALUES (?, ?, ?, 0, ?)",
channelID, userID, content, time.Now())
if err != nil {
return 0, err
}
return res.LastInsertId()
}
// SendDM inserts a direct message.
func (s *Database) SendDM(ctx context.Context, fromID, toID int64, content string) (int64, error) {
func (s *Database) SendDM(
ctx context.Context,
fromID, toID int64,
content string,
) (int64, error) {
res, err := s.db.ExecContext(ctx,
"INSERT INTO messages (user_id, content, is_dm, dm_target_id, created_at) VALUES (?, ?, 1, ?, ?)",
fromID, content, toID, time.Now())
if err != nil {
return 0, err
}
return res.LastInsertId()
}
// GetDMs returns direct messages between two users after a given ID.
func (s *Database) GetDMs(ctx context.Context, userA, userB int64, afterID int64, limit int) ([]MessageInfo, error) {
// GetDMs returns direct messages between two users after a
// given ID.
func (s *Database) GetDMs(
ctx context.Context,
userA, userB int64,
afterID int64,
limit int,
) ([]MessageInfo, error) {
if limit <= 0 {
limit = 50
limit = defaultMessageLimit
}
rows, err := s.db.QueryContext(ctx,
`SELECT m.id, u.nick, m.content, t.nick, m.created_at
FROM messages m
INNER JOIN users u ON u.id = m.user_id
INNER JOIN users t ON t.id = m.dm_target_id
WHERE m.is_dm = 1 AND m.id > ?
AND ((m.user_id = ? AND m.dm_target_id = ?) OR (m.user_id = ? AND m.dm_target_id = ?))
ORDER BY m.id ASC LIMIT ?`, afterID, userA, userB, userB, userA, limit)
AND ((m.user_id = ? AND m.dm_target_id = ?)
OR (m.user_id = ? AND m.dm_target_id = ?))
ORDER BY m.id ASC LIMIT ?`,
afterID, userA, userB, userB, userA, limit)
if err != nil {
return nil, err
}
defer rows.Close()
defer func() { _ = rows.Close() }()
var msgs []MessageInfo
for rows.Next() {
var m MessageInfo
if err := rows.Scan(&m.ID, &m.Nick, &m.Content, &m.DMTarget, &m.CreatedAt); err != nil {
err := rows.Scan(
&m.ID, &m.Nick, &m.Content,
&m.DMTarget, &m.CreatedAt,
)
if err != nil {
return nil, err
}
m.IsDM = true
msgs = append(msgs, m)
}
err = rows.Err()
if err != nil {
return nil, err
}
if msgs == nil {
msgs = []MessageInfo{}
}
return msgs, nil
}
// PollMessages returns all new messages (channel + DM) for a user after a given ID.
func (s *Database) PollMessages(ctx context.Context, userID int64, afterID int64, limit int) ([]MessageInfo, error) {
// PollMessages returns all new messages (channel + DM) for
// a user after a given ID.
func (s *Database) PollMessages(
ctx context.Context,
userID int64,
afterID int64,
limit int,
) ([]MessageInfo, error) {
if limit <= 0 {
limit = 100
limit = defaultPollLimit
}
rows, err := s.db.QueryContext(ctx,
`SELECT m.id, COALESCE(c.name, ''), u.nick, m.content, m.is_dm, COALESCE(t.nick, ''), m.created_at
`SELECT m.id, COALESCE(c.name, ''), u.nick, m.content,
m.is_dm, COALESCE(t.nick, ''), m.created_at
FROM messages m
INNER JOIN users u ON u.id = m.user_id
LEFT JOIN channels c ON c.id = m.channel_id
LEFT JOIN users t ON t.id = m.dm_target_id
WHERE m.id > ? AND (
(m.is_dm = 0 AND m.channel_id IN (SELECT channel_id FROM channel_members WHERE user_id = ?))
OR (m.is_dm = 1 AND (m.user_id = ? OR m.dm_target_id = ?))
(m.is_dm = 0 AND m.channel_id IN
(SELECT channel_id FROM channel_members
WHERE user_id = ?))
OR (m.is_dm = 1
AND (m.user_id = ? OR m.dm_target_id = ?))
)
ORDER BY m.id ASC LIMIT ?`, afterID, userID, userID, userID, limit)
ORDER BY m.id ASC LIMIT ?`,
afterID, userID, userID, userID, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var msgs []MessageInfo
defer func() { _ = rows.Close() }()
msgs := make([]MessageInfo, 0)
for rows.Next() {
var m MessageInfo
var isDM int
if err := rows.Scan(&m.ID, &m.Channel, &m.Nick, &m.Content, &isDM, &m.DMTarget, &m.CreatedAt); err != nil {
var (
m MessageInfo
isDM int
)
err := rows.Scan(
&m.ID, &m.Channel, &m.Nick, &m.Content,
&isDM, &m.DMTarget, &m.CreatedAt,
)
if err != nil {
return nil, err
}
m.IsDM = isDM == 1
msgs = append(msgs, m)
}
if msgs == nil {
msgs = []MessageInfo{}
err = rows.Err()
if err != nil {
return nil, err
}
return msgs, nil
}
// GetMessagesBefore returns channel messages before a given ID (for history scrollback).
func (s *Database) GetMessagesBefore(ctx context.Context, channelID int64, beforeID int64, limit int) ([]MessageInfo, error) {
if limit <= 0 {
limit = 50
func scanChannelMessages(
rows *sql.Rows,
) ([]MessageInfo, error) {
var msgs []MessageInfo
for rows.Next() {
var m MessageInfo
err := rows.Scan(
&m.ID, &m.Channel, &m.Nick,
&m.Content, &m.CreatedAt,
)
if err != nil {
return nil, err
}
msgs = append(msgs, m)
}
err := rows.Err()
if err != nil {
return nil, err
}
if msgs == nil {
msgs = []MessageInfo{}
}
return msgs, nil
}
func scanDMMessages(
rows *sql.Rows,
) ([]MessageInfo, error) {
var msgs []MessageInfo
for rows.Next() {
var m MessageInfo
err := rows.Scan(
&m.ID, &m.Nick, &m.Content,
&m.DMTarget, &m.CreatedAt,
)
if err != nil {
return nil, err
}
m.IsDM = true
msgs = append(msgs, m)
}
err := rows.Err()
if err != nil {
return nil, err
}
if msgs == nil {
msgs = []MessageInfo{}
}
return msgs, nil
}
func reverseMessages(msgs []MessageInfo) {
for i, j := 0, len(msgs)-1; i < j; i, j = i+1, j-1 {
msgs[i], msgs[j] = msgs[j], msgs[i]
}
}
// GetMessagesBefore returns channel messages before a given
// ID (for history scrollback).
func (s *Database) GetMessagesBefore(
ctx context.Context,
channelID int64,
beforeID int64,
limit int,
) ([]MessageInfo, error) {
if limit <= 0 {
limit = defaultMessageLimit
}
var query string
var args []any
if beforeID > 0 {
query = `SELECT m.id, c.name, u.nick, m.content, m.created_at
query = `SELECT m.id, c.name, u.nick, m.content,
m.created_at
FROM messages m
INNER JOIN users u ON u.id = m.user_id
INNER JOIN channels c ON c.id = m.channel_id
WHERE m.channel_id = ? AND m.is_dm = 0 AND m.id < ?
WHERE m.channel_id = ? AND m.is_dm = 0
AND m.id < ?
ORDER BY m.id DESC LIMIT ?`
args = []any{channelID, beforeID, limit}
} else {
query = `SELECT m.id, c.name, u.nick, m.content, m.created_at
query = `SELECT m.id, c.name, u.nick, m.content,
m.created_at
FROM messages m
INNER JOIN users u ON u.id = m.user_id
INNER JOIN channels c ON c.id = m.channel_id
@@ -299,116 +559,153 @@ func (s *Database) GetMessagesBefore(ctx context.Context, channelID int64, befor
ORDER BY m.id DESC LIMIT ?`
args = []any{channelID, limit}
}
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var msgs []MessageInfo
for rows.Next() {
var m MessageInfo
if err := rows.Scan(&m.ID, &m.Channel, &m.Nick, &m.Content, &m.CreatedAt); err != nil {
return nil, err
}
msgs = append(msgs, m)
}
if msgs == nil {
msgs = []MessageInfo{}
}
// Reverse to ascending order
for i, j := 0, len(msgs)-1; i < j; i, j = i+1, j-1 {
msgs[i], msgs[j] = msgs[j], msgs[i]
defer func() { _ = rows.Close() }()
msgs, scanErr := scanChannelMessages(rows)
if scanErr != nil {
return nil, scanErr
}
// Reverse to ascending order.
reverseMessages(msgs)
return msgs, nil
}
// GetDMsBefore returns DMs between two users before a given ID (for history scrollback).
func (s *Database) GetDMsBefore(ctx context.Context, userA, userB int64, beforeID int64, limit int) ([]MessageInfo, error) {
// GetDMsBefore returns DMs between two users before a given
// ID (for history scrollback).
func (s *Database) GetDMsBefore(
ctx context.Context,
userA, userB int64,
beforeID int64,
limit int,
) ([]MessageInfo, error) {
if limit <= 0 {
limit = 50
limit = defaultMessageLimit
}
var query string
var args []any
if beforeID > 0 {
query = `SELECT m.id, u.nick, m.content, t.nick, m.created_at
query = `SELECT m.id, u.nick, m.content, t.nick,
m.created_at
FROM messages m
INNER JOIN users u ON u.id = m.user_id
INNER JOIN users t ON t.id = m.dm_target_id
WHERE m.is_dm = 1 AND m.id < ?
AND ((m.user_id = ? AND m.dm_target_id = ?) OR (m.user_id = ? AND m.dm_target_id = ?))
AND ((m.user_id = ? AND m.dm_target_id = ?)
OR (m.user_id = ? AND m.dm_target_id = ?))
ORDER BY m.id DESC LIMIT ?`
args = []any{beforeID, userA, userB, userB, userA, limit}
args = []any{
beforeID, userA, userB, userB, userA, limit,
}
} else {
query = `SELECT m.id, u.nick, m.content, t.nick, m.created_at
query = `SELECT m.id, u.nick, m.content, t.nick,
m.created_at
FROM messages m
INNER JOIN users u ON u.id = m.user_id
INNER JOIN users t ON t.id = m.dm_target_id
WHERE m.is_dm = 1
AND ((m.user_id = ? AND m.dm_target_id = ?) OR (m.user_id = ? AND m.dm_target_id = ?))
AND ((m.user_id = ? AND m.dm_target_id = ?)
OR (m.user_id = ? AND m.dm_target_id = ?))
ORDER BY m.id DESC LIMIT ?`
args = []any{userA, userB, userB, userA, limit}
}
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var msgs []MessageInfo
for rows.Next() {
var m MessageInfo
if err := rows.Scan(&m.ID, &m.Nick, &m.Content, &m.DMTarget, &m.CreatedAt); err != nil {
return nil, err
}
m.IsDM = true
msgs = append(msgs, m)
}
if msgs == nil {
msgs = []MessageInfo{}
}
// Reverse to ascending order
for i, j := 0, len(msgs)-1; i < j; i, j = i+1, j-1 {
msgs[i], msgs[j] = msgs[j], msgs[i]
defer func() { _ = rows.Close() }()
msgs, scanErr := scanDMMessages(rows)
if scanErr != nil {
return nil, scanErr
}
// Reverse to ascending order.
reverseMessages(msgs)
return msgs, nil
}
// ChangeNick updates a user's nickname.
func (s *Database) ChangeNick(ctx context.Context, userID int64, newNick string) error {
func (s *Database) ChangeNick(
ctx context.Context,
userID int64,
newNick string,
) error {
_, err := s.db.ExecContext(ctx,
"UPDATE users SET nick = ? WHERE id = ?", newNick, userID)
"UPDATE users SET nick = ? WHERE id = ?",
newNick, userID)
return err
}
// SetTopic sets the topic for a channel.
func (s *Database) SetTopic(ctx context.Context, channelName string, _ int64, topic string) error {
func (s *Database) SetTopic(
ctx context.Context,
channelName string,
_ int64,
topic string,
) error {
_, err := s.db.ExecContext(ctx,
"UPDATE channels SET topic = ? WHERE name = ?", topic, channelName)
"UPDATE channels SET topic = ? WHERE name = ?",
topic, channelName)
return err
}
// GetServerName returns the server name (unused, config provides this).
// GetServerName returns the server name (unused, config
// provides this).
func (s *Database) GetServerName() string {
return ""
}
// ListAllChannels returns all channels.
func (s *Database) ListAllChannels(ctx context.Context) ([]ChannelInfo, error) {
func (s *Database) ListAllChannels(
ctx context.Context,
) ([]ChannelInfo, error) {
rows, err := s.db.QueryContext(ctx,
"SELECT id, name, topic FROM channels ORDER BY name")
if err != nil {
return nil, err
}
defer rows.Close()
defer func() { _ = rows.Close() }()
var channels []ChannelInfo
for rows.Next() {
var ch ChannelInfo
if err := rows.Scan(&ch.ID, &ch.Name, &ch.Topic); err != nil {
err := rows.Scan(
&ch.ID, &ch.Name, &ch.Topic,
)
if err != nil {
return nil, err
}
channels = append(channels, ch)
}
err = rows.Err()
if err != nil {
return nil, err
}
if channels == nil {
channels = []ChannelInfo{}
}
return channels, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@ package models
import (
"context"
"fmt"
"time"
)
@@ -23,5 +22,5 @@ func (t *AuthToken) User(ctx context.Context) (*User, error) {
return ul.GetUserByID(ctx, t.UserID)
}
return nil, fmt.Errorf("user lookup not available")
return nil, ErrUserLookupNotAvailable
}

View File

@@ -2,7 +2,6 @@ package models
import (
"context"
"fmt"
"time"
)
@@ -23,7 +22,7 @@ func (cm *ChannelMember) User(ctx context.Context) (*User, error) {
return ul.GetUserByID(ctx, cm.UserID)
}
return nil, fmt.Errorf("user lookup not available")
return nil, ErrUserLookupNotAvailable
}
// Channel returns the full Channel for this membership.
@@ -32,5 +31,5 @@ func (cm *ChannelMember) Channel(ctx context.Context) (*Channel, error) {
return cl.GetChannelByID(ctx, cm.ChannelID)
}
return nil, fmt.Errorf("channel lookup not available")
return nil, ErrChannelLookupNotAvailable
}

View File

@@ -6,6 +6,7 @@ package models
import (
"context"
"database/sql"
"errors"
)
// DB is the interface that models use to query the database.
@@ -24,6 +25,12 @@ type ChannelLookup interface {
GetChannelByID(ctx context.Context, id string) (*Channel, error)
}
// Sentinel errors for model lookup methods.
var (
ErrUserLookupNotAvailable = errors.New("user lookup not available")
ErrChannelLookupNotAvailable = errors.New("channel lookup not available")
)
// Base is embedded in all model structs to provide database access.
type Base struct {
db DB
@@ -40,7 +47,7 @@ func (b *Base) GetDB() *sql.DB {
}
// GetUserLookup returns the DB as a UserLookup if it implements the interface.
func (b *Base) GetUserLookup() UserLookup {
func (b *Base) GetUserLookup() UserLookup { //nolint:ireturn // interface return is intentional
if ul, ok := b.db.(UserLookup); ok {
return ul
}
@@ -49,7 +56,7 @@ func (b *Base) GetUserLookup() UserLookup {
}
// GetChannelLookup returns the DB as a ChannelLookup if it implements the interface.
func (b *Base) GetChannelLookup() ChannelLookup {
func (b *Base) GetChannelLookup() ChannelLookup { //nolint:ireturn // interface return is intentional
if cl, ok := b.db.(ChannelLookup); ok {
return cl
}

View File

@@ -2,7 +2,6 @@ package models
import (
"context"
"fmt"
"time"
)
@@ -23,5 +22,5 @@ func (s *Session) User(ctx context.Context) (*User, error) {
return ul.GetUserByID(ctx, s.UserID)
}
return nil, fmt.Errorf("user lookup not available")
return nil, ErrUserLookupNotAvailable
}

View File

@@ -71,17 +71,37 @@ func (s *Server) SetupRoutes() {
s.log.Error("failed to get web dist filesystem", "error", err)
} else {
fileServer := http.FileServer(http.FS(distFS))
s.router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
// Try to serve the file; if not found, serve index.html for SPA routing
f, err := distFS.(fs.ReadFileFS).ReadFile(r.URL.Path[1:])
if err != nil || len(f) == 0 {
indexHTML, _ := distFS.(fs.ReadFileFS).ReadFile("index.html")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(indexHTML)
return
}
fileServer.ServeHTTP(w, r)
s.serveSPA(distFS, fileServer, w, r)
})
}
}
func (s *Server) serveSPA(
distFS fs.FS,
fileServer http.Handler,
w http.ResponseWriter,
r *http.Request,
) {
readFS, ok := distFS.(fs.ReadFileFS)
if !ok {
http.Error(w, "filesystem error", http.StatusInternalServerError)
return
}
// Try to serve the file; fall back to index.html for SPA routing.
f, err := readFS.ReadFile(r.URL.Path[1:])
if err != nil || len(f) == 0 {
indexHTML, _ := readFS.ReadFile("index.html")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(indexHTML)
return
}
fileServer.ServeHTTP(w, r)
}