把zoom视频会议web客户端嵌入kintone

cnDevNet发表于:2020年09月02日 14:52:54更新于:2023年03月10日 16:40:14

Index

概要

现在中国企业已步入全球化新时代,视频会议软件的使用率越来越高。之前我们讲了如何将腾讯会议接入到我们的系统中,这次,我们将zoom这个国际流行化的视频会议接入进来,无需安装客户端就能在kintone上开视频会议了。

ZOOM端准备

本教程是教大家如何将zoom直接以web嵌入的形式在kintone中展现。用到的技术是zoom web sdk。所以在开发部署前,请先对他的内容做一些了解。

以下我们就从创建zoom jwt应用开始:

创建zoom jwt应用

参考资料:https://marketplace.zoom.us/docs/sdk/native-sdks/web

按照zoom的web sdk 开发文档,首先去zoom注册一个jwt应用:https://marketplace.zoom.us/docs/guides/build/jwt-app

0015f508cd9992ab2bb17580cdcd645

获取应用的API Key 和 API Secret,Token

将应用中的API Key 和 API Secret准备好。

API Key & API Secret:

0015f508ced2a3cad5a6576c0198631

设置一个过期时间比较长的Token,用来调用zoom api来预定会议。

0015f61784c285164a5174579de8ff6

kintone端导入

导入zoom的模板应用

通过导入模板文件来创建zoom相关的应用

0015f4f51f29fa75aa6bf333ac09786

应用说明

这时我们创建了两个应用。分别是“Calendar” 和 “ZoomClient”。

  • Calendar是日程应用,用户可以直接登记会议,参加会议。本例使用的是预定会议。

  • ZoomClient是zoom客户端应用。用来配置好zoom的信息及被Calendar应用调用来显示zoom会议。

应用配置

ZoomClient应用需要安装kintone插件并且预先配置好之前申请的zoom jwt应用的API Key 和 API Secret

STEP1:导入插件

  1. 准备插件: kintone2Zoom ,ZoomWebClient。关于插件的导入方法,请参考kintone帮助文档 在kintone中安装插件

  2. 导入ZoomWebClient和kintone2Zoom 两个插件。

STEP2:在应用中添加插件

在ZoomClient应用中添加插件(ZoomWebClient)。

在Calendar应用中添加插件(kintone2Zoom)。

关于插件的添加方法,请参考kintone帮助文档 在应用中添加插件

STEP3:配置插件

  • 在ZoomWebClient插件中设置好之前准备好的API Key 和 API Secret 并保存

0015f6184a106e998bd2f3fe0c58d34

  • 在kintone2Zoom插件中设置好之前准备好的Token 并保存


0015f6184a12080ce7e2648783690de

Calendar应用使用

在Calendar应用中实现了创建会议(预约),取消会议。用户可以直接在线主持会议及参加会议。

创建会议

在Calendar中创建的会议会自动生成zoom的会议链接,并同步到zoom中。

取消会议

删除这条会议记录时,这条预定会自动从zoom中删除。

主持会议

点击主持会议,会自动更新kintone的Host字段。这样其他用户可以看到这个会议是否已经开始,并且知道是谁在主持。

参加会议

等待主持人主持会议后,参加者才能点击attend来参加会议。

0015f509a0374d5067479728fda3264

代码解析

kintone2Zoom插件代码片段

index.js

