import { DataNotFoundError, TcContext } from 'tailchat-server-sdk'; import { TcService, TcDbService, db } from 'tailchat-server-sdk'; import type { AgoraMeetingDocument, AgoraMeetingModel, } from '../models/agora-meeting'; import { RtcTokenBuilder, Role as RtcRole } from './utils/RtcTokenBuilder2'; import got from 'got'; import _ from 'lodash'; // Reference: https://docs.agora.io/cn/metachat/rtc_channel_management_restfulapi#查询用户列表 interface ChannelUserListRet { success: boolean; data: | { channel_exist: false; } | { channel_exist: true; mode: 1 | 2; total: number; users: string[]; // 频道内所有用户的用户 ID broadcasters: string[]; // 频道内所有主播的用户 ID。该字段仅在直播场景 audience: string[]; // 频道内观众的用户 ID。最多包含当前频道内前 10,000 名观众的用户 ID。该字段仅在直播场景 (mode 的值为 2)下返回。 audience_total: number; // 频道内的观众总人数。该字段仅在直播场景 (mode 的值为 2)下返回。 }; } /** * 声网音视频 * * 为Tailchat增加声网音视频通信功能 */ interface AgoraService extends TcService, TcDbService {} class AgoraService extends TcService { get serviceName() { return 'plugin:com.msgbyte.agora'; } /** * 声网服务端的appid */ get serverAppId() { return process.env.AGORA_APP_ID; } /** * 声网服务端的app证书 */ get serverAppCertificate() { return process.env.AGORA_APP_CERT; } /** * 声网服务端restful的客户 ID */ get serverCustomerKey() { return process.env.AGORA_CUSTOMER_KEY; } /** * 声网服务端restful的客户 秘钥 */ get serverCustomerSecret() { return process.env.AGORA_CUSTOMER_SECRET; } onInit() { if ( !this.serverAppId || !this.serverAppCertificate || !this.serverCustomerKey || !this.serverCustomerSecret ) { this.logger.warn( '声网服务启动失败, 缺少必要的环境变量。AGORA_APP_ID, AGORA_APP_CERT, AGORA_CUSTOMER_KEY, AGORA_CUSTOMER_SECRET' ); return; } this.registerLocalDb(require('../models/agora-meeting').default); this.registerAction('generateJoinInfo', this.generateJoinInfo, { params: { channelName: 'string', }, }); this.registerAction('getChannelUserList', this.getChannelUserList, { params: { channelName: 'string', }, }); this.registerAction('webhook', this.webhook, { params: { noticeId: 'string', productId: 'number', eventType: 'number', notifyMs: 'number', payload: 'any', }, }); } generateJoinInfo( ctx: TcContext<{ channelName: string; }> ) { const { channelName } = ctx.params; const appId = this.serverAppId; const appCert = this.serverAppCertificate; if (!appId || !appCert) { throw new Error('Agora.io AppId/AppCert not init'); } const role = RtcRole.PUBLISHER; const userId = ctx.meta.userId; const tokenExpirationInSecond = 3600; // 1h const privilegeExpirationInSecond = 3600; // 1h // Build token with user account const token = RtcTokenBuilder.buildTokenWithUserAccount( appId, appCert, channelName, userId, role, tokenExpirationInSecond, privilegeExpirationInSecond ); return { appId, token }; } /** * 获取频道用户列表 */ async getChannelUserList( ctx: TcContext<{ channelName: string; }> ): Promise { const { channelName } = ctx.params; const { data } = await got .get( `https://api.agora.io/dev/v1/channel/user/${this.serverAppId}/${channelName}`, { headers: this.generateRESTfulHeaders(), } ) .json(); if (data.channel_exist === false) { throw new DataNotFoundError('Channel not exist'); } return data; } /** * agora服务的回调 * Reference: https://docs.agora.io/cn/live-streaming-premium-legacy/rtc_channel_event?platform=RESTful#101-channel-create */ async webhook( ctx: TcContext<{ noticeId: string; productId: number; eventType: number; notifyMs: number; payload: any; }> ) { const { eventType, payload } = ctx.params; if (eventType === 101) { // 频道被创建 const { channelName } = payload; const converseId = this.getConverseIdFromChannelName(channelName); const meeting = await this.adapter.model.create({ channelName, converseId, active: true, }); this.roomcastNotify(ctx, converseId, 'agoraChannelCreate', { converseId, meetingId: String(meeting._id), }); } else if (eventType === 102) { // 频道被销毁 const { channelName } = payload; const converseId = this.getConverseIdFromChannelName(channelName); const meeting = await this.adapter.model.findLastestMeetingByConverseId( converseId ); if (!meeting) { return; } meeting.active = false; await meeting.save(); this.roomcastNotify(ctx, converseId, 'agoraChannelDestroy', { converseId, meetingId: String(meeting._id), }); } else if (eventType === 103) { // 用户加入 const { channelName, uid: userId } = payload; const converseId = this.getConverseIdFromChannelName(channelName); const meeting = await this.adapter.model.findLastestMeetingByConverseId( converseId ); if (!meeting) { return; } meeting.members = _.uniq([...meeting.members, userId]); await meeting.save(); this.roomcastNotify(ctx, converseId, 'agoraBroadcasterJoin', { converseId, meetingId: String(meeting._id), userId, }); } else if (eventType === 104) { // 用户离开 const { channelName, uid } = payload; const converseId = this.getConverseIdFromChannelName(channelName); const meeting = await this.adapter.model.findLastestMeetingByConverseId( converseId ); if (!meeting) { return; } this.roomcastNotify(ctx, converseId, 'agoraBroadcasterLeave', { converseId, meetingId: String(meeting._id), userId: uid, }); } } /** * 生成restful api需要的请求头 */ private generateRESTfulHeaders() { const encodedCredential = Buffer.from( this.serverCustomerKey + ':' + this.serverCustomerSecret ).toString('base64'); return { Authorization: 'Basic ' + encodedCredential, 'Content-Type': 'application/json', }; } /** * NOTICE: 这里不用每次唯一的ChannelName是期望设计是会话维度的,即可以重复使用 */ private getConverseIdFromChannelName(channelName: string): string { if (!channelName) { this.logger.error('channel name invalid', channelName); throw new Error('channel name invalid'); } const [groupId, converseId] = channelName.split('|'); if (!db.Types.ObjectId.isValid(converseId)) { this.logger.error('converseId invalid', converseId); throw new Error('converseId invalid'); } return converseId; } } export default AgoraService;