mirror of
https://github.com/larksuite/cli.git
synced 2026-07-05 15:47:54 +08:00
* feat(risk): implement confirmation for high-risk write operations * feat(risk): streamline confirmation for high-risk write operations * feat(risk): document approval protocol for high-risk write operations * feat(risk): refine confirmation protocol for high-risk write operations * feat(risk): remove redundant variable declaration in risk test * feat(risk): add 'Yes' flag to various test cases for confirmation
191 lines
4.9 KiB
Go
191 lines
4.9 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package common
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/larksuite/cli/internal/util"
|
|
)
|
|
|
|
func FormatSize(bytes int64) string {
|
|
if bytes < 1024 {
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
if bytes < 1024*1024 {
|
|
return fmt.Sprintf("%.1f KB", float64(bytes)/1024)
|
|
}
|
|
if bytes < 1024*1024*1024 {
|
|
return fmt.Sprintf("%.1f MB", float64(bytes)/1024/1024)
|
|
}
|
|
return fmt.Sprintf("%.1f GB", float64(bytes)/1024/1024/1024)
|
|
}
|
|
|
|
func MaskToken(token string) string {
|
|
if len(token) < 2 {
|
|
return "***"
|
|
}
|
|
if len(token) <= 8 {
|
|
return token[:2] + "***"
|
|
}
|
|
return token[:4] + "..." + token[len(token)-4:]
|
|
}
|
|
|
|
// ParseTime converts time expressions to Unix seconds string.
|
|
//
|
|
// Optional hint: "end" makes day-granularity inputs snap to 23:59:59 instead of 00:00:00.
|
|
//
|
|
// ParseTime("2026-01-01") → 2026-01-01 00:00:00
|
|
// ParseTime("2026-01-01", "end") → 2026-01-01 23:59:59
|
|
//
|
|
// Supported formats: ISO 8601 (with or without time/timezone), date-only, Unix timestamp.
|
|
func ParseTime(input string, hint ...string) (string, error) {
|
|
input = strings.TrimSpace(input)
|
|
isEnd := len(hint) > 0 && hint[0] == "end"
|
|
|
|
// snapDay aligns to start-of-day or end-of-day based on hint.
|
|
snapDay := func(t time.Time) time.Time {
|
|
if isEnd {
|
|
return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, t.Location())
|
|
}
|
|
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
|
}
|
|
|
|
// ISO 8601 with timezone (precise)
|
|
tzFormats := []string{
|
|
time.RFC3339,
|
|
"2006-01-02T15:04Z07:00",
|
|
"2006-01-02T15:04:05Z07:00",
|
|
}
|
|
for _, f := range tzFormats {
|
|
if t, err := time.Parse(f, input); err == nil {
|
|
return fmt.Sprintf("%d", t.Unix()), nil
|
|
}
|
|
}
|
|
// ISO 8601 without timezone — with time component (precise)
|
|
preciseFormats := []string{
|
|
"2006-01-02T15:04:05",
|
|
"2006-01-02 15:04:05",
|
|
"2006-01-02T15:04",
|
|
"2006-01-02 15:04",
|
|
}
|
|
for _, f := range preciseFormats {
|
|
if t, err := time.ParseInLocation(f, input, time.Local); err == nil {
|
|
return fmt.Sprintf("%d", t.Unix()), nil
|
|
}
|
|
}
|
|
// Date-only (day-granularity)
|
|
if t, err := time.ParseInLocation("2006-01-02", input, time.Local); err == nil {
|
|
return fmt.Sprintf("%d", snapDay(t).Unix()), nil
|
|
}
|
|
// Unix timestamp (precise, passed through as-is) — must be purely numeric
|
|
var ts int64
|
|
if n, err := fmt.Sscanf(input, "%d", &ts); err == nil && n == 1 && ts > 0 && fmt.Sprintf("%d", ts) == input {
|
|
return input, nil
|
|
}
|
|
return "", fmt.Errorf("cannot parse time %q (supported: ISO 8601 e.g. 2026-01-01 / 2026-01-01T15:04:05+08:00, Unix timestamp)", input)
|
|
}
|
|
|
|
// FormatTimeWithSeconds converts Unix seconds/ms string to local time string with seconds precision.
|
|
func FormatTimeWithSeconds(ts interface{}) string {
|
|
if ts == nil {
|
|
return ""
|
|
}
|
|
s := fmt.Sprintf("%v", ts)
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
var n int64
|
|
fmt.Sscanf(s, "%d", &n)
|
|
if n == 0 {
|
|
return s
|
|
}
|
|
if n > 1e12 {
|
|
n = n / 1000
|
|
}
|
|
t := time.Unix(n, 0)
|
|
return t.Local().Format("2006-01-02 15:04:05")
|
|
}
|
|
|
|
// FormatTime converts Unix seconds/ms string to local time string.
|
|
func FormatTime(ts interface{}) string {
|
|
if ts == nil {
|
|
return ""
|
|
}
|
|
s := fmt.Sprintf("%v", ts)
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
var n int64
|
|
fmt.Sscanf(s, "%d", &n)
|
|
if n == 0 {
|
|
return s
|
|
}
|
|
// Detect ms vs seconds
|
|
if n > 1e12 {
|
|
n = n / 1000
|
|
}
|
|
t := time.Unix(n, 0)
|
|
return t.Local().Format("2006-01-02 15:04")
|
|
}
|
|
|
|
// SplitCSV 解析逗号分隔的列表,忽略空项并去除空格
|
|
func SplitCSV(input string) []string {
|
|
if input == "" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(input, ",")
|
|
var result []string
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
result = append(result, p)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// CheckApiError checks if API result is an error and prints it to w.
|
|
func CheckApiError(w io.Writer, result interface{}, action string) bool {
|
|
if resultMap, ok := result.(map[string]interface{}); ok {
|
|
code, _ := util.ToFloat64(resultMap["code"])
|
|
if code != 0 {
|
|
msg, _ := resultMap["msg"].(string)
|
|
output.PrintError(w, fmt.Sprintf("%s: [%.0f] %s", action, code, msg))
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// HandleApiResult checks for network/API errors and returns the "data" field.
|
|
func HandleApiResult(result interface{}, err error, action string) (map[string]interface{}, error) {
|
|
if err != nil {
|
|
return nil, output.Errorf(output.ExitAPI, "api_error", "%s: %s", action, err)
|
|
}
|
|
resultMap, _ := result.(map[string]interface{})
|
|
code, _ := util.ToFloat64(resultMap["code"])
|
|
if code != 0 {
|
|
msg, _ := resultMap["msg"].(string)
|
|
larkCode := int(code)
|
|
fullMsg := fmt.Sprintf("%s: [%d] %s", action, larkCode, msg)
|
|
return nil, output.ErrAPI(larkCode, fullMsg, resultMap["error"])
|
|
}
|
|
data, _ := resultMap["data"].(map[string]interface{})
|
|
return data, nil
|
|
}
|
|
|
|
// TruncateStr truncates s to at most n runes.
|
|
func TruncateStr(s string, n int) string {
|
|
r := []rune(s)
|
|
if len(r) <= n {
|
|
return s
|
|
}
|
|
return string(r[:n])
|
|
}
|