현재 사이드 프로젝트로 개발 중인 앱은 저자와 독자 간의 모임을 만드는 플랫폼인데, 온라인 모임을 위해 Zoom을 이용합니다. 서비스 초기에는 플랫폼 측에서 모임을 개설하는 저자님들을 위해 줌 미팅을 대신 만들어 제공해보기로 했습니다.
사람이 일일이 모임마다 줌 예약 미팅 링크를 만들어 넣어줄 수도 있지만 모임이 많아질수록 너무 번거롭고 실수로 다른 링크를 넣을 수도 있습니다. 그래서 Zoom API를 연결해 모임 타입을 '온라인'으로 선택하고 모임을 공개할 경우 자동으로 줌 미팅 링크를 생성해 넣어주는 기능을 만들 생각을 하게 됐습니다.
준비물: Nestjs 앱, 줌 계정
도움 받음:
줌에 로그인한 뒤, 좌측 메뉴에서 Advanced > 앱 Marketplace로 이동
우측 상단 메뉴에서 Build Server-to-Server App 선택
App Name을 입력하고 Create, App Credentials 확인하고 기본 Information 입력 후 Continue해줍니다.
Scopes에서 원하는 권한을 체크해줍니다.
activate를 눌러 앱을 활성화해줍니다.
(코딩하기 전에 Postman이나 insomnia 등의 앱을 이용해 테스트해보실 수 있어요!)
위에서 받은 'client ID'와 'client secret'을 "client_id:client_secret" 형태로 문자열을 합친 뒤 base64 인코딩합니다.
예를 들어 부여받은 client_id: hVYv7sEcM4ahGzSFWUo1ag
client_secret: 2ckag1KABmTYZ1xJYqRmxa4OOKssEK8D 라면
hVYv7sEcM4ahGzSFWUo1ag:2ckag1KABmTYZ1xJYqRmxa4OOKssEK8D
를 base64 encode하여, 아래와 같은 문자열을 만드는 겁니다. aFZZdjdzRWNNNGFoR3pTRldVbzFhZzoyY2thZzFLQUJtVFlaMXhKWXFSbXhhNE9PS3NzRUs4RA==
(https://www.base64encode.org/)
zoom oauth API에 accessToken을 요청합니다.
(주의: 헤더 Authorization 항목에 "Basic"하고 한칸 띄운 뒤 1번에서 Base64로 인코딩한 Client_id:Client_secret 문자열을 넣어줍니다.)
{
"access_token": "eyJzdiI6IjAwMDAwMSIsImFsZyI6IkhTNTEyIiwidiI6IjIuMCIsImtpZCI6Ijg5ODE1ZWMwLTdkZTktNGNjZS1hMzQzLTljNDQ0MGFiMGY5MCJ9.eyJhdWQiOiJodHRwczovL29hdXRoLnpvb20udXMiLCJ1aWQiOiJmcWpZZkczOVE1Ml83UmdOQlJ1TGlBIiwidmVyIjo5LCJhdWlkIjoiNDJkYWE2ZjVkZmE5MjAxNDUyOWRjMzMwMGIxZWMzMjkiLCJuYmYiOjE3MTQ4Njg2MjYsImNvasdjhfnHZvRDNRb1MydXVpY0hKdUZmZEV3VUVYclg2SzhzSTMiLCJpc3MiOiJ6bTpjaWQ6aFZZdjdzV2tTOWFoVXpTRldVbzhqZyIsImdubyI6MCwiZXhwIjoxNzE0ODcyMasdfaamsJ0eXBlIjozLCJpYXQiOjjEMD24Njg2MjYsImFpZCI6InROX1ZRZjVuUnJxR25vb3UxRTgtNGcifQ.U6ctbM-YPAurT949wza6RAyeXos9ASDfisxMcTVOyVUiwcIgG4DHujvSQEYHphZ9RQ-0Uh450-nFcq31u0syQ",
"token_type": "bearer",
"expires_in": 3600,
"scope": "meeting:write:meeting:admin meeting:update:meeting:admin meeting:write:invite_links:admin meeting:update:status:admin"
}
참조: https://developers.zoom.us/docs/internal-apps/s2s-oauth/#generate-access-token
위에서 받은 accessToken을 통해 Zoom REST API로 원하는 요청을 할 수 있습니다.
줌 API document에서 REST API > meetings api 문서로 이동해 미팅 생성 기능을 제공하는 api를 찾았습니다.
https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meetingCreate
(24-05-05 기준)
기본 API host 도메인은 https://api.zoom.us/v2 입니다
참조: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#servers
필수값이 없어 위처럼 빈 JSON object({})만 보내도 모임 생성에 성공하는 것을 확인할 수 있었습니다. 저는 Insomnia app으로 여기까지 테스트해보았습니다.
ZoomApiModule, Service를 만들었습니다.
import { HttpService } from '@nestjs/axios';
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { Meetup } from '@prisma/client/wasm';
import { catchError, firstValueFrom } from 'rxjs';
import { dateUtil } from '../util/date-util';
@Injectable()
export class ZoomApiService {
baseUrl = 'https://api.zoom.us/v2';
constructor(private readonly httpService: HttpService) {}
// async onApplicationBootstrap() {
// console.log(await this.getAccessToken());
// }
private async getAccessToken(): Promise<string> {
const accountId = process.env.ZOOM_ACCOUNT_ID;
const base64EncodedClientInfo = Buffer.from(
`${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`,
).toString('base64');
// application/x-www-form-urlencoded 로 보내야함
const params = new URLSearchParams();
params.append('account_id', accountId as string);
params.append('grant_type', 'account_credentials');
const urlEncodedParams = params.toString();
const { data } = await firstValueFrom(
this.httpService
.post(
`https://zoom.us/oauth/token?` + urlEncodedParams,
{},
{
headers: {
ContentType: 'application/x-www-form-urlencoded',
Authorization: `Basic ${base64EncodedClientInfo}`,
},
},
)
.pipe(
catchError((error) => {
console.log(error);
throw new InternalServerErrorException(
'ZOOM 액세스 토큰 요청 에러가 발생했습니다.',
);
}),
),
);
return data.access_token;
}
async createMeeting(meetup: Meetup) {
const subUrl = '/users/me/meetings';
const body = {
topic: meetup.title,
type: 2, // 2: A scheduled meeting. (default: 2) // 1: An instant meeting.
start_time: dateUtil.convertUtcToKstDate(meetup.talkStartAt as Date),
duration: 150,
timezone: 'Asia/Seoul',
settings: {
host_video: true,
participant_video: true,
mute_upon_entry: true, // 참가시 음소거 여부 default: false
waiting_room: true,
},
};
return await this.requestZoomApi(subUrl, body);
}
async requestZoomApi(subUrl: string, body: any) {
const accessToken = await this.getAccessToken();
const headers = {
headers: {
Authorization: `Bearer ${accessToken}`,
},
};
const { data } = await firstValueFrom(
this.httpService.post(`${this.baseUrl}${subUrl}`, body, headers).pipe(
catchError((error) => {
console.log(error);
throw new InternalServerErrorException(
'ZOOM API 에러가 발생했습니다.',
);
}),
),
);
return data;
}
}
return되는 데이터는 start_time, join_url 등 엄청 많은데, 저는 join_url만 일단 저장하도록 했습니다.
자세한 request에설정할 수 있는 파라미터와 response로 돌려주는 정보는 위에 api 문서에서 확인하실 수 있습니다!