RocketChat/Rocket.Chat

View on GitHub
apps/meteor/app/api/server/v1/channels.ts

Summary

Maintainability
F
2 wks
Test Coverage
import { Team, Room } from '@rocket.chat/core-services';
import type { IRoom, ISubscription, IUser, RoomType, IUpload } from '@rocket.chat/core-typings';
import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models';
import {
    isChannelsAddAllProps,
    isChannelsArchiveProps,
    isChannelsHistoryProps,
    isChannelsUnarchiveProps,
    isChannelsRolesProps,
    isChannelsJoinProps,
    isChannelsKickProps,
    isChannelsLeaveProps,
    isChannelsMessagesProps,
    isChannelsOpenProps,
    isChannelsSetAnnouncementProps,
    isChannelsGetAllUserMentionsByChannelProps,
    isChannelsModeratorsProps,
    isChannelsConvertToTeamProps,
    isChannelsSetReadOnlyProps,
    isChannelsDeleteProps,
    isChannelsImagesProps,
} from '@rocket.chat/rest-typings';
import { Meteor } from 'meteor/meteor';

import { isTruthy } from '../../../../lib/isTruthy';
import { findUsersOfRoom } from '../../../../server/lib/findUsersOfRoom';
import { hideRoomMethod } from '../../../../server/methods/hideRoom';
import { removeUserFromRoomMethod } from '../../../../server/methods/removeUserFromRoom';
import { canAccessRoomAsync } from '../../../authorization/server';
import { hasPermissionAsync, hasAtLeastOnePermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { saveRoomSettings } from '../../../channel-settings/server/methods/saveRoomSettings';
import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission';
import { addUsersToRoomMethod } from '../../../lib/server/methods/addUsersToRoom';
import { createChannelMethod } from '../../../lib/server/methods/createChannel';
import { leaveRoomMethod } from '../../../lib/server/methods/leaveRoom';
import { settings } from '../../../settings/server';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
import { API } from '../api';
import { addUserToFileObj } from '../helpers/addUserToFileObj';
import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessage';
import { getLoggedInUser } from '../helpers/getLoggedInUser';
import { getPaginationItems } from '../helpers/getPaginationItems';
import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams';

// Returns the channel IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property
async function findChannelByIdOrName({
    params,
    checkedArchived = true,
    userId,
}: {
    params:
        | {
                roomId?: string;
          }
        | {
                roomName?: string;
          };
    userId?: string;
    checkedArchived?: boolean;
}): Promise<IRoom> {
    const projection = { ...API.v1.defaultFieldsToExclude };

    let room;
    if ('roomId' in params) {
        room = await Rooms.findOneById(params.roomId || '', { projection });
    } else if ('roomName' in params) {
        room = await Rooms.findOneByName(params.roomName || '', { projection });
    }

    if (!room || (room.t !== 'c' && room.t !== 'l')) {
        throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any channel');
    }

    if (checkedArchived && room.archived) {
        throw new Meteor.Error('error-room-archived', `The channel, ${room.name}, is archived`);
    }
    if (userId && room.lastMessage) {
        const [lastMessage] = await normalizeMessagesForUser([room.lastMessage], userId);
        room.lastMessage = lastMessage;
    }

    return room;
}

API.v1.addRoute(
    'channels.addAll',
    {
        authRequired: true,
        validateParams: isChannelsAddAllProps,
    },
    {
        async post() {
            const { activeUsersOnly, ...params } = this.bodyParams;
            const findResult = await findChannelByIdOrName({ params, userId: this.userId });

            await Meteor.callAsync('addAllUserToRoom', findResult._id, activeUsersOnly);

            return API.v1.success({
                channel: await findChannelByIdOrName({ params, userId: this.userId }),
            });
        },
    },
);

API.v1.addRoute(
    'channels.archive',
    {
        authRequired: true,
        validateParams: isChannelsArchiveProps,
    },
    {
        async post() {
            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            await Meteor.callAsync('archiveRoom', findResult._id);

            return API.v1.success();
        },
    },
);

API.v1.addRoute(
    'channels.unarchive',
    {
        authRequired: true,
        validateParams: isChannelsUnarchiveProps,
    },
    {
        async post() {
            const findResult = await findChannelByIdOrName({
                params: this.bodyParams,
                checkedArchived: false,
            });

            if (!findResult.archived) {
                return API.v1.failure(`The channel, ${findResult.name}, is not archived`);
            }

            await Meteor.callAsync('unarchiveRoom', findResult._id);

            return API.v1.success();
        },
    },
);

API.v1.addRoute(
    'channels.history',
    {
        authRequired: true,
        validateParams: isChannelsHistoryProps,
    },
    {
        async get() {
            const { unreads, oldest, latest, showThreadMessages, inclusive, ...params } = this.queryParams;
            const findResult = await findChannelByIdOrName({
                params,
                checkedArchived: false,
            });

            const { count = 20, offset = 0 } = await getPaginationItems(this.queryParams);

            const result = await Meteor.callAsync('getChannelHistory', {
                rid: findResult._id,
                latest: latest ? new Date(latest) : new Date(),
                oldest: oldest && new Date(oldest),
                inclusive: inclusive === 'true',
                offset,
                count,
                unreads: unreads === 'true',
                showThreadMessages: showThreadMessages === 'true',
            });

            if (!result) {
                return API.v1.unauthorized();
            }

            return API.v1.success(result);
        },
    },
);

API.v1.addRoute(
    'channels.roles',
    {
        authRequired: true,
        validateParams: isChannelsRolesProps,
    },
    {
        async get() {
            const findResult = await findChannelByIdOrName({ params: this.queryParams });

            const roles = await Meteor.callAsync('getRoomRoles', findResult._id);

            return API.v1.success({
                roles,
            });
        },
    },
);

API.v1.addRoute(
    'channels.join',
    {
        authRequired: true,
        validateParams: isChannelsJoinProps,
    },
    {
        async post() {
            const { joinCode, ...params } = this.bodyParams;
            const findResult = await findChannelByIdOrName({ params });

            await Room.join({ room: findResult, user: this.user, joinCode });

            return API.v1.success({
                channel: await findChannelByIdOrName({ params, userId: this.userId }),
            });
        },
    },
);

API.v1.addRoute(
    'channels.kick',
    {
        authRequired: true,
        validateParams: isChannelsKickProps,
    },
    {
        async post() {
            const { ...params /* userId */ } = this.bodyParams;
            const findResult = await findChannelByIdOrName({ params });

            const user = await getUserFromParams(this.bodyParams);
            if (!user?.username) {
                return API.v1.failure('Invalid user');
            }

            await removeUserFromRoomMethod(this.userId, { rid: findResult._id, username: user.username });

            return API.v1.success({
                channel: await findChannelByIdOrName({ params, userId: this.userId }),
            });
        },
    },
);

API.v1.addRoute(
    'channels.leave',
    {
        authRequired: true,
        validateParams: isChannelsLeaveProps,
    },
    {
        async post() {
            const { ...params } = this.bodyParams;
            const findResult = await findChannelByIdOrName({ params });

            const user = await Users.findOneById(this.userId);
            if (!user) {
                return API.v1.failure('Invalid user');
            }
            await leaveRoomMethod(user, findResult._id);

            return API.v1.success({
                channel: await findChannelByIdOrName({ params, userId: this.userId }),
            });
        },
    },
);

API.v1.addRoute(
    'channels.messages',
    {
        authRequired: true,
        validateParams: isChannelsMessagesProps,
    },
    {
        async get() {
            const { roomId } = this.queryParams;
            const findResult = await findChannelByIdOrName({
                params: { roomId },
                checkedArchived: false,
            });
            const { offset, count } = await getPaginationItems(this.queryParams);
            const { sort, fields, query } = await this.parseJsonQuery();

            const ourQuery = { ...query, rid: findResult._id };

            // Special check for the permissions
            if (
                (await hasPermissionAsync(this.userId, 'view-joined-room')) &&
                !(await Subscriptions.findOneByRoomIdAndUserId(findResult._id, this.userId, { projection: { _id: 1 } }))
            ) {
                return API.v1.unauthorized();
            }
            if (!(await hasPermissionAsync(this.userId, 'view-c-room'))) {
                return API.v1.unauthorized();
            }

            const { cursor, totalCount } = await Messages.findPaginated(ourQuery, {
                sort: sort || { ts: -1 },
                skip: offset,
                limit: count,
                projection: fields,
            });

            const [messages, total] = await Promise.all([cursor.toArray(), totalCount]);

            return API.v1.success({
                messages: await normalizeMessagesForUser(messages, this.userId),
                count: messages.length,
                offset,
                total,
            });
        },
    },
);

API.v1.addRoute(
    'channels.open',
    {
        authRequired: true,
        validateParams: isChannelsOpenProps,
    },
    {
        async post() {
            const { ...params } = this.bodyParams;

            const findResult = await findChannelByIdOrName({
                params,
                checkedArchived: false,
            });

            const sub = await Subscriptions.findOneByRoomIdAndUserId(findResult._id, this.userId);

            if (!sub) {
                return API.v1.failure(`The user/callee is not in the channel "${findResult.name}".`);
            }

            if (sub.open) {
                return API.v1.failure(`The channel, ${findResult.name}, is already open to the sender`);
            }

            await Meteor.callAsync('openRoom', findResult._id);

            return API.v1.success();
        },
    },
);

API.v1.addRoute(
    'channels.setReadOnly',
    {
        authRequired: true,
        validateParams: isChannelsSetReadOnlyProps,
    },
    {
        async post() {
            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            if (findResult.ro === this.bodyParams.readOnly) {
                return API.v1.failure('The channel read only setting is the same as what it would be changed to.');
            }

            await saveRoomSettings(this.userId, findResult._id, 'readOnly', this.bodyParams.readOnly);

            return API.v1.success({
                channel: await findChannelByIdOrName({ params: this.bodyParams, userId: this.userId }),
            });
        },
    },
);

API.v1.addRoute(
    'channels.setAnnouncement',
    {
        authRequired: true,
        validateParams: isChannelsSetAnnouncementProps,
    },
    {
        async post() {
            const { announcement, ...params } = this.bodyParams;

            const findResult = await findChannelByIdOrName({ params });

            await saveRoomSettings(this.userId, findResult._id, 'roomAnnouncement', announcement);

            return API.v1.success({
                announcement: this.bodyParams.announcement,
            });
        },
    },
);

API.v1.addRoute(
    'channels.getAllUserMentionsByChannel',
    {
        authRequired: true,
        validateParams: isChannelsGetAllUserMentionsByChannelProps,
    },
    {
        async get() {
            const { roomId } = this.queryParams;
            const { offset, count } = await getPaginationItems(this.queryParams);
            const { sort } = await this.parseJsonQuery();

            const mentions = await Meteor.callAsync('getUserMentionsByChannel', {
                roomId,
                options: {
                    sort: sort || { ts: 1 },
                    skip: offset,
                    limit: count,
                },
            });

            const allMentions = await Meteor.callAsync('getUserMentionsByChannel', {
                roomId,
                options: {},
            });

            return API.v1.success({
                mentions,
                count: mentions.length,
                offset,
                total: allMentions.length,
            });
        },
    },
);

API.v1.addRoute(
    'channels.moderators',
    {
        authRequired: true,
        validateParams: isChannelsModeratorsProps,
    },
    {
        async get() {
            const { ...params } = this.queryParams;

            const findResult = await findChannelByIdOrName({ params });

            const moderators = (
                await Subscriptions.findByRoomIdAndRoles(findResult._id, ['moderator'], {
                    projection: { u: 1 },
                }).toArray()
            ).map((sub: ISubscription) => sub.u);

            return API.v1.success({
                moderators,
            });
        },
    },
);

API.v1.addRoute(
    'channels.delete',
    {
        authRequired: true,
        validateParams: isChannelsDeleteProps,
    },
    {
        async post() {
            const room = await findChannelByIdOrName({
                params: this.bodyParams,
                checkedArchived: false,
            });

            await Meteor.callAsync('eraseRoom', room._id);

            return API.v1.success();
        },
    },
);

API.v1.addRoute(
    'channels.convertToTeam',
    {
        authRequired: true,
        validateParams: isChannelsConvertToTeamProps,
    },
    {
        async post() {
            if (!(await hasPermissionAsync(this.userId, 'create-team'))) {
                return API.v1.unauthorized();
            }

            const { channelId, channelName } = this.bodyParams;

            if (!channelId && !channelName) {
                return API.v1.failure('The parameter "channelId" or "channelName" is required');
            }

            if (channelId && !(await hasPermissionAsync(this.userId, 'edit-room', channelId))) {
                return API.v1.unauthorized();
            }

            const room = await findChannelByIdOrName({
                params: channelId !== undefined ? { roomId: channelId } : { roomName: channelName },
                userId: this.userId,
            });

            if (!room) {
                return API.v1.failure('Channel not found');
            }

            const subscriptions = await Subscriptions.findByRoomId(room._id, {
                projection: { 'u._id': 1 },
            });

            const members = (await subscriptions.toArray()).map((s: ISubscription) => s.u?._id);

            const teamData = {
                team: {
                    name: room.name ?? '',
                    type: room.t === 'c' ? 0 : 1,
                },
                members,
                room: {
                    name: room.name,
                    id: room._id,
                },
            };

            const team = await Team.create(this.userId, teamData);

            return API.v1.success({ team });
        },
    },
);

API.v1.addRoute(
    'channels.addModerator',
    { authRequired: true },
    {
        async post() {
            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            const user = await getUserFromParams(this.bodyParams);

            await Meteor.callAsync('addRoomModerator', findResult._id, user._id);

            return API.v1.success();
        },
    },
);

API.v1.addRoute(
    'channels.addOwner',
    { authRequired: true },
    {
        async post() {
            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            const user = await getUserFromParams(this.bodyParams);

            await Meteor.callAsync('addRoomOwner', findResult._id, user._id);

            return API.v1.success();
        },
    },
);

API.v1.addRoute(
    'channels.close',
    { authRequired: true },
    {
        async post() {
            const findResult = await findChannelByIdOrName({
                params: this.bodyParams,
                checkedArchived: false,
            });

            const sub = await Subscriptions.findOneByRoomIdAndUserId(findResult._id, this.userId);

            if (!sub) {
                return API.v1.failure(`The user/callee is not in the channel "${findResult.name}.`);
            }

            if (!sub.open) {
                return API.v1.failure(`The channel, ${findResult.name}, is already closed to the sender`);
            }

            await hideRoomMethod(this.userId, findResult._id);

            return API.v1.success();
        },
    },
);

API.v1.addRoute(
    'channels.counters',
    { authRequired: true },
    {
        async get() {
            const access = await hasPermissionAsync(this.userId, 'view-room-administration');
            const { userId } = this.queryParams;
            let user = this.userId;
            let unreads = null;
            let userMentions = null;
            let unreadsFrom = null;
            let joined = false;
            let msgs = null;
            let latest = null;
            let members = null;

            if (userId) {
                if (!access) {
                    return API.v1.unauthorized();
                }
                user = userId;
            }
            const room = await findChannelByIdOrName({
                params: this.queryParams,
            });
            const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, user);
            const lm = room.lm ? room.lm : room._updatedAt;

            if (subscription?.open) {
                unreads = await Messages.countVisibleByRoomIdBetweenTimestampsInclusive(subscription.rid, subscription.ls, lm);
                unreadsFrom = subscription.ls || subscription.ts;
                userMentions = subscription.userMentions;
                joined = true;
            }

            if (access || joined) {
                msgs = room.msgs;
                latest = lm;
                members = room.usersCount;
            }

            return API.v1.success({
                joined,
                members,
                unreads,
                unreadsFrom,
                msgs,
                latest,
                userMentions,
            });
        },
    },
);

