convertlib

package
v1.0.61 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 30, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Index

Constants

View Source
const ThreadRepliesPerThread = 50

ThreadRepliesPerThread is the default max replies fetched per thread in auto-expand.

View Source
const ThreadRepliesTotalLimit = 500

ThreadRepliesTotalLimit is the default max total thread replies across all threads.

Variables

This section is empty.

Functions

func AttachSenderNames

func AttachSenderNames(messages []map[string]interface{}, nameMap map[string]string)

AttachSenderNames enriches message sender objects with resolved display names. Senders whose name could not be resolved are left unchanged (id is preserved).

func BuildMentionKeyMap

func BuildMentionKeyMap(mentions []interface{}) map[string]string

BuildMentionKeyMap builds a key→name lookup from the message "mentions" array.

func BuildMergeForwardChildrenMap

func BuildMergeForwardChildrenMap(items []map[string]interface{}, rootMessageID string) map[string][]map[string]interface{}

BuildMergeForwardChildrenMap builds a parent→children map from a flat items list. Items without upper_message_id are treated as direct children of rootMessageID. The root container message itself is skipped.

func ConvertBodyContent

func ConvertBodyContent(msgType string, ctx *ConvertContext) string

ConvertBodyContent converts body.content (a raw JSON string) to human-readable text.

func ConvertInteractiveEventContent added in v1.0.56

func ConvertInteractiveEventContent(rawContent string, mentions []interface{}) string

ConvertInteractiveEventContent extracts user_dsl from an interactive event message.content and converts it to human-readable text with the same format as cardConverter. mentions is the message-level mentions array used to resolve @at references in markdown content.

func EnrichReactions added in v1.0.42

func EnrichReactions(runtime *common.RuntimeContext, messages []map[string]interface{})

EnrichReactions enriches messages with their reactions by calling the im.reactions.batch_query API. Messages are modified in place: each message that the server returns reactions for gets a "reactions" map attached.

Failure modes (warning to stderr + skip; never aborts main message output):

  • batch_query call fails (network, 5xx, scope insufficient, rate limited): each message in the failed batch is marked with "reactions_error": true so callers can distinguish "fetch failed" from "no reactions exist".
  • batch_query returns a partial result: only messages the server failed on get "reactions_error": true; the successful ones get the reactions block.

The "reactions_error" flag mirrors the "thread_replies_error" pattern in thread.go so downstream consumers handle both enrichment failures uniformly.

Output shape (only on messages that the server actually returned data for):

"reactions": {
  "counts":  [{"reaction_type": "SMILE", "count": 3}],
  "details": [{"reaction_id": "...", "emoji_type": "SMILE",
                "operator": {...}, "action_time": "..."}]
}

The server caps queries[] at 20 per call, so messages are split into batches of size <= 20 before invoking the API.

func EnrichResourceDownloads added in v1.0.51

func EnrichResourceDownloads(runtime *common.RuntimeContext, messages []map[string]interface{}, dl ResourceDownloader)

EnrichResourceDownloads walks every message node (including nested thread_replies) for "resources" blocks attached during formatting, downloads each distinct (message_id, key) once with bounded concurrency, and fills local_path/size_bytes back into every ref sharing that key. A single resource failing is isolated: its ref is flagged "error": true and a warning is written to stderr, while the main message and the other resources are unaffected (S2.STA-DES-P0-002 weak-dependency isolation).

func ExpandThreadReplies

func ExpandThreadReplies(runtime *common.RuntimeContext, messages []map[string]interface{}, nameCache map[string]string, perThread, totalLimit int)

ExpandThreadReplies fetches and embeds thread replies for messages that contain a thread_id. For each unique thread_id found in messages, it fetches up to perThread replies (asc order) and attaches them as "thread_replies" on the first outer message that referenced that thread. Expansion stops once totalLimit cumulative replies have been allocated across planned fetches. nameCache is the shared open_id→name map.

Implementation is two-phase:

  1. Plan + concurrent fetch. Walk messages in order, recording every unique thread_id with a fetch limit of perThread (no upfront budget deduction — see below). Then dispatch the planned fetches with bounded concurrency; each goroutine writes only to its own result slot, no shared mutable state besides that slot.

  2. Sequential attach with post-hoc budget enforcement. Walk the planned threads in their original first-seen order, accumulating actual returned reply counts against totalLimit. When a thread's actual replies would push the running total past totalLimit, its reply slice is truncated to fit the remaining budget and thread_has_more is set on its host so consumers know more replies exist server-side. Threads that arrive past a fully-exhausted budget keep their thread_id on the host but don't get thread_replies attached (semantically identical to the pre-existing serial behavior for over-budget threads). The phase stays single-threaded because ResolveSenderNames writes to the shared nameCache and FormatMessageItem may trigger merge_forward expansion that also touches nameCache.

