feat: `POST /chats/:roomId/users`

isekai-main
Julian Lam 3 years ago
parent 6eea6451d2
commit d62ee84637

@ -94,6 +94,44 @@ MessageObject:
type: boolean
cleanedContent:
type: string
RoomUserList:
type: object
properties:
users:
type: array
items:
type: object
properties:
uid:
type: number
description: A user identifier
username:
type: string
description: A friendly name for a given user account
picture:
nullable: true
type: string
status:
type: string
displayname:
type: string
description: This is either username or fullname depending on forum and user settings
icon:text:
type: string
description: A single-letter representation of a username. This is used in the
auto-generated icon given to users
without an avatar
icon:bgColor:
type: string
description: A six-character hexadecimal colour code assigned to the user. This
value is used in conjunction with
`icon:text` for the user's
auto-generated icon
example: "#f44336"
isOwner:
type: boolean
canKick:
type: boolean
RoomObjectFull:
# Messaging.loadRoom
allOf:

@ -22,40 +22,43 @@ get:
status:
$ref: ../../../components/schemas/Status.yaml#/Status
response:
type: object
properties:
users:
type: array
items:
type: object
properties:
uid:
type: number
description: A user identifier
username:
type: string
description: A friendly name for a given user account
picture:
nullable: true
type: string
status:
type: string
displayname:
type: string
description: This is either username or fullname depending on forum and user settings
icon:text:
type: string
description: A single-letter representation of a username. This is used in the
auto-generated icon given to users
without an avatar
icon:bgColor:
type: string
description: A six-character hexadecimal colour code assigned to the user. This
value is used in conjunction with
`icon:text` for the user's
auto-generated icon
example: "#f44336"
isOwner:
type: boolean
canKick:
type: boolean
$ref: ../../../components/schemas/Chats.yaml#/RoomUserList
post:
tags:
- chats
summary: add users to chat room
description: This operation invites users to a chat room
parameters:
- in: path
name: roomId
schema:
type: number
required: true
description: a valid chat room id
example: 1
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
uids:
type: array
description: A list of valid uids
example: [2]
items:
type: number
description: A valid uid
responses:
'200':
description: users successfully invited to chat room
content:
application/json:
schema:
type: object
properties:
status:
$ref: ../../../components/schemas/Status.yaml#/Status
response:
$ref: ../../../components/schemas/Chats.yaml#/RoomUserList

