Metadata-Version: 2.1
Name: odoo-addon-social_media_linkedin
Version: 17.0.1.0.0.1
Requires-Python: >=3.10
Requires-Dist: odoo-addon-social_media_base>=17.0dev,<17.1dev
Requires-Dist: odoo>=17.0a,<17.1dev
Summary: Publish on the LinkedIn pages you administrate, with daily figures
Home-page: https://github.com/OCA/social
License: AGPL-3
Author: Binhex, Odoo Community Association (OCA)
Author-email: support@odoo-community.org
Classifier: Programming Language :: Python
Classifier: Framework :: Odoo
Classifier: Framework :: Odoo :: 17.0
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
Classifier: Development Status :: 4 - Beta
Description-Content-Type: text/x-rst

.. image:: https://odoo-community.org/readme-banner-image
   :target: https://odoo-community.org/get-involved?utm_source=readme
   :alt: Odoo Community Association

=====================
Social Media Linkedin
=====================

.. 
   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
   !! This file is generated by oca-gen-addon-readme !!
   !! changes will be overwritten.                   !!
   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
   !! source digest: sha256:a5aa91967e5e46d6a13016b9655949a663ccc64c3b9793178beb0a3393432869
   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
    :target: https://odoo-community.org/page/development-status
    :alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
    :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
    :alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github
    :target: https://github.com/OCA/social/tree/17.0/social_media_linkedin
    :alt: OCA/social
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
    :target: https://translation.odoo-community.org/projects/social-17-0/social-17-0-social_media_linkedin
    :alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
    :target: https://runboat.odoo-community.org/builds?repo=OCA/social&target_branch=17.0
    :alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module publishes on the LinkedIn company pages an Odoo user
administrates, and keeps the daily figures of those pages.

Everything it asks LinkedIn for costs a fixed number of calls per
account or per publication, so what it costs does not change whether a
page published once or ten thousand times. Reading back what the page
already published --- importing the publications, their comments and
their reactions --- grows with that history and lives in *Social Media
LinkedIn Sync*, which installs on its own as soon as *Social Media Sync*
is present.

Main features:

- Integration of the LinkedIn company pages (organizations) the user
  administrates; personal profiles are not supported.
- Post creation, with images or a video: LinkedIn publishes either the
  images or the video, never both.
- Daily statistics of the page: the figures LinkedIn reports by day are
  written as a time series, with reports and native graph and pivot
  views over them. It costs a fixed number of calls per account, decided
  by the width of the window asked for and not by what the page
  published: one call for the refresh of the last days, a handful for
  the whole period LinkedIn reports, because the figures asked for are
  those of the whole organization and the URNs of the publications never
  enter the query.
- What LinkedIn will not publish, shown on the post while it is written.
  The message is checked against **3000 characters**, and the medias
  against **20 images** of at most **10 MB** each in JPG, PNG or GIF,
  and **one video** of at most **500 MB** in MP4. A post carrying a
  video is published without its images, so with a video none of the
  image rules apply and the post is warned that only the video goes out.
  The same checks refuse the publication if the post reaches it anyway,
  through an import or an RPC call.

Statistics account
------------------

1. The eye icon: Total number of views, which may include multiple views
   by the same user.

2. The hand icon: the interactions (clicks, likes, comments and shares)
   the page accumulated over the days of the series.

3. The star icon: the engagement of the account, its interactions over
   its impressions, shown as a percentage. The engagement LinkedIn
   reports by day is kept on each row of the time series and read in the
   graph and pivot views; it is never averaged into the card.

   |STATISTICS_ACCOUNT|

.. |STATISTICS_ACCOUNT| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_linkedin/static/img/readme/STATISTICS_ACCOUNT.png

**Table of contents**

.. contents::
   :local:

Configuration
=============

To configure this module, you need to:
--------------------------------------

Please note that you must have a developer account. The steps required
for using it are defined below:

Before creating this developer account, you must have a partner company
on your LinkedIn account and be an administrator.

Only **company pages (organizations)** can be linked, never personal
profiles: a post is always published on behalf of the organization. Odoo
only reads the organizations where the member who authorizes is an
``ADMINISTRATOR`` whose invitation has already been accepted
(``APPROVED`` state). If your role is still pending, LinkedIn does not
return the organization and the wizard ends without creating any
account.

