chore: 初始化项目基础结构和资源文件

- 添加项目图标文件(app-icon.png、各平台图标)
- 配置开发环境文件(.env、.nvmrc、.npmrc)
- 添加静态资源文件(背景图片、字体、音频)
- 初始化Tauri后端结构(build.rs、main.rs、模块文件)
- 配置前端项目结构(TypeScript、Vue组件、样式)
- 添加Node.js API服务基础结构
- 配置构建和开发工具(vite、prettier、gitignore)
This commit is contained in:
2026-03-13 10:03:05 +08:00
commit 78af453fe1
357 changed files with 70605 additions and 0 deletions

View File

@ -0,0 +1,12 @@
import { Global, Module } from '@nestjs/common';
import { LoggerService } from './logger.service';
/**
* 日志模块.
*/
@Global()
@Module({
providers: [LoggerService],
exports: [LoggerService],
})
export class LoggerModule {}

View File

@ -0,0 +1,159 @@
import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
import { join } from 'path';
import pino, { type Logger } from 'pino';
import pinoPretty from 'pino-pretty';
import dayjs from 'dayjs';
import { execSync } from 'child_process';
/**
* 检测终端是否支持 UTF-8 编码
* @returns 是否支持 UTF-8
*/
function isTerminalUTF8(): boolean {
// Windows 系统检查代码页
if (process.platform === 'win32') {
try {
// 检查是否设置了 NODE_SKIP_UTF8_CHECK 环境变量
if (process.env.NODE_SKIP_UTF8_CHECK) {
return false;
}
// 尝试执行 chcp 命令检查代码页
const codePage = execSync('chcp', { encoding: 'utf8' }).toString();
// 65001 是 UTF-8 代码页
return codePage.includes('65001');
} catch {
// 默认 Windows 终端使用 GBK 编码
return false;
}
}
// macOS 和 Linux 通常默认使用 UTF-8
return true;
}
/**
* 设置终端为 UTF-8 编码(仅 Windows
*/
function setTerminalToUTF8(): void {
if (process.platform === 'win32' && !isTerminalUTF8()) {
try {
// 设置 stdout 为 UTF-8
if (process.stdout && typeof process.stdout.setEncoding === 'function') {
process.stdout.setEncoding('utf-8');
}
} catch {
// 忽略错误
}
}
}
/**
* 日志服务.
*/
@Injectable({ scope: Scope.TRANSIENT })
export class LoggerService extends ConsoleLogger {
/** 日志实例 */
public logger: Logger | undefined = undefined;
/** 上下文 */
public override context = '';
constructor(context?: string) {
super(context || '');
// 设置终端编码
setTerminalToUTF8();
if (process.env.NODE_ENV === 'development') {
const prettyStream = pinoPretty({
colorize: true,
colorizeObjects: true,
singleLine: true,
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss.l',
// 根据终端编码设置输出
sync: true,
customPrettifiers: {
/** 自定义 err 的显示 */
err: (err: unknown) => {
return `\x1b[31m${JSON.stringify(err, null, 2)}\x1b[0m`;
},
/** 自定义 err 的显示 */
error: (err: unknown) => {
return `\x1b[31m${JSON.stringify(err, null, 2)}\x1b[0m`;
},
},
});
this.logger = pino(prettyStream);
} else {
this.logger = pino({
// level: 'warn',
/** 处理时间字段 */
timestamp: () => `,"time":"${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')}"`,
transport: {
target: 'pino-roll',
options: {
file: join('logs', 'log'), // 日志文件的绝对或相对路径
size: '40m', // 日志文件的最大大小
dateFormat: 'yyyy-MM-dd',
frequency: 'daily', // 周期
mkdir: true,
extension: `.log`,
},
},
});
}
}
/**
* 设置日志上下文.
*/
override setContext(context: string) {
this.context = context;
}
/**
* Info级别日志.
*/
info(obj: any, msg?: string, ...args: any[]) {
this.logger!.info(obj, msg, ...args, this.context);
}
/**
* Error级别日志.
*/
override error(obj: any, msg?: string, ...args: any[]) {
this.logger!.error(obj, msg, ...args, this.context);
}
/**
* Warn级别日志.
*/
override warn(obj: any, msg?: string, ...args: any[]) {
this.logger!.warn(obj, msg, ...args, this.context);
}
/**
* Debug级别日志.
*/
override debug(obj: any, msg?: string, ...args: any[]) {
this.logger!.debug(obj, msg, ...args, this.context);
}
/**
* Trace级别日志.
*/
trace(obj: any, msg?: string, ...args: any[]) {
this.logger!.trace(obj, msg, ...args, this.context);
}
/**
* Fatal级别日志
* 由于 'fatal' 级别的消息旨在在退出进程之前记录,因此 fatal 方法将始终同步刷新目标。因此,重要的是不要滥用 fatal因为如果将其用于进程崩溃或退出之前写入最终日志消息之外的任何其他目的则会导致性能开销。.
*/
override fatal(obj: any, msg?: string, ...args: any[]) {
this.logger!.fatal(obj, msg, ...args, this.context);
}
}

View File

@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import config from '../../config/mikro-orm.config';
@Global()
@Module({
imports: [MikroOrmModule.forRoot(config)],
})
export class MikroOrmConfigModule {}

View File

