mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
7675185f9d
Read the server-provided sender_name for both user and bot senders (previously only users resolved) so message-read commands display bot names instead of raw ids. The CLI opts into server-side name filling by sending with_sender_name=true on chat-messages-list, threads-messages-list, messages-mget and messages-search, as well as on the inline fetches that render nested senders: merge_forward sub-messages and auto-expanded thread replies. Without it those nested-only senders carry no sender_name and, with no fallback, render as raw ids. Names come solely from the server (single source of truth): there is no contact or mention fallback, so the contact scope is dropped from these four commands and the contact/mention resolution code is removed. A sender the server does not name falls back to its id; system messages show no name. The resolved name is exposed in the existing `name` field (backward compatible); the duplicate raw `sender_name` is stripped while the full `sender_i18n_names` map and `open_bot_id` are preserved for consumers. No new permission scope is required. Updates the lark-im skill docs.
130 lines
4.8 KiB
Go
130 lines
4.8 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package im
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
|
||
"github.com/larksuite/cli/errs"
|
||
"github.com/larksuite/cli/internal/output"
|
||
"github.com/larksuite/cli/shortcuts/common"
|
||
convertlib "github.com/larksuite/cli/shortcuts/im/convert_lib"
|
||
)
|
||
|
||
const maxMGetMessageIDs = 50
|
||
|
||
var ImMessagesMGet = common.Shortcut{
|
||
Service: "im",
|
||
Command: "+messages-mget",
|
||
Description: "Batch get messages by IDs; user/bot; fetches up to 50 om_ message IDs, formats sender names, expands thread replies",
|
||
Risk: "read",
|
||
Scopes: []string{"im:message:readonly"},
|
||
UserScopes: []string{"im:message.group_msg:get_as_user", "im:message.p2p_msg:get_as_user", "im:message.reactions:read"},
|
||
BotScopes: []string{"im:message.group_msg", "im:message.p2p_msg:readonly", "im:message.reactions:read"},
|
||
AuthTypes: []string{"user", "bot"},
|
||
HasFormat: true,
|
||
Flags: []common.Flag{
|
||
{Name: "message-ids", Desc: "message IDs, comma-separated (om_xxx,om_yyy)", Required: true},
|
||
{Name: "no-reactions", Type: "bool", Desc: "skip auto-fetching reactions for each message (default: enrichment enabled)"},
|
||
downloadResourcesFlag,
|
||
},
|
||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||
ids := common.SplitCSV(runtime.Str("message-ids"))
|
||
d := common.NewDryRunAPI().GET(buildMGetURL(ids))
|
||
if !runtime.Bool("no-reactions") {
|
||
d = d.POST("/open-apis/im/v1/messages/reactions/batch_query").
|
||
Desc("Reaction enrichment: queries returned messages in batches of up to 20 to attach the reactions block (operator, action_time, counts). Pass --no-reactions to skip.")
|
||
}
|
||
if runtime.Bool("download-resources") {
|
||
d = d.Desc(downloadResourcesDryRunDesc)
|
||
}
|
||
return d
|
||
},
|
||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||
ids := common.SplitCSV(runtime.Str("message-ids"))
|
||
if len(ids) == 0 {
|
||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--message-ids is required (comma-separated om_xxx)").WithParam("--message-ids")
|
||
}
|
||
if len(ids) > maxMGetMessageIDs {
|
||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--message-ids supports at most %d IDs per request (got %d)", maxMGetMessageIDs, len(ids)).WithParam("--message-ids")
|
||
}
|
||
for _, id := range ids {
|
||
if _, err := validateMessageID(id); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
},
|
||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||
ids := common.SplitCSV(runtime.Str("message-ids"))
|
||
mgetURL := buildMGetURL(ids)
|
||
|
||
data, err := runtime.DoAPIJSONTyped(http.MethodGet, mgetURL, nil, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
rawItems, _ := data["items"].([]interface{})
|
||
|
||
nameCache := make(map[string]string)
|
||
// Pre-fetch merge_forward sub-messages concurrently before the per-item
|
||
// conversion loop, so N merge_forwards in the input don't serialize
|
||
// into N × ~1s of stall inside FormatMessageItem. Passing nameCache
|
||
// also pre-resolves every sub-item's sender open_id in one batched
|
||
// contact API call.
|
||
mergePrefetch := convertlib.PrefetchMergeForwardSubItems(runtime, rawItems, nameCache)
|
||
|
||
downloadResources := runtime.Bool("download-resources")
|
||
messages := make([]map[string]interface{}, 0, len(rawItems))
|
||
for _, item := range rawItems {
|
||
m, _ := item.(map[string]interface{})
|
||
messages = append(messages, convertlib.FormatMessageItemWithMergePrefetchOpts(m, runtime, nameCache, mergePrefetch, downloadResources))
|
||
}
|
||
|
||
convertlib.ResolveSenderNames(runtime, messages, nameCache)
|
||
convertlib.AttachSenderNames(messages, nameCache)
|
||
convertlib.ExpandThreadRepliesWithResources(runtime, messages, nameCache, convertlib.ThreadRepliesPerThread, convertlib.ThreadRepliesTotalLimit, downloadResources)
|
||
if !runtime.Bool("no-reactions") {
|
||
convertlib.EnrichReactions(runtime, messages)
|
||
}
|
||
if downloadResources {
|
||
enrichMessageResourceDownloads(runtime, messages)
|
||
}
|
||
|
||
outData := map[string]interface{}{
|
||
"messages": messages,
|
||
"total": len(messages),
|
||
}
|
||
runtime.OutFormat(outData, nil, func(w io.Writer) {
|
||
if len(messages) == 0 {
|
||
fmt.Fprintln(w, "No messages found.")
|
||
return
|
||
}
|
||
var rows []map[string]interface{}
|
||
for _, msg := range messages {
|
||
row := map[string]interface{}{
|
||
"message_id": msg["message_id"],
|
||
"time": msg["create_time"],
|
||
"type": msg["msg_type"],
|
||
}
|
||
if sender, ok := msg["sender"].(map[string]interface{}); ok {
|
||
if disp := senderDisplay(sender); disp != "" {
|
||
row["sender"] = disp
|
||
}
|
||
}
|
||
if content, _ := msg["content"].(string); content != "" {
|
||
row["content"] = convertlib.TruncateContent(content, 40)
|
||
}
|
||
rows = append(rows, row)
|
||
}
|
||
output.PrintTable(w, rows)
|
||
fmt.Fprintf(w, "\n%d message(s)\ntip: use --format json to view full message content\n", len(messages))
|
||
})
|
||
return nil
|
||
},
|
||
}
|