The connector needs no additional Python library: it uses ``requests``,
which is already shipped with Odoo, so there is nothing to install on
the server besides the module itself.

- Go to https://developer.linkedin.com/

- Create a new Developer App with the *Create app* button.

  |BUTTON_CREATE_APP|

- Fill in the requested fields. In the *Privacy Policy URL* field, copy
  your company's URL.

  |FORM_CREATE_APP|

- Once the app is created, go to the *My Apps* menu and you will see the
  newly created app; select it.

- Within the app, in the *Settings* tab, verify your company. You will
  see a button that says Verify. Click it, and in the window that
  appears in the lower left corner, click the *Generate URL* button.
  Copy the generated URL into your browser and accept.

- Then go to the *Products* tab and request access to the products
  granting the scopes the installed modules ask for:

  - Community Management API, which grants the three scopes this
    connector needs: ``rw_organization_admin``,
    ``w_organization_social`` and ``r_organization_social``. It is not
    self-serve: LinkedIn reviews the request, and until it is approved
    **no account can be associated at all** — the consent screen is all
    or nothing, so a scope the App was not granted fails the whole
    authorization, not only the calls that need it.
  - Advertising API, only if *Social Media Advertising LinkedIn* is
    installed, for ``r_ads``, ``rw_ads`` and ``r_ads_reporting``. It is
    not self-serve either.

  |PRODUCTS|

  Note that some products require you to fill out a form; you must do
  so, otherwise, the necessary scopes for basic use of your account will
  not be enabled. The products, the scopes they grant and the versions
  of the API are described in the `LinkedIn Marketing API
  documentation <https://learn.microsoft.com/en-us/linkedin/marketing/getting-started>`__
  and in its `versioning
  guide <https://learn.microsoft.com/en-us/linkedin/marketing/versioning>`__.

- After requesting the aforementioned Products, go to the Auth tab and
  you will see all the enabled scopes at the bottom.

  Each module asks for the permissions its own calls consume, so what is
  requested depends on what is installed:

  +----------------------+---------------------------+----------------------+
  | Module               | Scopes                    | What needs them      |
  +======================+===========================+======================+
  | *Social Media        | ``rw_organization_admin`` | Listing the          |
  | LinkedIn*            |                           | organizations the    |
  |                      |                           | member administers,  |
  |                      |                           | and the figures of   |
  |                      |                           | the page             |
  +----------------------+---------------------------+----------------------+
  |                      | ``w_organization_social`` | Publishing and       |
  |                      |                           | deleting a post, and |
  |                      |                           | uploading its images |
  |                      |                           | and videos           |
  +----------------------+---------------------------+----------------------+
  |                      | ``r_organization_social`` | Reading a            |
  |                      |                           | publication back,    |
  |                      |                           | which is what tells  |
  |                      |                           | one deleted on       |
  |                      |                           | LinkedIn from one    |
  |                      |                           | still online         |
  +----------------------+---------------------------+----------------------+
  | *Social Media        | ``r_organization_social`` | Reading the feed of  |
  | LinkedIn Sync*       |                           | the page, the        |
  |                      |                           | comments and the     |
  |                      |                           | reactions            |
  +----------------------+---------------------------+----------------------+
  | *Social Media        | ``r_ads``, ``rw_ads``,    | The advertising      |
  | Advertising          | ``r_ads_reporting``       | accounts, their      |
  | LinkedIn*            |                           | campaigns and their  |
  |                      |                           | figures              |
  +----------------------+---------------------------+----------------------+

  All of them have to appear enabled in the *Auth* tab of your App; if
  one is missing, LinkedIn refuses the whole authorization and the
  Product that grants it has to be requested. LinkedIn does not let the
  member pick part of them either: the consent screen is all or nothing,
  which is why nothing is requested "just in case".

  What LinkedIn granted is stored on the account, in the *Granted
  Scopes* field of its *Configuration* tab, so a permission error can be
  read there instead of in the logs. That same field is what the
  **next** authorization requests, and it can be edited:

  - Enable the new Product on your LinkedIn App and wait for it to be
    approved.
  - Add the scopes it grants to the *Granted Scopes* field of the
    account.
  - Open *Update account*, tick **Update keys** and authorize again on
    LinkedIn.

  Editing the field on its own changes nothing, and ticking only *Update
  token* is not enough either: refreshing a token keeps the scopes it
  was granted, only a new authorization can ask for more. Go straight to
  *Update account* after editing the field, because *Validate token* and
  any token refresh rewrite it with the scopes LinkedIn reports, and the
  additions typed by hand are lost. Beware that a scope the Products of
  your App do not grant makes LinkedIn refuse the whole authorization,
  and the account cannot be authorized again until it is removed from
  the field.

  Every call to the versioned REST API carries the
  ``LinkedIn-Version: 202607`` and ``X-Restli-Protocol-Version: 2.0.0``
  headers. The OAuth calls of the association — the code-for-token
  exchange and the token introspection — and the download of the
  organization logo do not. LinkedIn retires each version of the API
  about a year after publishing it, so the module has to be updated
  periodically: once the version is no longer supported, LinkedIn
  answers every request with a version error.

