Files

222 lines
9.4 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
import dateutil.parser
import json
import requests
from odoo import models, api
from odoo.tools.urls import urljoin as url_join
class SocialStream(models.Model):
_inherit = 'social.stream'
FACEBOOK_REACTIONS = {'LIKE', 'LOVE', 'HAHA', 'WOW', 'SAD', 'ANGRY', 'CARE'}
FACEBOOK_REACTIONS_FIELDS = ','.join({
f'reactions.type({reaction}).limit(0).summary(1).as({reaction})'
for reaction in FACEBOOK_REACTIONS
})
def _apply_default_name(self):
facebook_streams = self.filtered(lambda s: s.media_id.media_type == 'facebook')
super(SocialStream, (self - facebook_streams))._apply_default_name()
for stream in facebook_streams:
stream.write({'name': '%s: %s' % (stream.stream_type_id.name, stream.account_id.name)})
def _fetch_stream_data(self):
if self.media_id.media_type != 'facebook':
return super()._fetch_stream_data()
if self.stream_type_id.stream_type == 'facebook_page_posts':
return self._fetch_page_posts('published_posts')
elif self.stream_type_id.stream_type == 'facebook_page_mentions':
return self._fetch_page_posts('tagged')
def _fetch_page_posts(self, endpoint_name):
self.ensure_one()
facebook_fields = [
'id',
'message',
'from',
'shares',
'likes.limit(1).summary(true)',
'comments.limit(10).summary(true){message,from,like_count}',
'attachments',
'created_time',
'message_tags',
self.FACEBOOK_REACTIONS_FIELDS,
]
if endpoint_name == 'published_posts':
# used for `facebook_reach`
facebook_fields.append('insights.metric(post_media_view)')
posts_endpoint_url = url_join(
self.env['social.media']._FACEBOOK_ENDPOINT_VERSIONED,
f"{self.account_id.facebook_account_id}/{endpoint_name}"
)
result = requests.get(
posts_endpoint_url,
params={
'access_token': self.account_id.facebook_access_token,
'fields': ','.join(facebook_fields)
},
timeout=5
)
result_posts = result.json().get('data')
if not result_posts:
self.account_id._action_disconnect_accounts(result.json())
return False
facebook_post_ids = [post.get('id') for post in result_posts]
existing_posts = self.env['social.stream.post'].search([
('stream_id', '=', self.id),
('facebook_post_id', 'in', facebook_post_ids)
])
existing_posts_by_facebook_post_id = {
post.facebook_post_id: post for post in existing_posts
}
posts_to_create = []
for post in result_posts:
reactions = {
reaction: count
for reaction in self.env['social.stream'].FACEBOOK_REACTIONS
if (count := post.get(reaction, {}).get('summary', {}).get('total_count', 0))
}
values = {
'stream_id': self.id,
'message': self._format_facebook_message(post.get('message'), post.get('message_tags')),
'author_name': post.get('from', {}).get('name', ''),
'facebook_author_id': post.get('from', {}).get('id'),
'published_date': dateutil.parser.parse(post.get('created_time'), ignoretz=True),
'facebook_shares_count': post.get('shares', {}).get('count'),
'facebook_likes_count': sum(reactions.values()),
'facebook_reactions_count': json.dumps(reactions),
'facebook_user_likes': post.get('likes', {}).get('summary', {}).get('has_liked'),
'facebook_comments_count': post.get('comments', {}).get('summary', {}).get('total_count'),
'facebook_reach': post.get('insights', {}).get('data', [{}])[0].get('values', [{}])[0].get('value'),
'facebook_post_id': post.get('id'),
'facebook_is_event_post': post.get('attachments', {}).get('data', [{}])[0].get('type') == 'event',
}
attachments = self._extract_facebook_attachments(post)
existing_post = existing_posts_by_facebook_post_id.get(post.get('id'))
if existing_post:
if attachments.get('stream_post_image_ids'):
values['stream_post_image_ids'] = [(5, 0, 0)] + attachments['stream_post_image_ids']
existing_post.sudo().write(values)
else:
if attachments or values['message']:
# do not create post without content
values.update(attachments)
posts_to_create.append(values)
stream_posts = self.env['social.stream.post'].sudo().create(posts_to_create)
return any(stream_post.stream_id.create_uid.id == self.env.uid for stream_post in stream_posts)
@api.model
def _extract_facebook_attachments(self, post):
result = {}
for attachment in post.get('attachments', {}).get('data', []):
if attachment.get('type') == 'share':
result.update({
'link_title': attachment.get('title'),
'link_description': attachment.get('description'),
'link_url': attachment.get('url'),
})
if attachment.get('media'):
result.update({
'link_image_url': attachment.get('media').get('image').get('src')
})
elif attachment.get('type') == 'album':
images = []
images_urls = []
for sub_image in attachment.get('subattachments', {}).get('data', []):
image_url = sub_image.get('media').get('image').get('src')
images.append({
'image_url': image_url
})
images_urls.append(image_url)
if images:
result.update({
'stream_post_image_ids': [(0, 0, attachment) for attachment in images],
})
elif attachment.get('type') in ['photo', 'animated_image_video']:
# TODO improvement later: handle videos in Feed view to correctly display FB GIFs?
image_src = attachment.get('media', {}).get('image', {}).get('src')
if image_src:
result.update({'stream_post_image_ids': [(0, 0, {'image_url': image_src})]})
elif attachment.get('type') == 'event':
# events creation post are handle like link and image in the frontend
result.update({
'link_title': attachment.get('title'),
'link_description': attachment.get('description'),
'link_url': attachment.get('target', {}).get('url', ''),
})
image_src = attachment.get('media', {}).get('image', {}).get('src')
if image_src:
result.update({'stream_post_image_ids': [(0, 0, {'image_url': image_src})]})
return result
@api.model
def _format_facebook_message(self, message, tags):
"""
This allows to create links to the referenced Facebook pages
example:
"Hello Odoo :)" -> "Hello @[542132] Odoo :)"
"Hello Odoo Social :)" -> "Hello @[542132] Odoo-Social :)"
"Hello Odoo - Social :)" -> "Hello @[542132] Odoo-Social :)"
details:
"Hello @[42] faketag Odoo - Social :)" -> "Hello @ [42] faketag @[542132] Odoo-Social :)""
-> With associated tag: {name: "Odoo - Social", offset: 20, length: 13}
We take the message until the tag offset: "Hello @[42] faketag "
and remove the fake tag in the process "Hello @ [42] faketag "
Then we insert the forged tag "@[542132] Odoo-Social"
Then take the rest of the message, starting at index tag offset + length, since we inserted the forged tag instead
:param message: str message
:param tags: list of tags in the message as received through Facebook endpoint
- id: ID of the page/user/group tagged
- name: name of the page/user/group tagged
- offset: position of the tag in the text
- length: length of the tag in the text
"""
if not message:
return message
def remove_forged_tags(message):
"""
Remove False positive in the message
(e.g: if someone write "@[42] test" without tagging someone)
"""
return re.sub(r'\B@\[', '@ [', message)
# Remove Facebook hashtags that have are considered as tags with ID but are not managed by this method
tags = [tag for tag in tags or [] if not tag.get('name', '').startswith('#')]
message_index = 0
message_with_tags = ''
for tag in tags or []:
no_space_name = re.sub(r'\s+', '-', re.sub(r'\s*-\s*', '-', tag['name']))
forged_tag = '@[%s] %s' % (tag['id'], no_space_name)
message_with_tags += remove_forged_tags(message[message_index:tag['offset']]) + forged_tag
message_index = tag['offset'] + tag['length']
message_with_tags += remove_forged_tags(message[message_index:])
return message_with_tags