From 33cddef3127ce54407e1d8e4e56c178f5430e459 Mon Sep 17 00:00:00 2001 From: OpenWaygate <27287694+OpenWaygate@users.noreply.github.com> Date: Wed, 8 Jul 2026 19:25:40 +0800 Subject: [PATCH] :sparkles: Add liveChatModerator resource package --- pkg/liveChatModerator/BUILD.bazel | 23 ++ pkg/liveChatModerator/liveChatModerator.go | 138 +++++++ .../liveChatModerator_test.go | 374 ++++++++++++++++++ 3 files changed, 535 insertions(+) create mode 100644 pkg/liveChatModerator/BUILD.bazel create mode 100644 pkg/liveChatModerator/liveChatModerator.go create mode 100644 pkg/liveChatModerator/liveChatModerator_test.go diff --git a/pkg/liveChatModerator/BUILD.bazel b/pkg/liveChatModerator/BUILD.bazel new file mode 100644 index 0000000..634a22b --- /dev/null +++ b/pkg/liveChatModerator/BUILD.bazel @@ -0,0 +1,23 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "liveChatModerator", + srcs = ["liveChatModerator.go"], + importpath = "github.com/eat-pray-ai/yutu/pkg/liveChatModerator", + visibility = ["//visibility:public"], + deps = [ + "//pkg/common", + "@com_github_jedib0t_go_pretty_v6//table", + "@org_golang_google_api//youtube/v3:youtube", + ], +) + +go_test( + name = "liveChatModerator_test", + srcs = ["liveChatModerator_test.go"], + embed = [":liveChatModerator"], + deps = [ + "//pkg/common", + "@org_golang_google_api//youtube/v3:youtube", + ], +) diff --git a/pkg/liveChatModerator/liveChatModerator.go b/pkg/liveChatModerator/liveChatModerator.go new file mode 100644 index 0000000..8ad15ed --- /dev/null +++ b/pkg/liveChatModerator/liveChatModerator.go @@ -0,0 +1,138 @@ +// Copyright 2026 eat-pray-ai & OpenWaygate +// SPDX-License-Identifier: Apache-2.0 + +package liveChatModerator + +import ( + "errors" + "fmt" + "io" + + "github.com/eat-pray-ai/yutu/pkg/common" + "github.com/jedib0t/go-pretty/v6/table" + "google.golang.org/api/youtube/v3" +) + +var ( + errGetLiveChatModerator = errors.New("failed to get live chat moderator") + errInsertLiveChatModerator = errors.New("failed to insert live chat moderator") + errDeleteLiveChatModerator = errors.New("failed to delete live chat moderator") +) + +type LiveChatModerator struct { + common.Fields + LiveChatId string `yaml:"live_chat_id" json:"live_chat_id,omitempty"` + ModeratorChannelId string `yaml:"moderator_channel_id" json:"moderator_channel_id,omitempty"` +} + +type ILiveChatModerator[T any] interface { + List(io.Writer) error + Get() ([]*T, error) + Insert(io.Writer) error + Delete(io.Writer) error +} + +type Option func(*LiveChatModerator) + +func NewLiveChatModerator(opts ...Option) ILiveChatModerator[youtube.LiveChatModerator] { + m := &LiveChatModerator{Fields: common.Fields{}} + for _, opt := range opts { + opt(m) + } + return m +} + +func (m *LiveChatModerator) Get() ([]*youtube.LiveChatModerator, error) { + if err := m.EnsureService(); err != nil { + return nil, err + } + call := m.Service.LiveChatModerators.List(m.LiveChatId, m.Parts) + + return common.Paginate( + &m.Fields, call, + func(r *youtube.LiveChatModeratorListResponse) ([]*youtube.LiveChatModerator, string) { + return r.Items, r.NextPageToken + }, errGetLiveChatModerator, + ) +} + +func (m *LiveChatModerator) List(writer io.Writer) error { + moderators, err := m.Get() + if err != nil && moderators == nil { + return err + } + + common.PrintList( + m.Output, moderators, writer, + table.Row{"ID", "Channel ID", "Display Name"}, + func(mod *youtube.LiveChatModerator) table.Row { + return table.Row{ + mod.Id, + mod.Snippet.ModeratorDetails.ChannelId, + mod.Snippet.ModeratorDetails.DisplayName, + } + }, + ) + return err +} + +func (m *LiveChatModerator) Insert(writer io.Writer) error { + if err := m.EnsureService(); err != nil { + return err + } + moderator := &youtube.LiveChatModerator{ + Snippet: &youtube.LiveChatModeratorSnippet{ + LiveChatId: m.LiveChatId, + ModeratorDetails: &youtube.ChannelProfileDetails{ + ChannelId: m.ModeratorChannelId, + }, + }, + } + + call := m.Service.LiveChatModerators.Insert(m.Parts, moderator) + res, err := call.Do() + if err != nil { + return errors.Join(errInsertLiveChatModerator, err) + } + + common.PrintResult( + m.Output, res, writer, "Live chat moderator inserted: %s\n", res.Id, + ) + return nil +} + +func (m *LiveChatModerator) Delete(writer io.Writer) error { + if err := m.EnsureService(); err != nil { + return err + } + for _, id := range m.Ids { + call := m.Service.LiveChatModerators.Delete(id) + err := call.Do() + if err != nil { + return errors.Join(errDeleteLiveChatModerator, err) + } + + _, _ = fmt.Fprintf(writer, "Live chat moderator %s deleted\n", id) + } + return nil +} + +func WithLiveChatId(liveChatId string) Option { + return func(m *LiveChatModerator) { + m.LiveChatId = liveChatId + } +} + +func WithModeratorChannelId(channelId string) Option { + return func(m *LiveChatModerator) { + m.ModeratorChannelId = channelId + } +} + +var ( + WithMaxResults = common.WithMaxResults[*LiveChatModerator] + WithParts = common.WithParts[*LiveChatModerator] + WithOutput = common.WithOutput[*LiveChatModerator] + WithService = common.WithService[*LiveChatModerator] + WithIds = common.WithIds[*LiveChatModerator] +) diff --git a/pkg/liveChatModerator/liveChatModerator_test.go b/pkg/liveChatModerator/liveChatModerator_test.go new file mode 100644 index 0000000..a8b2573 --- /dev/null +++ b/pkg/liveChatModerator/liveChatModerator_test.go @@ -0,0 +1,374 @@ +// Copyright 2026 eat-pray-ai & OpenWaygate +// SPDX-License-Identifier: Apache-2.0 + +package liveChatModerator + +import ( + "bytes" + "encoding/json" + "io" + "math" + "net/http" + "reflect" + "testing" + + "github.com/eat-pray-ai/yutu/pkg/common" + "google.golang.org/api/youtube/v3" +) + +func TestNewLiveChatModerator(t *testing.T) { + type args struct { + opts []Option + } + + svc := &youtube.Service{} + + tests := []struct { + name string + args args + want ILiveChatModerator[youtube.LiveChatModerator] + }{ + { + name: "with all options", + args: args{ + opts: []Option{ + WithIds([]string{"mod1", "mod2"}), + WithLiveChatId("liveChat123"), + WithModeratorChannelId("channel456"), + WithMaxResults(50), + WithParts([]string{"snippet", "id"}), + WithOutput("json"), + WithService(svc), + }, + }, + want: &LiveChatModerator{ + Fields: common.Fields{ + Service: svc, + Parts: []string{"snippet", "id"}, + Output: "json", + MaxResults: 50, + Ids: []string{"mod1", "mod2"}, + }, + LiveChatId: "liveChat123", + ModeratorChannelId: "channel456", + }, + }, + { + name: "with no options", + args: args{ + opts: []Option{}, + }, + want: &LiveChatModerator{Fields: common.Fields{}}, + }, + { + name: "with zero max results", + args: args{ + opts: []Option{ + WithMaxResults(0), + }, + }, + want: &LiveChatModerator{ + Fields: common.Fields{MaxResults: math.MaxInt64}, + }, + }, + { + name: "with negative max results", + args: args{ + opts: []Option{ + WithMaxResults(-10), + }, + }, + want: &LiveChatModerator{ + Fields: common.Fields{MaxResults: 1}, + }, + }, + { + name: "with empty string values", + args: args{ + opts: []Option{ + WithLiveChatId(""), + WithModeratorChannelId(""), + }, + }, + want: &LiveChatModerator{ + Fields: common.Fields{}, + LiveChatId: "", + ModeratorChannelId: "", + }, + }, + { + name: "with partial options", + args: args{ + opts: []Option{ + WithLiveChatId("chat789"), + WithMaxResults(25), + WithParts([]string{"id"}), + }, + }, + want: &LiveChatModerator{ + Fields: common.Fields{ + Parts: []string{"id"}, + MaxResults: 25, + }, + LiveChatId: "chat789", + }, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + if got := NewLiveChatModerator(tt.args.opts...); !reflect.DeepEqual( + got, tt.want, + ) { + t.Errorf("%s\nNewLiveChatModerator() = %v\nwant %v", tt.name, got, tt.want) + } + }, + ) + } +} + +func TestLiveChatModerator_Get(t *testing.T) { + tests := []struct { + name string + opts []Option + verify func(*http.Request) + wantLen int + wantErr bool + }{ + { + name: "get moderators for live chat", + opts: []Option{ + WithLiveChatId("live-chat-id"), + WithMaxResults(1), + }, + verify: func(r *http.Request) { + q := r.URL.Query() + if q.Get("liveChatId") != "live-chat-id" { + t.Errorf( + "expected liveChatId=live-chat-id, got %s", + q.Get("liveChatId"), + ) + } + }, + wantLen: 1, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + svc := common.NewTestService( + t, http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if tt.verify != nil { + tt.verify(r) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write( + []byte(`{ + "items": [ + { + "id": "mod-1", + "snippet": { + "liveChatId": "live-chat-id", + "moderatorDetails": { + "channelId": "channel-1", + "displayName": "Moderator 1" + } + } + } + ] + }`), + ) + }, + ), + ) + + opts := append([]Option{WithService(svc)}, tt.opts...) + m := NewLiveChatModerator(opts...) + got, err := m.Get() + if (err != nil) != tt.wantErr { + t.Errorf("LiveChatModerator.Get() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(got) != tt.wantLen { + t.Errorf("LiveChatModerator.Get() got length = %v, want %v", len(got), tt.wantLen) + } + }, + ) + } +} + +func TestLiveChatModerator_Get_Pagination(t *testing.T) { + svc := common.NewTestService(t, common.PaginationHandler("moderator")) + + m := NewLiveChatModerator( + WithService(svc), + WithLiveChatId("live-chat-id"), + WithMaxResults(22), + ) + got, err := m.Get() + if err != nil { + t.Errorf("LiveChatModerator.Get() error = %v", err) + } + if len(got) != 22 { + t.Errorf("LiveChatModerator.Get() got length = %v, want 22", len(got)) + } +} + +func TestLiveChatModerator_List(t *testing.T) { + mockResponse := `{ + "items": [ + { + "id": "mod-1", + "snippet": { + "liveChatId": "live-chat-id", + "moderatorDetails": { + "channelId": "channel-1", + "displayName": "Moderator 1" + } + } + } + ] + }` + + common.RunListTest( + t, mockResponse, + func(svc *youtube.Service, output string) func(io.Writer) error { + m := NewLiveChatModerator( + WithService(svc), + WithLiveChatId("live-chat-id"), + WithOutput(output), + WithMaxResults(1), + ) + return m.List + }, + ) +} + +func TestLiveChatModerator_Insert(t *testing.T) { + tests := []struct { + name string + opts []Option + verify func(*http.Request) + wantErr bool + }{ + { + name: "insert live chat moderator", + opts: []Option{ + WithLiveChatId("live-chat-id"), + WithModeratorChannelId("moderator-channel-id"), + WithParts([]string{"snippet"}), + }, + verify: func(r *http.Request) { + if r.Method != "POST" { + t.Errorf("expected POST, got %s", r.Method) + } + + defer func() { _ = r.Body.Close() }() + var body struct { + Snippet struct { + LiveChatId string `json:"liveChatId"` + ModeratorDetails struct { + ChannelId string `json:"channelId"` + } `json:"moderatorDetails"` + } `json:"snippet"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("failed to decode request body: %v", err) + } + + if body.Snippet.LiveChatId != "live-chat-id" { + t.Errorf( + "expected snippet.liveChatId=live-chat-id, got %s", + body.Snippet.LiveChatId, + ) + } + if body.Snippet.ModeratorDetails.ChannelId != "moderator-channel-id" { + t.Errorf( + "expected snippet.moderatorDetails.channelId=moderator-channel-id, got %s", + body.Snippet.ModeratorDetails.ChannelId, + ) + } + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + svc := common.NewTestService( + t, http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if tt.verify != nil { + tt.verify(r) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id": "new-mod-id"}`)) + }, + ), + ) + + opts := append([]Option{WithService(svc)}, tt.opts...) + m := NewLiveChatModerator(opts...) + var buf bytes.Buffer + if err := m.Insert(&buf); (err != nil) != tt.wantErr { + t.Errorf( + "LiveChatModerator.Insert() error = %v, wantErr %v", err, tt.wantErr, + ) + } + }, + ) + } +} + +func TestLiveChatModerator_Delete(t *testing.T) { + tests := []struct { + name string + opts []Option + verify func(*http.Request) + wantErr bool + }{ + { + name: "delete live chat moderator", + opts: []Option{ + WithIds([]string{"mod-id"}), + }, + verify: func(r *http.Request) { + if r.Method != "DELETE" { + t.Errorf("expected DELETE, got %s", r.Method) + } + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + svc := common.NewTestService( + t, http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if tt.verify != nil { + tt.verify(r) + } + w.WriteHeader(http.StatusNoContent) + }, + ), + ) + + opts := append([]Option{WithService(svc)}, tt.opts...) + m := NewLiveChatModerator(opts...) + var buf bytes.Buffer + if err := m.Delete(&buf); (err != nil) != tt.wantErr { + t.Errorf( + "LiveChatModerator.Delete() error = %v, wantErr %v", err, tt.wantErr, + ) + } + }, + ) + } +} \ No newline at end of file