- At the top of the aforementioned tab, you will see the Client ID and
  Primary Client Secret information.

- Configure the access points for which you want to use the account.
  Follow these steps:

  - Go to *Settings* > *Technical* > *Parameters* > *System Parameters*.
  - Search for web.base.url
  - Copy the base URL and concatenate it with the endpoint. Then, in
    your LinkedIn Developer Account, on the Authentication tab, in the
    Authorized Redirect URLs for Your App section, add a new item. \*
    Example: web.base.url: http://192.168.1.7:8017 endpoint:
    /linkedin/callback linkedin_url:
    http://192.168.1.7:8017/linkedin/callback

  |CONFIGURE_URL_CALLBACK|

Registering the Client ID and Client Secret. Integration of a user account.
---------------------------------------------------------------------------

- Go to *Social Media* > Configuration > Social Media

- Click on the *Associate Account* button for the desired social media.

  |ASSOCIATE_ACCOUNT|

- A wizard will open for you to add the Client ID and Client Secret
  obtained from your developer account.

  |WIZARD_ASSOCIATE_ACCOUNT|

- By clicking the *Associate* button here, you'll be taken to a LinkedIn
  authentication page. Once you validate your information, LinkedIn
  brings you back to Odoo: if the association succeeded you land
  directly on the *Social Media* Dashboard with a success notification,
  and if it failed you come back to the start screen with the
  notification of the error.

  |AUTHORIZE_ACCOUNT|

- Once you have completed these steps and everything is working
  correctly, you can see your account in *Social Media* > Configuration
  > Accounts

- After the account is associated, the daily figures of the page are
  read and the card of the dashboard is filled. Importing what the page
  already published needs *Social Media LinkedIn Sync*.

- Note that the account credentials (Client ID, Client Secret and
  tokens) are masked and only visible to administrator users.

- The authorization flow is bound to the session that started it: the
  state token sent to LinkedIn only works for the user who opened the
  wizard, and it is discarded once the flow ends, so the same
  authorization cannot be replayed. If the state does not match, the
  association is refused with *Invalid OAuth state token. Please restart
  the account association process.* The flow itself is the one described
  in the `authorization code
  flow <https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow>`__
  of LinkedIn.

- If the LinkedIn organization is already linked to an account of
  another Odoo user, the association is refused and nothing is
  overwritten: only the responsible user of the account and the *Social
  Media / Administrator* group can relink it.

- Every account has to use its own credentials: if the Client ID and the
  Client Secret are already registered on another account, the
  association is refused with *An account with this information already
  exists; please also check archived accounts.* The check includes the
  archived accounts, which keep their credentials, so check the
  *Archived* filter as well before trying again. To reauthorize an
  existing account, use its *Update account* button instead of the
  wizard.

System parameters
-----------------

Nothing here has to be set for the connector to work: each key defaults
to the value the code carries, and it only exists once it is written by
hand in *Settings* > *Technical* > *Parameters* > *System Parameters*.
They are bounded when they are read, so a value outside its range is
brought back into it instead of being obeyed.

+-----------------------------------------------+---------+-------------------+-------------------+
| Parameter                                     | Default | Unit              | Bounds            |
+===============================================+=========+===================+===================+
| ``social_media_linkedin.video_poll_attempts`` | 30      | polls             | at least 1, and   |
|                                               |         |                   | at most what the  |
|                                               |         |                   | 600 second        |
|                                               |         |                   | ceiling allows    |
+-----------------------------------------------+---------+-------------------+-------------------+
| ``social_media_linkedin.video_poll_delay``    | 2       | seconds between   | at least 1        |
|                                               |         | polls             |                   |
+-----------------------------------------------+---------+-------------------+-------------------+

