mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 09:11:44 +08:00
fix(vc): simplify meeting events output contract
This commit is contained in:
@@ -168,10 +168,8 @@ type meetingEventsEvent struct {
|
||||
EventID string `json:"event_id,omitempty"`
|
||||
EventType string `json:"event_type,omitempty"`
|
||||
EventTime string `json:"event_time,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Actors []meetingEventsIdentity `json:"actors,omitempty"`
|
||||
Payload map[string]interface{} `json:"payload,omitempty"`
|
||||
Raw map[string]interface{} `json:"raw,omitempty"`
|
||||
}
|
||||
|
||||
func buildMeetingEventsOutput(data map[string]interface{}, events []interface{}, currentRoster []interface{}, identity meetingEventsIdentity, warnings ...string) meetingEventsOutput {
|
||||
@@ -197,7 +195,7 @@ func buildMeetingEventsOutput(data map[string]interface{}, events []interface{},
|
||||
}
|
||||
output.Events = append(output.Events, meetingEventsEventFromPayload(event, output.Identity))
|
||||
}
|
||||
output.CurrentRoster = meetingEventsCurrentRoster(currentRoster, output.Identity)
|
||||
output.CurrentRoster = enrichMeetingEventsCurrentRoster(meetingEventsCurrentRoster(currentRoster, output.Identity), output.Events)
|
||||
return output
|
||||
}
|
||||
|
||||
@@ -217,7 +215,7 @@ func meetingEventsCurrentIdentity(runtime *common.RuntimeContext) (meetingEvents
|
||||
Role: "user",
|
||||
IsSelf: true,
|
||||
}
|
||||
identity.Label = identityLabel(identity)
|
||||
identity.Label = currentIdentityLabel(identity)
|
||||
if userOpenID == "" {
|
||||
return identity, "identity unavailable: current user open_id is unavailable"
|
||||
}
|
||||
@@ -227,7 +225,7 @@ func meetingEventsCurrentIdentity(runtime *common.RuntimeContext) (meetingEvents
|
||||
func fetchMeetingEventsCurrentRoster(runtime *common.RuntimeContext) ([]interface{}, string) {
|
||||
meetingID := strings.TrimSpace(runtime.Str("meeting-id"))
|
||||
data, err := runtime.CallAPITyped(http.MethodGet, fmt.Sprintf("/open-apis/vc/v1/meetings/%s", validate.EncodePathSegment(meetingID)),
|
||||
map[string]interface{}{"with_participants": "true", "query_mode": "0"}, nil)
|
||||
map[string]interface{}{"with_participants": "true", "query_mode": "0", "user_id_type": "open_id"}, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Sprintf("current_roster unavailable: %v", err)
|
||||
}
|
||||
@@ -248,7 +246,7 @@ func meetingEventsBotIdentity(botInfo *common.BotInfo) meetingEventsIdentity {
|
||||
Role: "bot",
|
||||
IsSelf: true,
|
||||
}
|
||||
identity.Label = identityLabel(identity)
|
||||
identity.Label = currentIdentityLabel(identity)
|
||||
return identity
|
||||
}
|
||||
|
||||
@@ -276,14 +274,11 @@ func meetingEventsMeetingFromPayload(meeting map[string]interface{}) meetingEven
|
||||
|
||||
func meetingEventsEventFromPayload(event map[string]interface{}, selfIdentity meetingEventsIdentity) meetingEventsEvent {
|
||||
payload := common.GetMap(event, "payload")
|
||||
rawCopy := cloneStringMap(event)
|
||||
out := meetingEventsEvent{
|
||||
EventID: common.GetString(event, "event_id"),
|
||||
EventType: meetingEventType(event),
|
||||
EventTime: meetingEventsTimeString(common.GetString(event, "event_time")),
|
||||
Summary: meetingEventSummary(event),
|
||||
Payload: payload,
|
||||
Raw: rawCopy,
|
||||
}
|
||||
out.Actors = eventActors(out.EventType, payload, selfIdentity)
|
||||
return out
|
||||
@@ -305,6 +300,30 @@ func meetingEventsCurrentRoster(rawRoster []interface{}, selfIdentity meetingEve
|
||||
return roster
|
||||
}
|
||||
|
||||
func enrichMeetingEventsCurrentRoster(roster []meetingEventsIdentity, events []meetingEventsEvent) []meetingEventsIdentity {
|
||||
typeByID := make(map[string]string)
|
||||
for _, event := range events {
|
||||
for _, actor := range event.Actors {
|
||||
if actor.ID == "" || !isKnownParticipantType(actor.ParticipantType) {
|
||||
continue
|
||||
}
|
||||
if _, exists := typeByID[actor.ID]; !exists {
|
||||
typeByID[actor.ID] = actor.ParticipantType
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range roster {
|
||||
if !isUnknownParticipantType(roster[i].ParticipantType) {
|
||||
continue
|
||||
}
|
||||
if participantType := typeByID[roster[i].ID]; participantType != "" {
|
||||
roster[i].ParticipantType = participantType
|
||||
roster[i].Label = identityLabel(roster[i])
|
||||
}
|
||||
}
|
||||
return roster
|
||||
}
|
||||
|
||||
func eventActors(eventType string, payload map[string]interface{}, selfIdentity meetingEventsIdentity) []meetingEventsIdentity {
|
||||
var actors []meetingEventsIdentity
|
||||
addFromItems := func(key, participantKey string) {
|
||||
@@ -378,7 +397,7 @@ func meetingEventsParticipantTypeFromParticipantType(raw string) string {
|
||||
case "":
|
||||
return ""
|
||||
default:
|
||||
return raw
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,10 +418,23 @@ func meetingEventsParticipantTypeFromUserType(raw string) string {
|
||||
case "":
|
||||
return ""
|
||||
default:
|
||||
return raw
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func isKnownParticipantType(participantType string) bool {
|
||||
switch participantType {
|
||||
case "human", "bot":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isUnknownParticipantType(participantType string) bool {
|
||||
return participantType == "" || participantType == "unknown"
|
||||
}
|
||||
|
||||
func meetingEventsRoleFromRosterRole(raw string) string {
|
||||
raw = strings.ToLower(strings.TrimSpace(raw))
|
||||
switch raw {
|
||||
@@ -481,6 +513,11 @@ func identityLabel(identity meetingEventsIdentity) string {
|
||||
return fmt.Sprintf("%s [%s]", name, strings.Join(tags, ","))
|
||||
}
|
||||
|
||||
func currentIdentityLabel(identity meetingEventsIdentity) string {
|
||||
identity.IsSelf = false
|
||||
return identityLabel(identity)
|
||||
}
|
||||
|
||||
func meetingEventsTimeString(raw string) string {
|
||||
if parsed, ok := parseFlexibleTime(raw); ok {
|
||||
return parsed.UTC().Format(time.RFC3339)
|
||||
@@ -511,10 +548,8 @@ func meetingEventsEventRows(events []meetingEventsEvent, metadata map[string]int
|
||||
"event_id": event.EventID,
|
||||
"event_type": event.EventType,
|
||||
"event_time": event.EventTime,
|
||||
"summary": event.Summary,
|
||||
"actors": event.Actors,
|
||||
"payload": event.Payload,
|
||||
"raw": event.Raw,
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
|
||||
@@ -569,11 +569,25 @@ func TestMeetingEvents_ExecuteJSON_PageAll(t *testing.T) {
|
||||
}
|
||||
reg.Verify(t)
|
||||
|
||||
var envelope map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(stdout.String()), &envelope); err != nil {
|
||||
t.Fatalf("unmarshal stdout: %v: %s", err, stdout.String())
|
||||
}
|
||||
events := common.GetSlice(common.GetMap(envelope, "data"), "events")
|
||||
if got := len(events); got != 2 {
|
||||
t.Fatalf("events len = %d, want 2: %s", got, stdout.String())
|
||||
}
|
||||
for _, raw := range events {
|
||||
event, _ := raw.(map[string]interface{})
|
||||
if _, ok := event["summary"]; ok {
|
||||
t.Fatalf("event should not expose summary: %s", stdout.String())
|
||||
}
|
||||
if _, ok := event["raw"]; ok {
|
||||
t.Fatalf("event should not expose raw: %s", stdout.String())
|
||||
}
|
||||
}
|
||||
out := strings.ReplaceAll(stdout.String(), " ", "")
|
||||
out = strings.ReplaceAll(out, "\n", "")
|
||||
if count := strings.Count(out, `"summary":"participantbot_001(DemoBot)joined"`); count != 2 {
|
||||
t.Fatalf("expected 2 aggregated events, got %d: %s", count, stdout.String())
|
||||
}
|
||||
if !strings.Contains(out, `"has_more":false`) {
|
||||
t.Fatalf("expected final has_more=false: %s", stdout.String())
|
||||
}
|
||||
@@ -602,7 +616,7 @@ func TestMeetingEvents_ExecuteJSON(t *testing.T) {
|
||||
out := strings.ReplaceAll(stdout.String(), " ", "")
|
||||
out = strings.ReplaceAll(out, "\n", "")
|
||||
for _, want := range []string{
|
||||
`"identity":{"id":"bot_001","name":"DemoBot","participant_type":"bot","role":"bot","is_self":true,"label":"DemoBot[bot,self]"}`,
|
||||
`"identity":{"id":"bot_001","name":"DemoBot","participant_type":"bot","role":"bot","is_self":true,"label":"DemoBot[bot]"}`,
|
||||
`"current_roster":[`,
|
||||
`"role":"host"`,
|
||||
`"is_self":true`,
|
||||
@@ -617,6 +631,14 @@ func TestMeetingEvents_ExecuteJSON(t *testing.T) {
|
||||
t.Fatalf("json output missing %q: %s", want, stdout.String())
|
||||
}
|
||||
}
|
||||
for _, unwanted := range []string{
|
||||
`"summary":`,
|
||||
`"raw":`,
|
||||
} {
|
||||
if strings.Contains(out, unwanted) {
|
||||
t.Fatalf("json output should not contain %q: %s", unwanted, stdout.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeetingEvents_ExecuteJSON_RosterErrorDoesNotBlockEvents(t *testing.T) {
|
||||
@@ -703,7 +725,7 @@ func TestMeetingEvents_ExecuteJSON_UserIdentitySkipsBotInfo(t *testing.T) {
|
||||
out := strings.ReplaceAll(stdout.String(), " ", "")
|
||||
out = strings.ReplaceAll(out, "\n", "")
|
||||
for _, want := range []string{
|
||||
`"identity":{"id":"ou_testuser","participant_type":"human","role":"user","is_self":true,"label":"ou_testuser[human,user,self]"}`,
|
||||
`"identity":{"id":"ou_testuser","participant_type":"human","role":"user","is_self":true,"label":"ou_testuser[human,user]"}`,
|
||||
`"current_roster":[`,
|
||||
`"id":"ou_testuser"`,
|
||||
`"is_self":true`,
|
||||
@@ -798,6 +820,14 @@ func TestMeetingEvents_ExecuteNDJSONIncludesMetadataRow(t *testing.T) {
|
||||
if !strings.Contains(lines[0], `"row_type":"event"`) || !strings.Contains(lines[0], `"event_type":"participant_joined"`) {
|
||||
t.Fatalf("first ndjson row should be event: %s", lines[0])
|
||||
}
|
||||
for _, unwanted := range []string{
|
||||
`"summary":`,
|
||||
`"raw":`,
|
||||
} {
|
||||
if strings.Contains(lines[0], unwanted) {
|
||||
t.Fatalf("event ndjson row should not contain %q: %s", unwanted, lines[0])
|
||||
}
|
||||
}
|
||||
for _, want := range []string{
|
||||
`"row_type":"metadata"`,
|
||||
`"has_more":true`,
|
||||
@@ -901,7 +931,7 @@ func TestMeetingEvents_ExecutePretty(t *testing.T) {
|
||||
|
||||
out := stdout.String()
|
||||
for _, want := range []string{
|
||||
"当前身份:Demo Bot [bot,self]",
|
||||
"当前身份:Demo Bot [bot]",
|
||||
"当前名单:",
|
||||
"- Demo Bot [bot,self]",
|
||||
"- Alice [human,host]",
|
||||
@@ -1274,6 +1304,46 @@ func TestMeetingEventsIdentityFromParticipant_UserTypeApp(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeetingEventsIdentityFromParticipant_UnknownUserType(t *testing.T) {
|
||||
got := meetingEventsIdentityFromParticipant(map[string]interface{}{
|
||||
"id": "u_unknown",
|
||||
"user_name": "Unknown",
|
||||
"user_type": 0,
|
||||
"user_role": 1,
|
||||
}, meetingEventsIdentity{})
|
||||
|
||||
if got.ParticipantType != "unknown" {
|
||||
t.Fatalf("identity = %#v, want participant_type=unknown", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMeetingEventsOutput_EnrichesUnknownRosterTypeFromActors(t *testing.T) {
|
||||
event := participantJoinedEvent()
|
||||
payload := common.GetMap(event, "payload")
|
||||
items := common.GetSlice(payload, "participant_joined_items")
|
||||
item, _ := items[0].(map[string]interface{})
|
||||
item["participant"] = map[string]interface{}{
|
||||
"id": "ou_bot",
|
||||
"user_name": "Meeting Bot",
|
||||
"user_type": 10,
|
||||
"user_role": 1,
|
||||
}
|
||||
|
||||
out := buildMeetingEventsOutput(map[string]interface{}{}, []interface{}{event}, []interface{}{
|
||||
map[string]interface{}{"id": "ou_bot", "user_name": "Meeting Bot", "user_type": 0, "role": "3"},
|
||||
}, meetingEventsIdentity{})
|
||||
|
||||
if got := len(out.CurrentRoster); got != 1 {
|
||||
t.Fatalf("current_roster len = %d, want 1", got)
|
||||
}
|
||||
if got := out.CurrentRoster[0].ParticipantType; got != "bot" {
|
||||
t.Fatalf("current_roster participant_type = %q, want bot: %#v", got, out.CurrentRoster[0])
|
||||
}
|
||||
if got := out.CurrentRoster[0].Label; got != "Meeting Bot [bot,participant]" {
|
||||
t.Fatalf("current_roster label = %q, want enriched bot label", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeetingEventsIdentityFromParticipant_IgnoresGenericTypeField(t *testing.T) {
|
||||
got := meetingEventsIdentityFromParticipant(map[string]interface{}{
|
||||
"id": "u1",
|
||||
|
||||
Reference in New Issue
Block a user