parent
6e0e33dedb
commit
78c27ddbdd
@ -235,6 +235,7 @@ func SendIntegrationChatMessage(integration user.ExternalAPIUser, w http.Respons
|
|||||||
DisplayName: name,
|
DisplayName: name,
|
||||||
DisplayColor: integration.DisplayColor,
|
DisplayColor: integration.DisplayColor,
|
||||||
CreatedAt: integration.CreatedAt,
|
CreatedAt: integration.CreatedAt,
|
||||||
|
IsBot: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := chat.Broadcast(&event); err != nil {
|
if err := chat.Broadcast(&event); err != nil {
|
||||||
|
@ -99,6 +99,9 @@ func makeUserMessageEventFromRowData(row rowData) events.UserMessageEvent {
|
|||||||
createdAt = *row.userCreatedAt
|
createdAt = *row.userCreatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isBot := (row.userType != nil && *row.userType == "API")
|
||||||
|
scopeSlice := strings.Split(scopes, ",")
|
||||||
|
|
||||||
u := user.User{
|
u := user.User{
|
||||||
ID: *row.userID,
|
ID: *row.userID,
|
||||||
AccessToken: "",
|
AccessToken: "",
|
||||||
@ -108,7 +111,8 @@ func makeUserMessageEventFromRowData(row rowData) events.UserMessageEvent {
|
|||||||
DisabledAt: row.userDisabledAt,
|
DisabledAt: row.userDisabledAt,
|
||||||
NameChangedAt: row.userNameChangedAt,
|
NameChangedAt: row.userNameChangedAt,
|
||||||
PreviousNames: previousUsernames,
|
PreviousNames: previousUsernames,
|
||||||
Scopes: strings.Split(scopes, ","),
|
Scopes: scopeSlice,
|
||||||
|
IsBot: isBot,
|
||||||
}
|
}
|
||||||
|
|
||||||
message := events.UserMessageEvent{
|
message := events.UserMessageEvent{
|
||||||
@ -197,6 +201,7 @@ type rowData struct {
|
|||||||
previousUsernames *string
|
previousUsernames *string
|
||||||
userNameChangedAt *time.Time
|
userNameChangedAt *time.Time
|
||||||
userScopes *string
|
userScopes *string
|
||||||
|
userType *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func getChat(query string) []interface{} {
|
func getChat(query string) []interface{} {
|
||||||
@ -230,6 +235,7 @@ func getChat(query string) []interface{} {
|
|||||||
&row.previousUsernames,
|
&row.previousUsernames,
|
||||||
&row.userNameChangedAt,
|
&row.userNameChangedAt,
|
||||||
&row.userScopes,
|
&row.userScopes,
|
||||||
|
&row.userType,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Errorln("There is a problem converting query to chat objects. Please report this:", query)
|
log.Errorln("There is a problem converting query to chat objects. Please report this:", query)
|
||||||
break
|
break
|
||||||
@ -267,7 +273,7 @@ func GetChatModerationHistory() []interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all messages regardless of visibility
|
// Get all messages regardless of visibility
|
||||||
query := "SELECT messages.id, user_id, body, title, subtitle, image, link, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, scopes FROM messages INNER JOIN users ON messages.user_id = users.id ORDER BY timestamp DESC"
|
query := "SELECT messages.id, user_id, body, title, subtitle, image, link, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, scopes, users.type FROM messages INNER JOIN users ON messages.user_id = users.id ORDER BY timestamp DESC"
|
||||||
result := getChat(query)
|
result := getChat(query)
|
||||||
|
|
||||||
_historyCache = &result
|
_historyCache = &result
|
||||||
@ -278,7 +284,7 @@ func GetChatModerationHistory() []interface{} {
|
|||||||
// GetChatHistory will return all the chat messages suitable for returning as user-facing chat history.
|
// GetChatHistory will return all the chat messages suitable for returning as user-facing chat history.
|
||||||
func GetChatHistory() []interface{} {
|
func GetChatHistory() []interface{} {
|
||||||
// Get all visible messages
|
// Get all visible messages
|
||||||
query := fmt.Sprintf("SELECT messages.id,messages.user_id, messages.body, messages.title, messages.subtitle, messages.image, messages.link, messages.eventType, messages.hidden_at, messages.timestamp, users.display_name, users.display_color, users.created_at, users.disabled_at, users.previous_names, users.namechanged_at, users.scopes FROM messages LEFT JOIN users ON messages.user_id = users.id WHERE hidden_at IS NULL AND disabled_at IS NULL ORDER BY timestamp DESC LIMIT %d", maxBacklogNumber)
|
query := fmt.Sprintf("SELECT messages.id,messages.user_id, messages.body, messages.title, messages.subtitle, messages.image, messages.link, messages.eventType, messages.hidden_at, messages.timestamp, users.display_name, users.display_color, users.created_at, users.disabled_at, users.previous_names, users.namechanged_at, users.scopes, users.type FROM messages LEFT JOIN users ON messages.user_id = users.id WHERE hidden_at IS NULL AND disabled_at IS NULL ORDER BY timestamp DESC LIMIT %d", maxBacklogNumber)
|
||||||
m := getChat(query)
|
m := getChat(query)
|
||||||
|
|
||||||
// Invert order of messages
|
// Invert order of messages
|
||||||
@ -298,7 +304,7 @@ func SetMessageVisibilityForUserID(userID string, visible bool) error {
|
|||||||
|
|
||||||
// Get a list of IDs to send to the connected clients to hide
|
// Get a list of IDs to send to the connected clients to hide
|
||||||
ids := make([]string, 0)
|
ids := make([]string, 0)
|
||||||
query := fmt.Sprintf("SELECT messages.id, user_id, body, title, subtitle, image, link, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, scopes FROM messages INNER JOIN users ON messages.user_id = users.id WHERE user_id IS '%s'", userID)
|
query := fmt.Sprintf("SELECT messages.id, user_id, body, title, subtitle, image, link, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, scopes, type FROM messages INNER JOIN users ON messages.user_id = users.id WHERE user_id IS '%s'", userID)
|
||||||
messages := getChat(query)
|
messages := getChat(query)
|
||||||
|
|
||||||
if len(messages) == 0 {
|
if len(messages) == 0 {
|
||||||
|
@ -22,6 +22,7 @@ type ExternalAPIUser struct {
|
|||||||
Scopes []string `json:"scopes"`
|
Scopes []string `json:"scopes"`
|
||||||
Type string `json:"type,omitempty"` // Should be API
|
Type string `json:"type,omitempty"` // Should be API
|
||||||
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
||||||
|
IsBot bool `json:"isBot"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -240,6 +241,7 @@ func makeExternalAPIUsersFromRows(rows *sql.Rows) ([]ExternalAPIUser, error) {
|
|||||||
CreatedAt: createdAt,
|
CreatedAt: createdAt,
|
||||||
Scopes: strings.Split(scopes, ","),
|
Scopes: strings.Split(scopes, ","),
|
||||||
LastUsedAt: lastUsedAt,
|
LastUsedAt: lastUsedAt,
|
||||||
|
IsBot: true,
|
||||||
}
|
}
|
||||||
integrations = append(integrations, integration)
|
integrations = append(integrations, integration)
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,10 @@ import (
|
|||||||
|
|
||||||
var _datastore *data.Datastore
|
var _datastore *data.Datastore
|
||||||
|
|
||||||
const moderatorScopeKey = "MODERATOR"
|
const (
|
||||||
const minSuggestedUsernamePoolLength = 10
|
moderatorScopeKey = "MODERATOR"
|
||||||
|
minSuggestedUsernamePoolLength = 10
|
||||||
|
)
|
||||||
|
|
||||||
// User represents a single chat user.
|
// User represents a single chat user.
|
||||||
type User struct {
|
type User struct {
|
||||||
@ -29,7 +31,8 @@ type User struct {
|
|||||||
DisabledAt *time.Time `json:"disabledAt,omitempty"`
|
DisabledAt *time.Time `json:"disabledAt,omitempty"`
|
||||||
PreviousNames []string `json:"previousNames"`
|
PreviousNames []string `json:"previousNames"`
|
||||||
NameChangedAt *time.Time `json:"nameChangedAt,omitempty"`
|
NameChangedAt *time.Time `json:"nameChangedAt,omitempty"`
|
||||||
Scopes []string `json:"scopes"`
|
Scopes []string `json:"scopes,omitempty"`
|
||||||
|
IsBot bool `json:"isBot"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEnabled will return if this single user is enabled.
|
// IsEnabled will return if this single user is enabled.
|
||||||
|
@ -8,171 +8,170 @@ const webhook = 'https://super.duper.cool.thing.biz/owncast';
|
|||||||
const events = ['CHAT'];
|
const events = ['CHAT'];
|
||||||
|
|
||||||
test('create webhook', async (done) => {
|
test('create webhook', async (done) => {
|
||||||
const res = await sendIntegrationsChangePayload('webhooks/create', {
|
const res = await sendIntegrationsChangePayload('webhooks/create', {
|
||||||
url: webhook,
|
url: webhook,
|
||||||
events: events,
|
events: events,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.body.url).toBe(webhook);
|
expect(res.body.url).toBe(webhook);
|
||||||
expect(res.body.timestamp).toBeTruthy();
|
expect(res.body.timestamp).toBeTruthy();
|
||||||
expect(res.body.events).toStrictEqual(events);
|
expect(res.body.events).toStrictEqual(events);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('check webhooks', (done) => {
|
test('check webhooks', (done) => {
|
||||||
request
|
request
|
||||||
.get('/api/admin/webhooks')
|
.get('/api/admin/webhooks')
|
||||||
.auth('admin', 'abc123')
|
.auth('admin', 'abc123')
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
expect(res.body).toHaveLength(1);
|
expect(res.body).toHaveLength(1);
|
||||||
expect(res.body[0].url).toBe(webhook);
|
expect(res.body[0].url).toBe(webhook);
|
||||||
expect(res.body[0].events).toStrictEqual(events);
|
expect(res.body[0].events).toStrictEqual(events);
|
||||||
webhookID = res.body[0].id;
|
webhookID = res.body[0].id;
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('delete webhook', async (done) => {
|
test('delete webhook', async (done) => {
|
||||||
const res = await sendIntegrationsChangePayload('webhooks/delete', {
|
const res = await sendIntegrationsChangePayload('webhooks/delete', {
|
||||||
id: webhookID,
|
id: webhookID,
|
||||||
});
|
});
|
||||||
expect(res.body.success).toBe(true);
|
expect(res.body.success).toBe(true);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('check that webhook was deleted', (done) => {
|
test('check that webhook was deleted', (done) => {
|
||||||
request
|
request
|
||||||
.get('/api/admin/webhooks')
|
.get('/api/admin/webhooks')
|
||||||
.auth('admin', 'abc123')
|
.auth('admin', 'abc123')
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
expect(res.body).toHaveLength(0);
|
expect(res.body).toHaveLength(0);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('create access token', async (done) => {
|
test('create access token', async (done) => {
|
||||||
const name = 'Automated integration test';
|
const name = 'Automated integration test';
|
||||||
const scopes = [
|
const scopes = [
|
||||||
'CAN_SEND_SYSTEM_MESSAGES',
|
'CAN_SEND_SYSTEM_MESSAGES',
|
||||||
'CAN_SEND_MESSAGES',
|
'CAN_SEND_MESSAGES',
|
||||||
'HAS_ADMIN_ACCESS',
|
'HAS_ADMIN_ACCESS',
|
||||||
];
|
];
|
||||||
const res = await sendIntegrationsChangePayload('accesstokens/create', {
|
const res = await sendIntegrationsChangePayload('accesstokens/create', {
|
||||||
name: name,
|
name: name,
|
||||||
scopes: scopes,
|
scopes: scopes,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.body.accessToken).toBeTruthy();
|
expect(res.body.accessToken).toBeTruthy();
|
||||||
expect(res.body.createdAt).toBeTruthy();
|
expect(res.body.createdAt).toBeTruthy();
|
||||||
expect(res.body.displayName).toBe(name);
|
expect(res.body.displayName).toBe(name);
|
||||||
expect(res.body.scopes).toStrictEqual(scopes);
|
expect(res.body.scopes).toStrictEqual(scopes);
|
||||||
accessToken = res.body.accessToken;
|
accessToken = res.body.accessToken;
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('check access tokens', async (done) => {
|
test('check access tokens', async (done) => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.get('/api/admin/accesstokens')
|
.get('/api/admin/accesstokens')
|
||||||
.auth('admin', 'abc123')
|
.auth('admin', 'abc123')
|
||||||
.expect(200);
|
.expect(200);
|
||||||
const tokenCheck = res.body.filter(
|
const tokenCheck = res.body.filter(
|
||||||
(token) => token.accessToken === accessToken
|
(token) => token.accessToken === accessToken
|
||||||
);
|
);
|
||||||
expect(tokenCheck).toHaveLength(1);
|
expect(tokenCheck).toHaveLength(1);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('send a system message using access token', async (done) => {
|
test('send a system message using access token', async (done) => {
|
||||||
const payload = {
|
const payload = {
|
||||||
body: 'This is a test system message from the automated integration test',
|
body: 'This is a test system message from the automated integration test',
|
||||||
};
|
};
|
||||||
const res = await request
|
const res = await request
|
||||||
.post('/api/integrations/chat/system')
|
.post('/api/integrations/chat/system')
|
||||||
.set('Authorization', 'Bearer ' + accessToken)
|
.set('Authorization', 'Bearer ' + accessToken)
|
||||||
.send(payload)
|
.send(payload)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('send an external integration message using access token', async (done) => {
|
test('send an external integration message using access token', async (done) => {
|
||||||
const payload = {
|
const payload = {
|
||||||
body: 'This is a test external message from the automated integration test',
|
body: 'This is a test external message from the automated integration test',
|
||||||
};
|
};
|
||||||
const res = await request
|
const res = await request
|
||||||
.post('/api/integrations/chat/send')
|
.post('/api/integrations/chat/send')
|
||||||
.set('Authorization', 'Bearer ' + accessToken)
|
.set('Authorization', 'Bearer ' + accessToken)
|
||||||
.send(payload)
|
.send(payload)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('send an external integration action using access token', async (done) => {
|
test('send an external integration action using access token', async (done) => {
|
||||||
const payload = {
|
const payload = {
|
||||||
body: 'This is a test external action from the automated integration test',
|
body: 'This is a test external action from the automated integration test',
|
||||||
};
|
};
|
||||||
const res = await request
|
const res = await request
|
||||||
.post('/api/integrations/chat/action')
|
.post('/api/integrations/chat/action')
|
||||||
.set('Authorization', 'Bearer ' + accessToken)
|
.set('Authorization', 'Bearer ' + accessToken)
|
||||||
.send(payload)
|
.send(payload)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test fetch chat history using access token', async (done) => {
|
test('test fetch chat history using access token', async (done) => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.get('/api/integrations/chat')
|
.get('/api/integrations/chat')
|
||||||
.set('Authorization', 'Bearer ' + accessToken)
|
.set('Authorization', 'Bearer ' + accessToken)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('test fetch chat history failure using invalid access token', async (done) => {
|
test('test fetch chat history failure using invalid access token', async (done) => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.get('/api/integrations/chat')
|
.get('/api/integrations/chat')
|
||||||
.set('Authorization', 'Bearer ' + 'invalidToken')
|
.set('Authorization', 'Bearer ' + 'invalidToken')
|
||||||
.expect(401);
|
.expect(401);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test fetch chat history OPTIONS request', async (done) => {
|
test('test fetch chat history OPTIONS request', async (done) => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.options('/api/integrations/chat')
|
.options('/api/integrations/chat')
|
||||||
.set('Authorization', 'Bearer ' + accessToken)
|
.set('Authorization', 'Bearer ' + accessToken)
|
||||||
.expect(204);
|
.expect(204);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('delete access token', async (done) => {
|
test('delete access token', async (done) => {
|
||||||
const res = await sendIntegrationsChangePayload('accesstokens/delete', {
|
const res = await sendIntegrationsChangePayload('accesstokens/delete', {
|
||||||
token: accessToken,
|
token: accessToken,
|
||||||
});
|
});
|
||||||
expect(res.body.success).toBe(true);
|
expect(res.body.success).toBe(true);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('check token delete was successful', async (done) => {
|
test('check token delete was successful', async (done) => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.get('/api/admin/accesstokens')
|
.get('/api/admin/accesstokens')
|
||||||
.auth('admin', 'abc123')
|
.auth('admin', 'abc123')
|
||||||
.expect(200);
|
.expect(200);
|
||||||
const tokenCheck = res.body.filter(
|
const tokenCheck = res.body.filter(
|
||||||
(token) => token.accessToken === accessToken
|
(token) => token.accessToken === accessToken
|
||||||
);
|
);
|
||||||
expect(tokenCheck).toHaveLength(0);
|
expect(tokenCheck).toHaveLength(0);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function sendIntegrationsChangePayload(endpoint, payload) {
|
async function sendIntegrationsChangePayload(endpoint, payload) {
|
||||||
const url = '/api/admin/' + endpoint;
|
const url = '/api/admin/' + endpoint;
|
||||||
const res = await request
|
const res = await request
|
||||||
.post(url)
|
.post(url)
|
||||||
.auth('admin', 'abc123')
|
.auth('admin', 'abc123')
|
||||||
.send(payload)
|
.send(payload)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
2
webroot/img/bot.svg
Normal file
2
webroot/img/bot.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.8 KiB |
@ -51,7 +51,7 @@ export default class ChatMessageView extends Component {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { displayName, displayColor, createdAt } = user;
|
const { displayName, displayColor, createdAt, isBot } = user;
|
||||||
const isAuthorModerator = checkIsModerator(message);
|
const isAuthorModerator = checkIsModerator(message);
|
||||||
|
|
||||||
const isMessageModeratable =
|
const isMessageModeratable =
|
||||||
@ -88,6 +88,15 @@ export default class ChatMessageView extends Component {
|
|||||||
/>`
|
/>`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const isBotFlair = isBot
|
||||||
|
? html`<img
|
||||||
|
title="Bot"
|
||||||
|
class="inline-block mr-1 w-4 h-4 relative"
|
||||||
|
style=${{ bottom: '1px' }}
|
||||||
|
src="/img/bot.svg"
|
||||||
|
/>`
|
||||||
|
: null;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
style=${backgroundStyle}
|
style=${backgroundStyle}
|
||||||
@ -100,7 +109,7 @@ export default class ChatMessageView extends Component {
|
|||||||
class="message-author font-bold"
|
class="message-author font-bold"
|
||||||
title=${userMetadata}
|
title=${userMetadata}
|
||||||
>
|
>
|
||||||
${messageAuthorFlair} ${displayName}
|
${isBotFlair} ${messageAuthorFlair} ${displayName}
|
||||||
</div>
|
</div>
|
||||||
${isMessageModeratable &&
|
${isMessageModeratable &&
|
||||||
html`<${ModeratorActions}
|
html`<${ModeratorActions}
|
||||||
|
@ -99,7 +99,7 @@ export default function Message(props) {
|
|||||||
`;
|
`;
|
||||||
return html`<${SystemMessage} contents=${contents} />`;
|
return html`<${SystemMessage} contents=${contents} />`;
|
||||||
} else if (type === SOCKET_MESSAGE_TYPES.USER_JOINED) {
|
} else if (type === SOCKET_MESSAGE_TYPES.USER_JOINED) {
|
||||||
const { displayName } = user;
|
const { displayName, isBot } = user;
|
||||||
const isAuthorModerator = checkIsModerator(message);
|
const isAuthorModerator = checkIsModerator(message);
|
||||||
const messageAuthorFlair = isAuthorModerator
|
const messageAuthorFlair = isAuthorModerator
|
||||||
? html`<img
|
? html`<img
|
||||||
@ -108,6 +108,7 @@ export default function Message(props) {
|
|||||||
src="/img/moderator-nobackground.svg"
|
src="/img/moderator-nobackground.svg"
|
||||||
/>`
|
/>`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const contents = html`<div>
|
const contents = html`<div>
|
||||||
<span class="font-bold">${messageAuthorFlair}${displayName}</span>
|
<span class="font-bold">${messageAuthorFlair}${displayName}</span>
|
||||||
${' '}joined the chat.
|
${' '}joined the chat.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user