Zoom是个好东西,但是License也不便宜。免费的用户主持的会议,又会存在45分钟限制。
本文介绍,如何通过后台脚本,定时对用户进行授权和回收授权操作。列出Zoom内所有用户的所有会议,到点了自动授权。不管通过什么渠道创建的会议,只要有会议,到点就授权。
通过这种方式,可以无视前台对接了多少个会议管理系统,例如会议室管理、招聘、IM工具等等。只要生成了Zoom会议,到点就可以自动授权。
一、整体逻辑
这个整体逻辑说来真的很简单
- 1、列出所有用户
- 2、剔除需要永久保留授权的用户
- 3、列出所有用户的会议
- 4、筛选、整理出30分钟内即将开始的,和已经结束30分钟以上的会议
- 5、整理出上述会议的主持人列表
- 6、根据名单,进行授权、回收授权操作。
使用到的Zoom API Reference
1、Zoom List User/列出所有用户
https://marketplace.zoom.us/docs/api-reference/zoom-api/methods#operation/users
2、Zoom List Meeting/根据用户列出名下所有会议
https://marketplace.zoom.us/docs/api-reference/zoom-api/methods#operation/meetings
3、Zoom Update User/使用此接口修改用户类型为授权或非授权用户
https://marketplace.zoom.us/docs/api-reference/zoom-api/methods#operation/userUpdate
二、准备工作
获取Zoom JWT格式Token
访问以下链接
Zoom App Marketplace App Marketplace
点击右上角Develop->Build App。
点击JWT->Create,创建一个JWT应用。
填写基本信息
然后点击next,配置 App credentials。这里我们点击生成一个JWT Token,然后过期时间选择other,配置一个你觉得你在这个日期前指定已经跑路了的日子,避免未来给自己挖坑。
至此,我们已经得到了一个JWT TOKEN,可以用于Zoom API内的鉴权。
三、直接上脚本,你们自己看去吧
# coding=utf-8
#ZOOM相关API
import datetime
import re
import sys
import time
import requests
import warnings
import json
import os
import threading
from multiprocessing import Process,Manager,freeze_support
from save_log import save_log
warnings.filterwarnings("ignore")
requests.packages.urllib3.disable_warnings
utc_time=datetime.datetime.utcnow()
utc_time_stamp=int(time.mktime(utc_time.timetuple()))
GLOBAL_UTC_time_stamp=utc_time_stamp
#定义全局UTC标准时间
process_list = []
#定义全局线程列表
ZOOM_TOKEN="eyJhbGciOi************IMJpkRA"
#Your token will be expired at 15:23 05/27/2030
class Zoom_User_API:
def list_user():
try:
#----------------------------------
#列出组织内所有用户
session = requests.session()
postUrl = 'https://api.zoom.us/v2/users/'
headers = {
'authorization': 'Bearer %s'%ZOOM_TOKEN
}
user_list=[]
total_user=json.loads(session.get(postUrl,headers=headers).text)['total_records']
n=total_user//300
for i in range(1,n+2):
request_data={
'page_size':300,
'page_number':i
}
resp = session.get(postUrl,headers=headers,params=request_data)
user_list.extend(json.loads(resp.text)['users'])
except Exception as err:
raise err
else:
return user_list
def list_user_meeting(username,all_meeting_list,retry_flag=0):
try:
#----------------------------------
#获取用户名下的会议列表
if retry_flag>20:
return
session = requests.session()
postUrl = 'https://api.zoom.us/v2/users/%s/meetings'%username
headers = {
'authorization': 'Bearer %s'%ZOOM_TOKEN
}
request_data={'page_size':300}
resp = session.get(postUrl,headers=headers,params=request_data)
run_result=json.loads(resp.text)['meetings']
all_meeting_list[username]=run_result
except Exception as err:
save_log('get user meeting list error with user%s err code %s'%(username,str(err)))
Zoom_User_API.list_user_meeting(username,all_meeting_list,retry_flag+1)
else:
return run_result
def list_all_meeting(user_list):
try:
#----------------------------------
#获取所有即将开始的会议列表
manager=Manager()
all_meeting_list=manager.dict()
for i in user_list:
p = Process(target=Zoom_User_API.list_user_meeting, args=(i['id'],all_meeting_list))
time.sleep(0.05)
process_list.append(p)
p.start()
for p in process_list:
p.join()
all_meeting_list=all_meeting_list.copy()
except Exception as err:
raise err
else:
return all_meeting_list
def list_schedule_meeting(all_meeting_list):
try:
#----------------------------------
#筛选预约会议列表
schedule_meeting_list={}
for userid in all_meeting_list:
user_meeting_list=[]
for meeting_object in all_meeting_list[userid]:
if meeting_object['type']==2:
user_meeting_list.append(meeting_object)
if user_meeting_list!=[]:
schedule_meeting_list[userid]=user_meeting_list
except Exception as err:
raise err
else:
return schedule_meeting_list
def list_min30_meeting(schedule_meeting_list):
try:
#----------------------------------
#筛选出30分钟内开始即将开始和已经结束30分钟以上的会议列表
min30_upcoming_meeting_list=[]
#30分钟内即将开始的会议列表
min30_end_meeting_list=[]
#30分钟内已经结束的会议列表
for userid in schedule_meeting_list:
for meeting_object in schedule_meeting_list[userid]:
start_time=meeting_object['start_time']
start_time_stamp=int(time.mktime(time.strptime(start_time, '%Y-%m-%dT%H:%M:%SZ')))
end_time_stamp=start_time_stamp+meeting_object['duration']*60
if -3600<=start_time_stamp-GLOBAL_UTC_time_stamp<=1800:
min30_upcoming_meeting_list.append(meeting_object)
if -3600<end_time_stamp-GLOBAL_UTC_time_stamp<-1800:
min30_end_meeting_list.append(meeting_object)
except Exception as err:
raise err
else:
return min30_upcoming_meeting_list,min30_end_meeting_list
def list_license_user(user_list,min30_upcoming_meeting_list,min30_end_meeting_list):
try:
#----------------------------------
#整理授权用户列表
assign_license_user_list=[]
#增加授权列表
remove_license_user_list=[]
#移除授权列表
for i in min30_upcoming_meeting_list:
if i['host_id'] not in assign_license_user_list:
assign_license_user_list.append(i['host_id'])
for i in min30_end_meeting_list:
if i['host_id'] not in assign_license_user_list and i['host_id'] not in remove_license_user_list:
remove_license_user_list.append(i['host_id'])
except Exception as err:
raise err
else:
return assign_license_user_list,remove_license_user_list
def assign_license(username,type):
try:
save_log("assign_license--username:%s,type:%s"%(username,type))
##type字段1为免费用户,2为授权用户
session = requests.session()
postUrl = 'https://api.zoom.us/v2/users/%s'%username
headers = {
'authorization': 'Bearer %s'%ZOOM_TOKEN
}
request_json={
'type':type
}
resp = session.patch(postUrl,headers=headers,json=request_json)
if resp.status_code!=204:
run_result=json.loads(resp.text)
else:
run_result={'code': 204}
except Exception as err:
return err
else:
save_log(run_result)
def print_PRTG_json(value_list):
try:
data={
"prtg": {
"result": []
}
}
for i in value_list:
Channel_data={
"Channel": "%s"%i,
"Unit": "#",
"Mode":"Absolute",
"Value":value_list[i]
}
data['prtg']['result'].append(Channel_data)
print(json.dumps(data, sort_keys=True, indent=2))
except Exception as err:
raise err
def main():
try:
save_log('----------------------------START--------------------------------\n\n')
save_log('UTC Time:'+str(utc_time))
monitor_data={}
#----------------------------------
#获取所有用户列表
user_list=Zoom_User_API.list_user()
save_log('get user list. Total user:%s'%len(user_list))
monitor_data['Total_User']=len(user_list)
#----------------------------------
#剔除永久授权组内用户
for i in user_list:
if 'group_ids' in i:
#[iWL*****ra4_V9Q]-Permanent_authorization group id
if 'iWL*****ra4_V9Q' in i['group_ids']:
user_list.remove(i)
#----------------------------test_temp_only----------------------------
#仅测试期间使用以下代码,仅使用测试组内用户
temp_user_list=[]
for i in user_list:
if 'group_ids' in i:
#[V4A****nLKrcQ]-Test User group id
if 'V4A****nLKrcQ' in i['group_ids']:
temp_user_list.append(i)
user_list=temp_user_list
#仅测试期间使用以上代码,仅使用测试组内用户
#----------------------------test_temp_only----------------------------
save_log('dynamic license user:%s'%len(user_list))
monitor_data['Dynamic_User']=len(user_list)
#----------------------------------
#获取所有会议列表
all_meeting_list=Zoom_User_API.list_all_meeting(user_list)
save_log('all_meeting_list:%s'%len(all_meeting_list))
monitor_data['Get All User Meeting Status']=len(all_meeting_list)-len(user_list)
#----------------------------------
#筛选预约类型的会议列表
schedule_meeting_list=Zoom_User_API.list_schedule_meeting(all_meeting_list)
save_log('schedule_meeting_list:%s'%len(schedule_meeting_list))
monitor_data['Schedule Meeting Count']=len(schedule_meeting_list)
#----------------------------------
#筛选出30分钟内开始即将开始和已经结束30分钟以上的会议列表
min30_upcoming_meeting_list,min30_end_meeting_list=Zoom_User_API.list_min30_meeting(schedule_meeting_list)
save_log('min30_upcoming_meeting_list:'+json.dumps(min30_upcoming_meeting_list))
save_log('min30_end_meeting_list:'+json.dumps(min30_end_meeting_list))
monitor_data['30 Minutes Upcomming Meeting Count']=len(min30_upcoming_meeting_list)
monitor_data['30 Minutes End Meeting Count']=len(min30_end_meeting_list)
#----------------------------------
#整理授权用户列表
assign_license_user_list,remove_license_user_list=Zoom_User_API.list_license_user(user_list,min30_upcoming_meeting_list,min30_end_meeting_list)
save_log('assign_license_user_list:'+json.dumps(assign_license_user_list))
save_log('remove_license_user_list:'+json.dumps(remove_license_user_list))
monitor_data['Assign license User Count']=len(assign_license_user_list)
monitor_data['Remove license User Count']=len(remove_license_user_list)
#----------------------------------
#授权操作
for i in remove_license_user_list:
for user_object in user_list:
if user_object['id']==i:
i=user_object['email']
Zoom_User_API.assign_license(i,1)
for i in assign_license_user_list:
for user_object in user_list:
if user_object['id']==i:
i=user_object['email']
Zoom_User_API.assign_license(i,2)
#----------------------------------
#格式化输出PRTG监控数据
print_PRTG_json(monitor_data)
save_log('----------------------------END--------------------------------\n\n')
except Exception as err:
raise
if __name__ == '__main__':
main()
四、特别注意
1、API接口调用速率限制
Zoom的所有API根据负载不同,存在不同的调用速率限制。具体参考
https://marketplace.zoom.us/docs/api-reference/rate-limits/
本文中使用的List User和List Meeting接口属于中度负载,限制速率20/秒,update user属于轻型负载,限制速率30/秒。对于用户数量较多的组织,代码中需要控制调用速率。
2、快速枚举所有用户会议
List Meeting接口只能一个一个用户的查询其名下会议,且接口调用时间约在1-2S。这要是顺序调用接口,获取几百个用户的会议信息,那疯了。没个半小时跑不完。
本例中使用多线程并发调用,但是需要控制接口速率。
def list_user_meeting(username,all_meeting_list,retry_flag=0):
try:
#----------------------------------
#获取用户名下的会议列表
if retry_flag>20:#最大20次重试
return
session = requests.session()
postUrl = 'https://api.zoom.us/v2/users/%s/meetings'%username
headers = {
'authorization': 'Bearer %s'%ZOOM_TOKEN
}
request_data={'page_size':300}
resp = session.get(postUrl,headers=headers,params=request_data)
run_result=json.loads(resp.text)['meetings']
all_meeting_list[username]=run_result
except Exception as err:
save_log('get user meeting list error with user%s err code %s'%(username,str(err)))
Zoom_User_API.list_user_meeting(username,all_meeting_list,retry_flag+1)
#失败重试
else:
return run_result
def list_all_meeting(user_list):
try:
#----------------------------------
#多线程获取所有即将开始的会议列表
manager=Manager()
all_meeting_list=manager.dict()
for i in user_list:
p = Process(target=Zoom_User_API.list_user_meeting, args=(i['id'],all_meeting_list))
time.sleep(0.05)#线程间隔0.05s,每秒送出20个请求,不限制并发
process_list.append(p)
p.start()
for p in process_list:
p.join()
all_meeting_list=all_meeting_list.copy()
except Exception as err:
raise err
else:
return all_meeting_list
此步骤整理出一个以用户ID为主键的字典。value为一个列表,包含这个用户名下的所有未过期的会议信息。如果用户不存在任何会议,value为空列表[]
同时,对于可能存在的极端情况,完成后校验会议信息字典的长度与用户列表长度是否相同。
#----------------------------------
#获取所有会议列表
all_meeting_list=Zoom_User_API.list_all_meeting(user_list)
save_log('all_meeting_list:%s'%len(all_meeting_list))
monitor_data['Get All User Meeting Status']=len(all_meeting_list)-len(user_list)
3、授权逻辑
def list_min30_meeting(schedule_meeting_list):
try:
#----------------------------------
#筛选出30分钟内开始即将开始和已经结束30分钟以上的会议列表
min30_upcoming_meeting_list=[]
#30分钟内即将开始的会议列表
min30_end_meeting_list=[]
#30分钟内已经结束的会议列表
for userid in schedule_meeting_list:
for meeting_object in schedule_meeting_list[userid]:
start_time=meeting_object['start_time']
start_time_stamp=int(time.mktime(time.strptime(start_time, '%Y-%m-%dT%H:%M:%SZ')))
end_time_stamp=start_time_stamp+meeting_object['duration']*60
if -3600<=start_time_stamp-GLOBAL_UTC_time_stamp<=1800:
min30_upcoming_meeting_list.append(meeting_object)
if -3600<end_time_stamp-GLOBAL_UTC_time_stamp<-1800:
min30_end_meeting_list.append(meeting_object)
except Exception as err:
raise err
else:
return min30_upcoming_meeting_list,min30_end_meeting_list
def list_license_user(user_list,min30_upcoming_meeting_list,min30_end_meeting_list):
try:
#----------------------------------
#整理授权用户列表
assign_license_user_list=[]
#增加授权列表
remove_license_user_list=[]
#移除授权列表
for i in min30_upcoming_meeting_list:
if i['host_id'] not in assign_license_user_list:
assign_license_user_list.append(i['host_id'])
for i in min30_end_meeting_list:
if i['host_id'] not in assign_license_user_list and i['host_id'] not in remove_license_user_list:
remove_license_user_list.append(i['host_id'])
except Exception as err:
raise err
else:
return assign_license_user_list,remove_license_user_list
对于每一个会议,我们将开始和结束时间算作一个标志。
判定需要增加授权的逻辑
1、未来1800s内有开始标志
2、过去3600s内有开始标志
判定需要回收授权的逻辑
1、过去1800s-3600区间内有结束标志
2、不在增加授权的用户列表内
可能比较绕哈,我们简单画个图
绿色时间范围内存在开始标记,增加授权
橙色时间范围内存在结束标记,回收授权
我们尝试模拟各种情况,超长会议、超短会议、重叠会议等等。
此逻辑针对绝大部分情况均可以保证正确增加和回收授权。但仅对图示的情况下,会存在会议中途被移除授权的情况。
对于这个逻辑,也希望各位大佬们集思广益,让我们后续迭代出更完美的逻辑。
不过,BUT,Zoom好在还是很人性的。对于已经开始的会议,即使移除了主持人授权,会议依然会保留为不限时会议状态,除非主持人刚好在这15min掉线了。
4、永久保留授权的用户
自动动态授权一旦开始之后,对于没有会议状态下的用户都会回收授权。但是对于VIP来说,可能希望永久保留Zoom授权,本例中,可将VIP人员创建一个单独的群组,对于此群组内的人员,脚本不在进行任何授权和回收授权操作。
#----------------------------------
#剔除永久授权组内用户
for i in user_list:
if 'group_ids' in i:
#[iWL*****ra4_V9Q]-Permanent_authorization group id
if 'iWL*****ra4_V9Q' in i['group_ids']:
user_list.remove(i)
5、监控
众所周知,逗老师老PRTG监控狗了。本例的代码,可以直接作为PRTG的自定义Python传感器使用,并且可以返回符合PRTG要求的JSON格式数据。
可以看实时的统计数据,包括给多少人授权了,总共有多少用户,并发量有多大等等。
往期文章回顾:
【逗老师带你学IT】Zoom联动Google日历,实现Zoom Rooms高逼格会议体验
【逗老师带你学IT】ZoomRooms房间控制器通过Modbus控制电器开关
【逗老师带你学IT】ZoomRooms兼容硬件设计方案
【逗老师带你学IT】Zoom联动Google日历,实现Zoom Rooms高逼格会议体验