2026-03-14 20:52:11 +01:00
|
|
|
package db
|
|
|
|
|
|
|
|
|
|
import "time"
|
|
|
|
|
|
|
|
|
|
// User represents a row in the users table.
|
|
|
|
|
type User struct {
|
|
|
|
|
ID int64
|
|
|
|
|
Username string
|
2026-04-02 14:44:41 +02:00
|
|
|
PasswordHash string `json:"-"`
|
2026-03-14 20:52:11 +01:00
|
|
|
Avatar *string
|
|
|
|
|
RoleID int64
|
2026-04-02 14:44:41 +02:00
|
|
|
TOTPSecret *string `json:"-"`
|
2026-03-14 20:52:11 +01:00
|
|
|
Status string
|
|
|
|
|
CreatedAt string
|
|
|
|
|
LastSeen *string
|
|
|
|
|
Banned bool
|
|
|
|
|
BanReason *string
|
|
|
|
|
BanExpires *string
|
2026-07-23 21:07:09 +02:00
|
|
|
// IdentityPublicKey is the long-term E2EE identity public key (base64,
|
|
|
|
|
// ECDSA P-256) used for TOFU pinning of voice E2EE announces. Nil = not
|
|
|
|
|
// published (legacy client).
|
|
|
|
|
IdentityPublicKey *string
|
2026-08-01 22:06:14 +02:00
|
|
|
// DisplayName is the optional nickname shown instead of Username. Nil =
|
|
|
|
|
// unset, and every renderer falls back to Username. Mentions still resolve
|
|
|
|
|
// against Username alone — it is the unique key.
|
|
|
|
|
DisplayName *string
|
|
|
|
|
// About is the optional profile bio shown in the profile popup. Nil = unset.
|
|
|
|
|
About *string
|
|
|
|
|
// CustomStatus is the optional free-text status line shown under the name.
|
|
|
|
|
// Nil = unset. Set over the WebSocket presence path and cleared on logout.
|
|
|
|
|
CustomStatus *string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// EffectiveDisplayName returns the name to render for the user: the display
|
|
|
|
|
// name when set and non-empty, the username otherwise. Every payload builder
|
|
|
|
|
// goes through this so the fallback cannot be spelled three different ways.
|
|
|
|
|
func (u *User) EffectiveDisplayName() string {
|
|
|
|
|
if u == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
if u.DisplayName != nil && *u.DisplayName != "" {
|
|
|
|
|
return *u.DisplayName
|
|
|
|
|
}
|
|
|
|
|
return u.Username
|
2026-03-14 20:52:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Session represents a row in the sessions table.
|
|
|
|
|
type Session struct {
|
|
|
|
|
ID int64
|
|
|
|
|
UserID int64
|
2026-04-02 14:44:41 +02:00
|
|
|
TokenHash string `json:"-"`
|
2026-03-14 20:52:11 +01:00
|
|
|
Device string
|
|
|
|
|
IP string
|
|
|
|
|
CreatedAt string
|
|
|
|
|
LastUsed string
|
|
|
|
|
ExpiresAt string
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-29 13:25:46 +02:00
|
|
|
// APIToken represents a row in the api_tokens table — a long-lived, revocable
|
|
|
|
|
// bearer token that authenticates as UserID with that user's role/permissions.
|
|
|
|
|
// Raw tokens are never stored; TokenHash is the SHA-256 hex, like Session.
|
|
|
|
|
type APIToken struct {
|
|
|
|
|
ID int64
|
|
|
|
|
UserID int64
|
|
|
|
|
TokenHash string `json:"-"`
|
|
|
|
|
Label string
|
|
|
|
|
CreatedAt string
|
|
|
|
|
LastUsed *string
|
|
|
|
|
ExpiresAt *string // nil = never expires
|
|
|
|
|
RevokedAt *string // nil = active
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// APITokenListItem is one row of the admin/CLI token listing. It carries the
|
|
|
|
|
// owning user's name for display and deliberately omits the hash.
|
|
|
|
|
type APITokenListItem struct {
|
|
|
|
|
ID int64 `json:"id"`
|
|
|
|
|
UserID int64 `json:"user_id"`
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
Label string `json:"label"`
|
|
|
|
|
CreatedAt string `json:"created_at"`
|
|
|
|
|
LastUsed *string `json:"last_used"`
|
|
|
|
|
ExpiresAt *string `json:"expires_at"`
|
|
|
|
|
RevokedAt *string `json:"revoked_at"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 20:52:11 +01:00
|
|
|
// Invite represents a row in the invites table.
|
|
|
|
|
type Invite struct {
|
|
|
|
|
ID int64
|
|
|
|
|
Code string
|
|
|
|
|
CreatedBy int64
|
|
|
|
|
Uses int
|
|
|
|
|
MaxUses *int
|
|
|
|
|
ExpiresAt *string
|
|
|
|
|
Revoked bool
|
|
|
|
|
CreatedAt string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Role represents a row in the roles table.
|
|
|
|
|
type Role struct {
|
2026-03-15 12:18:19 +01:00
|
|
|
ID int64 `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Color *string `json:"color"`
|
|
|
|
|
Permissions int64 `json:"permissions"`
|
|
|
|
|
Position int `json:"position"`
|
|
|
|
|
IsDefault bool `json:"is_default"`
|
2026-03-14 20:52:11 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-14 21:17:09 +01:00
|
|
|
// Channel represents a row in the channels table.
|
|
|
|
|
type Channel struct {
|
2026-03-15 11:42:25 +01:00
|
|
|
ID int64 `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
Category string `json:"category"`
|
|
|
|
|
Topic string `json:"topic"`
|
|
|
|
|
Position int `json:"position"`
|
|
|
|
|
SlowMode int `json:"slow_mode"`
|
|
|
|
|
Archived bool `json:"archived"`
|
|
|
|
|
CreatedAt string `json:"created_at"`
|
|
|
|
|
VoiceMaxUsers int `json:"voice_max_users"`
|
|
|
|
|
VoiceQuality *string `json:"voice_quality,omitempty"`
|
|
|
|
|
MixingThreshold *int `json:"mixing_threshold,omitempty"`
|
|
|
|
|
VoiceMaxVideo int `json:"voice_max_video"`
|
2026-08-01 22:06:14 +02:00
|
|
|
// NSFW marks the channel as possibly carrying sensitive content. It is
|
|
|
|
|
// metadata only: the server stores, ships and audits it but imposes no
|
|
|
|
|
// content behaviour of its own (see migration 025). Clients decide what
|
|
|
|
|
// to do with it — the desktop client shows a per-session age gate.
|
|
|
|
|
NSFW bool `json:"nsfw"`
|
2026-03-14 21:17:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Message represents a row in the messages table.
|
|
|
|
|
type Message struct {
|
|
|
|
|
ID int64
|
|
|
|
|
ChannelID int64
|
|
|
|
|
UserID int64
|
|
|
|
|
Content string
|
|
|
|
|
ReplyTo *int64
|
|
|
|
|
EditedAt *string
|
|
|
|
|
Deleted bool
|
|
|
|
|
Pinned bool
|
|
|
|
|
Timestamp string
|
2026-08-01 22:06:14 +02:00
|
|
|
// MentionsEveryone is set when the message resolved an @everyone or @here
|
|
|
|
|
// token and the author held MENTION_EVERYONE. Per-user mentions live in
|
|
|
|
|
// message_mentions.
|
|
|
|
|
MentionsEveryone bool
|
2026-03-14 21:17:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MessageWithUser joins a Message with the author's public fields.
|
|
|
|
|
type MessageWithUser struct {
|
|
|
|
|
Message
|
|
|
|
|
Username string
|
|
|
|
|
Avatar *string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ReactionCount is an aggregated reaction count for a single emoji.
|
|
|
|
|
type ReactionCount struct {
|
|
|
|
|
Emoji string
|
|
|
|
|
Count int
|
|
|
|
|
MeReacted bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MessageSearchResult is a row returned by the FTS5 message search.
|
|
|
|
|
type MessageSearchResult struct {
|
2026-08-01 22:06:14 +02:00
|
|
|
MessageID int64 `json:"message_id"`
|
|
|
|
|
ChannelID int64 `json:"channel_id"`
|
|
|
|
|
ChannelName string `json:"channel_name"`
|
|
|
|
|
User UserPublic `json:"user"`
|
|
|
|
|
Content string `json:"content"`
|
|
|
|
|
Timestamp string `json:"timestamp"`
|
|
|
|
|
Mentions []int64 `json:"mentions"`
|
|
|
|
|
MentionsEveryone bool `json:"mentions_everyone"`
|
2026-03-16 16:57:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UserPublic is the public-facing user shape for API responses.
|
|
|
|
|
type UserPublic struct {
|
|
|
|
|
ID int64 `json:"id"`
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
Avatar *string `json:"avatar,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MessageAPIResponse matches the API.md shape for GET /channels/{id}/messages.
|
|
|
|
|
type MessageAPIResponse struct {
|
2026-03-31 11:41:59 +02:00
|
|
|
ID int64 `json:"id"`
|
|
|
|
|
ChannelID int64 `json:"channel_id"`
|
|
|
|
|
User UserPublic `json:"user"`
|
|
|
|
|
Content string `json:"content"`
|
|
|
|
|
ReplyTo *int64 `json:"reply_to"`
|
2026-03-16 16:57:01 +01:00
|
|
|
Attachments []AttachmentInfo `json:"attachments"`
|
2026-03-31 11:41:59 +02:00
|
|
|
Reactions []ReactionInfo `json:"reactions"`
|
|
|
|
|
Pinned bool `json:"pinned"`
|
|
|
|
|
EditedAt *string `json:"edited_at"`
|
|
|
|
|
Deleted bool `json:"deleted"`
|
|
|
|
|
Timestamp string `json:"timestamp"`
|
2026-08-01 22:06:14 +02:00
|
|
|
// Mentions is the resolved user ids the message mentions (never nil);
|
|
|
|
|
// MentionsEveryone reports an authorized @everyone/@here.
|
|
|
|
|
Mentions []int64 `json:"mentions"`
|
|
|
|
|
MentionsEveryone bool `json:"mentions_everyone"`
|
2026-03-16 16:57:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AttachmentInfo is the attachment shape in API responses.
|
|
|
|
|
type AttachmentInfo struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Filename string `json:"filename"`
|
|
|
|
|
Size int64 `json:"size"`
|
|
|
|
|
Mime string `json:"mime"`
|
|
|
|
|
URL string `json:"url"`
|
2026-03-21 12:34:31 +01:00
|
|
|
Width *int `json:"width,omitempty"`
|
|
|
|
|
Height *int `json:"height,omitempty"`
|
2026-03-16 16:57:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ReactionInfo is the reaction shape in API responses.
|
|
|
|
|
type ReactionInfo struct {
|
|
|
|
|
Emoji string `json:"emoji"`
|
|
|
|
|
Count int `json:"count"`
|
|
|
|
|
Me bool `json:"me"`
|
2026-03-14 21:17:09 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-01 22:06:14 +02:00
|
|
|
// ReactionUser is one reactor in the who-reacted list returned by
|
|
|
|
|
// GET /channels/{id}/messages/{messageId}/reactions/{emoji}/users. Avatar is a
|
|
|
|
|
// plain string ("" = none) rather than UserPublic's pointer: the tooltip that
|
|
|
|
|
// consumes this never distinguishes null from empty.
|
|
|
|
|
type ReactionUser struct {
|
|
|
|
|
ID int64 `json:"id"`
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
Avatar string `json:"avatar"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 21:31:03 +01:00
|
|
|
// VoiceState represents a row in the voice_states table.
|
|
|
|
|
// It tracks which voice channel a user is in and their current audio state.
|
2026-08-01 22:06:14 +02:00
|
|
|
//
|
|
|
|
|
// ServerMuted/ServerDeafened are moderator-imposed and, unlike Muted/Deafened,
|
|
|
|
|
// the user cannot clear them: while set, their own voice_mute/voice_deafen
|
|
|
|
|
// unmute attempts are refused. They are scoped to the voice session — the row
|
|
|
|
|
// is deleted on leave — but survive a channel switch.
|
2026-03-14 21:31:03 +01:00
|
|
|
type VoiceState struct {
|
2026-08-01 22:06:14 +02:00
|
|
|
UserID int64 `json:"user_id"`
|
|
|
|
|
ChannelID int64 `json:"channel_id"`
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
Muted bool `json:"muted"`
|
|
|
|
|
Deafened bool `json:"deafened"`
|
|
|
|
|
Speaking bool `json:"speaking"`
|
|
|
|
|
Camera bool `json:"camera"`
|
|
|
|
|
Screenshare bool `json:"screenshare"`
|
|
|
|
|
ServerMuted bool `json:"server_muted"`
|
|
|
|
|
ServerDeafened bool `json:"server_deafened"`
|
|
|
|
|
JoinedAt string `json:"-"`
|
2026-03-14 21:31:03 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-17 02:56:19 +01:00
|
|
|
// ChannelUnread holds per-user unread data for a single channel.
|
|
|
|
|
type ChannelUnread struct {
|
|
|
|
|
LastMessageID int64 `json:"last_message_id"`
|
|
|
|
|
UnreadCount int `json:"unread_count"`
|
2026-08-01 22:06:14 +02:00
|
|
|
// MentionCount is read_states.mention_count: unread messages that mention
|
|
|
|
|
// this user directly or via an authorized @everyone/@here. Zeroed by
|
|
|
|
|
// channel_focus, never advanced by an edit.
|
|
|
|
|
MentionCount int `json:"mention_count"`
|
2026-03-17 02:56:19 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-14 21:31:03 +01:00
|
|
|
// ServerStats contains aggregate counts for the admin dashboard.
|
|
|
|
|
type ServerStats struct {
|
|
|
|
|
UserCount int64 `json:"user_count"`
|
|
|
|
|
MessageCount int64 `json:"message_count"`
|
|
|
|
|
ChannelCount int64 `json:"channel_count"`
|
|
|
|
|
InviteCount int64 `json:"invite_count"`
|
|
|
|
|
DBSizeBytes int64 `json:"db_size_bytes"`
|
2026-03-17 03:20:37 +01:00
|
|
|
OnlineCount int `json:"online_count"`
|
2026-03-14 21:31:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UserWithRole extends User with the name of the user's role.
|
|
|
|
|
type UserWithRole struct {
|
|
|
|
|
User
|
|
|
|
|
RoleName string `json:"role_name"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AuditEntry represents a single row from the audit_log table joined with the
|
|
|
|
|
// actor's username.
|
|
|
|
|
type AuditEntry struct {
|
|
|
|
|
ID int64 `json:"id"`
|
|
|
|
|
ActorID int64 `json:"actor_id"`
|
|
|
|
|
ActorName string `json:"actor_name"`
|
|
|
|
|
Action string `json:"action"`
|
|
|
|
|
TargetType string `json:"target_type"`
|
|
|
|
|
TargetID int64 `json:"target_id"`
|
|
|
|
|
Detail string `json:"detail"`
|
|
|
|
|
CreatedAt string `json:"created_at"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 22:06:14 +02:00
|
|
|
// Emoji represents a row in the emoji table: one server-wide custom emoji.
|
|
|
|
|
//
|
|
|
|
|
// StoredAs is the storage-layer UUID the image bytes live under (the table's
|
|
|
|
|
// legacy column name is `filename`); it is never shown to a user and never
|
|
|
|
|
// derived from anything the uploader sent. Shortcode is always lowercase --
|
|
|
|
|
// the only spelling the validator admits -- which is what makes the table's
|
|
|
|
|
// plain UNIQUE index a case-insensitive one.
|
|
|
|
|
type Emoji struct {
|
|
|
|
|
ID int64 `json:"id"`
|
|
|
|
|
Shortcode string `json:"shortcode"`
|
|
|
|
|
StoredAs string `json:"-"`
|
|
|
|
|
MimeType string `json:"-"`
|
|
|
|
|
UploadedBy int64 `json:"uploaded_by"`
|
|
|
|
|
CreatedAt string `json:"created_at"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 20:52:11 +01:00
|
|
|
// sessionTTL is the duration a session remains valid after creation.
|
|
|
|
|
const sessionTTL = 30 * 24 * time.Hour
|
2026-08-15 20:50:47 +02:00
|
|
|
|
|
|
|
|
// sessionTimeLayout is the storage format for sessions.expires_at (and the
|
|
|
|
|
// other RFC3339-UTC expiry columns). DeleteExpiredSessions compares these as
|
|
|
|
|
// plain text against an index, so every writer and the sweep's cutoff must
|
|
|
|
|
// use exactly this layout.
|
|
|
|
|
const sessionTimeLayout = "2006-01-02T15:04:05Z"
|