The two are the wait LinkedIn needs to finish processing an uploaded
video before the post can be published, and what matters is their
product: it is time the publication spends inside its own transaction.
Past the ``limit_time_real`` of the deployment — 120 seconds by default,
and the scheduled actions inherit it — the worker is killed with the
video already uploaded on LinkedIn and nothing published in Odoo. That
is why the number of polls is cut down to what 600 seconds allow at the
delay in force, and why raising the wait for a long video means raising
``limit_time_real`` as well. A delay longer than the ceiling itself is
not cut: it becomes one single wait, and setting one is asking for the
worker to be killed.

Below their bounds the numbers stop making sense rather than merely
being small: zero polls publishes nothing without ever asking LinkedIn,
and a delay under a second turns the wait into a burst against an API
whose rate limit the module does not manage.

.. |BUTTON_CREATE_APP| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_linkedin/static/img/readme/BUTTON_CREATE_APP.png
.. |FORM_CREATE_APP| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_linkedin/static/img/readme/FORM_CREATE_APP.png
.. |PRODUCTS| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_linkedin/static/img/readme/PRODUCTS.png
.. |CONFIGURE_URL_CALLBACK| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_linkedin/static/img/readme/CONFIGURE_URL_CALLBACK.png
.. |ASSOCIATE_ACCOUNT| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_linkedin/static/img/readme/ASSOCIATE_ACCOUNT.png
.. |WIZARD_ASSOCIATE_ACCOUNT| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_linkedin/static/img/readme/WIZARD_ASSOCIATE_ACCOUNT.png
.. |AUTHORIZE_ACCOUNT| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_linkedin/static/img/readme/AUTHORIZE_ACCOUNT.png

Usage
=====

List of posts generated from Odoo.
----------------------------------

Only posts generated using Odoo are displayed.

- Go to *Social Media* > Posts
- Opening a publication, from its form or from its card on the
  dashboard, reads it on LinkedIn first. One that was deleted there is
  reported as *The post does not exist or has been deleted.* and marked
  as *Deleted* on the spot, keeping its reference. Only a ``404`` counts
  as a deletion: a lost page role or a throttled application leaves the
  publication alone, because a publication is not gone just because Odoo
  could not read it.

Generate a post.
----------------

This feature acts as a template for generating multiple posts from a
single view, depending on the selected accounts.

- Go to *Social Media* > Posts > New or Go to *Social Media* > Dashboard
  > Add Post
- Fill in the required fields
- When the post is created, every LinkedIn account of the active company
  is selected by default in *Accounts*; remove the ones you do not want
  to use before publishing. |CREATE_POST|
- Save
- Click on the *Post* button
- LinkedIn publishes either images or a video, never both: when the post
  carries a video, its images are left out. The preview of the post says
  so by showing the video alone, so what is previewed is what LinkedIn
  receives, and a banner on the form explains it as well.
- A publication shares the images and the video of its post, so the
  dashboard card shows them as soon as LinkedIn accepts the post and
  nothing else has to run. What LinkedIn made of each media is recorded
  on the publication, which is what lets an image deleted on LinkedIn
  leave the card while the one in the post stays; noticing that deletion
  needs *Social Media LinkedIn Sync*.

Update token, client ID, client Secret and organization data
------------------------------------------------------------

- Go to *Social Media* > Configuration > Accounts

- Select the account

- Click on the *Update account* button

  |BUTTON_UPDATE_ACCOUNT|

- In the wizard that appears, if none of the checkboxes are selected and
  the *Update* button is pressed, the system will update only the
  organization's data.

- If the *Update keys* checkbox is selected, the current Client ID is
  proposed to the administrator users, the only ones allowed to read it,
  and the Client Secret has to be typed again: the stored secret is
  never sent to the browser. Authentication is then performed again
  through LinkedIn, and the keys are only written on the account once
  LinkedIn has accepted them, so an authorization left halfway keeps the
  credentials that still work.

  |UPDATE_KEYS|

- Selecting the *Update token* checkbox will update the current token.

  |UPDATE_TOKEN|

Validate the token
------------------

- Go to *Social Media* > Configuration > Accounts

- Select the account and open the *Configuration* tab

