Add support for established chat user mode. #1587 (#1681)

* Add support for established user mode. #1587

* Tweak tests

* Tweak tests

* Update test

* Fix test.
This commit is contained in:
Gabe Kangas 2022-03-06 23:26:24 -08:00 committed by GitHub
parent 123d559ba4
commit e0a75d5d54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 245 additions and 83 deletions

View File

@ -1,6 +1,10 @@
package config
import "github.com/owncast/owncast/models"
import (
"time"
"github.com/owncast/owncast/models"
)
// Defaults will hold default configuration values.
type Defaults struct {
@ -27,6 +31,8 @@ type Defaults struct {
FederationUsername string
FederationGoLiveMessage string
ChatEstablishedUserModeTimeDuration time.Duration
}
// GetDefaults will return default configuration values.
@ -54,6 +60,8 @@ func GetDefaults() Defaults {
RTMPServerPort: 1935,
StreamKey: "abc123",
ChatEstablishedUserModeTimeDuration: time.Minute * 15,
StreamVariants: []models.StreamOutputVariant{
{
IsAudioPassthrough: true,

View File

@ -345,3 +345,24 @@ func SendChatAction(integration user.ExternalAPIUser, w http.ResponseWriter, r *
controllers.WriteSimpleResponse(w, true, "sent")
}
// SetEnableEstablishedChatUserMode sets the requirement for a chat user
// to be "established" for some time before taking part in chat.
func SetEnableEstablishedChatUserMode(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to update chat established user only mode")
return
}
if err := data.SetChatEstablishedUsersOnlyMode(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "chat established users only mode updated")
}

View File

@ -54,6 +54,7 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
ChatDisabled: data.GetChatDisabled(),
ChatJoinMessagesEnabled: data.GetChatJoinMessagesEnabled(),
SocketHostOverride: data.GetWebsocketOverrideHost(),
ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(),
VideoSettings: videoSettings{
VideoQualityVariants: videoQualityVariants,
LatencyLevel: data.GetStreamLatencyLevel().Level,
@ -98,6 +99,7 @@ type serverConfigAdminResponse struct {
YP yp `json:"yp"`
ChatDisabled bool `json:"chatDisabled"`
ChatJoinMessagesEnabled bool `json:"chatJoinMessagesEnabled"`
ChatEstablishedUserMode bool `json:"chatEstablishedUserMode"`
ExternalActions []models.ExternalAction `json:"externalActions"`
SupportedCodecs []string `json:"supportedCodecs"`
VideoCodec string `json:"videoCodec"`

View File

@ -11,6 +11,7 @@ import (
"github.com/gorilla/websocket"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
@ -330,6 +331,16 @@ func SendActionToUser(userID string, text string) error {
}
func (s *Server) eventReceived(event chatClientEvent) {
c := event.client
u := c.User
// If established chat user only mode is enabled and the user is not old
// enough then reject this event and send them an informative message.
if u != nil && data.GetChatEstbalishedUsersOnlyMode() && time.Since(event.client.User.CreatedAt) < config.GetDefaults().ChatEstablishedUserModeTimeDuration && !u.IsModerator() {
s.sendActionToClient(c, "You have not been an established chat participant long enough to take part in chat. Please enjoy the stream and try again later.")
return
}
var typecheck map[string]interface{}
if err := json.Unmarshal(event.data, &typecheck); err != nil {
log.Debugln(err)

View File

@ -14,47 +14,48 @@ import (
)
const (
extraContentKey = "extra_page_content"
streamTitleKey = "stream_title"
streamKeyKey = "stream_key"
logoPathKey = "logo_path"
serverSummaryKey = "server_summary"
serverWelcomeMessageKey = "server_welcome_message"
serverNameKey = "server_name"
serverURLKey = "server_url"
httpPortNumberKey = "http_port_number"
httpListenAddressKey = "http_listen_address"
websocketHostOverrideKey = "websocket_host_override"
rtmpPortNumberKey = "rtmp_port_number"
serverMetadataTagsKey = "server_metadata_tags"
directoryEnabledKey = "directory_enabled"
directoryRegistrationKeyKey = "directory_registration_key"
socialHandlesKey = "social_handles"
peakViewersSessionKey = "peak_viewers_session"
peakViewersOverallKey = "peak_viewers_overall"
lastDisconnectTimeKey = "last_disconnect_time"
ffmpegPathKey = "ffmpeg_path"
nsfwKey = "nsfw"
s3StorageEnabledKey = "s3_storage_enabled"
s3StorageConfigKey = "s3_storage_config"
videoLatencyLevel = "video_latency_level"
videoStreamOutputVariantsKey = "video_stream_output_variants"
chatDisabledKey = "chat_disabled"
externalActionsKey = "external_actions"
customStylesKey = "custom_styles"
videoCodecKey = "video_codec"
blockedUsernamesKey = "blocked_usernames"
publicKeyKey = "public_key"
privateKeyKey = "private_key"
serverInitDateKey = "server_init_date"
federationEnabledKey = "federation_enabled"
federationUsernameKey = "federation_username"
federationPrivateKey = "federation_private"
federationGoLiveMessageKey = "federation_go_live_message"
federationShowEngagementKey = "federation_show_engagement"
federationBlockedDomainsKey = "federation_blocked_domains"
suggestedUsernamesKey = "suggested_usernames"
chatJoinMessagesEnabledKey = "chat_join_messages_enabled"
extraContentKey = "extra_page_content"
streamTitleKey = "stream_title"
streamKeyKey = "stream_key"
logoPathKey = "logo_path"
serverSummaryKey = "server_summary"
serverWelcomeMessageKey = "server_welcome_message"
serverNameKey = "server_name"
serverURLKey = "server_url"
httpPortNumberKey = "http_port_number"
httpListenAddressKey = "http_listen_address"
websocketHostOverrideKey = "websocket_host_override"
rtmpPortNumberKey = "rtmp_port_number"
serverMetadataTagsKey = "server_metadata_tags"
directoryEnabledKey = "directory_enabled"
directoryRegistrationKeyKey = "directory_registration_key"
socialHandlesKey = "social_handles"
peakViewersSessionKey = "peak_viewers_session"
peakViewersOverallKey = "peak_viewers_overall"
lastDisconnectTimeKey = "last_disconnect_time"
ffmpegPathKey = "ffmpeg_path"
nsfwKey = "nsfw"
s3StorageEnabledKey = "s3_storage_enabled"
s3StorageConfigKey = "s3_storage_config"
videoLatencyLevel = "video_latency_level"
videoStreamOutputVariantsKey = "video_stream_output_variants"
chatDisabledKey = "chat_disabled"
externalActionsKey = "external_actions"
customStylesKey = "custom_styles"
videoCodecKey = "video_codec"
blockedUsernamesKey = "blocked_usernames"
publicKeyKey = "public_key"
privateKeyKey = "private_key"
serverInitDateKey = "server_init_date"
federationEnabledKey = "federation_enabled"
federationUsernameKey = "federation_username"
federationPrivateKey = "federation_private"
federationGoLiveMessageKey = "federation_go_live_message"
federationShowEngagementKey = "federation_show_engagement"
federationBlockedDomainsKey = "federation_blocked_domains"
suggestedUsernamesKey = "suggested_usernames"
chatJoinMessagesEnabledKey = "chat_join_messages_enabled"
chatEstablishedUsersOnlyModeKey = "chat_established_users_only_mode"
)
// GetExtraPageBodyContent will return the user-supplied body content.
@ -501,6 +502,21 @@ func GetChatDisabled() bool {
return false
}
// SetChatEstablishedUsersOnlyMode sets the state of established user only mode.
func SetChatEstablishedUsersOnlyMode(enabled bool) error {
return _datastore.SetBool(chatEstablishedUsersOnlyModeKey, enabled)
}
// GetChatEstbalishedUsersOnlyMode returns the state of established user only mode.
func GetChatEstbalishedUsersOnlyMode() bool {
enabled, err := _datastore.GetBool(chatEstablishedUsersOnlyModeKey)
if err == nil {
return enabled
}
return false
}
// GetExternalActions will return the registered external actions.
func GetExternalActions() []models.ExternalAction {
configEntry, err := _datastore.Get(externalActionsKey)

View File

@ -177,6 +177,9 @@ func Start() error {
// Disable chat user join messages
http.HandleFunc("/api/admin/config/chat/joinmessagesenabled", middleware.RequireAdminAuth(admin.SetChatJoinMessagesEnabled))
// Enable/disable chat established user mode
http.HandleFunc("/api/admin/config/chat/establishedusermode", middleware.RequireAdminAuth(admin.SetEnableEstablishedChatUserMode))
// Set chat usernames that are not allowed
http.HandleFunc("/api/admin/config/chat/forbiddenusernames", middleware.RequireAdminAuth(admin.SetForbiddenUsernameList))

View File

@ -27,10 +27,12 @@ test('can fetch chat messages', async (done) => {
.auth('admin', 'abc123')
.expect(200);
const expectedBody = `${testMessage.body}`;
const message = res.body.filter(function (msg) {
return msg.body === expectedBody;
})[0];
const message = res.body.filter((m) => m.body === testMessage.body)[0];
if (!message) {
throw new Error('Message not found');
}
const expectedBody = testMessage.body;
expect(message.body).toBe(expectedBody);
expect(message.user.displayName).toBe(userDisplayName);
@ -52,7 +54,7 @@ test('can derive display name from user header', async (done) => {
test('can overwrite user header derived display name with body', async (done) => {
const res = await request
.post('/api/chat/register')
.send({displayName: 'TestUserChat'})
.send({ displayName: 'TestUserChat' })
.set('X-Forwarded-User', 'test-user')
.expect(200);

View File

@ -5,13 +5,19 @@ const WebSocket = require('ws');
const registerChat = require('./lib/chat').registerChat;
const sendChatMessage = require('./lib/chat').sendChatMessage;
const listenForEvent = require('./lib/chat').listenForEvent;
const testVisibilityMessage = {
body: 'message ' + Math.floor(Math.random() * 100),
type: 'CHAT',
};
var messageId;
const establishedUserFailedChatMessage = {
body: 'this message should fail to send ' + Math.floor(Math.random() * 100),
type: 'CHAT',
};
test('can send a chat message', async (done) => {
const registration = await registerChat();
const accessToken = registration.accessToken;
@ -30,14 +36,14 @@ test('verify we can make API call to mark message as hidden', async (done) => {
);
// Verify the visibility change comes through the websocket
ws.on('message', function incoming(message) {
ws.on('message', async function incoming(message) {
const messages = message.split('\n');
messages.forEach(function (message) {
messages.forEach(async function (message) {
const event = JSON.parse(message);
if (event.type === 'VISIBILITY-UPDATE') {
done();
ws.close();
done();
}
});
});
@ -48,7 +54,7 @@ test('verify we can make API call to mark message as hidden', async (done) => {
.expect(200);
const message = res.body[0];
const messageId = message.id;
messageId = message.id;
await request
.post('/api/admin/chat/updatemessagevisibility')
.auth('admin', 'abc123')
@ -57,15 +63,56 @@ test('verify we can make API call to mark message as hidden', async (done) => {
});
test('verify message has become hidden', async (done) => {
await new Promise((r) => setTimeout(r, 2000));
const res = await request
.get('/api/admin/chat/messages')
.expect(200)
.auth('admin', 'abc123');
const message = res.body.filter((obj) => {
return obj.body === `${testVisibilityMessage.body}`;
return obj.id === messageId;
});
expect(message.length).toBe(1);
// expect(message[0].hiddenAt).toBeTruthy();
done();
});
test('can enable established chat user mode', async (done) => {
await request
.post('/api/admin/config/chat/establishedusermode')
.auth('admin', 'abc123')
.send({ value: true })
.expect(200);
done();
});
test('can send a message after established user mode is enabled', async (done) => {
const registration = await registerChat();
const accessToken = registration.accessToken;
sendChatMessage(establishedUserFailedChatMessage, accessToken, done);
});
test('verify rejected message is not in the chat feed', async (done) => {
const res = await request
.get('/api/admin/chat/messages')
.expect(200)
.auth('admin', 'abc123');
const message = res.body.filter((obj) => {
return obj.body === establishedUserFailedChatMessage.body;
});
expect(message.length).toBe(0);
done();
});
test('can disable established chat user mode', async (done) => {
await request
.post('/api/admin/config/chat/establishedusermode')
.auth('admin', 'abc123')
.send({ value: false })
.expect(200);
done();
});

View File

@ -11,16 +11,11 @@ async function registerChat() {
}
}
function sendChatMessage(message, accessToken, done) {
const ws = new WebSocket(
`ws://localhost:8080/ws?accessToken=${accessToken}`,
{
origin: 'http://localhost:8080',
}
);
async function sendChatMessage(message, accessToken, done) {
const ws = new WebSocket(`ws://localhost:8080/ws?accessToken=${accessToken}`);
function onOpen() {
ws.send(JSON.stringify(message), function () {
async function onOpen() {
ws.send(JSON.stringify(message), async function () {
ws.close();
done();
});
@ -30,12 +25,7 @@ function sendChatMessage(message, accessToken, done) {
}
async function listenForEvent(name, accessToken, done) {
const ws = new WebSocket(
`ws://localhost:8080/ws?accessToken=${accessToken}`,
{
origin: 'http://localhost:8080',
}
);
const ws = new WebSocket(`ws://localhost:8080/ws?accessToken=${accessToken}`);
ws.on('message', function incoming(message) {
const messages = message.split('\n');

View File

@ -600,6 +600,15 @@
"node": ">= 10.14.2"
}
},
"node_modules/@jest/core/node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/@jest/core/node_modules/strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
@ -984,15 +993,6 @@
"node": ">=8"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -3373,6 +3373,15 @@
"node": ">= 10.14.2"
}
},
"node_modules/jest-runtime/node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/jest-runtime/node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@ -3638,6 +3647,15 @@
"node": ">= 10.13.0"
}
},
"node_modules/jest/node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/jest/node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@ -4613,6 +4631,15 @@
"node": ">= 10"
}
},
"node_modules/pretty-format/node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/prompts": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz",
@ -5634,6 +5661,15 @@
"node": ">=10"
}
},
"node_modules/string-length/node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/string-length/node_modules/strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
@ -6791,6 +6827,12 @@
"strip-ansi": "^6.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
@ -7132,12 +7174,6 @@
}
}
},
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -8688,6 +8724,12 @@
"jest-cli": "^26.6.3"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@ -9211,6 +9253,12 @@
"yargs": "^15.4.1"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@ -10062,6 +10110,14 @@
"ansi-regex": "^5.0.0",
"ansi-styles": "^4.0.0",
"react-is": "^17.0.1"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
}
}
},
"prompts": {
@ -10907,6 +10963,12 @@
"strip-ansi": "^6.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",