@ -0,0 +1,159 @@
/**
* Nacos 配置模块
* 从 Nacos 配置中心加载和管理配置
*/
import { Injectable } from '@nestjs/common';
import * as nacos from 'nacos';
import * as yaml from 'js-yaml';
import { ConfigService } from '@nestjs/config';
import { LoggerService } from '../logger/logger.service';
/** 声网应用配置接口 */
export interface AgoraAppConfig {
appId: string;
appCertificate: string;
}
/** 声网配置接口 */
export interface AgoraConfig {
apps: Record<string, AgoraAppConfig>;
defaultAppId: string;
}
/**
* Nacos 配置服务
* 提供配置的读取和缓存功能
*/
@Injectable()
export class NacosConfigService {
private readonly logger = new LoggerService();
private agoraConfig: AgoraConfig | null = null;
private configClient!: nacos.NacosConfigClient;
constructor(private readonly configService: ConfigService) {
this.initNacosClient();
this.loadAgoraConfig();
}
/**
* 初始化 Nacos 客户端
*/
private initNacosClient(): void {
const serverAddr = this.configService.get<string>('NACOS_SERVER_ADDR');
const username = this.configService.get<string>('NACOS_USERNAME');
const password = this.configService.get<string>('NACOS_PASSWORD');
const namespace = this.configService.get<string>('NACOS_NAMESPACE_ID');
if (!serverAddr) {
throw new Error('NACOS_SERVER_ADDR 环境变量未配置');
}
try {
// 创建 Nacos 配置客户端
this.configClient = new nacos.NacosConfigClient({
serverAddr,
username: username || undefined,
password: password || undefined,
namespace: namespace || undefined,
});
this.logger.info(`Nacos 客户端初始化成功,服务器地址:${serverAddr}`);
} catch (error) {
this.logger.error('Nacos 客户端初始化失败:', error);
throw new Error(`Nacos 客户端初始化失败:${error.message}`, { cause: error });
}
}
/**
* 从 Nacos 加载声网配置文件
*/
private async loadAgoraConfig(): Promise<void> {
try {
const dataId = this.configService.get<string>('NACOS_DATA_ID');
const group = this.configService.get<string>('NACOS_GROUP') || 'DEFAULT_GROUP';
// 从 Nacos 获取配置内容
const configContent = await this.configClient.getConfig(
this.configService.get<string>('NACOS_DATA_ID')!,
this.configService.get<string>('NACOS_GROUP') || 'DEFAULT_GROUP'
);
if (!configContent) {
throw new Error(`Nacos 中未找到配置dataId=${dataId}, group=${group}`);
}
// 解析 YAML 配置
const config = yaml.load(configContent) as AgoraConfig;
if (!config.apps || !config.defaultAppId) {
throw new Error('Nacos 配置格式错误:必须包含 apps 和 defaultAppId 字段');
}
if (!config.apps[config.defaultAppId]) {
throw new Error(`Nacos 配置错误defaultAppId '${config.defaultAppId}' 在 apps 中不存在`);
}
this.agoraConfig = config;
this.logger.info(`声网配置从 Nacos 加载成功,默认应用:${config.defaultAppId}`);
// 监听配置变化(可选,实现热更新)
try {
// 使用已解析的 dataId 和 group传入对象格式
this.configClient.subscribe({ dataId, group }, (content: string) => {
this.logger.info('检测到 Nacos 配置变更,重新加载...');
try {
const newConfig = yaml.load(content) as AgoraConfig;
this.agoraConfig = newConfig;
this.logger.info(`声网配置已更新,默认应用:${newConfig.defaultAppId}`);
} catch (error) {
this.logger.error('配置更新失败', error);
}
});
} catch {
// 配置监听失败不影响主流程,记录警告日志
this.logger.warn('Nacos 配置监听器设置失败,但配置已成功加载');
}
} catch (error) {
this.logger.error('从 Nacos 加载声网配置失败', error);
throw new Error(`从 Nacos 加载声网配置失败:${error.message}`, { cause: error });
}
}
/**
* 获取声网配置
* @returns {AgoraConfig} 声网配置对象
*/
getAgoraConfig(): AgoraConfig {
if (!this.agoraConfig) {
throw new Error('声网配置未加载,请检查 Nacos 配置是否正确且格式有效');
}
return this.agoraConfig;
}
/**
* 获取指定的声网应用配置
* @param {string} appId - 应用 ID可选默认使用 defaultAppId
* @returns {AgoraAppConfig} 声网应用配置
*/
getAgoraAppConfig(appId?: string): AgoraAppConfig {
const agoraConfig = this.getAgoraConfig();
const targetAppId = appId || agoraConfig.defaultAppId;
const appConfig = agoraConfig.apps[targetAppId];
if (!appConfig) {
const errorMsg = `未找到声网应用配置:${targetAppId}。可用的应用 ID: ${Object.keys(agoraConfig.apps).join(', ')}`;
this.logger.error(errorMsg);
throw new Error(errorMsg);
}
return appConfig;
}
/**
* 获取默认的声网应用配置
* @returns {AgoraAppConfig} 默认声网应用配置
*/
getDefaultAgoraAppConfig(): AgoraAppConfig {
return this.getAgoraAppConfig();
}
}

View File