- Click on the *Validate token* button. It always asks LinkedIn whether
  the token is still active, because a token can be revoked there long
  before the stored expiry dates: a notification confirms that it is
  valid, and if it is not, the token is renewed. Outside the renewal
  window, the check made before every call to LinkedIn uses the stored
  dates, so it costs no extra request.

  |VALIDATE_TOKEN|

Archive Account Linkedin
------------------------

- Go to *Social Media* > Configuration > Accounts

- Select the account and pick *Archive* in the *Actions* menu of the
  form.

  |ARCHIVE_ACCOUNT|

- Please note that all data associated with this account will be
  archived.

- If you associate the same LinkedIn account again later, the archived
  account and its related data will be reactivated instead of creating a
  duplicate.

- An archived account can be deleted permanently with the *Delete
  permanently* button, only available to a social media administrator.
  The LinkedIn publications stay online, only the Odoo history is
  removed.

Uninstalling the module
-----------------------

Uninstalling *Social Media Linkedin* does not delete the accounts nor
their publication history:

- The access token and the refresh token are cleared, so no credential
  outlives the module.
- The LinkedIn accounts are archived, together with their posts.
- The LinkedIn specific data is lost, because Odoo drops the columns of
  an uninstalled module: the application Client ID and Client Secret.
- The identifier of each account and publication on LinkedIn is kept, so
  installing the module back and associating the account again
  reactivates the archived history and updates it, instead of importing
  everything as duplicated records.

Time series of the account
--------------------------

- LinkedIn is asked for its figures **by day**, with the
  ```organizationalEntityShareStatistics`` <https://learn.microsoft.com/en-us/linkedin/marketing/community-management/organizations/share-statistics>`__
  finder and ``timeGranularityType=DAY``, and every bucket it answers
  becomes one row of *Social Media* > Statistics. Nothing is invented
  for the days it reports nothing for.
- Right after the account is linked, the rewrite window is read and the
  whole period LinkedIn reports is asked for straight after, so the card
  of the dashboard is filled with the year and not only with the last
  days. No synchronization module is needed for that.
- The finder answers at most 100 daily buckets per call and does not
  paginate, so a period wider than that is asked for in several calls,
  one after another, instead of coming back cut at the hundredth day.
- The whole period is read only once. It is then skipped, because the
  rewrite window is the only thing written on every pass and a row older
  than it can only come from that first reading. If it failed --LinkedIn
  refused the call, the token had just been issued-- the *Rebuild
  statistics history* button of the account form asks for it again.
- The last week is asked for again on every pass of the two-hourly
  check, and by the *Update statistics* button of the account form.
  LinkedIn revises figures of days already past, so the recent ones are
  rewritten instead of being trusted as final; the days before that
  window stay as they were left.
- The *Rebuild statistics history* button asks LinkedIn for the daily
  figures of the page again, as far back as it reports them, and
  rewrites the time series with them. It **does not import
  publications**: what it rebuilds is the graph of the account. Bringing
  in the publications themselves is what *Social Media LinkedIn Sync*
  does.
- An account whose statistics LinkedIn refuses is reported and skipped,
  and the rest of the accounts keep the rows already written for them.
- The account is asked for its organization page as a whole, which
  includes what was published before Odoo managed the account or outside
  of it. The figures are therefore not the sum of what Odoo imported and
  are not meant to be compared against it.

Figures of a publication
------------------------

- The figures of the publications of the last 30 days are read back from
  LinkedIn once a day, by the *Social: Refresh the statistics of the
  recent publications* scheduled action, and on the spot by the *Update*
  button of the dashboard and *Update statistics* of the account form.
- Nothing walks the feed to do it: Odoo already knows the URN of every
  publication it is asking about, so a whole page of them is answered by
  **at most three calls** — ``organizationalEntityShareStatistics`` once
  for the shares and once for the UGC posts, and ``socialActions`` for
  the likes and the comments of the UGC posts, which LinkedIn documents
  as the up-to-date ones.
- Only the kinds the page really carries are asked for. The publications
  Odoo publishes are shares, so a page of them costs **one call**: the
  finder answers their likes and comments inside the same block, and
  ``socialActions`` is only asked about UGC posts, which are the
  publications made outside Odoo.
- Those URNs travel in the query string of a finder LinkedIn documents
  as not paginated, so a page whose URNs do not fit in 4 KB is asked for
  in as many calls as it takes, around 95 publications each. That is the
  only thing that adds a call to the three.
