mirror of
https://github.com/GAM-team/GAM.git
synced 2025-07-09 14:13:35 +00:00
Initial Support for Google Chat API
This commit is contained in:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -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
|
||||||
|
@ -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
177
src/gam/gapi/chat.py
Normal 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"]}')
|
@ -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'
|
||||||
|
Reference in New Issue
Block a user