@ -0,0 +1,14 @@
/**
* Nacos 配置模块
*/
import { Global, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { NacosConfigService } from './nacos-config.service';
@Global()
@Module({
imports: [ConfigModule],
providers: [NacosConfigService],
exports: [NacosConfigService],
})
export class NacosConfigModule {}

View File

@ -0,0 +1,13 @@
// redis.module.ts
import { Global, Module } from '@nestjs/common';
import { RedisService } from './redis.service';
/**
* Redis模块。.
*/
@Global()
@Module({
providers: [RedisService],
exports: [RedisService],
})
export class RedisModule {}

View File

@ -0,0 +1,144 @@
/**
* Redis 服务 - 统一管理多个 Redis 数据库连接
* 提供全局 Redis 客户端和会议专用 Redis 客户端
*/
import { Injectable, type OnModuleDestroy, type OnModuleInit } from '@nestjs/common';
import Redis from 'ioredis';
import { LoggerService } from '../logger/logger.service';
/**
* Redis 数据库枚举
*/
export enum RedisDatabase {
/** 全局业务数据库DB 1 */
GLOBAL = 1,
/** 会议业务数据库DB 0 */
MEETING = 0,
}
/**
* Redis 配置选项
*/
interface RedisClientOptions {
/** 数据库编号 */
db: RedisDatabase;
/** 是否为生产环境 */
isProduction?: boolean;
}
@Injectable()
export class RedisService implements OnModuleInit, OnModuleDestroy {
/** Redis 客户端实例 Map */
private clients = new Map<RedisDatabase, Redis>();
/** 日志服务实例 */
private readonly logger = new LoggerService();
/**
* 初始化 Redis 连接
*/
async onModuleInit() {
const isProduction = process.env.NODE_ENV === 'production';
// 创建全局业务 Redis 客户端DB 0
await this.createClient({
db: RedisDatabase.GLOBAL,
isProduction,
});
// 创建会议业务 Redis 客户端DB 1
await this.createClient({
db: RedisDatabase.MEETING,
isProduction,
});
}
/**
* 创建并初始化 Redis 客户端
*/
private async createClient(options: RedisClientOptions): Promise<void> {
const redisConfig = {
host: process.env.REDIS_HOST || 'localhost',
port: Number(process.env.REDIS_PORT) || 6379,
password: process.env.REDIS_PASSWORD || undefined,
db: options.db,
retryStrategy: (times: number) => {
if (times > 10) {
this.logger.error(`Redis DB ${options.db} 重连次数过多,放弃重连`, undefined, 'RedisService');
return null;
}
const delay = Math.min(times * 100, 3000);
this.logger.warn(`Redis DB ${options.db} ${delay}ms 后重连...`, 'RedisService');
return delay;
},
};
const client = new Redis(redisConfig);
// 监听事件
client.on('ready', () => {
this.logger.info(`Redis 连接成功,数据库编号为:${options.db}${options.isProduction ? ' (生产环境)' : ' (开发环境)'}`, 'RedisService');
});
client.on('error', (error: Error) => {
this.logger.error(error, `Redis DB ${options.db} 错误`, 'RedisService');
});
// 存储客户端
this.clients.set(options.db, client);
// 等待连接就绪(如果尚未就绪)
if (client.status === 'ready') {
return;
}
await new Promise<void>((resolve, reject) => {
client.once('ready', () => resolve());
client.once('error', reject);
});
}
/**
* 获取指定数据库的 Redis 客户端
* @param db - 数据库编号,默认为全局数据库
*/
getClient(db: RedisDatabase = RedisDatabase.GLOBAL): Redis {
const client = this.clients.get(db);
if (!client) {
throw new Error(`Redis 客户端不存在,数据库编号:${db}`);
}
return client;
}
/**
* 获取全局业务 Redis 客户端DB 0
*/
getGlobalClient(): Redis {
return this.getClient(RedisDatabase.GLOBAL);
}
/**
* 获取会议业务 Redis 客户端DB 1
*/
getMeetingClient(): Redis {
return this.getClient(RedisDatabase.MEETING);
}
/**
* 销毁模块时关闭所有 Redis 连接
*/
async onModuleDestroy(): Promise<void> {
const quitPromises: Array<Promise<void>> = [];
for (const [db, client] of this.clients.entries()) {
quitPromises.push(
client.quit().then(() => {
this.logger.info(`Redis DB ${db} 已关闭连接`, 'RedisService');
})
);
}
await Promise.all(quitPromises);
this.logger.info('所有 Redis 连接已关闭', 'RedisService');
}
}

View File

@ -0,0 +1,280 @@
import crypto from 'node:crypto';
import crc32 from 'crc-32';
import { UINT32 } from 'cuint';
const version = '006';
const randomInt = Math.floor(Math.random() * 0xffffffff);
const VERSION_LENGTH = 3;
const APP_ID_LENGTH = 32;
export const priviledges = {
kJoinChannel: 1,
kPublishAudioStream: 2,
kPublishVideoStream: 3,
kPublishDataStream: 4,
kRtmLogin: 1000,
};
type Messages = Record<number, number>;
interface MessageOptions {
salt: number;
ts: number;
messages: Messages;
pack?: () => Buffer;
}
interface AccessTokenContentOptions {
signature: Buffer | string;
crc_channel: number;
crc_uid: number;
crc_channel_name?: number;
m: Buffer | string;
pack?: () => Buffer;
}
interface ByteBufInterface {
buffer: Buffer;
position: number;
pack: () => Buffer;
putUint16: (v: number) => ByteBufInterface;
putUint32: (v: number) => ByteBufInterface;
putBytes: (bytes: Buffer) => ByteBufInterface;
putString: (str: string) => ByteBufInterface;
putTreeMap: (map?: Record<string, string>) => ByteBufInterface;
putTreeMapUInt32: (map?: Messages) => ByteBufInterface;
}
interface ReadByteBufInterface {
buffer: Buffer;
position: number;
getUint16: () => number;
getUint32: () => number;
getString: () => Buffer;
getTreeMapUInt32: () => Messages;
}
const encodeHMac = (key: string, message: Buffer): Buffer => {
return crypto.createHmac('sha256', key).update(message).digest();
};
const ByteBuf = (): ByteBufInterface => {
const that: ByteBufInterface = {
buffer: Buffer.alloc(1024),
position: 0,
pack() {
const out = Buffer.alloc(that.position);
that.buffer.copy(out, 0, 0, out.length);
return out;
},
putUint16(v: number) {
that.buffer.writeUInt16LE(v, that.position);
that.position += 2;
return that;
},
putUint32(v: number) {
that.buffer.writeUInt32LE(v, that.position);
that.position += 4;
return that;
},
putBytes(bytes: Buffer) {
that.putUint16(bytes.length);
bytes.copy(that.buffer, that.position);
that.position += bytes.length;
return that;
},
putString(str: string) {
return that.putBytes(Buffer.from(str));
},
putTreeMap(map?: Record<string, string>) {
if (!map) {
that.putUint16(0);
return that;
}
that.putUint16(Object.keys(map).length);
for (const key in map) {
that.putUint16(parseInt(key, 10));
that.putString(map[key]);
}
return that;
},
putTreeMapUInt32(map?: Messages) {
if (!map) {
that.putUint16(0);
return that;
}
that.putUint16(Object.keys(map).length);
for (const key in map) {
that.putUint16(parseInt(key, 10));
that.putUint32(map[key]);
}
return that;
},
};
that.buffer.fill(0);
return that;
};
const ReadByteBuf = (bytes: Buffer): ReadByteBufInterface => {
const that: ReadByteBufInterface = {
buffer: bytes,
position: 0,
getUint16() {
const ret = that.buffer.readUInt16LE(that.position);
that.position += 2;
return ret;
},
getUint32() {
const ret = that.buffer.readUInt32LE(that.position);
that.position += 4;
return ret;
},
getString() {
const len = that.getUint16();
const out = Buffer.alloc(len);
that.buffer.copy(out, 0, that.position, that.position + len);
that.position += len;
return out;
},
getTreeMapUInt32() {
const map: Messages = {};
const len = that.getUint16();
for (let i = 0; i < len; i++) {
const key = that.getUint16();
const value = that.getUint32();
map[key] = value;
}
return map;
},
};
return that;
};
const AccessTokenContent = (options: AccessTokenContentOptions): AccessTokenContentOptions => {
options.pack = () => {
const out = ByteBuf();
return out
.putBytes(options.signature as Buffer)
.putUint32(options.crc_channel)
.putUint32(options.crc_uid)
.putBytes(options.m as Buffer)
.pack();
};
return options;
};
const unPackContent = (bytes: Buffer): AccessTokenContentOptions => {
const readbuf = ReadByteBuf(bytes);
return AccessTokenContent({
signature: readbuf.getString(),
crc_channel_name: readbuf.getUint32(),
crc_uid: readbuf.getUint32(),
m: readbuf.getString(),
crc_channel: 0,
});
};
const Message = (options: MessageOptions): MessageOptions => {
options.pack = () => {
const out = ByteBuf();
return out.putUint32(options.salt).putUint32(options.ts).putTreeMapUInt32(options.messages).pack();
};
return options;
};
const unPackMessages = (bytes: Buffer): MessageOptions => {
const readbuf = ReadByteBuf(bytes);
return Message({
salt: readbuf.getUint32(),
ts: readbuf.getUint32(),
messages: readbuf.getTreeMapUInt32(),
});
};
export class AccessToken {
public appID: string;
public appCertificate: string;
public channelName: string;
public uid: string;
public messages: Messages;
public salt: number;
public ts: number;
public constructor(appID: string, appCertificate: string, channelName: string, uid: number | string) {
this.appID = appID;
this.appCertificate = appCertificate;
this.channelName = channelName;
this.messages = {};
this.salt = randomInt;
this.ts = Math.floor(new Date().getTime() / 1000) + 24 * 3600;
if (uid === 0) {
this.uid = '';
} else {
this.uid = `${uid}`;
}
}
public build(): string {
const m = Message({
salt: this.salt,
ts: this.ts,
messages: this.messages,
}).pack!();
const toSign = Buffer.concat([Buffer.from(this.appID, 'utf8'), Buffer.from(this.channelName, 'utf8'), Buffer.from(this.uid, 'utf8'), m]);
const signature = encodeHMac(this.appCertificate, toSign);
const crc_channel = UINT32(crc32.str(this.channelName)).and(UINT32(0xffffffff)).toNumber();
const crc_uid = UINT32(crc32.str(this.uid)).and(UINT32(0xffffffff)).toNumber();
const content = AccessTokenContent({
signature,
crc_channel,
crc_uid,
m,
}).pack!();
return version + this.appID + content.toString('base64');
}
public addPriviledge(priviledge: number, expireTimestamp: number): void {
this.messages[priviledge] = expireTimestamp;
}
public fromString(originToken: string): boolean {
try {
const originVersion = originToken.substr(0, VERSION_LENGTH);
if (originVersion !== version) {
return false;
}
this.appID = originToken.substr(VERSION_LENGTH, APP_ID_LENGTH);
const originContent = originToken.substr(VERSION_LENGTH + APP_ID_LENGTH);
const originContentDecodedBuf = Buffer.from(originContent, 'base64');
const content = unPackContent(originContentDecodedBuf);
const msgs = unPackMessages(content.m as Buffer);
this.salt = msgs.salt;
this.ts = msgs.ts;
this.messages = msgs.messages;
} catch (err) {
console.log(err);
return false;
}
return true;
}
}
export { version };

View File

@ -0,0 +1,447 @@
import crypto from 'node:crypto';
import zlib from 'node:zlib';
interface ByteBufInterface {
buffer: Buffer;
position: number;
pack: () => Buffer;
putUint16: (v: number) => ByteBufInterface;
putUint32: (v: number) => ByteBufInterface;
putInt32: (v: number) => ByteBufInterface;
putInt16: (v: number) => ByteBufInterface;
putBytes: (bytes: Buffer) => ByteBufInterface;
putString: (str: string) => ByteBufInterface;
putTreeMap: (map?: Record<string, string>) => ByteBufInterface;
putTreeMapUInt32: (map?: Privileges) => ByteBufInterface;
}
const VERSION_LENGTH = 3;
const APP_ID_LENGTH = 32;
const encodeHMac = (key: Buffer, message: Buffer | string): Buffer => {
return crypto.createHmac('sha256', key).update(message).digest();
};
const getVersion = () => {
return '007';
};
type Privileges = Record<number, number>;
class ByteBuf implements ByteBufInterface {
public buffer: Buffer;
public position: number;
public constructor() {
this.buffer = Buffer.alloc(1024);
this.position = 0;
this.buffer.fill(0);
}
public pack(): Buffer {
const out = Buffer.alloc(this.position);
this.buffer.copy(out, 0, 0, out.length);
return out;
}
public putUint16(v: number): ByteBufInterface {
this.buffer.writeUInt16LE(v, this.position);
this.position += 2;
return this;
}
public putUint32(v: number): ByteBufInterface {
this.buffer.writeUInt32LE(v, this.position);
this.position += 4;
return this;
}
public putInt32(v: number): ByteBufInterface {
this.buffer.writeInt32LE(v, this.position);
this.position += 4;
return this;
}
public putInt16(v: number): ByteBufInterface {
this.buffer.writeInt16LE(v, this.position);
this.position += 2;
return this;
}
public putBytes(bytes: Buffer): ByteBufInterface {
this.putUint16(bytes.length);
bytes.copy(this.buffer, this.position);
this.position += bytes.length;
return this;
}
public putString(str: string): ByteBufInterface {
return this.putBytes(Buffer.from(str));
}
public putTreeMap(map?: Record<string, string>): ByteBufInterface {
if (!map) {
this.putUint16(0);
return this;
}
this.putUint16(Object.keys(map).length);
for (const key in map) {
this.putUint16(parseInt(key, 10));
this.putString(map[key]);
}
return this;
}
public putTreeMapUInt32(map?: Privileges): ByteBufInterface {
if (!map) {
this.putUint16(0);
return this;
}
this.putUint16(Object.keys(map).length);
for (const key in map) {
this.putUint16(parseInt(key, 10));
this.putUint32(map[key]);
}
return this;
}
}
class ReadByteBuf {
public buffer: Buffer;
public position: number;
public constructor(bytes: Buffer) {
this.buffer = bytes;
this.position = 0;
}
public getUint16(): number {
const ret = this.buffer.readUInt16LE(this.position);
this.position += 2;
return ret;
}
public getUint32(): number {
const ret = this.buffer.readUInt32LE(this.position);
this.position += 4;
return ret;
}
public getInt16(): number {
const ret = this.buffer.readInt16LE(this.position);
this.position += 2;
return ret;
}
public getString(): string {
const len = this.getUint16();
const out = Buffer.alloc(len);
this.buffer.copy(out, 0, this.position, this.position + len);
this.position += len;
return out.toString();
}
public getTreeMapUInt32(): Privileges {
const map: Privileges = {};
const len = this.getUint16();
for (let i = 0; i < len; i++) {
const key = this.getUint16();
const value = this.getUint32();
map[key] = value;
}
return map;
}
public pack(): Buffer {
const length = this.buffer.length;
const out = Buffer.alloc(length);
this.buffer.copy(out, 0, this.position, length);
return out;
}
}
class Service {
protected __type: number;
protected __privileges: Privileges;
public constructor(service_type: number) {
this.__type = service_type;
this.__privileges = {};
}
protected __pack_type(): Buffer {
const buf = new ByteBuf();
buf.putUint16(this.__type);
return buf.pack();
}
protected __pack_privileges(): Buffer {
const buf = new ByteBuf();
buf.putTreeMapUInt32(this.__privileges);
return buf.pack();
}
public service_type(): number {
return this.__type;
}
public add_privilege(privilege: number, expire: number): void {
this.__privileges[privilege] = expire;
}
public pack() {
return Buffer.concat([this.__pack_type(), this.__pack_privileges()]);
}
public unpack(buffer: Buffer): ReadByteBuf {
const bufReader = new ReadByteBuf(buffer);
this.__privileges = bufReader.getTreeMapUInt32();
return bufReader;
}
}
const kRtcServiceType = 1;
export class ServiceRtc extends Service {
protected __channel_name: string;
protected __uid: string;
public static kPrivilegeJoinChannel = 1;
public static kPrivilegePublishAudioStream = 2;
public static kPrivilegePublishVideoStream = 3;
public static kPrivilegePublishDataStream = 4;
public constructor(channel_name: string, uid: number | string) {
super(kRtcServiceType);
this.__channel_name = channel_name;
this.__uid = uid === 0 ? '' : `${uid}`;
}
public pack() {
const buffer = new ByteBuf();
buffer.putString(this.__channel_name).putString(this.__uid);
return Buffer.concat([super.pack(), buffer.pack()]);
}
public unpack(buffer: Buffer): ReadByteBuf {
const bufReader = super.unpack(buffer);
this.__channel_name = bufReader.getString();
this.__uid = bufReader.getString();
return bufReader;
}
}
const kRtmServiceType = 2;
export class ServiceRtm extends Service {
protected __user_id: string;
public static kPrivilegeLogin = 1;
public constructor(user_id?: string) {
super(kRtmServiceType);
this.__user_id = user_id || '';
}
public pack() {
const buffer = new ByteBuf();
buffer.putString(this.__user_id);
return Buffer.concat([super.pack(), buffer.pack()]);
}
public unpack(buffer: Buffer): ReadByteBuf {
const bufReader = super.unpack(buffer);
this.__user_id = bufReader.getString();
return bufReader;
}
}
const kFpaServiceType = 4;
export class ServiceFpa extends Service {
public static kPrivilegeLogin = 1;
public constructor() {
super(kFpaServiceType);
}
public pack() {
return super.pack();
}
public unpack(buffer: Buffer): ReadByteBuf {
const bufReader = super.unpack(buffer);
return bufReader;
}
}
const kChatServiceType = 5;
export class ServiceChat extends Service {
protected __user_id: string;
public static kPrivilegeUser = 1;
public static kPrivilegeApp = 2;
public constructor(user_id?: string) {
super(kChatServiceType);
this.__user_id = user_id || '';
}
public pack() {
const buffer = new ByteBuf();
buffer.putString(this.__user_id);
return Buffer.concat([super.pack(), buffer.pack()]);
}
public unpack(buffer: Buffer): ReadByteBuf {
const bufReader = super.unpack(buffer);
this.__user_id = bufReader.getString();
return bufReader;
}
}
const kApaasServiceType = 7;
export class ServiceApaas extends Service {
protected __room_uuid: string;
protected __user_uuid: string;
protected __role: number;
public static PRIVILEGE_ROOM_USER = 1;
public static PRIVILEGE_USER = 2;
public static PRIVILEGE_APP = 3;
public constructor(roomUuid?: string, userUuid?: string, role?: number) {
super(kApaasServiceType);
this.__room_uuid = roomUuid || '';
this.__user_uuid = userUuid || '';
this.__role = role || -1;
}
public 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()]);
}
public unpack(buffer: Buffer): ReadByteBuf {
const bufReader = super.unpack(buffer);
this.__room_uuid = bufReader.getString();
this.__user_uuid = bufReader.getString();
this.__role = bufReader.getInt16();
return bufReader;
}
}
type Services = Record<number, Service>;
export class AccessToken2 {
public appId: string;
public appCertificate: string;
public issueTs: number;
public expire: number;
public salt: number;
public services: Services;
public static kServices: Record<number, new (...args: any[]) => Service> = {};
public constructor(appId: string, appCertificate: string, issueTs?: number, expire?: number) {
this.appId = appId;
this.appCertificate = appCertificate;
this.issueTs = issueTs || new Date().getTime() / 1000;
this.expire = expire || 0;
// salt ranges in (1, 99999999)
this.salt = Math.floor(Math.random() * 99999999) + 1;
this.services = {};
}
private __signing() {
let signing = encodeHMac(new ByteBuf().putUint32(this.issueTs).pack(), this.appCertificate);
signing = encodeHMac(new ByteBuf().putUint32(this.salt).pack(), signing);
return signing;
}
private __build_check() {
const is_uuid = (data: string): boolean => {
if (data.length !== APP_ID_LENGTH) {
return false;
}
const buf = Buffer.from(data, 'hex');
return Boolean(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;
}
public add_service(service: Service): void {
this.services[service.service_type()] = service;
}
public 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) => {
signing_info = Buffer.concat([signing_info, service.pack()]);
});
const signature = encodeHMac(signing, signing_info);
const content = Buffer.concat([new ByteBuf().putBytes(signature).pack(), signing_info]);
const compressed = zlib.deflateSync(content);
return `${getVersion()}${Buffer.from(compressed).toString('base64')}`;
}
public from_string(origin_token: string): boolean {
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(Buffer.from(origin_content, 'base64'));
const bufferReader = new ReadByteBuf(buffer);
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.kServices[service_type]();
remainBuf = service.unpack(bufferReaderService.pack()).pack();
this.services[service_type] = service;
}
return true;
}
}
// 初始化 kServices
AccessToken2.kServices[kApaasServiceType] = ServiceApaas;
AccessToken2.kServices[kChatServiceType] = ServiceChat;
AccessToken2.kServices[kFpaServiceType] = ServiceFpa;
AccessToken2.kServices[kRtcServiceType] = ServiceRtc;
AccessToken2.kServices[kRtmServiceType] = ServiceRtm;
export { kApaasServiceType, kChatServiceType, kFpaServiceType, kRtcServiceType, kRtmServiceType };

