[BREAK] Remove legacy FB Messenger integration (#27760)
Co-authored-by: Kevin Aleman <11577696+KevLehman@users.noreply.github.com>pull/27898/head^2
parent
ebbb8471a8
commit
344809bda0
@ -1,119 +0,0 @@ |
||||
import crypto from 'crypto'; |
||||
|
||||
import { isPOSTLivechatFacebookParams } from '@rocket.chat/rest-typings'; |
||||
import { Random } from 'meteor/random'; |
||||
import { LivechatVisitors } from '@rocket.chat/models'; |
||||
import type { ILivechatVisitor } from '@rocket.chat/core-typings'; |
||||
|
||||
import { API } from '../../../../api/server'; |
||||
import { LivechatRooms } from '../../../../models/server'; |
||||
import { settings } from '../../../../settings/server'; |
||||
import { Livechat } from '../../../server/lib/Livechat'; |
||||
|
||||
type SentMessage = { |
||||
message: { |
||||
_id: string; |
||||
rid?: string; |
||||
token?: string; |
||||
msg?: string; |
||||
}; |
||||
roomInfo: { |
||||
facebook: { |
||||
page: string; |
||||
}; |
||||
}; |
||||
guest?: ILivechatVisitor | null; |
||||
}; |
||||
|
||||
/** |
||||
* @api {post} /livechat/facebook Send Facebook message |
||||
* @apiName Facebook |
||||
* @apiGroup Livechat |
||||
* |
||||
* @apiParam {String} mid Facebook message id |
||||
* @apiParam {String} page Facebook pages id |
||||
* @apiParam {String} token Facebook user's token |
||||
* @apiParam {String} first_name Facebook user's first name |
||||
* @apiParam {String} last_name Facebook user's last name |
||||
* @apiParam {String} [text] Facebook message text |
||||
* @apiParam {String} [attachments] Facebook message attachments |
||||
*/ |
||||
API.v1.addRoute( |
||||
'livechat/facebook', |
||||
{ validateParams: isPOSTLivechatFacebookParams }, |
||||
{ |
||||
async post() { |
||||
if (!this.bodyParams.text && !this.bodyParams.attachments) { |
||||
return API.v1.failure('Invalid request'); |
||||
} |
||||
|
||||
if (!this.request.headers['x-hub-signature']) { |
||||
return API.v1.unauthorized(); |
||||
} |
||||
|
||||
if (!settings.get<boolean>('Livechat_Facebook_Enabled')) { |
||||
return API.v1.failure('Facebook integration is disabled'); |
||||
} |
||||
|
||||
// validate if request come from omni
|
||||
const signature = crypto |
||||
.createHmac('sha1', settings.get<string>('Livechat_Facebook_API_Secret')) |
||||
.update(JSON.stringify(this.request.body)) |
||||
.digest('hex'); |
||||
if (this.request.headers['x-hub-signature'] !== `sha1=${signature}`) { |
||||
return API.v1.unauthorized(); |
||||
} |
||||
|
||||
const sendMessage: SentMessage = { |
||||
message: { |
||||
_id: this.bodyParams.mid, |
||||
msg: this.bodyParams.text, |
||||
}, |
||||
roomInfo: { |
||||
facebook: { |
||||
page: this.bodyParams.page, |
||||
}, |
||||
}, |
||||
}; |
||||
let visitor = await LivechatVisitors.getVisitorByToken(this.bodyParams.token, {}); |
||||
if (visitor) { |
||||
const rooms = LivechatRooms.findOpenByVisitorToken(visitor.token).fetch(); |
||||
if (rooms && rooms.length > 0) { |
||||
sendMessage.message.rid = rooms[0]._id; |
||||
} else { |
||||
sendMessage.message.rid = Random.id(); |
||||
} |
||||
sendMessage.message.token = visitor.token; |
||||
} else { |
||||
sendMessage.message.rid = Random.id(); |
||||
sendMessage.message.token = this.bodyParams.token; |
||||
|
||||
const userId = await Livechat.registerGuest({ |
||||
token: sendMessage.message.token, |
||||
name: `${this.bodyParams.first_name} ${this.bodyParams.last_name}`, |
||||
// TODO: type livechat big file :(
|
||||
id: undefined, |
||||
email: undefined, |
||||
phone: undefined, |
||||
department: undefined, |
||||
username: undefined, |
||||
connectionData: undefined, |
||||
}); |
||||
|
||||
visitor = await LivechatVisitors.findOneById(userId); |
||||
} |
||||
|
||||
sendMessage.guest = visitor; |
||||
|
||||
try { |
||||
return API.v1.success({ |
||||
// @ts-expect-error - Typings on Livechat.sendMessage are wrong
|
||||
message: await Livechat.sendMessage(sendMessage), |
||||
}); |
||||
} catch (err) { |
||||
Livechat.logger.error({ msg: 'Error using Facebook ->', err }); |
||||
return API.v1.failure(err); |
||||
} |
||||
}, |
||||
}, |
||||
); |
||||
@ -1,49 +0,0 @@ |
||||
import { isOmnichannelRoom } from '@rocket.chat/core-typings'; |
||||
|
||||
import { callbacks } from '../../../../lib/callbacks'; |
||||
import { settings } from '../../../settings/server'; |
||||
import OmniChannel from '../lib/OmniChannel'; |
||||
import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload'; |
||||
|
||||
callbacks.add( |
||||
'afterSaveMessage', |
||||
function (message, room) { |
||||
// skips this callback if the message was edited
|
||||
if (message.editedAt) { |
||||
return message; |
||||
} |
||||
|
||||
// only send the sms by SMS if it is a livechat room with SMS set to true
|
||||
if (!(isOmnichannelRoom(room) && room.facebook && room.v && room.v.token)) { |
||||
return message; |
||||
} |
||||
|
||||
if (!settings.get('Livechat_Facebook_Enabled') || !settings.get('Livechat_Facebook_API_Key')) { |
||||
return message; |
||||
} |
||||
|
||||
// if the message has a token, it was sent from the visitor, so ignore it
|
||||
if (message.token) { |
||||
return message; |
||||
} |
||||
|
||||
// if the message has a type means it is a special message (like the closing comment), so skips
|
||||
if (message.t) { |
||||
return message; |
||||
} |
||||
|
||||
if (message.file) { |
||||
message = Promise.await(normalizeMessageFileUpload(message)); |
||||
} |
||||
|
||||
OmniChannel.reply({ |
||||
page: room.facebook.page.id, |
||||
token: room.v.token, |
||||
text: message.msg, |
||||
}); |
||||
|
||||
return message; |
||||
}, |
||||
callbacks.priority.LOW, |
||||
'sendMessageToFacebook', |
||||
); |
||||
@ -1,70 +0,0 @@ |
||||
import { HTTP } from 'meteor/http'; |
||||
|
||||
import { settings } from '../../../settings/server'; |
||||
|
||||
const gatewayURL = 'https://omni.rocket.chat'; |
||||
|
||||
export default { |
||||
enable() { |
||||
const result = HTTP.call('POST', `${gatewayURL}/facebook/enable`, { |
||||
headers: { |
||||
'authorization': `Bearer ${settings.get('Livechat_Facebook_API_Key')}`, |
||||
'content-type': 'application/json', |
||||
}, |
||||
data: { |
||||
url: settings.get('Site_Url'), |
||||
}, |
||||
}); |
||||
return result.data; |
||||
}, |
||||
|
||||
disable() { |
||||
const result = HTTP.call('DELETE', `${gatewayURL}/facebook/enable`, { |
||||
headers: { |
||||
'authorization': `Bearer ${settings.get('Livechat_Facebook_API_Key')}`, |
||||
'content-type': 'application/json', |
||||
}, |
||||
}); |
||||
return result.data; |
||||
}, |
||||
|
||||
listPages() { |
||||
const result = HTTP.call('GET', `${gatewayURL}/facebook/pages`, { |
||||
headers: { |
||||
authorization: `Bearer ${settings.get('Livechat_Facebook_API_Key')}`, |
||||
}, |
||||
}); |
||||
return result.data; |
||||
}, |
||||
|
||||
subscribe(pageId) { |
||||
const result = HTTP.call('POST', `${gatewayURL}/facebook/page/${pageId}/subscribe`, { |
||||
headers: { |
||||
authorization: `Bearer ${settings.get('Livechat_Facebook_API_Key')}`, |
||||
}, |
||||
}); |
||||
return result.data; |
||||
}, |
||||
|
||||
unsubscribe(pageId) { |
||||
const result = HTTP.call('DELETE', `${gatewayURL}/facebook/page/${pageId}/subscribe`, { |
||||
headers: { |
||||
authorization: `Bearer ${settings.get('Livechat_Facebook_API_Key')}`, |
||||
}, |
||||
}); |
||||
return result.data; |
||||
}, |
||||
|
||||
reply({ page, token, text }) { |
||||
return HTTP.call('POST', `${gatewayURL}/facebook/reply`, { |
||||
headers: { |
||||
authorization: `Bearer ${settings.get('Livechat_Facebook_API_Key')}`, |
||||
}, |
||||
data: { |
||||
page, |
||||
token, |
||||
text, |
||||
}, |
||||
}); |
||||
}, |
||||
}; |
||||
@ -1,68 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Settings } from '@rocket.chat/models'; |
||||
|
||||
import { hasPermission } from '../../../authorization'; |
||||
import { SystemLogger } from '../../../../server/lib/logger/system'; |
||||
import { settings } from '../../../settings/server'; |
||||
import OmniChannel from '../lib/OmniChannel'; |
||||
|
||||
Meteor.methods({ |
||||
'livechat:facebook'(options) { |
||||
if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) { |
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:addAgent' }); |
||||
} |
||||
|
||||
try { |
||||
switch (options.action) { |
||||
case 'initialState': { |
||||
return { |
||||
enabled: settings.get('Livechat_Facebook_Enabled'), |
||||
hasToken: !!settings.get('Livechat_Facebook_API_Key'), |
||||
}; |
||||
} |
||||
|
||||
case 'enable': { |
||||
const result = OmniChannel.enable(); |
||||
|
||||
if (!result.success) { |
||||
return result; |
||||
} |
||||
|
||||
return Settings.updateValueById('Livechat_Facebook_Enabled', true); |
||||
} |
||||
|
||||
case 'disable': { |
||||
OmniChannel.disable(); |
||||
|
||||
return Settings.updateValueById('Livechat_Facebook_Enabled', false); |
||||
} |
||||
|
||||
case 'list-pages': { |
||||
return OmniChannel.listPages(); |
||||
} |
||||
|
||||
case 'subscribe': { |
||||
return OmniChannel.subscribe(options.page); |
||||
} |
||||
|
||||
case 'unsubscribe': { |
||||
return OmniChannel.unsubscribe(options.page); |
||||
} |
||||
} |
||||
} catch (err) { |
||||
if (err.response && err.response.data && err.response.data.error) { |
||||
if (err.response.data.error.error) { |
||||
throw new Meteor.Error(err.response.data.error.error, err.response.data.error.message); |
||||
} |
||||
if (err.response.data.error.response) { |
||||
throw new Meteor.Error('integration-error', err.response.data.error.response.error.message); |
||||
} |
||||
if (err.response.data.error.message) { |
||||
throw new Meteor.Error('integration-error', err.response.data.error.message); |
||||
} |
||||
} |
||||
SystemLogger.error({ msg: 'Error contacting omni.rocket.chat:', err }); |
||||
throw new Meteor.Error('integration-error', err.error); |
||||
} |
||||
}, |
||||
}); |
||||
@ -1,80 +0,0 @@ |
||||
import { Box, Button, ButtonGroup, FieldGroup, Divider } from '@rocket.chat/fuselage'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import type { FC, Dispatch } from 'react'; |
||||
import React from 'react'; |
||||
|
||||
import Page from '../../../components/Page'; |
||||
import PageToggleAssembler from './PageToggleAssembler'; |
||||
|
||||
type OnToggleProps = { |
||||
onToggle: (id: string, isSubscribed: boolean, setSubscribed: Dispatch<boolean>) => void; |
||||
}; |
||||
|
||||
type PageItem = { |
||||
name: string; |
||||
subscribed: boolean; |
||||
id: string; |
||||
}; |
||||
|
||||
type FacebookPageProps = OnToggleProps & { |
||||
enabled: boolean; |
||||
hasToken: boolean; |
||||
pages: PageItem[]; |
||||
onRefresh: () => void; |
||||
onDisable: () => void; |
||||
onEnable: () => void; |
||||
}; |
||||
|
||||
const FacebookPage: FC<FacebookPageProps> = ({ pages, enabled, hasToken, onToggle, onRefresh, onEnable, onDisable }) => { |
||||
const t = useTranslation(); |
||||
|
||||
return ( |
||||
<Page> |
||||
<Page.Header title={t('Facebook')} /> |
||||
<Page.ScrollableContentWithShadow> |
||||
<Box maxWidth='x600' w='full' alignSelf='center'> |
||||
{!enabled && ( |
||||
<> |
||||
<ButtonGroup stretch mb='x8'> |
||||
<Button primary onClick={onEnable} disabled={!hasToken}> |
||||
{t('Enable')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
{!hasToken && ( |
||||
<> |
||||
<p>{t('You_have_to_set_an_API_token_first_in_order_to_use_the_integration')}</p> |
||||
<p>{t('Please_go_to_the_Administration_page_then_Livechat_Facebook')}</p> |
||||
</> |
||||
)} |
||||
</> |
||||
)} |
||||
{enabled && ( |
||||
<> |
||||
<Box fontScale='h2' mbe='x8'> |
||||
{t('Pages')} |
||||
</Box> |
||||
{pages?.length ? ( |
||||
<FieldGroup> |
||||
<PageToggleAssembler pages={pages} onToggle={onToggle} /> |
||||
</FieldGroup> |
||||
) : ( |
||||
t('No_pages_yet_Try_hitting_Reload_Pages_button') |
||||
)} |
||||
<Box w='full' mb='x16'> |
||||
<Divider /> |
||||
</Box> |
||||
<ButtonGroup stretch vertical> |
||||
<Button onClick={onRefresh}>{t('Reload_Pages')}</Button> |
||||
<Button secondary danger onClick={onDisable}> |
||||
{t('Disable')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</> |
||||
)} |
||||
</Box> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page> |
||||
); |
||||
}; |
||||
|
||||
export default FacebookPage; |
||||
@ -1,135 +0,0 @@ |
||||
import { Callout } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import { useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import { useQuery } from '@tanstack/react-query'; |
||||
import type { ReactElement } from 'react'; |
||||
import React from 'react'; |
||||
|
||||
import Page from '../../../components/Page'; |
||||
import PageSkeleton from '../../../components/PageSkeleton'; |
||||
import FacebookPage from './FacebookPage'; |
||||
|
||||
type PageItem = { |
||||
name: string; |
||||
subscribed: boolean; |
||||
id: string; |
||||
}; |
||||
|
||||
type PageData = { |
||||
pages: PageItem[]; |
||||
}; |
||||
|
||||
type InitialStateData = { |
||||
enabled: boolean; |
||||
hasToken: boolean; |
||||
}; |
||||
|
||||
const FacebookPageContainer = (): ReactElement => { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const livechatFacebook = useMethod('livechat:facebook'); |
||||
|
||||
const initialStateResult = useQuery( |
||||
['omnichannel/facebook/initial-state'], |
||||
async () => livechatFacebook({ action: 'initialState' }) as unknown as Promise<InitialStateData>, |
||||
{ |
||||
initialData: { enabled: false, hasToken: false }, |
||||
}, |
||||
); |
||||
|
||||
const listPagesResult = useQuery( |
||||
['omnichannel/facebook/list-pages'], |
||||
async () => livechatFacebook({ action: 'list-pages' }) as unknown as Promise<PageData>, |
||||
{ |
||||
initialData: { pages: [] }, |
||||
}, |
||||
); |
||||
|
||||
const { enabled, hasToken } = initialStateResult.data ?? { enabled: false, hasToken: false }; |
||||
const { pages } = listPagesResult.data ?? { pages: [] }; |
||||
|
||||
const onToggle = useMutableCallback(async (id, isSubscribed, setSubscribed) => { |
||||
setSubscribed(!isSubscribed); |
||||
try { |
||||
const action = isSubscribed ? 'unsubscribe' : 'subscribe'; |
||||
await livechatFacebook({ |
||||
action, |
||||
page: id, |
||||
}); |
||||
} catch (error: unknown) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
setSubscribed(isSubscribed); |
||||
} |
||||
}); |
||||
|
||||
const onDisable = useMutableCallback(async () => { |
||||
try { |
||||
await livechatFacebook({ action: 'disable' }); |
||||
dispatchToastMessage({ type: 'success', message: t('Integration_disabled') }); |
||||
initialStateResult.refetch(); |
||||
listPagesResult.refetch(); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
const openOauthWindow = (url: string, callback: () => void): void => { |
||||
const oauthWindow = window.open(url, 'facebook-integration-oauth', 'width=600,height=400'); |
||||
const checkInterval = setInterval(() => { |
||||
if (oauthWindow?.closed) { |
||||
clearInterval(checkInterval); |
||||
callback(); |
||||
} |
||||
}, 300); |
||||
}; |
||||
|
||||
const onEnable = useMutableCallback(async () => { |
||||
try { |
||||
const result = await livechatFacebook({ action: 'enable' }); |
||||
if (result && 'url' in result) { |
||||
openOauthWindow(result.url, () => { |
||||
onEnable(); |
||||
}); |
||||
} else { |
||||
initialStateResult.refetch(); |
||||
listPagesResult.refetch(); |
||||
} |
||||
} catch (error: unknown) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
if (initialStateResult.isLoading || listPagesResult.isLoading) { |
||||
return <PageSkeleton />; |
||||
} |
||||
|
||||
if (initialStateResult.isError) { |
||||
return ( |
||||
<Page> |
||||
<Page.Header title={t('Edit_Custom_Field')} /> |
||||
<Page.ScrollableContentWithShadow> |
||||
<Callout type='danger'>{t('Error')}</Callout> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page> |
||||
); |
||||
} |
||||
|
||||
if (enabled && hasToken && listPagesResult.isError) { |
||||
onEnable(); |
||||
} |
||||
|
||||
return ( |
||||
<FacebookPage |
||||
pages={pages} |
||||
enabled={enabled} |
||||
hasToken={hasToken} |
||||
onToggle={onToggle} |
||||
onRefresh={listPagesResult.refetch} |
||||
onDisable={onDisable} |
||||
onEnable={onEnable} |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export default FacebookPageContainer; |
||||
@ -1,36 +0,0 @@ |
||||
import { Box, Field, ToggleSwitch } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import type { FC, Dispatch, ComponentProps } from 'react'; |
||||
import React, { useState } from 'react'; |
||||
|
||||
type OnToggleProps = { |
||||
onToggle: (id: string, isSubscribed: boolean, setSubscribed: Dispatch<boolean>) => void; |
||||
}; |
||||
|
||||
type PageItem = { |
||||
name: string; |
||||
subscribed: boolean; |
||||
id: string; |
||||
}; |
||||
|
||||
type PageToggleProps = OnToggleProps & |
||||
PageItem & { |
||||
className?: ComponentProps<typeof Field>['className']; |
||||
}; |
||||
|
||||
const PageToggle: FC<PageToggleProps> = ({ name, id, subscribed, onToggle, className }) => { |
||||
const [isSubscribed, setIsSubscribed] = useState(subscribed); |
||||
const handleToggle = useMutableCallback(() => onToggle(id, isSubscribed, setIsSubscribed)); |
||||
return ( |
||||
<Field className={className}> |
||||
<Box display='flex' flexDirection='row'> |
||||
<Field.Label>{name}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch checked={isSubscribed} onChange={handleToggle} /> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
); |
||||
}; |
||||
|
||||
export default PageToggle; |
||||
@ -1,30 +0,0 @@ |
||||
import { FieldGroup } from '@rocket.chat/fuselage'; |
||||
import type { FC, Dispatch, ComponentProps } from 'react'; |
||||
import React from 'react'; |
||||
|
||||
import PageToggle from './PageToggle'; |
||||
|
||||
type OnToggleProps = { |
||||
onToggle: (id: string, isSubscribed: boolean, setSubscribed: Dispatch<boolean>) => void; |
||||
}; |
||||
|
||||
type PageItem = { |
||||
name: string; |
||||
subscribed: boolean; |
||||
id: string; |
||||
}; |
||||
|
||||
type PageToggleAssemblerProps = OnToggleProps & { |
||||
pages: PageItem[]; |
||||
className?: ComponentProps<typeof PageToggle>['className']; |
||||
}; |
||||
|
||||
const PageToggleAssembler: FC<PageToggleAssemblerProps> = ({ pages, onToggle, className }) => ( |
||||
<FieldGroup> |
||||
{pages.map((page) => ( |
||||
<PageToggle key={page.id} {...page} onToggle={onToggle} className={className} /> |
||||
))} |
||||
</FieldGroup> |
||||
); |
||||
|
||||
export default PageToggleAssembler; |
||||
@ -0,0 +1,49 @@ |
||||
import { Settings, Permissions, LivechatRooms, LivechatInquiry, Subscriptions } from '@rocket.chat/models'; |
||||
|
||||
import { addMigration } from '../../lib/migrations'; |
||||
|
||||
addMigration({ |
||||
version: 283, |
||||
async up() { |
||||
// Removing all settings & permissions related to Legacy FB Messenger integration
|
||||
await Promise.all([ |
||||
Settings.deleteMany({ |
||||
_id: { |
||||
$in: ['Livechat_Facebook_Enabled', 'Livechat_Facebook_API_Key', 'Livechat_Facebook_API_Secret'], |
||||
}, |
||||
}), |
||||
Permissions.removeById('view-livechat-facebook'), |
||||
]); |
||||
|
||||
// close all open Fb Messenger rooms since the integration is no longer available
|
||||
const openRoomsIds = ( |
||||
await LivechatRooms.find( |
||||
{ |
||||
open: true, |
||||
facebook: { $exists: true }, |
||||
}, |
||||
{ projection: { _id: 1 } }, |
||||
).toArray() |
||||
).map((room) => room._id); |
||||
await Promise.all([ |
||||
LivechatRooms.updateMany( |
||||
{ |
||||
_id: { |
||||
$in: openRoomsIds, |
||||
}, |
||||
}, |
||||
{ |
||||
$unset: { |
||||
open: 1, |
||||
}, |
||||
}, |
||||
), |
||||
LivechatInquiry.deleteMany({ |
||||
rid: { |
||||
$in: openRoomsIds, |
||||
}, |
||||
}), |
||||
...openRoomsIds.map((room) => Subscriptions.removeByRoomId(room)), |
||||
]); |
||||
}, |
||||
}); |
||||
Loading…
Reference in new issue