Example #1
0
    def _base_request(self, url, content_type, response_type, data):
        """Send a generic authenticated POST request.

        Args:
            url (str): URL of request.
            content_type (str): Request content type.
            response_type (str): The desired response format. Valid options
                are: 'json' (JSON), 'protojson' (pblite), and 'proto' (binary
                Protocol Buffer). 'proto' requires manually setting an extra
                header 'X-Goog-Encode-Response-If-Executable: base64'.
            data (str): Request body data.

        Returns:
            FetchResponse: Response containing HTTP code, cookies, and body.

        Raises:
            NetworkError: If the request fails.
        """
        sapisid_cookie = self._get_cookie('SAPISID')
        headers = channel.get_authorization_headers(sapisid_cookie)
        headers['content-type'] = content_type
        required_cookies = ['SAPISID', 'HSID', 'SSID', 'APISID', 'SID']
        cookies = {cookie: self._get_cookie(cookie)
                   for cookie in required_cookies}
        params = {
            # "alternative representation type" (desired response format).
            'alt': response_type,
        }
        res = yield from http_utils.fetch(
            'post', url, headers=headers, cookies=cookies, params=params,
            data=data, connector=self._connector
        )
        return res
Example #2
0
    def _fetch_channel_sid(self):
        """Request a new session ID for the push channel.

        Raises hangups.NetworkError.
        """
        logger.info('Requesting new session...')
        url = 'https://talkgadget.google.com{}bind'.format(self._channel_path)
        params = {
            'VER': 8,
            'clid': self._clid_param,
            'ec': self._ec_param,
            'RID': 81187,
            # Required if we want our client to be called "AChromeExtension":
            'prop': self._prop_param,
        }
        try:
            res = yield from http_utils.fetch('post',
                                              url,
                                              cookies=self._cookies,
                                              params=params,
                                              data='count=0',
                                              connector=self._connector)
        except exceptions.NetworkError as e:
            raise exceptions.HangupsError(
                'Failed to request SID: {}'.format(e))
        # TODO: Re-write the function we're calling here to use a schema so we
        # can easily catch its failure.
        self._sid_param, self._email, self._header_client, self._gsessionid_param = (
            _parse_sid_response(res.body))
        logger.info('New SID: {}'.format(self._sid_param))
        logger.info('New email: {}'.format(self._email))
        logger.info('New client: {}'.format(self._header_client))
        logger.info('New gsessionid: {}'.format(self._gsessionid_param))
Example #3
0
 def send_maps(self, map_list):
     """Sends a request to the server containing maps (dicts)."""
     params = {
         'VER': 8,  # channel protocol version
         'RID': 81188,  # request identifier
         'ctype': 'hangouts',  # client type
     }
     if self._gsessionid_param is not None:
         params['gsessionid'] = self._gsessionid_param
     if self._sid_param is not None:
         params['SID'] = self._sid_param
     data_dict = dict(count=len(map_list), ofs=0)
     for map_num, map_ in enumerate(map_list):
         for map_key, map_val in map_.items():
             data_dict['req{}_{}'.format(map_num, map_key)] = map_val
     res = yield from http_utils.fetch(
         'post',
         CHANNEL_URL_PREFIX.format('channel/bind'),
         cookies=self._cookies,
         connector=self._connector,
         headers=get_authorization_headers(self._cookies['SAPISID']),
         params=params,
         data=data_dict,
     )
     return res
Example #4
0
 def _fetch_channel_sid(self):
     """Request a new session ID for the push channel."""
     logger.info('Requesting new session ID...')
     url = 'https://talkgadget.google.com{}bind'.format(self._channel_path)
     params = {
         'VER': 8,
         'clid': self._clid,
         'ec': self._channel_ec_param,
         'RID': 81187, # TODO: "request ID"? should probably increment
         # Required if we want our client to be called "AChromeExtension":
         'prop': self._channel_prop_param,
     }
     res = yield http_utils.fetch(
         url, method='POST', cookies=self._cookies, params=params,
         data='count=0', connect_timeout=CONNECT_TIMEOUT,
         request_timeout=REQUEST_TIMEOUT
     )
     logger.debug('Fetch SID response:\n{}'.format(res.body))
     if res.code != 200:
         # TODO use better exception
         raise ValueError("SID fetch request failed with {}: {}"
                          .format(res.code, res.raw.read()))
     # TODO: handle errors here
     self._channel_session_id, self._header_client, self._gsessionid = (
         _parse_sid_response(res.body)
     )
     logger.info('Received new session ID: {}'
                 .format(self._channel_session_id))
Example #5
0
 def _request(self, endpoint, body_json):
     """Make chat API request."""
     url = 'https://clients6.google.com/chat/v1/{}'.format(endpoint)
     headers = {
         'authorization': self._get_authorization_header(),
         'x-origin': ORIGIN_URL,
         'x-goog-authuser': '******',
         'content-type': 'application/json+protobuf',
     }
     required_cookies = ['SAPISID', 'HSID', 'SSID', 'APISID', 'SID']
     cookies = {
         cookie: self._get_cookie(cookie)
         for cookie in required_cookies
     }
     params = {
         'key': self._api_key,
         'alt': 'json',  # json or protojson
     }
     res = yield http_utils.fetch(url,
                                  method='POST',
                                  headers=headers,
                                  cookies=cookies,
                                  params=params,
                                  data=json.dumps(body_json),
                                  request_timeout=REQUEST_TIMEOUT,
                                  connect_timeout=CONNECT_TIMEOUT)
     logger.debug('Response to request for {} was {}:\n{}'.format(
         endpoint, res.code, res.body))
     if res.code != 200:
         raise ValueError(
             'Request to {} endpoint failed with {}: {}'.format(
                 endpoint, res.code, res.body.decode()))
     return res
