1"""Utility class for sending requests to BonD, adding and controlling bots.""" 2 3from __future__ import absolute_import 4from __future__ import division 5from __future__ import print_function 6 7import json 8import logging 9import requests 10 11from datetime import datetime 12from oauth2client.client import GoogleCredentials 13 14 15_BOND_API_URL = 'https://bond-pa.sandbox.googleapis.com' 16_HANGOUTS_API_URL = 'https://preprod-hangouts.googleapis.com/hangouts/v1_meetings/' 17_MEETINGS_API_URL = 'https://preprod-meetings.sandbox.googleapis.com' 18 19_TOKEN_TTL_SECONDS = 3500 20 21# See https://crbug.com/874835 for details on the credentials files. 22_SERVICE_CREDS_FILE = '/creds/service_accounts/bond_service_account.json' 23 24 25class BondHttpApi(object): 26 """Utility class for sending requests to BonD for bots.""" 27 28 def __init__(self): 29 self._last_token_request_time = None 30 self._last_token = None 31 32 def GetAvailableWorkers(self): 33 """Gets the number of available workers for a conference.""" 34 token = self._GetAccessToken() 35 resp = requests.get( 36 '%s/v1/workers:count' % _BOND_API_URL, 37 headers={ 38 'Content-Type': 'application/json', 39 'Authorization': 'Bearer %s' % token 40 }) 41 return json.loads(resp.text)["numOfAvailableWorkers"] 42 43 def CreateConference(self): 44 """Creates a conference. 45 46 Returns: 47 The meeting code of the created conference. 48 """ 49 token = self._GetAccessToken() 50 51 request_data = { 52 'conference_type': 'THOR', 53 'backend_options': { 54 'mesi_apiary_url': _HANGOUTS_API_URL, 55 'mas_one_platform_url': _MEETINGS_API_URL 56 }, 57 } 58 59 resp = requests.post( 60 '%s/v1/conferences:create' % _BOND_API_URL, 61 headers={ 62 'Content-Type': 'application/json', 63 'Authorization': 'Bearer %s' % token, 64 }, 65 data=json.dumps(request_data)) 66 json_response = json.loads(resp.text) 67 logging.info("CreateConference response: %s", json_response) 68 return json_response["conference"]["conferenceCode"] 69 70 def ExecuteScript(self, script, meeting_code): 71 """Executes the specified script. 72 73 Args: 74 script: Script to execute. 75 meeting_code: The meeting to execute the script for. 76 77 Returns: 78 RunScriptRequest denoting failure or success of the request. 79 """ 80 token = self._GetAccessToken() 81 82 request_data = { 83 'script': script, 84 'conference': { 85 'conference_code': meeting_code 86 } 87 } 88 89 resp = requests.post( 90 '%s/v1/conference/%s/script' % (_BOND_API_URL, meeting_code), 91 headers={ 92 'Content-Type': 'application/json', 93 'Authorization': 'Bearer %s' % token, 94 }, 95 data=json.dumps(request_data)) 96 97 json_response = json.loads(resp.text) 98 logging.info("ExecuteScript response: %s", json_response) 99 return json_response['success'] 100 101 102 def AddBotsRequest(self, 103 meeting_code, 104 number_of_bots, 105 ttl_secs, 106 send_fps=24, 107 requested_layout='BRADY_BUNCH_4_4', 108 allow_vp9=True, 109 send_vp9=True): 110 """Adds a number of bots to a meeting for a specified period of time. 111 112 Args: 113 meeting_code: The meeting to join. 114 number_of_bots: The number of bots to add to the meeting. 115 ttl_secs: The time in seconds that the bots will stay in the meeting after 116 joining. 117 send_fps: FPS the bots are sending. 118 requested_layout: The type of layout the bots are requesting. Valid values 119 include CLASSIC, BRADY_BUNCH_4_4 and BRADY_BUNCH_7_7. 120 allow_vp9: If set, VP9 will be negotiated for devices where support is 121 available. This only activates receiving VP9. See `send_vp9` for sending 122 send_vp9: If set, VP9 will be preferred over VP8 when sending. 123 `allow_vp9` must also be set for VP9 to be negotiated. 124 125 Returns: 126 List of IDs of the started bots. 127 """ 128 # Not allowing VP9, but sending it is an invalid combination. 129 assert(allow_vp9 or not send_vp9) 130 token = self._GetAccessToken() 131 132 request_data = { 133 'num_of_bots': number_of_bots, 134 'ttl_secs': ttl_secs, 135 'video_call_options': { 136 'allow_vp9': allow_vp9, 137 'send_vp9': send_vp9, 138 }, 139 'media_options': { 140 'audio_file_path': "audio_32bit_48k_stereo.raw", 141 'mute_audio': True, 142 'video_fps': send_fps, 143 'mute_video': False, 144 'requested_layout': requested_layout, 145 }, 146 'backend_options': { 147 'mesi_apiary_url': _HANGOUTS_API_URL, 148 'mas_one_platform_url': _MEETINGS_API_URL 149 }, 150 'conference': { 151 'conference_code': meeting_code 152 }, 153 'bot_type': "MEETINGS", 154 'use_random_video_file_for_playback': True 155 } 156 157 resp = requests.post( 158 '%s/v1/conference/%s/bots:add' % (_BOND_API_URL, meeting_code), 159 headers={ 160 'Content-Type': 'application/json', 161 'Authorization': 'Bearer %s' % token 162 }, 163 data=json.dumps(request_data)) 164 165 json_response = json.loads(resp.text) 166 logging.info("AddBotsRequest response: %s", json_response) 167 return json_response["botIds"] 168 169 def _GetAccessToken(self): 170 if self._last_token is None or self._CheckTokenExpired(): 171 credentials = self._CreateApiCredentials() 172 scope = 'https://www.googleapis.com/auth/meetings' 173 credentials = credentials.create_scoped(scope) 174 self._last_token_request_time = datetime.now() 175 self._last_token = credentials.get_access_token().access_token 176 return self._last_token 177 178 def _CreateApiCredentials(self): 179 return GoogleCredentials.from_stream(_SERVICE_CREDS_FILE) 180 181 def _CheckTokenExpired(self): 182 elapsed = datetime.now() - self._last_token_request_time 183 return elapsed.total_seconds() > _TOKEN_TTL_SECONDS 184