- A publication missing from the answer is one nobody interacted with:
  the finder leaves out the entities with no activity at all, so its
  figures are written as zeros and its date as read all the same.
- No new permission is needed: the figures of a publication come from
  ``organizationalEntityShareStatistics`` and ``socialActions``, which
  the scopes already requested when the account was associated cover, so
  an account already associated is not asked to authorize anything
  again.
- Reading the publications LinkedIn has and Odoo does not is another
  matter, and it stays in *Social Media LinkedIn Sync*: that one costs
  one call per page of the feed.

LinkedIn tokens
---------------

- The access token of LinkedIn lasts two months and its refresh token a
  year, as stated in the `refresh tokens
  documentation <https://learn.microsoft.com/en-us/linkedin/shared/authentication/programmatic-refresh-tokens>`__.
  Within the week before the expiry date, and on every run of the
  scheduled action *Social: Checking social media updates* and before
  publishing, Odoo asks LinkedIn whether the token is still active
  (``introspectToken`` endpoint) and only renews it when LinkedIn
  answers that it is not. Outside of that window the check is answered
  with the stored dates and costs no request. Nothing has to be done for
  a post planned weeks ahead.
- If LinkedIn refuses the token anyway, it is renewed and the
  publication is sent again straight away.
- Once the refresh token expires, or the authorization is revoked from
  LinkedIn, no renewal is possible: the account shows the update warning
  and has to be authorized again with *Update account*, which is the
  only step that needs the browser.

LinkedIn limits and validations
-------------------------------

What LinkedIn refuses is checked in Odoo before the publication is sent.
The post shows the reason while it is being written, saving is never
blocked, and the publication of the account that raises the objection is
refused instead of being sent and failing on LinkedIn. The same checks
are applied when the post reaches the publication through an import or
an RPC call, so nothing gets past them.

- The message is checked against **3000 characters**, the length the
  ``commentary`` field of the `Posts
  API <https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api>`__
  takes. A longer message is reported on the post and the publication is
  not sent.
- The images are checked against **20** per post, against the formats
  LinkedIn publishes, **JPG, PNG and GIF**, and against **10 MB** each.
  One image is published as a single image and two or more as a
  `multi-image
  post <https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/multiimage-post-api>`__.
  The message names the files that cannot be published.
- The video is checked against **one** per post, against **MP4** and
  against **500 MB**.
- A post carrying images and a video is not refused: LinkedIn publishes
  the video and leaves the images out, so the post is told what goes out
  instead of being stopped. That is also why the image rules are not
  applied at all to a post carrying a video — its images decide nothing.
- The file picker only filters what the browser proposes, so a file
  added by drag and drop reaches the checks all the same, and they are
  what refuses the ones LinkedIn does not take.
- What is left to LinkedIn is everything about the content itself: the
  dimensions and the aspect ratio of an image, the codecs and the
  duration of a video, the text it reads as spam. Nothing of that is
  checked here, and a publication LinkedIn refuses for one of those
  reasons is left as *Failed* with the validation error it answered.
- Every call to LinkedIn has a timeout of **10 seconds**, not
  configurable. If LinkedIn or the connection take longer, the operation
  fails with *Error connecting to LinkedIn* and has to be retried; a
  publication is left as *Failed* and can be sent again with the *Post*
  button.
- An account without an access token does not publish: this is what
  happens after uninstalling and installing the module back, which
  clears the credentials. The line is left as *Failed* stating that the
  account has no access token, and the account shows the update warning.
  Authorize it again with *Update account*.
- Recommending a publication or one of its comments, and reading those
  comments back, is served by *Social Media LinkedIn Sync*: this module
  draws none of those buttons and never calls the Reactions API.
- Deleting a publication from the dashboard deletes it on LinkedIn
  first. If LinkedIn does not confirm the deletion, the operation is
  cancelled with *Error deleting LinkedIn post* and the record is kept
  in Odoo, so the two sides never get out of sync.

Video upload
------------

- LinkedIn decides how a video is split: ``initializeUpload`` answers
  one instruction per part, of 4 MiB each except the last one, and every
  part is uploaded with its own request. The identifiers LinkedIn
  returns for the parts are sent back to ``finalizeUpload`` in the same
  order, so the video is put together as it was cut. A 22 MB video takes
  6 parts, and publishing then waits for LinkedIn to finish processing
  it, up to 30 polls two seconds apart.

  https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/videos-api