Example #6
0
    def _request(self, endpoint, body_json, use_json=True):
        """Make chat API request.

        Raises hangups.NetworkError if the request fails.
        """
        url = 'https://clients6.google.com/chat/v1/{}'.format(endpoint)
        headers = {
            'authorization': self._get_authorization_header(),
            'x-origin': ORIGIN_URL,
            'x-goog-authuser': '******',
            'content-type': 'application/json+protobuf',
        }
        required_cookies = ['SAPISID', 'HSID', 'SSID', 'APISID', 'SID']
        cookies = {
            cookie: self._get_cookie(cookie)
            for cookie in required_cookies
        }
        params = {
            'key': self._api_key,
            'alt': 'json' if use_json else 'protojson',
        }

        logger.debug("Fetching '{}' with '{}'".format(url, body_json))

        res = yield from http_utils.fetch('post',
                                          url,
                                          headers=headers,
                                          cookies=cookies,
                                          params=params,
                                          data=json.dumps(body_json),
                                          connector=self._connector)
        logger.debug('Response to request for {} was {}:\n{}'.format(
            endpoint, res.code, res.body))
        return res
Example #7
0
    def _base_request(self, url, content_type, data, use_json=True):
        """Make API request.

        Raises hangups.NetworkError if the request fails.
        """
        headers = channel.get_authorization_headers(
            self._get_cookie('SAPISID'))
        headers['content-type'] = content_type
        required_cookies = ['SAPISID', 'HSID', 'SSID', 'APISID', 'SID']
        cookies = {
            cookie: self._get_cookie(cookie)
            for cookie in required_cookies
        }
        params = {
            'key': self._api_key,
            'alt': 'json' if use_json else 'protojson',
        }
        res = yield from http_utils.fetch('post',
                                          url,
                                          headers=headers,
                                          cookies=cookies,
                                          params=params,
                                          data=data,
                                          connector=self._connector)
        logger.debug('Response to request for {} was {}:\n{}'.format(
            url, res.code, res.body))
        return res
Example #8
0
 def _fetch_channel_sid(self):
     """Request a new session ID for the push channel."""
     logger.info('Requesting new session ID...')
     url = 'https://talkgadget.google.com{}bind'.format(self._channel_path)
     params = {
         'VER': 8,
         'clid': self._clid,
         'ec': self._channel_ec_param,
         'RID': 81187,  # TODO: "request ID"? should probably increment
         # Required if we want our client to be called "AChromeExtension":
         'prop': self._channel_prop_param,
     }
     res = yield http_utils.fetch(url,
                                  method='POST',
                                  cookies=self._cookies,
                                  params=params,
                                  data='count=0',
                                  connect_timeout=CONNECT_TIMEOUT,
                                  request_timeout=REQUEST_TIMEOUT)
     logger.debug('Fetch SID response:\n{}'.format(res.body))
     if res.code != 200:
         # TODO use better exception
         raise ValueError("SID fetch request failed with {}: {}".format(
             res.code, res.raw.read()))
     # TODO: handle errors here
     self._channel_session_id, self._header_client, self._gsessionid = (
         _parse_sid_response(res.body))
     logger.info('Received new session ID: {}'.format(
         self._channel_session_id))
Example #9
0
    def _request(self, endpoint, body_json, use_json=True):
        """Make chat API request.

        Raises hangups.NetworkError if the request fails.
        """
        url = 'https://clients6.google.com/chat/v1/{}'.format(endpoint)
        headers = {
            'authorization': self._get_authorization_header(),
            'x-origin': ORIGIN_URL,
            'x-goog-authuser': '******',
            'content-type': 'application/json+protobuf',
        }
        required_cookies = ['SAPISID', 'HSID', 'SSID', 'APISID', 'SID']
        cookies = {cookie: self._get_cookie(cookie)
                   for cookie in required_cookies}
        params = {
            'key': self._api_key,
            'alt': 'json' if use_json else 'protojson',
        }

        logger.debug("Fetching '{}' with '{}'".format(url, body_json))

        res = yield from http_utils.fetch(
            'post', url, headers=headers, cookies=cookies, params=params,
            data=json.dumps(body_json), connector=self._connector
        )
        logger.debug('Response to request for {} was {}:\n{}'
                     .format(endpoint, res.code, res.body))
        return res
Example #10
0
 def _request(self, endpoint, body_json):
     """Make chat API request."""
     url = 'https://clients6.google.com/chat/v1/{}'.format(endpoint)
     headers = {
         'authorization': self._get_authorization_header(),
         'x-origin': ORIGIN_URL,
         'x-goog-authuser': '******',
         'content-type': 'application/json+protobuf',
     }
     required_cookies = ['SAPISID', 'HSID', 'SSID', 'APISID', 'SID']
     cookies = {cookie: self._get_cookie(cookie)
                for cookie in required_cookies}
     params = {
         'key': self._api_key,
         'alt': 'json', # json or protojson
     }
     res = yield http_utils.fetch(
         url, method='POST', headers=headers, cookies=cookies,
         params=params, data=json.dumps(body_json),
         request_timeout=REQUEST_TIMEOUT, connect_timeout=CONNECT_TIMEOUT
     )
     logger.debug('Response to request for {} was {}:\n{}'
                  .format(endpoint, res.code, res.body))
     if res.code != 200:
         raise ValueError('Request to {} endpoint failed with {}: {}'
                          .format(endpoint, res.code, res.body.decode()))
     return res
Example #11
0
    def _base_request(self, url, content_type, response_type, data):
        """Send a generic authenticated POST request.

        Args:
            url (str): URL of request.
            content_type (str): Request content type.
            response_type (str): The desired response format. Valid options
                are: 'json' (JSON), 'protojson' (pblite), and 'proto' (binary
                Protocol Buffer). 'proto' requires manually setting an extra
                header 'X-Goog-Encode-Response-If-Executable: base64'.
            data (str): Request body data.

        Returns:
            FetchResponse: Response containing HTTP code, cookies, and body.

        Raises:
            NetworkError: If the request fails.
        """
        sapisid_cookie = self._get_cookie('SAPISID')
        headers = channel.get_authorization_headers(sapisid_cookie)
        headers['content-type'] = content_type
        required_cookies = ['SAPISID', 'HSID', 'SSID', 'APISID', 'SID']
        cookies = {cookie: self._get_cookie(cookie)
                   for cookie in required_cookies}
        params = {
            # "alternative representation type" (desired response format).
            'alt': response_type,
        }
        res = yield from http_utils.fetch(
            'post', url, headers=headers, cookies=cookies, params=params,
            data=data, connector=self._connector
        )
        return res
