feat: 声网插件webhhok处理与消息通知方式

顺便修复了一些小的样式问题
pull/64/head
moonrailgun 3 years ago
parent 58dba494a0
commit bd3f2e129c

@ -979,12 +979,14 @@ importers:
'@types/styled-components': ^5.1.26 '@types/styled-components': ^5.1.26
agora-rtc-react: ^1.1.3 agora-rtc-react: ^1.1.3
ahooks: ^3.7.4 ahooks: ^3.7.4
lodash: ^4.17.21
react: 18.2.0 react: 18.2.0
styled-components: ^5.3.6 styled-components: ^5.3.6
zustand: ^4.1.5 zustand: ^4.1.5
dependencies: dependencies:
agora-rtc-react: 1.1.3_react@18.2.0 agora-rtc-react: 1.1.3_react@18.2.0
ahooks: 3.7.4_react@18.2.0 ahooks: 3.7.4_react@18.2.0
lodash: 4.17.21
devDependencies: devDependencies:
'@types/styled-components': 5.1.26 '@types/styled-components': 5.1.26
react: 18.2.0 react: 18.2.0
@ -13538,7 +13540,7 @@ packages:
babel-plugin-syntax-jsx: 6.18.0 babel-plugin-syntax-jsx: 6.18.0
lodash: 4.17.21 lodash: 4.17.21
picomatch: 2.3.1 picomatch: 2.3.1
styled-components: 5.3.6_7i5myeigehqah43i5u7wbekgba styled-components: 5.3.6_react@18.2.0
/babel-plugin-syntax-jsx/6.18.0: /babel-plugin-syntax-jsx/6.18.0:
resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==} resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==}
@ -32885,6 +32887,7 @@ packages:
react-is: 18.2.0 react-is: 18.2.0
shallowequal: 1.1.0 shallowequal: 1.1.0
supports-color: 5.5.0 supports-color: 5.5.0
dev: false
/styled-components/5.3.6_mdz3marskokvq6744hhidi3r5a: /styled-components/5.3.6_mdz3marskokvq6744hhidi3r5a:
resolution: {integrity: sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==} resolution: {integrity: sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==}
@ -32930,7 +32933,6 @@ packages:
react: 18.2.0 react: 18.2.0
shallowequal: 1.1.0 shallowequal: 1.1.0
supports-color: 5.5.0 supports-color: 5.5.0
dev: true
/styled-system/5.1.5: /styled-system/5.1.5:
resolution: {integrity: sha512-7VoD0o2R3RKzOzPK0jYrVnS8iJdfkKsQJNiLRDjikOpQVqQHns/DXWaPZOH4tIKkhAT7I6wIsy9FWTWh2X3q+A==} resolution: {integrity: sha512-7VoD0o2R3RKzOzPK0jYrVnS8iJdfkKsQJNiLRDjikOpQVqQHns/DXWaPZOH4tIKkhAT7I6wIsy9FWTWh2X3q+A==}

@ -24,6 +24,7 @@
"k82f5a5d4": "Password incorrect", "k82f5a5d4": "Password incorrect",
"k89bf46fc": "Unable to recall messages from {{minutes}} minutes ago", "k89bf46fc": "Unable to recall messages from {{minutes}} minutes ago",
"k986040de": "No group found", "k986040de": "No group found",
"ka3eb52f8": "Call ended, duration: {{num}} minutes",
"ka8b712f7": "Email already exists!", "ka8b712f7": "Email already exists!",
"kb143afe": "This data is not allowed to be modified", "kb143afe": "This data is not allowed to be modified",
"kb32d3d62": "Anonymous", "kb32d3d62": "Anonymous",
@ -47,5 +48,6 @@
"ke82b4383": "You cannot send messages because you are banned", "ke82b4383": "You cannot send messages because you are banned",
"ke99cd649": "No access to converse information permission", "ke99cd649": "No access to converse information permission",
"ke9fabda8": "Token Invalid", "ke9fabda8": "Token Invalid",
"kea5b4254": "This channel has opened a call",
"kef3676e1": "The invitation code has expired" "kef3676e1": "The invitation code has expired"
} }