async function createChannelValidator(params: {
    user: { value: string };
    name?: { key: string; value?: string };
    members?: { key: string; value?: string[] };
    customFields?: { key: string; value?: string };
    teams?: { key: string; value?: string[] };
}) {
    if (!(await hasPermissionAsync(params.user.value, 'create-c'))) {
        throw new Error('unauthorized');
    }

    if (!params.name?.value) {
        throw new Error(`Param "${params.name?.key}" is required`);
    }

    if (params.members?.value && !Array.isArray(params.members.value)) {
        throw new Error(`Param "${params.members.key}" must be an array if provided`);
    }

    if (params.customFields?.value && !(typeof params.customFields.value === 'object')) {
        throw new Error(`Param "${params.customFields.key}" must be an object if provided`);
    }

    if (params.teams?.value && !Array.isArray(params.teams.value)) {
        throw new Error(`Param ${params.teams.key} must be an array`);
    }
}

async function createChannel(
    userId: string,
    params: {
        name?: string;
        members?: string[];
        customFields?: Record<string, any>;
        extraData?: Record<string, any>;
        readOnly?: boolean;
        excludeSelf?: boolean;
    },
): Promise<{ channel: IRoom }> {
    const readOnly = typeof params.readOnly !== 'undefined' ? params.readOnly : false;
    const id = await createChannelMethod(
        userId,
        params.name || '',
        params.members ? params.members : [],
        readOnly,
        params.customFields,
        params.extraData,
        params.excludeSelf,
    );

    return {
        channel: await findChannelByIdOrName({ params: { roomId: id.rid }, userId }),
    };
}