import { ZoomApi } from './zoomApi';
((PLUGIN_ID) => {
  const topic = 'topic';
  const start_time = 'start_time';
  const duration = 'duration';
  const Attendees = 'Attendees';
  const host = 'host';
  const meetingNumber = 'meetingNumber';
  const password = 'password';
  const join_url = 'join_url';
  const relatedZoomClient = 'relatedZoomClient';
  const joinSpace = 'join';
  const zoomSpace = 'zoom';

  const zoomClientApp = kintone.app.getRelatedRecordsTargetAppId(relatedZoomClient);

  const hostRole = 1;

  const attendRole = 0;
  const zoomapi = new ZoomApi(PLUGIN_ID);

  const meetingType = 2;
  const loginUser = kintone.getLoginUser();

  const detailHandle = event => {
    kintone.app.record.setFieldShown(relatedZoomClient, false);
    const zoomApp = kintone.app.getId();
    const record = event.record;
    if (document.getElementById('join') !== null) {
      return;
    }

    const addZoomDom = zoomClientUrl => {
      if (document.getElementById('zoom') !== null) {
        document.getElementById('zoom').remove();
      }
      const zoomDiv = document.createElement('div');
      const zoomIframe = `<div id="zoom"><iframe style="border:none; height: 600px; width: 100%;" src= ${zoomClientUrl} 
            sandbox="allow-forms allow-scripts allow-same-origin" allow="microphone; camera"></iframe></div>`;
      zoomDiv.innerHTML = zoomIframe;
      kintone.app.record.getSpaceElement(zoomSpace).appendChild(zoomDiv);
    };
   
    const updateHost = () => {
      const updateHostParams = {
        'app': zoomApp,
        'id': kintone.app.record.getId(),
        'record': {
          host: {
            'value': [
              {
                'code': loginUser.code
              }
            ]
          }
        }
      };
      kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', updateHostParams).catch(error => {
        console.log(error);
      });
    };
  
    const checkTheHost = () => {
      const checkTheHostParams = {
        'app': zoomApp,
        'id': kintone.app.record.getId()
      };
      return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', checkTheHostParams).then(resp => {
        return resp.record[host].value;
      }, error => {
        return Promise.reject(error);
      });
    };
    const updateRealAttend = () => {
      const updateRealAttendParams = {
        'app': zoomApp,
        'id': kintone.app.record.getId()
      };
      kintone.api(kintone.api.url('/k/v1/record', true), 'GET', updateRealAttendParams).then(resp => {
        const AttendeesValue = resp.record[Attendees].value;
        for (const value of AttendeesValue) {
          if ('code' in value && value.code === loginUser.code) {
            return;
          }
        }
        const newAttend = {
          'code': loginUser.code
        };
        AttendeesValue.push(newAttend);
        const params = {
          'app': zoomApp,
          'id': kintone.app.record.getId(),
          'record': {
            [Attendees]: {
              'value': AttendeesValue
            }
          }
        };
        kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', params).catch(error => {
          console.log(error);
        });
      }, error => {
        console.log(error);
      });
    };
    const zoom = {
      init: function () {
        this.addHost();
        this.addJoin();
      },
      addHost() {
        const hostButton = document.createElement('button');
        hostButton.id = 'host';
        hostButton.innerText = 'Host';
        hostButton.className = 'btn btn-primary';
        kintone.app.record.getSpaceElement(joinSpace).appendChild(hostButton);
        hostButton.onclick = () => {
          updateHost();
          updateRealAttend();
          const meetingNumberValue = record[meetingNumber].value;
          const passwordValue = record[password].value;
          const zoomClientUrl = `/k/${zoomClientApp}/?meetingNumber=${meetingNumberValue}&password=${passwordValue}&role=${hostRole}`;
 
          addZoomDom(zoomClientUrl);
        };
      },
      addJoin() {
        const joinButton = document.createElement('button');
        joinButton.id = 'attend';
        joinButton.innerText = 'Attend';
        joinButton.className = 'btn btn-info';
        joinButton.setAttribute('style', 'margin-left:10px;');
        kintone.app.record.getSpaceElement(joinSpace).appendChild(joinButton);

        joinButton.onclick = () => {

          checkTheHost().then(resp => {
            if (resp.length === 0) {
              alert('ホストが本ミーティングを開始するまでお待ちください');
              return;
            }

            updateRealAttend();
            const meetingNumberValue = record[meetingNumber].value;
            const passwordValue = record.password.value;
            const zoomClientUrl = `/k/${zoomClientApp}/?meetingNumber=${meetingNumberValue}&password=${passwordValue}&role=${attendRole}`;

            addZoomDom(zoomClientUrl);
          }, error => {
            console.log(error);
          });
        };
      }
    };
    zoom.init();
  };

  kintone.events.on('app.record.detail.show', detailHandle);

  kintone.events.on('app.record.create.submit', async event => {
    const record = event.record;
    const data = {
      [topic]: record[topic].value,
      'type': meetingType,
      [start_time]: record[start_time].value,
      [duration]: record[duration].value,
      'timezone': loginUser.timezone
    };

    const user = await zoomapi.getUsers().catch(error => {
      const resp = JSON.parse(error[0]);
      alert(resp.message);
    });
    if (!user) return event;
    const userId = user.users[0].id;

    const meetingInfo = await zoomapi.createMeeting(userId, data).catch(error => {
      const resp = JSON.parse(error[0]);
      alert(resp.message);
    });
    if (!meetingInfo) return event;
    record[meetingNumber].value = meetingInfo.id;
    record[join_url].value = meetingInfo.join_url;
    record[password].value = meetingInfo.encrypted_password;
    return event;
  });

  kintone.events.on(['app.record.create.show', 'app.record.edit.show', 'app.record.index.edit.show'], event => {
    const record = event.record;

    record[meetingNumber].disabled = true;
    record[password].disabled = true;
    record[join_url].disabled = true;

    kintone.app.record.setFieldShown(Attendees, false);
    kintone.app.record.setFieldShown(host, false);
    return event;
  });

  kintone.events.on(['app.record.detail.delete.submit', 'app.record.index.delete.submit'], event => {
    const record = event.record;
    const meetingId = Number(record[meetingNumber].value);
    return zoomapi.deleteMeeting(meetingId).catch(error => {
      const resp = JSON.parse(error[0]);
      alert(resp.message);
    });
  });
})(kintone.$PLUGIN_ID);