@ -24,6 +24,7 @@
"k82f5a5d4": "密码不正确", "k82f5a5d4": "密码不正确",
"k89bf46fc": "无法撤回 {{minutes}} 分钟前的消息", "k89bf46fc": "无法撤回 {{minutes}} 分钟前的消息",
"k986040de": "没有找到群组", "k986040de": "没有找到群组",
"ka3eb52f8": "通话已结束, 时长: {{num}}分钟",
"ka8b712f7": "邮箱已存在!", "ka8b712f7": "邮箱已存在!",
"kb143afe": "该数据不允许修改", "kb143afe": "该数据不允许修改",
"kb32d3d62": "匿名用户", "kb32d3d62": "匿名用户",
@ -47,5 +48,6 @@
"ke82b4383": "您因为被禁言无法发送消息", "ke82b4383": "您因为被禁言无法发送消息",
"ke99cd649": "没有获取会话信息权限", "ke99cd649": "没有获取会话信息权限",
"ke9fabda8": "Token不合规", "ke9fabda8": "Token不合规",
"kea5b4254": "本频道开启了通话",
"kef3676e1": "该邀请码已过期" "kef3676e1": "该邀请码已过期"
} }

@ -13,6 +13,12 @@ export class AgoraMeeting extends TimeStamps implements db.Base {
@prop() @prop()
converseId: string; converseId: string;
@prop()
groupId?: string;
@prop()
messageId: string;
@prop() @prop()
channelName: string; channelName: string;

@ -1,4 +1,9 @@
import { DataNotFoundError, TcContext } from 'tailchat-server-sdk'; import {
DataNotFoundError,
MessageStruct,
TcContext,
TcPureContext,
} from 'tailchat-server-sdk';
import { TcService, TcDbService, db } from 'tailchat-server-sdk'; import { TcService, TcDbService, db } from 'tailchat-server-sdk';
import type { import type {
AgoraMeetingDocument, AgoraMeetingDocument,
@ -26,6 +31,8 @@ interface ChannelUserListRet {
}; };
} }
const noticeCacheKey = 'agora:notice:';
/** /**
* *
* *
@ -35,6 +42,8 @@ interface AgoraService
extends TcService, extends TcService,
TcDbService<AgoraMeetingDocument, AgoraMeetingModel> {} TcDbService<AgoraMeetingDocument, AgoraMeetingModel> {}
class AgoraService extends TcService { class AgoraService extends TcService {
botUserId: string | undefined;
get serviceName() { get serviceName() {
return 'plugin:com.msgbyte.agora'; return 'plugin:com.msgbyte.agora';
} }
@ -104,6 +113,25 @@ class AgoraService extends TcService {
this.registerAuthWhitelist(['/webhook']); this.registerAuthWhitelist(['/webhook']);
} }
protected onInited(): void {
// 确保机器人用户存在, 并记录机器人用户id
this.waitForServices(['user']).then(async () => {
try {
const botUserId = await this.broker.call('user.ensurePluginBot', {
botId: 'agora-meeting',
nickname: 'Agora Bot',
avatar: '{BACKEND}/plugins/com.msgbyte.agora/assets/icon.png',
});
this.logger.info('Agora Meeting Bot Id:', botUserId);
this.botUserId = String(botUserId);
} catch (e) {
this.logger.error(e);
}
});
}
generateJoinInfo( generateJoinInfo(
ctx: TcContext<{ ctx: TcContext<{
channelName: string; channelName: string;
@ -166,6 +194,7 @@ class AgoraService extends TcService {
/** /**
* agora * agora
* NOTICE: (header)
* Reference: https://docs.agora.io/cn/live-streaming-premium-legacy/rtc_channel_event?platform=RESTful#101-channel-create * Reference: https://docs.agora.io/cn/live-streaming-premium-legacy/rtc_channel_event?platform=RESTful#101-channel-create
*/ */
async webhook( async webhook(
@ -177,9 +206,16 @@ class AgoraService extends TcService {
payload: any; payload: any;
}> }>
) { ) {
const { eventType, payload } = ctx.params; const { eventType, payload, noticeId } = ctx.params;
const { t } = ctx.meta;
const channelName = payload.channelName; const channelName = payload.channelName;
const valid = await this.checkNoticeValid(noticeId);
if (!valid) {
this.logger.debug('agora notice has been handled');
return true;
}
this.logger.info('webhook received', { eventType, payload }); this.logger.info('webhook received', { eventType, payload });
if (channelName === 'test_webhook') { if (channelName === 'test_webhook') {
// 连通性检查 // 连通性检查
@ -190,42 +226,74 @@ class AgoraService extends TcService {
// 频道被创建 // 频道被创建
const ts = payload.ts; const ts = payload.ts;
const converseId = this.getConverseIdFromChannelName(channelName); const [converseId, groupId] = this.getSourceFromChannelName(channelName);
const existedMeeting =
await this.adapter.model.findLastestMeetingByConverseId(converseId);
if (existedMeeting) {
// 已经创建了,则跳过
return;
}
const message = await this.sendPluginBotMessage(ctx, {
converseId,
groupId,
content: t('本频道开启了通话'),
});
const messageId = String(message._id);
const meeting = await this.adapter.model.create({ const meeting = await this.adapter.model.create({
channelName, channelName,
converseId, converseId,
groupId,
messageId,
active: true, active: true,
createdAt: new Date(ts), createdAt: new Date(ts * 1000),
}); });
this.roomcastNotify(ctx, converseId, 'agoraChannelCreate', { this.roomcastNotify(ctx, converseId, 'agoraChannelCreate', {
converseId, converseId,
groupId,
meetingId: String(meeting._id), meetingId: String(meeting._id),
}); });
} else if (eventType === 102) { } else if (eventType === 102) {
// 频道被销毁 // 频道被销毁
const ts = payload.ts; const ts = payload.ts;
const converseId = this.getConverseIdFromChannelName(channelName); const [converseId, groupId] = this.getSourceFromChannelName(channelName);
const meeting = await this.adapter.model.findLastestMeetingByConverseId( const meeting = await this.adapter.model.findLastestMeetingByConverseId(
converseId converseId
); );
if (!meeting) { if (!meeting) {
// 会议不存在,直接跳过
return; return;
} }
meeting.active = false; meeting.active = false;
meeting.endAt = new Date(ts); meeting.endAt = new Date(ts * 1000);
await meeting.save(); await meeting.save();
this.roomcastNotify(ctx, converseId, 'agoraChannelDestroy', { this.roomcastNotify(ctx, converseId, 'agoraChannelDestroy', {
converseId, converseId,
groupId,
meetingId: String(meeting._id), meetingId: String(meeting._id),
}); });
const duration =
new Date(meeting.endAt).valueOf() -
new Date(meeting.createdAt).valueOf();
this.sendPluginBotMessage(ctx, {
converseId,
groupId,
content: t('通话已结束, 时长: {{num}}分钟', {
num: Math.round(duration / 1000 / 60),
}),
});
} else if (eventType === 103) { } else if (eventType === 103) {
// 用户加入 // 用户加入
const { channelName, uid: userId } = payload; const { channelName, uid: userId } = payload;
const converseId = this.getConverseIdFromChannelName(channelName); const [converseId, groupId] = this.getSourceFromChannelName(channelName);
const meeting = await this.adapter.model.findLastestMeetingByConverseId( const meeting = await this.adapter.model.findLastestMeetingByConverseId(
converseId converseId
@ -238,13 +306,14 @@ class AgoraService extends TcService {
this.roomcastNotify(ctx, converseId, 'agoraBroadcasterJoin', { this.roomcastNotify(ctx, converseId, 'agoraBroadcasterJoin', {
converseId, converseId,
groupId,
meetingId: String(meeting._id), meetingId: String(meeting._id),
userId, userId,
}); });
} else if (eventType === 104) { } else if (eventType === 104) {
// 用户离开 // 用户离开
const { channelName, uid } = payload; const { channelName, uid } = payload;
const converseId = this.getConverseIdFromChannelName(channelName); const [converseId, groupId] = this.getSourceFromChannelName(channelName);
const meeting = await this.adapter.model.findLastestMeetingByConverseId( const meeting = await this.adapter.model.findLastestMeetingByConverseId(
converseId converseId
@ -255,6 +324,7 @@ class AgoraService extends TcService {
this.roomcastNotify(ctx, converseId, 'agoraBroadcasterLeave', { this.roomcastNotify(ctx, converseId, 'agoraBroadcasterLeave', {
converseId, converseId,
groupId,
meetingId: String(meeting._id), meetingId: String(meeting._id),
userId: uid, userId: uid,
}); });
@ -278,7 +348,9 @@ class AgoraService extends TcService {
/** /**
* NOTICE: ChannelName使 * NOTICE: ChannelName使
*/ */
private getConverseIdFromChannelName(channelName: string): string { private getSourceFromChannelName(
channelName: string
): [string, string | undefined] {
if (!channelName) { if (!channelName) {
this.logger.error('channel name invalid', channelName); this.logger.error('channel name invalid', channelName);
throw new Error('channel name invalid'); throw new Error('channel name invalid');
@ -290,7 +362,57 @@ class AgoraService extends TcService {
throw new Error('converseId invalid'); throw new Error('converseId invalid');
} }
return converseId; if (groupId === 'personal') {
return [converseId, undefined];
} else {
if (!db.Types.ObjectId.isValid(groupId)) {
this.logger.error('groupId invalid:', groupId);
throw new Error('groupId invalid');
}
return [converseId, groupId];
}
}
private async checkNoticeValid(noticeId: string) {
const key = noticeCacheKey + noticeId;
const v = await this.broker.cacher.get(key);
if (v) {
return false;
}
await this.broker.cacher.set(key, '1', 60 * 10); // 1分钟
return true;
}
private async sendPluginBotMessage(
ctx: TcPureContext<any>,
messagePayload: {
converseId: string;
groupId?: string;
content: string;
meta?: any;
}
): Promise<MessageStruct> {
if (!this.botUserId) {
this.logger.warn('机器人尚未初始化,无法发送插件消息');
return;
}
const res = await ctx.call(
'chat.message.sendMessage',
{
...messagePayload,
},
{
meta: {
userId: this.botUserId,
},
}
);
return res as MessageStruct;
} }
} }

@ -9,7 +9,8 @@
}, },
"dependencies": { "dependencies": {
"agora-rtc-react": "^1.1.3", "agora-rtc-react": "^1.1.3",
"ahooks": "^3.7.4" "ahooks": "^3.7.4",
"lodash": "^4.17.21"
}, },
"devDependencies": { "devDependencies": {
"@types/styled-components": "^5.1.26", "@types/styled-components": "^5.1.26",

@ -9,6 +9,8 @@ import { request } from '../request';
import styled from 'styled-components'; import styled from 'styled-components';
import { useMeetingStore } from './store'; import { useMeetingStore } from './store';
import { NetworkStats } from './NetworkStats'; import { NetworkStats } from './NetworkStats';
import _once from 'lodash/once';
import type { IAgoraRTCClient } from 'agora-rtc-react';
const Root = styled.div` const Root = styled.div`
.body { .body {
@ -25,6 +27,10 @@ const Root = styled.div`
} }
`; `;
const enableDualStream = _once((client: IAgoraRTCClient) => {
return client.enableDualStream();
});
export interface MeetingViewProps { export interface MeetingViewProps {
meetingId: string; meetingId: string;
onClose: () => void; onClose: () => void;
@ -83,7 +89,7 @@ export const MeetingView: React.FC<MeetingViewProps> = React.memo((props) => {
const { appId, token } = data ?? {}; const { appId, token } = data ?? {};
await client.join(appId, channelName, token, _id); await client.join(appId, channelName, token, _id);
await client.enableDualStream(); await enableDualStream(client);
client.enableAudioVolumeIndicator(); client.enableAudioVolumeIndicator();
setStart(true); setStart(true);
} catch (err) { } catch (err) {

@ -5,6 +5,7 @@ import { OwnVideoView, VideoView } from './VideoView';
const Root = styled.div` const Root = styled.div`
height: 70vh; height: 70vh;
overflow: hidden;
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(440px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(440px, 1fr));
`; `;

@ -19,8 +19,7 @@ regPluginPanelAction({
title: '发起通话', title: '发起通话',
content: '是否通过声网插件在当前会话开启音视频通讯?', content: '是否通过声网插件在当前会话开启音视频通讯?',
onConfirm: async () => { onConfirm: async () => {
// startFastMeeting(`${groupId}|${panelId}`); startFastMeeting(`${groupId}|${panelId}`);
startFastMeeting('123456'); // for test
}, },
}); });
}, },

Loading…
Cancel
Save