API.channels = {
    create: {
        validate: createChannelValidator,
        execute: createChannel,
    },
};

API.v1.addRoute(
    'channels.create',
    { authRequired: true },
    {
        async post() {
            const { userId, bodyParams } = this;

            let error;

            try {
                await API.channels?.create.validate({
                    user: {
                        value: userId,
                    },
                    name: {
                        value: bodyParams.name,
                        key: 'name',
                    },
                    members: {
                        value: bodyParams.members,
                        key: 'members',
                    },
                    teams: {
                        value: bodyParams.teams,
                        key: 'teams',
                    },
                });
            } catch (e: any) {
                if (e.message === 'unauthorized') {
                    error = API.v1.unauthorized();
                } else {
                    error = API.v1.failure(e.message);
                }
            }

            if (error) {
                return error;
            }

            if (bodyParams.teams) {
                const canSeeAllTeams = await hasPermissionAsync(this.userId, 'view-all-teams');
                const teams = await Team.listByNames(bodyParams.teams, { projection: { _id: 1 } });
                const teamMembers = [];

                for (const team of teams) {
                    // eslint-disable-next-line no-await-in-loop
                    const { records: members } = await Team.members(this.userId, team._id, canSeeAllTeams, {
                        offset: 0,
                        count: Number.MAX_SAFE_INTEGER,
                    });
                    const uids = members.map((member) => member.user.username);
                    teamMembers.push(...uids);
                }

                const membersToAdd = new Set([...teamMembers, ...(bodyParams.members || [])]);
                bodyParams.members = [...membersToAdd].filter(Boolean) as string[];
            }

            return API.v1.success(await API.channels?.create.execute(userId, bodyParams));
        },
    },
);

