// Copyright 2026 Moko Consulting // SPDX-License-Identifier: GPL-3.0-or-later package updateserver import ( "context" "fmt" "strings" "time" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" ) // WHMCS module update JSON structures. // WHMCS marketplace modules check for updates via a simple JSON endpoint. type WHMCSUpdate struct { Name string `json:"name"` Version string `json:"version"` Description string `json:"description,omitempty"` DownloadURL string `json:"download_url,omitempty"` Changelog string `json:"changelog,omitempty"` ReleaseDate string `json:"release_date"` Author string `json:"author,omitempty"` AuthorURL string `json:"author_url,omitempty"` SHA256 string `json:"sha256,omitempty"` } // GenerateWHMCSJSON builds a WHMCS-compatible module update response. func GenerateWHMCSJSON(ctx context.Context, repo *repo_model.Repository, licenseKey string) (*WHMCSUpdate, error) { releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{ RepoID: repo.ID, ListOptions: db.ListOptionsAll, IncludeDrafts: false, IncludeTags: false, }) if err != nil { return nil, fmt.Errorf("FindReleases: %w", err) } if err := repo.LoadOwner(ctx); err != nil { return nil, fmt.Errorf("LoadOwner: %w", err) } baseURL := strings.TrimSuffix(setting.AppURL, "/") repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name) cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) meta := resolveExtensionMetadata(ctx, repo, cfg) displayName := meta.DisplayName description := meta.Description maintainer := repo.Owner.Name maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name) if cfg != nil { if cfg.Maintainer != "" { maintainer = cfg.Maintainer } if cfg.MaintainerURL != "" { maintainerURL = cfg.MaintainerURL } } streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) // Find latest stable release. var latestStable *repo_model.Release for _, rel := range releases { if rel.IsDraft || rel.IsTag { continue } ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) if ch == "stable" { if latestStable == nil || rel.CreatedUnix > latestStable.CreatedUnix { latestStable = rel } } } if latestStable == nil { return &WHMCSUpdate{ Name: displayName, Version: "0.0.0", }, nil } if err := latestStable.LoadAttributes(ctx); err != nil { return nil, fmt.Errorf("LoadAttributes: %w", err) } var downloadURL, sha256Hash string for _, att := range latestStable.Attachments { if strings.HasSuffix(strings.ToLower(att.Name), ".zip") && !strings.HasSuffix(att.Name, ".sha256") { downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, latestStable.TagName, att.Name) break } } if downloadURL == "" { downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, latestStable.TagName) } if licenseKey != "" { downloadURL += "?dlid=" + licenseKey } for _, att := range latestStable.Attachments { if strings.HasSuffix(att.Name, ".sha256") { sha256Hash = readSHA256FromSidecar(ctx, att) break } } version := extractVersion(latestStable.TagName) if isStreamName(version, streams) || version == "" { version = extractVersion(latestStable.Title) } if version == "" { version = latestStable.TagName } changelog := "" if latestStable.Note != "" { changelog = latestStable.Note } return &WHMCSUpdate{ Name: displayName, Version: version, Description: description, DownloadURL: downloadURL, Changelog: changelog, ReleaseDate: time.Unix(int64(latestStable.CreatedUnix), 0).Format("2006-01-02"), Author: maintainer, AuthorURL: maintainerURL, SHA256: sha256Hash, }, nil }