MM-43904 - Fix: Calls: "Access to route for non-existent plugin" error log (#6210) (#6212)

* add isCallsPluginEnabled; refactor Calls.PluginId

* revert Podfile.lock changes

(cherry picked from commit 912287fbe0)

Co-authored-by: Christopher Poile <cpoile@gmail.com>
This commit is contained in:
Mattermost Build 2022-05-03 21:10:46 +02:00 committed by GitHub
parent b476fc00ff
commit 20d106b609
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 131 additions and 28 deletions

View file

@ -7,6 +7,7 @@
import {RNFetchBlobFetchRepsonse} from 'rn-fetch-blob';
import urlParse from 'url-parse';
import Calls from '@constants/calls';
import {Options} from '@mm-redux/types/client4';
import * as ClientConstants from './constants';
@ -286,12 +287,16 @@ export default class ClientBase {
return `${this.getUserThreadsRoute(userId, teamId)}/${threadId}`;
}
getPluginsRoute() {
return `${this.getBaseRoute()}/plugins`;
}
getAppsProxyRoute() {
return `${this.url}/plugins/com.mattermost.apps`;
}
getCallsRoute() {
return `${this.url}/plugins/com.mattermost.calls`;
return `${this.url}/plugins/${Calls.PluginId}`;
}
// Client Helpers

View file

@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import ClientPlugins, {ClientPluginsMix} from '@client/rest/plugins';
import ClientCalls, {ClientCallsMix} from '@mmproducts/calls/client/rest';
import mix from '@utils/mix';
@ -36,7 +37,8 @@ interface Client extends ClientBase,
ClientTeamsMix,
ClientTosMix,
ClientUsersMix,
ClientCallsMix
ClientCallsMix,
ClientPluginsMix
{}
class Client extends mix(ClientBase).with(
@ -55,6 +57,7 @@ class Client extends mix(ClientBase).with(
ClientTos,
ClientUsers,
ClientCalls,
ClientPlugins,
) {}
const Client4 = new Client();

View file

@ -0,0 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ClientPluginManifest} from '@mm-redux/types/plugins';
export interface ClientPluginsMix {
getPluginsManifests: () => Promise<ClientPluginManifest[]>;
}
const ClientPlugins = (superclass: any) => class extends superclass {
getPluginsManifests = async () => {
return this.doFetch(
`${this.getPluginsRoute()}/webapp`,
{method: 'get'},
);
};
};
export default ClientPlugins;

View file

@ -10,4 +10,6 @@ const RequiredServer = {
PATCH_VERSION: 0,
};
export default {RequiredServer, RefreshConfigMillis};
const PluginId = 'com.mattermost.calls';
export default {RequiredServer, RefreshConfigMillis, PluginId};

View file

@ -1,5 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Calls from '@constants/calls';
const WebsocketEvents = {
POSTED: 'posted',
POST_EDITED: 'post_edited',
@ -53,18 +56,18 @@ const WebsocketEvents = {
SIDEBAR_CATEGORY_UPDATED: 'sidebar_category_updated',
SIDEBAR_CATEGORY_DELETED: 'sidebar_category_deleted',
SIDEBAR_CATEGORY_ORDER_UPDATED: 'sidebar_category_order_updated',
CALLS_CHANNEL_ENABLED: 'custom_com.mattermost.calls_channel_enable_voice',
CALLS_CHANNEL_DISABLED: 'custom_com.mattermost.calls_channel_disable_voice',
CALLS_USER_CONNECTED: 'custom_com.mattermost.calls_user_connected',
CALLS_USER_DISCONNECTED: 'custom_com.mattermost.calls_user_disconnected',
CALLS_USER_MUTED: 'custom_com.mattermost.calls_user_muted',
CALLS_USER_UNMUTED: 'custom_com.mattermost.calls_user_unmuted',
CALLS_USER_VOICE_ON: 'custom_com.mattermost.calls_user_voice_on',
CALLS_USER_VOICE_OFF: 'custom_com.mattermost.calls_user_voice_off',
CALLS_CALL_START: 'custom_com.mattermost.calls_call_start',
CALLS_SCREEN_ON: 'custom_com.mattermost.calls_user_screen_on',
CALLS_SCREEN_OFF: 'custom_com.mattermost.calls_user_screen_off',
CALLS_USER_RAISE_HAND: 'custom_com.mattermost.calls_user_raise_hand',
CALLS_USER_UNRAISE_HAND: 'custom_com.mattermost.calls_user_unraise_hand',
CALLS_CHANNEL_ENABLED: `custom_${Calls.PluginId}_channel_enable_voice`,
CALLS_CHANNEL_DISABLED: `custom_${Calls.PluginId}_channel_disable_voice`,
CALLS_USER_CONNECTED: `custom_${Calls.PluginId}_user_connected`,
CALLS_USER_DISCONNECTED: `custom_${Calls.PluginId}_user_disconnected`,
CALLS_USER_MUTED: `custom_${Calls.PluginId}_user_muted`,
CALLS_USER_UNMUTED: `custom_${Calls.PluginId}_user_unmuted`,
CALLS_USER_VOICE_ON: `custom_${Calls.PluginId}_user_voice_on`,
CALLS_USER_VOICE_OFF: `custom_${Calls.PluginId}_user_voice_off`,
CALLS_CALL_START: `custom_${Calls.PluginId}_call_start`,
CALLS_SCREEN_ON: `custom_${Calls.PluginId}_user_screen_on`,
CALLS_SCREEN_OFF: `custom_${Calls.PluginId}_user_screen_off`,
CALLS_USER_RAISE_HAND: `custom_${Calls.PluginId}_user_raise_hand`,
CALLS_USER_UNRAISE_HAND: `custom_${Calls.PluginId}_user_unraise_hand`,
};
export default WebsocketEvents;

View file

@ -11,7 +11,12 @@ import {getCurrentChannel} from '@mm-redux/selectors/entities/channels';
import {getCurrentUserRoles} from '@mm-redux/selectors/entities/users';
import {isAdmin as checkIsAdmin, isChannelAdmin as checkIsChannelAdmin} from '@mm-redux/utils/user_utils';
import {loadConfig} from '@mmproducts/calls/store/actions/calls';
import {getConfig, isCallsExplicitlyDisabled, isCallsExplicitlyEnabled} from '@mmproducts/calls/store/selectors/calls';
import {
getConfig,
isCallsExplicitlyDisabled,
isCallsExplicitlyEnabled,
isCallsPluginEnabled,
} from '@mmproducts/calls/store/selectors/calls';
// Check if calls is enabled. If it is, then run fn; if it isn't, show an alert and set
// msgPostfix to ' (Not Available)'.
@ -45,12 +50,15 @@ export const useCallsChannelSettings = () => {
const dispatch = useDispatch();
const config = useSelector(getConfig);
const currentChannel = useSelector(getCurrentChannel);
const pluginEnabled = useSelector(isCallsPluginEnabled);
const explicitlyDisabled = useSelector(isCallsExplicitlyDisabled);
const explicitlyEnabled = useSelector(isCallsExplicitlyEnabled);
const roles = useSelector(getCurrentUserRoles);
useEffect(() => {
dispatch(loadConfig());
if (pluginEnabled) {
dispatch(loadConfig());
}
}, []);
const isDirectMessage = currentChannel.type === General.DM_CHANNEL;
@ -58,9 +66,11 @@ export const useCallsChannelSettings = () => {
const isAdmin = checkIsAdmin(roles);
const isChannelAdmin = isAdmin || checkIsChannelAdmin(roles);
const enabled = (explicitlyEnabled || (!explicitlyDisabled && config.DefaultEnabled));
const enabled = pluginEnabled && (explicitlyEnabled || (!explicitlyDisabled && config.DefaultEnabled));
let canEnableDisable;
if (config.AllowEnableCalls) {
if (!pluginEnabled) {
canEnableDisable = false;
} else if (config.AllowEnableCalls) {
canEnableDisable = isDirectMessage || isGroupMessage || isChannelAdmin;
} else {
canEnableDisable = isAdmin;

View file

@ -24,4 +24,5 @@ export default keyMirror({
SET_SCREENSHARE_URL: null,
SET_SPEAKERPHONE: null,
RECEIVED_CONFIG: null,
RECEIVED_PLUGIN_ENABLED: null,
});

View file

@ -39,6 +39,12 @@ jest.mock('@client/rest', () => ({
DefaultEnabled: true,
last_retrieved_at: 1234,
})),
getPluginsManifests: jest.fn(() => (
[
{id: 'playbooks'},
{id: 'com.mattermost.calls'},
]
)),
enableChannelCalls: jest.fn(() => null),
disableChannelCalls: jest.fn(() => null),
},
@ -150,6 +156,7 @@ describe('Actions.Calls', () => {
it('batchLoadConfig', async () => {
await store.dispatch(CallsActions.batchLoadCalls());
expect(Client4.getPluginsManifests).toBeCalledWith();
expect(Client4.getCallsConfig).toBeCalledWith();
expect(Client4.getCalls).toBeCalledWith();

View file

@ -92,6 +92,12 @@ export function loadCalls(): ActionFunc {
export function batchLoadCalls(forceConfig = false): ActionFunc {
return async (dispatch: DispatchFunc) => {
const res = await dispatch(checkIsCallsPluginEnabled());
if (!res.data) {
// Calls is not enabled.
return {};
}
const promises = [dispatch(loadConfig(forceConfig)), dispatch(loadCalls())];
Promise.all(promises).then((actions: Array<Awaited<GenericAction>>) => {
dispatch(batchActions(actions, 'BATCH_LOAD_CALLS'));
@ -101,6 +107,24 @@ export function batchLoadCalls(forceConfig = false): ActionFunc {
};
}
export function checkIsCallsPluginEnabled(): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let data;
try {
data = await Client4.getPluginsManifests();
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {} as GenericAction;
}
const enabled = data.findIndex((m) => m.id === Calls.PluginId) !== -1;
dispatch({type: CallsTypes.RECEIVED_PLUGIN_ENABLED, data: enabled});
return {data: enabled};
};
}
export function enableChannelCalls(channelId: string): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
try {
@ -135,6 +159,10 @@ export function disableChannelCalls(channelId: string): ActionFunc {
export function joinCall(channelId: string, intl: typeof intlShape): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
// Edge case: calls was disabled when app loaded, and then enabled, but app hasn't
// reconnected its websocket since then (i.e., hasn't called batchLoadCalls yet)
dispatch(checkIsCallsPluginEnabled());
const hasPermission = await hasMicrophonePermission(intl);
if (!hasPermission) {
return {error: 'no permissions to microphone, unable to start call'};

View file

@ -216,6 +216,16 @@ function speakerphoneOn(state = false, action: GenericAction) {
}
}
function pluginEnabled(state = false, action: GenericAction) {
switch (action.type) {
case CallsTypes.RECEIVED_PLUGIN_ENABLED: {
return action.data;
}
default:
return state;
}
}
export default combineReducers({
calls,
enabled,
@ -223,4 +233,5 @@ export default combineReducers({
screenShareURL,
speakerphoneOn,
config,
pluginEnabled,
});

View file

@ -61,3 +61,7 @@ export function isSupportedServer(state: GlobalState) {
return false;
}
export function isCallsPluginEnabled(state: GlobalState) {
return state.entities.calls.pluginEnabled;
}

View file

@ -11,6 +11,7 @@ export type CallsState = {
screenShareURL: string;
speakerphoneOn: boolean;
config: ServerConfig;
pluginEnabled: boolean;
}
export type Call = {

View file

@ -2,13 +2,14 @@
// See LICENSE.txt for license information.
import {EventEmitter} from 'events';
import Calls from '@constants/calls';
import {encode} from '@msgpack/msgpack/dist';
export default class WebSocketClient extends EventEmitter {
private ws: WebSocket | null;
private seqNo = 0;
private connID = '';
private eventPrefix = 'custom_com.mattermost.calls';
private eventPrefix = `custom_${Calls.PluginId}`;
constructor(connURL: string) {
super();

View file

@ -64,7 +64,7 @@ export default class ChannelAndroid extends ChannelBase {
}
render() {
const {theme, viewingGlobalThreads, isSupportedServerCalls} = this.props;
const {theme, viewingGlobalThreads, isCallsEnabled} = this.props;
let component;
if (viewingGlobalThreads) {
@ -106,11 +106,12 @@ export default class ChannelAndroid extends ChannelBase {
{component}
<NetworkIndicator/>
<AnnouncementBanner/>
{isSupportedServerCalls &&
{isCallsEnabled &&
<FloatingCallContainer>
<JoinCall/>
<CurrentCall/>
</FloatingCallContainer>}
</FloatingCallContainer>
}
</>
);

View file

@ -57,7 +57,7 @@ export default class ChannelIOS extends ChannelBase {
};
render() {
const {currentChannelId, theme, viewingGlobalThreads, isSupportedServerCalls} = this.props;
const {currentChannelId, theme, viewingGlobalThreads, isCallsEnabled} = this.props;
let component;
let renderDraftArea = false;
@ -85,11 +85,12 @@ export default class ChannelIOS extends ChannelBase {
<>
<AnnouncementBanner/>
<NetworkIndicator/>
{isSupportedServerCalls &&
{isCallsEnabled &&
<FloatingCallContainer>
<JoinCall/>
<CurrentCall/>
</FloatingCallContainer>}
</FloatingCallContainer>
}
</>
);
const header = (

View file

@ -40,6 +40,7 @@ export default class ChannelBase extends PureComponent {
viewingGlobalThreads: PropTypes.bool,
collapsedThreadsEnabled: PropTypes.bool.isRequired,
isSupportedServerCalls: PropTypes.bool.isRequired,
isCallsEnabled: PropTypes.bool.isRequired,
selectedPost: PropTypes.shape({
id: PropTypes.string.isRequired,
channel_id: PropTypes.string.isRequired,

View file

@ -18,7 +18,10 @@ import {getCurrentUserId, getCurrentUserRoles, shouldShowTermsOfService} from '@
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
import {isSystemAdmin as checkIsSystemAdmin} from '@mm-redux/utils/user_utils';
import {batchLoadCalls} from '@mmproducts/calls/store/actions/calls';
import {isSupportedServer as isSupportedServerForCalls} from '@mmproducts/calls/store/selectors/calls';
import {
isCallsPluginEnabled,
isSupportedServer as isSupportedServerForCalls,
} from '@mmproducts/calls/store/selectors/calls';
import {getViewingGlobalThreads} from '@selectors/threads';
import Channel from './channel';
@ -44,6 +47,7 @@ function mapStateToProps(state) {
const currentChannelId = currentTeam?.delete_at === 0 ? getCurrentChannelId(state) : '';
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
const isSupportedServerCalls = isSupportedServerForCalls(state);
const isCallsEnabled = isCallsPluginEnabled(state);
return {
currentChannelId,
@ -58,6 +62,7 @@ function mapStateToProps(state) {
theme: getTheme(state),
viewingGlobalThreads: collapsedThreadsEnabled && getViewingGlobalThreads(state),
isSupportedServerCalls,
isCallsEnabled,
};
}