Example #12
0
    def _fetch_channel_sid(self):
        """Request a new session ID for the push channel.

        Raises hangups.NetworkError.
        """
        logger.info('Requesting new session...')
        url = 'https://talkgadget.google.com{}bind'.format(self._channel_path)
        params = {
            'VER': 8,
            'clid': self._clid_param,
            'ec': self._ec_param,
            'RID': 81187,
            # Required if we want our client to be called "AChromeExtension":
            'prop': self._prop_param,
        }
        try:
            res = yield from http_utils.fetch(
                'post', url, cookies=self._cookies, params=params,
                data='count=0', connector=self._connector
            )
        except exceptions.NetworkError as e:
            raise exceptions.HangupsError('Failed to request SID: {}'.format(e))
        # TODO: Re-write the function we're calling here to use a schema so we
        # can easily catch its failure.
        self._sid_param, self._email, self._header_client, self._gsessionid_param = (
            _parse_sid_response(res.body)
        )
        logger.info('New SID: {}'.format(self._sid_param))
        logger.info('New email: {}'.format(self._email))
        logger.info('New client: {}'.format(self._header_client))
        logger.info('New gsessionid: {}'.format(self._gsessionid_param))
Example #13
0
    def _fetch_channel_sid(self):
        """Creates a new channel for receiving push data.

        Raises hangups.NetworkError if the channel can not be created.
        """
        logger.info('Requesting new gsessionid and SID...')
        # There's a separate API to get the gsessionid alone that Hangouts for
        # Chrome uses, but if we don't send a gsessionid with this request, it
        # will return a gsessionid as well as the SID.
        res = yield from http_utils.fetch(
            'post',
            CHANNEL_URL_PREFIX.format('channel/bind'),
            cookies=self._cookies,
            data='count=0',
            connector=self._connector,
            headers=get_authorization_headers(self._cookies['SAPISID']),
            params={
                'VER': 8,
                'RID': 81187,
                'ctype': 'hangouts',  # client type
            })
        self._sid_param, self._gsessionid_param = _parse_sid_response(res.body)
        self._is_subscribed = False
        logger.info('New SID: {}'.format(self._sid_param))
        logger.info('New gsessionid: {}'.format(self._gsessionid_param))
Example #14
0
    def _subscribe(self):
        """Subscribes the channel to receive relevant events.

        Only needs to be called when a new channel (SID/gsessionid) is opened.
        """
        # XXX: Temporary workaround for #58
        yield from asyncio.sleep(1)

        logger.info('Subscribing channel...')
        timestamp = str(int(time.time() * 1000))
        # Hangouts for Chrome splits this over 2 requests, but it's possible to
        # do everything in one.
        yield from http_utils.fetch(
            'post',
            CHANNEL_URL_PREFIX.format('channel/bind'),
            cookies=self._cookies,
            connector=self._connector,
            headers=get_authorization_headers(self._cookies['SAPISID']),
            params={
                'VER': 8,
                'RID': 81188,
                'ctype': 'hangouts',  # client type
                'gsessionid': self._gsessionid_param,
                'SID': self._sid_param,
            },
            data={
                'count':
                3,
                'ofs':
                0,
                'req0_p': ('{"1":{"1":{"1":{"1":3,"2":2}},"2":{"1":{"1":3,"2":'
                           '2},"2":"","3":"JS","4":"lcsclient"},"3":' +
                           timestamp + ',"4":0,"5":"c1"},"2":{}}'),
                'req1_p':
                ('{"1":{"1":{"1":{"1":3,"2":2}},"2":{"1":{"1":3,"2":'
                 '2},"2":"","3":"JS","4":"lcsclient"},"3":' + timestamp +
                 ',"4":' + timestamp + ',"5":"c3"},"3":{"1":{"1":"babel"}}}'),
                'req2_p': ('{"1":{"1":{"1":{"1":3,"2":2}},"2":{"1":{"1":3,"2":'
                           '2},"2":"","3":"JS","4":"lcsclient"},"3":' +
                           timestamp + ',"4":' + timestamp +
                           ',"5":"c4"},"3":{"1":{"1":"hangout_invite"}}}'),
            },
        )
        logger.info('Channel is now subscribed')
        self._is_subscribed = True
Example #15
0
    def upload_images(self, links):
        """Download images and upload them to Google+"""
        image_id_list = []
        for link in links:
            # Download image
            try:
                res = yield from http_utils.fetch('get', link)
            except hangups.NetworkError as e:
                print('Failed to download image: {}'.format(e))
                continue

            # Upload image and get image_id
            try:
                image_id = yield from self._client.upload_image(
                    io.BytesIO(res.body), filename=os.path.basename(link))
                image_id_list.append(image_id)
            except hangups.NetworkError as e:
                print('Failed to upload image: {}'.format(e))
                continue
        return image_id_list
Example #16
0
    def upload_images(self, links):
        """Download images and upload them to Google+"""
        image_id_list = []
        for link in links:
            # Download image
            try:
                res = yield from http_utils.fetch('get', link)
            except hangups.NetworkError as e:
                print('Failed to download image: {}'.format(e))
                continue

            # Upload image and get image_id
            try:
                image_id = yield from self._client.upload_image(io.BytesIO(res.body),
                                                                filename=os.path.basename(link))
                image_id_list.append(image_id)
            except hangups.NetworkError as e:
                print('Failed to upload image: {}'.format(e))
                continue
        return image_id_list
