【逗老师带你学IT】Zoom动态授权用户Pro License妈妈再也不用担心预算超标了

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高逼格会议体验

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逗老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值