1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/social_twitter/models/social_stream.py
2024-12-10 09:04:09 +07:00

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 {}