Example #17
0
    def _subscribe(self):
        """Subscribes the channel to receive relevant events.

        Only needs to be called when a new channel (SID/gsessionid) is opened.
        """
        # XXX: Temporary workaround for #58
        yield from asyncio.sleep(1)

        logger.info('Subscribing channel...')
        timestamp = str(int(time.time() * 1000))
        # Hangouts for Chrome splits this over 2 requests, but it's possible to
        # do everything in one.
        yield from http_utils.fetch(
            'post', CHANNEL_URL_PREFIX.format('channel/bind'),
            cookies=self._cookies, connector=self._connector,
            headers=get_authorization_headers(self._cookies['SAPISID']),
            params={
                'VER': 8,
                'RID': 81188,
                'ctype': 'hangouts',  # client type
                'gsessionid': self._gsessionid_param,
                'SID': self._sid_param,
            },
            data={
                'count': 3,
                'ofs': 0,
                'req0_p': ('{"1":{"1":{"1":{"1":3,"2":2}},"2":{"1":{"1":3,"2":'
                           '2},"2":"","3":"JS","4":"lcsclient"},"3":' +
                           timestamp + ',"4":0,"5":"c1"},"2":{}}'),
                'req1_p': ('{"1":{"1":{"1":{"1":3,"2":2}},"2":{"1":{"1":3,"2":'
                           '2},"2":"","3":"JS","4":"lcsclient"},"3":' +
                           timestamp + ',"4":' + timestamp +
                           ',"5":"c3"},"3":{"1":{"1":"babel"}}}'),
                'req2_p': ('{"1":{"1":{"1":{"1":3,"2":2}},"2":{"1":{"1":3,"2":'
                           '2},"2":"","3":"JS","4":"lcsclient"},"3":' +
                           timestamp + ',"4":' + timestamp +
                           ',"5":"c4"},"3":{"1":{"1":"hangout_invite"}}}'),
            },
        )
        logger.info('Channel is now subscribed')
        self._is_subscribed = True
Example #18
0
    def _base_request(self, url, content_type, data, use_json=True):
        """Make API request.

        Raises hangups.NetworkError if the request fails.
        """
        headers = channel.get_authorization_headers(self._get_cookie('SAPISID'))
        headers['content-type'] = content_type
        required_cookies = ['SAPISID', 'HSID', 'SSID', 'APISID', 'SID']
        cookies = {cookie: self._get_cookie(cookie)
                   for cookie in required_cookies}
        params = {
            'key': self._api_key,
            'alt': 'json' if use_json else 'protojson',
        }
        res = yield from http_utils.fetch(
            'post', url, headers=headers, cookies=cookies, params=params,
            data=data, connector=self._connector
        )
        logger.debug('Response to request for {} was {}:\n{}'
                     .format(url, res.code, res.body))
        return res
Example #19
0
 def send_maps(self, map_list):
     """Sends a request to the server containing maps (dicts)."""
     params = {
         'VER': 8,  # channel protocol version
         'RID': 81188,  # request identifier
         'ctype': 'hangouts',  # client type
     }
     if self._gsessionid_param is not None:
         params['gsessionid'] = self._gsessionid_param
     if self._sid_param is not None:
         params['SID'] = self._sid_param
     data_dict = dict(count=len(map_list), ofs=0)
     for map_num, map_ in enumerate(map_list):
         for map_key, map_val in map_.items():
             data_dict['req{}_{}'.format(map_num, map_key)] = map_val
     res = yield from http_utils.fetch(
         'post', CHANNEL_URL_PREFIX.format('channel/bind'),
         cookies=self._cookies, connector=self._connector,
         headers=get_authorization_headers(self._cookies['SAPISID']),
         params=params, data=data_dict,
     )
     return res
Example #20
0
    def _fetch_channel_sid(self):
        """Creates a new channel for receiving push data.

        Raises hangups.NetworkError if the channel can not be created.
        """
        logger.info('Requesting new gsessionid and SID...')
        # There's a separate API to get the gsessionid alone that Hangouts for
        # Chrome uses, but if we don't send a gsessionid with this request, it
        # will return a gsessionid as well as the SID.
        res = yield from http_utils.fetch(
            'post', CHANNEL_URL_PREFIX.format('channel/bind'),
            cookies=self._cookies, data='count=0', connector=self._connector,
            headers=get_authorization_headers(self._cookies['SAPISID']),
            params={
                'VER': 8,
                'RID': 81187,
                'ctype': 'hangouts',  # client type
            }
        )
        self._sid_param, self._gsessionid_param = _parse_sid_response(res.body)
        self._is_subscribed = False
        logger.info('New SID: {}'.format(self._sid_param))
        logger.info('New gsessionid: {}'.format(self._gsessionid_param))
Example #21
0
    def _base_request(self, url, content_type, response_type, data):
        """Send a generic authenticated POST request.

        Args:
            url (str): URL of request.
            content_type (str): Request content type.
            response_type (str): The desired response format. Valid options
                are: 'json' (JSON), 'protojson' (pblite), and 'proto' (binary
                Protocol Buffer). 'proto' requires manually setting an extra
                header 'X-Goog-Encode-Response-If-Executable: base64'.
            data (str): Request body data.

        Returns:
            FetchResponse: Response containing HTTP code, cookies, and body.

        Raises:
            NetworkError: If the request fails.
        """
        sapisid_cookie = self._get_cookie('SAPISID')
        headers = channel.get_authorization_headers(sapisid_cookie)
        headers['content-type'] = content_type
        # This header is required for Protocol Buffer responses, which causes
        # them to be base64 encoded:
        headers['X-Goog-Encode-Response-If-Executable'] = 'base64'
        params = {
            # "alternative representation type" (desired response format).
            'alt': response_type,
            # API key (required to avoid 403 Forbidden "Daily Limit for
            # Unauthenticated Use Exceeded. Continued use requires signup").
            'key': API_KEY,
        }
        res = yield from http_utils.fetch(
            self._session, 'post', url, headers=headers, params=params,
            data=data
        )
        return res