Budget semantics match the pre-existing serial implementation exactly: each thread's actual returned count is what gets deducted from the budget, not its planned per-thread ceiling. An earlier draft of this refactor allocated the budget against the planned ceiling upfront for implementation simplicity, but that silently dropped later threads in chats where many threads return well under perThread replies (e.g. totalLimit=500 + perThread=50 + 12 short threads of 3 replies each → old code attached all 12, planned-allocation code attached only 10). The trade-off here is a small amount of server-side over-fetching for threads that will end up truncated or dropped — bounded by perThread per thread — in exchange for preserving the original "every thread that fits gets its data" guarantee.

func ExpandThreadRepliesWithResources added in v1.0.51

func ExpandThreadRepliesWithResources(runtime *common.RuntimeContext, messages []map[string]interface{}, nameCache map[string]string, perThread, totalLimit int, extractResources bool)

ExpandThreadRepliesWithResources is ExpandThreadReplies with an explicit extractResources gate, threaded through to each reply's formatting so that (when on) every reply — including a reply that is itself a merge_forward — gets its own resources block. extractResources=false reproduces the original behavior exactly.

func FormatEventMessage

func FormatEventMessage(msgType, rawContent, messageID string, mentions []interface{}) map[string]interface{}

FormatEventMessage converts an event-pushed message to a human-readable map. Event messages have a different structure from API responses:

  • message_type (not msg_type), content is a direct JSON string (not under body.content)
  • mentions are nested under message.mentions

This is the entry point for im.message.receive_v1 event processors.

func FormatMergeForwardSubTree

func FormatMergeForwardSubTree(parentID string, childrenMap map[string][]map[string]interface{}) string

FormatMergeForwardSubTree recursively formats a sub-tree rooted at parentID. For merge_forward children it recurses via the tree (no extra API calls). For other types it delegates to the provided convert callback.

func FormatMergeForwardTimestamp

func FormatMergeForwardTimestamp(tsStr string) string

FormatMergeForwardTimestamp formats a millisecond timestamp string to local RFC3339 with offset.

func FormatMessageItem

func FormatMessageItem(m map[string]interface{}, runtime *common.RuntimeContext, senderNames ...map[string]string) map[string]interface{}

FormatMessageItem converts a raw API message item to a human-readable map. senderNames is an optional shared cache (open_id -> name) accumulated across messages; pass nil to disable sender name caching.

func FormatMessageItemWithMergePrefetch added in v1.0.43

func FormatMessageItemWithMergePrefetch(m map[string]interface{}, runtime *common.RuntimeContext, nameCache map[string]string, mergePrefetch map[string][]map[string]interface{}) map[string]interface{}

FormatMessageItemWithMergePrefetch is like FormatMessageItem but threads a pre-fetched merge_forward sub-message map (typically built via PrefetchMergeForwardSubItems) through to the merge_forward converter so it can skip its own per-message GET. Shortcuts that iterate a page of raw items should pre-fetch once and call this variant in the loop to avoid the N × ~1s serial-merge_forward stall in the original code path.

func FormatMessageItemWithMergePrefetchOpts added in v1.0.51

func FormatMessageItemWithMergePrefetchOpts(m map[string]interface{}, runtime *common.RuntimeContext, nameCache map[string]string, mergePrefetch map[string][]map[string]interface{}, extractResources bool) map[string]interface{}

FormatMessageItemWithMergePrefetchOpts is FormatMessageItemWithMergePrefetch with an explicit extractResources gate. When extractResources is true and the message carries downloadable resources, a "resources" block (ref list without local_path/size_bytes) is attached for the download enrichment stage to fill. The other entry points are thin extractResources=false wrappers, so default output is unchanged.

func IndentLines

func IndentLines(text, indent string) string

IndentLines prefixes every line of text with the given indent string.

func ParseJSONObject

func ParseJSONObject(raw string) (map[string]interface{}, error)

ParseJSONObject parses a raw JSON string into a map.

func ParseMergeForwardIDs

func ParseMergeForwardIDs(raw string) []string

ParseMergeForwardIDs extracts message IDs from a merge_forward content JSON.

func PrefetchMergeForwardSubItems added in v1.0.43

func PrefetchMergeForwardSubItems(runtime *common.RuntimeContext, rawItems []interface{}, nameCache map[string]string) map[string][]map[string]interface{}

PrefetchMergeForwardSubItems scans rawItems for merge_forward messages, concurrently fetches each one's flat sub-message list, and returns a map keyed by the merge_forward message_id. Callers thread the returned map through FormatMessageItemWithMergePrefetch (or directly into a ConvertContext.MergeForwardSubItems) so the per-item conversion loop can reuse cached sub-trees instead of issuing its own serial GET.

Each fetch is independent (different message_id, different sub-tree), so concurrent goroutines never contend on shared mutable state — the result map is written under a mutex purely to make the map safe for concurrent inserts.