View File

@ -0,0 +1,65 @@
import md5 from 'md5';
import { AccessToken2, ServiceApaas, ServiceChat, ServiceRtm } from './AccessToken2';
export class ApaasTokenBuilder {
/**
* build user room token
* @param appId - The App ID issued to you by Agora.
* @param appCertificate - Certificate of the application that you registered in the Agora Dashboard.
* @param roomUuid - The room's id, must be unique.
* @param userUuid - The user's id, must be unique.
* @param role - The user's role.
* @param expire - represented by the number of seconds elapsed since now.
* @returns The user room token.
*/
public static buildRoomUserToken(appId: string, appCertificate: string, roomUuid: string, userUuid: string, role: number, expire: number): string {
const accessToken = new AccessToken2(appId, appCertificate, 0, expire);
const chatUserId = md5(userUuid);
const apaasService = new ServiceApaas(roomUuid, userUuid, role);
accessToken.add_service(apaasService);
const rtmService = new ServiceRtm(userUuid);
rtmService.add_privilege(ServiceRtm.kPrivilegeLogin, expire);
accessToken.add_service(rtmService);
const chatService = new ServiceChat(chatUserId);
chatService.add_privilege(ServiceChat.kPrivilegeUser, expire);
accessToken.add_service(chatService);
return accessToken.build();
}
/**
* build user token
* @param appId - The App ID issued to you by Agora.
* @param appCertificate - Certificate of the application that you registered in the Agora Dashboard.
* @param userUuid - The user's id, must be unique.
* @param expire - represented by the number of seconds elapsed since now.
* @returns The user token.
*/
public static buildUserToken(appId: string, appCertificate: string, userUuid: string, expire: number): string {
const accessToken = new AccessToken2(appId, appCertificate, 0, expire);
const apaasService = new ServiceApaas('', userUuid);
apaasService.add_privilege(ServiceApaas.PRIVILEGE_USER, expire);
accessToken.add_service(apaasService);
return accessToken.build();
}
/**
* build app token
* @param appId - The App ID issued to you by Agora.
* @param appCertificate - Certificate of the application that you registered in the Agora Dashboard.
* @param expire - represented by the number of seconds elapsed since now.
* @returns The app token.
*/
public static buildAppToken(appId: string, appCertificate: string, expire: number): string {
const accessToken = new AccessToken2(appId, appCertificate, 0, expire);
const apaasService = new ServiceApaas();
apaasService.add_privilege(ServiceApaas.PRIVILEGE_APP, expire);
accessToken.add_service(apaasService);
return accessToken.build();
}
}