Example #22
0
    def _initialize_chat(self):
        """Request push channel creation and initial chat data.

        Returns instance of InitialData.

        The response body is a HTML document containing a series of script tags
        containing JavaScript objects. We need to parse the objects to get at
        the data.
        """
        # We first need to fetch the 'pvt' token, which is required for the
        # initialization request (otherwise it will return 400).
        try:
            res = yield from http_utils.fetch(
                'get', PVT_TOKEN_URL, cookies=self._cookies,
                connector=self._connector
            )
            CHAT_INIT_PARAMS['pvt'] = javascript.loads(res.body.decode())[1]
            logger.info('Found PVT token: {}'.format(CHAT_INIT_PARAMS['pvt']))
        except (exceptions.NetworkError, ValueError) as e:
            raise exceptions.HangupsError('Failed to fetch PVT token: {}'
                                          .format(e))
        # Now make the actual initialization request:
        try:
            res = yield from http_utils.fetch(
                'get', CHAT_INIT_URL, cookies=self._cookies,
                params=CHAT_INIT_PARAMS, connector=self._connector
            )
        except exceptions.NetworkError as e:
            raise exceptions.HangupsError('Initialize chat request failed: {}'
                                          .format(e))

        # Parse the response by using a regex to find all the JS objects, and
        # parsing them. Not everything will be parsable, but we don't care if
        # an object we don't need can't be parsed.

        data_dict = {}
        for data in CHAT_INIT_REGEX.findall(res.body.decode()):
            try:
                logger.debug("Attempting to load javascript: {}..."
                             .format(repr(data[:100])))
                data = javascript.loads(data)
                # pylint: disable=invalid-sequence-index
                data_dict[data['key']] = data['data']
            except ValueError as e:
                try:
                    data = data.replace("data:function(){return", "data:")
                    data = data.replace("}}", "}")
                    data = javascript.loads(data)
                    data_dict[data['key']] = data['data']

                except ValueError as e:
                    raise

                # logger.debug('Failed to parse initialize chat object: {}\n{}'
                #              .format(e, data))

        # Extract various values that we will need.
        try:
            self._api_key = data_dict['ds:7'][0][2]
            self._email = data_dict['ds:34'][0][2]
            self._header_date = data_dict['ds:2'][0][4]
            self._header_version = data_dict['ds:2'][0][6]
            self._header_id = data_dict['ds:4'][0][7]
            _sync_timestamp = parsers.from_timestamp(
                # cgserp?
                # data_dict['ds:21'][0][1][4]
                # data_dict['ds:35'][0][1][4]
                data_dict['ds:21'][0][1][4]
            )
        except KeyError as e:
            raise exceptions.HangupsError('Failed to get initialize chat '
                                          'value: {}'.format(e))

        # Parse the entity representing the current user.
        self_entity = schemas.CLIENT_GET_SELF_INFO_RESPONSE.parse(
            # cgsirp?
            # data_dict['ds:20'][0]
            # data_dict['ds:35'][0]
            data_dict['ds:20'][0]
        ).self_entity

        # Parse every existing conversation's state, including participants.
        initial_conv_states = schemas.CLIENT_CONVERSATION_STATE_LIST.parse(
            # csrcrp?
            # data_dict['ds:19'][0][3]
            # data_dict['ds:36'][0][3]
            data_dict['ds:19'][0][3]
        )
        initial_conv_parts = []
        for conv_state in initial_conv_states:
            initial_conv_parts.extend(conv_state.conversation.participant_data)

        # Parse the entities for the user's contacts (doesn't include users not
        # in contacts). If this fails, continue without the rest of the
        # entities.
        initial_entities = []
        try:
            entities = schemas.INITIAL_CLIENT_ENTITIES.parse(
                # cgserp?
                # data_dict['ds:21'][0]
                # data_dict['ds:37'][0]
                data_dict['ds:21'][0]
            )
        except ValueError as e:
            logger.warning('Failed to parse initial client entities: {}'
                           .format(e))
        else:
            initial_entities.extend(entities.entities)
            initial_entities.extend(e.entity for e in itertools.chain(
                entities.group1.entity, entities.group2.entity,
                entities.group3.entity, entities.group4.entity,
                entities.group5.entity
            ))

        return InitialData(initial_conv_states, self_entity, initial_entities,
                           initial_conv_parts, _sync_timestamp)
Example #23
0
    def _initialize_chat(self):
        """Request push channel creation and initial chat data.

        Returns instance of InitialData.

        The response body is a HTML document containing a series of script tags
        containing JavaScript objects. We need to parse the objects to get at
        the data.
        """
        try:
            res = yield from http_utils.fetch(
                'get', CHAT_INIT_URL, cookies=self._cookies,
                params=CHAT_INIT_PARAMS, connector=self._connector
            )
        except exceptions.NetworkError as e:
            raise exceptions.HangupsError('Initialize chat request failed: {}'
                                          .format(e))

        # Parse the response by using a regex to find all the JS objects, and
        # parsing them. Not everything will be parsable, but we don't care if
        # an object we don't need can't be parsed.
        data_dict = {}
        for data in CHAT_INIT_REGEX.findall(res.body.decode()):
            try:
                data = javascript.loads(data)
                # pylint: disable=invalid-sequence-index
                data_dict[data['key']] = data['data']
            except ValueError as e:
                logger.debug('Failed to parse initialize chat object: {}\n{}'
                             .format(e, data))

        # Extract various values that we will need.
        try:
            self._api_key = data_dict['ds:7'][0][2]
            self._header_date = data_dict['ds:2'][0][4]
            self._header_version = data_dict['ds:2'][0][6]
            self._header_id = data_dict['ds:4'][0][7]
            self._channel_path = data_dict['ds:4'][0][1]
            self._clid = data_dict['ds:4'][0][7]
            self._channel_ec_param = data_dict['ds:4'][0][4]
            self._channel_prop_param = data_dict['ds:4'][0][5]
            _sync_timestamp = parsers.from_timestamp(
                data_dict['ds:21'][0][1][4]
            )
        except KeyError as e:
            raise exceptions.HangupsError('Failed to get initialize chat '
                                          'value: {}'.format(e))

        # Parse the entity representing the current user.
        self_entity = schemas.CLIENT_GET_SELF_INFO_RESPONSE.parse(
            data_dict['ds:20'][0]
        ).self_entity

        # Parse every existing conversation's state, including participants.
        initial_conv_states = schemas.CLIENT_CONVERSATION_STATE_LIST.parse(
            data_dict['ds:19'][0][3]
        )
        initial_conv_parts = []
        for conv_state in initial_conv_states:
            initial_conv_parts.extend(conv_state.conversation.participant_data)

        # Parse the entities for the user's contacts (doesn't include users not
        # in contacts). If this fails, continue without the rest of the
        # entities.
        initial_entities = []
        try:
            entities = schemas.INITIAL_CLIENT_ENTITIES.parse(
                data_dict['ds:21'][0]
            )
        except ValueError as e:
            logger.warning('Failed to parse initial client entities: {}'
                           .format(e))
        else:
            initial_entities.extend(entities.entities)
            initial_entities.extend(e.entity for e in itertools.chain(
                entities.group1.entity, entities.group2.entity,
                entities.group3.entity, entities.group4.entity,
                entities.group5.entity
            ))

        return InitialData(initial_conv_states, self_entity, initial_entities,
                           initial_conv_parts, _sync_timestamp)
