package main import ( "fmt" "net/url" "os/exec" "runtime" "strings" "time" "verstak/internal/core/util" ) type LinkDTO struct { ID string `json:"id"` NodeID string `json:"nodeId"` Title string `json:"title"` URL string `json:"url"` Hostname string `json:"hostname"` Note string `json:"note"` Source string `json:"source"` CapturedAt string `json:"capturedAt"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } func (a *App) ListLinks(nodeID string) ([]LinkDTO, error) { if err := a.requireVault(); err != nil { return nil, err } if nodeID == "" { return []LinkDTO{}, nil } rows, err := a.db.Query( `SELECT id,node_id,title,url,hostname,note,source,COALESCE(captured_at,''),created_at,updated_at FROM links WHERE node_id = ? ORDER BY created_at DESC, title`, nodeID) if err != nil { return nil, err } defer rows.Close() var out []LinkDTO for rows.Next() { var l LinkDTO if err := rows.Scan(&l.ID, &l.NodeID, &l.Title, &l.URL, &l.Hostname, &l.Note, &l.Source, &l.CapturedAt, &l.CreatedAt, &l.UpdatedAt); err != nil { return nil, err } out = append(out, l) } return out, rows.Err() } func (a *App) UpdateLink(id, title, rawURL, note string) (*LinkDTO, error) { if err := a.requireVault(); err != nil { return nil, err } id = strings.TrimSpace(id) title = strings.TrimSpace(title) rawURL = strings.TrimSpace(rawURL) if id == "" { return nil, fmt.Errorf("link id required") } if rawURL == "" { return nil, fmt.Errorf("url required") } if title == "" { title = linkTitle(rawURL, "") } updatedAt := time.Now().UTC().Format(time.RFC3339) res, err := a.db.Exec( `UPDATE links SET title=?, url=?, hostname=?, note=?, updated_at=? WHERE id=?`, title, rawURL, hostnameForURL(rawURL), note, updatedAt, id) if err != nil { return nil, err } if n, _ := res.RowsAffected(); n == 0 { return nil, fmt.Errorf("link not found") } return a.getLink(id) } func (a *App) DeleteLink(id string) error { if err := a.requireVault(); err != nil { return err } res, err := a.db.Exec(`DELETE FROM links WHERE id = ?`, strings.TrimSpace(id)) if err != nil { return err } if n, _ := res.RowsAffected(); n == 0 { return fmt.Errorf("link not found") } return nil } func (a *App) OpenLink(id string) error { if err := a.requireVault(); err != nil { return err } l, err := a.getLink(id) if err != nil { return err } return openExternalURL(l.URL) } func (a *App) OpenURL(rawURL string) error { if err := a.requireVault(); err != nil { return err } return openExternalURL(rawURL) } func (a *App) createResolvedLink(nodeID, rawURL, title, note, source, capturedAt string) (*LinkDTO, error) { rawURL = strings.TrimSpace(rawURL) if rawURL == "" { return nil, fmt.Errorf("url required") } normalizedURL, ok := normalizeHTTPURL(rawURL) if !ok { return nil, fmt.Errorf("invalid url") } rawURL = normalizedURL title = linkTitle(rawURL, title) now := time.Now().UTC().Format(time.RFC3339) id := util.UUID7() if _, err := a.db.Exec( `INSERT INTO links (id,node_id,title,url,hostname,note,source,captured_at,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)`, id, nodeID, title, rawURL, hostnameForURL(rawURL), note, source, capturedAt, now, now); err != nil { return nil, err } return a.getLink(id) } func (a *App) getLink(id string) (*LinkDTO, error) { var l LinkDTO err := a.db.QueryRow( `SELECT id,node_id,title,url,hostname,note,source,COALESCE(captured_at,''),created_at,updated_at FROM links WHERE id = ?`, strings.TrimSpace(id)). Scan(&l.ID, &l.NodeID, &l.Title, &l.URL, &l.Hostname, &l.Note, &l.Source, &l.CapturedAt, &l.CreatedAt, &l.UpdatedAt) if err != nil { return nil, err } return &l, nil } func linkTitle(rawURL, title string) string { title = strings.TrimSpace(title) if title != "" { return firstLineTitle(title, title) } if h := hostnameForURL(rawURL); h != "" { return h } return rawURL } func isURLLike(text string) bool { _, ok := normalizeHTTPURL(text) return ok } func normalizeHTTPURL(text string) (string, bool) { text = strings.TrimSpace(text) if text == "" { return "", false } if strings.ContainsAny(text, " \t\r\n") || strings.Contains(text, "@") { return "", false } u, err := url.Parse(text) if err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != "" { return text, true } if u != nil && u.Scheme != "" { return "", false } withScheme := "https://" + text u, err = url.Parse(withScheme) if err != nil || u.Host == "" { return "", false } host := u.Hostname() if host == "" || !strings.Contains(host, ".") { return "", false } return withScheme, true } func openExternalURL(rawURL string) error { normalizedURL, ok := normalizeHTTPURL(rawURL) if !ok { return fmt.Errorf("invalid url") } var cmd *exec.Cmd switch runtime.GOOS { case "darwin": cmd = exec.Command("open", normalizedURL) case "windows": cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", normalizedURL) default: cmd = exec.Command("xdg-open", normalizedURL) } return cmd.Start() }