Files
MengMengCode 9fc3f1c5b8 feat: add Telegram API URL handling and version comparison utilities
- Implemented `telegramAPIURL` and `validateTelegramAPIURL` functions for constructing and validating Telegram API URLs.
- Added semantic versioning utilities in `version.go` to compare versions and validate semantic version formats.
- Created tests for version comparison logic in `version_test.go`.
- Introduced IMS call handling in `call_runtime.go`, including methods for dialing, answering, and hanging up calls.
- Added tests for incoming call handling and validation in `call_runtime_test.go`.
- Developed a new `PluginsCard` component for managing plugins via URL or file upload in the web interface.
- Implemented plugin management functions in `extensions.ts` and created an `ExtensionPage` for displaying plugin contributions.
2026-08-09 19:28:12 +08:00

121 lines
3.3 KiB
Go

package update
import (
"fmt"
"strconv"
"strings"
)
type semanticVersion struct {
major int
minor int
patch int
prerelease string
}
// IsNewerVersion reports whether latest is newer than current. Both values
// may include the conventional v prefix, prerelease suffixes, and build
// metadata. Invalid release versions are rejected instead of triggering a
// downgrade or an arbitrary file replacement.
func IsNewerVersion(current, latest string) (bool, error) {
currentVersion, err := parseSemanticVersion(current)
if err != nil {
return false, fmt.Errorf("current version: %w", err)
}
latestVersion, err := parseSemanticVersion(latest)
if err != nil {
return false, fmt.Errorf("latest version: %w", err)
}
if currentVersion.major != latestVersion.major {
return latestVersion.major > currentVersion.major, nil
}
if currentVersion.minor != latestVersion.minor {
return latestVersion.minor > currentVersion.minor, nil
}
if currentVersion.patch != latestVersion.patch {
return latestVersion.patch > currentVersion.patch, nil
}
if currentVersion.prerelease == latestVersion.prerelease {
return false, nil
}
if currentVersion.prerelease != "" && latestVersion.prerelease == "" {
return true, nil
}
if currentVersion.prerelease == "" {
return false, nil
}
return comparePrerelease(currentVersion.prerelease, latestVersion.prerelease) < 0, nil
}
func parseSemanticVersion(raw string) (semanticVersion, error) {
value := strings.TrimPrefix(strings.TrimSpace(raw), "v")
if build := strings.IndexByte(value, '+'); build >= 0 {
value = value[:build]
}
prerelease := ""
hasPrerelease := false
if dash := strings.IndexByte(value, '-'); dash >= 0 {
hasPrerelease = true
prerelease = value[dash+1:]
value = value[:dash]
}
parts := strings.Split(value, ".")
if len(parts) != 3 || (hasPrerelease && prerelease == "") {
return semanticVersion{}, fmt.Errorf("%q is not a semantic version", raw)
}
numbers := make([]int, 3)
for index, part := range parts {
if part == "" || (len(part) > 1 && part[0] == '0') {
return semanticVersion{}, fmt.Errorf("%q is not a semantic version", raw)
}
value, err := strconv.Atoi(part)
if err != nil || value < 0 {
return semanticVersion{}, fmt.Errorf("%q is not a semantic version", raw)
}
numbers[index] = value
}
if strings.ContainsAny(prerelease, " \t\r\n") {
return semanticVersion{}, fmt.Errorf("%q is not a semantic version", raw)
}
return semanticVersion{
major: numbers[0],
minor: numbers[1],
patch: numbers[2],
prerelease: prerelease,
}, nil
}
func comparePrerelease(left, right string) int {
leftParts := strings.Split(left, ".")
rightParts := strings.Split(right, ".")
for index := 0; index < len(leftParts) && index < len(rightParts); index++ {
if leftParts[index] == rightParts[index] {
continue
}
leftNumber, leftErr := strconv.Atoi(leftParts[index])
rightNumber, rightErr := strconv.Atoi(rightParts[index])
switch {
case leftErr == nil && rightErr == nil:
if leftNumber < rightNumber {
return -1
}
return 1
case leftErr == nil:
return -1
case rightErr == nil:
return 1
case leftParts[index] < rightParts[index]:
return -1
default:
return 1
}
}
if len(leftParts) < len(rightParts) {
return -1
}
if len(leftParts) > len(rightParts) {
return 1
}
return 0
}