View File

@ -0,0 +1,34 @@
import { AccessToken2, ServiceChat } from './AccessToken2';
export class ChatTokenBuilder {
/**
* Build the Chat user token.
* @param appId - The App ID issued to you by Agora.
* @param appCertificate - Certificate of the application that you registered in the Agora Dashboard.
* @param userUuid - The user's id, must be unique.
* @param expire - represented by the number of seconds elapsed since now.
* @returns The Chat User token.
*/
public static buildUserToken(appId: string, appCertificate: string, userUuid: string, expire: number): string {
const token = new AccessToken2(appId, appCertificate, undefined, expire);
const serviceChat = new ServiceChat(userUuid);
serviceChat.add_privilege(ServiceChat.kPrivilegeUser, expire);
token.add_service(serviceChat);
return token.build();
}
/**
* Build the Chat App token.
* @param appId - The App ID issued to you by Agora.
* @param appCertificate - Certificate of the application that you registered in the Agora Dashboard.
* @param expire - represented by the number of seconds elapsed since now.
* @returns The Chat App token.
*/
public static buildAppToken(appId: string, appCertificate: string, expire: number): string {
const token = new AccessToken2(appId, appCertificate, undefined, expire);
const serviceChat = new ServiceChat();
serviceChat.add_privilege(ServiceChat.kPrivilegeApp, expire);
token.add_service(serviceChat);
return token.build();
}
}

View File

