forked from Mapan/odoo17e
263 lines
11 KiB
Python
263 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import re
|
|
import requests
|
|
from urllib.parse import quote
|
|
from datetime import datetime
|
|
from werkzeug.urls import url_join
|
|
|
|
from odoo import _, models, fields
|
|
from odoo.exceptions import UserError
|
|
|
|
|
|
class SocialStreamPostLinkedIn(models.Model):
|
|
_inherit = 'social.stream.post'
|
|
|
|
linkedin_post_urn = fields.Char('LinkedIn post URN')
|
|
linkedin_author_urn = fields.Char('LinkedIn author URN')
|
|
linkedin_author_id = fields.Char('LinkedIn author ID', compute='_compute_linkedin_author_urn')
|
|
linkedin_author_vanity_name = fields.Char('LinkedIn Vanity Name', help='Vanity name, used to generate a link to the author')
|
|
linkedin_author_image_url = fields.Char('LinkedIn author image URL')
|
|
|
|
linkedin_comments_count = fields.Integer('LinkedIn Comments')
|
|
linkedin_likes_count = fields.Integer('LinkedIn Likes')
|
|
|
|
def _compute_linkedin_author_urn(self):
|
|
for post in self:
|
|
if post.linkedin_author_urn:
|
|
post.linkedin_author_id = post.linkedin_author_urn.split(':')[-1]
|
|
else:
|
|
post.linkedin_author_id = False
|
|
|
|
def _compute_author_link(self):
|
|
linkedin_posts = self._filter_by_media_types(['linkedin'])
|
|
super(SocialStreamPostLinkedIn, (self - linkedin_posts))._compute_author_link()
|
|
|
|
for post in linkedin_posts:
|
|
if post.linkedin_author_urn:
|
|
post.author_link = 'https://linkedin.com/company/%s' % post.linkedin_author_id
|
|
else:
|
|
post.author_link = False
|
|
|
|
def _compute_post_link(self):
|
|
linkedin_posts = self._filter_by_media_types(['linkedin'])
|
|
super(SocialStreamPostLinkedIn, (self - linkedin_posts))._compute_post_link()
|
|
|
|
for post in linkedin_posts:
|
|
if post.linkedin_post_urn:
|
|
post.post_link = 'https://www.linkedin.com/feed/update/%s' % post.linkedin_post_urn
|
|
else:
|
|
post.post_link = False
|
|
|
|
def _compute_is_author(self):
|
|
linkedin_posts = self._filter_by_media_types(['linkedin'])
|
|
super(SocialStreamPostLinkedIn, (self - linkedin_posts))._compute_is_author()
|
|
|
|
for post in linkedin_posts:
|
|
post.is_author = post.linkedin_author_urn == post.account_id.linkedin_account_urn
|
|
|
|
# ========================================================
|
|
# COMMENTS / LIKES
|
|
# ========================================================
|
|
|
|
def _linkedin_comment_add(self, message, comment_urn=None):
|
|
data = {
|
|
'actor': self.account_id.linkedin_account_urn,
|
|
'message': {
|
|
'text': message,
|
|
},
|
|
'object': self.linkedin_post_urn,
|
|
}
|
|
|
|
if comment_urn:
|
|
# we reply yo an existing comment
|
|
data['parentComment'] = comment_urn
|
|
|
|
response = self.account_id._linkedin_request(
|
|
'socialActions/%s/comments' % quote(self.linkedin_post_urn),
|
|
method="POST",
|
|
json=data,
|
|
).json()
|
|
|
|
if 'created' not in response:
|
|
self.sudo().account_id._action_disconnect_accounts(response)
|
|
return {}
|
|
|
|
response['from'] = { # fill with our own information to save an API call
|
|
'id': self.account_id.linkedin_account_urn,
|
|
'name': self.account_id.name,
|
|
'authorUrn': self.account_id.linkedin_account_urn,
|
|
'picture': f"/web/image?model=social.account&id={self.account_id.id}&field=image",
|
|
'isOrganization': True,
|
|
}
|
|
return self._linkedin_format_comment(response)
|
|
|
|
def _linkedin_comment_delete(self, comment_urn):
|
|
comment_id = re.search(r'urn:li:comment:\(urn:li:activity:\w+,(\w+)\)', comment_urn).group(1)
|
|
|
|
response = self.account_id._linkedin_request(
|
|
'socialActions/%s/comments/%s' % (quote(self.linkedin_post_urn), quote(comment_id)),
|
|
method='DELETE',
|
|
params={'actor': self.account_id.linkedin_account_urn},
|
|
)
|
|
|
|
if response.status_code != 204:
|
|
self.sudo().account_id._action_disconnect_accounts(response.json())
|
|
|
|
def _linkedin_comment_fetch(self, comment_urn=None, offset=0, count=20):
|
|
"""Retrieve comments on a LinkedIn element.
|
|
|
|
:param element_urn: URN of the element (UGC Post or Comment) on which we want to retrieve comments
|
|
If no specified, retrieve comments on the current post
|
|
:param offset: Used to scroll over the comments, position of the first retrieved comment
|
|
:param count: Number of comments returned
|
|
"""
|
|
element_urn = comment_urn or self.linkedin_post_urn
|
|
|
|
response = self.account_id._linkedin_request(
|
|
'socialActions/%s/comments' % quote(element_urn),
|
|
params={
|
|
'start': offset,
|
|
'count': count,
|
|
},
|
|
)
|
|
response_json = response.json()
|
|
if 'elements' not in response_json:
|
|
self.sudo().account_id._action_disconnect_accounts(response_json)
|
|
if not response.ok:
|
|
raise UserError(
|
|
_(
|
|
'Failed to retrieve the post. It might have been deleted or you may not have permission to view it.'
|
|
)
|
|
)
|
|
|
|
comments = response_json.get('elements', [])
|
|
|
|
persons_ids = {comment.get('actor') for comment in comments if comment.get('actor')}
|
|
organizations_ids = {author.split(":")[-1] for author in persons_ids if author.startswith("urn:li:organization:")}
|
|
persons = {author.split(":")[-1] for author in persons_ids if author.startswith("urn:li:person:")}
|
|
images_ids = []
|
|
|
|
formatted_authors = {}
|
|
|
|
# get the author information if it's an organization
|
|
if organizations_ids:
|
|
response_json = self.account_id._linkedin_request(
|
|
'organizations',
|
|
object_ids=organizations_ids,
|
|
fields=('id', 'name', 'localizedName', 'vanityName', 'logoV2:(original)'),
|
|
).json()
|
|
for organization_id, organization in response_json.get('results', {}).items():
|
|
organization_urn = f"urn:li:organization:{organization_id}"
|
|
image_id = organization.get('logoV2', {}).get('original', '').split(':')[-1]
|
|
images_ids.append(image_id)
|
|
formatted_authors[organization_urn] = {
|
|
'id': organization_urn,
|
|
'name': organization.get('localizedName'),
|
|
'authorUrn': organization_urn,
|
|
'picture': image_id,
|
|
'vanityName': organization.get('vanityName'),
|
|
'isOrganization': True,
|
|
}
|
|
|
|
# get the author information if it's normal user
|
|
if persons:
|
|
# On the 3 December 2023, /people is still not in the rest API...
|
|
# As the LinkedIn support suggested, we need to use the old endpoint...
|
|
response_json = requests.get(
|
|
"https://api.linkedin.com/v2/people?ids=List(%s)" % ",".join("(id:%s)" % p for p in persons),
|
|
headers=self.account_id._linkedin_bearer_headers(),
|
|
timeout=5).json()
|
|
|
|
for person_id, person_values in response_json.get('results', {}).items():
|
|
person_id = person_id.split(':')[-1][:-1] # LinkedIn return weird format
|
|
person_urn = f"urn:li:person:{person_id}"
|
|
image_id = person_values.get('profilePicture', {}).get('displayImage', '').split(':')[-1]
|
|
images_ids.append(image_id)
|
|
formatted_authors[person_urn] = {
|
|
'id': person_urn,
|
|
'name': self.stream_id._format_linkedin_name(person_values),
|
|
'authorUrn': person_urn,
|
|
'picture': image_id,
|
|
'vanityName': person_values.get('vanityName'),
|
|
'isOrganization': False,
|
|
}
|
|
|
|
if images_ids:
|
|
image_ids_to_url = self.account_id._linkedin_request_images(images_ids)
|
|
for author in formatted_authors.values():
|
|
author['picture'] = image_ids_to_url.get(author['picture'])
|
|
|
|
default_author = {'id': '', 'authorUrn': '', 'name': _('Unknown')}
|
|
for comment in comments:
|
|
comment['from'] = formatted_authors.get(comment.get('actor'), default_author)
|
|
|
|
comments = [self._linkedin_format_comment(comment) for comment in comments]
|
|
if 'comment' in element_urn:
|
|
# replies on comments should be sorted chronologically
|
|
comments = comments[::-1]
|
|
|
|
return {
|
|
'postAuthorImage': self.linkedin_author_image_url,
|
|
'currentUserUrn': self.account_id.linkedin_account_urn,
|
|
'accountId': self.account_id.id,
|
|
'comments': comments,
|
|
'offset': offset + count,
|
|
'summary': {'total_count': response_json.get('paging', {}).get('total', 0)},
|
|
}
|
|
|
|
# ========================================================
|
|
# MISC / UTILITY
|
|
# ========================================================
|
|
|
|
def _linkedin_format_comment(self, json_data):
|
|
"""Formats a comment returned by the LinkedIn API to a dict that will be interpreted by our frontend."""
|
|
created_time = json_data.get('created', {}).get('time', 0)
|
|
data = {
|
|
'id': json_data.get('commentUrn'),
|
|
'from': json_data.get('from'),
|
|
'message': json_data.get('message', {}).get('text', ''),
|
|
'created_time': created_time,
|
|
'formatted_created_time': self.env['social.stream.post']._format_published_date(
|
|
datetime.fromtimestamp(created_time / 1000)),
|
|
'likes': {
|
|
'summary': {
|
|
'total_count': json_data.get('likesSummary', {}).get('totalLikes', 0),
|
|
'can_like': False,
|
|
'has_liked': json_data.get('likesSummary', {}).get('likedByCurrentUser', 0),
|
|
}
|
|
},
|
|
'comments': {
|
|
'data': {
|
|
'length': json_data.get('commentsSummary', {}).get('totalFirstLevelComments', 0),
|
|
'parentUrn': json_data.get('commentUrn'),
|
|
},
|
|
},
|
|
}
|
|
|
|
image_content = next(
|
|
(content for content in json_data.get('content', [])
|
|
if content.get('type') == 'IMAGE'),
|
|
None,
|
|
)
|
|
if image_content:
|
|
# Sometimes we can't access the image (e.g. if it's still being process)
|
|
# so we have a placeholder image if the download URL is not yet available
|
|
data['attachment'] = {
|
|
'type': 'photo',
|
|
'media': {'image': {'src': image_content.get('url', '/web/static/img/placeholder.png')}},
|
|
}
|
|
|
|
return data
|
|
|
|
def _fetch_matching_post(self):
|
|
self.ensure_one()
|
|
|
|
if self.account_id.media_type == 'linkedin' and self.linkedin_post_urn:
|
|
return self.env['social.live.post'].search(
|
|
[('linkedin_post_id', '=', self.linkedin_post_urn)], limit=1
|
|
).post_id
|
|
else:
|
|
return super(SocialStreamPostLinkedIn, self)._fetch_matching_post()
|