API.v1.addRoute(
    'channels.files',
    { authRequired: true },
    {
        async get() {
            const findResult = await findChannelByIdOrName({
                params: this.queryParams,
                checkedArchived: false,
            });

            if (!(await canAccessRoomAsync(findResult, { _id: this.userId }))) {
                return API.v1.unauthorized();
            }

            const { offset, count } = await getPaginationItems(this.queryParams);
            const { sort, fields, query } = await this.parseJsonQuery();

            const ourQuery = Object.assign({}, query, { rid: findResult._id });

            const { cursor, totalCount } = await Uploads.findPaginatedWithoutThumbs(ourQuery, {
                sort: sort || { name: 1 },
                skip: offset,
                limit: count,
                projection: fields,
            });

            const [files, total] = await Promise.all([cursor.toArray(), totalCount]);

            return API.v1.success({
                files: await addUserToFileObj(files),
                count: files.length,
                offset,
                total,
            });
        },
    },
);

API.v1.addRoute(
    'channels.images',
    { authRequired: true, validateParams: isChannelsImagesProps },
    {
        async get() {
            const room = await Rooms.findOneById<Pick<IRoom, '_id' | 't' | 'teamId' | 'prid'>>(this.queryParams.roomId, {
                projection: { t: 1, teamId: 1, prid: 1 },
            });

            if (!room || !(await canAccessRoomAsync(room, { _id: this.userId }))) {
                return API.v1.unauthorized();
            }

            let initialImage: IUpload | null = null;
            if (this.queryParams.startingFromId) {
                initialImage = await Uploads.findOneById(this.queryParams.startingFromId);
            }

            const { offset, count } = await getPaginationItems(this.queryParams);

            const { cursor, totalCount } = Uploads.findImagesByRoomId(room._id, initialImage?.uploadedAt, {
                skip: offset,
                limit: count,
            });

            const [files, total] = await Promise.all([cursor.toArray(), totalCount]);

            // If the initial image was not returned in the query, insert it as the first element of the list
            if (initialImage && !files.find(({ _id }) => _id === (initialImage as IUpload)._id)) {
                files.splice(0, 0, initialImage);
            }

            return API.v1.success({
                files,
                count,
                offset,
                total,
            });
        },
    },
);

