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 uses: actions/upload-artifact@v2
if: github.event_name == 'push' && matrix.goal != 'test' if: github.event_name == 'push' && matrix.goal != 'test'
with: with:
name: gam-binaries.zip name: gam-binaries
path: | path: |
src/*.tar.xz src/*.tar.xz
src/*.zip src/*.zip

View File

@ -38,6 +38,7 @@ import googleapiclient.errors
import googleapiclient.http import googleapiclient.http
import google.oauth2.service_account import google.oauth2.service_account
import httplib2 import httplib2
from google.auth.jwt import Credentials as JWTCredentials
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend 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 calendar as gapi_calendar
from gam.gapi import cloudidentity as gapi_cloudidentity from gam.gapi import cloudidentity as gapi_cloudidentity
from gam.gapi import cbcm as gapi_cbcm 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 chromehistory as gapi_chromehistory
from gam.gapi import chromemanagement as gapi_chromemanagement from gam.gapi import chromemanagement as gapi_chromemanagement
from gam.gapi import chromepolicy as gapi_chromepolicy from gam.gapi import chromepolicy as gapi_chromepolicy
@ -822,11 +824,12 @@ def _getSvcAcctData():
controlflow.system_error_exit(6, None) controlflow.system_error_exit(6, None)
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA] = json.loads(json_string) GM_Globals[GM_OAUTH2SERVICE_JSON_DATA] = json.loads(json_string)
jwt_apis = ['chat'] # APIs which can handle OAuthless JWT tokens
def getSvcAcctCredentials(scopes, act_as): def getSvcAcctCredentials(scopes, act_as, api=None):
try: try:
_getSvcAcctData() _getSvcAcctData()
sign_method = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA].get('key_type', 'default') sign_method = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
if act_as or api not in jwt_apis:
if sign_method == 'default': if sign_method == 'default':
credentials = google.oauth2.service_account.Credentials.from_service_account_info( credentials = google.oauth2.service_account.Credentials.from_service_account_info(
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]) GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
@ -837,6 +840,16 @@ def getSvcAcctCredentials(scopes, act_as):
credentials = credentials.with_scopes(scopes) credentials = credentials.with_scopes(scopes)
if act_as: if act_as:
credentials = credentials.with_subject(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_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = GM_Globals[
GM_OAUTH2SERVICE_JSON_DATA]['client_id'] GM_OAUTH2SERVICE_JSON_DATA]['client_id']
return credentials return credentials
@ -1087,14 +1100,17 @@ def convertEmailAddressToUID(emailAddressOrUID, cd=None, email_type='user'):
return normalizedEmailAddressOrUID 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]) httpObj = transport.create_http(cache=GM_Globals[GM_CACHE_DIR])
service = getService(api, httpObj) service = getService(api, httpObj)
GM_Globals[GM_CURRENT_API_USER] = act_as GM_Globals[GM_CURRENT_API_USER] = act_as
if scopes:
GM_Globals[GM_CURRENT_API_SCOPES] = scopes
else:
GM_Globals[GM_CURRENT_API_SCOPES] = API_SCOPE_MAPPING.get( GM_Globals[GM_CURRENT_API_SCOPES] = API_SCOPE_MAPPING.get(
api, service._rootDesc['auth']['oauth2']['scopes']) api, service._rootDesc['auth']['oauth2']['scopes'])
credentials = getSvcAcctCredentials(GM_Globals[GM_CURRENT_API_SCOPES], credentials = getSvcAcctCredentials(GM_Globals[GM_CURRENT_API_SCOPES],
act_as) act_as, api)
request = transport.create_request(httpObj) request = transport.create_request(httpObj)
retries = 3 retries = 3
for n in range(1, retries + 1): for n in range(1, retries + 1):
@ -11342,6 +11358,8 @@ def ProcessGAMCommand(args):
gapi_cbcm.createtoken() gapi_cbcm.createtoken()
elif argument in ['printer']: elif argument in ['printer']:
gapi_directory_printers.create() gapi_directory_printers.create()
elif argument in ['chatmessage']:
gapi_chat.create_message()
else: else:
controlflow.invalid_argument_exit(argument, 'gam create') controlflow.invalid_argument_exit(argument, 'gam create')
sys.exit(0) sys.exit(0)
@ -11404,6 +11422,8 @@ def ProcessGAMCommand(args):
gapi_chromepolicy.update_policy() gapi_chromepolicy.update_policy()
elif argument in ['printer']: elif argument in ['printer']:
gapi_directory_printers.update() gapi_directory_printers.update()
elif argument in ['chatmessage']:
gapi_chat.update_message()
else: else:
controlflow.invalid_argument_exit(argument, 'gam update') controlflow.invalid_argument_exit(argument, 'gam update')
sys.exit(0) sys.exit(0)
@ -11540,6 +11560,8 @@ def ProcessGAMCommand(args):
gapi_directory_printers.delete() gapi_directory_printers.delete()
elif argument == 'chromepolicy': elif argument == 'chromepolicy':
gapi_chromepolicy.delete_policy() gapi_chromepolicy.delete_policy()
elif argument == 'chatmessage':
gapi_chat.delete_message()
else: else:
controlflow.invalid_argument_exit(argument, 'gam delete') controlflow.invalid_argument_exit(argument, 'gam delete')
sys.exit(0) sys.exit(0)
@ -11655,6 +11677,10 @@ def ProcessGAMCommand(args):
gapi_chromemanagement.printVersions() gapi_chromemanagement.printVersions()
elif argument in ['chromehistory']: elif argument in ['chromehistory']:
gapi_chromehistory.printHistory() gapi_chromehistory.printHistory()
elif argument in ['chatspaces']:
gapi_chat.print_spaces()
elif argument in ['chatmembers']:
gapi_chat.print_members()
else: else:
controlflow.invalid_argument_exit(argument, 'gam print') controlflow.invalid_argument_exit(argument, 'gam print')
sys.exit(0) 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' FIVE_O_THREE = '503'
FOUR_O_NINE = '409' FOUR_O_NINE = '409'
FOUR_O_O = '400' FOUR_O_O = '400'
FOUR_O_FOUR = '404'
FOUR_O_THREE = '403' FOUR_O_THREE = '403'
FOUR_TWO_NINE = '429' FOUR_TWO_NINE = '429'
GATEWAY_TIMEOUT = 'gatewayTimeout' GATEWAY_TIMEOUT = 'gatewayTimeout'