Example #24
0
    def _initialize_chat(self):
        """Request push channel creation and initial chat data.

        Returns instance of InitialData.

        The response body is a HTML document containing a series of script tags
        containing JavaScript objects. We need to parse the objects to get at
        the data.
        """
        try:
            res = yield from http_utils.fetch('get',
                                              CHAT_INIT_URL,
                                              cookies=self._cookies,
                                              params=CHAT_INIT_PARAMS,
                                              connector=self._connector)
        except exceptions.NetworkError as e:
            raise exceptions.HangupsError(
                'Initialize chat request failed: {}'.format(e))

        # Parse the response by using a regex to find all the JS objects, and
        # parsing them. Not everything will be parsable, but we don't care if
        # an object we don't need can't be parsed.
        data_dict = {}
        for data in CHAT_INIT_REGEX.findall(res.body.decode()):
            try:
                data = javascript.loads(data)
                # pylint: disable=invalid-sequence-index
                data_dict[data['key']] = data['data']
            except ValueError as e:
                logger.debug(
                    'Failed to parse initialize chat object: {}\n{}'.format(
                        e, data))

        # Extract various values that we will need.
        try:
            self._api_key = data_dict['ds:7'][0][2]
            self._header_date = data_dict['ds:2'][0][4]
            self._header_version = data_dict['ds:2'][0][6]
            self._header_id = data_dict['ds:4'][0][7]
            self._channel_path = data_dict['ds:4'][0][1]
            self._clid = data_dict['ds:4'][0][7]
            self._channel_ec_param = data_dict['ds:4'][0][4]
            self._channel_prop_param = data_dict['ds:4'][0][5]
            _sync_timestamp = parsers.from_timestamp(
                data_dict['ds:21'][0][1][4])
        except KeyError as e:
            raise exceptions.HangupsError('Failed to get initialize chat '
                                          'value: {}'.format(e))

        # Parse the entity representing the current user.
        self_entity = schemas.CLIENT_GET_SELF_INFO_RESPONSE.parse(
            data_dict['ds:20'][0]).self_entity

        # Parse every existing conversation's state, including participants.
        initial_conv_states = schemas.CLIENT_CONVERSATION_STATE_LIST.parse(
            data_dict['ds:19'][0][3])
        initial_conv_parts = []
        for conv_state in initial_conv_states:
            initial_conv_parts.extend(conv_state.conversation.participant_data)

        # Parse the entities for the user's contacts (doesn't include users not
        # in contacts). If this fails, continue without the rest of the
        # entities.
        initial_entities = []
        try:
            entities = schemas.INITIAL_CLIENT_ENTITIES.parse(
                data_dict['ds:21'][0])
        except ValueError as e:
            logger.warning(
                'Failed to parse initial client entities: {}'.format(e))
        else:
            initial_entities.extend(entities.entities)
            initial_entities.extend(e.entity for e in itertools.chain(
                entities.group1.entity, entities.group2.entity, entities.
                group3.entity, entities.group4.entity, entities.group5.entity))

        return InitialData(initial_conv_states, self_entity, initial_entities,
                           initial_conv_parts, _sync_timestamp)