@ -0,0 +1,251 @@
import crypto from 'node:crypto';
const version = '005';
export const noUpload = '0';
export const audioVideoUpload = '3';
// Service Type
const MEDIA_CHANNEL_SERVICE = 1;
const RECORDING_SERVICE = 2;
const PUBLIC_SHARING_SERVICE = 3;
const IN_CHANNEL_PERMISSION = 4;
// InChannelPermissionKey
const ALLOW_UPLOAD_IN_CHANNEL = 1;
type ExtraMap = Record<number, string>;
interface MessageOptions {
serviceType: number;
appID: Buffer;
unixTs: number;
salt: number;
channelName: string;
uid: number;
expiredTs: number;
extra?: ExtraMap;
pack?: () => Buffer;
}
interface DynamicKey5ContentOptions {
serviceType: number;
signature: string;
appID: Buffer;
unixTs: number;
salt: number;
expiredTs: number;
extra?: ExtraMap;
pack?: () => Buffer;
}
interface ByteBufInterface {
buffer: Buffer;
position: number;
pack: () => Buffer;
putUint16: (v: number) => ByteBufInterface;
putUint32: (v: number) => ByteBufInterface;
putBytes: (bytes: Buffer) => ByteBufInterface;
putString: (str: string) => ByteBufInterface;
putTreeMap: (map?: ExtraMap) => ByteBufInterface;
}
const ByteBuf = (): ByteBufInterface => {
const that: ByteBufInterface = {
buffer: Buffer.alloc(1024),
position: 0,
pack() {
const out = Buffer.alloc(that.position);
that.buffer.copy(out, 0, 0, out.length);
return out;
},
putUint16(v: number) {
that.buffer.writeUInt16LE(v, that.position);
that.position += 2;
return that;
},
putUint32(v: number) {
that.buffer.writeUInt32LE(v, that.position);
that.position += 4;
return that;
},
putBytes(bytes: Buffer) {
that.putUint16(bytes.length);
bytes.copy(that.buffer, that.position);
that.position += bytes.length;
return that;
},
putString(str: string) {
return that.putBytes(Buffer.from(str));
},
putTreeMap(map?: ExtraMap) {
if (!map) {
that.putUint16(0);
return that;
}
that.putUint16(Object.keys(map).length);
for (const key in map) {
that.putUint16(parseInt(key, 10));
that.putString(map[key]);
}
return that;
},
};
that.buffer.fill(0);
return that;
};
const hexDecode = (str: string): Buffer => {
return Buffer.from(str, 'hex');
};
const encodeHMac = (key: Buffer, message: Buffer): string => {
return crypto.createHmac('sha1', key).update(message).digest('hex').toUpperCase();
};
const Message = (options: MessageOptions): MessageOptions => {
options.pack = () => {
const out = ByteBuf();
return out
.putUint16(options.serviceType)
.putBytes(options.appID)
.putUint32(options.unixTs)
.putUint32(options.salt)
.putString(options.channelName)
.putUint32(options.uid)
.putUint32(options.expiredTs)
.putTreeMap(options.extra)
.pack();
};
return options;
};
const generateSignature5 = (
appCertificate: string,
serviceType: number,
appID: string,
unixTs: number,
randomInt: number,
channelName: string,
uid: number,
expiredTs: number,
extra?: ExtraMap
): string => {
const rawAppID = hexDecode(appID);
const rawAppCertificate = hexDecode(appCertificate);
const m = Message({
serviceType,
appID: rawAppID,
unixTs,
salt: randomInt,
channelName,
uid,
expiredTs,
extra,
});
const toSign = m.pack!();
return encodeHMac(rawAppCertificate, toSign);
};
const DynamicKey5Content = (options: DynamicKey5ContentOptions): DynamicKey5ContentOptions => {
options.pack = () => {
const out = ByteBuf();
return out
.putUint16(options.serviceType)
.putString(options.signature)
.putBytes(options.appID)
.putUint32(options.unixTs)
.putUint32(options.salt)
.putUint32(options.expiredTs)
.putTreeMap(options.extra)
.pack();
};
return options;
};
export const generateDynamicKey = (
appID: string,
appCertificate: string,
channelName: string,
unixTs: number,
randomInt: number,
uid: number,
expiredTs: number,
extra?: ExtraMap,
serviceType: number = MEDIA_CHANNEL_SERVICE
): string => {
const signature = generateSignature5(appCertificate, serviceType, appID, unixTs, randomInt, channelName, uid, expiredTs, extra);
const content = DynamicKey5Content({
serviceType,
signature,
appID: hexDecode(appID),
unixTs,
salt: randomInt,
expiredTs,
extra,
}).pack!();
return version + content.toString('base64');
};
export const generatePublicSharingKey = (
appID: string,
appCertificate: string,
channelName: string,
unixTs: number,
randomInt: number,
uid: number,
expiredTs: number
): string => {
const channelNameStr = channelName.toString();
return generateDynamicKey(appID, appCertificate, channelNameStr, unixTs, randomInt, uid, expiredTs, undefined, PUBLIC_SHARING_SERVICE);
};
export const generateRecordingKey = (
appID: string,
appCertificate: string,
channelName: string,
unixTs: number,
randomInt: number,
uid: number,
expiredTs: number
): string => {
const channelNameStr = channelName.toString();
return generateDynamicKey(appID, appCertificate, channelNameStr, unixTs, randomInt, uid, expiredTs, undefined, RECORDING_SERVICE);
};
export const generateMediaChannelKey = (
appID: string,
appCertificate: string,
channelName: string,
unixTs: number,
randomInt: number,
uid: number,
expiredTs: number
): string => {
const channelNameStr = channelName.toString();
return generateDynamicKey(appID, appCertificate, channelNameStr, unixTs, randomInt, uid, expiredTs, undefined, MEDIA_CHANNEL_SERVICE);
};
export const generateInChannelPermissionKey = (
appID: string,
appCertificate: string,
channelName: string,
unixTs: number,
randomInt: number,
uid: number,
expiredTs: number,
permission: string
): string => {
const extra: ExtraMap = {};
extra[ALLOW_UPLOAD_IN_CHANNEL] = permission;
return generateDynamicKey(appID, appCertificate, channelName, unixTs, randomInt, uid, expiredTs, extra, IN_CHANNEL_PERMISSION);
};
export { version };

View File

@ -0,0 +1,65 @@
import md5 from 'md5';
import { AccessToken2, ServiceApaas, ServiceChat, ServiceRtm } from './AccessToken2';
export class EducationTokenBuilder {
/**
* build user room token
* @param appId - The App ID issued to you by Agora.
* @param appCertificate - Certificate of the application that you registered in the Agora Dashboard.
* @param roomUuid - The room's id, must be unique.
* @param userUuid - The user's id, must be unique.
* @param role - The user's role.
* @param expire - represented by the number of seconds elapsed since now.
* @returns The user room token.
*/
public static buildRoomUserToken(appId: string, appCertificate: string, roomUuid: string, userUuid: string, role: number, expire: number): string {
const accessToken = new AccessToken2(appId, appCertificate, 0, expire);
const chatUserId = md5(userUuid);
const apaasService = new ServiceApaas(roomUuid, userUuid, role);
accessToken.add_service(apaasService);
const rtmService = new ServiceRtm(userUuid);
rtmService.add_privilege(ServiceRtm.kPrivilegeLogin, expire);
accessToken.add_service(rtmService);
const chatService = new ServiceChat(chatUserId);
chatService.add_privilege(ServiceChat.kPrivilegeUser, expire);
accessToken.add_service(chatService);
return accessToken.build();
}
/**
* build user token
* @param appId - The App ID issued to you by Agora.
* @param appCertificate - Certificate of the application that you registered in the Agora Dashboard.
* @param userUuid - The user's id, must be unique.
* @param expire - represented by the number of seconds elapsed since now.
* @returns The user token.
*/
public static buildUserToken(appId: string, appCertificate: string, userUuid: string, expire: number): string {
const accessToken = new AccessToken2(appId, appCertificate, 0, expire);
const apaasService = new ServiceApaas('', userUuid);
apaasService.add_privilege(ServiceApaas.PRIVILEGE_USER, expire);
accessToken.add_service(apaasService);
return accessToken.build();
}
/**
* build app token
* @param appId - The App ID issued to you by Agora.
* @param appCertificate - Certificate of the application that you registered in the Agora Dashboard.
* @param expire - represented by the number of seconds elapsed since now.
* @returns The app token.
*/
public static buildAppToken(appId: string, appCertificate: string, expire: number): string {
const accessToken = new AccessToken2(appId, appCertificate, 0, expire);
const apaasService = new ServiceApaas();
apaasService.add_privilege(ServiceApaas.PRIVILEGE_APP, expire);
accessToken.add_service(apaasService);
return accessToken.build();
}
}