zoomApi.js

export class ZoomApi {
  constructor(PLUGIN_ID) {
    this.preUrl = 'https://api.zoom.us/v2';
    this.plugin_id = PLUGIN_ID;
  }
  zoomUrl(apiUrl) {
    return this.preUrl + apiUrl;
  }

  getUsers() {
    const apiUrl = '/users';
    return kintone.plugin.app.proxy(this.plugin_id, this.zoomUrl(apiUrl), 'GET', {}, '').then(args => {
      if (args[1] === 200) {
        const resp = JSON.parse(args[0]);
        return resp;
      }
      return Promise.reject(args);
    }).catch(error => {
      return Promise.reject(error);
    });
  }

  createMeeting(userId, data) {
    const apiUrl = `/users/${userId}/meetings`;
    const headers = {
      'Content-Type': 'application/json'
    };
    return kintone.plugin.app.proxy(this.plugin_id, this.zoomUrl(apiUrl), 'POST', headers, data).then(args => {
      if (args[1] === 201) {
        const resp = JSON.parse(args[0]);
        return resp;
      }
      return Promise.reject(args);
    }).catch(error => {
      return Promise.reject(error);
    });
  }

  deleteMeeting(meetingId) {
    const apiUrl = `/meetings/${meetingId}`;
    return kintone.plugin.app.proxy(this.plugin_id, this.zoomUrl(apiUrl), 'DELETE', {}, '').then(args => {
      if (args[1] !== 204) alert('update to zoom failed');
    }).catch(error => {
      return Promise.reject(error);
    });
  }
}


注意

  • 此次使用了zoom免费帐户做演示,存在一些限制,而且这个zoom帐户下也没有添加其他帐户。如您使用zoom企业版,可尝试添加帐户,会议室等,并且在这些设备上创建日程。

具体zoom api请参见 zoom官方api文档

代码打包

此次使用了es6的一些写法, 所以需要将代码通过webpack打包。具体请参见 向JavaScript自定义中级开发者的目标前进(1) 〜webpack篇〜

插件目录下执行

初始化:npm run build

打包:kintone-plugin-packer plugin --ppk xxx.ppk   (详见 用plugin-packer打包插件文件)

全部代码github地址

kintone-samples/sample-kintone-to-zoom-CN (github.com)

注意事项

  • 本插件不直接提供zoom产品,和zoom有关的产品和技术问题请自行咨询zoom官方网站。

  • 此插件是用于演示如何开发插件的范例,才望子不予以保证可正常运行。

  • 不对此范例提供技术支持。

  • kintone的插件功能只可在标准版使用,简易版不可使用。