Example #25
0
    def _init_talkgadget_1(self):
        """Make first talkgadget request and parse response.

        The response body is a HTML document containing a series of script tags
        containing JavaScript object. We need to parse the object to get at the
        data.
        """
        url = 'https://talkgadget.google.com/u/0/talkgadget/_/chat'
        params = {
            'prop': 'aChromeExtension',
            'fid': 'gtn-roster-iframe-id',
            'ec': '["ci:ec",true,true,false]',
        }
        headers = {
            # appears to require a browser user agent
            'user-agent':
            ('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
             '(KHTML, like Gecko) Chrome/34.0.1847.132 Safari/537.36'),
        }
        res = yield http_utils.fetch(url,
                                     cookies=self._cookies,
                                     params=params,
                                     headers=headers,
                                     connect_timeout=CONNECT_TIMEOUT,
                                     request_timeout=REQUEST_TIMEOUT)
        logger.debug('First talkgadget request result:\n{}'.format(res.body))
        if res.code != 200:
            raise ValueError(
                "First talkgadget request failed with {}: {}".format(
                    res.code, res.body))
        res = res.body.decode()

        # Parse the response by using a regex to find all the JS objects, and
        # parsing them.
        res = res.replace('\n', '')
        regex = re.compile(
            r"(?:<script>AF_initDataCallback\((.*?)\);</script>)")
        data_dict = {}
        for data in regex.findall(res):
            try:
                data = javascript.loads(data)
                data_dict[data['key']] = data['data']
            except ValueError as e:
                # not everything will be parsable, but we don't care
                logger.debug('Failed to parse JavaScript: {}\n{}'.format(
                    e, data))

        # TODO: handle errors here
        self._api_key = data_dict['ds:7'][0][2]
        self._header_date = data_dict['ds:2'][0][4]
        self._header_version = data_dict['ds:2'][0][6]
        self._header_id = data_dict['ds:4'][0][7]
        self._channel_path = data_dict['ds:4'][0][1]
        self._clid = data_dict['ds:4'][0][7]
        self._channel_ec_param = data_dict['ds:4'][0][4]
        self._channel_prop_param = data_dict['ds:4'][0][5]

        # build dict of conversations and their participants
        initial_conversations = {}
        self.initial_users = {}  # {UserID: User}

        # add self to the contacts
        self_contact = data_dict['ds:20'][0][2]
        self.self_user_id = UserID(chat_id=self_contact[8][0],
                                   gaia_id=self_contact[8][1])
        self.initial_users[self.self_user_id] = User(
            id_=self.self_user_id,
            full_name=self_contact[9][1],
            first_name=self_contact[9][2],
            is_self=True)

        conversations = data_dict['ds:19'][0][3]
        for c in conversations:
            id_ = c[1][0][0]
            participants = c[1][13]
            last_modified = c[1][3][12]
            # With every converstion, we get a list of up to 20 of the most
            # recent messages, sorted oldest to newest.
            messages = []
            for raw_message in c[2]:
                message = longpoll._parse_chat_message([raw_message])
                # A message may parse to None if it's just a conversation name
                # change.
                if message is not None:
                    messages.append(message[1:])
            initial_conversations[id_] = {
                'participants': [],
                'last_modified': last_modified,
                'name': c[1][2],
                'messages': messages,
            }
            # Add the participants for this conversation.
            for p in participants:
                user_id = UserID(chat_id=p[0][0], gaia_id=p[0][1])
                initial_conversations[id_]['participants'].append(user_id)
                # Add the participant to our list of contacts as a fallback, in
                # case they can't be found later by other methods.
                # TODO We should note who these users are and try to request
                # them.
                # p[1] can be a full name, None, or out of range.
                try:
                    display_name = p[1]
                except IndexError:
                    display_name = None
                if display_name is None:
                    display_name = 'Unknown'
                self.initial_users[user_id] = User(
                    id_=user_id,
                    first_name=display_name.split()[0],
                    full_name=display_name,
                    is_self=(user_id == self.self_user_id))

        # build dict of contacts and their names (doesn't include users not in
        # contacts)
        contacts_main = data_dict['ds:21'][0]
        # contacts_main[2] has some, but the format is slightly different
        contacts = (contacts_main[4][2] + contacts_main[5][2] +
                    contacts_main[6][2] + contacts_main[7][2] +
                    contacts_main[8][2])
        for c in contacts:
            user_id = UserID(chat_id=c[0][8][0], gaia_id=c[0][8][1])
            self.initial_users[user_id] = User(
                id_=user_id,
                full_name=c[0][9][1],
                first_name=c[0][9][2],
                is_self=(user_id == self.self_user_id))

        # Create a dict of the known conversations.
        self.initial_conversations = {
            conv_id: Conversation(
                self,
                conv_id,
                [
                    self.initial_users[user_id]
                    for user_id in conv_info['participants']
                ],
                conv_info['last_modified'],
                conv_info['name'],
                conv_info['messages'],
            )
            for conv_id, conv_info in initial_conversations.items()
        }
Example #26
0
    def _init_talkgadget_1(self):
        """Make first talkgadget request and parse response.

        The response body is a HTML document containing a series of script tags
        containing JavaScript object. We need to parse the object to get at the
        data.
        """
        url = 'https://talkgadget.google.com/u/0/talkgadget/_/chat'
        params = {
            'prop': 'aChromeExtension',
            'fid': 'gtn-roster-iframe-id',
            'ec': '["ci:ec",true,true,false]',
        }
        headers = {
            # appears to require a browser user agent
            'user-agent': (
                'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
                '(KHTML, like Gecko) Chrome/34.0.1847.132 Safari/537.36'
            ),
        }
        res = yield http_utils.fetch(
            url, cookies=self._cookies, params=params, headers=headers,
            connect_timeout=CONNECT_TIMEOUT, request_timeout=REQUEST_TIMEOUT
        )
        logger.debug('First talkgadget request result:\n{}'.format(res.body))
        if res.code != 200:
            raise ValueError("First talkgadget request failed with {}: {}"
                             .format(res.code, res.body))
        res = res.body.decode()

        # Parse the response by using a regex to find all the JS objects, and
        # parsing them.
        res = res.replace('\n', '')
        regex = re.compile(
            r"(?:<script>AF_initDataCallback\((.*?)\);</script>)"
        )
        data_dict = {}
        for data in regex.findall(res):
            try:
                data = javascript.loads(data)
                data_dict[data['key']] = data['data']
            except ValueError as e:
                # not everything will be parsable, but we don't care
                logger.debug('Failed to parse JavaScript: {}\n{}'
                             .format(e, data))

        # TODO: handle errors here
        self._api_key = data_dict['ds:7'][0][2]
        self._header_date = data_dict['ds:2'][0][4]
        self._header_version = data_dict['ds:2'][0][6]
        self._header_id = data_dict['ds:4'][0][7]
        self._channel_path = data_dict['ds:4'][0][1]
        self._clid = data_dict['ds:4'][0][7]
        self._channel_ec_param = data_dict['ds:4'][0][4]
        self._channel_prop_param = data_dict['ds:4'][0][5]

        # build dict of conversations and their participants
        initial_conversations = {}
        self.initial_users = {} # {UserID: User}

        # add self to the contacts
        self_contact = data_dict['ds:20'][0][2]
        self.self_user_id = UserID(chat_id=self_contact[8][0],
                                   gaia_id=self_contact[8][1])
        self.initial_users[self.self_user_id] = User(
            id_=self.self_user_id, full_name=self_contact[9][1],
            first_name=self_contact[9][2], is_self=True
        )

        conversations = data_dict['ds:19'][0][3]
        for c in conversations:
            id_ = c[1][0][0]
            participants = c[1][13]
            last_modified = c[1][3][12]
            initial_conversations[id_] = {
                'participants': [],
                'last_modified': last_modified,
            }
            for p in participants:
                user_id = UserID(chat_id=p[0][0], gaia_id=p[0][1])
                initial_conversations[id_]['participants'].append(
                    user_id
                )
                # Add the user to our list of contacts if their name is
                # present. This is a hack to deal with some contacts not being
                # found via the other methods.
                # TODO We should note who these users are and try to request
                # them.
                if len(p) > 1:
                    display_name = p[1]
                    self.initial_users[user_id] = User(
                        id_=user_id, first_name=display_name.split()[0],
                        full_name=display_name,
                        is_self=(user_id == self.self_user_id)
                    )

        # build dict of contacts and their names (doesn't include users not in
        # contacts)
        contacts_main = data_dict['ds:21'][0]
        # contacts_main[2] has some, but the format is slightly different
        contacts = (contacts_main[4][2] + contacts_main[5][2] +
                    contacts_main[6][2] + contacts_main[7][2] +
                    contacts_main[8][2])
        for c in contacts:
            user_id = UserID(chat_id=c[0][8][0], gaia_id=c[0][8][1])
            self.initial_users[user_id] = User(
                id_=user_id, full_name=c[0][9][1], first_name=c[0][9][2],
                is_self=(user_id == self.self_user_id)
            )

        # Create a dict of the known conversations.
        self.initial_conversations = {conv_id: Conversation(
            self, conv_id, [self.initial_users[user_id] for user_id
                            in conv_info['participants']],
            conv_info['last_modified'],
        ) for conv_id, conv_info in initial_conversations.items()}