View File

@ -0,0 +1,19 @@
import { AccessToken2, ServiceFpa } from './AccessToken2';
export class FpaTokenBuilder {
/**
* Build the FPA token.
* @param appId - The App ID issued to you by Agora.
* @param appCertificate - Certificate of the application that you registered in the Agora Dashboard.
* @returns The FPA token.
*/
public static buildToken(appId: string, appCertificate: string): string {
const token = new AccessToken2(appId, appCertificate, 0, 24 * 3600);
const serviceFpa = new ServiceFpa();
serviceFpa.add_privilege(ServiceFpa.kPrivilegeLogin, 0);
token.add_service(serviceFpa);
return token.build();
}
}

View File

@ -0,0 +1,59 @@
import { AccessToken, priviledges } from './AccessToken';
export enum Role {
// DEPRECATED. Role::ATTENDEE has the same privileges as Role.PUBLISHER.
ATTENDEE = 0,
// RECOMMENDED. Use this role for a voice/video call or a live broadcast
PUBLISHER = 1,
// Only use this role if your scenario require authentication for Co-host
SUBSCRIBER = 2,
// DEPRECATED. Role.ADMIN has the same privileges as Role.PUBLISHER.
ADMIN = 101,
}
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.
* @param uid - User ID. A 32-bit unsigned integer with a value ranging from 1 to (2^32-1).
* @param role - See #userRole.
* @param privilegeExpiredTs - represented by the number of seconds elapsed since 1/1/1970.
* @returns The new Token.
*/
public static buildTokenWithUid(appID: string, appCertificate: string, channelName: string, uid: number, role: Role, privilegeExpiredTs: number): string {
return this.buildTokenWithAccount(appID, appCertificate, channelName, uid, role, privilegeExpiredTs);
}
/**
* Builds an RTC token with account.
* @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.
* @param account - The user account.
* @param role - See #userRole.
* @param privilegeExpiredTs - represented by the number of seconds elapsed since 1/1/1970.
* @returns The new Token.
*/
public static buildTokenWithAccount(
appID: string,
appCertificate: string,
channelName: string,
account: number | string,
role: Role,
privilegeExpiredTs: number
): string {
const key = new AccessToken(appID, appCertificate, channelName, account);
key.addPriviledge(priviledges.kJoinChannel, privilegeExpiredTs);
if (role === Role.ATTENDEE || role === Role.PUBLISHER || role === Role.ADMIN) {
key.addPriviledge(priviledges.kPublishAudioStream, privilegeExpiredTs);
key.addPriviledge(priviledges.kPublishVideoStream, privilegeExpiredTs);
key.addPriviledge(priviledges.kPublishDataStream, privilegeExpiredTs);
}
return key.build();
}
}

View File

