@@ -1432,3 +1432,166 @@ func uploadFileFromReader(ctx context.Context, runtime *common.RuntimeContext, r
14321432 }
14331433 return fileKey , nil
14341434}
1435+
1436+ // FlagType enumerates the kind of bookmark.
1437+ // Aligned with server-side constants: Unknown=0, Feed=1, Message=2.
1438+ type FlagType int
1439+
1440+ const (
1441+ FlagTypeUnknown FlagType = 0
1442+ FlagTypeFeed FlagType = 1
1443+ FlagTypeMessage FlagType = 2
1444+ )
1445+
1446+ // ItemType enumerates the kind of thing being bookmarked.
1447+ // Server-side constants (only the types used by IM flags):
1448+ //
1449+ // default=0, thread=4, msg_thread=11.
1450+ //
1451+ // Note on the two thread-shaped item types:
1452+ // - ItemTypeThread (4) — thread inside a topic-style chat
1453+ // - ItemTypeMsgThread (11) — thread inside a regular chat
1454+ type ItemType int
1455+
1456+ const (
1457+ ItemTypeDefault ItemType = 0
1458+ ItemTypeThread ItemType = 4 // thread in a topic-style chat
1459+ ItemTypeMsgThread ItemType = 11 // thread in a regular chat
1460+ )
1461+
1462+ // flagItem is one entry in the flags API body. The server expects numeric
1463+ // enums serialized as strings.
1464+ type flagItem struct {
1465+ ItemID string `json:"item_id"`
1466+ ItemType string `json:"item_type"`
1467+ FlagType string `json:"flag_type"`
1468+ }
1469+
1470+ // parseItemID inspects an om_/omt_ prefix and returns a best-guess
1471+ // (itemType, flagType) pair. Used when the user omits the explicit enums.
1472+ // - om_xxx → (default, message)
1473+ // - omt_xxx → (thread, feed)
1474+ func parseItemID (id string ) (ItemType , FlagType , error ) {
1475+ id = strings .TrimSpace (id )
1476+ switch {
1477+ case strings .HasPrefix (id , "omt_" ):
1478+ return ItemTypeThread , FlagTypeFeed , nil
1479+ case strings .HasPrefix (id , "om_" ):
1480+ return ItemTypeDefault , FlagTypeMessage , nil
1481+ case id == "" :
1482+ return 0 , 0 , output .ErrValidation ("--item-id / --message-id / --thread-id cannot be empty" )
1483+ default :
1484+ return 0 , 0 , output .ErrValidation (
1485+ "cannot infer item type from id %q: expected om_ (message) or omt_ (thread) prefix; " +
1486+ "pass --item-type and --flag-type explicitly if you are using a different id format" , id )
1487+ }
1488+ }
1489+
1490+ // parseItemType converts a user-facing string to the server enum.
1491+ func parseItemType (s string ) (ItemType , error ) {
1492+ switch strings .ToLower (strings .TrimSpace (s )) {
1493+ case "" , "default" , "message" :
1494+ return ItemTypeDefault , nil
1495+ case "thread" :
1496+ return ItemTypeThread , nil
1497+ case "msg_thread" :
1498+ return ItemTypeMsgThread , nil
1499+ }
1500+ return 0 , output .ErrValidation ("invalid --item-type %q: expected one of default|thread|msg_thread" , s )
1501+ }
1502+
1503+ // parseFlagType converts a user-facing string to the server enum.
1504+ func parseFlagType (s string ) (FlagType , error ) {
1505+ switch strings .ToLower (strings .TrimSpace (s )) {
1506+ case "" , "message" :
1507+ return FlagTypeMessage , nil
1508+ case "feed" :
1509+ return FlagTypeFeed , nil
1510+ case "unknown" :
1511+ return FlagTypeUnknown , nil
1512+ }
1513+ return 0 , output .ErrValidation ("invalid --flag-type %q: expected one of message|feed" , s )
1514+ }
1515+
1516+ // isValidCombo checks if the (ItemType, FlagType) pair is accepted by the server.
1517+ // Valid combinations are:
1518+ // - (default, message) — regular chat message
1519+ // - (thread, feed) — thread as feed-layer flag
1520+ // - (msg_thread, feed) — message-thread as feed-layer flag
1521+ func isValidCombo (it ItemType , ft FlagType ) bool {
1522+ return (it == ItemTypeDefault && ft == FlagTypeMessage ) ||
1523+ (it == ItemTypeThread && ft == FlagTypeFeed ) ||
1524+ (it == ItemTypeMsgThread && ft == FlagTypeFeed )
1525+ }
1526+
1527+ // newFlagItem builds a payload entry with numeric-stringified enums.
1528+ func newFlagItem (itemID string , it ItemType , ft FlagType ) flagItem {
1529+ return flagItem {
1530+ ItemID : itemID ,
1531+ ItemType : fmt .Sprintf ("%d" , int (it )),
1532+ FlagType : fmt .Sprintf ("%d" , int (ft )),
1533+ }
1534+ }
1535+
1536+ // checkIsThreadRoot queries the message API to check if a message is a thread root.
1537+ // Returns (isThreadRoot, chatID, error). chatID is empty when the message is not a thread root.
1538+ func checkIsThreadRoot (rt * common.RuntimeContext , messageID string ) (bool , string , error ) {
1539+ data , err := rt .DoAPIJSON ("GET" , "/open-apis/im/v1/messages/" + validate .EncodePathSegment (messageID ), nil , nil )
1540+ if err != nil {
1541+ return false , "" , err
1542+ }
1543+
1544+ // Check items array in response
1545+ items , _ := data ["items" ].([]any )
1546+ if len (items ) == 0 {
1547+ return false , "" , nil
1548+ }
1549+
1550+ msg , _ := items [0 ].(map [string ]any )
1551+ threadID , _ := msg ["thread_id" ].(string )
1552+ chatID , _ := msg ["chat_id" ].(string )
1553+ return threadID != "" , chatID , nil
1554+ }
1555+
1556+ // resolveThreadFeedItemType determines the correct feed-layer ItemType for a thread
1557+ // by querying the chat API for chat_mode.
1558+ // - topic-style chat → ItemTypeThread
1559+ // - regular chat → ItemTypeMsgThread
1560+ //
1561+ // Falls back to ItemTypeMsgThread if the chat query fails.
1562+ func resolveThreadFeedItemType (rt * common.RuntimeContext , chatID string ) ItemType {
1563+ data , err := rt .DoAPIJSON ("GET" , "/open-apis/im/v1/chats/" + validate .EncodePathSegment (chatID ), nil , nil )
1564+ if err != nil {
1565+ return ItemTypeMsgThread
1566+ }
1567+
1568+ // DoAPIJSON returns envelope.Data, so chat_mode is at the top level
1569+ chatMode , _ := data ["chat_mode" ].(string )
1570+ if chatMode == "topic" {
1571+ return ItemTypeThread
1572+ }
1573+ return ItemTypeMsgThread
1574+ }
1575+
1576+ // resolveThreadFeedItemTypeFromThread resolves the feed-layer ItemType for an omt_ thread ID.
1577+ // It queries the message API to get the chat_id, then queries the chat API for chat_mode.
1578+ // Falls back to ItemTypeThread if any query fails.
1579+ func resolveThreadFeedItemTypeFromThread (rt * common.RuntimeContext , threadID string ) ItemType {
1580+ data , err := rt .DoAPIJSON ("GET" , "/open-apis/im/v1/messages/" + validate .EncodePathSegment (threadID ), nil , nil )
1581+ if err != nil {
1582+ return ItemTypeThread
1583+ }
1584+
1585+ items , _ := data ["items" ].([]any )
1586+ if len (items ) == 0 {
1587+ return ItemTypeThread
1588+ }
1589+
1590+ msg , _ := items [0 ].(map [string ]any )
1591+ chatID , _ := msg ["chat_id" ].(string )
1592+ if chatID == "" {
1593+ return ItemTypeThread
1594+ }
1595+
1596+ return resolveThreadFeedItemType (rt , chatID )
1597+ }
0 commit comments