- A post publishes a single video, so a post carrying several is refused
  before uploading anything: LinkedIn would only keep the first one and
  the others would be transferred and processed for nothing.

- LinkedIn processes an uploaded video before it can be published, so
  publishing a post with a video waits until the video is available: 30
  attempts every 2 seconds, which a long video may need more than. Both
  numbers are system parameters, described in CONFIGURE.

- The publication shares the video of its post the same way it shares
  its images, and what LinkedIn made of it is recorded in its media
  references. The camera icon without a count is the fallback drawn for
  a publication that carries only the *has video* flag — one imported
  from LinkedIn, whose video was never downloaded and can only be
  watched there.

Publishing options
------------------

- Every publication is created **public**, in the main feed, with no
  targeting by country, language or industry, and letting it be shared:
  ``visibility``, ``feedDistribution``, ``targetEntities`` and
  ``thirdPartyDistributionChannels`` are fixed in the code.

- Scheduling is not delegated to LinkedIn either: the Odoo scheduled
  action is the one that publishes when the date arrives.

  https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api

Errors reported by LinkedIn
---------------------------

- When LinkedIn rejects a request because of its content, its answer
  only says that the validation failed and lists the rejected fields
  apart, in the shape described in its `error handling
  guide <https://learn.microsoft.com/en-us/linkedin/shared/api-guide/concepts/error-handling>`__.
  Those explanations are the ones shown in Odoo, one per line, so the
  message names the field and the rule that was broken instead of the
  generic summary.

.. |CREATE_POST| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_linkedin/static/img/readme/CREATE_POST.png
.. |BUTTON_UPDATE_ACCOUNT| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_linkedin/static/img/readme/BUTTON_UPDATE_ACCOUNT.png
.. |UPDATE_KEYS| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_linkedin/static/img/readme/UPDATE_KEYS.png
.. |UPDATE_TOKEN| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_linkedin/static/img/readme/UPDATE_TOKEN.png
.. |VALIDATE_TOKEN| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_linkedin/static/img/readme/VALIDATE_TOKEN.png
.. |ARCHIVE_ACCOUNT| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_linkedin/static/img/readme/ARCHIVE_ACCOUNT.png

Known issues / Roadmap
======================

Publishing options are not configurable
---------------------------------------

- The visibility, the feed distribution, the targeting by country,
  language or industry and the third party distribution are fixed in the
  code, so a publication cannot be restricted nor targeted from Odoo.
  Offering them means exposing them on the post and validating the
  combinations LinkedIn accepts.

  https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api

Duration, codecs, dimensions and aspect ratio of a video are not checked
------------------------------------------------------------------------

- Odoo does not read them before uploading: those limits are the ones of
  the `Videos
  API <https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/videos-api>`__
  and LinkedIn applies them while processing, so a video that breaks one
  of them is transferred whole and rejected afterwards, in the
  processing phase, with *LinkedIn could not process the video*. The
  size, 500 MB, and the MP4 format are checked in Odoo, on the post
  while it is written and again before the publication is sent.

Rate limits are not handled
---------------------------

- The module does not handle the `throttle
  limits <https://learn.microsoft.com/en-us/linkedin/shared/api-guide/concepts/rate-limits>`__
  of LinkedIn, applied per day and per application. When LinkedIn
  answers with a limit error, the operation is recorded as failed like
  any other error and has to be retried later by hand; only a credential
  rejection triggers an automatic retry, and only once.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/social/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/social/issues/new?body=module:%20social_media_linkedin%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* Binhex

Contributors
------------

- `Binhex <https://www.binhex.cloud>`__:

  - Edilio Escalona Almira e.escalona@binhex.cloud
  - Jorge Elena Poblet j.elena@binhex.cloud

Other credits
-------------

This work has been funded by AEOdoo (Asociación Española de Odoo -
https://www.aeodoo.org)

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
   :alt: Odoo Community Association
   :target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

.. |maintainer-edescalona| image:: https://github.com/edescalona.png?size=40px
    :target: https://github.com/edescalona
    :alt: edescalona

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-edescalona| 

This module is part of the `OCA/social <https://github.com/OCA/social/tree/17.0/social_media_linkedin>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
