Disable buttons backing non-implemented functions

This commit is contained in:
Dmitrii Okunev
2024-10-31 00:11:04 +00:00
parent ebd71e92b1
commit 12e051c5b3
18 changed files with 1952 additions and 1657 deletions

View File

@@ -0,0 +1,11 @@
package streamcontrol
type Capability uint
const (
CapabilityUndefined = Capability(iota)
CapabilitySendChatMessage
CapabilityDeleteChatMessage
CapabilityBanUser
EndOfCapability
)

View File

@@ -179,3 +179,18 @@ func (k *Kick) StartStream(
logger.Warnf(ctx, "not implemented yet") logger.Warnf(ctx, "not implemented yet")
return nil return nil
} }
func (k *Kick) IsCapable(
ctx context.Context,
cap streamcontrol.Capability,
) bool {
switch cap {
case streamcontrol.CapabilitySendChatMessage:
return false
case streamcontrol.CapabilityDeleteChatMessage:
return false
case streamcontrol.CapabilityBanUser:
return false
}
return false
}

View File

@@ -284,3 +284,10 @@ func (obs *OBS) BanUser(
) error { ) error {
return fmt.Errorf("not implemented, yet") return fmt.Errorf("not implemented, yet")
} }
func (obs *OBS) IsCapable(
ctx context.Context,
cap streamcontrol.Capability,
) bool {
return false
}

View File

