Initial Support for Google Chat API

This commit is contained in:
Jay Lee
2021-05-24 16:37:39 -04:00
parent e998c78609
commit 0e7472de50
4 changed files with 221 additions and 17 deletions

View File

@ -347,7 +347,7 @@ jobs:
uses: actions/upload-artifact@v2
if: github.event_name == 'push' && matrix.goal != 'test'
with:
name: gam-binaries.zip
name: gam-binaries
path: |
src/*.tar.xz
src/*.zip

View File

@ -38,6 +38,7 @@ import googleapiclient.errors
import googleapiclient.http
import google.oauth2.service_account
import httplib2
from google.auth.jwt import Credentials as JWTCredentials
from cryptography import x509
from cryptography.hazmat.backends import default_backend
@ -53,6 +54,7 @@ from gam import fileutils
from gam.gapi import calendar as gapi_calendar
from gam.gapi import cloudidentity as gapi_cloudidentity
from gam.gapi import cbcm as gapi_cbcm
from gam.gapi import chat as gapi_chat
from gam.gapi import chromehistory as gapi_chromehistory
from gam.gapi import chromemanagement as gapi_chromemanagement
from gam.gapi import chromepolicy as gapi_chromepolicy
@ -822,21 +824,32 @@ def _getSvcAcctData():
controlflow.system_error_exit(6, None)
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA] = json.loads(json_string)
def getSvcAcctCredentials(scopes, act_as):
jwt_apis = ['chat'] # APIs which can handle OAuthless JWT tokens
def getSvcAcctCredentials(scopes, act_as, api=None):
try:
_getSvcAcctData()
sign_method = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
if sign_method == 'default':
credentials = google.oauth2.service_account.Credentials.from_service_account_info(
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
elif sign_method == 'yubikey':
yksigner = yubikey.YubiKey(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = google.oauth2.service_account.Credentials._from_signer_and_info(yksigner,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = credentials.with_scopes(scopes)
if act_as:
credentials = credentials.with_subject(act_as)
if act_as or api not in jwt_apis:
if sign_method == 'default':
credentials = google.oauth2.service_account.Credentials.from_service_account_info(
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
elif sign_method == 'yubikey':
yksigner = yubikey.YubiKey(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = google.oauth2.service_account.Credentials._from_signer_and_info(yksigner,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = credentials.with_scopes(scopes)
if act_as:
credentials = credentials.with_subject(act_as)
else:
audience = f'https://{api}.googleapis.com/'
if sign_method == 'default':
return JWTCredentials.from_service_account_info(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA],
audience=audience)
elif sign_method == 'yubikey':
yksigner = yubikey.YubiKey(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = JWTCredentials._from_signer_and_info(yksigner,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA],
audience=audience)
GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = GM_Globals[
GM_OAUTH2SERVICE_JSON_DATA]['client_id']
return credentials
@ -1087,14 +1100,17 @@ def convertEmailAddressToUID(emailAddressOrUID, cd=None, email_type='user'):
return normalizedEmailAddressOrUID
def buildGAPIServiceObject(api, act_as, showAuthError=True):
def buildGAPIServiceObject(api, act_as, showAuthError=True, scopes=None):
httpObj = transport.create_http(cache=GM_Globals[GM_CACHE_DIR])
service = getService(api, httpObj)
GM_Globals[GM_CURRENT_API_USER] = act_as
GM_Globals[GM_CURRENT_API_SCOPES] = API_SCOPE_MAPPING.get(
api, service._rootDesc['auth']['oauth2']['scopes'])
if scopes:
GM_Globals[GM_CURRENT_API_SCOPES] = scopes
else:
GM_Globals[GM_CURRENT_API_SCOPES] = API_SCOPE_MAPPING.get(
api, service._rootDesc['auth']['oauth2']['scopes'])
credentials = getSvcAcctCredentials(GM_Globals[GM_CURRENT_API_SCOPES],
act_as)
act_as, api)
request = transport.create_request(httpObj)
retries = 3
for n in range(1, retries + 1):
@ -11342,6 +11358,8 @@ def ProcessGAMCommand(args):
gapi_cbcm.createtoken()
elif argument in ['printer']:
gapi_directory_printers.create()
elif argument in ['chatmessage']:
gapi_chat.create_message()
else:
controlflow.invalid_argument_exit(argument, 'gam create')
sys.exit(0)
@ -11404,6 +11422,8 @@ def ProcessGAMCommand(args):
gapi_chromepolicy.update_policy()
elif argument in ['printer']:
gapi_directory_printers.update()
elif argument in ['chatmessage']:
gapi_chat.update_message()
else:
controlflow.invalid_argument_exit(argument, 'gam update')
sys.exit(0)
@ -11540,6 +11560,8 @@ def ProcessGAMCommand(args):
gapi_directory_printers.delete()
elif argument == 'chromepolicy':
gapi_chromepolicy.delete_policy()
elif argument == 'chatmessage':
gapi_chat.delete_message()
else:
controlflow.invalid_argument_exit(argument, 'gam delete')
sys.exit(0)
@ -11655,6 +11677,10 @@ def ProcessGAMCommand(args):
gapi_chromemanagement.printVersions()
elif argument in ['chromehistory']:
gapi_chromehistory.printHistory()
elif argument in ['chatspaces']:
gapi_chat.print_spaces()
elif argument in ['chatmembers']:
gapi_chat.print_members()
else:
controlflow.invalid_argument_exit(argument, 'gam print')
sys.exit(0)

177
src/gam/gapi/chat.py Normal file
View File

@ -0,0 +1,177 @@
import sys
import googleapiclient.errors
import gam
from gam.var import *
from gam import controlflow
from gam import display
from gam import fileutils
from gam import gapi
from gam import utils
from gam.gapi import errors as gapi_errors
# Chat scope isn't in discovery doc so need to manually set
CHAT_SCOPES = ['https://www.googleapis.com/auth/chat.bot']
def build():
return gam.buildGAPIServiceObject('chat',
act_as=None,
scopes=CHAT_SCOPES)
THROW_REASONS = [
gapi_errors.ErrorReason.FOUR_O_FOUR, # Chat API not configured
gapi_errors.ErrorReason.FOUR_O_THREE, # Bot not added to room
]
def _chat_error_handler(chat, err):
if err.status_code == 404:
project_id = chat._http.credentials.project_id
url = f'https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat?project={project_id}'
print('ERROR: you need to configure Google Chat for your API project. Please go to:')
print()
print(url)
print()
print('and complete all fields.')
elif err.status_code == 403:
print('ERROR: no access to that Chat space or message. Make sure your bot created the mesage and the user has chatted the bot first or added it to the Chat room')
sys.exit(1)
def print_spaces():
chat = build()
try:
spaces = gapi.get_all_pages(chat.spaces(), 'list', 'spaces', throw_reasons=THROW_REASONS)
except googleapiclient.errors.HttpError as err:
_chat_error_handler(chat, err)
if not spaces:
print('Bot not added to any Chat rooms or users yet.')
else:
display.write_csv_file(spaces, spaces[0].keys(), 'Chat Spaces', False)
def print_members():
chat = build()
space = None
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'space':
space = sys.argv[i+1]
if space[:7] != 'spaces/':
space = f'spaces/{space}'
i += 2
else:
controlflow.invalid_argument_exit(myarg, "gam print chatmembers")
try:
results = gapi.get_all_pages(chat.spaces().members(), 'list', 'memberships', parent=space)
except googleapiclient.errors.HttpError as err:
_chat_error_handler(chat, err)
members = []
titles = []
for result in results:
member = utils.flatten_json(result)
for key in member:
if key not in titles:
titles.append(key)
members.append(utils.flatten_json(result))
display.write_csv_file(members, titles, 'Chat Members', False)
def create_message():
chat = build()
body = {}
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'text':
body['text'] = sys.argv[i+1]
i += 2
elif myarg == 'textfile':
filename = sys.argv[i + 1]
i, encoding = gam.getCharSet(i + 2)
body['text'] = fileutils.read_file(filename, encoding=encoding)
elif myarg == 'space':
space = sys.argv[i+1]
if space[:7] != 'spaces/':
space = f'spaces/{space}'
i += 2
elif myarg == 'thread':
body['thread'] = {'name': sys.argv[i+1]}
i += 2
else:
controlflow.invalid_argument_exit(myarg, "gam create chat")
if len(body['text']) > 4096:
body['text'] = body['text'][:4095]
print('WARNING: trimmed message longer than 4k to be 4k in length.')
try:
resp = gapi.call(chat.spaces().messages(),
'create',
parent=space,
body=body,
throw_reasons=THROW_REASONS)
except googleapiclient.errors.HttpError as err:
_chat_error_handler(chat, err)
if 'thread' in body:
print(f'responded to thread {resp["thread"]["name"]}')
else:
print(f'started new thread {resp["thread"]["name"]}')
print(f'message {resp["name"]}')
def delete_message():
chat = build()
name = None
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'name':
name = sys.argv[i+1]
i += 2
else:
controlflow.invalid_argument_exit(myarg, "gam delete chat")
try:
gapi.call(chat.spaces().messages(),
'delete',
name=name)
except googleapiclient.errors.HttpError as err:
_chat_error_handler(chat, err)
def update_message():
chat = build()
body = {}
name = None
updateMask = 'text'
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'text':
body['text'] = sys.argv[i+1]
i += 2
elif myarg == 'textfile':
filename = sys.argv[i + 1]
i, encoding = gam.getCharSet(i + 2)
body['text'] = fileutils.read_file(filename, encoding=encoding)
elif myarg == 'name':
name = sys.argv[i+1]
i += 2
else:
controlflow.invalid_argument_exit(myarg, "gam update chat")
if len(body['text']) > 4096:
body['text'] = body['text'][:4095]
print('WARNING: trimmed message longer than 4k to be 4k in length.')
try:
resp = gapi.call(chat.spaces().messages(),
'update',
name=name,
updateMask=updateMask,
body=body)
except googleapiclient.errors.HttpError as err:
_chat_error_handler(chat, err)
if 'thread' in body:
print(f'updated response to thread {resp["thread"]["name"]}')
else:
print(f'updated message on thread {resp["thread"]["name"]}')
print(f'message {resp["name"]}')

View File

@ -119,6 +119,7 @@ class ErrorReason(Enum):
FIVE_O_THREE = '503'
FOUR_O_NINE = '409'
FOUR_O_O = '400'
FOUR_O_FOUR = '404'
FOUR_O_THREE = '403'
FOUR_TWO_NINE = '429'
GATEWAY_TIMEOUT = 'gatewayTimeout'