On fetch failure: emit a stderr warning and intentionally do NOT insert the failed id into the result map. The downstream mergeForwardConverter.Convert path keys off "is this id present in the prefetch?" — by leaving the key absent on failure, Convert falls through to its inline-fetch slow path, which (a) gets a second attempt at the GET, and (b) if that ALSO fails, surfaces the real "[Merged forward: fetch failed: ...]" string the user used to see in stdout. Inserting nil would have silently produced an empty <forwarded_messages> tree instead, dropping the failure signal from the user-visible output.

When nameCache is non-nil, this function also runs one batched ResolveSenderNames across every sub-item it fetched, populating the cache before returning. Without this step, each per-merge_forward render in the caller's loop would issue its own contact API request for any uncached sender, re-introducing an N × ~400ms serial stall (measured at 5 merge_forwards × ~400ms = ~2s in production traces). Pre-populating the cache makes those per-render ResolveSenderNames calls effective no-ops.

func ResolveMentionKeys

func ResolveMentionKeys(text string, mentionMap map[string]string) string

ResolveMentionKeys replaces mention keys in text with @name format.

func ResolveSenderNames

func ResolveSenderNames(runtime *common.RuntimeContext, messages []map[string]interface{}, cache map[string]string) map[string]string

ResolveSenderNames batch-resolves sender open_ids to display names. The cache map is used to share already-resolved IDs across calls; newly resolved names are written back into it. Pass an empty map if no prior cache exists.

Step 1: extract names from message mentions (free, no API call). Step 2: for remaining unresolved IDs, call contact batch API (requires contact:user.base:readonly). Silently returns partial results on API error.

[#22] Changed from variadic `cache ...map[string]string` to a required parameter. The variadic form was misleading: every caller passed exactly one map, and the function body both modified it and returned it, making the dual semantics confusing.

func TruncateContent

func TruncateContent(s string, max int) string

TruncateContent truncates a string for table display.

Types

type ContentConverter

type ContentConverter interface {
	Convert(ctx *ConvertContext) string
}

ContentConverter defines the interface for converting a message type's raw content to human-readable text.

type ConvertContext

type ConvertContext struct {
	RawContent string
	MentionMap map[string]string
	// Mentions is the raw mentions array from the API response.
	// Used by interactive card converter to resolve @user references via mention_key.
	Mentions []interface{}
	// MessageID and Runtime are used by merge_forward to fetch and expand sub-messages via API.
	// For other message types these can be zero values.
	MessageID string
	Runtime   *common.RuntimeContext
	// SenderNames is a shared cache of open_id -> display name, accumulated across messages
	// to avoid redundant contact API calls. May be nil.
	SenderNames map[string]string
	// MergeForwardSubItems is an optional pre-fetched cache of merge_forward
	// sub-message lists, keyed by merge_forward message_id. When set, the
	// merge_forward converter uses the cached entry instead of issuing its
	// own GET; populated by callers via PrefetchMergeForwardSubItems before
	// the FormatMessageItem loop. nil means "no prefetch — fall back to the
	// per-message inline GET", which keeps non-shortcut callers (events,
	// ad-hoc tests) working unchanged.
	MergeForwardSubItems map[string][]map[string]interface{}
}

ConvertContext holds all context needed for content conversion.

type ResourceDownloader added in v1.0.51

type ResourceDownloader func(ctx context.Context, messageID, key, fileType string) (string, int64, error)

ResourceDownloader downloads one resource and returns its local path and size in bytes. messageID is the resource's owning message id (the download API path parameter), key is the file_key/image_key, and fileType is the download API resource type ("image" or "file"). A non-nil error means the single resource failed; the engine isolates that failure (fail-silent).

type ResourceRef added in v1.0.51

type ResourceRef struct {
	MessageID string
	Key       string
	Type      string
}

ResourceRef is a downloadable resource reference extracted from a message during formatting. Type is the download API resource type ("image" or "file"); MessageID is the message id used as the download API path parameter. For a standalone message that is the message's own id; for a resource carried inside a merge_forward it is the TOP-LEVEL container's id, because the download API addresses forwarded resources by the container, not the sub-item (see extractMergeForwardResourceRefs). The extract stage fills these three fields only — local_path and size_bytes are filled later by the download enrichment stage.

func ExtractResourceRefs added in v1.0.51

func ExtractResourceRefs(msgType, rawContent, messageID string, mergeSub map[string][]map[string]interface{}) []ResourceRef

ExtractResourceRefs returns the downloadable resource refs carried by a single message's raw content. It is a pure function (no IO/runtime).

Type mapping (design GAP-002):

  • image, post img (image_key) -> type "image"
  • file, audio, video, media, post media -> type "file"
  • sticker -> never extracted (unsupported)

For merge_forward the sub-items are not standalone message nodes, so this folds them in from mergeSub (the pre-fetched flat sub-item list keyed by the merge_forward message_id); each sub-item ref carries the TOP-LEVEL container's message_id, since the download API rejects sub-item ids (234003 File not in msg) and can only fetch a forwarded resource through the container. Refs without a usable key are skipped.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL