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

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

@ -94,6 +94,44 @@ MessageObject:
type: boolean type: boolean
cleanedContent: cleanedContent:
type: string 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: RoomObjectFull:
# Messaging.loadRoom # Messaging.loadRoom
allOf: allOf:

@ -22,40 +22,43 @@ get:
status: status:
$ref: ../../../components/schemas/Status.yaml#/Status $ref: ../../../components/schemas/Status.yaml#/Status
response: response:
type: object $ref: ../../../components/schemas/Chats.yaml#/RoomUserList
properties: post:
users: tags:
type: array - chats
items: summary: add users to chat room
type: object description: This operation invites users to a chat room
properties: parameters:
uid: - in: path
type: number name: roomId
description: A user identifier schema:
username: type: number
type: string required: true
description: A friendly name for a given user account description: a valid chat room id
picture: example: 1
nullable: true requestBody:
type: string required: true
status: content:
type: string application/json:
displayname: schema:
type: string type: object
description: This is either username or fullname depending on forum and user settings properties:
icon:text: uids:
type: string type: array
description: A single-letter representation of a username. This is used in the description: A list of valid uids
auto-generated icon given to users example: [2]
without an avatar items:
icon:bgColor: type: number
type: string description: A valid uid
description: A six-character hexadecimal colour code assigned to the user. This responses:
value is used in conjunction with '200':
`icon:text` for the user's description: users successfully invited to chat room
auto-generated icon content:
example: "#f44336" application/json:
isOwner: schema:
type: boolean type: object
canKick: properties:
type: boolean 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) { require(['autocomplete', 'translator'], function (autocomplete, translator) {
autocomplete.user(searchInput, function (event, selected) { autocomplete.user(searchInput, function (event, selected) {
errorEl.text(''); errorEl.text('');
socket.emit('modules.chats.addUserToRoom', { api.post(`/chats/${roomId}/users`, {
roomId: roomId, uids: [selected.item.user.uid],
username: selected.item.user.name, }).then((body) => {
}, function (err) { Chats.refreshParticipantsList(roomId, modal, body);
if (err) {
translator.translate(err.message, function (translated) {
errorEl.text(translated);
});
}
Chats.refreshParticipantsList(roomId, modal);
searchInput.val(''); 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'); const listEl = modal.find('.list-group');
api.get(`/chats/${roomId}/users`, {}).then(({ users }) => {
app.parseAndTranslate('partials/modals/manage_room_users', { users }, function (html) { if (!data) {
listEl.html(html); try {
}); data = await api.get(`/chats/${roomId}/users`, {});
}).catch(() => { } catch (err) {
translator.translate('[[error:invalid-data]]', function (translated) { translator.translate('[[error:invalid-data]]', function (translated) {
listEl.find('li').text(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 }; 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) => { 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) => { Chats.kick = async (req, res) => {

@ -20,7 +20,7 @@ module.exports = function () {
// no route for room deletion, noted here just in case... // 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, '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, '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); 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) { 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) { if (!data || !data.roomId) {
throw new Error('[[error:invalid-data]]'); throw new Error('[[error:invalid-data]]');
@ -98,6 +98,8 @@ SocketModules.chats.getUsersInRoom = async function (socket, data) {
}; };
SocketModules.chats.addUserToRoom = 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) { if (!data || !data.roomId || !data.username) {
throw new Error('[[error:invalid-data]]'); throw new Error('[[error:invalid-data]]');
} }
@ -107,18 +109,11 @@ SocketModules.chats.addUserToRoom = async function (socket, data) {
throw new Error('[[error:no-privileges]]'); throw new Error('[[error:no-privileges]]');
} }
const userCount = await Messaging.getUserCountInRoom(data.roomId); // Revised API now takes uids, not usernames
const maxUsers = meta.config.maximumUsersInChatRoom; data.uids = [await user.getUidByUsername(data.username)];
if (maxUsers && userCount >= maxUsers) { delete data.username;
throw new Error('[[error:cant-add-more-users-to-chat-room]]');
}
const uid = await user.getUidByUsername(data.username); await api.chats.invite(socket, data);
if (!uid) {
throw new Error('[[error:no-user]]');
}
await Messaging.canMessageUser(socket.uid, uid);
await Messaging.addUsersToRoom(socket.uid, [uid], data.roomId);
}; };
SocketModules.chats.removeUserFromRoom = async function (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) => { it('should NOT allow messages to be sent to a restricted user', async () => {
User.setSetting(mocks.users.baz.uid, 'restrictChat', '1', (err) => { await User.setSetting(mocks.users.baz.uid, 'restrictChat', '1');
assert.ifError(err); try {
Messaging.canMessageUser(mocks.users.herp.uid, mocks.users.baz.uid, (err) => { await Messaging.canMessageUser(mocks.users.herp.uid, mocks.users.baz.uid);
assert.strictEqual(err.message, '[[error:chat-restricted]]'); } catch (err) {
socketModules.chats.addUserToRoom({ uid: mocks.users.herp.uid }, { roomId: 1, username: 'baz' }, (err) => { assert.strictEqual(err.message, '[[error:chat-restricted]]');
assert.equal(err.message, '[[error:chat-restricted]]'); }
done();
});
});
});
}); });
it('should always allow admins through', (done) => { 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]]')); 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) => { it('should fail to add user to room with invalid data', async () => {
socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, null, (err) => { let { statusCode, body } = await callv3API('post', `/chats/${roomId}/users`, {}, 'foo');
assert.equal(err.message, '[[error:invalid-data]]'); assert.strictEqual(statusCode, 400);
socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, { roomId: null }, (err) => { assert.strictEqual(body.status.message, await translator.translate('[[error:required-parameters-missing, uids]]'));
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, { roomId: roomId, username: null }, (err) => { ({ statusCode, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [null] }, 'foo'));
assert.equal(err.message, '[[error:invalid-data]]'); assert.strictEqual(statusCode, 400);
done(); assert.strictEqual(body.status.message, await translator.translate('[[error:no-user]]'));
});
});
});
}); });
it('should add a user to room', (done) => { it('should add a user to room', async () => {
socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, { roomId: roomId, username: 'herp' }, (err) => { await callv3API('post', `/chats/${roomId}/users`, { uids: [mocks.users.herp.uid] }, 'foo');
assert.ifError(err); const isInRoom = await Messaging.isUserInRoom(mocks.users.herp.uid, roomId);
Messaging.isUserInRoom(mocks.users.herp.uid, roomId, (err, isInRoom) => { assert(isInRoom);
assert.ifError(err);
assert(isInRoom);
done();
});
});
}); });
it('should get users in room', async () => { it('should get users in room', async () => {
const { body } = await callv3API('get', `/chats/${roomId}/users`, {}, 'foo'); const { body } = await callv3API('get', `/chats/${roomId}/users`, {}, 'foo');
assert(Array.isArray(body.response.users)); assert(Array.isArray(body.response.users));
assert.strictEqual(body.response.users.length, 3); assert.strictEqual(body.response.users.length, 3);
console.log(body.response.users);
}); });
it('should throw error if user is not in room', async () => { 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]]')); 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; meta.config.maximumUsersInChatRoom = 2;
socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, { roomId: roomId, username: 'test' }, (err) => { const { statusCode, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [mocks.users.bar.uid] }, 'foo');
assert.equal(err.message, '[[error:cant-add-more-users-to-chat-room]]'); assert.strictEqual(statusCode, 400);
meta.config.maximumUsersInChatRoom = 0; assert.equal(body.status.message, await translator.translate('[[error:cant-add-more-users-to-chat-room]]'));
done(); meta.config.maximumUsersInChatRoom = 0;
});
}); });
it('should fail to add users to room if user does not exist', (done) => { it('should fail to add users to room if user does not exist', async () => {
socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, { roomId: roomId, username: 'doesnotexist' }, (err) => { const { statusCode, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [98237498234] }, 'foo');
assert.equal(err.message, '[[error:no-user]]'); assert.strictEqual(statusCode, 400);
done(); assert.strictEqual(body.status.message, await translator.translate('[[error:no-user]]'));
});
}); });
it('should fail to add self to room', (done) => { it('should fail to add self to room', async () => {
socketModules.chats.addUserToRoom({ uid: mocks.users.foo.uid }, { roomId: roomId, username: 'foo' }, (err) => { const { statusCode, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [mocks.users.foo.uid] }, 'foo');
assert.equal(err.message, '[[error:cant-chat-with-yourself]]'); assert.strictEqual(statusCode, 400);
done(); assert.strictEqual(body.status.message, await translator.translate('[[error:cant-chat-with-yourself]]'));
});
}); });
it('should fail to leave room with invalid data', (done) => { it('should fail to leave room with invalid data', (done) => {
@ -286,7 +270,7 @@ describe('Messaging Library', () => {
uids: [mocks.users.foo.uid], uids: [mocks.users.foo.uid],
}, 'herp'); }, '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); await util.promisify(socketModules.chats.leave)({ uid: mocks.users.herp.uid }, body.response.roomId);
const data = await Messaging.getRoomData(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]]'); assert.equal(err.message, '[[error:cant-remove-last-user]]');
} }
await util.promisify( await callv3API('post', `/chats/${roomId}/users`, { uids: [mocks.users.baz.uid] }, 'foo');
socketModules.chats.addUserToRoom
)({ uid: mocks.users.foo.uid }, { roomId: roomId, username: 'baz' });
await util.promisify( await util.promisify(
socketModules.chats.removeUserFromRoom socketModules.chats.removeUserFromRoom
)({ uid: mocks.users.foo.uid }, { roomId: roomId, uid: mocks.users.herp.uid }); )({ uid: mocks.users.foo.uid }, { roomId: roomId, uid: mocks.users.herp.uid });
@ -437,7 +419,7 @@ describe('Messaging Library', () => {
const { roomId } = body.response; const { roomId } = body.response;
assert(roomId); 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 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'); await callv3API('post', `/chats/${roomId}`, { roomId: roomId, message: 'second chat message **bold** text' }, 'foo');
@ -626,7 +608,7 @@ describe('Messaging Library', () => {
let mid; let mid;
let mid2; let mid2;
before(async () => { 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'); let { body } = await callv3API('post', `/chats/${roomId}`, { roomId: roomId, message: 'first chat message' }, 'foo');
mid = body.response.mid; mid = body.response.mid;
({ body } = await callv3API('post', `/chats/${roomId}`, { roomId: roomId, message: 'second chat message' }, 'baz')); ({ body } = await callv3API('post', `/chats/${roomId}`, { roomId: roomId, message: 'second chat message' }, 'baz'));

Loading…
Cancel
Save