Index
概要
现在中国企业已步入全球化新时代,视频会议软件的使用率越来越高。之前我们讲了如何将腾讯会议接入到我们的系统中,这次,我们将zoom这个国际流行化的视频会议接入进来,无需安装客户端就能在kintone上开视频会议了。
ZOOM端准备
本教程是教大家如何将zoom直接以web嵌入的形式在kintone中展现。用到的技术是zoom web sdk。所以在开发部署前,请先对他的内容做一些了解。
了解zoom api
准备好应用的API Key 和 API Secret
以下我们就从创建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
获取应用的API Key 和 API Secret,Token
将应用中的API Key 和 API Secret准备好。
API Key & API Secret:
设置一个过期时间比较长的Token,用来调用zoom api来预定会议。
kintone端导入
导入zoom的模板应用
通过导入模板文件来创建zoom相关的应用
应用说明
这时我们创建了两个应用。分别是“Calendar” 和 “ZoomClient”。
Calendar是日程应用,用户可以直接登记会议,参加会议。本例使用的是预定会议。
ZoomClient是zoom客户端应用。用来配置好zoom的信息及被Calendar应用调用来显示zoom会议。
应用配置
ZoomClient应用需要安装kintone插件并且预先配置好之前申请的zoom jwt应用的API Key 和 API Secret
STEP1:导入插件
准备插件: kintone2Zoom ,ZoomWebClient。关于插件的导入方法,请参考kintone帮助文档 在kintone中安装插件。
导入ZoomWebClient和kintone2Zoom 两个插件。
STEP2:在应用中添加插件
在ZoomClient应用中添加插件(ZoomWebClient)。
在Calendar应用中添加插件(kintone2Zoom)。
关于插件的添加方法,请参考kintone帮助文档 在应用中添加插件
STEP3:配置插件
在ZoomWebClient插件中设置好之前准备好的API Key 和 API Secret 并保存
在kintone2Zoom插件中设置好之前准备好的Token 并保存
Calendar应用使用
在Calendar应用中实现了创建会议(预约),取消会议。用户可以直接在线主持会议及参加会议。
创建会议
在Calendar中创建的会议会自动生成zoom的会议链接,并同步到zoom中。
取消会议
删除这条会议记录时,这条预定会自动从zoom中删除。
主持会议
点击主持会议,会自动更新kintone的Host字段。这样其他用户可以看到这个会议是否已经开始,并且知道是谁在主持。
参加会议
等待主持人主持会议后,参加者才能点击attend来参加会议。
代码解析
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地址
注意事项
本插件不直接提供zoom产品,和zoom有关的产品和技术问题请自行咨询zoom官方网站。
此插件是用于演示如何开发插件的范例,才望子不予以保证可正常运行。
不对此范例提供技术支持。
kintone的插件功能只可在标准版使用,简易版不可使用。