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

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()