Example #27
0
    def _initialize_chat(self):
        """Request push channel creation and initial chat data.

        Returns instance of InitialData.

        The response body is a HTML document containing a series of script tags
        containing JavaScript objects. We need to parse the objects to get at
        the data.
        """
        # We first need to fetch the 'pvt' token, which is required for the
        # initialization request (otherwise it will return 400).
        try:
            res = yield from http_utils.fetch('get',
                                              PVT_TOKEN_URL,
                                              cookies=self._cookies,
                                              connector=self._connector)
            CHAT_INIT_PARAMS['pvt'] = javascript.loads(res.body.decode())[1]
            logger.info('Found PVT token: {}'.format(CHAT_INIT_PARAMS['pvt']))
        except (exceptions.NetworkError, ValueError) as e:
            raise exceptions.HangupsError(
                'Failed to fetch PVT token: {}'.format(e))
        # Now make the actual initialization request:
        try:
            res = yield from http_utils.fetch('get',
                                              CHAT_INIT_URL,
                                              cookies=self._cookies,
                                              params=CHAT_INIT_PARAMS,
                                              connector=self._connector)
        except exceptions.NetworkError as e:
            raise exceptions.HangupsError(
                'Initialize chat request failed: {}'.format(e))

        # Parse the response by using a regex to find all the JS objects, and
        # parsing them. Not everything will be parsable, but we don't care if
        # an object we don't need can't be parsed.

        data_dict = {}
        for data in CHAT_INIT_REGEX.findall(res.body.decode()):
            try:
                logger.debug("Attempting to load javascript: {}...".format(
                    repr(data[:100])))
                data = javascript.loads(data)
                # pylint: disable=invalid-sequence-index
                data_dict[data['key']] = data['data']
            except ValueError as e:
                try:
                    data = data.replace("data:function(){return", "data:")
                    data = data.replace("}}", "}")
                    data = javascript.loads(data)
                    data_dict[data['key']] = data['data']

                except ValueError as e:
                    raise

                # logger.debug('Failed to parse initialize chat object: {}\n{}'
                #              .format(e, data))

        # Extract various values that we will need.
        try:
            self._api_key = data_dict['ds:7'][0][2]
            self._email = data_dict['ds:34'][0][2]
            self._header_date = data_dict['ds:2'][0][4]
            self._header_version = data_dict['ds:2'][0][6]
            self._header_id = data_dict['ds:4'][0][7]
            _sync_timestamp = parsers.from_timestamp(
                # cgserp?
                # data_dict['ds:21'][0][1][4]
                # data_dict['ds:35'][0][1][4]
                data_dict['ds:21'][0][1][4])
        except KeyError as e:
            raise exceptions.HangupsError('Failed to get initialize chat '
                                          'value: {}'.format(e))

        # Parse the entity representing the current user.
        self_entity = schemas.CLIENT_GET_SELF_INFO_RESPONSE.parse(
            # cgsirp?
            # data_dict['ds:20'][0]
            # data_dict['ds:35'][0]
            data_dict['ds:20'][0]).self_entity

        # Parse every existing conversation's state, including participants.
        initial_conv_states = schemas.CLIENT_CONVERSATION_STATE_LIST.parse(
            # csrcrp?
            # data_dict['ds:19'][0][3]
            # data_dict['ds:36'][0][3]
            data_dict['ds:19'][0][3])
        initial_conv_parts = []
        for conv_state in initial_conv_states:
            initial_conv_parts.extend(conv_state.conversation.participant_data)

        # Parse the entities for the user's contacts (doesn't include users not
        # in contacts). If this fails, continue without the rest of the
        # entities.
        initial_entities = []
        try:
            entities = schemas.INITIAL_CLIENT_ENTITIES.parse(
                # cgserp?
                # data_dict['ds:21'][0]
                # data_dict['ds:37'][0]
                data_dict['ds:21'][0])
        except ValueError as e:
            logger.warning(
                'Failed to parse initial client entities: {}'.format(e))
        else:
            initial_entities.extend(entities.entities)
            initial_entities.extend(e.entity for e in itertools.chain(
                entities.group1.entity, entities.group2.entity, entities.
                group3.entity, entities.group4.entity, entities.group5.entity))

        return InitialData(initial_conv_states, self_entity, initial_entities,
                           initial_conv_parts, _sync_timestamp)