mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-04 07:45:30 +00:00
766 lines
18 KiB
Go
766 lines
18 KiB
Go
package webview
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/dop251/goja"
|
|
)
|
|
|
|
var ErrUnavailable = errors.New("gopeed runtime webview is unavailable")
|
|
|
|
type Opener interface {
|
|
Open(opts OpenOptions) (Page, error)
|
|
}
|
|
|
|
type Page interface {
|
|
AddInitScript(script string) error
|
|
Goto(url string, opts GotoOptions) error
|
|
Execute(expression string, args ...any) (any, error)
|
|
GetCookies() ([]Cookie, error)
|
|
SetCookie(cookie Cookie) error
|
|
DeleteCookie(cookie Cookie) error
|
|
ClearCookies() error
|
|
Close() error
|
|
}
|
|
|
|
type OpenOptions struct {
|
|
Headless bool
|
|
Debug bool
|
|
Title string
|
|
Width int
|
|
Height int
|
|
UserAgent string
|
|
}
|
|
|
|
type GotoOptions struct {
|
|
TimeoutMS int64
|
|
WaitUntil string
|
|
}
|
|
|
|
type ClickOptions struct {
|
|
DelayMS int64
|
|
}
|
|
|
|
type TypeOptions struct {
|
|
DelayMS int64
|
|
}
|
|
|
|
type WaitOptions struct {
|
|
TimeoutMS int64
|
|
PollIntervalMS int64
|
|
}
|
|
|
|
type WaitForSelectorOptions struct {
|
|
TimeoutMS int64
|
|
PollIntervalMS int64
|
|
Visible bool
|
|
Hidden bool
|
|
}
|
|
|
|
type Cookie struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
Domain string `json:"domain,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
Expires time.Time `json:"expires,omitempty,omitzero"`
|
|
Secure bool `json:"secure,omitempty"`
|
|
HTTPOnly bool `json:"httpOnly,omitempty"`
|
|
}
|
|
|
|
type Runtime struct {
|
|
opener Opener
|
|
available bool
|
|
|
|
mu sync.Mutex
|
|
pageSeq uint64
|
|
pages map[string]Page
|
|
}
|
|
|
|
func NewRuntime(opener Opener, available bool) *Runtime {
|
|
if opener == nil {
|
|
available = false
|
|
}
|
|
return &Runtime{
|
|
opener: opener,
|
|
available: available,
|
|
pages: make(map[string]Page),
|
|
}
|
|
}
|
|
|
|
func (r *Runtime) IsAvailable() bool {
|
|
return r != nil && r.available && r.opener != nil
|
|
}
|
|
|
|
func (r *Runtime) Open(opts ...map[string]any) (*PageHandle, error) {
|
|
if !r.IsAvailable() {
|
|
return nil, ErrUnavailable
|
|
}
|
|
page, err := r.opener.Open(parseOpenOptions(firstMap(opts)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.pageSeq++
|
|
id := fmt.Sprintf("page-%d", r.pageSeq)
|
|
r.pages[id] = page
|
|
return &PageHandle{runtime: r, id: id}, nil
|
|
}
|
|
|
|
func (r *Runtime) Close() error {
|
|
r.mu.Lock()
|
|
pageIDs := make([]string, 0, len(r.pages))
|
|
for id := range r.pages {
|
|
pageIDs = append(pageIDs, id)
|
|
}
|
|
r.mu.Unlock()
|
|
|
|
var errs []error
|
|
for _, id := range pageIDs {
|
|
if err := r.closePage(id); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
type PageHandle struct {
|
|
runtime *Runtime
|
|
id string
|
|
}
|
|
|
|
func (p *PageHandle) AddInitScript(script string) error {
|
|
page, err := p.page()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return page.AddInitScript(script)
|
|
}
|
|
|
|
func (p *PageHandle) Goto(url string, opts ...map[string]any) error {
|
|
page, err := p.page()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gotoOpts, err := parseGotoOptions(firstMap(opts))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return page.Goto(url, gotoOpts)
|
|
}
|
|
|
|
func (p *PageHandle) Execute(scriptOrFn any, args ...any) (any, error) {
|
|
page, err := p.page()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
expression, err := normalizeExecutable(scriptOrFn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return page.Execute(expression, args...)
|
|
}
|
|
|
|
func (p *PageHandle) Focus(selector string) error {
|
|
_, err := p.Execute(`(selector) => {
|
|
const element = document.querySelector(selector);
|
|
if (!element) {
|
|
throw new Error("element not found: " + selector);
|
|
}
|
|
if (typeof element.focus !== "function") {
|
|
throw new Error("element is not focusable: " + selector);
|
|
}
|
|
element.focus();
|
|
return true;
|
|
}`, selector)
|
|
return err
|
|
}
|
|
|
|
func (p *PageHandle) Click(selector string, opts ...map[string]any) error {
|
|
clickOpts := parseClickOptions(firstMap(opts))
|
|
_, err := p.Execute(`(selector, delayMS) => {
|
|
const element = document.querySelector(selector);
|
|
if (!element) {
|
|
throw new Error("element not found: " + selector);
|
|
}
|
|
if (typeof element.scrollIntoView === "function") {
|
|
element.scrollIntoView({ block: "center", inline: "center" });
|
|
}
|
|
const performClick = () => {
|
|
if (typeof element.click === "function") {
|
|
element.click();
|
|
return;
|
|
}
|
|
element.dispatchEvent(new MouseEvent("click", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
view: window,
|
|
}));
|
|
};
|
|
if (delayMS > 0) {
|
|
return new Promise((resolve) => {
|
|
setTimeout(() => {
|
|
performClick();
|
|
resolve(true);
|
|
}, delayMS);
|
|
});
|
|
}
|
|
performClick();
|
|
return true;
|
|
}`, selector, clickOpts.DelayMS)
|
|
return err
|
|
}
|
|
|
|
func (p *PageHandle) Type(selector string, text string, opts ...map[string]any) error {
|
|
typeOpts := parseTypeOptions(firstMap(opts))
|
|
_, err := p.Execute(`(selector, text, delayMS) => {
|
|
const element = document.querySelector(selector);
|
|
if (!element) {
|
|
throw new Error("element not found: " + selector);
|
|
}
|
|
if (typeof element.focus === "function") {
|
|
element.focus();
|
|
}
|
|
const emitInput = () => {
|
|
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
};
|
|
const insertIntoInput = (chunk) => {
|
|
if (element.disabled) {
|
|
throw new Error("element is disabled: " + selector);
|
|
}
|
|
if (element.readOnly) {
|
|
throw new Error("element is readonly: " + selector);
|
|
}
|
|
const value = typeof element.value === "string" ? element.value : "";
|
|
const hasSelection = typeof element.selectionStart === "number" && typeof element.selectionEnd === "number";
|
|
const start = hasSelection ? element.selectionStart : value.length;
|
|
const end = hasSelection ? element.selectionEnd : value.length;
|
|
element.value = value.slice(0, start) + chunk + value.slice(end);
|
|
const caret = start + chunk.length;
|
|
if (typeof element.setSelectionRange === "function") {
|
|
element.setSelectionRange(caret, caret);
|
|
}
|
|
emitInput();
|
|
};
|
|
const insertIntoEditable = (chunk) => {
|
|
if (element.isContentEditable) {
|
|
element.textContent = (element.textContent || "") + chunk;
|
|
emitInput();
|
|
return;
|
|
}
|
|
insertIntoInput(chunk);
|
|
};
|
|
const insertChunk = (chunk) => {
|
|
const tagName = (element.tagName || "").toUpperCase();
|
|
if (tagName === "INPUT" || tagName === "TEXTAREA" || typeof element.value === "string") {
|
|
insertIntoInput(chunk);
|
|
return;
|
|
}
|
|
if (element.isContentEditable) {
|
|
insertIntoEditable(chunk);
|
|
return;
|
|
}
|
|
throw new Error("element is not typable: " + selector);
|
|
};
|
|
const chars = Array.from(String(text));
|
|
if (delayMS > 0) {
|
|
return new Promise((resolve) => {
|
|
let index = 0;
|
|
const step = () => {
|
|
if (index >= chars.length) {
|
|
resolve(true);
|
|
return;
|
|
}
|
|
insertChunk(chars[index]);
|
|
index += 1;
|
|
setTimeout(step, delayMS);
|
|
};
|
|
step();
|
|
});
|
|
}
|
|
for (const char of chars) {
|
|
insertChunk(char);
|
|
}
|
|
return true;
|
|
}`, selector, text, typeOpts.DelayMS)
|
|
return err
|
|
}
|
|
|
|
func (p *PageHandle) WaitForSelector(selector string, opts ...map[string]any) (bool, error) {
|
|
waitOpts := parseWaitForSelectorOptions(firstMap(opts))
|
|
_, matched, err := p.poll(WaitOptions{
|
|
TimeoutMS: waitOpts.TimeoutMS,
|
|
PollIntervalMS: waitOpts.PollIntervalMS,
|
|
}, func() (any, bool, error) {
|
|
value, err := p.Execute(`(selector, visible, hidden) => {
|
|
const element = document.querySelector(selector);
|
|
if (hidden) {
|
|
if (!element) return true;
|
|
const style = window.getComputedStyle(element);
|
|
if (!style) return !!element.hidden;
|
|
const rect = element.getBoundingClientRect();
|
|
return element.hidden || style.display === "none" || style.visibility === "hidden" || rect.width === 0 || rect.height === 0;
|
|
}
|
|
if (!element) return false;
|
|
if (!visible) return true;
|
|
const style = window.getComputedStyle(element);
|
|
if (!style) return true;
|
|
const rect = element.getBoundingClientRect();
|
|
return !element.hidden && style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
|
|
}`, selector, waitOpts.Visible, waitOpts.Hidden)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
return true, truthy(value), nil
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return matched, nil
|
|
}
|
|
|
|
func (p *PageHandle) WaitForFunction(scriptOrFn any, args ...any) (any, error) {
|
|
expression, err := normalizeExecutable(scriptOrFn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
waitOpts := WaitOptions{}
|
|
callArgs := args
|
|
if len(args) > 0 {
|
|
if raw, ok := args[0].(map[string]any); ok {
|
|
waitOpts = parseWaitOptions(raw)
|
|
callArgs = args[1:]
|
|
}
|
|
}
|
|
|
|
value, matched, err := p.poll(waitOpts, func() (any, bool, error) {
|
|
result, err := p.Execute(`(expression, args) => {
|
|
const target = (0, eval)(expression);
|
|
return Promise.resolve(typeof target === "function" ? target(...args) : target).then((value) => ({
|
|
matched: !!value,
|
|
value: value ?? null,
|
|
}));
|
|
}`, expression, callArgs)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
payload, ok := result.(map[string]any)
|
|
if !ok {
|
|
return nil, false, fmt.Errorf("unexpected waitForFunction payload: %T", result)
|
|
}
|
|
return payload["value"], truthy(payload["matched"]), nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !matched {
|
|
return nil, nil
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (p *PageHandle) GetCookies() ([]Cookie, error) {
|
|
page, err := p.page()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return page.GetCookies()
|
|
}
|
|
|
|
func (p *PageHandle) SetCookie(raw map[string]any) error {
|
|
page, err := p.page()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return page.SetCookie(parseCookie(raw))
|
|
}
|
|
|
|
func (p *PageHandle) DeleteCookie(raw map[string]any) error {
|
|
page, err := p.page()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return page.DeleteCookie(parseCookie(raw))
|
|
}
|
|
|
|
func (p *PageHandle) ClearCookies() error {
|
|
page, err := p.page()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return page.ClearCookies()
|
|
}
|
|
|
|
func (p *PageHandle) URL() (string, error) {
|
|
value, err := p.Execute(`() => location.href`)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return parseString(value), nil
|
|
}
|
|
|
|
func (p *PageHandle) Content() (string, error) {
|
|
value, err := p.Execute(`() => document.documentElement ? document.documentElement.outerHTML : ""`)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return parseString(value), nil
|
|
}
|
|
|
|
func (p *PageHandle) Close() error {
|
|
if p == nil || p.runtime == nil {
|
|
return nil
|
|
}
|
|
return p.runtime.closePage(p.id)
|
|
}
|
|
|
|
func (p *PageHandle) page() (Page, error) {
|
|
if p == nil || p.runtime == nil {
|
|
return nil, fmt.Errorf("webview page handle is not initialized")
|
|
}
|
|
return p.runtime.getPage(p.id)
|
|
}
|
|
|
|
func (p *PageHandle) poll(opts WaitOptions, fn func() (any, bool, error)) (any, bool, error) {
|
|
timeout := time.Duration(defaultWaitTimeoutMS(opts.TimeoutMS)) * time.Millisecond
|
|
pollInterval := time.Duration(defaultPollIntervalMS(opts.PollIntervalMS)) * time.Millisecond
|
|
deadline := time.Now().Add(timeout)
|
|
for {
|
|
value, matched, err := fn()
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
if matched {
|
|
return value, true, nil
|
|
}
|
|
if time.Now().After(deadline) {
|
|
return nil, false, nil
|
|
}
|
|
time.Sleep(pollInterval)
|
|
}
|
|
}
|
|
|
|
func (r *Runtime) getPage(id string) (Page, error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
page, ok := r.pages[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("page not found")
|
|
}
|
|
return page, nil
|
|
}
|
|
|
|
func (r *Runtime) closePage(id string) error {
|
|
r.mu.Lock()
|
|
page, ok := r.pages[id]
|
|
if !ok {
|
|
r.mu.Unlock()
|
|
return nil
|
|
}
|
|
delete(r.pages, id)
|
|
r.mu.Unlock()
|
|
return page.Close()
|
|
}
|
|
|
|
func parseOpenOptions(raw map[string]any) OpenOptions {
|
|
return OpenOptions{
|
|
Headless: parseBool(raw["headless"]),
|
|
Debug: parseBool(raw["debug"]),
|
|
Title: parseString(raw["title"]),
|
|
Width: parseInt(raw["width"]),
|
|
Height: parseInt(raw["height"]),
|
|
UserAgent: parseString(raw["userAgent"]),
|
|
}
|
|
}
|
|
|
|
func parseGotoOptions(raw map[string]any) (GotoOptions, error) {
|
|
waitUntil, err := normalizeWaitUntil(parseString(raw["waitUntil"]))
|
|
if err != nil {
|
|
return GotoOptions{}, err
|
|
}
|
|
return GotoOptions{
|
|
TimeoutMS: parseInt64(raw["timeoutMs"]),
|
|
WaitUntil: waitUntil,
|
|
}, nil
|
|
}
|
|
|
|
func normalizeWaitUntil(raw string) (string, error) {
|
|
if raw == "" {
|
|
return "load", nil
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(raw)) {
|
|
case "load":
|
|
return "load", nil
|
|
case "domcontentloaded":
|
|
return "domcontentloaded", nil
|
|
default:
|
|
return "", fmt.Errorf("invalid waitUntil: %s", raw)
|
|
}
|
|
}
|
|
|
|
func parseClickOptions(raw map[string]any) ClickOptions {
|
|
return ClickOptions{DelayMS: parseInt64(raw["delay"])}
|
|
}
|
|
|
|
func parseTypeOptions(raw map[string]any) TypeOptions {
|
|
return TypeOptions{DelayMS: parseInt64(raw["delay"])}
|
|
}
|
|
|
|
func parseWaitOptions(raw map[string]any) WaitOptions {
|
|
return WaitOptions{
|
|
TimeoutMS: parseInt64(raw["timeoutMs"]),
|
|
PollIntervalMS: parseInt64(raw["pollIntervalMs"]),
|
|
}
|
|
}
|
|
|
|
func parseWaitForSelectorOptions(raw map[string]any) WaitForSelectorOptions {
|
|
return WaitForSelectorOptions{
|
|
TimeoutMS: parseInt64(raw["timeoutMs"]),
|
|
PollIntervalMS: parseInt64(raw["pollIntervalMs"]),
|
|
Visible: parseBool(raw["visible"]),
|
|
Hidden: parseBool(raw["hidden"]),
|
|
}
|
|
}
|
|
|
|
func parseCookie(raw map[string]any) Cookie {
|
|
cookie := Cookie{
|
|
Name: parseString(raw["name"]),
|
|
Value: parseString(raw["value"]),
|
|
Domain: parseString(raw["domain"]),
|
|
Path: parseString(raw["path"]),
|
|
Secure: parseBool(raw["secure"]),
|
|
HTTPOnly: parseBool(raw["httpOnly"]),
|
|
}
|
|
if rawExpires, ok := raw["expires"]; ok {
|
|
cookie.Expires = parseTime(rawExpires)
|
|
}
|
|
return cookie
|
|
}
|
|
|
|
func defaultWaitTimeoutMS(value int64) int64 {
|
|
if value <= 0 {
|
|
return 10000
|
|
}
|
|
return value
|
|
}
|
|
|
|
func defaultPollIntervalMS(value int64) int64 {
|
|
if value <= 0 {
|
|
return 100
|
|
}
|
|
return value
|
|
}
|
|
|
|
func normalizeExecutable(scriptOrFn any) (string, error) {
|
|
switch value := scriptOrFn.(type) {
|
|
case string:
|
|
return value, nil
|
|
case *goja.Object:
|
|
return NormalizeExecutableValue(value)
|
|
default:
|
|
return "", fmt.Errorf("execute expects a string or function, got %T", scriptOrFn)
|
|
}
|
|
}
|
|
|
|
func NormalizeExecutableValue(value goja.Value) (string, error) {
|
|
if value == nil {
|
|
return "", fmt.Errorf("execute expects a string or function")
|
|
}
|
|
switch raw := value.Export().(type) {
|
|
case string:
|
|
return raw, nil
|
|
}
|
|
obj, ok := value.(*goja.Object)
|
|
if ok {
|
|
if _, ok := goja.AssertFunction(obj); ok {
|
|
source, err := functionSource(obj)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return normalizeFunctionSource(source), nil
|
|
}
|
|
}
|
|
source := strings.TrimSpace(value.String())
|
|
if looksLikeFunctionSource(source) {
|
|
return normalizeFunctionSource(source), nil
|
|
}
|
|
return "", fmt.Errorf("execute expects a string or function, got %T (%q)", value, source)
|
|
}
|
|
|
|
func normalizeFunctionSource(source string) string {
|
|
repaired := repairArrowObjectLiteralSource(source)
|
|
return "(" + repaired + ")"
|
|
}
|
|
|
|
func functionSource(fn *goja.Object) (string, error) {
|
|
toStringValue := fn.Get("toString")
|
|
toString, ok := goja.AssertFunction(toStringValue)
|
|
if !ok {
|
|
return "", fmt.Errorf("function.toString is not callable")
|
|
}
|
|
value, err := toString(fn)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return value.String(), nil
|
|
}
|
|
|
|
func repairArrowObjectLiteralSource(source string) string {
|
|
trimmed := strings.TrimSpace(source)
|
|
if !strings.Contains(trimmed, "=>") || !strings.Contains(trimmed, "({") {
|
|
return source
|
|
}
|
|
if !strings.HasSuffix(trimmed, "}") {
|
|
return source
|
|
}
|
|
if strings.Count(trimmed, "(")-strings.Count(trimmed, ")") != 1 {
|
|
return source
|
|
}
|
|
if strings.Count(trimmed, "{") != strings.Count(trimmed, "}") {
|
|
return source
|
|
}
|
|
// TODO: Remove this narrow repair once goja fixes toString() for concise arrow
|
|
// functions that directly return object literals, then upstream the fix as a PR.
|
|
return source + ")"
|
|
}
|
|
|
|
func looksLikeFunctionSource(source string) bool {
|
|
if source == "" || source == "[object Object]" {
|
|
return false
|
|
}
|
|
return strings.Contains(source, "=>") ||
|
|
strings.HasPrefix(source, "function") ||
|
|
strings.HasPrefix(source, "async function") ||
|
|
strings.HasPrefix(source, "async (") ||
|
|
strings.HasPrefix(source, "async(")
|
|
}
|
|
|
|
func truthy(value any) bool {
|
|
switch v := value.(type) {
|
|
case nil:
|
|
return false
|
|
case bool:
|
|
return v
|
|
case string:
|
|
return v != ""
|
|
case int:
|
|
return v != 0
|
|
case int8:
|
|
return v != 0
|
|
case int16:
|
|
return v != 0
|
|
case int32:
|
|
return v != 0
|
|
case int64:
|
|
return v != 0
|
|
case uint:
|
|
return v != 0
|
|
case uint8:
|
|
return v != 0
|
|
case uint16:
|
|
return v != 0
|
|
case uint32:
|
|
return v != 0
|
|
case uint64:
|
|
return v != 0
|
|
case float32:
|
|
return v != 0
|
|
case float64:
|
|
return v != 0
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func firstMap(values []map[string]any) map[string]any {
|
|
if len(values) == 0 || values[0] == nil {
|
|
return map[string]any{}
|
|
}
|
|
return values[0]
|
|
}
|
|
|
|
func parseString(value any) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
return fmt.Sprint(value)
|
|
}
|
|
|
|
func parseBool(value any) bool {
|
|
switch v := value.(type) {
|
|
case bool:
|
|
return v
|
|
case string:
|
|
parsed, _ := strconv.ParseBool(v)
|
|
return parsed
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func parseInt(value any) int {
|
|
return int(parseInt64(value))
|
|
}
|
|
|
|
func parseInt64(value any) int64 {
|
|
switch v := value.(type) {
|
|
case int:
|
|
return int64(v)
|
|
case int8:
|
|
return int64(v)
|
|
case int16:
|
|
return int64(v)
|
|
case int32:
|
|
return int64(v)
|
|
case int64:
|
|
return v
|
|
case uint:
|
|
return int64(v)
|
|
case uint8:
|
|
return int64(v)
|
|
case uint16:
|
|
return int64(v)
|
|
case uint32:
|
|
return int64(v)
|
|
case uint64:
|
|
return int64(v)
|
|
case float32:
|
|
return int64(v)
|
|
case float64:
|
|
return int64(v)
|
|
case string:
|
|
parsed, _ := strconv.ParseInt(v, 10, 64)
|
|
return parsed
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func parseTime(value any) time.Time {
|
|
switch v := value.(type) {
|
|
case time.Time:
|
|
return v
|
|
case int64:
|
|
return time.UnixMilli(v)
|
|
case int:
|
|
return time.UnixMilli(int64(v))
|
|
case float64:
|
|
return time.UnixMilli(int64(v))
|
|
case string:
|
|
if parsed, err := time.Parse(time.RFC3339Nano, v); err == nil {
|
|
return parsed
|
|
}
|
|
if ms, err := strconv.ParseInt(v, 10, 64); err == nil {
|
|
return time.UnixMilli(ms)
|
|
}
|
|
}
|
|
return time.Time{}
|
|
}
|