@ -244,18 +244,15 @@ define('forum/chats', [
require(['autocomplete', 'translator'], function (autocomplete, translator) {
autocomplete.user(searchInput, function (event, selected) {
errorEl.text('');
socket.emit('modules.chats.addUserToRoom', {
roomId: roomId,
username: selected.item.user.name,
}, function (err) {
if (err) {
translator.translate(err.message, function (translated) {
errorEl.text(translated);
});
}
Chats.refreshParticipantsList(roomId, modal);
api.post(`/chats/${roomId}/users`, {
uids: [selected.item.user.uid],
}).then((body) => {
Chats.refreshParticipantsList(roomId, modal, body);
searchInput.val('');
}).catch((err) => {
translator.translate(err.message, function (translated) {
errorEl.text(translated);
});
});
});
});
@ -307,16 +304,21 @@ define('forum/chats', [
});
};
Chats.refreshParticipantsList = function (roomId, modal) {
Chats.refreshParticipantsList = async (roomId, modal, data) => {
const listEl = modal.find('.list-group');
api.get(`/chats/${roomId}/users`, {}).then(({ users }) => {
app.parseAndTranslate('partials/modals/manage_room_users', { users }, function (html) {
listEl.html(html);
});
}).catch(() => {
translator.translate('[[error:invalid-data]]', function (translated) {
listEl.find('li').text(translated);
});
if (!data) {
try {
data = await api.get(`/chats/${roomId}/users`, {});
} catch (err) {
translator.translate('[[error:invalid-data]]', function (translated) {
listEl.find('li').text(translated);
});
}
}
app.parseAndTranslate('partials/modals/manage_room_users', data, function (html) {
listEl.html(html);
});
};

@ -83,3 +83,21 @@ chatsAPI.users = async (caller, data) => {
});
return { users };
};
chatsAPI.invite = async (caller, data) => {
const userCount = await messaging.getUserCountInRoom(data.roomId);
const maxUsers = meta.config.maximumUsersInChatRoom;
if (maxUsers && userCount >= maxUsers) {
throw new Error('[[error:cant-add-more-users-to-chat-room]]');
}
const uidsExist = await user.exists(data.uids);
if (!uidsExist.every(Boolean)) {
throw new Error('[[error:no-user]]');
}
await Promise.all(data.uids.map(async uid => messaging.canMessageUser(caller.uid, uid)));
await messaging.addUsersToRoom(caller.uid, data.uids, data.roomId);
delete data.uids;
return chatsAPI.users(caller, data);
};

@ -61,7 +61,12 @@ Chats.users = async (req, res) => {
};
Chats.invite = async (req, res) => {
// ...
const users = await api.chats.invite(req, {
...req.body,
roomId: req.params.roomId,
});
helpers.formatApiResponse(200, res, users);
};
Chats.kick = async (req, res) => {

@ -20,7 +20,7 @@ module.exports = function () {
// no route for room deletion, noted here just in case...
setupApiRoute(router, 'get', '/:roomId/users', [...middlewares, middleware.assert.room], controllers.write.chats.users);
// setupApiRoute(router, 'put', '/:roomId/users', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.invite);
setupApiRoute(router, 'post', '/:roomId/users', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.invite);
// setupApiRoute(router, 'delete', '/:roomId/users', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.kick);
setupApiRoute(router, 'get', '/:roomId/:mid', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.get);

@ -84,7 +84,7 @@ SocketModules.chats.loadRoom = async function (socket, data) {
};
SocketModules.chats.getUsersInRoom = async function (socket, data) {
sockets.warnDeprecated(socket, 'GET /api/v3/chats/:roomId/user');
sockets.warnDeprecated(socket, 'GET /api/v3/chats/:roomId/users');
if (!data || !data.roomId) {
throw new Error('[[error:invalid-data]]');
@ -98,6 +98,8 @@ SocketModules.chats.getUsersInRoom = async function (socket, data) {
};
SocketModules.chats.addUserToRoom = async function (socket, data) {
sockets.warnDeprecated(socket, 'POST /api/v3/chats/:roomId/users');
if (!data || !data.roomId || !data.username) {
throw new Error('[[error:invalid-data]]');
}
@ -107,18 +109,11 @@ SocketModules.chats.addUserToRoom = async function (socket, data) {
throw new Error('[[error:no-privileges]]');
}
const userCount = await Messaging.getUserCountInRoom(data.roomId);
const maxUsers = meta.config.maximumUsersInChatRoom;
if (maxUsers && userCount >= maxUsers) {
throw new Error('[[error:cant-add-more-users-to-chat-room]]');
}
// Revised API now takes uids, not usernames
data.uids = [await user.getUidByUsername(data.username)];
delete data.username;
const uid = await user.getUidByUsername(data.username);
if (!uid) {
throw new Error('[[error:no-user]]');
}
await Messaging.canMessageUser(socket.uid, uid);
await Messaging.addUsersToRoom(socket.uid, [uid], data.roomId);
await api.chats.invite(socket, data);
};
SocketModules.chats.removeUserFromRoom = async function (socket, data) {

@ -88,17 +88,13 @@ describe('Messaging Library', () => {
});
});
it('should NOT allow messages to be sent to a restricted user', (done) => {
User.setSetting(mocks.users.baz.uid, 'restrictChat', '1', (err) => {
assert.ifError(err);
Messaging.canMessageUser(mocks.users.herp.uid, mocks.users.baz.uid, (err) => {
assert.strictEqual(err.message, '[[error:chat-restricted]]');
socketModules.chats.addUserToRoom({ uid: mocks.users.herp.uid }, { roomId: 1, username: 'baz' }, (err) => {
assert.equal(err.message, '[[error:chat-restricted]]');
done();
});
});
});
it('should NOT allow messages to be sent to a restricted user', async () => {
await User.setSetting(mocks.users.baz.uid, 'restrictChat', '1');
try {
await Messaging.canMessageUser(mocks.users.herp.uid, mocks.users.baz.uid);
} catch (err) {
assert.strictEqual(err.message, '[[error:chat-restricted]]');
}
});
it('should always allow admins through', (done) => {
@ -169,35 +165,26 @@ describe('Messaging Library', () => {
assert.equal(body2.status.message, await translator.translate('[[error:cant-edit-chat-message]]'));
});
it('should fail to add user to room with invalid data', (done) => {
socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, null, (err) => {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, { roomId: null }, (err) => {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, { roomId: roomId, username: null }, (err) => {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
});
it('should fail to add user to room with invalid data', async () => {
let { statusCode, body } = await callv3API('post', `/chats/${roomId}/users`, {}, 'foo');
assert.strictEqual(statusCode, 400);
assert.strictEqual(body.status.message, await translator.translate('[[error:required-parameters-missing, uids]]'));
({ statusCode, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [null] }, 'foo'));
assert.strictEqual(statusCode, 400);
assert.strictEqual(body.status.message, await translator.translate('[[error:no-user]]'));
});
it('should add a user to room', (done) => {
socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, { roomId: roomId, username: 'herp' }, (err) => {
assert.ifError(err);
Messaging.isUserInRoom(mocks.users.herp.uid, roomId, (err, isInRoom) => {
assert.ifError(err);
assert(isInRoom);
done();
});
});
it('should add a user to room', async () => {
await callv3API('post', `/chats/${roomId}/users`, { uids: [mocks.users.herp.uid] }, 'foo');
const isInRoom = await Messaging.isUserInRoom(mocks.users.herp.uid, roomId);
assert(isInRoom);
});
it('should get users in room', async () => {
const { body } = await callv3API('get', `/chats/${roomId}/users`, {}, 'foo');
assert(Array.isArray(body.response.users));
assert.strictEqual(body.response.users.length, 3);
console.log(body.response.users);
});
it('should throw error if user is not in room', async () => {
@ -206,27 +193,24 @@ describe('Messaging Library', () => {
assert.equal(body.status.message, await translator.translate('[[error:no-privileges]]'));
});
it('should fail to add users to room if max is reached', (done) => {
it('should fail to add users to room if max is reached', async () => {
meta.config.maximumUsersInChatRoom = 2;
socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, { roomId: roomId, username: 'test' }, (err) => {
assert.equal(err.message, '[[error:cant-add-more-users-to-chat-room]]');
meta.config.maximumUsersInChatRoom = 0;
done();
});
const { statusCode, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [mocks.users.bar.uid] }, 'foo');
assert.strictEqual(statusCode, 400);
assert.equal(body.status.message, await translator.translate('[[error:cant-add-more-users-to-chat-room]]'));
meta.config.maximumUsersInChatRoom = 0;
});
it('should fail to add users to room if user does not exist', (done) => {
socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, { roomId: roomId, username: 'doesnotexist' }, (err) => {
assert.equal(err.message, '[[error:no-user]]');
done();
});
it('should fail to add users to room if user does not exist', async () => {
const { statusCode, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [98237498234] }, 'foo');
assert.strictEqual(statusCode, 400);
assert.strictEqual(body.status.message, await translator.translate('[[error:no-user]]'));
});
it('should fail to add self to room', (done) => {
socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, { roomId: roomId, username: 'foo' }, (err) => {
assert.equal(err.message, '[[error:cant-chat-with-yourself]]');
done();
});
it('should fail to add self to room', async () => {
const { statusCode, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [mocks.users.foo.uid] }, 'foo');
assert.strictEqual(statusCode, 400);
assert.strictEqual(body.status.message, await translator.translate('[[error:cant-chat-with-yourself]]'));
});
it('should fail to leave room with invalid data', (done) => {
@ -286,7 +270,7 @@ describe('Messaging Library', () => {
uids: [mocks.users.foo.uid],
}, 'herp');
await util.promisify(socketModules.chats.addUserToRoom)({ uid: mocks.users.herp.uid }, { roomId: body.response.roomId, username: 'baz' });
await callv3API('post', `/chats/${body.response.roomId}/users`, { uids: [mocks.users.baz.uid] }, 'herp');
await util.promisify(socketModules.chats.leave)({ uid: mocks.users.herp.uid }, body.response.roomId);
const data = await Messaging.getRoomData(body.response.roomId);
@ -348,9 +332,7 @@ describe('Messaging Library', () => {
assert.equal(err.message, '[[error:cant-remove-last-user]]');
}
await util.promisify(
socketModules.chats.addUserToRoom
)({ uid: mocks.users.foo.uid }, { roomId: roomId, username: 'baz' });
await callv3API('post', `/chats/${roomId}/users`, { uids: [mocks.users.baz.uid] }, 'foo');
await util.promisify(
socketModules.chats.removeUserFromRoom
)({ uid: mocks.users.foo.uid }, { roomId: roomId, uid: mocks.users.herp.uid });
@ -437,7 +419,7 @@ describe('Messaging Library', () => {
const { roomId } = body.response;
assert(roomId);
await socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, { roomId: roomId, username: 'herp' });
await callv3API('post', `/chats/${roomId}/users`, { uids: [mocks.users.herp.uid] }, 'foo');
await db.sortedSetAdd('users:online', Date.now() - ((meta.config.onlineCutoff * 60000) + 50000), mocks.users.herp.uid);
await callv3API('post', `/chats/${roomId}`, { roomId: roomId, message: 'second chat message **bold** text' }, 'foo');
@ -626,7 +608,7 @@ describe('Messaging Library', () => {
let mid;
let mid2;
before(async () => {
await socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, { roomId: roomId, username: 'baz' });
await callv3API('post', `/chats/${roomId}/users`, { uids: [mocks.users.baz.uid] }, 'foo');
let { body } = await callv3API('post', `/chats/${roomId}`, { roomId: roomId, message: 'first chat message' }, 'foo');
mid = body.response.mid;
({ body } = await callv3API('post', `/chats/${roomId}`, { roomId: roomId, message: 'second chat message' }, 'baz'));

Loading…
Cancel
Save