API.v1.addRoute(
    'channels.getIntegrations',
    { authRequired: true },
    {
        async get() {
            if (
                !(await hasAtLeastOnePermissionAsync(this.userId, [
                    'manage-outgoing-integrations',
                    'manage-own-outgoing-integrations',
                    'manage-incoming-integrations',
                    'manage-own-incoming-integrations',
                ]))
            ) {
                return API.v1.unauthorized();
            }

            const findResult = await findChannelByIdOrName({
                params: this.queryParams,
                checkedArchived: false,
            });

            let includeAllPublicChannels = true;
            if (typeof this.queryParams.includeAllPublicChannels !== 'undefined') {
                includeAllPublicChannels = this.queryParams.includeAllPublicChannels === 'true';
            }

            let ourQuery: { channel: string | { $in: string[] } } = {
                channel: `#${findResult.name}`,
            };

            if (includeAllPublicChannels) {
                ourQuery.channel = {
                    $in: [ourQuery.channel as string, 'all_public_channels'],
                };
            }

            const params = this.queryParams;
            const { offset, count } = await getPaginationItems(params);
            const { sort, fields: projection, query } = await this.parseJsonQuery();

            ourQuery = Object.assign(await mountIntegrationQueryBasedOnPermissions(this.userId), query, ourQuery);

            const { cursor, totalCount } = await Integrations.findPaginated(ourQuery, {
                sort: sort || { _createdAt: 1 },
                skip: offset,
                limit: count,
                projection,
            });

            const [integrations, total] = await Promise.all([cursor.toArray(), totalCount]);

            return API.v1.success({
                integrations,
                count: integrations.length,
                offset,
                total,
            });
        },
    },
);

