From 1e67c62626a4393281a83565dcbe812fe40da0f5 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Fri, 23 Dec 2022 17:39:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A3=B0=E7=BD=91=E9=89=B4=E6=9D=83?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/agora.service.dev.ts | 48 ++ .../services/utils/AccessToken2.ts | 452 ++++++++++++++++++ .../services/utils/README.md | 1 + .../services/utils/RtcTokenBuilder2.ts | 266 +++++++++++ 4 files changed, 767 insertions(+) create mode 100644 server/plugins/com.msgbyte.agora/services/utils/AccessToken2.ts create mode 100644 server/plugins/com.msgbyte.agora/services/utils/README.md create mode 100644 server/plugins/com.msgbyte.agora/services/utils/RtcTokenBuilder2.ts diff --git a/server/plugins/com.msgbyte.agora/services/agora.service.dev.ts b/server/plugins/com.msgbyte.agora/services/agora.service.dev.ts index 06b3edd9..2bb0dab6 100644 --- a/server/plugins/com.msgbyte.agora/services/agora.service.dev.ts +++ b/server/plugins/com.msgbyte.agora/services/agora.service.dev.ts @@ -1,5 +1,7 @@ +import { TcContext } from 'tailchat-server-sdk'; import { TcService, TcDbService } from 'tailchat-server-sdk'; import type { AgoraDocument, AgoraModel } from '../models/agora'; +import { RtcTokenBuilder, Role as RtcRole } from './utils/RtcTokenBuilder2'; /** * 声网音视频 @@ -30,6 +32,52 @@ class AgoraService extends TcService { onInit() { // this.registerLocalDb(require('../models/agora').default); + + this.registerAction('generateToken', this.generateToken, { + params: { + channelName: 'string', + appId: { type: 'string', optional: true }, + appCert: { type: 'string', optional: true }, + }, + }); + } + + generateToken( + ctx: TcContext<{ + channelName: string; + appId?: string; + appCert?: string; + }> + ) { + const { + channelName, + appId = this.serverAppId, + appCert = this.serverAppCertificate, + } = ctx.params; + + 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 token; } } diff --git a/server/plugins/com.msgbyte.agora/services/utils/AccessToken2.ts b/server/plugins/com.msgbyte.agora/services/utils/AccessToken2.ts new file mode 100644 index 00000000..f8e4de60 --- /dev/null +++ b/server/plugins/com.msgbyte.agora/services/utils/AccessToken2.ts @@ -0,0 +1,452 @@ +import crypto from 'crypto'; +import zlib from 'zlib'; +const VERSION_LENGTH = 3; +const APP_ID_LENGTH = 32; + +const getVersion = () => { + return '007'; +}; + +class Service { + __type; + __privileges; + + constructor(service_type) { + this.__type = service_type; + this.__privileges = {}; + } + + __pack_type() { + const buf = new ByteBuf(); + buf.putUint16(this.__type); + return buf.pack(); + } + + __pack_privileges() { + const buf = new ByteBuf(); + buf.putTreeMapUInt32(this.__privileges); + return buf.pack(); + } + + service_type() { + return this.__type; + } + + add_privilege(privilege, expire) { + this.__privileges[privilege] = expire; + } + + pack() { + return Buffer.concat([this.__pack_type(), this.__pack_privileges()]); + } + + unpack(buffer) { + const bufReader = new ReadByteBuf(buffer); + this.__privileges = bufReader.getTreeMapUInt32(); + return bufReader; + } +} + +const kRtcServiceType = 1; + +class ServiceRtc extends Service { + static kPrivilegeJoinChannel = 1; + static kPrivilegePublishAudioStream = 2; + static kPrivilegePublishVideoStream = 3; + static kPrivilegePublishDataStream = 4; + + __channel_name; + __uid; + + constructor(channel_name, uid) { + super(kRtcServiceType); + this.__channel_name = channel_name; + this.__uid = uid === 0 ? '' : `${uid}`; + } + + pack() { + const buffer = new ByteBuf(); + buffer.putString(this.__channel_name).putString(this.__uid); + return Buffer.concat([super.pack(), buffer.pack()]); + } + + unpack(buffer) { + const bufReader = super.unpack(buffer); + this.__channel_name = bufReader.getString(); + this.__uid = bufReader.getString(); + return bufReader; + } +} + +const kRtmServiceType = 2; + +class ServiceRtm extends Service { + __user_id; + + constructor(user_id) { + super(kRtmServiceType); + this.__user_id = user_id || ''; + } + + pack() { + const buffer = new ByteBuf(); + buffer.putString(this.__user_id); + return Buffer.concat([super.pack(), buffer.pack()]); + } + + unpack(buffer) { + const bufReader = super.unpack(buffer); + this.__user_id = bufReader.getString(); + return bufReader; + } +} + +(ServiceRtm as any).kPrivilegeLogin = 1; + +const kFpaServiceType = 4; + +class ServiceFpa extends Service { + constructor() { + super(kFpaServiceType); + } + + pack() { + return super.pack(); + } + + unpack(buffer) { + const bufReader = super.unpack(buffer); + return bufReader; + } +} + +(ServiceFpa as any).kPrivilegeLogin = 1; + +const kChatServiceType = 5; + +class ServiceChat extends Service { + __user_id; + + constructor(user_id) { + super(kChatServiceType); + this.__user_id = user_id || ''; + } + + pack() { + const buffer = new ByteBuf(); + buffer.putString(this.__user_id); + return Buffer.concat([super.pack(), buffer.pack()]); + } + + unpack(buffer) { + const bufReader = super.unpack(buffer); + this.__user_id = bufReader.getString(); + return bufReader; + } +} + +(ServiceChat as any).kPrivilegeUser = 1; +(ServiceChat as any).kPrivilegeApp = 2; + +const kEducationServiceType = 7; + +class ServiceEducation extends Service { + __room_uuid; + __user_uuid; + __role; + + constructor(roomUuid, userUuid, role) { + super(kEducationServiceType); + this.__room_uuid = roomUuid || ''; + this.__user_uuid = userUuid || ''; + this.__role = role || -1; + } + + pack() { + const buffer = new ByteBuf(); + buffer.putString(this.__room_uuid); + buffer.putString(this.__user_uuid); + buffer.putInt16(this.__role); + return Buffer.concat([super.pack(), buffer.pack()]); + } + + unpack(buffer) { + const bufReader = super.unpack(buffer); + this.__room_uuid = bufReader.getString(); + this.__user_uuid = bufReader.getString(); + this.__role = bufReader.getInt16(); + return bufReader; + } +} + +(ServiceEducation as any).PRIVILEGE_ROOM_USER = 1; +(ServiceEducation as any).PRIVILEGE_USER = 2; +(ServiceEducation as any).PRIVILEGE_APP = 3; + +class AccessToken2 { + appId; + appCertificate; + issueTs; + expire; + salt; + services; + + constructor(appId, appCertificate, issueTs, expire) { + this.appId = appId; + this.appCertificate = appCertificate; + this.issueTs = issueTs || new Date().getTime() / 1000; + this.expire = expire; + // salt ranges in (1, 99999999) + this.salt = Math.floor(Math.random() * 99999999) + 1; + this.services = {}; + } + + __signing() { + let signing = encodeHMac( + new ByteBuf().putUint32(this.issueTs).pack(), + this.appCertificate + ); + signing = encodeHMac(new ByteBuf().putUint32(this.salt).pack(), signing); + return signing; + } + + __build_check() { + const is_uuid = (data) => { + if (data.length !== APP_ID_LENGTH) { + return false; + } + const buf = Buffer.from(data, 'hex'); + return !!buf; + }; + + const { appId, appCertificate, services } = this; + if (!is_uuid(appId) || !is_uuid(appCertificate)) { + return false; + } + + if (Object.keys(services).length === 0) { + return false; + } + return true; + } + + add_service(service) { + this.services[service.service_type()] = service; + } + + build() { + if (!this.__build_check()) { + return ''; + } + + const signing = this.__signing(); + let signing_info = new ByteBuf() + .putString(this.appId) + .putUint32(this.issueTs) + .putUint32(this.expire) + .putUint32(this.salt) + .putUint16(Object.keys(this.services).length) + .pack(); + Object.values(this.services).forEach((service: any) => { + signing_info = Buffer.concat([signing_info, service.pack()]); + }); + + const signature = encodeHMac(signing, signing_info); + const content = Buffer.concat([ + new ByteBuf().putString(signature).pack(), + signing_info, + ]); + const compressed = zlib.deflateSync(content); + return `${getVersion()}${Buffer.from(compressed).toString('base64')}`; + } + + from_string(origin_token) { + const origin_version = origin_token.substring(0, VERSION_LENGTH); + if (origin_version !== getVersion()) { + return false; + } + + const origin_content = origin_token.substring( + VERSION_LENGTH, + origin_token.length + ); + const buffer = zlib.inflateSync(new Buffer(origin_content, 'base64')); + const bufferReader = new ReadByteBuf(buffer); + + const signature = bufferReader.getString(); + this.appId = bufferReader.getString(); + this.issueTs = bufferReader.getUint32(); + this.expire = bufferReader.getUint32(); + this.salt = bufferReader.getUint32(); + const service_count = bufferReader.getUint16(); + + let remainBuf = bufferReader.pack(); + for (let i = 0; i < service_count; i++) { + const bufferReaderService = new ReadByteBuf(remainBuf); + const service_type = bufferReaderService.getUint16(); + const service = new (AccessToken2 as any).kServices[service_type](); + remainBuf = service.unpack(bufferReaderService.pack()).pack(); + this.services[service_type] = service; + } + } +} + +const encodeHMac: any = function (key, message) { + return crypto.createHmac('sha256', key).update(message).digest(); +}; + +const ByteBuf: any = function () { + const that: any = { + buffer: Buffer.alloc(1024), + position: 0, + }; + + that.buffer.fill(0); + + that.pack = function () { + const out = Buffer.alloc(that.position); + that.buffer.copy(out, 0, 0, out.length); + return out; + }; + + that.putUint16 = function (v) { + that.buffer.writeUInt16LE(v, that.position); + that.position += 2; + return that; + }; + + that.putUint32 = function (v) { + that.buffer.writeUInt32LE(v, that.position); + that.position += 4; + return that; + }; + that.putInt32 = function (v) { + that.buffer.writeInt32LE(v, that.position); + that.position += 4; + return that; + }; + + that.putInt16 = function (v) { + that.buffer.writeInt16LE(v, that.position); + that.position += 2; + return that; + }; + + that.putBytes = function (bytes) { + that.putUint16(bytes.length); + bytes.copy(that.buffer, that.position); + that.position += bytes.length; + return that; + }; + + that.putString = function (str) { + return that.putBytes(Buffer.from(str)); + }; + + that.putTreeMap = function (map) { + if (!map) { + that.putUint16(0); + return that; + } + + that.putUint16(Object.keys(map).length); + for (const key in map) { + that.putUint16(key); + that.putString(map[key]); + } + + return that; + }; + + that.putTreeMapUInt32 = function (map) { + if (!map) { + that.putUint16(0); + return that; + } + + that.putUint16(Object.keys(map).length); + for (const key in map) { + that.putUint16(key); + that.putUint32(map[key]); + } + + return that; + }; + + return that; +}; + +const ReadByteBuf: any = function (bytes) { + const that: any = { + buffer: bytes, + position: 0, + }; + + that.getUint16 = function () { + const ret = that.buffer.readUInt16LE(that.position); + that.position += 2; + return ret; + }; + + that.getUint32 = function () { + const ret = that.buffer.readUInt32LE(that.position); + that.position += 4; + return ret; + }; + + that.getInt16 = function () { + const ret = that.buffer.readUInt16LE(that.position); + that.position += 2; + return ret; + }; + + that.getString = function () { + const len = that.getUint16(); + + const out = Buffer.alloc(len); + that.buffer.copy(out, 0, that.position, that.position + len); + that.position += len; + return out; + }; + + that.getTreeMapUInt32 = function () { + const map = {}; + const len = that.getUint16(); + for (let i = 0; i < len; i++) { + const key = that.getUint16(); + const value = that.getUint32(); + map[key] = value; + } + return map; + }; + + that.pack = function () { + const length = that.buffer.length; + const out = Buffer.alloc(length); + that.buffer.copy(out, 0, that.position, length); + return out; + }; + + return that; +}; + +(AccessToken2 as any).kServices = {}; +(AccessToken2 as any).kServices[kRtcServiceType] = ServiceRtc; +(AccessToken2 as any).kServices[kRtmServiceType] = ServiceRtm; +(AccessToken2 as any).kServices[kFpaServiceType] = ServiceFpa; +(AccessToken2 as any).kServices[kChatServiceType] = ServiceChat; +(AccessToken2 as any).kServices[kEducationServiceType] = ServiceEducation; + +export { + AccessToken2, + ServiceRtc, + ServiceRtm, + ServiceFpa, + ServiceChat, + ServiceEducation, + kRtcServiceType, + kRtmServiceType, + kFpaServiceType, + kChatServiceType, + kEducationServiceType, +}; diff --git a/server/plugins/com.msgbyte.agora/services/utils/README.md b/server/plugins/com.msgbyte.agora/services/utils/README.md new file mode 100644 index 00000000..1ef5bf52 --- /dev/null +++ b/server/plugins/com.msgbyte.agora/services/utils/README.md @@ -0,0 +1 @@ +Copy from https://github.com/AgoraIO/Tools/tree/master/DynamicKey/AgoraDynamicKey/nodejs diff --git a/server/plugins/com.msgbyte.agora/services/utils/RtcTokenBuilder2.ts b/server/plugins/com.msgbyte.agora/services/utils/RtcTokenBuilder2.ts new file mode 100644 index 00000000..1e7c1efc --- /dev/null +++ b/server/plugins/com.msgbyte.agora/services/utils/RtcTokenBuilder2.ts @@ -0,0 +1,266 @@ +import { AccessToken2 as AccessToken, ServiceRtc } from './AccessToken2'; + +export const Role = { + // for live broadcaster + PUBLISHER: 1, + + // default, for live audience + SUBSCRIBER: 2, +}; + +export class RtcTokenBuilder { + /** + * Builds an RTC token using an Integer uid. + * @param {*} appId The App ID issued to you by Agora. + * @param {*} appCertificate Certificate of the application that you registered in the Agora Dashboard. + * @param {*} channelName The unique channel name for the AgoraRTC session in the string format. The string length must be less than 64 bytes. Supported character scopes are: + * - The 26 lowercase English letters: a to z. + * - The 26 uppercase English letters: A to Z. + * - The 10 digits: 0 to 9. + * - The space. + * - "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", "_", " {", "}", "|", "~", ",". + * @param {*} uid User ID. A 32-bit unsigned integer with a value ranging from 1 to (2^32-1). + * @param {*} role See #userRole. + * - Role.PUBLISHER; RECOMMENDED. Use this role for a voice/video call or a live broadcast. + * - Role.SUBSCRIBER: ONLY use this role if your live-broadcast scenario requires authentication for [Hosting-in](https://docs.agora.io/en/Agora%20Platform/terms?platform=All%20Platforms#hosting-in). In order for this role to take effect, please contact our support team to enable authentication for Hosting-in for you. Otherwise, Role_Subscriber still has the same privileges as Role_Publisher. + * @param {*} token_expire epresented by the number of seconds elapsed since now. If, for example, you want to access the Agora Service within 10 minutes after the token is generated, set token_expire as 600(seconds) + * @param {*} privilege_expire represented by the number of seconds elapsed since now. If, for example, you want to enable your privilege for 10 minutes, set privilege_expire as 600(seconds). * @return The new Token. + */ + static buildTokenWithUid( + appId, + appCertificate, + channelName, + uid, + role, + token_expire, + privilege_expire = 0 + ) { + return this.buildTokenWithUserAccount( + appId, + appCertificate, + channelName, + uid, + role, + token_expire, + privilege_expire + ); + } + + /** + * Builds an RTC token using an Integer uid. + * @param {*} appId The App ID issued to you by Agora. + * @param {*} appCertificate Certificate of the application that you registered in the Agora Dashboard. + * @param {*} channelName The unique channel name for the AgoraRTC session in the string format. The string length must be less than 64 bytes. Supported character scopes are: + * - The 26 lowercase English letters: a to z. + * - The 26 uppercase English letters: A to Z. + * - The 10 digits: 0 to 9. + * - The space. + * - "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", "_", " {", "}", "|", "~", ",". + * @param {*} account The user account. + * @param {*} role See #userRole. + * - Role.PUBLISHER; RECOMMENDED. Use this role for a voice/video call or a live broadcast. + * - Role.SUBSCRIBER: ONLY use this role if your live-broadcast scenario requires authentication for [Hosting-in](https://docs.agora.io/en/Agora%20Platform/terms?platform=All%20Platforms#hosting-in). In order for this role to take effect, please contact our support team to enable authentication for Hosting-in for you. Otherwise, Role_Subscriber still has the same privileges as Role_Publisher. + * @param {*} token_expire epresented by the number of seconds elapsed since now. If, for example, you want to access the Agora Service within 10 minutes after the token is generated, set token_expire as 600(seconds) + * @param {*} privilege_expire represented by the number of seconds elapsed since now. If, for example, you want to enable your privilege for 10 minutes, set privilege_expire as 600(seconds). + * @return The new Token. + */ + static buildTokenWithUserAccount( + appId, + appCertificate, + channelName, + account, + role, + token_expire, + privilege_expire = 0 + ) { + const token = new AccessToken(appId, appCertificate, 0, token_expire); + + const serviceRtc = new ServiceRtc(channelName, account); + serviceRtc.add_privilege( + ServiceRtc.kPrivilegeJoinChannel, + privilege_expire + ); + if (role == Role.PUBLISHER) { + serviceRtc.add_privilege( + ServiceRtc.kPrivilegePublishAudioStream, + privilege_expire + ); + serviceRtc.add_privilege( + ServiceRtc.kPrivilegePublishVideoStream, + privilege_expire + ); + serviceRtc.add_privilege( + ServiceRtc.kPrivilegePublishDataStream, + privilege_expire + ); + } + token.add_service(serviceRtc); + + return token.build(); + } + + /** + * Generates an RTC token with the specified privilege. + * + * This method supports generating a token with the following privileges: + * - Joining an RTC channel. + * - Publishing audio in an RTC channel. + * - Publishing video in an RTC channel. + * - Publishing data streams in an RTC channel. + * + * The privileges for publishing audio, video, and data streams in an RTC channel apply only if you have + * enabled co-host authentication. + * + * A user can have multiple privileges. Each privilege is valid for a maximum of 24 hours. + * The SDK triggers the onTokenPrivilegeWillExpire and onRequestToken callbacks when the token is about to expire + * or has expired. The callbacks do not report the specific privilege affected, and you need to maintain + * the respective timestamp for each privilege in your app logic. After receiving the callback, you need + * to generate a new token, and then call renewToken to pass the new token to the SDK, or call joinChannel to re-join + * the channel. + * + * @note + * Agora recommends setting a reasonable timestamp for each privilege according to your scenario. + * Suppose the expiration timestamp for joining the channel is set earlier than that for publishing audio. + * When the token for joining the channel expires, the user is immediately kicked off the RTC channel + * and cannot publish any audio stream, even though the timestamp for publishing audio has not expired. + * + * @param appId The App ID of your Agora project. + * @param appCertificate The App Certificate of your Agora project. + * @param channelName The unique channel name for the Agora RTC session in string format. The string length must be less than 64 bytes. The channel name may contain the following characters: + * - All lowercase English letters: a to z. + * - All uppercase English letters: A to Z. + * - All numeric characters: 0 to 9. + * - The space character. + * - "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", "_", " {", "}", "|", "~", ",". + * @param uid The user ID. A 32-bit unsigned integer with a value range from 1 to (2^32 - 1). It must be unique. Set uid as 0, if you do not want to authenticate the user ID, that is, any uid from the app client can join the channel. + * @param tokenExpire represented by the number of seconds elapsed since now. If, for example, you want to access the + * Agora Service within 10 minutes after the token is generated, set token_expire as 600(seconds). + * @param joinChannelPrivilegeExpire The Unix timestamp when the privilege for joining the channel expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set joinChannelPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. + * @param pubAudioPrivilegeExpire The Unix timestamp when the privilege for publishing audio expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set pubAudioPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. If you do not want to enable this privilege, + * set pubAudioPrivilegeExpire as the current Unix timestamp. + * @param pubVideoPrivilegeExpire The Unix timestamp when the privilege for publishing video expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set pubVideoPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. If you do not want to enable this privilege, + * set pubVideoPrivilegeExpire as the current Unix timestamp. + * @param pubDataStreamPrivilegeExpire The Unix timestamp when the privilege for publishing data streams expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set pubDataStreamPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. If you do not want to enable this privilege, + * set pubDataStreamPrivilegeExpire as the current Unix timestamp. + * @return The new Token + */ + static buildTokenWithUidAndPrivilege( + appId, + appCertificate, + channelName, + uid, + tokenExpire, + joinChannelPrivilegeExpire, + pubAudioPrivilegeExpire, + pubVideoPrivilegeExpire, + pubDataStreamPrivilegeExpire + ) { + return this.BuildTokenWithUserAccountAndPrivilege( + appId, + appCertificate, + channelName, + uid, + tokenExpire, + joinChannelPrivilegeExpire, + pubAudioPrivilegeExpire, + pubVideoPrivilegeExpire, + pubDataStreamPrivilegeExpire + ); + } + + /** + * Generates an RTC token with the specified privilege. + * + * This method supports generating a token with the following privileges: + * - Joining an RTC channel. + * - Publishing audio in an RTC channel. + * - Publishing video in an RTC channel. + * - Publishing data streams in an RTC channel. + * + * The privileges for publishing audio, video, and data streams in an RTC channel apply only if you have + * enabled co-host authentication. + * + * A user can have multiple privileges. Each privilege is valid for a maximum of 24 hours. + * The SDK triggers the onTokenPrivilegeWillExpire and onRequestToken callbacks when the token is about to expire + * or has expired. The callbacks do not report the specific privilege affected, and you need to maintain + * the respective timestamp for each privilege in your app logic. After receiving the callback, you need + * to generate a new token, and then call renewToken to pass the new token to the SDK, or call joinChannel to re-join + * the channel. + * + * @note + * Agora recommends setting a reasonable timestamp for each privilege according to your scenario. + * Suppose the expiration timestamp for joining the channel is set earlier than that for publishing audio. + * When the token for joining the channel expires, the user is immediately kicked off the RTC channel + * and cannot publish any audio stream, even though the timestamp for publishing audio has not expired. + * + * @param appId The App ID of your Agora project. + * @param appCertificate The App Certificate of your Agora project. + * @param channelName The unique channel name for the Agora RTC session in string format. The string length must be less than 64 bytes. The channel name may contain the following characters: + * - All lowercase English letters: a to z. + * - All uppercase English letters: A to Z. + * - All numeric characters: 0 to 9. + * - The space character. + * - "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", "_", " {", "}", "|", "~", ",". + * @param userAccount The user account. + * @param tokenExpire represented by the number of seconds elapsed since now. If, for example, you want to access the + * Agora Service within 10 minutes after the token is generated, set token_expire as 600(seconds). + * @param joinChannelPrivilegeExpire The Unix timestamp when the privilege for joining the channel expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set joinChannelPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. + * @param pubAudioPrivilegeExpire The Unix timestamp when the privilege for publishing audio expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set pubAudioPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. If you do not want to enable this privilege, + * set pubAudioPrivilegeExpire as the current Unix timestamp. + * @param pubVideoPrivilegeExpire The Unix timestamp when the privilege for publishing video expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set pubVideoPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. If you do not want to enable this privilege, + * set pubVideoPrivilegeExpire as the current Unix timestamp. + * @param pubDataStreamPrivilegeExpire The Unix timestamp when the privilege for publishing data streams expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set pubDataStreamPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. If you do not want to enable this privilege, + * set pubDataStreamPrivilegeExpire as the current Unix timestamp. + * @return The new Token. + */ + static BuildTokenWithUserAccountAndPrivilege( + appId, + appCertificate, + channelName, + account, + tokenExpire, + joinChannelPrivilegeExpire, + pubAudioPrivilegeExpire, + pubVideoPrivilegeExpire, + pubDataStreamPrivilegeExpire + ) { + const token = new AccessToken(appId, appCertificate, 0, tokenExpire); + + const serviceRtc = new ServiceRtc(channelName, account); + serviceRtc.add_privilege( + ServiceRtc.kPrivilegeJoinChannel, + joinChannelPrivilegeExpire + ); + serviceRtc.add_privilege( + ServiceRtc.kPrivilegePublishAudioStream, + pubAudioPrivilegeExpire + ); + serviceRtc.add_privilege( + ServiceRtc.kPrivilegePublishVideoStream, + pubVideoPrivilegeExpire + ); + serviceRtc.add_privilege( + ServiceRtc.kPrivilegePublishDataStream, + pubDataStreamPrivilegeExpire + ); + token.add_service(serviceRtc); + + return token.build(); + } +}