@@ -131,6 +131,8 @@ type StreamControllerCommons interface {
SendChatMessage(ctx context.Context, message string) error SendChatMessage(ctx context.Context, message string) error
RemoveChatMessage(ctx context.Context, messageID ChatMessageID) error RemoveChatMessage(ctx context.Context, messageID ChatMessageID) error
BanUser(ctx context.Context, userID ChatUserID, reason string, deadline time.Time) error BanUser(ctx context.Context, userID ChatUserID, reason string, deadline time.Time) error
IsCapable(context.Context, Capability) bool
} }
type StreamController[ProfileType StreamProfile] interface { type StreamController[ProfileType StreamProfile] interface {
@@ -241,6 +243,9 @@ func (c *abstractStreamController) RemoveChatMessage(ctx context.Context, messag
func (c *abstractStreamController) BanUser(ctx context.Context, userID ChatUserID, reason string, deadline time.Time) error { func (c *abstractStreamController) BanUser(ctx context.Context, userID ChatUserID, reason string, deadline time.Time) error {
return c.StreamController.BanUser(ctx, userID, reason, deadline) return c.StreamController.BanUser(ctx, userID, reason, deadline)
} }
func (c *abstractStreamController) IsCapable(ctx context.Context, cap Capability) bool {
return c.StreamController.IsCapable(ctx, cap)
}
func ToAbstract[T StreamProfile](c StreamController[T]) AbstractStreamController { func ToAbstract[T StreamProfile](c StreamController[T]) AbstractStreamController {
if c == nil { if c == nil {

View File

@@ -903,3 +903,18 @@ func (t *Twitch) BanUser(
}) })
return err return err
} }
func (t *Twitch) IsCapable(
ctx context.Context,
cap streamcontrol.Capability,
) bool {
switch cap {
case streamcontrol.CapabilitySendChatMessage:
return true
case streamcontrol.CapabilityDeleteChatMessage:
return true
case streamcontrol.CapabilityBanUser:
return true
}
return false
}

View File

@@ -1400,3 +1400,18 @@ func (yt *YouTube) BanUser(
) error { ) error {
return fmt.Errorf("not implemented, yet") return fmt.Errorf("not implemented, yet")
} }
func (yt *YouTube) IsCapable(
ctx context.Context,
cap streamcontrol.Capability,
) bool {
switch cap {
case streamcontrol.CapabilitySendChatMessage:
return true
case streamcontrol.CapabilityDeleteChatMessage:
return true
case streamcontrol.CapabilityBanUser:
return false
}
return false
}

View File

@@ -0,0 +1,10 @@
package api
import (
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
)
type BackendInfo struct {
Data any
Capabilities map[streamcontrol.Capability]struct{}
}

View File

@@ -66,10 +66,10 @@ type StreamD interface {
profile streamcontrol.AbstractStreamProfile, profile streamcontrol.AbstractStreamProfile,
customArgs ...any, customArgs ...any,
) error ) error
GetBackendData( GetBackendInfo(
ctx context.Context, ctx context.Context,
platID streamcontrol.PlatformName, platID streamcontrol.PlatformName,
) (any, error) ) (*BackendInfo, error)
EXPERIMENTAL_ReinitStreamControllers(ctx context.Context) error EXPERIMENTAL_ReinitStreamControllers(ctx context.Context) error
GetStreamStatus( GetStreamStatus(
ctx context.Context, ctx context.Context,

View File

@@ -26,9 +26,6 @@ import (
"github.com/xaionaro-go/streamctl/pkg/player" "github.com/xaionaro-go/streamctl/pkg/player"
"github.com/xaionaro-go/streamctl/pkg/player/protobuf/go/player_grpc" "github.com/xaionaro-go/streamctl/pkg/player/protobuf/go/player_grpc"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol" "github.com/xaionaro-go/streamctl/pkg/streamcontrol"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/kick"
obs "github.com/xaionaro-go/streamctl/pkg/streamcontrol/obs/types"
twitch "github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch/types"
youtube "github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube/types" youtube "github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube/types"
"github.com/xaionaro-go/streamctl/pkg/streamd/api" "github.com/xaionaro-go/streamctl/pkg/streamd/api"
streamdconfig "github.com/xaionaro-go/streamctl/pkg/streamd/config" streamdconfig "github.com/xaionaro-go/streamctl/pkg/streamd/config"
@@ -710,10 +707,10 @@ func (c *Client) EndStream(
return err return err
} }
func (c *Client) GetBackendData( func (c *Client) GetBackendInfo(
ctx context.Context, ctx context.Context,
platID streamcontrol.PlatformName, platID streamcontrol.PlatformName,
) (any, error) { ) (*api.BackendInfo, error) {
reply, err := withStreamDClient(ctx, c, func( reply, err := withStreamDClient(ctx, c, func(
ctx context.Context, ctx context.Context,
client streamd_grpc.StreamDClient, client streamd_grpc.StreamDClient,
@@ -735,50 +732,17 @@ func (c *Client) GetBackendData(
) )
} }
var data any caps := goconv.CapabilitiesGRPC2Go(ctx, reply.Capabilities)
switch platID {
case obs.ID: data, err := goconv.BackendDataGRPC2Go(platID, reply.GetData())
_data := api.BackendDataOBS{} if err != nil {
err = json.Unmarshal( return nil, fmt.Errorf("unable to deserialize data: %w", err)
[]byte(reply.GetData()),
&_data,
)
data = _data
case twitch.ID:
_data := api.BackendDataTwitch{}
err = json.Unmarshal(
[]byte(reply.GetData()),
&_data,
)
data = _data
case kick.ID:
_data := api.BackendDataKick{}
err = json.Unmarshal(
[]byte(reply.GetData()),
&_data,
)
data = _data
case youtube.ID:
_data := api.BackendDataYouTube{}
err = json.Unmarshal(
[]byte(reply.GetData()),
&_data,
)
data = _data
default:
return nil, fmt.Errorf(
"unknown platform: '%s'",
platID,
)
} }
if err != nil { return &api.BackendInfo{
return nil, fmt.Errorf( Data: data,
"unable to deserialize data: %w", Capabilities: caps,
err, }, nil
)
}
return data, nil
} }
func (c *Client) Restart(ctx context.Context) error { func (c *Client) Restart(ctx context.Context) error {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
package goconv
import (
"encoding/json"
"fmt"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/kick"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/obs"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube"
"github.com/xaionaro-go/streamctl/pkg/streamd/api"
)
func BackendDataGRPC2Go(
platID streamcontrol.PlatformName,
dataString string,
) (any, error) {
var data any
var err error
switch platID {
case obs.ID:
_data := api.BackendDataOBS{}
err = json.Unmarshal([]byte(dataString), &_data)
data = _data
case twitch.ID:
_data := api.BackendDataTwitch{}
err = json.Unmarshal([]byte(dataString), &_data)
data = _data
case kick.ID:
_data := api.BackendDataKick{}
err = json.Unmarshal([]byte(dataString), &_data)
data = _data
case youtube.ID:
_data := api.BackendDataYouTube{}
err = json.Unmarshal([]byte(dataString), &_data)
data = _data
default:
return nil, fmt.Errorf(
"unknown platform: '%s'",
platID,
)
}
return data, err
}

View File

@@ -0,0 +1,57 @@
package goconv
import (
"context"
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
"github.com/xaionaro-go/streamctl/pkg/streamd/grpc/go/streamd_grpc"
)
func CapabilitiesGRPC2Go(
ctx context.Context,
caps []streamd_grpc.Capability,
) map[streamcontrol.Capability]struct{} {
m := map[streamcontrol.Capability]struct{}{}
for _, cap := range caps {
var r streamcontrol.Capability
switch cap {
case streamd_grpc.Capability_SendChatMessage:
r = streamcontrol.CapabilitySendChatMessage
case streamd_grpc.Capability_DeleteChatMessage:
r = streamcontrol.CapabilityDeleteChatMessage
case streamd_grpc.Capability_BanUser:
r = streamcontrol.CapabilityBanUser
default:
logger.Warnf(ctx, "unexpected capability: %v", cap)
continue
}
m[r] = struct{}{}
}
return m
}
func CapabilitiesGo2GRPC(
ctx context.Context,
caps map[streamcontrol.Capability]struct{},
) []streamd_grpc.Capability {
var result []streamd_grpc.Capability
for cap := range caps {
var item streamd_grpc.Capability
switch cap {
case streamcontrol.CapabilitySendChatMessage:
item = streamd_grpc.Capability_SendChatMessage
case streamcontrol.CapabilityDeleteChatMessage:
item = streamd_grpc.Capability_DeleteChatMessage
case streamcontrol.CapabilityBanUser:
item = streamd_grpc.Capability_BanUser
default:
logger.Warnf(ctx, "unexpected capability: %v", cap)
continue
}
result = append(result, item)
}
return result
}

View File

@@ -160,6 +160,7 @@ message GetBackendInfoRequest {
message GetBackendInfoReply { message GetBackendInfoReply {
bool isInitialized = 1; bool isInitialized = 1;
string data = 2; string data = 2;
repeated Capability capabilities = 3;
} }
message IsBackendEnabledRequest { message IsBackendEnabledRequest {
string platID = 1; string platID = 1;
@@ -167,6 +168,12 @@ message IsBackendEnabledRequest {
message IsBackendEnabledReply { message IsBackendEnabledReply {
bool isInitialized = 1; bool isInitialized = 1;
} }
enum Capability {
capabilityUndefined = 0;
SendChatMessage = 1;
DeleteChatMessage = 2;
BanUser = 3;
}
message RestartRequest {} message RestartRequest {}
message RestartReply {} message RestartReply {}

View File

@@ -334,14 +334,15 @@ func (grpc *GRPCServer) GetBackendInfo(
err, err,
) )
} }
data, err := grpc.StreamD.GetBackendData(ctx, platID) info, err := grpc.StreamD.GetBackendInfo(ctx, platID)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"unable to get the backend info: %w", "unable to get the backend info: %w",
err, err,
) )
} }
dataSerialized, err := json.Marshal(data)
dataSerialized, err := json.Marshal(info.Data)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"unable to serialize the backend info: %w", "unable to serialize the backend info: %w",
@@ -352,6 +353,7 @@ func (grpc *GRPCServer) GetBackendInfo(
return &streamd_grpc.GetBackendInfoReply{ return &streamd_grpc.GetBackendInfoReply{
IsInitialized: isEnabled, IsInitialized: isEnabled,
Data: string(dataSerialized), Data: string(dataSerialized),
Capabilities: goconv.CapabilitiesGo2GRPC(ctx, info.Capabilities),
}, nil }, nil
} }

View File

@@ -680,9 +680,37 @@ func (d *StreamD) EndStream(ctx context.Context, platID streamcontrol.PlatformNa
}) })
} }
func (d *StreamD) GetBackendData( func (d *StreamD) GetBackendInfo(
ctx context.Context, ctx context.Context,
platID streamcontrol.PlatformName, platID streamcontrol.PlatformName,
) (*api.BackendInfo, error) {
ctrl, err := d.streamController(ctx, platID)
if err != nil {
return nil, fmt.Errorf("unable to get stream controller for platform '%s': %w", platID, err)
}
caps := map[streamcontrol.Capability]struct{}{}
for cap := streamcontrol.CapabilityUndefined + 1; cap < streamcontrol.EndOfCapability; cap++ {
isCapable := ctrl.IsCapable(ctx, cap)
if isCapable {
caps[cap] = struct{}{}
}
}
data, err := d.getBackendData(ctx, platID)
if err != nil {
return nil, fmt.Errorf("unable to get backend data: %w", err)
}
return &api.BackendInfo{
Data: data,
Capabilities: caps,
}, nil
}
func (d *StreamD) getBackendData(
_ context.Context,
platID streamcontrol.PlatformName,
) (any, error) { ) (any, error) {
switch platID { switch platID {
case obs.ID: case obs.ID:

View File

@@ -31,6 +31,9 @@ type chatUI struct {
MessagesHistoryLocker sync.Mutex MessagesHistoryLocker sync.Mutex
MessagesHistory []api.ChatMessage MessagesHistory []api.ChatMessage
CapabilitiesCacheLocker sync.Mutex
CapabilitiesCache map[streamcontrol.PlatformName]map[streamcontrol.Capability]struct{}
CurrentlyPlayingChatMessageSoundCount int32 CurrentlyPlayingChatMessageSoundCount int32
// TODO: do not store ctx in a struct: // TODO: do not store ctx in a struct:
@@ -42,8 +45,9 @@ func newChatUI(
panel *Panel, panel *Panel,
) (*chatUI, error) { ) (*chatUI, error) {
ui := &chatUI{ ui := &chatUI{
Panel: panel, Panel: panel,
ctx: ctx, CapabilitiesCache: make(map[streamcontrol.PlatformName]map[streamcontrol.Capability]struct{}),
ctx: ctx,
} }
if err := ui.init(ctx); err != nil { if err := ui.init(ctx); err != nil {
return nil, err return nil, err
@@ -224,6 +228,28 @@ func (ui *chatUI) listCreateItem() fyne.CanvasObject {
) )
} }
func (ui *chatUI) getPlatformCapabilities(
ctx context.Context,
platID streamcontrol.PlatformName,
) (_ret map[streamcontrol.Capability]struct{}, _err error) {
logger.Debugf(ctx, "getPlatformCapabilities(ctx, '%s')", platID)
defer func() { logger.Debugf(ctx, "/getPlatformCapabilities(ctx, '%s'): %#+v, %v", platID, _ret, _err) }()
ui.CapabilitiesCacheLocker.Lock()
defer ui.CapabilitiesCacheLocker.Unlock()
if m, ok := ui.CapabilitiesCache[platID]; ok {
return m, nil
}
info, err := ui.Panel.StreamD.GetBackendInfo(ctx, platID)
if err != nil {
return nil, fmt.Errorf("GetBackendInfo returned error: %w", err)
}
ui.CapabilitiesCache[platID] = info.Capabilities
return info.Capabilities, nil
}
func (ui *chatUI) listUpdateItem( func (ui *chatUI) listUpdateItem(
rowID int, rowID int,
obj fyne.CanvasObject, obj fyne.CanvasObject,
@@ -234,6 +260,12 @@ func (ui *chatUI) listUpdateItem(
entryID := len(ui.MessagesHistory) - 1 - rowID entryID := len(ui.MessagesHistory) - 1 - rowID
msg := ui.MessagesHistory[entryID] msg := ui.MessagesHistory[entryID]
platCaps, err := ui.getPlatformCapabilities(ctx, msg.Platform)
if err != nil {
ui.Panel.ReportError(fmt.Errorf("unable to get capabilities of platform '%s': %w", msg.Platform, err))
platCaps = map[streamcontrol.Capability]struct{}{}
}
containerPtr := obj.(*fyne.Container) containerPtr := obj.(*fyne.Container)
objs := containerPtr.Objects objs := containerPtr.Objects
label := objs[0].(*widget.Label) label := objs[0].(*widget.Label)
@@ -252,6 +284,11 @@ func (ui *chatUI) listUpdateItem(
) )
w.Show() w.Show()
} }
if _, ok := platCaps[streamcontrol.CapabilityBanUser]; !ok {
banUserButton.Disable()
} else {
banUserButton.Enable()
}
removeMsgButton := subContainer.Objects[1].(*widget.Button) removeMsgButton := subContainer.Objects[1].(*widget.Button)
removeMsgButton.OnTapped = func() { removeMsgButton.OnTapped = func() {
w := dialog.NewConfirm( w := dialog.NewConfirm(
@@ -267,6 +304,11 @@ func (ui *chatUI) listUpdateItem(
) )
w.Show() w.Show()
} }
if _, ok := platCaps[streamcontrol.CapabilityDeleteChatMessage]; !ok {
removeMsgButton.Disable()
} else {
removeMsgButton.Enable()
}
label.SetText(fmt.Sprintf( label.SetText(fmt.Sprintf(
"%s: %s: %s: %s", "%s: %s: %s: %s",
msg.CreatedAt.Format("15:04"), msg.CreatedAt.Format("15:04"),

View File

@@ -3222,14 +3222,14 @@ func (p *Panel) profileWindow(
} }
backendEnabled[backendID] = isEnabled backendEnabled[backendID] = isEnabled
data, err := p.StreamD.GetBackendData(ctx, backendID) info, err := p.StreamD.GetBackendInfo(ctx, backendID)
if err != nil { if err != nil {
w.Close() w.Close()
p.DisplayError(fmt.Errorf("unable to get data of backend '%s': %w", backendID, err)) p.DisplayError(fmt.Errorf("unable to get data of backend '%s': %w", backendID, err))
return nil return nil
} }
backendData[backendID] = data backendData[backendID] = info.Data
} }
_ = backendData[obs.ID].(api.BackendDataOBS) _ = backendData[obs.ID].(api.BackendDataOBS)
dataTwitch := backendData[twitch.ID].(api.BackendDataTwitch) dataTwitch := backendData[twitch.ID].(api.BackendDataTwitch)

View File

@@ -406,6 +406,7 @@ func (p *Panel) displayIncomingServers(
playButton := widget.NewButtonWithIcon("", theme.MediaPlayIcon(), func() { playButton := widget.NewButtonWithIcon("", theme.MediaPlayIcon(), func() {
p.DisplayError(fmt.Errorf("playback is not implemented, yet")) p.DisplayError(fmt.Errorf("playback is not implemented, yet"))
}) })
playButton.Hide() // TODO: unhide when it will be implemented
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() { deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
w := dialog.NewConfirm( w := dialog.NewConfirm(
fmt.Sprintf("Delete incoming server %s ?", stream.StreamID), fmt.Sprintf("Delete incoming server %s ?", stream.StreamID),