forked from Mapan/odoo17e
221 lines
10 KiB
Python
221 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import dateutil.parser
|
|
import logging
|
|
import requests
|
|
from html import unescape
|
|
|
|
from odoo import models, fields, api, _
|
|
from odoo.exceptions import UserError
|
|
from werkzeug.urls import url_join
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SocialStreamTwitter(models.Model):
|
|
_inherit = 'social.stream'
|
|
|
|
twitter_searched_keyword = fields.Char('Search Keyword')
|
|
twitter_followed_account_search = fields.Char('Search User')
|
|
# TODO awa: clean unused 'social.twitter.account' in a cron job
|
|
twitter_followed_account_id = fields.Many2one('social.twitter.account')
|
|
|
|
@api.constrains('stream_type_id', 'twitter_followed_account_id')
|
|
def _check_twitter_followed_account_id(self):
|
|
if any(
|
|
stream.stream_type_id.stream_type in ('twitter_follow', 'twitter_likes')
|
|
and not stream.twitter_followed_account_id
|
|
for stream in self
|
|
):
|
|
raise UserError(_("Please select a Twitter account for this stream type."))
|
|
|
|
def _apply_default_name(self):
|
|
twitter_streams = self.filtered(lambda s: s.media_id.media_type == 'twitter')
|
|
super(SocialStreamTwitter, (self - twitter_streams))._apply_default_name()
|
|
|
|
for stream in twitter_streams:
|
|
name = False
|
|
if stream.stream_type_id.stream_type in ['twitter_follow', 'twitter_likes'] and stream.twitter_followed_account_id:
|
|
name = '%s: %s' % (stream.stream_type_id.name, stream.twitter_followed_account_id.name)
|
|
elif stream.stream_type_id.stream_type == 'twitter_user_mentions' and stream.account_id:
|
|
name = '%s: %s' % (stream.stream_type_id.name, stream.account_id.name)
|
|
elif stream.stream_type_id.stream_type == 'twitter_keyword' and stream.twitter_searched_keyword:
|
|
name = '%s: %s' % (stream.stream_type_id.name, stream.twitter_searched_keyword)
|
|
|
|
if name:
|
|
stream.write({'name': name})
|
|
|
|
def _fetch_stream_data(self):
|
|
if self.media_id.media_type != 'twitter':
|
|
return super()._fetch_stream_data()
|
|
|
|
if self.stream_type_id.stream_type == 'twitter_user_mentions':
|
|
return self._fetch_tweets('/2/users/%s/mentions' % self.account_id.twitter_user_id)
|
|
if self.stream_type_id.stream_type == 'twitter_follow':
|
|
return self._fetch_tweets('/2/users/%s/tweets' % self.twitter_followed_account_id.twitter_id)
|
|
if self.stream_type_id.stream_type == 'twitter_likes':
|
|
return self._fetch_tweets('/2/users/%s/liked_tweets' % self.twitter_followed_account_id.twitter_id)
|
|
if self.stream_type_id.stream_type == 'twitter_keyword':
|
|
keyword = self.twitter_searched_keyword
|
|
if not keyword.startswith("#"):
|
|
keyword = "#%s" % keyword
|
|
return self._fetch_tweets('/2/tweets/search/recent', {'query': keyword + ' -is:retweet'})
|
|
|
|
def _fetch_tweets(self, endpoint_name, extra_params={}):
|
|
self.ensure_one()
|
|
query_params = {
|
|
'max_results': 100,
|
|
'tweet.fields': 'created_at,public_metrics,referenced_tweets,conversation_id',
|
|
'expansions': 'author_id,attachments.media_keys,referenced_tweets.id,referenced_tweets.id.author_id',
|
|
'user.fields': 'id,name,username,profile_image_url',
|
|
'media.fields': 'type,url,preview_image_url',
|
|
}
|
|
query_params.update(extra_params)
|
|
tweets_endpoint_url = url_join(self.env['social.media']._TWITTER_ENDPOINT, endpoint_name)
|
|
# TODO awa: check the "TE" header (Transfer-Encoding) to get a (smaller) gzip response
|
|
headers = self.account_id._get_twitter_oauth_header(
|
|
tweets_endpoint_url,
|
|
params=query_params,
|
|
method='GET',
|
|
)
|
|
response = requests.get(
|
|
tweets_endpoint_url,
|
|
query_params,
|
|
headers=headers,
|
|
timeout=5
|
|
)
|
|
result = response.json()
|
|
|
|
if not response.ok:
|
|
_logger.error('Failed to fetch social stream posts: %r for account %i.', response.text, self.account_id.id)
|
|
# an error occurred
|
|
if 'Not authorized' in result.get('title', ''):
|
|
# no error code is returned by the Twitter API in that case
|
|
# it's probably because the Twitter account we tried to add
|
|
# is private
|
|
error_message = _(
|
|
"You cannot create a Stream from this Twitter account.\n"
|
|
"It may be because it's protected. To solve this, please make sure you follow it before trying again."
|
|
)
|
|
elif response.status_code == 400 and result.get('errors', [{}])[0].get('parameters', {}).get('query'):
|
|
# invalid query
|
|
error_message = _("The keyword you've typed in does not look valid. Please try again with other words.")
|
|
else:
|
|
error_code = result.get('status')
|
|
error_message = result.get('title')
|
|
ERROR_MESSAGES = {
|
|
429: _("Looks like you've made too many requests. Please wait a few minutes before giving it another try."),
|
|
}
|
|
error_message = ERROR_MESSAGES.get(error_code, error_message)
|
|
|
|
if error_message:
|
|
raise UserError(error_message)
|
|
|
|
if isinstance(result, dict) and not result.get('data') and result.get('errors') or result is None:
|
|
self.account_id._action_disconnect_accounts(result)
|
|
return False
|
|
|
|
tweets_by_tweet_id = {
|
|
tweet['id']: tweet
|
|
for tweet in result.get('data', [])
|
|
}
|
|
|
|
existing_tweets = self.env['social.stream.post'].sudo().search([
|
|
('stream_id', '=', self.id),
|
|
('twitter_tweet_id', 'in', list(tweets_by_tweet_id)),
|
|
])
|
|
existing_tweets_by_tweet_id = {
|
|
tweet.twitter_tweet_id: tweet for tweet in existing_tweets
|
|
}
|
|
|
|
# TODO awa: handle deleted tweets ?
|
|
tweets_to_create = []
|
|
|
|
users_per_id = {
|
|
user['id']: user
|
|
for user in result.get('includes', {}).get('users', [])
|
|
}
|
|
medias_per_id = {
|
|
media['media_key']: media
|
|
for media in result.get('includes', {}).get('media', [])
|
|
}
|
|
|
|
quote_and_retweet_per_ids = {
|
|
tweet.get('id'): tweet
|
|
for tweet in result.get('includes', {}).get('tweets', [])
|
|
}
|
|
|
|
for twitter_tweet_id, tweet in tweets_by_tweet_id.items():
|
|
public_metrics = tweet.get('public_metrics', {})
|
|
user_info = users_per_id.get(tweet.get('author_id'), {})
|
|
created_date = tweet.get('created_at')
|
|
if created_date:
|
|
created_date = fields.Datetime.from_string(dateutil.parser.parse(created_date).strftime('%Y-%m-%d %H:%M:%S'))
|
|
values = {
|
|
'stream_id': self.id,
|
|
'message': unescape(tweet.get('text', '')),
|
|
'author_name': user_info.get('name'),
|
|
'published_date': created_date,
|
|
'twitter_likes_count': public_metrics.get('like_count'),
|
|
'twitter_retweet_count': public_metrics.get('retweet_count'),
|
|
'twitter_tweet_id': twitter_tweet_id,
|
|
'twitter_conversation_id': tweet.get('conversation_id'),
|
|
'twitter_author_id': tweet.get('author_id'),
|
|
'twitter_screen_name': user_info.get('username'),
|
|
'twitter_profile_image_url': user_info.get('profile_image_url'),
|
|
}
|
|
|
|
# Handle quote and retweet
|
|
referenced_tweets = tweet.get('referenced_tweets', [])
|
|
if referenced_tweets and referenced_tweets[0]['type'] == 'retweeted':
|
|
values['twitter_retweeted_tweet_id_str'] = referenced_tweets[0]['id']
|
|
elif referenced_tweets and referenced_tweets[0]['type'] == 'quoted':
|
|
quote = quote_and_retweet_per_ids.get(referenced_tweets[0]['id'], {})
|
|
quote_author = users_per_id.get(quote.get('author_id'), {})
|
|
|
|
values.update({
|
|
'twitter_quoted_tweet_id_str': quote.get('id'),
|
|
'twitter_quoted_tweet_message': quote.get('text', ''),
|
|
'twitter_quoted_tweet_author_name': quote_author.get('name', ''),
|
|
'twitter_quoted_tweet_profile_image_url': quote_author.get('profile_image_url', ''),
|
|
})
|
|
if quote_author.get('username'):
|
|
values['twitter_quoted_tweet_author_link'] = 'https://twitter.com/%s' % quote_author['username']
|
|
|
|
retweets = list(filter(lambda ref: ref.get('type') == 'retweeted', referenced_tweets))
|
|
if retweets:
|
|
origin_tweet_msg = quote_and_retweet_per_ids.get(retweets[0].get('id'), {}).get('text')
|
|
if origin_tweet_msg:
|
|
username = users_per_id[quote_and_retweet_per_ids.get(retweets[0].get('id'), {}).get('author_id')].get('username', _('Unknown'))
|
|
values['message'] = unescape(
|
|
f"RT @{username}: "
|
|
f"{origin_tweet_msg}"
|
|
)
|
|
|
|
existing_tweet = existing_tweets_by_tweet_id.get(tweet.get('id'))
|
|
if existing_tweet:
|
|
existing_tweet.sudo().write(values)
|
|
else:
|
|
# attachments are only extracted for new posts
|
|
values.update(self._extract_twitter_attachments(tweet, medias_per_id))
|
|
tweets_to_create.append(values)
|
|
|
|
stream_posts = self.env['social.stream.post'].sudo().create(tweets_to_create)
|
|
return any(stream_post.stream_id.create_uid.id == self.env.uid for stream_post in stream_posts)
|
|
|
|
@api.model
|
|
def _extract_twitter_attachments(self, tweet, medias_per_id=None):
|
|
if not medias_per_id:
|
|
return {}
|
|
medias = [
|
|
medias_per_id[media]
|
|
for media in tweet.get('attachments', {}).get('media_keys', [])
|
|
]
|
|
images = [
|
|
{'image_url': media['url']}
|
|
for media in medias
|
|
if media['type'] == 'photo'
|
|
]
|
|
return {'stream_post_image_ids': [(0, 0, attachment) for attachment in images]} if images else {}
|