diff --git a/.golangci.yaml b/.golangci.yaml index 4b6547a..29a02ad 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -114,7 +114,7 @@ linters-settings: allowStrs: "\"\"" allowInts: "-1,0,1,2" allowFloats: "-1.0,-1.,0.0,0.,1.0,1.,2.0,2." - ignoreFuncs: "os\\.*,fmt\\.Println,make" + ignoreFuncs: "os\\.*,fmt\\.Println,make,logger\\.*" - name: argument-limit disabled: true - name: banned-characters diff --git a/cmd/service/main.go b/cmd/service/main.go index d6aaa0a..753e6b1 100644 --- a/cmd/service/main.go +++ b/cmd/service/main.go @@ -25,6 +25,7 @@ import ( "github.com/nijeti/cinema-keeper/internal/services/listQuotes" "github.com/nijeti/cinema-keeper/internal/services/lockVoiceChan" "github.com/nijeti/cinema-keeper/internal/services/mentionVoiceChan" + "github.com/nijeti/cinema-keeper/internal/services/presence" "github.com/nijeti/cinema-keeper/internal/services/unlockVoiceChan" ) @@ -103,6 +104,7 @@ func run() (code int) { listQuotesSvc := listQuotes.New(dcAdapter, quotesRepo) lockVoiceChanSvc := lockVoiceChan.New(dcAdapter) mentionVoiceChanSvc := mentionVoiceChan.New(dcAdapter) + presenceSvc := presence.New(dcAdapter) rollSvc := diceRoll.New(dcAdapter) unlockVoiceChanSvc := unlockVoiceChan.New(dcAdapter) @@ -154,6 +156,12 @@ func run() (code int) { // run logger.Info("startup complete") + + err = presenceSvc.Set(ctx) + if err != nil { + logger.ErrorContext(ctx, "failed to set presence", "error", err) + } + <-ctx.Done() // shutdown diff --git a/internal/adapters/discord/adapter.go b/internal/adapters/discord/adapter.go index a4aaef0..10f4ee0 100644 --- a/internal/adapters/discord/adapter.go +++ b/internal/adapters/discord/adapter.go @@ -171,3 +171,19 @@ func (a *Adapter) ChannelUnsetUserLimit( return nil } + +func (a *Adapter) SetActivity( + _ context.Context, activity *discordgo.Activity, +) error { + err := a.session.UpdateStatusComplex( + discordgo.UpdateStatusData{ + Status: string(discordgo.StatusOnline), + Activities: []*discordgo.Activity{activity}, + }, + ) + if err != nil { + return fmt.Errorf("failed to update status: %w", err) + } + + return nil +} diff --git a/internal/discord/commands/responses/common.go b/internal/discord/commands/responses/common.go index 8cc8d59..3e20b7a 100644 --- a/internal/discord/commands/responses/common.go +++ b/internal/discord/commands/responses/common.go @@ -4,6 +4,13 @@ import ( "github.com/bwmarrin/discordgo" ) +func Activity() *discordgo.Activity { + return &discordgo.Activity{ + Name: "Collecting movies and quotes", + Type: discordgo.ActivityTypeCustom, + } +} + func UserNotInVoiceChannel() *discordgo.InteractionResponse { return &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, diff --git a/internal/generated/mocks/services/presence/discord.go b/internal/generated/mocks/services/presence/discord.go new file mode 100644 index 0000000..7ece74d --- /dev/null +++ b/internal/generated/mocks/services/presence/discord.go @@ -0,0 +1,84 @@ +// Code generated by mockery. DO NOT EDIT. + +package presence_test + +import ( + context "context" + + discordgo "github.com/bwmarrin/discordgo" + mock "github.com/stretchr/testify/mock" +) + +// MockDiscord is an autogenerated mock type for the discord type +type MockDiscord struct { + mock.Mock +} + +type MockDiscord_Expecter struct { + mock *mock.Mock +} + +func (_m *MockDiscord) EXPECT() *MockDiscord_Expecter { + return &MockDiscord_Expecter{mock: &_m.Mock} +} + +// SetActivity provides a mock function with given fields: ctx, activity +func (_m *MockDiscord) SetActivity(ctx context.Context, activity *discordgo.Activity) error { + ret := _m.Called(ctx, activity) + + if len(ret) == 0 { + panic("no return value specified for SetActivity") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *discordgo.Activity) error); ok { + r0 = rf(ctx, activity) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockDiscord_SetActivity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetActivity' +type MockDiscord_SetActivity_Call struct { + *mock.Call +} + +// SetActivity is a helper method to define mock.On call +// - ctx context.Context +// - activity *discordgo.Activity +func (_e *MockDiscord_Expecter) SetActivity(ctx interface{}, activity interface{}) *MockDiscord_SetActivity_Call { + return &MockDiscord_SetActivity_Call{Call: _e.mock.On("SetActivity", ctx, activity)} +} + +func (_c *MockDiscord_SetActivity_Call) Run(run func(ctx context.Context, activity *discordgo.Activity)) *MockDiscord_SetActivity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*discordgo.Activity)) + }) + return _c +} + +func (_c *MockDiscord_SetActivity_Call) Return(_a0 error) *MockDiscord_SetActivity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockDiscord_SetActivity_Call) RunAndReturn(run func(context.Context, *discordgo.Activity) error) *MockDiscord_SetActivity_Call { + _c.Call.Return(run) + return _c +} + +// NewMockDiscord creates a new instance of MockDiscord. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockDiscord(t interface { + mock.TestingT + Cleanup(func()) +}) *MockDiscord { + mock := &MockDiscord{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/services/presence/deps.go b/internal/services/presence/deps.go new file mode 100644 index 0000000..e6a066e --- /dev/null +++ b/internal/services/presence/deps.go @@ -0,0 +1,11 @@ +package presence + +import ( + "context" + + "github.com/bwmarrin/discordgo" +) + +type discord interface { + SetActivity(ctx context.Context, activity *discordgo.Activity) error +} diff --git a/internal/services/presence/service.go b/internal/services/presence/service.go new file mode 100644 index 0000000..fe37ecb --- /dev/null +++ b/internal/services/presence/service.go @@ -0,0 +1,29 @@ +package presence + +import ( + "context" + "fmt" + + "github.com/nijeti/cinema-keeper/internal/discord/commands/responses" +) + +type Service struct { + discord discord +} + +func New( + discord discord, +) *Service { + return &Service{ + discord: discord, + } +} + +func (s *Service) Set(ctx context.Context) error { + err := s.discord.SetActivity(ctx, responses.Activity()) + if err != nil { + return fmt.Errorf("failed to set activity: %w", err) + } + + return nil +} diff --git a/internal/services/presence/service_test.go b/internal/services/presence/service_test.go new file mode 100644 index 0000000..189e01e --- /dev/null +++ b/internal/services/presence/service_test.go @@ -0,0 +1,57 @@ +package presence_test + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/nijeti/cinema-keeper/internal/discord/commands/responses" + mocks "github.com/nijeti/cinema-keeper/internal/generated/mocks/services/presence" + "github.com/nijeti/cinema-keeper/internal/services/presence" +) + +func TestService_Set(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + type setup func(t *testing.T, err error) *presence.Service + + tests := map[string]struct { + err error + setup setup + }{ + "set_activity_error": { + err: errors.New("set activity error"), + setup: func(t *testing.T, err error) *presence.Service { + d := mocks.NewMockDiscord(t) + + d.EXPECT().SetActivity(ctx, responses.Activity()).Return(err) + + return presence.New(d) + }, + }, + "success": { + err: nil, + setup: func(t *testing.T, _ error) *presence.Service { + d := mocks.NewMockDiscord(t) + + d.EXPECT().SetActivity(ctx, responses.Activity()).Return(nil) + + return presence.New(d) + }, + }, + } + + for name, tt := range tests { + t.Run( + name, func(t *testing.T) { + s := tt.setup(t, tt.err) + err := s.Set(ctx) + assert.ErrorIs(t, err, tt.err) + }, + ) + } +}