Keep read receipts for the other person only, and let photos, voice, and files stay cached until they are actually replaced.
332 lines
12 KiB
Go
332 lines
12 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
|
|
type Role string
|
|
|
|
const (
|
|
RoleStudent Role = "student"
|
|
RoleMentor Role = "mentor"
|
|
RoleAdmin Role = "admin"
|
|
)
|
|
|
|
func (r Role) String() string { return string(r) }
|
|
|
|
// IsStaff reports whether the role is exempt from student heart/lockout limits.
|
|
func (r Role) IsStaff() bool {
|
|
return r == RoleAdmin || r == RoleMentor
|
|
}
|
|
|
|
type User struct {
|
|
ID int64 `json:"id"`
|
|
Email string `json:"email"`
|
|
Username string `json:"username"`
|
|
PasswordHash string `json:"-"`
|
|
Name string `json:"name"`
|
|
Role Role `json:"role"`
|
|
XP int `json:"xp"`
|
|
Hearts int `json:"hearts"`
|
|
HeartsUpdatedAt time.Time `json:"heartsUpdatedAt"`
|
|
StreakCurrent int `json:"streakCurrent"`
|
|
StreakLongest int `json:"streakLongest"`
|
|
LastActivityDate *time.Time `json:"lastActivityDate,omitempty"`
|
|
IsLocked bool `json:"isLocked"`
|
|
LockedAt *time.Time `json:"lockedAt,omitempty"`
|
|
UnlockedBy *int64 `json:"unlockedBy,omitempty"`
|
|
IsActive bool `json:"isActive"`
|
|
AvatarVariant string `json:"avatarVariant"`
|
|
AvatarPalette string `json:"avatarPalette"`
|
|
AvatarPhoto string `json:"avatarPhoto,omitempty"`
|
|
Phone string `json:"phone"`
|
|
TelegramID *int64 `json:"-"`
|
|
TelegramUsername string `json:"-"`
|
|
SecurityQuestion string `json:"securityQuestion,omitempty"`
|
|
SecurityAnswerHash string `json:"-"`
|
|
HasSecurityAnswer bool `json:"hasSecurityAnswer"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
}
|
|
|
|
type Certificate struct {
|
|
ID int64 `json:"id"`
|
|
UserID int64 `json:"userId"`
|
|
PublicID string `json:"publicId"`
|
|
FullName string `json:"fullName"`
|
|
IssuedAt time.Time `json:"issuedAt"`
|
|
RevokedAt *time.Time `json:"revokedAt,omitempty"`
|
|
}
|
|
|
|
type PhysicalOrderStatus string
|
|
|
|
const (
|
|
PhysicalRequested PhysicalOrderStatus = "requested"
|
|
PhysicalPaid PhysicalOrderStatus = "paid"
|
|
PhysicalShipped PhysicalOrderStatus = "shipped"
|
|
PhysicalCancelled PhysicalOrderStatus = "cancelled"
|
|
)
|
|
|
|
type CertificatePhysicalOrder struct {
|
|
ID int64 `json:"id"`
|
|
CertificateID int64 `json:"certificateId"`
|
|
UserID int64 `json:"userId"`
|
|
Status PhysicalOrderStatus `json:"status"`
|
|
Note string `json:"note"`
|
|
RecipientName string `json:"recipientName"`
|
|
Phone string `json:"phone"`
|
|
Address string `json:"address"`
|
|
City string `json:"city"`
|
|
PostalCode string `json:"postalCode"`
|
|
TrackingCode string `json:"trackingCode"`
|
|
WindowEndsAt time.Time `json:"windowEndsAt"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
UserName string `json:"userName,omitempty"`
|
|
PublicID string `json:"publicId,omitempty"`
|
|
}
|
|
|
|
type UnlockRequest struct {
|
|
ID int64 `json:"id"`
|
|
UserID int64 `json:"userId"`
|
|
Note string `json:"note"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
|
|
UserName string `json:"userName,omitempty"`
|
|
}
|
|
|
|
type NotificationPrefs struct {
|
|
UserID int64 `json:"userId"`
|
|
Progress bool `json:"progress"`
|
|
Gamification bool `json:"gamification"`
|
|
Mentor bool `json:"mentor"`
|
|
Event bool `json:"event"`
|
|
}
|
|
|
|
type Chapter struct {
|
|
ID int64 `json:"id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Icon string `json:"icon"`
|
|
SortOrder int `json:"sortOrder"`
|
|
}
|
|
|
|
type Lesson struct {
|
|
ID int64 `json:"id"`
|
|
ChapterID int64 `json:"chapterId"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
VideoKey string `json:"videoKey"`
|
|
DurationSeconds int `json:"durationSeconds"`
|
|
CompletionThresholdPct int `json:"completionThresholdPct"`
|
|
RequiresLessonID *int64 `json:"requiresLessonId,omitempty"`
|
|
X int `json:"x"`
|
|
Y int `json:"y"`
|
|
SortOrder int `json:"sortOrder"`
|
|
XPReward int `json:"xpReward"`
|
|
IsActive bool `json:"isActive"`
|
|
}
|
|
|
|
type MCQQuestion struct {
|
|
ID int64 `json:"id"`
|
|
LessonID int64 `json:"lessonId"`
|
|
Position int `json:"position"`
|
|
Question string `json:"question"`
|
|
Options []string `json:"options"`
|
|
AnswerIndex int `json:"answerIndex"`
|
|
Explanation string `json:"explanation"`
|
|
}
|
|
|
|
type LessonProgress struct {
|
|
UserID int64 `json:"userId"`
|
|
LessonID int64 `json:"lessonId"`
|
|
WatchedSeconds float64 `json:"watchedSeconds"`
|
|
WatchedPct float64 `json:"watchedPct"`
|
|
LastPosition float64 `json:"lastPosition"`
|
|
QuizUnlocked bool `json:"quizUnlocked"`
|
|
PassedQuiz bool `json:"passedQuiz"`
|
|
QuizCompletedAt *time.Time `json:"quizCompletedAt,omitempty"`
|
|
}
|
|
|
|
type WatchSession struct {
|
|
ID int64 `json:"id"`
|
|
UserID int64 `json:"userId"`
|
|
LessonID int64 `json:"lessonId"`
|
|
StartedAt time.Time `json:"startedAt"`
|
|
LastHeartbeatAt time.Time `json:"lastHeartbeatAt"`
|
|
LastPosition float64 `json:"lastPosition"`
|
|
}
|
|
|
|
type QuizQuestionView struct {
|
|
ID int64 `json:"id"`
|
|
Position int `json:"position"`
|
|
Question string `json:"question"`
|
|
Options []string `json:"options"`
|
|
}
|
|
|
|
type QuizAttempt struct {
|
|
ID int64 `json:"id"`
|
|
UserID int64 `json:"userId"`
|
|
LessonID int64 `json:"lessonId"`
|
|
Answers []int `json:"-"`
|
|
CorrectCount int `json:"correctCount"`
|
|
Total int `json:"total"`
|
|
ScorePct float64 `json:"scorePct"`
|
|
Status string `json:"status"`
|
|
HeartsLost int `json:"heartsLost"`
|
|
PassedAt *time.Time `json:"passedAt,omitempty"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
}
|
|
|
|
type Event struct {
|
|
ID int64 `json:"id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
EventType string `json:"eventType"`
|
|
ExternalURL string `json:"externalUrl"`
|
|
StartsAt time.Time `json:"startsAt"`
|
|
EndsAt time.Time `json:"endsAt"`
|
|
IsActive bool `json:"isActive"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
CreatedBy *int64 `json:"-"`
|
|
Rsvped bool `json:"rsvped,omitempty"`
|
|
}
|
|
|
|
// Challenge is a timed XP goal (typically monthly) for students.
|
|
type Challenge struct {
|
|
ID int64 `json:"id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
TargetXP int `json:"targetXp"`
|
|
StartsAt time.Time `json:"startsAt"`
|
|
EndsAt time.Time `json:"endsAt"`
|
|
IsActive bool `json:"isActive"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
CreatedBy *int64 `json:"-"`
|
|
// Progress fields for the requesting student (optional).
|
|
MyXP int `json:"myXp,omitempty"`
|
|
Completed bool `json:"completed,omitempty"`
|
|
}
|
|
|
|
type ChatMessage struct {
|
|
ID int64 `json:"id"`
|
|
UserID int64 `json:"userId"`
|
|
MentorID int64 `json:"mentorId"`
|
|
SenderRole Role `json:"senderRole"`
|
|
Body string `json:"body"`
|
|
ReadAt *time.Time `json:"readAt,omitempty"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
ReplyTo *int64 `json:"replyTo,omitempty"`
|
|
PinnedAt *time.Time `json:"pinnedAt,omitempty"`
|
|
Pinned bool `json:"pinned"`
|
|
EditedAt *time.Time `json:"editedAt,omitempty"`
|
|
Attachment *Attachment `json:"attachment,omitempty"`
|
|
Reactions []ChatReaction `json:"reactions,omitempty"`
|
|
}
|
|
|
|
type ChatReaction struct {
|
|
Emoji string `json:"emoji"`
|
|
Count int `json:"count"`
|
|
Mine bool `json:"mine"`
|
|
}
|
|
|
|
type Attachment struct {
|
|
Type string `json:"type"` // image | video | audio | file
|
|
URL string `json:"url"`
|
|
Name string `json:"name"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
type Notification struct {
|
|
ID int64 `json:"id"`
|
|
UserID int64 `json:"userId"`
|
|
Category string `json:"category"`
|
|
Type string `json:"type"`
|
|
Title string `json:"title"`
|
|
Body string `json:"body"`
|
|
Route string `json:"route"`
|
|
Data json.RawMessage `json:"data"`
|
|
ReadAt *time.Time `json:"readAt,omitempty"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
}
|
|
|
|
type InviteStatus string
|
|
|
|
const (
|
|
InviteActive InviteStatus = "active"
|
|
InvitePaused InviteStatus = "paused"
|
|
InviteRevoked InviteStatus = "revoked"
|
|
InviteExhausted InviteStatus = "exhausted"
|
|
)
|
|
|
|
// RegistrationInvite is a capacity-limited membership registration link.
|
|
type RegistrationInvite struct {
|
|
ID int64 `json:"id"`
|
|
Label string `json:"label"`
|
|
MaxUses int `json:"maxUses"`
|
|
UsedCount int `json:"usedCount"`
|
|
Remaining int `json:"remaining"`
|
|
Status InviteStatus `json:"status"`
|
|
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
|
CreatedBy *int64 `json:"createdBy,omitempty"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
// Token is the raw invite token; only populated when decrypting for admin URL display.
|
|
Token string `json:"token,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
}
|
|
|
|
// ChatExportToken is a time-limited, single-use download link for the project chat archive.
|
|
type ChatExportToken struct {
|
|
ID int64 `json:"id"`
|
|
Label string `json:"label"`
|
|
ExpiresAt time.Time `json:"expiresAt"`
|
|
RevokedAt *time.Time `json:"revokedAt,omitempty"`
|
|
UsedAt *time.Time `json:"usedAt,omitempty"`
|
|
CreatedBy *int64 `json:"createdBy,omitempty"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
|
Active bool `json:"active"`
|
|
// Token/URL are populated for admin display after decrypt.
|
|
Token string `json:"token,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
}
|
|
|
|
type WhitelistStatus string
|
|
|
|
const (
|
|
WhitelistAvailable WhitelistStatus = "available"
|
|
WhitelistConsumed WhitelistStatus = "consumed"
|
|
)
|
|
|
|
// RegistrationPhoneWhitelist is a one-shot phone allowed for public registration.
|
|
type RegistrationPhoneWhitelist struct {
|
|
ID int64 `json:"id"`
|
|
Phone string `json:"phone"`
|
|
Status WhitelistStatus `json:"status"`
|
|
ConsumedAt *time.Time `json:"consumedAt,omitempty"`
|
|
ConsumedUserID *int64 `json:"consumedUserId,omitempty"`
|
|
CreatedBy *int64 `json:"createdBy,omitempty"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
}
|
|
|
|
// RegistrationPhoneBlacklist blocks a phone from all registration paths.
|
|
type RegistrationPhoneBlacklist struct {
|
|
ID int64 `json:"id"`
|
|
Phone string `json:"phone"`
|
|
CreatedBy *int64 `json:"createdBy,omitempty"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
}
|
|
|
|
// RegistrationBlacklistAttempt records a failed registration try with a blacklisted phone.
|
|
type RegistrationBlacklistAttempt struct {
|
|
ID int64 `json:"id"`
|
|
Phone string `json:"phone"`
|
|
Path string `json:"path"` // public | invite
|
|
InviteID *int64 `json:"inviteId,omitempty"`
|
|
IP string `json:"ip"`
|
|
UserAgent string `json:"userAgent,omitempty"`
|
|
AttemptedUsername string `json:"attemptedUsername,omitempty"`
|
|
AttemptedEmail string `json:"attemptedEmail,omitempty"`
|
|
AttemptedAt time.Time `json:"attemptedAt"`
|
|
}
|