package domain import ( "crypto/rand" "encoding/hex" "encoding/json" "fmt" "regexp" "strings" "time" ) // NewID generates a readable prefixed ID (e.g. th_4f1c09e2b7a35d68c0e1f2a3). 96 random bits keep // collisions out of reach; older IDs with 40 bits stay valid (FLAW-32). func NewID(prefix string) string { b := make([]byte, 12) _, _ = rand.Read(b) return fmt.Sprintf("%s_%s", prefix, hex.EncodeToString(b)) } // NowISO returns the current UTC timestamp in ISO-8601 format func NowISO() string { return time.Now().UTC().Format(time.RFC3339) } // A hashtag starts the text or follows whitespace or "(". Fragments in links (example.com/blog#ueberschrift) // and words like "C#" are not tags. The browser uses the same rule (lib/markdown.tsx, lib/api.ts). var hashtagRegex = regexp.MustCompile(`(?:^|[\s(])#([a-zA-Z0-9äöüÄÖÜß_-]+)`) // legacyHashtagRegex is the rule before anchors were excluded; only used to clean up stored tags. var legacyHashtagRegex = regexp.MustCompile(`#([a-zA-Z0-9äöüÄÖÜß_-]+)`) // StaleAnchorTags returns tags the old rule found in content that the current rule does not, i.e. tags that // only came from URL fragments and similar. func StaleAnchorTags(content string) []string { current := map[string]bool{} for _, t := range ExtractTags(content) { current[t] = true } var stale []string seen := map[string]bool{} for _, m := range legacyHashtagRegex.FindAllStringSubmatch(content, -1) { t := strings.ToLower(m[1]) if !current[t] && !seen[t] { seen[t] = true stale = append(stale, t) } } return stale } // ExtractTags extracts #hashtags from content func ExtractTags(content string) []string { matches := hashtagRegex.FindAllStringSubmatch(content, -1) seen := make(map[string]bool) tags := make([]string, 0, len(matches)) for _, m := range matches { if len(m) > 1 { tag := strings.ToLower(m[1]) if !seen[tag] { seen[tag] = true tags = append(tags, tag) } } } return tags } // FormatGermanDateTime formats an ISO-8601 string into "25. Okt. 2026 - 15:40 Uhr" func FormatGermanDateTime(isoStr string) string { if isoStr == "" { return "" } var t time.Time var err error formats := []string{ time.RFC3339Nano, time.RFC3339, "2006-01-02T15:04:05Z07:00", "2006-01-02T15:04:05", "2006-01-02 15:04:05", "2006-01-02", } for _, f := range formats { if parsed, parseErr := time.Parse(f, isoStr); parseErr == nil { t = parsed err = nil break } else { err = parseErr } } if err != nil { return isoStr } t = t.Local() germanMonths := map[time.Month]string{ time.January: "Jan.", time.February: "Feb.", time.March: "März", time.April: "Apr.", time.May: "Mai", time.June: "Juni", time.July: "Juli", time.August: "Aug.", time.September: "Sept.", time.October: "Okt.", time.November: "Nov.", time.December: "Dez.", } mStr := germanMonths[t.Month()] if mStr == "" { mStr = t.Format("Jan") } return fmt.Sprintf("%d. %s %d - %02d:%02d Uhr", t.Day(), mStr, t.Year(), t.Hour(), t.Minute()) } type User struct { ID string `json:"id"` Email string `json:"email"` Username string `json:"username,omitempty"` PasswordHash string `json:"-"` DisplayName string `json:"displayName"` AvatarURL string `json:"avatarUrl,omitempty"` Role string `json:"role"` // admin | member IsActive bool `json:"isActive"` TokenVersion int `json:"tokenVersion"` AlarmChannels []string `json:"alarmChannels"` // email | discord | webhook AlarmEmail string `json:"alarmEmail"` DiscordWebhookURL string `json:"discordWebhookUrl"` CustomWebhookURL string `json:"customWebhookUrl"` NotifyBeforeMinutes int `json:"notifyBeforeMinutes"` OrganizationID string `json:"organizationId,omitempty"` CreatedAt string `json:"createdAt"` // Only filled for the user's own profile (GET /auth/me, PATCH /auth/settings) NotificationSettings *NotificationSettings `json:"notificationSettings,omitempty"` } // NotificationSettings: which events reach a user over which channel, the daily summary and a pause. type NotificationSettings struct { Paused bool `json:"paused"` IncludeOwnWatched bool `json:"includeOwnWatched"` Events NotificationEvents `json:"events"` DailySummary DailySummary `json:"dailySummary"` } type NotificationEvents struct { Due ChannelChoice `json:"due"` // task alarms (sent at once) Shared ChannelChoice `json:"shared"` // something was shared with me TaskDone ChannelChoice `json:"taskDone"` // a shared task was completed Watched ChannelChoice `json:"watched"` // a watched task changed } type ChannelChoice struct { Email bool `json:"email"` Discord bool `json:"discord"` Webhook bool `json:"webhook"` // own webhook (customWebhookUrl), JSON payload Push bool `json:"push"` // web push to the user's browsers and phones } // Any reports whether at least one channel is chosen. func (c ChannelChoice) Any() bool { return c.Email || c.Discord || c.Webhook || c.Push } type DailySummary struct { Enabled bool `json:"enabled"` Time string `json:"time"` // HH:MM, server time zone Email bool `json:"email"` Discord bool `json:"discord"` Webhook bool `json:"webhook"` Push bool `json:"push"` } // DefaultNotificationSettings for users who never saved any; task alarms follow the old alarm channels. func DefaultNotificationSettings(alarmChannels []string) NotificationSettings { due := ChannelChoice{} for _, c := range alarmChannels { due.Email = due.Email || c == "email" due.Discord = due.Discord || c == "discord" due.Webhook = due.Webhook || c == "webhook" } return NotificationSettings{ Events: NotificationEvents{ Due: due, Shared: ChannelChoice{Email: true}, Watched: ChannelChoice{Email: true, Discord: true}, }, DailySummary: DailySummary{Time: "07:00", Email: true}, } } // Channels returns the choice for an event kind (due, shared, taskDone, watched). func (n NotificationSettings) Channels(kind string) ChannelChoice { switch kind { case "due": return n.Events.Due case "shared": return n.Events.Shared case "taskDone": return n.Events.TaskDone case "watched": return n.Events.Watched } return ChannelChoice{} } type UserInvite struct { ID string `json:"id"` Code string `json:"code"` Email string `json:"email,omitempty"` Role string `json:"role"` // member | admin OrganizationID string `json:"organizationId,omitempty"` OrganizationName string `json:"organizationName,omitempty"` CreatedBy string `json:"createdBy"` ExpiresAt string `json:"expiresAt"` UsedBy string `json:"usedBy,omitempty"` CreatedAt string `json:"createdAt"` } type ThoughtStatus string const ( StatusInbox ThoughtStatus = "inbox" StatusKept ThoughtStatus = "kept" StatusDiscarded ThoughtStatus = "discarded" StatusConverted ThoughtStatus = "converted" ) type SubTask struct { ID string `json:"id"` Title string `json:"title"` IsCompleted bool `json:"isCompleted"` CreatedAt string `json:"createdAt,omitempty"` } type Thought struct { // IsWatching: the requesting user gets notified about changes (task watchers) IsWatching bool `json:"isWatching,omitempty"` ID string `json:"id"` UserID string `json:"userId"` Content string `json:"content"` Tags []string `json:"tags"` Priority string `json:"priority"` // low | normal | high | urgent Source string `json:"source"` // manual | voice | webhook Status ThoughtStatus `json:"status"` IsCompleted bool `json:"isCompleted"` ConvertedType string `json:"convertedType,omitempty"` // task | document | list_item | document_block ConvertedRefID string `json:"convertedRefId,omitempty"` // referenced entity ID DocID string `json:"docId,omitempty"` BlockID string `json:"blockId,omitempty"` DueDate string `json:"dueDate,omitempty"` TaskListID string `json:"taskListId,omitempty"` KanbanStatus string `json:"kanbanStatus,omitempty"` // todo | in_progress | done Notes string `json:"notes,omitempty"` Subtasks []SubTask `json:"subtasks"` Assignees []string `json:"assignees"` // user IDs; only people with access to the task list Recurrence string `json:"recurrence,omitempty"` // "" | daily | weekly | monthly | yearly AlarmSent bool `json:"alarmSent,omitempty"` SortOrder int `json:"sortOrder"` CreatedAt string `json:"createdAt"` TriagedAt string `json:"triagedAt,omitempty"` } type ThoughtReorderItem struct { ID string `json:"id"` SortOrder int `json:"sortOrder"` KanbanStatus *string `json:"kanbanStatus,omitempty"` } type ListItemReorderItem struct { ID string `json:"id"` SortOrder int `json:"sortOrder"` Category *string `json:"category,omitempty"` Content *string `json:"content,omitempty"` } type TaskListShare struct { ID string `json:"id"` TaskListID string `json:"taskListId"` TargetType string `json:"targetType"` // team | user TargetID string `json:"targetId"` TargetName string `json:"targetName,omitempty"` Email string `json:"email,omitempty"` AvatarURL string `json:"avatarUrl,omitempty"` Permission string `json:"permission"` // editor | viewer CreatedAt string `json:"createdAt"` } type TaskListMember struct { UserID string `json:"userId"` DisplayName string `json:"displayName"` Email string `json:"email"` AvatarURL string `json:"avatarUrl,omitempty"` Role string `json:"role"` // owner | member } type KanbanColumn struct { ID string `json:"id"` Name string `json:"name"` Color string `json:"color,omitempty"` IsDone bool `json:"isDone,omitempty"` } type TaskList struct { ID string `json:"id"` UserID string `json:"userId"` SpaceID string `json:"spaceId,omitempty"` SpaceName string `json:"spaceName,omitempty"` OwnerName string `json:"ownerName,omitempty"` IsOwner bool `json:"isOwner"` Permission string `json:"permission,omitempty"` // owner | editor | viewer Members []TaskListMember `json:"members,omitempty"` Shares []TaskListShare `json:"shares,omitempty"` Columns []KanbanColumn `json:"columns,omitempty"` Title string `json:"title"` Icon string `json:"icon"` Color string `json:"color"` TasksCount int `json:"tasksCount,omitempty"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } // Organization is defined by server admins; each user belongs to at most one. type Organization struct { ID string `json:"id"` Name string `json:"name"` MemberCount int `json:"memberCount"` CreatedAt string `json:"createdAt"` } type Team struct { ID string `json:"id"` OrgID string `json:"orgId,omitempty"` Name string `json:"name"` Description string `json:"description"` Members []TeamMember `json:"members"` CreatedAt string `json:"createdAt"` } type TeamMember struct { TeamID string `json:"teamId"` UserID string `json:"userId"` DisplayName string `json:"displayName"` Email string `json:"email"` AvatarURL string `json:"avatarUrl,omitempty"` Role string `json:"role"` // admin | member JoinedAt string `json:"joinedAt"` } type SystemSettings struct { SharingMode string `json:"sharingMode"` // private | organizations | open AllowRegistration bool `json:"allowRegistration"` InstanceName string `json:"instanceName"` SMTPHost string `json:"smtpHost"` SMTPPort string `json:"smtpPort"` SMTPUser string `json:"smtpUser"` // SMTPPass is never sent to the browser (FLAW-34): reads leave it empty and set SMTPPassSet; // writing it empty keeps the stored password SMTPPass string `json:"smtpPass"` SMTPPassSet bool `json:"smtpPassSet,omitempty"` SMTPFrom string `json:"smtpFrom"` // SMTPFromName is shown as sender when SMTPFrom is a bare address (empty = "MindSift") SMTPFromName string `json:"smtpFromName"` SMTPTLS bool `json:"smtpTls"` SMTPSkipVerify bool `json:"smtpSkipVerify"` // accept self-signed certificates // AllowPrivateWebhooks lets alarm webhooks reach LAN addresses (and plain http); localhost and cloud // metadata stay blocked AllowPrivateWebhooks bool `json:"allowPrivateWebhooks"` SMTPConfiguredViaEnv bool `json:"smtpConfiguredViaEnv,omitempty"` // Automatic backups: on/off, daily time (HH:MM, server time zone) and how many days to keep them BackupEnabled bool `json:"backupEnabled"` BackupTime string `json:"backupTime"` BackupRetentionDays int `json:"backupRetentionDays"` } // UnmarshalJSON accepts smtpPort as text or number: older clients sent a number, which made the whole // settings request fail with "Ungültiges JSON". func (s *SystemSettings) UnmarshalJSON(data []byte) error { type plain SystemSettings aux := struct { *plain SMTPPort json.RawMessage `json:"smtpPort"` }{plain: (*plain)(s)} if err := json.Unmarshal(data, &aux); err != nil { return err } port := strings.TrimSpace(string(aux.SMTPPort)) switch { case port == "" || port == "null": case strings.HasPrefix(port, `"`): var str string if err := json.Unmarshal(aux.SMTPPort, &str); err != nil { return err } s.SMTPPort = strings.TrimSpace(str) default: var n json.Number if err := json.Unmarshal(aux.SMTPPort, &n); err != nil { return err } s.SMTPPort = n.String() } return nil } type DocumentBlock struct { ID string `json:"id"` DocumentID string `json:"documentId,omitempty"` Type string `json:"type"` // h1 | h2 | h3 | paragraph | todo | bullet | code | callout | divider | table | progress Content string `json:"content"` Checked bool `json:"checked,omitempty"` Lang string `json:"lang,omitempty"` Level int `json:"level"` Position float64 `json:"position"` Collapsed bool `json:"collapsed,omitempty"` TaskID string `json:"taskId,omitempty"` } func CompileBlocksToMarkdown(blocks []DocumentBlock) string { var sb strings.Builder for _, b := range blocks { lvl := b.Level if lvl < 0 { lvl = 0 } indent := strings.Repeat(" ", lvl) switch b.Type { case "h1": sb.WriteString(indent + "- # " + b.Content + "\n") case "h2": sb.WriteString(indent + "- ## " + b.Content + "\n") case "h3": sb.WriteString(indent + "- ### " + b.Content + "\n") case "todo": mark := "[ ]" if b.Checked { mark = "[x]" } sb.WriteString(indent + "- " + mark + " " + b.Content + "\n") case "code": sb.WriteString(indent + "- ```" + b.Lang + "\n" + b.Content + "\n" + indent + " ```\n") case "divider": sb.WriteString(indent + "- ---\n") case "numbered": sb.WriteString(indent + "1. " + b.Content + "\n") default: sb.WriteString(indent + "- " + b.Content + "\n") } } return sb.String() } type DocShare struct { ID string `json:"id"` DocID string `json:"docId"` TargetType string `json:"targetType,omitempty"` // user | team TargetID string `json:"targetId,omitempty"` TargetName string `json:"targetName,omitempty"` UserID string `json:"userId,omitempty"` UserEmail string `json:"userEmail,omitempty"` DisplayName string `json:"displayName,omitempty"` Permission string `json:"permission"` // read | write CreatedBy string `json:"createdBy"` CreatedAt string `json:"createdAt"` } type Document struct { ID string `json:"id"` UserID string `json:"userId"` SpaceID string `json:"spaceId,omitempty"` Title string `json:"title"` Icon string `json:"icon"` Content string `json:"content"` MarkdownCache string `json:"markdownCache,omitempty"` Blocks []DocumentBlock `json:"blocks"` Tags []string `json:"tags"` FolderID string `json:"folderId,omitempty"` FolderPath string `json:"folderPath,omitempty"` IsPinned bool `json:"isPinned"` IsArchived bool `json:"isArchived"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` // Collaboration & Permissions Permission string `json:"permission"` // owner | write | read IsOwner bool `json:"isOwner"` OwnerName string `json:"ownerName,omitempty"` SharedWith []DocShare `json:"sharedWith,omitempty"` SharedFolderID string `json:"sharedFolderId,omitempty"` SubFolderPath string `json:"subFolderPath,omitempty"` IsSharedFolder bool `json:"isSharedFolder,omitempty"` FolderPermission string `json:"folderPermission,omitempty"` } type TagSummary struct { Tag string `json:"tag"` Count int `json:"count"` } type MatchedTagBlock struct { RootBlock DocumentBlock `json:"rootBlock"` Children []DocumentBlock `json:"children"` } type TagDocumentGroup struct { DocID string `json:"docId"` Title string `json:"title"` Icon string `json:"icon"` FolderPath string `json:"folderPath"` UpdatedAt string `json:"updatedAt"` IsOwner bool `json:"isOwner"` Permission string `json:"permission"` OwnerName string `json:"ownerName,omitempty"` MatchedBlocks []MatchedTagBlock `json:"matchedBlocks"` } type TagDetails struct { Tag string `json:"tag"` NoteContent string `json:"noteContent"` NoteUpdatedAt string `json:"noteUpdatedAt,omitempty"` Documents []TagDocumentGroup `json:"documents"` TotalMatches int `json:"totalMatches"` } type ListShare struct { ID string `json:"id"` ListID string `json:"listId"` TargetType string `json:"targetType"` // team | user TargetID string `json:"targetId"` TargetName string `json:"targetName,omitempty"` Email string `json:"email,omitempty"` Role string `json:"role"` // editor | viewer CreatedBy string `json:"createdBy"` CreatedAt string `json:"createdAt"` } type ListItem struct { ID string `json:"id"` ListID string `json:"listId"` Content string `json:"content"` Category string `json:"category,omitempty"` Quantity string `json:"quantity,omitempty"` IsCompleted bool `json:"isCompleted"` CompletedBy string `json:"completedBy,omitempty"` CompletedByName string `json:"completedByName,omitempty"` CreatedBy string `json:"createdBy,omitempty"` CreatedByName string `json:"createdByName,omitempty"` SortOrder int `json:"sortOrder"` CreatedAt string `json:"createdAt"` CompletedAt string `json:"completedAt,omitempty"` } type SharedList struct { ID string `json:"id"` UserID string `json:"userId"` OwnerName string `json:"ownerName,omitempty"` IsOwner bool `json:"isOwner"` Role string `json:"role,omitempty"` // editor | viewer Title string `json:"title"` Icon string `json:"icon"` ListType string `json:"listType"` // shopping | checklist | vacation Items []ListItem `json:"items"` Shares []ListShare `json:"shares,omitempty"` Members []TaskListMember `json:"members,omitempty"` // owner + everyone the list is shared with UpdatedAt string `json:"updatedAt"` CreatedAt string `json:"createdAt"` } // UserTheme is a custom CSS theme of one user. type UserTheme struct { ID string `json:"id"` Name string `json:"name"` CSS string `json:"css"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } type APIKey struct { ID string `json:"id"` UserID string `json:"userId"` Name string `json:"name"` KeyPrefix string `json:"keyPrefix"` KeyHash string `json:"-"` RawKey string `json:"rawKey,omitempty"` // Only returned once on creation Scopes string `json:"scopes"` LastUsedAt string `json:"lastUsedAt,omitempty"` CreatedAt string `json:"createdAt"` } type WSMessage struct { Event string `json:"event"` // ITEM_CREATED | ITEM_UPDATED | ITEM_TOGGLED | ITEM_DELETED | LIST_CLEARED | USER_PRESENCE ListID string `json:"list_id"` Payload interface{} `json:"payload"` Timestamp string `json:"timestamp"` } type DashboardStats struct { InboxCount int `json:"inboxCount"` TasksOpenCount int `json:"tasksOpenCount"` TasksDoneCount int `json:"tasksDoneCount"` DocsCount int `json:"docsCount"` DocsTodosOpenCount int `json:"docsTodosOpenCount"` ListsCount int `json:"listsCount"` ListItemsOpenCount int `json:"listItemsOpenCount"` // unchecked items in all lists the user sees DiscardedCount int `json:"discardedCount"` } type FullExport struct { ExportedAt string `json:"exportedAt"` Version string `json:"version"` User User `json:"user"` Thoughts []Thought `json:"thoughts"` Documents []Document `json:"documents"` Lists []SharedList `json:"lists"` TaskLists []TaskList `json:"taskLists,omitempty"` Themes []UserTheme `json:"themes,omitempty"` } type SharedFolder struct { ID string `json:"id"` OwnerID string `json:"ownerId"` OwnerName string `json:"ownerName,omitempty"` Name string `json:"name"` MountPath string `json:"mountPath"` IsOwner bool `json:"isOwner"` Permission string `json:"permission"` // owner | write | read Shares []FolderShare `json:"shares,omitempty"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } type FolderShare struct { ID string `json:"id"` FolderID string `json:"folderId"` TargetType string `json:"targetType"` // team | user TargetID string `json:"targetId"` TargetName string `json:"targetName,omitempty"` Email string `json:"email,omitempty"` AvatarURL string `json:"avatarUrl,omitempty"` Permission string `json:"permission"` // read | write CreatedBy string `json:"createdBy"` CreatedAt string `json:"createdAt"` }