Files
wehub-resource-sync bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

141 lines
4.0 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package im
import (
"fmt"
"io"
"strconv"
"time"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/shortcuts/common"
)
const (
// feedGroupReadScope is required to read feed-group items.
feedGroupReadScope = "im:feed_group_v1:read"
// chatReadScope is required to resolve chat_name from feed_id via chats/batch_query.
chatReadScope = "im:chat:read"
)
// enrichFeedGroupItemsChatName resolves a human-readable chat_name for each feed
// card in data["items"] and data["deleted_items"] using chats/batch_query.
//
// The feed_id of a v1 feed card is always a chat ID (oc_xxx), so the chat's name
// is the natural display label. Resolution degrades gracefully: any feed_id that
// cannot be resolved simply keeps no chat_name key, and the function never returns
// an error or alters the exit code.
//
// NOTE: This mutates the item maps in place by adding a "chat_name" key.
func enrichFeedGroupItemsChatName(rt *common.RuntimeContext, data map[string]any) {
if data == nil {
return
}
items, _ := data["items"].([]any)
deletedItems, _ := data["deleted_items"].([]any)
// Collect deduped, ordered feed_id strings from both lists.
ids := make([]string, 0, len(items)+len(deletedItems))
seen := make(map[string]bool)
collect := func(list []any) {
for _, it := range list {
m, _ := it.(map[string]any)
if m == nil {
continue
}
id, _ := m["feed_id"].(string)
if id == "" || seen[id] {
continue
}
seen[id] = true
ids = append(ids, id)
}
}
collect(items)
collect(deletedItems)
if len(ids) == 0 {
return
}
contexts := batchQueryChatContexts(rt, ids)
if len(contexts) == 0 {
// We had feed_ids to resolve but got nothing back — most likely the
// chats/batch_query call failed (it degrades silently). Tell the user so
// an empty chat_name column is not mistaken for chats that simply have no name.
fmt.Fprintf(rt.IO().ErrOut, "warning: could not resolve chat names for %d feed(s); chat_name will be empty\n", len(ids))
return
}
apply := func(list []any) {
for _, it := range list {
m, _ := it.(map[string]any)
if m == nil {
continue
}
id, _ := m["feed_id"].(string)
if id == "" {
continue
}
if ctx, ok := contexts[id]; ok {
if name, _ := ctx["name"].(string); name != "" {
m["chat_name"] = name
}
}
}
}
apply(items)
apply(deletedItems)
}
// renderFeedGroupItemsTable prints the active items[] as a table (feed_id /
// chat_name / update_time), followed by a summary line. When hasMore is true a
// pagination hint is appended; when there are deleted items their count is noted.
func renderFeedGroupItemsTable(w io.Writer, data map[string]any, hasMore bool) {
items, _ := data["items"].([]any)
rows := make([]map[string]interface{}, 0, len(items))
for _, it := range items {
m, _ := it.(map[string]any)
if m == nil {
continue
}
chatName, _ := m["chat_name"].(string)
updateTime, _ := m["update_time"].(string)
feedID, _ := m["feed_id"].(string)
rows = append(rows, map[string]interface{}{
"feed_id": feedID,
"chat_name": chatName,
"update_time": formatFeedGroupUpdateTime(updateTime),
})
}
output.PrintTable(w, rows)
moreHint := ""
if hasMore {
moreHint = " (more available, use --page-token to fetch next page)"
}
fmt.Fprintf(w, "\n%d item(s)%s\n", len(items), moreHint)
if deleted, _ := data["deleted_items"].([]any); len(deleted) > 0 {
fmt.Fprintf(w, "(%d deleted)\n", len(deleted))
}
}
// formatFeedGroupUpdateTime renders a Unix-millisecond timestamp string as a
// human-readable local time for the pretty table. The raw value is returned
// unchanged when it is empty or not a valid millisecond integer, so JSON output
// (which never calls this) keeps the original wire value.
func formatFeedGroupUpdateTime(raw string) string {
if raw == "" {
return raw
}
ms, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return raw
}
return time.UnixMilli(ms).Local().Format(time.RFC3339)
}