From af43db44ede1665c6a709413314d27221dfef846 Mon Sep 17 00:00:00 2001 From: Ross Scroggs Date: Sat, 15 Feb 2025 11:11:42 -0800 Subject: [PATCH 01/14] Added option `minimal|basic|full` to `gam print|show cigroup-members` --- src/GamCommands.txt | 7 +++- src/GamUpdate.txt | 12 ++++++ src/gam/__init__.py | 85 ++++++++++++++++++++++++++--------------- src/gam/gamlib/glcfg.py | 2 +- 4 files changed, 72 insertions(+), 34 deletions(-) diff --git a/src/GamCommands.txt b/src/GamCommands.txt index c4338305..fa03e22b 100644 --- a/src/GamCommands.txt +++ b/src/GamCommands.txt @@ -4017,16 +4017,19 @@ gam print cigroup-members [todrive *] [types ] [memberemaildisplaypattern|memberemailskippattern ] * [fields ] - [(recursive [noduplicates])includederivedmembership] [nogroupeemail] + [minimal|basic|full] + [(recursive [noduplicates]) | includederivedmembership] [nogroupemail] [formatjson [quotechar ]] gam show cigroup-members [(cimember|ciowner )|(cigroup )|(select )] [showownedby ] [emailmatchpattern [not] ] [namematchpattern [not] ] [descriptionmatchpattern [not] ] - [roles ] [members] [managers] [owners] [depth ] + [roles ] [members] [managers] [owners] [types ] [memberemaildisplaypattern|memberemailskippattern ] + [minimal|basic|full] + [(depth ) | includederivedmembership] # Cloud Identity Devices diff --git a/src/GamUpdate.txt b/src/GamUpdate.txt index e016492b..82ea0bc6 100644 --- a/src/GamUpdate.txt +++ b/src/GamUpdate.txt @@ -1,3 +1,15 @@ +7.04.03 + +Added option `minimal|basic|full` to `gam print cigroup-members`: +* `minimal` - Fields displayed: group, id, role, email +* `basic` - Fields displayed: group, type, id, role, email +* `full` - Fields displayed: group, type, id, role, email, createTime, updateTime; this is the default + +Added option `minimal|basic|full` to `gam show cigroup-members`: +* `minimal` - Fields displayed: role, email +* `basic` - Fields displayed: type, role, email +* `full` - Fields displayed: type, role, email, createTime, updateTime; this is the default + 7.04.02 Improved output formatting for the following commands: diff --git a/src/gam/__init__.py b/src/gam/__init__.py index a207c154..f27ce2fd 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -25,7 +25,7 @@ https://github.com/GAM-team/GAM/wiki """ __author__ = 'GAM Team ' -__version__ = '7.04.02' +__version__ = '7.04.03' __license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)' #pylint: disable=wrong-import-position @@ -5981,6 +5981,8 @@ def getCIGroupTransitiveMemberRoleFixType(groupName, tmember): member['type'] = Ent.TYPE_USER if not tid.endswith('.iam.gserviceaccount.com') else Ent.TYPE_SERVICE_ACCOUNT elif ttype == 'groups': member['type'] = Ent.TYPE_GROUP + elif tid.startswith('cbcm-browser.'): + member['type'] = Ent.TYPE_CBCM_BROWSER else: member['type'] = Ent.TYPE_OTHER else: @@ -33959,6 +33961,8 @@ def infoGroupMembers(entityList, ciGroupsAPI=False): printKeyValueList(['type', result['type']]) for field in ['createTime', 'updateTime']: printKeyValueList([field, formatLocalTime(result[field])]) + if 'deliverySetting' in result: + printKeyValueList(['deliverySetting', result['deliverySetting']]) Ind.Decrement() except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden) as e: entityActionFailedWarning([entityType, groupKey], str(e), j, jcount) @@ -36121,17 +36125,10 @@ def getCIGroupTransitiveMembers(ci, groupName, membersList, i, count): return True def getCIGroupMembers(ci, groupName, memberRoles, membersList, membersSet, i, count, - memberOptions, memberDisplayOptions, level, typesSet, listView='FULL', groupEmail=None): - if groupEmail: - nameToPrint = groupEmail - else: - nameToPrint = groupName + memberOptions, memberDisplayOptions, level, typesSet, groupEmail, kwargs): + nameToPrint = groupEmail if groupEmail else groupName printGettingAllEntityItemsForWhom(memberRoles if memberRoles else Ent.ROLE_MANAGER_MEMBER_OWNER, nameToPrint, i, count) validRoles = _getCIRoleVerification(memberRoles) - if listView == 'BASIC': - pageSize = GC.Values[GC.MEMBER_MAX_RESULTS_CI_BASIC] - else: - pageSize = GC.Values[GC.MEMBER_MAX_RESULTS_CI_FULL] if memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP]: groupMembers = [] if not getCIGroupTransitiveMembers(ci, groupName, groupMembers, i, count): @@ -36145,12 +36142,11 @@ def getCIGroupMembers(ci, groupName, memberRoles, membersList, membersSet, i, co groupMembers = callGAPIpages(ci.groups().memberships(), 'list', 'memberships', pageMessage=getPageMessageForWhom(), throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS, - parent=groupName, view=listView, - fields='nextPageToken,memberships(*)', pageSize=pageSize) + parent=groupName, **kwargs) except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument, GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable): - entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, groupName, i, count) + entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, nameToPrint, i, count) return checkCategory = memberDisplayOptions['showCategory'] if not memberOptions[MEMBEROPTION_RECURSIVE]: @@ -36184,7 +36180,7 @@ def getCIGroupMembers(ci, groupName, memberRoles, membersList, membersSet, i, co memberName not in membersSet): membersSet.add(memberName) member['level'] = level - member['subgroup'] = groupName + member['subgroup'] = nameToPrint membersList.append(member) else: if memberName not in membersSet: @@ -36193,37 +36189,40 @@ def getCIGroupMembers(ci, groupName, memberRoles, membersList, membersSet, i, co checkCIMemberMatch(member, memberOptions) and (not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions))): member['level'] = level - member['subgroup'] = groupName + member['subgroup'] = nameToPrint membersList.append(member) _, gname = member['name'].rsplit('/', 1) - groupMemberList.append(f'groups/{gname}') + groupMemberList.append((f'groups/{gname}', memberName)) for member in groupMemberList: - getCIGroupMembers(ci, member, memberRoles, membersList, membersSet, i, count, - memberOptions, memberDisplayOptions, level+1, typesSet) + getCIGroupMembers(ci, member[0], memberRoles, membersList, membersSet, i, count, + memberOptions, memberDisplayOptions, level+1, typesSet, member[1], kwargs) else: for member in groupMembers: getCIGroupMemberRoleFixType(member) + memberName = member.get('preferredMemberKey', {}).get('id', '') if member['type'] != Ent.TYPE_GROUP: if (member['type'] in typesSet and checkCIMemberMatch(member, memberOptions) and _checkMemberRole(member, validRoles) and (not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions))): member['level'] = level - member['subgroup'] = groupName + member['subgroup'] = nameToPrint membersList.append(member) else: if (member['type'] in typesSet and checkCIMemberMatch(member, memberOptions) and (not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions))): member['level'] = level - member['subgroup'] = groupName + member['subgroup'] = nameToPrint membersList.append(member) _, gname = member['name'].rsplit('/', 1) getCIGroupMembers(ci, f'groups/{gname}', memberRoles, membersList, membersSet, i, count, - memberOptions, memberDisplayOptions, level+1, typesSet) + memberOptions, memberDisplayOptions, level+1, typesSet, memberName, kwargs) CIGROUPMEMBERS_FIELDS_CHOICE_MAP = { 'createtime': 'createTime', + 'delivery': 'deliverySetting', + 'deliverysettings': 'deliverySetting', 'expiretime': 'expireTime', 'id': 'name', 'memberkey': 'preferredMemberKey', @@ -36244,6 +36243,16 @@ CIGROUPMEMBERS_SORT_FIELDS = [ ] CIGROUPMEMBERS_TIME_OBJECTS = {'createTime', 'updateTime', 'expireTime'} +def _getCIListGroupMembersArgs(listView): + if listView == 'full': + return {'view': 'FULL', 'pageSize': GC.Values[GC.MEMBER_MAX_RESULTS_CI_FULL], + 'fields': 'nextPageToken,memberships(*)'} + if listView == 'basic': + return {'view': 'FULL', 'pageSize': GC.Values[GC.MEMBER_MAX_RESULTS_CI_FULL], + 'fields': 'nextPageToken,memberships(name,preferredMemberKey,roles,type)'} + return {'view': 'BASIC', 'pageSize': GC.Values[GC.MEMBER_MAX_RESULTS_CI_BASIC], + 'fields': 'nextPageToken,memberships(*)'} + # gam print cigroup-members [todrive *] # [(cimember|ciowner )|(cigroup )|(select )] # [showownedby ] @@ -36253,6 +36262,7 @@ CIGROUPMEMBERS_TIME_OBJECTS = {'createTime', 'updateTime', 'expireTime'} # [types ] # [memberemaildisplaypattern|memberemailskippattern ] # * [fields ] +# [minimal|basic|full] # [(recursive [noduplicates])|includederivedmembership] [nogroupeemail] # [formatjson [quotechar ]] def doPrintCIGroupMembers(): @@ -36270,7 +36280,7 @@ def doPrintCIGroupMembers(): rolesSet = set() typesSet = set() matchPatterns = {} - listView = 'FULL' + listView = 'full' while Cmd.ArgumentsRemaining(): myarg = getArgument() if myarg == 'todrive': @@ -36311,10 +36321,12 @@ def doPrintCIGroupMembers(): memberOptions[MEMBEROPTION_RECURSIVE] = False elif myarg == 'nogroupemail': groupColumn = False - elif myarg == 'basic': - listView = 'BASIC' + elif myarg in {'minimal', 'basic', 'full'}: + listView = myarg else: FJQC.GetFormatJSONQuoteChar(myarg, False) + if listView == 'minimal' and memberOptions[MEMBEROPTION_RECURSIVE]: + usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('minimal', 'recursive')) if not typesSet: typesSet = {Ent.TYPE_USER} if memberOptions[MEMBEROPTION_RECURSIVE] else ALL_CIGROUP_MEMBER_TYPES fields = ','.join(set(groupFieldsLists['ci'])) @@ -36341,6 +36353,7 @@ def doPrintCIGroupMembers(): if showOwnedBy: getRolesSet.add(Ent.ROLE_OWNER) getRoles = ','.join(sorted(getRolesSet)) + kwargs = _getCIListGroupMembersArgs(listView) level = 0 i = 0 count = len(entityList) @@ -36367,7 +36380,7 @@ def doPrintCIGroupMembers(): membersList = [] membersSet = set() getCIGroupMembers(ci, groupEntity['name'], getRoles, membersList, membersSet, i, count, - memberOptions, memberDisplayOptions, level, typesSet, listView, groupEmail) + memberOptions, memberDisplayOptions, level, typesSet, groupEmail, kwargs) if showOwnedBy and not checkCIGroupShowOwnedBy(showOwnedBy, membersList): continue for member in membersList: @@ -36385,6 +36398,8 @@ def doPrintCIGroupMembers(): row['subgroup'] = member['subgroup'] if memberDisplayOptions['showCategory']: row['category'] = member['category'] + if listView == 'minimal': + dmember.pop('type', None) mapCIGroupMemberFieldNames(dmember) if not FJQC.formatJSON: csvPF.WriteRowTitles(flattenJSON(dmember, flattened=row, timeObjects=CIGROUPMEMBERS_TIME_OBJECTS)) @@ -36414,17 +36429,19 @@ def doPrintCIGroupMembers(): # [showownedby ] # [emailmatchpattern [not] ] [namematchpattern [not] ] # [descriptionmatchpattern [not] ] -# [roles ] [members] [managers] [owners] [depth ] +# [roles ] [members] [managers] [owners] # [internal] [internaldomains ] [external] # [types ] # [memberemaildisplaypattern|memberemailskippattern ] -# [includederivedmembership] +# [minimal|basic|full] +# [(depth ) | includederivedmembership] def doShowCIGroupMembers(): def _roleOrder(key): return {Ent.ROLE_OWNER: 0, Ent.ROLE_MANAGER: 1, Ent.ROLE_MEMBER: 2}.get(key, 3) def _typeOrder(key): - return {Ent.TYPE_CUSTOMER: 0, Ent.TYPE_USER: 1, Ent.TYPE_GROUP: 2, Ent.TYPE_EXTERNAL: 3}.get(key, 4) + return {Ent.TYPE_CUSTOMER: 0, Ent.TYPE_USER: 1, Ent.TYPE_GROUP: 2, + Ent.TYPE_CBCM_BROWSER: 3, Ent.TYPE_OTHER: 4, Ent.TYPE_EXTERNAL: 5}.get(key, 6) def _showGroup(groupName, groupEmail, depth): if includeDerivedMembership: @@ -36435,8 +36452,7 @@ def doShowCIGroupMembers(): try: membersList = callGAPIpages(ci.groups().memberships(), 'list', 'memberships', throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS, - parent=groupName, view='FULL', - fields='nextPageToken,memberships(*)', pageSize=GC.Values[GC.MEMBER_MAX_RESULTS]) + parent=groupName, **kwargs) for member in membersList: getCIGroupMemberRoleFixType(member) except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, @@ -36457,7 +36473,10 @@ def doShowCIGroupMembers(): if (_checkMemberRole(member, rolesSet) and member['type'] in typesSet and checkCIMemberMatch(member, memberOptions)): - memberDetails = f'{member.get("role", Ent.ROLE_MEMBER)}, {member["type"]}, {member["preferredMemberKey"]["id"]}' + if listView != 'minimal': + memberDetails = f'{member.get("role", Ent.ROLE_MEMBER)}, {member["type"]}, {member["preferredMemberKey"]["id"]}' + else: + memberDetails = f'{member.get("role", Ent.ROLE_MEMBER)}, {member["preferredMemberKey"]["id"]}' if checkCategory: memberDetails += f', {member["category"]}' for field in ['createTime', 'updateTime', 'expireTime']: @@ -36482,6 +36501,7 @@ def doShowCIGroupMembers(): matchPatterns = {} maxdepth = -1 includeDerivedMembership = False + listView = 'full' while Cmd.ArgumentsRemaining(): myarg = getArgument() if myarg == 'showownedby': @@ -36512,6 +36532,8 @@ def doShowCIGroupMembers(): maxdepth = getInteger(minVal=-1) elif myarg == 'includederivedmembership': includeDerivedMembership = True + elif myarg in {'minimal', 'basic', 'full'}: + listView = myarg else: unknownArgumentExit() if not rolesSet: @@ -36521,6 +36543,7 @@ def doShowCIGroupMembers(): checkCategory = memberDisplayOptions['showCategory'] fields = ','.join(set(groupFieldsLists['ci'])) entityList = getCIGroupMembersEntityList(ci, entityList, query, subTitle, matchPatterns, groupFieldsLists['ci'], None) + kwargs = _getCIListGroupMembersArgs(listView) i = 0 count = len(entityList) for group in entityList: diff --git a/src/gam/gamlib/glcfg.py b/src/gam/gamlib/glcfg.py index 49a6fc0f..7bfa7a68 100644 --- a/src/gam/gamlib/glcfg.py +++ b/src/gam/gamlib/glcfg.py @@ -180,7 +180,7 @@ LICENSE_SKUS = 'license_skus' # When retrieving lists of Google Group members from API, how many should be retrieved in each chunk MEMBER_MAX_RESULTS = 'member_max_results' # CI API Group members max page size when view=BASIC -MEMBER_MAX_RESULTS_CI_BASIC = 'member.max_results_ci_basic' +MEMBER_MAX_RESULTS_CI_BASIC = 'member_max_results_ci_basic' # CI API Group members max page size when view=FULL MEMBER_MAX_RESULTS_CI_FULL = 'member_max_results_ci_full' # When deleting or modifying Gmail messages, how many should be processed in each batch From 0d9e35d0139b3a6db88e213ec0fd9ad423f1ebd4 Mon Sep 17 00:00:00 2001 From: Ross Scroggs Date: Sun, 16 Feb 2025 10:35:35 -0800 Subject: [PATCH 02/14] Updated `gam print group-members|cigroup-members` to include the `email` column when `fields ` did not include `email`. --- src/GamCommands.txt | 5 +++-- src/GamUpdate.txt | 7 +++++++ src/gam/__init__.py | 34 +++++++++++++++++++++------------- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/GamCommands.txt b/src/GamCommands.txt index fa03e22b..01f747bd 100644 --- a/src/GamCommands.txt +++ b/src/GamCommands.txt @@ -3997,13 +3997,14 @@ gam print cigroups [todrive *] ::= createtime + email|useremail| expiretime| memberkey| name| role| type| - updatetime| - useremail + updatetime + ::= "(,)*" gam info cimember diff --git a/src/GamUpdate.txt b/src/GamUpdate.txt index 82ea0bc6..e07c0da7 100644 --- a/src/GamUpdate.txt +++ b/src/GamUpdate.txt @@ -1,3 +1,8 @@ +7.04.04 + +Updated `gam print group-members|cigroup-members` to include the `email` column +when `fields ` did not include `email`. + 7.04.03 Added option `minimal|basic|full` to `gam print cigroup-members`: @@ -10,6 +15,8 @@ Added option `minimal|basic|full` to `gam show cigroup-members`: * `basic` - Fields displayed: type, role, email * `full` - Fields displayed: type, role, email, createTime, updateTime; this is the default +Upgraded `gam print cigroup-members ... recursive` to display sub-group email addresses rather than IDs. + 7.04.02 Improved output formatting for the following commands: diff --git a/src/gam/__init__.py b/src/gam/__init__.py index f27ce2fd..ecec529d 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -25,7 +25,7 @@ https://github.com/GAM-team/GAM/wiki """ __author__ = 'GAM Team ' -__version__ = '7.04.03' +__version__ = '7.04.04' __license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)' #pylint: disable=wrong-import-position @@ -7324,12 +7324,6 @@ def _getRawFields(requiredField=None): return rawFields return f'{requiredField},{rawFields}' -def _addInitialField(fieldsList, initialField): - if isinstance(initialField, list): - fieldsList.extend(initialField) - else: - fieldsList.append(initialField) - def CheckInputRowFilterHeaders(titlesList, rowFilter, rowDropFilter): status = True for filterVal in rowFilter: @@ -7744,6 +7738,12 @@ def RowFilterMatch(row, titlesList, rowFilter, rowFilterModeAll, rowDropFilter, # } # fieldsList is the list of API fields def getFieldsList(myarg, fieldsChoiceMap, fieldsList, initialField=None, fieldsArg='fields', onlyFieldsArg=False): + def addInitialField(): + if isinstance(initialField, list): + fieldsList.extend(initialField) + else: + fieldsList.append(initialField) + def addMappedFields(mappedFields): if isinstance(mappedFields, list): fieldsList.extend(mappedFields) @@ -7752,11 +7752,11 @@ def getFieldsList(myarg, fieldsChoiceMap, fieldsList, initialField=None, fieldsA if not onlyFieldsArg and myarg in fieldsChoiceMap: if not fieldsList and initialField is not None: - _addInitialField(fieldsList, initialField) + addInitialField() addMappedFields(fieldsChoiceMap[myarg]) elif myarg == fieldsArg: if not fieldsList and initialField is not None: - _addInitialField(fieldsList, initialField) + addInitialField() for field in _getFieldsList(): if field in fieldsChoiceMap: addMappedFields(fieldsChoiceMap[field]) @@ -7933,14 +7933,21 @@ class CSVPrintFile(): fieldsList.append(fields) self.AddTitles(fields.replace('.', GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER])) + def addInitialField(self, initialField, fieldsChoiceMap, fieldsList): + if isinstance(initialField, list): + for field in initialField: + self.AddField(field, fieldsChoiceMap, fieldsList) + else: + self.AddField(initialField, fieldsChoiceMap, fieldsList) + def GetFieldsListTitles(self, fieldName, fieldsChoiceMap, fieldsList, initialField=None): if fieldName in fieldsChoiceMap: if not fieldsList and initialField is not None: - _addInitialField(fieldsList, initialField) + self.addInitialField(initialField, fieldsChoiceMap, fieldsList) self.AddField(fieldName, fieldsChoiceMap, fieldsList) elif fieldName == 'fields': if not fieldsList and initialField is not None: - _addInitialField(fieldsList, initialField) + self.addInitialField(initialField, fieldsChoiceMap, fieldsList) for field in _getFieldsList(): if field in fieldsChoiceMap: self.AddField(field, fieldsChoiceMap, fieldsList) @@ -34206,7 +34213,7 @@ def doPrintGroupMembers(): pass elif getMemberMatchOptions(myarg, memberOptions): pass - elif csvPF.GetFieldsListTitles(myarg, GROUPMEMBERS_FIELDS_CHOICE_MAP, fieldsList): + elif csvPF.GetFieldsListTitles(myarg, GROUPMEMBERS_FIELDS_CHOICE_MAP, fieldsList, initialField='email'): pass elif myarg == 'membernames': memberOptions[MEMBEROPTION_MEMBERNAMES] = True @@ -36223,6 +36230,7 @@ CIGROUPMEMBERS_FIELDS_CHOICE_MAP = { 'createtime': 'createTime', 'delivery': 'deliverySetting', 'deliverysettings': 'deliverySetting', + 'email': 'preferredMemberKey', 'expiretime': 'expireTime', 'id': 'name', 'memberkey': 'preferredMemberKey', @@ -36309,7 +36317,7 @@ def doPrintCIGroupMembers(): pass elif getMemberMatchOptions(myarg, memberOptions): pass - elif getFieldsList(myarg, CIGROUPMEMBERS_FIELDS_CHOICE_MAP, fieldsList): + elif getFieldsList(myarg, CIGROUPMEMBERS_FIELDS_CHOICE_MAP, fieldsList, initialField='preferredMemberKey'): pass elif myarg == 'noduplicates': memberOptions[MEMBEROPTION_NODUPLICATES] = True From bd699e2b3161f32ac2ab93b26995f216cb2b1b8c Mon Sep 17 00:00:00 2001 From: Ross Scroggs Date: Tue, 18 Feb 2025 10:40:44 -0800 Subject: [PATCH 03/14] Added initial support for Meet API v2beta --- src/GamCommands.txt | 11 ++++++++- src/GamUpdate.txt | 22 +++++++++++++++++ src/gam.spec | 1 + src/gam/__init__.py | 52 ++++++++++++++++++++++++++--------------- src/gam/gamlib/glapi.py | 2 ++ src/gam/gamlib/glcfg.py | 4 ++++ 6 files changed, 72 insertions(+), 20 deletions(-) diff --git a/src/GamCommands.txt b/src/GamCommands.txt index 01f747bd..408bf993 100644 --- a/src/GamCommands.txt +++ b/src/GamCommands.txt @@ -7882,7 +7882,16 @@ gam show lookerstudiopermissions ::= spaces/ | ::= accesstype open|trusted|restricted | - entrypointaccess all|creatorapponly + entrypointaccess all|creatorapponly | + moderation | + chatrestriction hostsonly|norestriction | + reactionrestriction hostsonly|norestriction | + presentrestriction hostsonly|norestriction | + defaultjoinasviewer | + firstjoiner hostsonly|anyone | + recording | + transcription | + smartnotes gam create meetspace * diff --git a/src/GamUpdate.txt b/src/GamUpdate.txt index e07c0da7..894a4c3a 100644 --- a/src/GamUpdate.txt +++ b/src/GamUpdate.txt @@ -1,3 +1,25 @@ +7.04.05 + +Added initial support for Meet API v2beta; you must be in the Developer Preview program +for this to be effective. +* https://developers.google.com/meet/api/guides/beta/configuration-beta#auto-artifacts + +Added `meet_v2_beta` Boolean variable to `gam.cfg`. When this variable is true, +the following options are added to `` used in `gam create|update meetspace`. +``` + moderation | + chatrestriction hostsonly|norestriction | + reactionrestriction hostsonly|norestriction | + presentrestriction hostsonly|norestriction | + defaultjoinasviewer | + firstjoiner hostsonly|anyone | + recording | + transcription | + smartnotes +``` + +This isn't called beta for nothing, I have found problems and reported them. + 7.04.04 Updated `gam print group-members|cigroup-members` to include the `email` column diff --git a/src/gam.spec b/src/gam.spec index 594cc59b..58080eff 100644 --- a/src/gam.spec +++ b/src/gam.spec @@ -31,6 +31,7 @@ for pkg in GAM_VER_LIBS: datas += [('gam/cbcm-v1.1beta1.json', '.')] datas += [('gam/contactdelegation-v1.json', '.')] datas += [('gam/datastudio-v1.json', '.')] +datas += [('gam/meet-v2beta.json', '.')] datas += [('gam/serviceaccountlookup-v1.json', '.')] datas += [('cacerts.pem', '.')] hiddenimports = [ diff --git a/src/gam/__init__.py b/src/gam/__init__.py index ecec529d..82d82a76 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -25,7 +25,7 @@ https://github.com/GAM-team/GAM/wiki """ __author__ = 'GAM Team ' -__version__ = '7.04.04' +__version__ = '7.04.05' __license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)' #pylint: disable=wrong-import-position @@ -4428,7 +4428,7 @@ class signjwtSignJwt(google.auth.crypt.Signer): except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError) as e: systemErrorExit(API_ACCESS_DENIED_RC, str(e)) httpObj = transportAuthorizedHttp(credentials, http=getHttpObj(override_min_tls='TLSv1_2')) - iamc = getService(API.IAM_CREDENTIALS, httpObj) + iamc, _ = getService(API.IAM_CREDENTIALS, httpObj) response = callGAPI(iamc.projects().serviceAccounts(), 'signJwt', name=self.name, body={'payload': json.dumps(message)}) signed_jwt = response.get('signedJwt') @@ -4717,7 +4717,7 @@ def getService(api, httpObj): service = googleapiclient.discovery.build_from_document(GM.Globals[GM.CURRENT_API_SERVICES][api][version], http=httpObj) if GM.Globals[GM.CACHE_DISCOVERY_ONLY]: clearServiceCache(service) - return service + return (service, api) if not hasLocalJSON: triesLimit = 3 for n in range(1, triesLimit+1): @@ -4731,7 +4731,7 @@ def getService(api, httpObj): setattr(service, '_baseUrl', getattr(service, '_baseUrl').replace('/v3/', '/v3beta/')) if GM.Globals[GM.CACHE_DISCOVERY_ONLY]: clearServiceCache(service) - return service + return (service, api) except googleapiclient.errors.UnknownApiNameOrVersion as e: systemErrorExit(GOOGLE_API_ERROR_RC, Msg.UNKNOWN_API_OR_VERSION.format(str(e), __author__)) except (googleapiclient.errors.InvalidJsonError, KeyError, ValueError) as e: @@ -4758,7 +4758,7 @@ def getService(api, httpObj): GM.Globals[GM.CURRENT_API_SERVICES][api][version] = service._rootDesc.copy() if GM.Globals[GM.CACHE_DISCOVERY_ONLY]: clearServiceCache(service) - return service + return (service, api) except (googleapiclient.errors.InvalidJsonError, KeyError, ValueError) as e: invalidDiscoveryJsonExit(disc_file, str(e)) except IOError as e: @@ -5541,7 +5541,7 @@ def buildGAPIObject(api, credentials=None): if credentials is None: credentials = getClientCredentials(api=api, refreshOnly=True) httpObj = transportAuthorizedHttp(credentials, http=getHttpObj(cache=GM.Globals[GM.CACHE_DIR])) - service = getService(api, httpObj) + service, api = getService(api, httpObj) if not GC.Values[GC.ENABLE_DASA]: try: API_Scopes = set(list(service._rootDesc['auth']['oauth2']['scopes'])) @@ -5570,7 +5570,7 @@ def getSaUser(user): def buildGAPIServiceObject(api, user, i=0, count=0, displayError=True): userEmail = getSaUser(user) httpObj = getHttpObj(cache=GM.Globals[GM.CACHE_DIR]) - service = getService(api, httpObj) + service, api = getService(api, httpObj) credentials = getSvcAcctCredentials(api, userEmail) request = transportCreateRequest(httpObj) triesLimit = 3 @@ -5607,7 +5607,7 @@ def buildGAPIServiceObject(api, user, i=0, count=0, displayError=True): def buildGAPIObjectNoAuthentication(api): httpObj = getHttpObj(cache=GM.Globals[GM.CACHE_DIR]) - service = getService(api, httpObj) + service, _ = getService(api, httpObj) return service def initGDataObject(gdataObj, api): @@ -27488,6 +27488,8 @@ def printShowChatEvents(users): csvPF.writeCSVfile('Chat Events') def buildMeetServiceObject(api=API.MEET, user=None, i=0, count=0, entityTypeList=None): + if GC.Values[GC.MEET_V2_BETA]: + api = API.MEET_BETA user, meet = buildGAPIServiceObject(api, user, i, count) kvList = [Ent.USER, user] if entityTypeList is not None: @@ -27511,7 +27513,10 @@ MEET_SPACE_OPTIONS_MAP = { 'reactionrestriction': 'reactionRestriction', 'presentrestriction': 'presentRestriction', 'defaultjoinasviewer': 'defaultJoinAsViewerType', - 'firstjoiner': 'firstJoinerType' + 'firstjoiner': 'firstJoinerType', + 'autorecording': 'recordingConfig', + 'autosmartnotes': 'smartNotesConfig', + 'autotranscription': 'transcriptionConfig', } MEET_SPACE_ACCESSTYPE_CHOICES = {'open', 'trusted', 'restricted'} @@ -27530,6 +27535,12 @@ MEET_SPACE_FIRSTJOINERTYPE_CHOICES_MAP = { 'anyone': 'ANYONE' } +MEET_SPACE_ARTIFACT_SUB_OPTIONS = { + 'recordingConfig': 'autoRecordingGeneration', + 'smartNotesConfig': 'autoSmartNotesGeneration', + 'transcriptionConfig': 'autoTranscriptionGeneration' + } + # [accesstype open|trusted|restricted] # [entrypointaccess all|creatorapponly] # [moderation ] @@ -27538,7 +27549,10 @@ MEET_SPACE_FIRSTJOINERTYPE_CHOICES_MAP = { # [presentrestriction hostsonly|norestriction] # [defaultjoinasviewer ] # [firstjoiner hostsonly|anyone] -def _getMeetSpaceParameters(myarg, body, updateMask): +# [autorecording ] +# [autosmartnotes ] +# [autotranscription ] +def _getMeetSpaceParameters(myarg, body): option = MEET_SPACE_OPTIONS_MAP.get(myarg, None) if option is None: return False @@ -27548,15 +27562,17 @@ def _getMeetSpaceParameters(myarg, body, updateMask): body['config'][option] = getChoice(MEET_SPACE_ENTRYPOINTACCESS_CHOICES_MAP, mapChoice=True) elif option == 'moderation': body['config'][option] = 'ON' if getBoolean() else 'OFF' - elif option in {'chatrestriction', 'reactionrestriction', 'presentrestriction'}: - body['config'].setdefault('moderationRestictions', {}) + elif option in {'chatRestriction', 'reactionRestriction', 'presentRestriction'}: + body['config'].setdefault('moderationRestrictions', {}) body['config']['moderationRestrictions'][option] = getChoice(MEET_SPACE_RESTRICTIONS_CHOICES_MAP, mapChoice=True) - option = f'moderationRestrictions.{option}' elif option == 'defaultJoinAsViewerType': body['config'][option] = 'ON' if getBoolean() else 'OFF' elif option == 'firstJoinerType': body['config'][option] = getChoice(MEET_SPACE_FIRSTJOINERTYPE_CHOICES_MAP, mapChoice=True) - updateMask.append(f'config.{option}') + elif option in {'recordingConfig', 'transcriptionConfig', 'smartNotesConfig'}: + body['config'].setdefault('artifactConfig', {}) + body['config']['artifactConfig'].setdefault(option, {}) + body['config']['artifactConfig'][option][MEET_SPACE_ARTIFACT_SUB_OPTIONS[option]] = 'ON' if getBoolean() else 'OFF' return True # gam create meetspace @@ -27571,10 +27587,9 @@ def createMeetSpace(users): # 'firstJoinerType': 'ANYONE', }} returnIdOnly = False - updateMask = [] while Cmd.ArgumentsRemaining(): myarg = getArgument() - if _getMeetSpaceParameters(myarg, body, updateMask): + if _getMeetSpaceParameters(myarg, body): pass elif myarg == 'returnidonly': returnIdOnly = True @@ -27609,12 +27624,11 @@ def updateMeetSpace(users): FJQC = FormatJSONQuoteChar() name = None body = {'config': {}} - updateMask = [] while Cmd.ArgumentsRemaining(): myarg = getArgument() if (myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/')): name = getSpaceName(myarg) - elif _getMeetSpaceParameters(myarg, body, updateMask): + elif _getMeetSpaceParameters(myarg, body): pass else: FJQC.GetFormatJSON(myarg) @@ -27629,7 +27643,7 @@ def updateMeetSpace(users): try: space = callGAPI(meet.spaces(), 'patch', throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED], - name=name, updateMask=','.join(updateMask), body=body) + name=name, updateMask='', body=body) if not FJQC.formatJSON: entityActionPerformed(kvList, i, count) Ind.Increment() diff --git a/src/gam/gamlib/glapi.py b/src/gam/gamlib/glapi.py index 3789b94d..e9d3ba99 100644 --- a/src/gam/gamlib/glapi.py +++ b/src/gam/gamlib/glapi.py @@ -75,6 +75,7 @@ KEEP = 'keep' LICENSING = 'licensing' LOOKERSTUDIO = 'datastudio' MEET = 'meet' +MEET_BETA = 'meetbeta' OAUTH2 = 'oauth2' ORGPOLICY = 'orgpolicy' PEOPLE = 'people' @@ -251,6 +252,7 @@ _INFO = { LICENSING: {'name': 'License Manager API', 'version': 'v1', 'v2discovery': True}, LOOKERSTUDIO: {'name': 'Looker Studio API', 'version': 'v1', 'v2discovery': True, 'localjson': True}, MEET: {'name': 'Meet API', 'version': 'v2', 'v2discovery': True}, + MEET_BETA: {'name': 'Meet API', 'version': 'v2beta', 'v2discovery': True, 'localjson': True, 'mappedAPI': MEET}, OAUTH2: {'name': 'OAuth2 API', 'version': 'v2', 'v2discovery': False}, ORGPOLICY: {'name': 'Organization Policy API', 'version': 'v2', 'v2discovery': True}, PEOPLE: {'name': 'People API', 'version': 'v1', 'v2discovery': True}, diff --git a/src/gam/gamlib/glcfg.py b/src/gam/gamlib/glcfg.py index 7bfa7a68..337e2b80 100644 --- a/src/gam/gamlib/glcfg.py +++ b/src/gam/gamlib/glcfg.py @@ -177,6 +177,8 @@ INTER_BATCH_WAIT = 'inter_batch_wait' LICENSE_MAX_RESULTS = 'license_max_results' # License SKUs to process LICENSE_SKUS = 'license_skus' +# Use Meet V2 beta +MEET_V2_BETA = 'meet_v2_beta' # When retrieving lists of Google Group members from API, how many should be retrieved in each chunk MEMBER_MAX_RESULTS = 'member_max_results' # CI API Group members max page size when view=BASIC @@ -388,6 +390,7 @@ Defaults = { INTER_BATCH_WAIT: '0', LICENSE_MAX_RESULTS: '100', LICENSE_SKUS: '', + MEET_V2_BETA: FALSE, MEMBER_MAX_RESULTS: '200', MEMBER_MAX_RESULTS_CI_BASIC: '1000', MEMBER_MAX_RESULTS_CI_FULL: '500', @@ -555,6 +558,7 @@ VAR_INFO = { INTER_BATCH_WAIT: {VAR_TYPE: TYPE_FLOAT, VAR_LIMITS: (0.0, 60.0)}, LICENSE_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (10, 1000)}, LICENSE_SKUS: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)}, + MEET_V2_BETA: {VAR_TYPE: TYPE_BOOLEAN}, MEMBER_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 200)}, MEMBER_MAX_RESULTS_CI_BASIC: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 1000)}, MEMBER_MAX_RESULTS_CI_FULL: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 500)}, From 3004da5ad7adf406524094faab75e55f2157f200 Mon Sep 17 00:00:00 2001 From: Ross Scroggs Date: Tue, 18 Feb 2025 10:47:03 -0800 Subject: [PATCH 04/14] Create meet-v2beta.json --- src/gam/meet-v2beta.json | 1152 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 1152 insertions(+) create mode 100644 src/gam/meet-v2beta.json diff --git a/src/gam/meet-v2beta.json b/src/gam/meet-v2beta.json new file mode 100644 index 00000000..a13aa4fa --- /dev/null +++ b/src/gam/meet-v2beta.json @@ -0,0 +1,1152 @@ +{ + "version": "v2beta", + "discoveryVersion": "v1", + "mtlsRootUrl": "https://meet.mtls.googleapis.com/", + "batchPath": "batch", + "servicePath": "", + "title": "Google Meet API", + "canonicalName": "Meet", + "protocol": "rest", + "ownerName": "Google", + "icons": { + "x32": "http://www.google.com/images/icons/product/search-32.gif", + "x16": "http://www.google.com/images/icons/product/search-16.gif" + }, + "revision": "20250213", + "schemas": { + "ListTranscriptsResponse": { + "id": "ListTranscriptsResponse", + "properties": { + "nextPageToken": { + "type": "string", + "description": "Token to be circulated back for further List call if current List doesn't include all the transcripts. Unset if all transcripts are returned." + }, + "transcripts": { + "type": "array", + "items": { + "$ref": "Transcript" + }, + "description": "List of transcripts in one page." + } + }, + "type": "object", + "description": "Response for ListTranscripts method." + }, + "Transcript": { + "type": "object", + "id": "Transcript", + "description": "Metadata for a transcript generated from a conference. It refers to the ASR (Automatic Speech Recognition) result of user's speech during the conference.", + "properties": { + "name": { + "readOnly": true, + "type": "string", + "description": "Output only. Resource name of the transcript. Format: `conferenceRecords/{conference_record}/transcripts/{transcript}`, where `{transcript}` is a 1:1 mapping to each unique transcription session of the conference." + }, + "docsDestination": { + "readOnly": true, + "description": "Output only. Where the Google Docs transcript is saved.", + "$ref": "DocsDestination" + }, + "endTime": { + "readOnly": true, + "description": "Output only. Timestamp when the transcript stopped.", + "format": "google-datetime", + "type": "string" + }, + "startTime": { + "type": "string", + "description": "Output only. Timestamp when the transcript started.", + "format": "google-datetime", + "readOnly": true + }, + "state": { + "type": "string", + "readOnly": true, + "enum": [ + "STATE_UNSPECIFIED", + "STARTED", + "ENDED", + "FILE_GENERATED" + ], + "description": "Output only. Current state.", + "enumDescriptions": [ + "Default, never used.", + "An active transcript session has started.", + "This transcript session has ended, but the transcript file hasn't been generated yet.", + "Transcript file is generated and ready to download." + ] + } + } + }, + "ListConferenceRecordsResponse": { + "type": "object", + "id": "ListConferenceRecordsResponse", + "description": "Response of ListConferenceRecords method.", + "properties": { + "nextPageToken": { + "type": "string", + "description": "Token to be circulated back for further List call if current List does NOT include all the Conferences. Unset if all conferences have been returned." + }, + "conferenceRecords": { + "description": "List of conferences in one page.", + "items": { + "$ref": "ConferenceRecord" + }, + "type": "array" + } + } + }, + "SpaceConfig": { + "properties": { + "accessType": { + "type": "string", + "enum": [ + "ACCESS_TYPE_UNSPECIFIED", + "OPEN", + "TRUSTED", + "RESTRICTED" + ], + "enumDescriptions": [ + "Default value specified by the user's organization. Note: This is never returned, as the configured access type is returned instead.", + "Anyone with the join information (for example, the URL or phone access information) can join without knocking.", + "Members of the host's organization, invited external users, and dial-in users can join without knocking. Everyone else must knock.", + "Only invitees can join without knocking. Everyone else must knock." + ], + "description": "Access type of the meeting space that determines who can join without knocking. Default: The user's default access settings. Controlled by the user's admin for enterprise users or RESTRICTED." + }, + "entryPointAccess": { + "description": "Defines the entry points that can be used to join meetings hosted in this meeting space. Default: EntryPointAccess.ALL", + "enumDescriptions": [ + "Unused.", + "All entry points are allowed.", + "Only entry points owned by the Google Cloud project that created the space can be used to join meetings in this space. Apps can use the Meet Embed SDK Web or mobile Meet SDKs to create owned entry points." + ], + "enum": [ + "ENTRY_POINT_ACCESS_UNSPECIFIED", + "ALL", + "CREATOR_APP_ONLY" + ], + "type": "string" + } + }, + "description": "The configuration pertaining to a meeting space.", + "type": "object", + "id": "SpaceConfig" + }, + "DriveDestination": { + "id": "DriveDestination", + "description": "Export location where a recording file is saved in Google Drive.", + "properties": { + "exportUri": { + "readOnly": true, + "description": "Output only. Link used to play back the recording file in the browser. For example, `https://drive.google.com/file/d/{$fileId}/view`.", + "type": "string" + }, + "file": { + "type": "string", + "description": "Output only. The `fileId` for the underlying MP4 file. For example, \"1kuceFZohVoCh6FulBHxwy6I15Ogpc4hP\". Use `$ GET https://www.googleapis.com/drive/v3/files/{$fileId}?alt=media` to download the blob. For more information, see https://developers.google.com/drive/api/v3/reference/files/get.", + "readOnly": true + } + }, + "type": "object" + }, + "Recording": { + "properties": { + "driveDestination": { + "$ref": "DriveDestination", + "description": "Output only. Recording is saved to Google Drive as an MP4 file. The `drive_destination` includes the Drive `fileId` that can be used to download the file using the `files.get` method of the Drive API.", + "readOnly": true + }, + "startTime": { + "format": "google-datetime", + "description": "Output only. Timestamp when the recording started.", + "readOnly": true, + "type": "string" + }, + "state": { + "enumDescriptions": [ + "Default, never used.", + "An active recording session has started.", + "This recording session has ended, but the recording file hasn't been generated yet.", + "Recording file is generated and ready to download." + ], + "type": "string", + "description": "Output only. Current state.", + "readOnly": true, + "enum": [ + "STATE_UNSPECIFIED", + "STARTED", + "ENDED", + "FILE_GENERATED" + ] + }, + "endTime": { + "format": "google-datetime", + "readOnly": true, + "type": "string", + "description": "Output only. Timestamp when the recording ended." + }, + "name": { + "description": "Output only. Resource name of the recording. Format: `conferenceRecords/{conference_record}/recordings/{recording}` where `{recording}` is a 1:1 mapping to each unique recording session during the conference.", + "readOnly": true, + "type": "string" + } + }, + "type": "object", + "id": "Recording", + "description": "Metadata about a recording created during a conference." + }, + "ParticipantSession": { + "properties": { + "startTime": { + "readOnly": true, + "type": "string", + "format": "google-datetime", + "description": "Output only. Timestamp when the user session starts." + }, + "name": { + "description": "Identifier. Session id.", + "type": "string" + }, + "endTime": { + "format": "google-datetime", + "type": "string", + "readOnly": true, + "description": "Output only. Timestamp when the user session ends. Unset if the user session hasn’t ended." + } + }, + "id": "ParticipantSession", + "type": "object", + "description": "Refers to each unique join or leave session when a user joins a conference from a device. Note that any time a user joins the conference a new unique ID is assigned. That means if a user joins a space multiple times from the same device, they're assigned different IDs, and are also be treated as different participant sessions." + }, + "ListRecordingsResponse": { + "id": "ListRecordingsResponse", + "description": "Response for ListRecordings method.", + "type": "object", + "properties": { + "nextPageToken": { + "type": "string", + "description": "Token to be circulated back for further List call if current List doesn't include all the recordings. Unset if all recordings are returned." + }, + "recordings": { + "description": "List of recordings in one page.", + "type": "array", + "items": { + "$ref": "Recording" + } + } + } + }, + "EndActiveConferenceRequest": { + "id": "EndActiveConferenceRequest", + "properties": {}, + "description": "Request to end an ongoing conference of a space.", + "type": "object" + }, + "ListParticipantsResponse": { + "properties": { + "totalSize": { + "type": "integer", + "format": "int32", + "description": "Total, exact number of `participants`. By default, this field isn't included in the response. Set the field mask in [SystemParameterContext](https://cloud.google.com/apis/docs/system-parameters) to receive this field in the response." + }, + "participants": { + "description": "List of participants in one page.", + "items": { + "$ref": "Participant" + }, + "type": "array" + }, + "nextPageToken": { + "type": "string", + "description": "Token to be circulated back for further List call if current List doesn't include all the participants. Unset if all participants are returned." + } + }, + "type": "object", + "description": "Response of ListParticipants method.", + "id": "ListParticipantsResponse" + }, + "Empty": { + "id": "Empty", + "type": "object", + "properties": {}, + "description": "A generic empty message that you can re-use to avoid defining duplicated empty messages in your APIs. A typical example is to use it as the request or the response type of an API method. For instance: service Foo { rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); }" + }, + "ListTranscriptEntriesResponse": { + "properties": { + "nextPageToken": { + "description": "Token to be circulated back for further List call if current List doesn't include all the transcript entries. Unset if all entries are returned.", + "type": "string" + }, + "transcriptEntries": { + "items": { + "$ref": "TranscriptEntry" + }, + "type": "array", + "description": "List of TranscriptEntries in one page." + } + }, + "id": "ListTranscriptEntriesResponse", + "description": "Response for ListTranscriptEntries method.", + "type": "object" + }, + "ConferenceRecord": { + "description": "Single instance of a meeting held in a space.", + "properties": { + "space": { + "description": "Output only. The space where the conference was held.", + "type": "string", + "readOnly": true + }, + "expireTime": { + "type": "string", + "description": "Output only. Server enforced expiration time for when this conference record resource is deleted. The resource is deleted 30 days after the conference ends.", + "readOnly": true, + "format": "google-datetime" + }, + "endTime": { + "readOnly": true, + "description": "Output only. Timestamp when the conference ended. Set for past conferences. Unset if the conference is ongoing.", + "format": "google-datetime", + "type": "string" + }, + "name": { + "type": "string", + "description": "Identifier. Resource name of the conference record. Format: `conferenceRecords/{conference_record}` where `{conference_record}` is a unique ID for each instance of a call within a space." + }, + "startTime": { + "type": "string", + "format": "google-datetime", + "description": "Output only. Timestamp when the conference started. Always set.", + "readOnly": true + } + }, + "id": "ConferenceRecord", + "type": "object" + }, + "AnonymousUser": { + "properties": { + "displayName": { + "type": "string", + "description": "Output only. User provided name when they join a conference anonymously.", + "readOnly": true + } + }, + "description": "User who joins anonymously (meaning not signed into a Google Account).", + "type": "object", + "id": "AnonymousUser" + }, + "ListParticipantSessionsResponse": { + "type": "object", + "properties": { + "participantSessions": { + "description": "List of participants in one page.", + "type": "array", + "items": { + "$ref": "ParticipantSession" + } + }, + "nextPageToken": { + "type": "string", + "description": "Token to be circulated back for further List call if current List doesn't include all the participants. Unset if all participants are returned." + } + }, + "description": "Response of ListParticipants method.", + "id": "ListParticipantSessionsResponse" + }, + "TranscriptEntry": { + "type": "object", + "id": "TranscriptEntry", + "properties": { + "participant": { + "type": "string", + "description": "Output only. Refers to the participant who speaks.", + "readOnly": true + }, + "languageCode": { + "type": "string", + "description": "Output only. Language of spoken text, such as \"en-US\". IETF BCP 47 syntax (https://tools.ietf.org/html/bcp47)", + "readOnly": true + }, + "startTime": { + "type": "string", + "format": "google-datetime", + "description": "Output only. Timestamp when the transcript entry started.", + "readOnly": true + }, + "name": { + "readOnly": true, + "type": "string", + "description": "Output only. Resource name of the entry. Format: \"conferenceRecords/{conference_record}/transcripts/{transcript}/entries/{entry}\"" + }, + "endTime": { + "readOnly": true, + "type": "string", + "description": "Output only. Timestamp when the transcript entry ended.", + "format": "google-datetime" + }, + "text": { + "readOnly": true, + "description": "Output only. The transcribed text of the participant's voice, at maximum 10K words. Note that the limit is subject to change.", + "type": "string" + } + }, + "description": "Single entry for one user’s speech during a transcript session." + }, + "ActiveConference": { + "description": "Active conference.", + "id": "ActiveConference", + "properties": { + "conferenceRecord": { + "readOnly": true, + "description": "Output only. Reference to 'ConferenceRecord' resource. Format: `conferenceRecords/{conference_record}` where `{conference_record}` is a unique ID for each instance of a call within a space.", + "type": "string" + } + }, + "type": "object" + }, + "PhoneUser": { + "id": "PhoneUser", + "type": "object", + "properties": { + "displayName": { + "type": "string", + "readOnly": true, + "description": "Output only. Partially redacted user's phone number when calling." + } + }, + "description": "User dialing in from a phone where the user's identity is unknown because they haven't signed in with a Google Account." + }, + "DocsDestination": { + "properties": { + "document": { + "type": "string", + "readOnly": true, + "description": "Output only. The document ID for the underlying Google Docs transcript file. For example, \"1kuceFZohVoCh6FulBHxwy6I15Ogpc4hP\". Use the `documents.get` method of the Google Docs API (https://developers.google.com/docs/api/reference/rest/v1/documents/get) to fetch the content." + }, + "exportUri": { + "type": "string", + "readOnly": true, + "description": "Output only. URI for the Google Docs transcript file. Use `https://docs.google.com/document/d/{$DocumentId}/view` to browse the transcript in the browser." + } + }, + "type": "object", + "id": "DocsDestination", + "description": "Google Docs location where the transcript file is saved." + }, + "SignedinUser": { + "description": "A signed-in user can be: a) An individual joining from a personal computer, mobile device, or through companion mode. b) A robot account used by conference room devices.", + "id": "SignedinUser", + "properties": { + "displayName": { + "readOnly": true, + "description": "Output only. For a personal device, it's the user's first name and last name. For a robot account, it's the administrator-specified device name. For example, \"Altostrat Room\".", + "type": "string" + }, + "user": { + "type": "string", + "description": "Output only. Unique ID for the user. Interoperable with Admin SDK API and People API. Format: `users/{user}`", + "readOnly": true + } + }, + "type": "object" + }, + "Space": { + "properties": { + "meetingUri": { + "description": "Output only. URI used to join meetings consisting of `https://meet.google.com/` followed by the `meeting_code`. For example, `https://meet.google.com/abc-mnop-xyz`.", + "type": "string", + "readOnly": true + }, + "activeConference": { + "description": "Active conference, if it exists.", + "$ref": "ActiveConference" + }, + "name": { + "type": "string", + "description": "Immutable. Resource name of the space. Format: `spaces/{space}`. `{space}` is the resource identifier for the space. It's a unique, server-generated ID and is case sensitive. For example, `jQCFfuBOdN5z`. For more information, see [How Meet identifies a meeting space](https://developers.google.com/meet/api/guides/meeting-spaces#identify-meeting-space)." + }, + "meetingCode": { + "description": "Output only. Type friendly unique string used to join the meeting. Format: `[a-z]+-[a-z]+-[a-z]+`. For example, `abc-mnop-xyz`. The maximum length is 128 characters. Can only be used as an alias of the space name to get the space.", + "readOnly": true, + "type": "string" + }, + "config": { + "description": "Configuration pertaining to the meeting space.", + "$ref": "SpaceConfig" + } + }, + "description": "Virtual place where conferences are held. Only one active conference can be held in one space at any given time.", + "id": "Space", + "type": "object" + }, + "Participant": { + "properties": { + "earliestStartTime": { + "type": "string", + "description": "Output only. Time when the participant first joined the meeting.", + "readOnly": true, + "format": "google-datetime" + }, + "phoneUser": { + "description": "User calling from their phone.", + "$ref": "PhoneUser" + }, + "latestEndTime": { + "type": "string", + "description": "Output only. Time when the participant left the meeting for the last time. This can be null if it's an active meeting.", + "readOnly": true, + "format": "google-datetime" + }, + "name": { + "readOnly": true, + "type": "string", + "description": "Output only. Resource name of the participant. Format: `conferenceRecords/{conference_record}/participants/{participant}`" + }, + "signedinUser": { + "description": "Signed-in user.", + "$ref": "SignedinUser" + }, + "anonymousUser": { + "description": "Anonymous user.", + "$ref": "AnonymousUser" + } + }, + "type": "object", + "id": "Participant", + "description": "User who attended or is attending a conference." + } + }, + "documentationLink": "https://developers.google.com/meet/api", + "kind": "discovery#restDescription", + "baseUrl": "https://meet.googleapis.com/v2beta/", + "basePath": "/meet/v2beta/", + "description": "Create and manage meetings in Google Meet.", + "fullyEncodeReservedExpansion": true, + "rootUrl": "https://meet.googleapis.com/", + "id": "meet:v2beta", + "resources": { + "conferenceRecords": { + "methods": { + "get": { + "flatPath": "v2beta/conferenceRecords/{conferenceRecordsId}", + "response": { + "$ref": "ConferenceRecord" + }, + "path": "v2beta/{+name}", + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created", + "https://www.googleapis.com/auth/meetings.space.readonly" + ], + "parameterOrder": [ + "name" + ], + "httpMethod": "GET", + "description": "Gets a conference record by conference ID.", + "parameters": { + "name": { + "location": "path", + "description": "Required. Resource name of the conference.", + "type": "string", + "pattern": "^conferenceRecords/[^/]+$", + "required": true + } + }, + "id": "meet.conferenceRecords.get" + }, + "list": { + "flatPath": "v2beta/conferenceRecords", + "id": "meet.conferenceRecords.list", + "response": { + "$ref": "ListConferenceRecordsResponse" + }, + "description": "Lists the conference records. By default, ordered by start time and in descending order.", + "httpMethod": "GET", + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created", + "https://www.googleapis.com/auth/meetings.space.readonly" + ], + "parameters": { + "pageToken": { + "location": "query", + "type": "string", + "description": "Optional. Page token returned from previous List Call." + }, + "filter": { + "type": "string", + "location": "query", + "description": "Optional. User specified filtering condition in [EBNF format](https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form). The following are the filterable fields: * `space.meeting_code` * `space.name` * `start_time` * `end_time` For example, consider the following filters: * `space.name = \"spaces/NAME\"` * `space.meeting_code = \"abc-mnop-xyz\"` * `start_time\u003e=\"2024-01-01T00:00:00.000Z\" AND start_time\u003c=\"2024-01-02T00:00:00.000Z\"` * `end_time IS NULL`" + }, + "pageSize": { + "type": "integer", + "format": "int32", + "description": "Optional. Maximum number of conference records to return. The service might return fewer than this value. If unspecified, at most 25 conference records are returned. The maximum value is 100; values above 100 are coerced to 100. Maximum might change in the future.", + "location": "query" + } + }, + "parameterOrder": [], + "path": "v2beta/conferenceRecords" + } + }, + "resources": { + "transcripts": { + "resources": { + "entries": { + "methods": { + "get": { + "response": { + "$ref": "TranscriptEntry" + }, + "description": "Gets a `TranscriptEntry` resource by entry ID. Note: The transcript entries returned by the Google Meet API might not match the transcription found in the Google Docs transcript file. This can occur when the Google Docs transcript file is modified after generation.", + "path": "v2beta/{+name}", + "id": "meet.conferenceRecords.transcripts.entries.get", + "parameters": { + "name": { + "type": "string", + "pattern": "^conferenceRecords/[^/]+/transcripts/[^/]+/entries/[^/]+$", + "location": "path", + "required": true, + "description": "Required. Resource name of the `TranscriptEntry`." + } + }, + "flatPath": "v2beta/conferenceRecords/{conferenceRecordsId}/transcripts/{transcriptsId}/entries/{entriesId}", + "parameterOrder": [ + "name" + ], + "httpMethod": "GET", + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created", + "https://www.googleapis.com/auth/meetings.space.readonly" + ] + }, + "list": { + "flatPath": "v2beta/conferenceRecords/{conferenceRecordsId}/transcripts/{transcriptsId}/entries", + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created", + "https://www.googleapis.com/auth/meetings.space.readonly" + ], + "id": "meet.conferenceRecords.transcripts.entries.list", + "parameterOrder": [ + "parent" + ], + "response": { + "$ref": "ListTranscriptEntriesResponse" + }, + "path": "v2beta/{+parent}/entries", + "parameters": { + "parent": { + "required": true, + "description": "Required. Format: `conferenceRecords/{conference_record}/transcripts/{transcript}`", + "type": "string", + "location": "path", + "pattern": "^conferenceRecords/[^/]+/transcripts/[^/]+$" + }, + "pageToken": { + "description": "Page token returned from previous List Call.", + "type": "string", + "location": "query" + }, + "pageSize": { + "location": "query", + "description": "Maximum number of entries to return. The service might return fewer than this value. If unspecified, at most 10 entries are returned. The maximum value is 100; values above 100 are coerced to 100. Maximum might change in the future.", + "format": "int32", + "type": "integer" + } + }, + "httpMethod": "GET", + "description": "Lists the structured transcript entries per transcript. By default, ordered by start time and in ascending order. Note: The transcript entries returned by the Google Meet API might not match the transcription found in the Google Docs transcript file. This can occur when the Google Docs transcript file is modified after generation." + } + } + } + }, + "methods": { + "list": { + "flatPath": "v2beta/conferenceRecords/{conferenceRecordsId}/transcripts", + "path": "v2beta/{+parent}/transcripts", + "httpMethod": "GET", + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created", + "https://www.googleapis.com/auth/meetings.space.readonly" + ], + "parameters": { + "pageSize": { + "description": "Maximum number of transcripts to return. The service might return fewer than this value. If unspecified, at most 10 transcripts are returned. The maximum value is 100; values above 100 are coerced to 100. Maximum might change in the future.", + "format": "int32", + "location": "query", + "type": "integer" + }, + "parent": { + "location": "path", + "pattern": "^conferenceRecords/[^/]+$", + "description": "Required. Format: `conferenceRecords/{conference_record}`", + "type": "string", + "required": true + }, + "pageToken": { + "type": "string", + "location": "query", + "description": "Page token returned from previous List Call." + } + }, + "description": "Lists the set of transcripts from the conference record. By default, ordered by start time and in ascending order.", + "parameterOrder": [ + "parent" + ], + "id": "meet.conferenceRecords.transcripts.list", + "response": { + "$ref": "ListTranscriptsResponse" + } + }, + "get": { + "path": "v2beta/{+name}", + "httpMethod": "GET", + "description": "Gets a transcript by transcript ID.", + "flatPath": "v2beta/conferenceRecords/{conferenceRecordsId}/transcripts/{transcriptsId}", + "parameterOrder": [ + "name" + ], + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created", + "https://www.googleapis.com/auth/meetings.space.readonly" + ], + "id": "meet.conferenceRecords.transcripts.get", + "parameters": { + "name": { + "pattern": "^conferenceRecords/[^/]+/transcripts/[^/]+$", + "location": "path", + "required": true, + "description": "Required. Resource name of the transcript.", + "type": "string" + } + }, + "response": { + "$ref": "Transcript" + } + } + } + }, + "participants": { + "resources": { + "participantSessions": { + "methods": { + "get": { + "description": "Gets a participant session by participant session ID.", + "flatPath": "v2beta/conferenceRecords/{conferenceRecordsId}/participants/{participantsId}/participantSessions/{participantSessionsId}", + "id": "meet.conferenceRecords.participants.participantSessions.get", + "response": { + "$ref": "ParticipantSession" + }, + "parameterOrder": [ + "name" + ], + "path": "v2beta/{+name}", + "parameters": { + "name": { + "location": "path", + "type": "string", + "required": true, + "pattern": "^conferenceRecords/[^/]+/participants/[^/]+/participantSessions/[^/]+$", + "description": "Required. Resource name of the participant." + } + }, + "httpMethod": "GET", + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created", + "https://www.googleapis.com/auth/meetings.space.readonly" + ] + }, + "list": { + "httpMethod": "GET", + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created", + "https://www.googleapis.com/auth/meetings.space.readonly" + ], + "parameters": { + "pageSize": { + "format": "int32", + "type": "integer", + "description": "Optional. Maximum number of participant sessions to return. The service might return fewer than this value. If unspecified, at most 100 participants are returned. The maximum value is 250; values above 250 are coerced to 250. Maximum might change in the future.", + "location": "query" + }, + "pageToken": { + "description": "Optional. Page token returned from previous List Call.", + "location": "query", + "type": "string" + }, + "filter": { + "type": "string", + "description": "Optional. User specified filtering condition in [EBNF format](https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form). The following are the filterable fields: * `start_time` * `end_time` For example, `end_time IS NULL` returns active participant sessions in the conference record.", + "location": "query" + }, + "parent": { + "description": "Required. Format: `conferenceRecords/{conference_record}/participants/{participant}`", + "type": "string", + "pattern": "^conferenceRecords/[^/]+/participants/[^/]+$", + "required": true, + "location": "path" + } + }, + "path": "v2beta/{+parent}/participantSessions", + "response": { + "$ref": "ListParticipantSessionsResponse" + }, + "parameterOrder": [ + "parent" + ], + "description": "Lists the participant sessions of a participant in a conference record. By default, ordered by join time and in descending order. This API supports `fields` as standard parameters like every other API. However, when the `fields` request parameter is omitted this API defaults to `'participantsessions/*, next_page_token'`.", + "flatPath": "v2beta/conferenceRecords/{conferenceRecordsId}/participants/{participantsId}/participantSessions", + "id": "meet.conferenceRecords.participants.participantSessions.list" + } + } + } + }, + "methods": { + "get": { + "description": "Gets a participant by participant ID.", + "parameterOrder": [ + "name" + ], + "httpMethod": "GET", + "response": { + "$ref": "Participant" + }, + "parameters": { + "name": { + "location": "path", + "required": true, + "type": "string", + "pattern": "^conferenceRecords/[^/]+/participants/[^/]+$", + "description": "Required. Resource name of the participant." + } + }, + "path": "v2beta/{+name}", + "id": "meet.conferenceRecords.participants.get", + "flatPath": "v2beta/conferenceRecords/{conferenceRecordsId}/participants/{participantsId}", + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created", + "https://www.googleapis.com/auth/meetings.space.readonly" + ] + }, + "list": { + "id": "meet.conferenceRecords.participants.list", + "path": "v2beta/{+parent}/participants", + "description": "Lists the participants in a conference record. By default, ordered by join time and in descending order. This API supports `fields` as standard parameters like every other API. However, when the `fields` request parameter is omitted, this API defaults to `'participants/*, next_page_token'`.", + "response": { + "$ref": "ListParticipantsResponse" + }, + "httpMethod": "GET", + "parameters": { + "filter": { + "description": "Optional. User specified filtering condition in [EBNF format](https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form). The following are the filterable fields: * `earliest_start_time` * `latest_end_time` For example, `latest_end_time IS NULL` returns active participants in the conference.", + "location": "query", + "type": "string" + }, + "parent": { + "location": "path", + "description": "Required. Format: `conferenceRecords/{conference_record}`", + "required": true, + "type": "string", + "pattern": "^conferenceRecords/[^/]+$" + }, + "pageToken": { + "type": "string", + "location": "query", + "description": "Page token returned from previous List Call." + }, + "pageSize": { + "type": "integer", + "description": "Maximum number of participants to return. The service might return fewer than this value. If unspecified, at most 100 participants are returned. The maximum value is 250; values above 250 are coerced to 250. Maximum might change in the future.", + "format": "int32", + "location": "query" + } + }, + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created", + "https://www.googleapis.com/auth/meetings.space.readonly" + ], + "flatPath": "v2beta/conferenceRecords/{conferenceRecordsId}/participants", + "parameterOrder": [ + "parent" + ] + } + } + }, + "recordings": { + "methods": { + "get": { + "parameters": { + "name": { + "description": "Required. Resource name of the recording.", + "pattern": "^conferenceRecords/[^/]+/recordings/[^/]+$", + "location": "path", + "required": true, + "type": "string" + } + }, + "path": "v2beta/{+name}", + "flatPath": "v2beta/conferenceRecords/{conferenceRecordsId}/recordings/{recordingsId}", + "id": "meet.conferenceRecords.recordings.get", + "description": "Gets a recording by recording ID.", + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created", + "https://www.googleapis.com/auth/meetings.space.readonly" + ], + "response": { + "$ref": "Recording" + }, + "parameterOrder": [ + "name" + ], + "httpMethod": "GET" + }, + "list": { + "description": "Lists the recording resources from the conference record. By default, ordered by start time and in ascending order.", + "path": "v2beta/{+parent}/recordings", + "parameters": { + "parent": { + "pattern": "^conferenceRecords/[^/]+$", + "location": "path", + "type": "string", + "required": true, + "description": "Required. Format: `conferenceRecords/{conference_record}`" + }, + "pageSize": { + "type": "integer", + "format": "int32", + "location": "query", + "description": "Maximum number of recordings to return. The service might return fewer than this value. If unspecified, at most 10 recordings are returned. The maximum value is 100; values above 100 are coerced to 100. Maximum might change in the future." + }, + "pageToken": { + "location": "query", + "type": "string", + "description": "Page token returned from previous List Call." + } + }, + "id": "meet.conferenceRecords.recordings.list", + "flatPath": "v2beta/conferenceRecords/{conferenceRecordsId}/recordings", + "httpMethod": "GET", + "response": { + "$ref": "ListRecordingsResponse" + }, + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created", + "https://www.googleapis.com/auth/meetings.space.readonly" + ], + "parameterOrder": [ + "parent" + ] + } + } + } + } + }, + "spaces": { + "methods": { + "create": { + "response": { + "$ref": "Space" + }, + "description": "Creates a space.", + "parameterOrder": [], + "request": { + "$ref": "Space" + }, + "httpMethod": "POST", + "path": "v2beta/spaces", + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created" + ], + "parameters": {}, + "id": "meet.spaces.create", + "flatPath": "v2beta/spaces" + }, + "get": { + "parameters": { + "name": { + "required": true, + "type": "string", + "location": "path", + "description": "Required. Resource name of the space. Format: `spaces/{space}` or `spaces/{meetingCode}`. `{space}` is the resource identifier for the space. It's a unique, server-generated ID and is case sensitive. For example, `jQCFfuBOdN5z`. `{meetingCode}` is an alias for the space. It's a typeable, unique character string and is non-case sensitive. For example, `abc-mnop-xyz`. The maximum length is 128 characters. A `meetingCode` shouldn't be stored long term as it can become dissociated from a meeting space and can be reused for different meeting spaces in the future. Generally, a `meetingCode` expires 365 days after last use. For more information, see [Learn about meeting codes in Google Meet](https://support.google.com/meet/answer/10710509). For more information, see [How Meet identifies a meeting space](https://developers.google.com/meet/api/guides/meeting-spaces#identify-meeting-space).", + "pattern": "^spaces/[^/]+$" + } + }, + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created", + "https://www.googleapis.com/auth/meetings.space.readonly", + "https://www.googleapis.com/auth/meetings.space.settings" + ], + "id": "meet.spaces.get", + "httpMethod": "GET", + "path": "v2beta/{+name}", + "parameterOrder": [ + "name" + ], + "response": { + "$ref": "Space" + }, + "description": "Gets details about a meeting space. For an example, see [Get a meeting space](https://developers.google.com/meet/api/guides/meeting-spaces#get-meeting-space).", + "flatPath": "v2beta/spaces/{spacesId}" + }, + "endActiveConference": { + "description": "Ends an active conference (if there's one). For an example, see [End active conference](https://developers.google.com/meet/api/guides/meeting-spaces#end-active-conference).", + "flatPath": "v2beta/spaces/{spacesId}:endActiveConference", + "parameters": { + "name": { + "description": "Required. Resource name of the space. Format: `spaces/{space}`. `{space}` is the resource identifier for the space. It's a unique, server-generated ID and is case sensitive. For example, `jQCFfuBOdN5z`. For more information, see [How Meet identifies a meeting space](https://developers.google.com/meet/api/guides/meeting-spaces#identify-meeting-space).", + "type": "string", + "location": "path", + "pattern": "^spaces/[^/]+$", + "required": true + } + }, + "request": { + "$ref": "EndActiveConferenceRequest" + }, + "response": { + "$ref": "Empty" + }, + "parameterOrder": [ + "name" + ], + "id": "meet.spaces.endActiveConference", + "httpMethod": "POST", + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created" + ], + "path": "v2beta/{+name}:endActiveConference" + }, + "patch": { + "path": "v2beta/{+name}", + "description": "Updates details about a meeting space. For an example, see [Update a meeting space](https://developers.google.com/meet/api/guides/meeting-spaces#update-meeting-space).", + "httpMethod": "PATCH", + "parameterOrder": [ + "name" + ], + "parameters": { + "name": { + "description": "Immutable. Resource name of the space. Format: `spaces/{space}`. `{space}` is the resource identifier for the space. It's a unique, server-generated ID and is case sensitive. For example, `jQCFfuBOdN5z`. For more information, see [How Meet identifies a meeting space](https://developers.google.com/meet/api/guides/meeting-spaces#identify-meeting-space).", + "pattern": "^spaces/[^/]+$", + "location": "path", + "type": "string", + "required": true + }, + "updateMask": { + "format": "google-fieldmask", + "location": "query", + "type": "string", + "description": "Optional. Field mask used to specify the fields to be updated in the space. If update_mask isn't provided(not set, set with empty paths, or only has \"\" as paths), it defaults to update all fields provided with values in the request. Using \"*\" as update_mask will update all fields, including deleting fields not set in the request." + } + }, + "scopes": [ + "https://www.googleapis.com/auth/meetings.space.created", + "https://www.googleapis.com/auth/meetings.space.settings" + ], + "request": { + "$ref": "Space" + }, + "id": "meet.spaces.patch", + "flatPath": "v2beta/spaces/{spacesId}", + "response": { + "$ref": "Space" + } + } + } + } + }, + "version_module": true, + "parameters": { + "fields": { + "description": "Selector specifying which fields to include in a partial response.", + "location": "query", + "type": "string" + }, + "$.xgafv": { + "location": "query", + "type": "string", + "description": "V1 error format.", + "enumDescriptions": [ + "v1 error format", + "v2 error format" + ], + "enum": [ + "1", + "2" + ] + }, + "alt": { + "type": "string", + "enum": [ + "json", + "media", + "proto" + ], + "enumDescriptions": [ + "Responses with Content-Type of application/json", + "Media download with context-dependent Content-Type", + "Responses with Content-Type of application/x-protobuf" + ], + "location": "query", + "default": "json", + "description": "Data format for response." + }, + "access_token": { + "location": "query", + "type": "string", + "description": "OAuth access token." + }, + "upload_protocol": { + "type": "string", + "description": "Upload protocol for media (e.g. \"raw\", \"multipart\").", + "location": "query" + }, + "uploadType": { + "description": "Legacy upload protocol for media (e.g. \"media\", \"multipart\").", + "location": "query", + "type": "string" + }, + "quotaUser": { + "location": "query", + "type": "string", + "description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters." + }, + "oauth_token": { + "location": "query", + "description": "OAuth 2.0 token for the current user.", + "type": "string" + }, + "callback": { + "location": "query", + "description": "JSONP", + "type": "string" + }, + "prettyPrint": { + "description": "Returns response with indentations and line breaks.", + "type": "boolean", + "location": "query", + "default": "true" + }, + "key": { + "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", + "type": "string", + "location": "query" + } + }, + "ownerDomain": "google.com", + "auth": { + "oauth2": { + "scopes": { + "https://www.googleapis.com/auth/meetings.space.readonly": { + "description": "Read information about any of your Google Meet conferences" + }, + "https://www.googleapis.com/auth/meetings.space.created": { + "description": "Create, edit, and see information about your Google Meet conferences created by the app." + }, + "https://www.googleapis.com/auth/meetings.space.settings": { + "description": "Edit, and see settings for all of your Google Meet calls." + } + } + } + }, + "name": "meet", + "basePath": "" +} From 96acd40692992509fc29ef9c08594319256cfd2d Mon Sep 17 00:00:00 2001 From: Ross Scroggs Date: Tue, 18 Feb 2025 12:23:11 -0800 Subject: [PATCH 05/14] Increment scratch_counter to flush out bug --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 98672d3a..7bc60d2c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ defaults: working-directory: src env: - SCRATCH_COUNTER: 9 + SCRATCH_COUNTER: 10 OPENSSL_CONFIG_OPTS: no-fips --api=3.0.0 OPENSSL_INSTALL_PATH: ${{ github.workspace }}/bin/ssl OPENSSL_SOURCE_PATH: ${{ github.workspace }}/src/openssl From 03148a6ae8cd84a53ec1433c05c7ef8e416408d8 Mon Sep 17 00:00:00 2001 From: Ross Scroggs Date: Tue, 18 Feb 2025 14:25:35 -0800 Subject: [PATCH 06/14] Added initial support for Meet API v2beta --- src/gam/__init__.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/gam/__init__.py b/src/gam/__init__.py index 82d82a76..1a463948 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -4428,7 +4428,7 @@ class signjwtSignJwt(google.auth.crypt.Signer): except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError) as e: systemErrorExit(API_ACCESS_DENIED_RC, str(e)) httpObj = transportAuthorizedHttp(credentials, http=getHttpObj(override_min_tls='TLSv1_2')) - iamc, _ = getService(API.IAM_CREDENTIALS, httpObj) + iamc = getService(API.IAM_CREDENTIALS, httpObj) response = callGAPI(iamc.projects().serviceAccounts(), 'signJwt', name=self.name, body={'payload': json.dumps(message)}) signed_jwt = response.get('signedJwt') @@ -4717,7 +4717,7 @@ def getService(api, httpObj): service = googleapiclient.discovery.build_from_document(GM.Globals[GM.CURRENT_API_SERVICES][api][version], http=httpObj) if GM.Globals[GM.CACHE_DISCOVERY_ONLY]: clearServiceCache(service) - return (service, api) + return service if not hasLocalJSON: triesLimit = 3 for n in range(1, triesLimit+1): @@ -4731,7 +4731,7 @@ def getService(api, httpObj): setattr(service, '_baseUrl', getattr(service, '_baseUrl').replace('/v3/', '/v3beta/')) if GM.Globals[GM.CACHE_DISCOVERY_ONLY]: clearServiceCache(service) - return (service, api) + return service except googleapiclient.errors.UnknownApiNameOrVersion as e: systemErrorExit(GOOGLE_API_ERROR_RC, Msg.UNKNOWN_API_OR_VERSION.format(str(e), __author__)) except (googleapiclient.errors.InvalidJsonError, KeyError, ValueError) as e: @@ -4758,7 +4758,7 @@ def getService(api, httpObj): GM.Globals[GM.CURRENT_API_SERVICES][api][version] = service._rootDesc.copy() if GM.Globals[GM.CACHE_DISCOVERY_ONLY]: clearServiceCache(service) - return (service, api) + return service except (googleapiclient.errors.InvalidJsonError, KeyError, ValueError) as e: invalidDiscoveryJsonExit(disc_file, str(e)) except IOError as e: @@ -5541,7 +5541,7 @@ def buildGAPIObject(api, credentials=None): if credentials is None: credentials = getClientCredentials(api=api, refreshOnly=True) httpObj = transportAuthorizedHttp(credentials, http=getHttpObj(cache=GM.Globals[GM.CACHE_DIR])) - service, api = getService(api, httpObj) + service = getService(api, httpObj) if not GC.Values[GC.ENABLE_DASA]: try: API_Scopes = set(list(service._rootDesc['auth']['oauth2']['scopes'])) @@ -5570,7 +5570,9 @@ def getSaUser(user): def buildGAPIServiceObject(api, user, i=0, count=0, displayError=True): userEmail = getSaUser(user) httpObj = getHttpObj(cache=GM.Globals[GM.CACHE_DIR]) - service, api = getService(api, httpObj) + service = getService(api, httpObj) + if api == API.MEET_BETA: + api = API.MEET credentials = getSvcAcctCredentials(api, userEmail) request = transportCreateRequest(httpObj) triesLimit = 3 @@ -5607,7 +5609,7 @@ def buildGAPIServiceObject(api, user, i=0, count=0, displayError=True): def buildGAPIObjectNoAuthentication(api): httpObj = getHttpObj(cache=GM.Globals[GM.CACHE_DIR]) - service, _ = getService(api, httpObj) + service = getService(api, httpObj) return service def initGDataObject(gdataObj, api): From 80933755c41565a681ccea9804c69ba0b45fa8e9 Mon Sep 17 00:00:00 2001 From: Ross Scroggs Date: Wed, 19 Feb 2025 18:59:37 -0800 Subject: [PATCH 07/14] Enabled support for Limited Access --- src/GamCommands.txt | 15 ++++- src/GamUpdate.txt | 29 +++++++++- src/gam/__init__.py | 136 +++++++++++++++++++++++++++++++------------- 3 files changed, 135 insertions(+), 45 deletions(-) diff --git a/src/GamCommands.txt b/src/GamCommands.txt index 408bf993..1e01484f 100644 --- a/src/GamCommands.txt +++ b/src/GamCommands.txt @@ -3846,6 +3846,7 @@ gam info group|groups [formatjson] gam print groups [todrive *] [([domain|domains ] ([member|showownedby ]|[(query )|(queries )]))| + (group|group_ns|group_susp )| (select )] [emailmatchpattern [not] ] [namematchpattern [not] ] [descriptionmatchpattern [not] ] (matchsetting [not] )* @@ -4963,6 +4964,7 @@ gam create|add permissions * [] gam delete permissions * [] + [enforceexpansiveaccess []] In these commands, you specify an administrator and then indicate that you want domain administrator access with the adminaccess option. @@ -4976,9 +4978,11 @@ gam create|add drivefileacl adminaccess gam update drivefileacl (role ) [expires|expiration