API.v1.addRoute(
    'channels.info',
    { authRequired: true },
    {
        async get() {
            return API.v1.success({
                channel: await findChannelByIdOrName({
                    params: this.queryParams,
                    checkedArchived: false,
                    userId: this.userId,
                }),
            });
        },
    },
);

API.v1.addRoute(
    'channels.invite',
    { authRequired: true },
    {
        async post() {
            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            const users = await getUserListFromParams(this.bodyParams);

            if (!users.length) {
                return API.v1.failure('invalid-user-invite-list', 'Cannot invite if no users are provided');
            }

            await addUsersToRoomMethod(this.userId, { rid: findResult._id, users: users.map((u) => u.username).filter(isTruthy) });

            return API.v1.success({
                channel: await findChannelByIdOrName({ params: this.bodyParams, userId: this.userId }),
            });
        },
    },
);

API.v1.addRoute(
    'channels.list',
    { authRequired: true },
    {
        async get() {
            const { offset, count } = await getPaginationItems(this.queryParams);
            const { sort, fields, query } = await this.parseJsonQuery();
            const hasPermissionToSeeAllPublicChannels = await hasPermissionAsync(this.userId, 'view-c-room');

            const ourQuery: Record<string, any> = { ...query, t: 'c' };

            if (!hasPermissionToSeeAllPublicChannels) {
                if (!(await hasPermissionAsync(this.userId, 'view-joined-room'))) {
                    return API.v1.unauthorized();
                }
                const roomIds = (
                    await Subscriptions.findByUserIdAndType(this.userId, 'c', {
                        projection: { rid: 1 },
                    }).toArray()
                ).map((s) => s.rid);
                ourQuery._id = { $in: roomIds };
            }

            // teams filter - I would love to have a way to apply this filter @ db level :(
            const ids = (await Subscriptions.findByUserId(this.userId, { projection: { rid: 1 } }).toArray()).map(
                (item: Record<string, any>) => item.rid,
            );

            ourQuery.$or = [
                {
                    teamId: {
                        $exists: false,
                    },
                },
                {
                    teamId: {
                        $exists: true,
                    },
                    _id: {
                        $in: ids,
                    },
                },
            ];

            const { cursor, totalCount } = await Rooms.findPaginated(ourQuery, {
                sort: sort || { name: 1 },
                skip: offset,
                limit: count,
                projection: fields,
            });

            const [channels, total] = await Promise.all([cursor.toArray(), totalCount]);

            return API.v1.success({
                channels: await Promise.all(channels.map((room) => composeRoomWithLastMessage(room, this.userId))),
                count: channels.length,
                offset,
                total,
            });
        },
    },
);

API.v1.addRoute(
    'channels.list.joined',
    { authRequired: true },
    {
        async get() {
            const { offset, count } = await getPaginationItems(this.queryParams);
            const { sort, fields } = await this.parseJsonQuery();

            const subs = await Subscriptions.findByUserIdAndTypes(this.userId, ['c'], { projection: { rid: 1 } }).toArray();
            const rids = subs.map(({ rid }) => rid).filter(Boolean);

            if (rids.length === 0) {
                return API.v1.success({
                    channels: [],
                    offset,
                    count: 0,
                    total: 0,
                });
            }

            const { cursor, totalCount } = await Rooms.findPaginatedByTypeAndIds('c', rids, {
                sort: sort || { name: 1 },
                skip: offset,
                limit: count,
                projection: fields,
            });

            const [channels, total] = await Promise.all([cursor.toArray(), totalCount]);

            return API.v1.success({
                channels: await Promise.all(channels.map((room) => composeRoomWithLastMessage(room, this.userId))),
                offset,
                count: channels.length,
                total,
            });
        },
    },
);

