diff --git a/src/GamCommands.txt b/src/GamCommands.txt index 9f0e19f7..37ec9771 100644 --- a/src/GamCommands.txt +++ b/src/GamCommands.txt @@ -1,4 +1,4 @@ -This document describes the GAM command line syntax in modified BNF, see https://en.wikipedia.org/wiki/Backus-Naur_Form +his document describes the GAM command line syntax in modified BNF, see https://en.wikipedia.org/wiki/Backus-Naur_Form Skip the History section and start reading at Introduction. Items on the command line are space separated, when an actual space character is required, it will be indicated by . @@ -380,6 +380,8 @@ If an item contains spaces, it should be surrounded by ". domain:|domain|default ::= ::= + ::= :: + ::= emojiname | customemojis/ ::= spaces//members/ ::= spaces//messages/ ::= spaces/ | space | space spaces/ @@ -664,6 +666,7 @@ If an item contains spaces, it should be surrounded by ". (gdoc|ghtml )| (gcsdoc|gcshtml ) ::= + ## Lists of basic items ::= "(,)*" @@ -6436,6 +6439,22 @@ gam print chatevents [todrive *] filter [formatjson [quotechar ]] + ::= :[0-9a-z_-]+: + ::= emojiname | customemojis/ + +gam create chatemoji + ([drivedir|(sourcefolder )] [filename ]) + [formatjson] +gam delete chatemoji +gam info chatemoji + [formatjson] +gam show chatemojis + [showcreatedby any|me|others] + [formatjson] +gam print chatemojis [todrive *] + [showcreatedby any|me|others] + [formatjson [quotechar ]] + # Users - Drive ::= diff --git a/src/GamUpdate.txt b/src/GamUpdate.txt index 60d55463..28b85ab4 100644 --- a/src/GamUpdate.txt +++ b/src/GamUpdate.txt @@ -1,3 +1,10 @@ +7.10.00 + +Added commands to manage/display Chat Custom Emojis. + +* See: https://github.com/GAM-team/GAM/wiki/Users-Chat#manage-chat-emojis +* See: https://github.com/GAM-team/GAM/wiki/Users-Chat#display-chat-emojis + 7.09.07 Added `webviewlink` to `` for use in `gam print|show filetree`. diff --git a/src/gam/__init__.py b/src/gam/__init__.py index d7ac6957..b8bf7519 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.09.07' +__version__ = '7.10.00' __license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)' #pylint: disable=wrong-import-position @@ -22786,7 +22786,7 @@ def _processPeopleContactPhotos(users, function): if function == 'GetContactPhoto' and not os.path.isdir(targetFolder): os.makedirs(targetFolder) elif myarg == 'filename': - filenamePattern = getString(Cmd.OB_PHOTO_FILENAME_PATTERN) + filenamePattern = getString(Cmd.OB_FILE_NAME_PATTERN) else: unknownArgumentExit() subForContactId = filenamePattern.find('#contactid#') != -1 @@ -25990,6 +25990,8 @@ def _printChatItem(user, citem, parent, entityType, csvPF, FJQC, addCSVData=None if entityType == Ent.CHAT_SPACE: _cleanChatSpace(citem) baserow = {'User': user} if user is not None else {} + elif entityType == Ent.CHAT_EMOJI: + baserow = {'User': user} else: if user is not None: baserow = {'User': user, 'space.name': parent['name'], 'space.displayName': parent['displayName']} @@ -26009,6 +26011,190 @@ def _printChatItem(user, citem, parent, entityType, csvPF, FJQC, addCSVData=None 'JSON': json.dumps(cleanJSON(citem, timeObjects=CHAT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)}) csvPF.WriteRowNoFilter(row) +def _getValidateEmojiName(): + name = getString(Cmd.OB_CHAT_EMOJI_NAME) + if re.match(r'^:[0-9a-z_-]+:$', name): + return name + Cmd.Backup() + usageErrorExit(Msg.INVALID_EMOJI_NAME.format(name)) + +# gam create chatemoji +# ([drivedir|(sourcefolder )] [filename ]) +# [formatjson] +def createChatEmoji(users): + FJQC = FormatJSONQuoteChar() + name = _getValidateEmojiName() + body = {'emojiName': name, 'payload': {'filename': '', 'fileContent': ''}} + sourceFolder = os.getcwd() + filenamePattern = '#email#.jpg' + while Cmd.ArgumentsRemaining(): + myarg = getArgument() + if myarg == 'drivedir': + sourceFolder = GC.Values[GC.DRIVE_DIR] + elif myarg == 'sourcefolder': + sourceFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH)) + if not os.path.isdir(sourceFolder): + entityDoesNotExistExit(Ent.DIRECTORY, sourceFolder) + elif myarg == 'filename': + filenamePattern = getString(Cmd.OB_FILE_NAME_PATTERN) + else: + FJQC.GetFormatJSON(myarg) + i, count, users = getEntityArgument(users) + for user in users: + i += 1 + user, chat, kvList = buildChatServiceObject(API.CHAT_CUSTOM_EMOJIS, user, i, count, [Ent.CHAT_EMOJI, name]) + if not chat: + continue + user, userName, _ = splitEmailAddressOrUID(user) + filename = _substituteForUser(filenamePattern, user, userName) + if sourceFolder is not None: + filename = os.path.join(sourceFolder, filename) + filename = os.path.expanduser(filename) + try: + with open(filename, 'rb') as f: + image_data = f.read() + except (OSError, IOError) as e: + entityActionFailedWarning([Ent.USER, user, Ent.CHAT_EMOJI, filename], str(e), i, count) + continue + body['payload'] = {'filename': os.path.basename(filename), + 'fileContent':base64.urlsafe_b64encode(image_data).decode(UTF8)} + try: + emoji = callGAPI(chat.customEmojis(), 'create', + throwReasons=[GAPI.ALREADY_EXISTS, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION], + body=body) + _showChatItem(emoji, Ent.CHAT_EMOJI, FJQC, i, count) + except (GAPI.alreadyExists, GAPI.invalidArgument, GAPI.permissionDenied) as e: + exitIfChatNotConfigured(chat, kvList, str(e), i, count) + except GAPI.failedPrecondition: + userChatServiceNotEnabledWarning(user, i, count) + +def getEmojiName(myarg): + if myarg == 'emojiname': + name = _getValidateEmojiName() + return 'customEmojis/'+name + _, chatEmoji = Cmd.Previous().split('/', 1) + return 'customEmojis/'+chatEmoji + +# gam delete chatemoji +def deleteChatEmoji(users): + name = None + while Cmd.ArgumentsRemaining(): + myarg = getArgument() + if myarg == 'emojiname': + name = getEmojiName(myarg) + elif myarg.startswith('customemojis/'): + name = getEmojiName(myarg) + else: + unknownArgumentExit() + if not name: + missingArgumentExit('ChatEmoji') + i, count, users = getEntityArgument(users) + for user in users: + i += 1 + user, chat, kvList = buildChatServiceObject(API.CHAT_CUSTOM_EMOJIS, user, i, count, [Ent.CHAT_EMOJI, name]) + if not chat: + continue + try: + callGAPI(chat.customEmojis(), 'delete', + throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION], + name=name) + entityActionPerformed(kvList, i, count) + except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e: + exitIfChatNotConfigured(chat, kvList, str(e), i, count) + except GAPI.failedPrecondition: + userChatServiceNotEnabledWarning(user, i, count) + +# gam info chatemoji +# [formatjson] +def infoChatEmoji(users): + FJQC = FormatJSONQuoteChar() + name = None + while Cmd.ArgumentsRemaining(): + myarg = getArgument() + if myarg == 'emojiname': + name = getEmojiName(myarg) + elif myarg.startswith('customemojis/'): + name = getEmojiName(myarg) + else: + FJQC.GetFormatJSON(myarg) + if not name: + missingArgumentExit('ChatEmoji') + i, count, users = getEntityArgument(users) + for user in users: + i += 1 + user, chat, kvList = buildChatServiceObject(API.CHAT_CUSTOM_EMOJIS, user, i, count, [Ent.CHAT_EMOJI, name]) + if not chat: + continue + try: + emoji = callGAPI(chat.customEmojis(), 'get', + throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION], + name=name) + if not FJQC.formatJSON: + entityPerformAction(kvList, i, count) + _showChatItem(emoji, Ent.CHAT_EMOJI, FJQC, i, count) + except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e: + exitIfChatNotConfigured(chat, kvList, str(e), i, count) + except GAPI.failedPrecondition: + userChatServiceNotEnabledWarning(user, i, count) + +CHAT_EMOJI_SHOW_CREATED_BY_CHOICE_MAP = { + 'any': None, + 'me': 'creator("users/me")', + 'others': 'NOT creator("users/me")' + } + +# gam show chatemojis +# [showcreatedby any|me|others] +# [formatjson] +# gam print chatemojis [todrive *] +# [showcreatedby any|me|others] +# [formatjson [quotechar ]] +def printShowChatEmojis(users): + csvPF = CSVPrintFile(['User', 'name']) if Act.csvFormat() else None + FJQC = FormatJSONQuoteChar(csvPF) + pfilter = CHAT_EMOJI_SHOW_CREATED_BY_CHOICE_MAP['me'] + while Cmd.ArgumentsRemaining(): + myarg = getArgument() + if csvPF and myarg == 'todrive': + csvPF.GetTodriveParameters() + elif myarg =='showcreatedby': + pfilter = getChoice(CHAT_EMOJI_SHOW_CREATED_BY_CHOICE_MAP, mapChoice=True) + else: + FJQC.GetFormatJSONQuoteChar(myarg, True) + i, count, users = getEntityArgument(users) + for user in users: + i += 1 + user, chat, kvList = buildChatServiceObject(API.CHAT_CUSTOM_EMOJIS, user, i, count, [Ent.CHAT_EMOJI, None]) + if not chat: + continue + try: + emojis = callGAPIpages(chat.customEmojis(), 'list', 'customEmojis', + pageMessage=_getChatPageMessage(Ent.CHAT_EMOJI, user, i, count, pfilter), + throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED], + retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS, + pageSize=CHAT_PAGE_SIZE, filter=pfilter) + except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e: + exitIfChatNotConfigured(chat, kvList, str(e), i, count) + continue + except GAPI.failedPrecondition: + userChatServiceNotEnabledWarning(user, i, count) + break + if not csvPF: + jcount = len(emojis) + if not FJQC.formatJSON: + entityPerformActionNumItems(kvList, jcount, Ent.CHAT_EMOJI, i, count) + Ind.Increment() + j = 0 + for emoji in emojis: + j += 1 + _showChatItem(emoji, Ent.CHAT_EMOJI, FJQC, j, jcount) + Ind.Decrement() + else: + for emoji in emojis: + _printChatItem(user, emoji, '', Ent.CHAT_EMOJI, csvPF, FJQC) + if csvPF: + csvPF.writeCSVfile('Chat Custom Emojis') + # gam setup chat def doSetupChat(): checkForExtraneousArguments() @@ -67871,7 +68057,7 @@ def updatePhoto(users): baseFileIdEntity = drive = owner = None sourceFolder = os.getcwd() if Cmd.NumArgumentsRemaining() == 1: - filenamePattern = getString(Cmd.OB_PHOTO_FILENAME_PATTERN) + filenamePattern = getString(Cmd.OB_FILE_NAME_PATTERN) else: filenamePattern = '#email#.jpg' while Cmd.ArgumentsRemaining(): @@ -67883,7 +68069,7 @@ def updatePhoto(users): if not os.path.isdir(sourceFolder): entityDoesNotExistExit(Ent.DIRECTORY, sourceFolder) elif myarg == 'filename': - filenamePattern = getString(Cmd.OB_PHOTO_FILENAME_PATTERN) + filenamePattern = getString(Cmd.OB_FILE_NAME_PATTERN) elif myarg == 'gphoto': owner, drive = buildGAPIServiceObject(API.DRIVE3, getEmailAddress()) if not drive: @@ -67984,7 +68170,7 @@ def getPhoto(users, profileMode): if not os.path.isdir(targetFolder): os.makedirs(targetFolder) elif myarg == 'filename': - filenamePattern = getString(Cmd.OB_PHOTO_FILENAME_PATTERN) + filenamePattern = getString(Cmd.OB_FILE_NAME_PATTERN) elif myarg == 'nofile': writeFileData = False elif myarg == 'noshow': @@ -76983,6 +77169,7 @@ USER_ADD_CREATE_FUNCTIONS = { Cmd.ARG_CALENDAR: addCreateCalendars, Cmd.ARG_GROUP: addUserToGroups, Cmd.ARG_CALENDARACL: createCalendarACLs, + Cmd.ARG_CHATEMOJI: createChatEmoji, Cmd.ARG_CHATMEMBER: createChatMember, Cmd.ARG_CHATMESSAGE: createChatMessage, Cmd.ARG_CHATSPACE: createChatSpace, @@ -77099,6 +77286,7 @@ USER_COMMANDS_WITH_OBJECTS = { Cmd.ARG_BACKUPCODE: deleteBackupCodes, Cmd.ARG_CALENDAR: deleteCalendars, Cmd.ARG_CALENDARACL: deleteCalendarACLs, + Cmd.ARG_CHATEMOJI: deleteChatEmoji, Cmd.ARG_CHATMEMBER: deleteUpdateChatMember, Cmd.ARG_CHATMESSAGE: deleteChatMessage, Cmd.ARG_CHATSPACE: deleteChatSpace, @@ -77203,6 +77391,7 @@ USER_COMMANDS_WITH_OBJECTS = { (Act.INFO, {Cmd.ARG_CALENDAR: infoCalendars, Cmd.ARG_CALENDARACL: infoCalendarACLs, + Cmd.ARG_CHATEMOJI: infoChatEmoji, Cmd.ARG_CHATEVENT: infoChatEvent, Cmd.ARG_CHATMEMBER: infoChatMember, Cmd.ARG_CHATMESSAGE: infoChatMessage, @@ -77275,6 +77464,7 @@ USER_COMMANDS_WITH_OBJECTS = { Cmd.ARG_CALENDAR: printShowCalendars, Cmd.ARG_CALENDARACL: printShowCalendarACLs, Cmd.ARG_CALSETTINGS: printShowCalSettings, + Cmd.ARG_CHATEMOJI: printShowChatEmojis, Cmd.ARG_CHATEVENT: printShowChatEvents, Cmd.ARG_CHATMEMBER: printShowChatMembers, Cmd.ARG_CHATMESSAGE: printShowChatMessages, @@ -77382,6 +77572,7 @@ USER_COMMANDS_WITH_OBJECTS = { Cmd.ARG_CALENDAR: printShowCalendars, Cmd.ARG_CALENDARACL: printShowCalendarACLs, Cmd.ARG_CALSETTINGS: printShowCalSettings, + Cmd.ARG_CHATEMOJI: printShowChatEmojis, Cmd.ARG_CHATEVENT: printShowChatEvents, Cmd.ARG_CHATMEMBER: printShowChatMembers, Cmd.ARG_CHATMESSAGE: printShowChatMessages, @@ -77598,6 +77789,7 @@ USER_COMMANDS_OBJ_ALIASES = { Cmd.ARG_CLASSIFICATIONLABELPERMISSION: Cmd.ARG_DRIVELABELPERMISSION, Cmd.ARG_CLASSIFICATIONLABELPERMISSIONS: Cmd.ARG_DRIVELABELPERMISSION, Cmd.ARG_CLASSROOMINVITATIONS: Cmd.ARG_CLASSROOMINVITATION, + Cmd.ARG_CHATEMOJIS: Cmd.ARG_CHATEMOJI, Cmd.ARG_CHATEVENTS: Cmd.ARG_CHATEVENT, Cmd.ARG_CHATMEMBERS: Cmd.ARG_CHATMEMBER, Cmd.ARG_CHATMESSAGES: Cmd.ARG_CHATMESSAGE, diff --git a/src/gam/gamlib/glapi.py b/src/gam/gamlib/glapi.py index a91f0fcc..82a8c5b7 100644 --- a/src/gam/gamlib/glapi.py +++ b/src/gam/gamlib/glapi.py @@ -26,6 +26,7 @@ ANALYTICS_ADMIN = 'analyticsadmin' CALENDAR = 'calendar' CBCM = 'cbcm' CHAT = 'chat' +CHAT_CUSTOM_EMOJIS = 'chatcustomemojis' CHAT_EVENTS = 'chatevents' CHAT_MEMBERSHIPS = 'chatmemberships' CHAT_MEMBERSHIPS_ADMIN = 'chatmembershipsadmin' @@ -210,6 +211,7 @@ _INFO = { CALENDAR: {'name': 'Calendar API', 'version': 'v3', 'v2discovery': True, 'mappedAPI': 'calendar-json'}, CBCM: {'name': 'Chrome Browser Cloud Management API', 'version': 'v1.1beta1', 'v2discovery': True, 'localjson': True}, CHAT: {'name': 'Chat API', 'version': 'v1', 'v2discovery': True}, + CHAT_CUSTOM_EMOJIS: {'name': 'Chat API - Custom Emojis', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT}, CHAT_EVENTS: {'name': 'Chat API - Events', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT}, CHAT_MEMBERSHIPS: {'name': 'Chat API - Memberships', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT}, CHAT_MEMBERSHIPS_ADMIN: {'name': 'Chat API - Memberships Admin', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT}, @@ -544,6 +546,10 @@ _SVCACCT_SCOPES = [ 'api': CALENDAR, 'subscopes': READONLY, 'scope': 'https://www.googleapis.com/auth/calendar'}, + {'name': 'Chat API - Custom Emojis', + 'api': CHAT_CUSTOM_EMOJIS, + 'subscopes': READONLY, + 'scope': 'https://www.googleapis.com/auth/chat.customemojis'}, {'name': 'Chat API - Memberships', 'api': CHAT_MEMBERSHIPS, 'subscopes': READONLY, diff --git a/src/gam/gamlib/glclargs.py b/src/gam/gamlib/glclargs.py index 0c76fb38..359b12ac 100644 --- a/src/gam/gamlib/glclargs.py +++ b/src/gam/gamlib/glclargs.py @@ -461,6 +461,8 @@ class GamCLArgs(): ARG_CHANNELSKU = 'channelsku' ARG_CHANNELSKUS = 'channelskus' ARG_CHAT = 'chat' + ARG_CHATEMOJI = 'chatemoji' + ARG_CHATEMOJIS = 'chatemojis' ARG_CHATEVENT = 'chatevent' ARG_CHATEVENTS = 'chatevents' ARG_CHATMEMBER = 'chatmember' @@ -843,6 +845,8 @@ class GamCLArgs(): OB_CHARACTER = 'Character' OB_CHAR_SET = 'CharacterSet' OG_CHAT_ATTACHMENT = 'ChatAttachment' + OB_CHAT_EMOJI = 'ChatEmoji' + OB_CHAT_EMOJI_NAME = 'ChatEmojiName' OB_CHAT_EVENT = 'ChatEvent' OB_CHAT_MEMBER = 'ChatMember' OB_CHAT_MESSAGE = 'ChatMessage' @@ -930,6 +934,7 @@ class GamCLArgs(): OB_FILE_NAME = 'FileName' OB_FILE_NAME_FIELD_NAME = OB_FILE_NAME+'(:'+OB_FIELD_NAME+')+' OB_FILE_NAME_OR_URL = 'FileName|URL' + OB_FILE_NAME_PATTERN = 'FileNamePattern' OB_FILE_PATH = 'FilePath' OB_FILTER_ID_ENTITY = 'FilterIDEntity' OB_FORMAT_LIST = 'FormatList' @@ -975,7 +980,6 @@ class GamCLArgs(): OB_PERMISSION_ID_LIST = 'PermissionIDList' OB_PERMISSION_ROLE_LIST = 'PermissionRoleList' OB_PERMISSION_TYPE_LIST = 'PermissionTypeList' - OB_PHOTO_FILENAME_PATTERN = 'FilenameNamePattern' OB_PRINTER_ID = 'PrinterID' OB_PRIVILEGE_LIST = 'PrivilegeList' OB_PRODUCT_ID = 'ProductID' diff --git a/src/gam/gamlib/glentity.py b/src/gam/gamlib/glentity.py index c6544043..b05d5046 100644 --- a/src/gam/gamlib/glentity.py +++ b/src/gam/gamlib/glentity.py @@ -86,6 +86,7 @@ class GamEntity(): CHANNEL_SKU = 'chsk' CHAT_BOT = 'chbo' CHAT_ADMIN = 'chad' + CHAT_EMOJI = 'chem' CHAT_EVENT = 'chev' CHAT_MANAGER_USER = 'chgu' CHAT_MEMBER = 'chme' @@ -436,6 +437,7 @@ class GamEntity(): CHANNEL_SKU: ['Channel SKUs', 'Channel SKU'], CHAT_BOT: ['Chat BOTs', 'Chat BOT'], CHAT_ADMIN: ['Chat Admins', 'Chat Admin'], + CHAT_EMOJI: ['Chat Emojis', 'Chat Emoji'], CHAT_EVENT: ['Chat Events', 'Chat Event'], CHAT_MANAGER_USER: ['Chat User Managers', 'Chat User Manager'], CHAT_MESSAGE: ['Chat Messages', 'Chat Message'], diff --git a/src/gam/gamlib/glmsgs.py b/src/gam/gamlib/glmsgs.py index 88a0d0af..498994e0 100644 --- a/src/gam/gamlib/glmsgs.py +++ b/src/gam/gamlib/glmsgs.py @@ -309,6 +309,7 @@ INVALID_ALIAS = 'Invalid Alias' INVALID_ATTENDEE_CHANGE = 'Invalid attendee change "{0}"' INVALID_CHARSET = 'Invalid charset "{0}"' INVALID_DATE_TIME_RANGE = '{0} {1} must be greater than/equal to {2} {3}' +INVALID_EMOJI_NAME = '{0} does not match pattern :[0-9a-z_-]:' INVALID_ENTITY = 'Invalid {0}, {1}' INVALID_EVENT_TIMERANGE = '{0} {1} must be less than {2}' INVALID_FILE_SELECTION_WITH_ADMIN_ACCESS = 'Invalid file selection with adminaccess|asadmin'