@ -0,0 +1,234 @@
import { AccessToken2, ServiceRtc, ServiceRtm } from './AccessToken2';
export enum Role {
/**
* 推荐使用。如果您的场景不需要对联合主播进行身份验证,
* 请使用此角色进行语音/视频通话或直播。
*/
PUBLISHER = 1,
/**
* 仅当您的场景需要对联合主播进行身份验证时才使用此角色。
* 为了使此角色生效,请联系我们的支持团队为您启用联合主播身份验证。
* 否则Role_Subscriber 仍然具有与 Role_Publisher 相同的权限。
*/
SUBSCRIBER = 2,
}
export class RtcTokenBuilder {
/**
* 使用 uid 构建 RTC Token
* @param appId - 声网颁发给您的 App ID
* @param appCertificate - 您在声网控制台注册的应用程序证书
* @param channelName - 字符串格式的 AgoraRTC 会话的唯一频道名称
* @param uid - 用户 ID。范围从 1 到 (2^32-1) 的 32 位无符号整数
* @param role - 用户角色
* @param tokenExpire - 从现在开始经过的秒数表示
* @param privilegeExpire - 从现在开始经过的秒数表示
* @returns RTC Token
*/
public static buildTokenWithUid(
appId: string,
appCertificate: string,
channelName: string,
uid: number | string,
role: Role,
tokenExpire: number,
privilegeExpire = 0
): string {
return this.buildTokenWithUserAccount(appId, appCertificate, channelName, uid, role, tokenExpire, privilegeExpire);
}
/**
* 使用账户构建 RTC Token
* @param appId - 声网颁发给您的 App ID
* @param appCertificate - 您在声网控制台注册的应用程序证书
* @param channelName - 字符串格式的 AgoraRTC 会话的唯一频道名称
* @param account - 用户账户
* @param role - 用户角色
* @param tokenExpire - 从现在开始经过的秒数表示
* @param privilegeExpire - 从现在开始经过的秒数表示
* @returns RTC Token
*/
public static buildTokenWithUserAccount(
appId: string,
appCertificate: string,
channelName: string,
account: number | string,
role: Role,
tokenExpire: number,
privilegeExpire = 0
): string {
const token = new AccessToken2(appId, appCertificate, 0, tokenExpire);
const serviceRtc = new ServiceRtc(channelName, account);
serviceRtc.add_privilege(ServiceRtc.kPrivilegeJoinChannel, privilegeExpire);
if (role === Role.PUBLISHER) {
serviceRtc.add_privilege(ServiceRtc.kPrivilegePublishAudioStream, privilegeExpire);
serviceRtc.add_privilege(ServiceRtc.kPrivilegePublishVideoStream, privilegeExpire);
serviceRtc.add_privilege(ServiceRtc.kPrivilegePublishDataStream, privilegeExpire);
}
token.add_service(serviceRtc);
return token.build();
}
/**
* Generates an RTC token with the specified privilege.
* @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.
* @param uid - The user ID.
* @param tokenExpire - represented by the number of seconds elapsed since now.
* @param joinChannelPrivilegeExpire - represented by the number of seconds elapsed since now.
* @param pubAudioPrivilegeExpire - represented by the number of seconds elapsed since now.
* @param pubVideoPrivilegeExpire - represented by the number of seconds elapsed since now.
* @param pubDataStreamPrivilegeExpire - represented by the number of seconds elapsed since now.
* @returns The RTC Token
*/
public static buildTokenWithUidAndPrivilege(
appId: string,
appCertificate: string,
channelName: string,
uid: number | string,
tokenExpire: number,
joinChannelPrivilegeExpire: number,
pubAudioPrivilegeExpire: number,
pubVideoPrivilegeExpire: number,
pubDataStreamPrivilegeExpire: number
): string {
return this.BuildTokenWithUserAccountAndPrivilege(
appId,
appCertificate,
channelName,
uid,
tokenExpire,
joinChannelPrivilegeExpire,
pubAudioPrivilegeExpire,
pubVideoPrivilegeExpire,
pubDataStreamPrivilegeExpire
);
}
/**
* Generates an RTC token with the specified privilege.
* @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.
* @param userAccount - The user account.
* @param tokenExpire - represented by the number of seconds elapsed since now.
* @param joinChannelPrivilegeExpire - represented by the number of seconds elapsed since now.
* @param pubAudioPrivilegeExpire - represented by the number of seconds elapsed since now.
* @param pubVideoPrivilegeExpire - represented by the number of seconds elapsed since now.
* @param pubDataStreamPrivilegeExpire - represented by the number of seconds elapsed since now.
* @returns The RTC Token.
*/
public static BuildTokenWithUserAccountAndPrivilege(
appId: string,
appCertificate: string,
channelName: string,
account: number | string,
tokenExpire: number,
joinChannelPrivilegeExpire: number,
pubAudioPrivilegeExpire: number,
pubVideoPrivilegeExpire: number,
pubDataStreamPrivilegeExpire: number
): string {
const token = new AccessToken2(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();
}
/**
* Build an RTC and RTM token with account.
* @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.
* @param account - The user account.
* @param role - See #userRole.
* @param tokenExpire - represented by the number of seconds elapsed since now.
* @param privilegeExpire - represented by the number of seconds elapsed since now.
* @returns The RTC and RTM Token.
*/
public static buildTokenWithRtm(
appId: string,
appCertificate: string,
channelName: string,
account: number | string,
role: Role,
tokenExpire: number,
privilegeExpire = 0
): string {
const token = new AccessToken2(appId, appCertificate, 0, tokenExpire);
const serviceRtc = new ServiceRtc(channelName, account);
serviceRtc.add_privilege(ServiceRtc.kPrivilegeJoinChannel, privilegeExpire);
if (role === Role.PUBLISHER) {
serviceRtc.add_privilege(ServiceRtc.kPrivilegePublishAudioStream, privilegeExpire);
serviceRtc.add_privilege(ServiceRtc.kPrivilegePublishVideoStream, privilegeExpire);
serviceRtc.add_privilege(ServiceRtc.kPrivilegePublishDataStream, privilegeExpire);
}
token.add_service(serviceRtc);
const serviceRtm = new ServiceRtm(String(account));
serviceRtm.add_privilege(ServiceRtm.kPrivilegeLogin, tokenExpire);
token.add_service(serviceRtm);
return token.build();
}
/**
* Build an RTC and RTM token with account.
* @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.
* @param rtcAccount - The RTC user's account, max length is 255 Bytes.
* @param rtcRole - See #userRole.
* @param rtcTokenExpire - represented by the number of seconds elapsed since now.
* @param joinChannelPrivilegeExpire - represented by the number of seconds elapsed since now.
* @param pubAudioPrivilegeExpire - represented by the number of seconds elapsed since now.
* @param pubVideoPrivilegeExpire - represented by the number of seconds elapsed since now.
* @param pubDataStreamPrivilegeExpire - represented by the number of seconds elapsed since now.
* @param rtmUserId - The RTM user's account, max length is 255 Bytes.
* @param rtmTokenExpire - represented by the number of seconds elapsed since now.
* @returns The RTC and RTM Token.
*/
public static buildTokenWithRtm2(
appId: string,
appCertificate: string,
channelName: string,
rtcAccount: number | string,
rtcRole: Role,
rtcTokenExpire: number,
joinChannelPrivilegeExpire: number,
pubAudioPrivilegeExpire: number,
pubVideoPrivilegeExpire: number,
pubDataStreamPrivilegeExpire: number,
rtmUserId: string,
rtmTokenExpire: number
): string {
const token = new AccessToken2(appId, appCertificate, 0, rtcTokenExpire);
const serviceRtc = new ServiceRtc(channelName, rtcAccount);
serviceRtc.add_privilege(ServiceRtc.kPrivilegeJoinChannel, joinChannelPrivilegeExpire);
if (rtcRole === Role.PUBLISHER) {
serviceRtc.add_privilege(ServiceRtc.kPrivilegePublishAudioStream, pubAudioPrivilegeExpire);
serviceRtc.add_privilege(ServiceRtc.kPrivilegePublishVideoStream, pubVideoPrivilegeExpire);
serviceRtc.add_privilege(ServiceRtc.kPrivilegePublishDataStream, pubDataStreamPrivilegeExpire);
}
token.add_service(serviceRtc);
const serviceRtm = new ServiceRtm(rtmUserId);
serviceRtm.add_privilege(ServiceRtm.kPrivilegeLogin, rtmTokenExpire);
token.add_service(serviceRtm);
return token.build();
}
}

View File

@ -0,0 +1,22 @@
import { AccessToken, priviledges } from './AccessToken';
export enum Role {
Rtm_User = 1,
}
export class RtmTokenBuilder {
/**
* Build RTM token
* @param appID - The App ID issued to you by Agora.
* @param appCertificate - Certificate of the application that you registered in the Agora Dashboard.
* @param account - The user account.
* @param role - User role
* @param privilegeExpiredTs - represented by the number of seconds elapsed since 1/1/1970.
* @returns token
*/
public static buildToken(appID: string, appCertificate: string, account: string, role: Role, privilegeExpiredTs: number): string {
const key = new AccessToken(appID, appCertificate, account, '');
key.addPriviledge(priviledges.kRtmLogin, privilegeExpiredTs);
return key.build();
}
}

View File

@ -0,0 +1,21 @@
import { AccessToken2, ServiceRtm } from './AccessToken2';
export class RtmTokenBuilder {
/**
* Build the RTM token.
* @param appId - The App ID issued to you by Agora.
* @param appCertificate - Certificate of the application that you registered in the Agora Dashboard.
* @param userId - The user's account, max length is 64 Bytes.
* @param expire - represented by the number of seconds elapsed since now.
* @returns The RTM token.
*/
public static buildToken(appId: string, appCertificate: string, userId: string, expire: number): string {
const token = new AccessToken2(appId, appCertificate, undefined, expire);
const serviceRtm = new ServiceRtm(userId);
serviceRtm.add_privilege(ServiceRtm.kPrivilegeLogin, expire);
token.add_service(serviceRtm);
return token.build();
}
}

View File

@ -0,0 +1,43 @@
import md5 from 'md5';
export class SignalingToken {
/**
* Get Signaling Token
* @param appid - The App ID
* @param appcertificate - The App Certificate
* @param account - The user account
* @param validTimeInSeconds - Valid time in seconds
* @returns The Signaling Token
*/
public static get(appid: string, appcertificate: string, account: string, validTimeInSeconds: number): string {
const expiredTime = parseInt(String(new Date().getTime() / 1000), 10) + validTimeInSeconds;
const token_items: string[] = [];
// append SDK VERSION
token_items.push('1');
// append appid
token_items.push(appid);
// expired time
token_items.push(String(expiredTime));
// md5 account + appid + appcertificate + expiredtime
token_items.push(md5(account + appid + appcertificate + expiredTime));
return token_items.join(':');
}
/**
* Convenience function to get token valid within 1 day
* @param appid - The App ID
* @param appcertificate - The App Certificate
* @param account - The user account
* @returns The Signaling Token valid for 1 day
*/
public static get1DayToken(appid: string, appcertificate: string, account: string): string {
return SignalingToken.get(appid, appcertificate, account, 3600 * 24);
}
}
export default SignalingToken;