feat: implement Tier 1 IRC numerics

- AWAY command with RPL_AWAY (301), RPL_UNAWAY (305), RPL_NOWAWAY (306)
- RPL_WHOISIDLE (317) with idle time and signon time in WHOIS
- RPL_TOPICWHOTIME (333) with topic setter and timestamp
- Schema migration 002 for away_message and topic metadata columns
- Refactor deliverJoinNumerics into smaller helper functions
This commit is contained in:
clawbot
2026-03-10 10:12:42 -07:00
committed by user
parent 27df999942
commit 0d0d023a50
4 changed files with 320 additions and 11 deletions

View File

@@ -1110,6 +1110,140 @@ func (database *Database) GetSessionCreatedAt(
return createdAt, nil
}
// SetAway sets the away message for a session.
// An empty message clears the away status.
func (database *Database) SetAway(
ctx context.Context,
sessionID int64,
message string,
) error {
_, err := database.conn.ExecContext(ctx,
"UPDATE sessions SET away_message = ? WHERE id = ?",
message, sessionID)
if err != nil {
return fmt.Errorf("set away: %w", err)
}
return nil
}
// GetAway returns the away message for a session.
// Returns an empty string if the user is not away.
func (database *Database) GetAway(
ctx context.Context,
sessionID int64,
) (string, error) {
var msg string
err := database.conn.QueryRowContext(ctx,
"SELECT away_message FROM sessions WHERE id = ?",
sessionID,
).Scan(&msg)
if err != nil {
return "", fmt.Errorf("get away: %w", err)
}
return msg, nil
}
// GetAwayByNick returns the away message for a nick.
// Returns an empty string if the user is not away.
func (database *Database) GetAwayByNick(
ctx context.Context,
nick string,
) (string, error) {
var msg string
err := database.conn.QueryRowContext(ctx,
"SELECT away_message FROM sessions WHERE nick = ?",
nick,
).Scan(&msg)
if err != nil {
return "", fmt.Errorf("get away by nick: %w", err)
}
return msg, nil
}
// SetTopicMeta sets the topic along with who set it and
// when.
func (database *Database) SetTopicMeta(
ctx context.Context,
channelName, topic, setBy string,
) error {
now := time.Now()
_, err := database.conn.ExecContext(ctx,
`UPDATE channels
SET topic = ?, topic_set_by = ?,
topic_set_at = ?, updated_at = ?
WHERE name = ?`,
topic, setBy, now, now, channelName)
if err != nil {
return fmt.Errorf("set topic meta: %w", err)
}
return nil
}
// TopicMeta holds topic metadata for a channel.
type TopicMeta struct {
SetBy string
SetAt time.Time
}
// GetTopicMeta returns who set the topic and when.
func (database *Database) GetTopicMeta(
ctx context.Context,
channelID int64,
) (*TopicMeta, error) {
var (
setBy string
setAt sql.NullTime
)
err := database.conn.QueryRowContext(ctx,
`SELECT topic_set_by, topic_set_at
FROM channels WHERE id = ?`,
channelID,
).Scan(&setBy, &setAt)
if err != nil {
return nil, fmt.Errorf(
"get topic meta: %w", err,
)
}
if setBy == "" || !setAt.Valid {
return nil, nil //nolint:nilnil
}
return &TopicMeta{
SetBy: setBy,
SetAt: setAt.Time,
}, nil
}
// GetSessionLastSeen returns the last_seen time for a
// session.
func (database *Database) GetSessionLastSeen(
ctx context.Context,
sessionID int64,
) (time.Time, error) {
var lastSeen time.Time
err := database.conn.QueryRowContext(ctx,
"SELECT last_seen FROM sessions WHERE id = ?",
sessionID,
).Scan(&lastSeen)
if err != nil {
return time.Time{}, fmt.Errorf(
"get session last_seen: %w", err,
)
}
return lastSeen, nil
}
// PruneOldQueueEntries deletes client output queue entries
// older than cutoff and returns the number of rows removed.
func (database *Database) PruneOldQueueEntries(

View File

@@ -0,0 +1,6 @@
-- Add away message to sessions
ALTER TABLE sessions ADD COLUMN away_message TEXT NOT NULL DEFAULT '';
-- Add topic metadata to channels
ALTER TABLE channels ADD COLUMN topic_set_by TEXT NOT NULL DEFAULT '';
ALTER TABLE channels ADD COLUMN topic_set_at DATETIME;