Refactoring #3
@@ -730,7 +730,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if cimgui.CollapsingHeaderTreeNodeFlagsV("Debug Info", collapsingHeaderFlags) {
|
if cimgui.CollapsingHeaderTreeNodeFlagsV("Debug Info", cimgui.TreeNodeFlagsNone) {
|
||||||
drawDebugSection()
|
drawDebugSection()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,8 +72,9 @@ func (s *stabilityAudioSink) ConsumeAudio(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *AudioWorker) emit(ctx context.Context, status Status) {
|
func (w *AudioWorker) emit(ctx context.Context, config FeedConfig, status Status) {
|
||||||
status.Generation = generationFromContext(ctx)
|
status.Generation = generationFromContext(ctx)
|
||||||
|
status.Feed = config
|
||||||
if w.observer != nil {
|
if w.observer != nil {
|
||||||
w.observer(status)
|
w.observer(status)
|
||||||
}
|
}
|
||||||
@@ -100,7 +101,7 @@ func (w *AudioWorker) Run(
|
|||||||
if attemptNumber > 1 {
|
if attemptNumber > 1 {
|
||||||
state = StateReconnecting
|
state = StateReconnecting
|
||||||
}
|
}
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, config, Status{
|
||||||
Unit: UnitAudio,
|
Unit: UnitAudio,
|
||||||
State: state,
|
State: state,
|
||||||
Attempt: attemptNumber,
|
Attempt: attemptNumber,
|
||||||
@@ -109,7 +110,7 @@ func (w *AudioWorker) Run(
|
|||||||
attemptSink := &stabilityAudioSink{
|
attemptSink := &stabilityAudioSink{
|
||||||
sink: w.sink,
|
sink: w.sink,
|
||||||
onStable: func() {
|
onStable: func() {
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, config, Status{
|
||||||
Unit: UnitAudio,
|
Unit: UnitAudio,
|
||||||
State: StatePlaying,
|
State: StatePlaying,
|
||||||
Attempt: attemptNumber,
|
Attempt: attemptNumber,
|
||||||
@@ -135,7 +136,7 @@ func (w *AudioWorker) Run(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, config, Status{
|
||||||
Unit: UnitAudio,
|
Unit: UnitAudio,
|
||||||
State: StateReconnecting,
|
State: StateReconnecting,
|
||||||
Attempt: attemptNumber + 1,
|
Attempt: attemptNumber + 1,
|
||||||
@@ -155,11 +156,11 @@ func (w *AudioWorker) Run(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, config, Status{
|
||||||
Unit: UnitAudio,
|
Unit: UnitAudio,
|
||||||
State: StateStopping,
|
State: StateStopping,
|
||||||
})
|
})
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, config, Status{
|
||||||
Unit: UnitAudio,
|
Unit: UnitAudio,
|
||||||
State: StateIdle,
|
State: StateIdle,
|
||||||
})
|
})
|
||||||
@@ -167,7 +168,7 @@ func (w *AudioWorker) Run(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, config, Status{
|
||||||
Unit: UnitAudio,
|
Unit: UnitAudio,
|
||||||
State: StateFailed,
|
State: StateFailed,
|
||||||
Attempt: attemptNumber,
|
Attempt: attemptNumber,
|
||||||
@@ -177,7 +178,7 @@ func (w *AudioWorker) Run(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, config, Status{
|
||||||
Unit: UnitAudio,
|
Unit: UnitAudio,
|
||||||
State: StateIdle,
|
State: StateIdle,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -127,10 +127,8 @@ func TestAudioWorkerStatusesInheritGeneration(t *testing.T) {
|
|||||||
func(error) bool { return true },
|
func(error) bool { return true },
|
||||||
func(status Status) { statuses = append(statuses, status) },
|
func(status Status) { statuses = append(statuses, status) },
|
||||||
)
|
)
|
||||||
_ = worker.Run(
|
config := FeedConfig{Domain: "/audio", UUID: "audio", Active: true}
|
||||||
withGeneration(context.Background(), 8),
|
_ = worker.Run(withGeneration(context.Background(), 8), config)
|
||||||
FeedConfig{Domain: "/audio", UUID: "audio", Active: true},
|
|
||||||
)
|
|
||||||
if len(statuses) == 0 {
|
if len(statuses) == 0 {
|
||||||
t.Fatal("no statuses emitted")
|
t.Fatal("no statuses emitted")
|
||||||
}
|
}
|
||||||
@@ -138,6 +136,9 @@ func TestAudioWorkerStatusesInheritGeneration(t *testing.T) {
|
|||||||
if status.Generation != 8 {
|
if status.Generation != 8 {
|
||||||
t.Fatalf("status generation = %d, want 8: %+v", status.Generation, status)
|
t.Fatalf("status generation = %d, want 8: %+v", status.Generation, status)
|
||||||
}
|
}
|
||||||
|
if status.Feed != config {
|
||||||
|
t.Fatalf("status feed = %#v, want %#v", status.Feed, config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ func IsSessionPlaying(
|
|||||||
statuses.Video,
|
statuses.Video,
|
||||||
statuses.HasVideo,
|
statuses.HasVideo,
|
||||||
session.Generation,
|
session.Generation,
|
||||||
|
session.Plan.Video,
|
||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -25,17 +26,17 @@ func IsSessionPlaying(
|
|||||||
statuses.Audio,
|
statuses.Audio,
|
||||||
statuses.HasAudio,
|
statuses.HasAudio,
|
||||||
session.Generation,
|
session.Generation,
|
||||||
|
session.Plan.Audio,
|
||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case TopologySynchronized:
|
case TopologySynchronized:
|
||||||
return statusIsPlaying(
|
return statuses.HasSync &&
|
||||||
statuses.Sync,
|
statuses.Sync.Generation == session.Generation &&
|
||||||
statuses.HasSync,
|
statuses.Sync.State == StatePlaying &&
|
||||||
session.Generation,
|
sameSyncSource(statuses.Sync.Pair, session.Plan.Sync)
|
||||||
)
|
|
||||||
|
|
||||||
case TopologyIdle:
|
case TopologyIdle:
|
||||||
return false
|
return false
|
||||||
@@ -45,8 +46,23 @@ func IsSessionPlaying(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusIsPlaying(status Status, present bool, generation uint64) bool {
|
func statusIsPlaying(
|
||||||
|
status Status,
|
||||||
|
present bool,
|
||||||
|
generation uint64,
|
||||||
|
feed FeedConfig,
|
||||||
|
) bool {
|
||||||
return present &&
|
return present &&
|
||||||
status.Generation == generation &&
|
status.Generation == generation &&
|
||||||
status.State == StatePlaying
|
status.State == StatePlaying &&
|
||||||
|
sameFeedSource(status.Feed, feed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sameFeedSource(a, b FeedConfig) bool {
|
||||||
|
return a.Domain == b.Domain && a.UUID == b.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func sameSyncSource(a, b SyncPairConfig) bool {
|
||||||
|
return sameFeedSource(a.Video, b.Video) &&
|
||||||
|
sameFeedSource(a.Audio, b.Audio)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ import "testing"
|
|||||||
func TestIsSessionPlaying(t *testing.T) {
|
func TestIsSessionPlaying(t *testing.T) {
|
||||||
const generation = 4
|
const generation = 4
|
||||||
playing := func(unit Unit) Status {
|
playing := func(unit Unit) Status {
|
||||||
return Status{Unit: unit, State: StatePlaying, Generation: generation}
|
status := Status{Unit: unit, State: StatePlaying, Generation: generation}
|
||||||
|
switch unit {
|
||||||
|
case UnitVideo:
|
||||||
|
status.Feed = FeedConfig{UUID: "video"}
|
||||||
|
case UnitAudio:
|
||||||
|
status.Feed = FeedConfig{UUID: "audio"}
|
||||||
|
}
|
||||||
|
return status
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -171,6 +178,72 @@ func TestIsSessionPlaying(t *testing.T) {
|
|||||||
HasSync: true,
|
HasSync: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "video status has wrong UUID",
|
||||||
|
session: SessionSnapshot{
|
||||||
|
Generation: generation,
|
||||||
|
Plan: SessionPlan{
|
||||||
|
Topology: TopologyIndependent,
|
||||||
|
Video: FeedConfig{Domain: "domain", UUID: "video", Active: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
statuses: PlaybackStatusSnapshot{
|
||||||
|
Generation: generation,
|
||||||
|
Video: Status{
|
||||||
|
Unit: UnitVideo,
|
||||||
|
State: StatePlaying,
|
||||||
|
Generation: generation,
|
||||||
|
Feed: FeedConfig{Domain: "domain", UUID: "other"},
|
||||||
|
},
|
||||||
|
HasVideo: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "audio status has wrong domain",
|
||||||
|
session: SessionSnapshot{
|
||||||
|
Generation: generation,
|
||||||
|
Plan: SessionPlan{
|
||||||
|
Topology: TopologyIndependent,
|
||||||
|
Audio: FeedConfig{Domain: "audio-domain", UUID: "audio", Active: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
statuses: PlaybackStatusSnapshot{
|
||||||
|
Generation: generation,
|
||||||
|
Audio: Status{
|
||||||
|
Unit: UnitAudio,
|
||||||
|
State: StatePlaying,
|
||||||
|
Generation: generation,
|
||||||
|
Feed: FeedConfig{Domain: "other-domain", UUID: "audio"},
|
||||||
|
},
|
||||||
|
HasAudio: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sync status has wrong audio source",
|
||||||
|
session: SessionSnapshot{
|
||||||
|
Generation: generation,
|
||||||
|
Plan: SessionPlan{
|
||||||
|
Topology: TopologySynchronized,
|
||||||
|
Sync: SyncPairConfig{
|
||||||
|
Video: FeedConfig{Domain: "domain", UUID: "video"},
|
||||||
|
Audio: FeedConfig{Domain: "domain", UUID: "audio"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
statuses: PlaybackStatusSnapshot{
|
||||||
|
Generation: generation,
|
||||||
|
Sync: Status{
|
||||||
|
Unit: UnitSync,
|
||||||
|
State: StatePlaying,
|
||||||
|
Generation: generation,
|
||||||
|
Pair: SyncPairConfig{
|
||||||
|
Video: FeedConfig{Domain: "domain", UUID: "video"},
|
||||||
|
Audio: FeedConfig{Domain: "domain", UUID: "other-audio"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HasSync: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "idle",
|
name: "idle",
|
||||||
session: SessionSnapshot{
|
session: SessionSnapshot{
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ type Status struct {
|
|||||||
Unit Unit
|
Unit Unit
|
||||||
State State
|
State State
|
||||||
Generation uint64
|
Generation uint64
|
||||||
|
|
||||||
|
Feed FeedConfig // Video/Audio worker
|
||||||
|
Pair SyncPairConfig // Sync worker
|
||||||
|
|
||||||
Attempt int
|
Attempt int
|
||||||
FailedAttempts int
|
FailedAttempts int
|
||||||
RetryIn time.Duration
|
RetryIn time.Duration
|
||||||
|
|||||||
@@ -59,8 +59,13 @@ func NewSyncWorker(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *SyncWorker) emit(ctx context.Context, status Status) {
|
func (w *SyncWorker) emit(
|
||||||
|
ctx context.Context,
|
||||||
|
pair SyncPairConfig,
|
||||||
|
status Status,
|
||||||
|
) {
|
||||||
status.Generation = generationFromContext(ctx)
|
status.Generation = generationFromContext(ctx)
|
||||||
|
status.Pair = pair
|
||||||
if w.observer != nil {
|
if w.observer != nil {
|
||||||
w.observer(status)
|
w.observer(status)
|
||||||
}
|
}
|
||||||
@@ -80,6 +85,7 @@ func (w *SyncWorker) Run(
|
|||||||
if !videoConfig.Active || !audioConfig.Active {
|
if !videoConfig.Active || !audioConfig.Active {
|
||||||
return ErrSyncFeedsInactive
|
return ErrSyncFeedsInactive
|
||||||
}
|
}
|
||||||
|
pair := SyncPairConfig{Video: videoConfig, Audio: audioConfig}
|
||||||
|
|
||||||
attemptNumber := 0
|
attemptNumber := 0
|
||||||
var latestRetry retryEvent
|
var latestRetry retryEvent
|
||||||
@@ -91,7 +97,7 @@ func (w *SyncWorker) Run(
|
|||||||
if attemptNumber > 1 {
|
if attemptNumber > 1 {
|
||||||
state = StateReconnecting
|
state = StateReconnecting
|
||||||
}
|
}
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, pair, Status{
|
||||||
Unit: UnitSync,
|
Unit: UnitSync,
|
||||||
State: state,
|
State: state,
|
||||||
Attempt: attemptNumber,
|
Attempt: attemptNumber,
|
||||||
@@ -100,7 +106,7 @@ func (w *SyncWorker) Run(
|
|||||||
attemptAudioSink := &stabilityAudioSink{
|
attemptAudioSink := &stabilityAudioSink{
|
||||||
sink: w.audioSink,
|
sink: w.audioSink,
|
||||||
onStable: func() {
|
onStable: func() {
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, pair, Status{
|
||||||
Unit: UnitSync,
|
Unit: UnitSync,
|
||||||
State: StatePlaying,
|
State: StatePlaying,
|
||||||
Attempt: attemptNumber,
|
Attempt: attemptNumber,
|
||||||
@@ -134,7 +140,7 @@ func (w *SyncWorker) Run(
|
|||||||
if !event.WillRetry {
|
if !event.WillRetry {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, pair, Status{
|
||||||
Unit: UnitSync,
|
Unit: UnitSync,
|
||||||
State: StateReconnecting,
|
State: StateReconnecting,
|
||||||
Attempt: attemptNumber + 1,
|
Attempt: attemptNumber + 1,
|
||||||
@@ -152,18 +158,18 @@ func (w *SyncWorker) Run(
|
|||||||
observeRetry,
|
observeRetry,
|
||||||
)
|
)
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, pair, Status{
|
||||||
Unit: UnitSync,
|
Unit: UnitSync,
|
||||||
State: StateStopping,
|
State: StateStopping,
|
||||||
})
|
})
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, pair, Status{
|
||||||
Unit: UnitSync,
|
Unit: UnitSync,
|
||||||
State: StateIdle,
|
State: StateIdle,
|
||||||
})
|
})
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, pair, Status{
|
||||||
Unit: UnitSync,
|
Unit: UnitSync,
|
||||||
State: StateFailed,
|
State: StateFailed,
|
||||||
Attempt: attemptNumber,
|
Attempt: attemptNumber,
|
||||||
@@ -172,7 +178,7 @@ func (w *SyncWorker) Run(
|
|||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, pair, Status{
|
||||||
Unit: UnitSync,
|
Unit: UnitSync,
|
||||||
State: StateIdle,
|
State: StateIdle,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -146,6 +146,10 @@ func TestSyncWorkerStatusesInheritGeneration(t *testing.T) {
|
|||||||
if status.Generation != 9 {
|
if status.Generation != 9 {
|
||||||
t.Fatalf("status generation = %d, want 9: %+v", status.Generation, status)
|
t.Fatalf("status generation = %d, want 9: %+v", status.Generation, status)
|
||||||
}
|
}
|
||||||
|
wantPair := SyncPairConfig{Video: video, Audio: audio}
|
||||||
|
if status.Pair != wantPair {
|
||||||
|
t.Fatalf("status pair = %#v, want %#v", status.Pair, wantPair)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,8 +72,9 @@ func (s *stabilityVideoSink) ConsumeVideo(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *VideoWorker) emit(ctx context.Context, status Status) {
|
func (w *VideoWorker) emit(ctx context.Context, config FeedConfig, status Status) {
|
||||||
status.Generation = generationFromContext(ctx)
|
status.Generation = generationFromContext(ctx)
|
||||||
|
status.Feed = config
|
||||||
if w.observer != nil {
|
if w.observer != nil {
|
||||||
w.observer(status)
|
w.observer(status)
|
||||||
}
|
}
|
||||||
@@ -100,7 +101,7 @@ func (w *VideoWorker) Run(
|
|||||||
if attemptNumber > 1 {
|
if attemptNumber > 1 {
|
||||||
state = StateReconnecting
|
state = StateReconnecting
|
||||||
}
|
}
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, config, Status{
|
||||||
Unit: UnitVideo,
|
Unit: UnitVideo,
|
||||||
State: state,
|
State: state,
|
||||||
Attempt: attemptNumber,
|
Attempt: attemptNumber,
|
||||||
@@ -109,7 +110,7 @@ func (w *VideoWorker) Run(
|
|||||||
attemptSink := &stabilityVideoSink{
|
attemptSink := &stabilityVideoSink{
|
||||||
sink: w.sink,
|
sink: w.sink,
|
||||||
onStable: func() {
|
onStable: func() {
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, config, Status{
|
||||||
Unit: UnitVideo,
|
Unit: UnitVideo,
|
||||||
State: StatePlaying,
|
State: StatePlaying,
|
||||||
Attempt: attemptNumber,
|
Attempt: attemptNumber,
|
||||||
@@ -135,7 +136,7 @@ func (w *VideoWorker) Run(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, config, Status{
|
||||||
Unit: UnitVideo,
|
Unit: UnitVideo,
|
||||||
State: StateReconnecting,
|
State: StateReconnecting,
|
||||||
Attempt: attemptNumber + 1,
|
Attempt: attemptNumber + 1,
|
||||||
@@ -155,11 +156,11 @@ func (w *VideoWorker) Run(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, config, Status{
|
||||||
Unit: UnitVideo,
|
Unit: UnitVideo,
|
||||||
State: StateStopping,
|
State: StateStopping,
|
||||||
})
|
})
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, config, Status{
|
||||||
Unit: UnitVideo,
|
Unit: UnitVideo,
|
||||||
State: StateIdle,
|
State: StateIdle,
|
||||||
})
|
})
|
||||||
@@ -167,7 +168,7 @@ func (w *VideoWorker) Run(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, config, Status{
|
||||||
Unit: UnitVideo,
|
Unit: UnitVideo,
|
||||||
State: StateFailed,
|
State: StateFailed,
|
||||||
Attempt: attemptNumber,
|
Attempt: attemptNumber,
|
||||||
@@ -177,7 +178,7 @@ func (w *VideoWorker) Run(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
w.emit(ctx, Status{
|
w.emit(ctx, config, Status{
|
||||||
Unit: UnitVideo,
|
Unit: UnitVideo,
|
||||||
State: StateIdle,
|
State: StateIdle,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -162,7 +162,8 @@ func TestVideoWorkerStatusesInheritGeneration(t *testing.T) {
|
|||||||
func(error) bool { return true },
|
func(error) bool { return true },
|
||||||
func(status Status) { statuses = append(statuses, status) },
|
func(status Status) { statuses = append(statuses, status) },
|
||||||
)
|
)
|
||||||
_ = worker.Run(withGeneration(context.Background(), 7), activeVideoConfig())
|
config := activeVideoConfig()
|
||||||
|
_ = worker.Run(withGeneration(context.Background(), 7), config)
|
||||||
if len(statuses) == 0 {
|
if len(statuses) == 0 {
|
||||||
t.Fatal("no statuses emitted")
|
t.Fatal("no statuses emitted")
|
||||||
}
|
}
|
||||||
@@ -170,6 +171,9 @@ func TestVideoWorkerStatusesInheritGeneration(t *testing.T) {
|
|||||||
if status.Generation != 7 {
|
if status.Generation != 7 {
|
||||||
t.Fatalf("status generation = %d, want 7: %+v", status.Generation, status)
|
t.Fatalf("status generation = %d, want 7: %+v", status.Generation, status)
|
||||||
}
|
}
|
||||||
|
if status.Feed != config {
|
||||||
|
t.Fatalf("status feed = %#v, want %#v", status.Feed, config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user