API.v1.addRoute(
    'channels.members',
    { authRequired: true },
    {
        async get() {
            const findResult = await findChannelByIdOrName({
                params: this.queryParams,
                checkedArchived: false,
            });

            if (findResult.broadcast && !(await hasPermissionAsync(this.userId, 'view-broadcast-member-list', findResult._id))) {
                return API.v1.unauthorized();
            }

            const { offset: skip, count: limit } = await getPaginationItems(this.queryParams);
            const { sort = {} } = await this.parseJsonQuery();

            check(
                this.queryParams,
                Match.ObjectIncluding({
                    status: Match.Maybe([String]),
                    filter: Match.Maybe(String),
                }),
            );
            const { status, filter } = this.queryParams;

            const { cursor, totalCount } = await findUsersOfRoom({
                rid: findResult._id,
                ...(status && { status: { $in: status } }),
                skip,
                limit,
                filter,
                ...(sort?.username && { sort: { username: sort.username } }),
            });

            const [members, total] = await Promise.all([cursor.toArray(), totalCount]);

            return API.v1.success({
                members,
                count: members.length,
                offset: skip,
                total,
            });
        },
    },
);

API.v1.addRoute(
    'channels.online',
    { authRequired: true },
    {
        async get() {
            const { query } = await this.parseJsonQuery();
            if (!query || Object.keys(query).length === 0) {
                return API.v1.failure('Invalid query');
            }

            const ourQuery = Object.assign({}, query, { t: 'c' });

            const room = await Rooms.findOne(ourQuery as Record<string, any>);
            if (!room) {
                return API.v1.failure('Channel does not exists');
            }

            const user = await getLoggedInUser(this.request);

            if (!room || !user || !(await canAccessRoomAsync(room, user))) {
                throw new Meteor.Error('error-not-allowed', 'Not Allowed');
            }

            const online: Pick<IUser, '_id' | 'username'>[] = await Users.findUsersNotOffline({
                projection: { username: 1 },
            }).toArray();

            const onlineInRoom = await Promise.all(
                online.map(async (user) => {
                    const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, {
                        projection: { _id: 1, username: 1 },
                    });
                    if (subscription) {
                        return {
                            _id: user._id,
                            username: user.username,
                        };
                    }
                }),
            );

            return API.v1.success({
                online: onlineInRoom.filter(Boolean) as IUser[],
            });
        },
    },
);

API.v1.addRoute(
    'channels.removeModerator',
    { authRequired: true },
    {
        async post() {
            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            const user = await getUserFromParams(this.bodyParams);

            await Meteor.callAsync('removeRoomModerator', findResult._id, user._id);

            return API.v1.success();
        },
    },
);

API.v1.addRoute(
    'channels.removeOwner',
    { authRequired: true },
    {
        async post() {
            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            const user = await getUserFromParams(this.bodyParams);

            await Meteor.callAsync('removeRoomOwner', findResult._id, user._id);

            return API.v1.success();
        },
    },
);

API.v1.addRoute(
    'channels.rename',
    { authRequired: true },
    {
        async post() {
            if (!this.bodyParams.name?.trim()) {
                return API.v1.failure('The bodyParam "name" is required');
            }

            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            if (findResult.name === this.bodyParams.name) {
                return API.v1.failure('The channel name is the same as what it would be renamed to.');
            }

            await saveRoomSettings(this.userId, findResult._id, 'roomName', this.bodyParams.name);

            return API.v1.success({
                channel: await findChannelByIdOrName({
                    params: this.bodyParams,
                    userId: this.userId,
                }),
            });
        },
    },
);

API.v1.addRoute(
    'channels.setCustomFields',
    { authRequired: true },
    {
        async post() {
            if (!this.bodyParams.customFields || !(typeof this.bodyParams.customFields === 'object')) {
                return API.v1.failure('The bodyParam "customFields" is required with a type like object.');
            }

            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            await saveRoomSettings(this.userId, findResult._id, 'roomCustomFields', this.bodyParams.customFields);

            return API.v1.success({
                channel: await findChannelByIdOrName({ params: this.bodyParams, userId: this.userId }),
            });
        },
    },
);

API.v1.addRoute(
    'channels.setDefault',
    { authRequired: true },
    {
        async post() {
            if (typeof this.bodyParams.default === 'undefined') {
                return API.v1.failure('The bodyParam "default" is required', 'error-channels-setdefault-is-same');
            }

            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            if (findResult.default === this.bodyParams.default) {
                return API.v1.failure(
                    'The channel default setting is the same as what it would be changed to.',
                    'error-channels-setdefault-missing-default-param',
                );
            }

            await saveRoomSettings(
                this.userId,
                findResult._id,
                'default',
                ['true', '1'].includes(this.bodyParams.default.toString().toLowerCase()),
            );

            return API.v1.success({
                channel: await findChannelByIdOrName({ params: this.bodyParams, userId: this.userId }),
            });
        },
    },
);

