Nestjs + Zoom API 연결, 모임 생성하기

·2024년 5월 5일
0

nestjs

목록 보기
10/10

배경, 하고자하는 것

현재 사이드 프로젝트로 개발 중인 앱은 저자와 독자 간의 모임을 만드는 플랫폼인데, 온라인 모임을 위해 Zoom을 이용합니다. 서비스 초기에는 플랫폼 측에서 모임을 개설하는 저자님들을 위해 줌 미팅을 대신 만들어 제공해보기로 했습니다.
사람이 일일이 모임마다 줌 예약 미팅 링크를 만들어 넣어줄 수도 있지만 모임이 많아질수록 너무 번거롭고 실수로 다른 링크를 넣을 수도 있습니다. 그래서 Zoom API를 연결해 모임 타입을 '온라인'으로 선택하고 모임을 공개할 경우 자동으로 줌 미팅 링크를 생성해 넣어주는 기능을 만들 생각을 하게 됐습니다.

준비물: Nestjs 앱, 줌 계정

도움 받음:

줌 API 연결하기

줌에서 앱 생성하고 API 키 받기

줌에 로그인한 뒤, 좌측 메뉴에서 Advanced > 앱 Marketplace로 이동

우측 상단 메뉴에서 Build Server-to-Server App 선택

App Name을 입력하고 Create, App Credentials 확인하고 기본 Information 입력 후 Continue해줍니다.

Scopes에서 원하는 권한을 체크해줍니다.

activate를 눌러 앱을 활성화해줍니다.

Oauth로 accessToken 받기

(코딩하기 전에 Postman이나 insomnia 등의 앱을 이용해 테스트해보실 수 있어요!)

  1. 위에서 받은 'client ID'와 'client secret'을 "client_id:client_secret" 형태로 문자열을 합친 뒤 base64 인코딩합니다.
    예를 들어 부여받은 client_id: hVYv7sEcM4ahGzSFWUo1ag
    client_secret: 2ckag1KABmTYZ1xJYqRmxa4OOKssEK8D 라면
    hVYv7sEcM4ahGzSFWUo1ag:2ckag1KABmTYZ1xJYqRmxa4OOKssEK8D
    를 base64 encode하여, 아래와 같은 문자열을 만드는 겁니다. aFZZdjdzRWNNNGFoR3pTRldVbzFhZzoyY2thZzFLQUJtVFlaMXhKWXFSbXhhNE9PS3NzRUs4RA==
    (https://www.base64encode.org/)

  2. zoom oauth API에 accessToken을 요청합니다.

  • Url: https://zoom.us/oauth/token
  • Method: POST
  • Header:
    • Content-Type: application/x-www-form-urlencoded
    • Authorization: Basic 위1번에서만든문자열(ex. aFZZdjdzRWNNNGFoR3pTRldVbzFhZzoyY2thZzFLQUJtVFlaMXhKWXFSbXhhNE9PS3NzRUs4RA==)
  • Body: (x-www-form-urlencoded)
    • grant_type: account_credentials
    • account_id: 발급받은 자신의 account id

(주의: 헤더 Authorization 항목에 "Basic"하고 한칸 띄운 뒤 1번에서 Base64로 인코딩한 Client_id:Client_secret 문자열을 넣어줍니다.)

  1. 리스폰스 예시
{
	"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

API로 미팅 생성하기

위에서 받은 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으로 여기까지 테스트해보았습니다.

Nestjs 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 문서에서 확인하실 수 있습니다!

profile
백엔드 개발자. 공동의 목표를 함께 이해한 상태에서 솔직하게 소통하며 일하는 게 가장 즐겁고 효율적이라고 믿는 사람.

0개의 댓글