# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import base64 import requests from werkzeug.urls import url_join from odoo import api, fields, models, _ from odoo.exceptions import UserError TWITTER_IMAGES_UPLOAD_ENDPOINT = "https://upload.twitter.com/1.1/media/upload.json" class SocialAccountTwitter(models.Model): _inherit = 'social.account' twitter_user_id = fields.Char('Twitter User ID') twitter_oauth_token = fields.Char('Twitter OAuth Token') twitter_oauth_token_secret = fields.Char('Twitter OAuth Token Secret') def _compute_statistics(self): """ See methods '_get_last_tweets_stats' for more info about Twitter stats. """ twitter_accounts = self._filter_by_media_types(['twitter']) super(SocialAccountTwitter, (self - twitter_accounts))._compute_statistics() for account in twitter_accounts: account_stats = account._get_account_stats() last_tweets_stats = account._get_last_tweets_stats() if account_stats and last_tweets_stats: account.write({ 'audience': account_stats.get('data', [{}])[0].get('public_metrics', {}).get('followers_count'), 'engagement': last_tweets_stats['engagement'], 'stories': last_tweets_stats['stories'], }) def _compute_stats_link(self): twitter_accounts = self._filter_by_media_types(['twitter']) super(SocialAccountTwitter, (self - twitter_accounts))._compute_stats_link() for account in twitter_accounts: account.stats_link = f"https://analytics.twitter.com/user/{account.social_account_handle}" @api.model_create_multi def create(self, vals_list): res = super(SocialAccountTwitter, self).create(vals_list) res.filtered(lambda account: account.media_type == 'twitter')._create_default_stream_twitter() return res def twitter_get_user_by_username(self, username): """Search a user based on his username (e.g: "fpodoo"). Can not search by name, can only get user by their usernames See: https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference """ user_search_endpoint = url_join( self.env['social.media']._TWITTER_ENDPOINT, '/2/users/by/username/%s' % username) params = {'user.fields': 'id,name,username,description,profile_image_url'} headers = self._get_twitter_oauth_header( user_search_endpoint, params=params, method='GET' ) response = requests.get( user_search_endpoint, params=params, headers=headers, timeout=5 ) return response.json().get('data', False) if response.ok else False def _create_default_stream_twitter(self): """ This will create a stream of type 'Twitter Follow' for each added accounts. It helps with onboarding to have your tweets show up on the 'Feed' view as soon as you have configured your accounts.""" if not self: return own_tweets_stream_type_id = self.env.ref('social_twitter.stream_type_twitter_follow').id streams_to_create = [] for account in self: # we have to create a matching social.twitter.account for each stream twitter_followed_account = self.env['social.twitter.account'].create({ 'name': account.name, 'twitter_id': account.twitter_user_id, 'image': account.image }) streams_to_create.append({ 'media_id': account.media_id.id, 'stream_type_id': own_tweets_stream_type_id, 'account_id': account.id, 'twitter_followed_account_id': twitter_followed_account.id }) self.env['social.stream'].create(streams_to_create) def _get_account_stats(self): """ Query the account information to retrieve the Twitter audience (= followers count). """ self.ensure_one() twitter_account_info_url = url_join(self.env['social.media']._TWITTER_ENDPOINT, '/2/users/by') params = {'user.fields': 'public_metrics', 'usernames': self.social_account_handle} headers = self._get_twitter_oauth_header( twitter_account_info_url, params=params, method='GET', ) result = requests.get( twitter_account_info_url, params=params, headers=headers, timeout=5 ) if isinstance(result.json(), dict) and result.json().get('errors'): self._action_disconnect_accounts(result.json()) return False return result.json() def _get_last_tweets_stats(self): """ To properly retrieve statistics and trends, we would need an Enterprise 'Engagement API' access. See: https://developer.twitter.com/en/docs/metrics/get-tweet-engagement/overview Since we don't have access, we use the last 100 user tweets (max for one request) to aggregate the data we are able to retrieve. """ self.ensure_one() tweets_endpoint_url = url_join( self.env['social.media']._TWITTER_ENDPOINT, '/2/users/%s/tweets' % self.twitter_user_id) params = { 'max_results': 100, 'tweet.fields': 'public_metrics', } headers = self._get_twitter_oauth_header( tweets_endpoint_url, params=params, method='GET' ) result = requests.get( tweets_endpoint_url, params, headers=headers, timeout=10, ) if isinstance(result.json(), dict) and result.json().get('errors'): self._action_disconnect_accounts(result.json()) return False last_tweets_stats = { 'engagement': 0, 'stories': 0 } for tweet in result.json().get('data', []): public_metrics = tweet.get('public_metrics', {}) last_tweets_stats['engagement'] += public_metrics.get('like_count', 0) last_tweets_stats['stories'] += public_metrics.get('retweet_count', 0) return last_tweets_stats def _get_twitter_oauth_header(self, url, headers={}, params={}, method='POST'): self.ensure_one() headers.update({ 'oauth_token': self.twitter_oauth_token, 'oauth_token_secret': self.twitter_oauth_token_secret, }) return self.media_id._get_twitter_oauth_header(url, headers=headers, params=params, method=method) def _format_attachments_to_images_twitter(self, image_ids): return self._format_images_twitter([{ 'bytes': base64.decodebytes(image.datas), 'file_size': image.file_size, 'mimetype': image.mimetype } for image in image_ids]) def _format_images_twitter(self, image_ids): """ Twitter needs a special kind of uploading to process images. It's done in 3 steps: - initialize upload transaction - send bytes - finalize upload transaction. More information: https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload.html """ self.ensure_one() if not image_ids: return False media_ids = [] for image in image_ids: media_id = self._init_twitter_upload(image) self._process_twitter_upload(image, media_id) self._finish_twitter_upload(media_id) media_ids.append(media_id) return media_ids def _init_twitter_upload(self, image): data = { 'command': 'INIT', 'total_bytes': image['file_size'], 'media_category': 'tweet_gif' if image['mimetype'] == 'image/gif' else 'tweet_image', 'media_type': image['mimetype'], } headers = self._get_twitter_oauth_header( TWITTER_IMAGES_UPLOAD_ENDPOINT, params=data ) result = requests.post( TWITTER_IMAGES_UPLOAD_ENDPOINT, data=data, headers=headers, timeout=5 ) if not result.ok: # unfortunately Twitter does not return a proper error code so we have to rely on the error message # last known max file size for the API is 20MB generic_api_error = result.json().get('error', '') raise UserError(_("We could not upload your image, it may be corrupted, it may exceed size limit or API may have send improper response (error: %s).", generic_api_error)) return result.json().get('media_id_string') def _process_twitter_upload(self, image, media_id): params = { 'command': 'APPEND', 'media_id': media_id, 'segment_index': 0, } files = { 'media': image['bytes'] } headers = self._get_twitter_oauth_header( TWITTER_IMAGES_UPLOAD_ENDPOINT, params=params ) requests.post( TWITTER_IMAGES_UPLOAD_ENDPOINT, params=params, files=files, headers=headers, timeout=15 ) def _finish_twitter_upload(self, media_id): data = { 'command': 'FINALIZE', 'media_id': media_id, } headers = self._get_twitter_oauth_header( TWITTER_IMAGES_UPLOAD_ENDPOINT, params=data ) requests.post( TWITTER_IMAGES_UPLOAD_ENDPOINT, data=data, headers=headers, timeout=5 )