API.v1.addRoute(
    'channels.setDescription',
    { authRequired: true },
    {
        async post() {
            if (!this.bodyParams.hasOwnProperty('description')) {
                return API.v1.failure('The bodyParam "description" is required');
            }

            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            if (findResult.description === this.bodyParams.description) {
                return API.v1.failure('The channel description is the same as what it would be changed to.');
            }

            await saveRoomSettings(this.userId, findResult._id, 'roomDescription', this.bodyParams.description || '');

            return API.v1.success({
                description: this.bodyParams.description || '',
            });
        },
    },
);

API.v1.addRoute(
    'channels.setPurpose',
    { authRequired: true },
    {
        async post() {
            if (!this.bodyParams.hasOwnProperty('purpose')) {
                return API.v1.failure('The bodyParam "purpose" is required');
            }

            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            if (findResult.description === this.bodyParams.purpose) {
                return API.v1.failure('The channel purpose (description) is the same as what it would be changed to.');
            }

            await saveRoomSettings(this.userId, findResult._id, 'roomDescription', this.bodyParams.purpose || '');

            return API.v1.success({
                purpose: this.bodyParams.purpose || '',
            });
        },
    },
);

API.v1.addRoute(
    'channels.setTopic',
    { authRequired: true },
    {
        async post() {
            if (!this.bodyParams.hasOwnProperty('topic')) {
                return API.v1.failure('The bodyParam "topic" is required');
            }

            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            if (findResult.topic === this.bodyParams.topic) {
                return API.v1.failure('The channel topic is the same as what it would be changed to.');
            }

            await saveRoomSettings(this.userId, findResult._id, 'roomTopic', this.bodyParams.topic || '');

            return API.v1.success({
                topic: this.bodyParams.topic || '',
            });
        },
    },
);

API.v1.addRoute(
    'channels.setType',
    { authRequired: true },
    {
        async post() {
            if (!this.bodyParams.type?.trim()) {
                return API.v1.failure('The bodyParam "type" is required');
            }

            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            if (findResult.t === this.bodyParams.type) {
                return API.v1.failure('The channel type is the same as what it would be changed to.');
            }

            await saveRoomSettings(this.userId, findResult._id, 'roomType', this.bodyParams.type as RoomType);

            const room = await Rooms.findOneById(findResult._id, { projection: API.v1.defaultFieldsToExclude });

            if (!room) {
                return API.v1.failure('The channel does not exist');
            }

            return API.v1.success({
                channel: await composeRoomWithLastMessage(room, this.userId),
            });
        },
    },
);

API.v1.addRoute(
    'channels.addLeader',
    { authRequired: true },
    {
        async post() {
            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            const user = await getUserFromParams(this.bodyParams);

            await Meteor.callAsync('addRoomLeader', findResult._id, user._id);

            return API.v1.success();
        },
    },
);

API.v1.addRoute(
    'channels.removeLeader',
    { authRequired: true },
    {
        async post() {
            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            const user = await getUserFromParams(this.bodyParams);

            await Meteor.callAsync('removeRoomLeader', findResult._id, user._id);

            return API.v1.success();
        },
    },
);

API.v1.addRoute(
    'channels.setJoinCode',
    { authRequired: true },
    {
        async post() {
            if (!this.bodyParams.joinCode?.trim()) {
                return API.v1.failure('The bodyParam "joinCode" is required');
            }

            const findResult = await findChannelByIdOrName({ params: this.bodyParams });

            await saveRoomSettings(this.userId, findResult._id, 'joinCode', this.bodyParams.joinCode);

            return API.v1.success({
                channel: await findChannelByIdOrName({ params: this.bodyParams, userId: this.userId }),
            });
        },
    },
);

API.v1.addRoute(
    'channels.anonymousread',
    { authRequired: false },
    {
        async get() {
            const findResult = await findChannelByIdOrName({
                params: this.queryParams,
                checkedArchived: false,
            });
            const { offset, count } = await getPaginationItems(this.queryParams);
            const { sort, fields, query } = await this.parseJsonQuery();

            const ourQuery = Object.assign({}, query, { rid: findResult._id });

            if (!settings.get<boolean>('Accounts_AllowAnonymousRead')) {
                throw new Meteor.Error('error-not-allowed', 'Enable "Allow Anonymous Read"', {
                    method: 'channels.anonymousread',
                });
            }

            const { cursor, totalCount } = await Messages.findPaginated(ourQuery, {
                sort: sort || { ts: -1 },
                skip: offset,
                limit: count,
                projection: fields,
            });

            const [messages, total] = await Promise.all([cursor.toArray(), totalCount]);

            return API.v1.success({
                messages: await normalizeMessagesForUser(messages, this.userId || ''),
                count: messages.length,
                offset,
                total,
            });
        },
    },
);