Metadata-Version: 2.1
Name: odoo-addon-social_media_sync
Version: 17.0.1.0.0.2
Requires-Python: >=3.10
Requires-Dist: odoo-addon-social_media_base>=17.0dev,<17.1dev
Requires-Dist: odoo>=17.0a,<17.1dev
Summary: Import posts, figures, comments and reactions from the social media
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 Sync
=================

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

.. |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_sync
    :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_sync
    :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 brings back into Odoo what an account already published on
its social media: the posts themselves, the figures each of them
collected, and their comments and reactions.

It is separate from *Social Media Base* because of what it costs. Base
asks the social media for a fixed number of things per account —
publish, delete, the daily series of the page, the figures of the
publications of the last 30 days — and that number does not change
whether the account published once or ten thousand times. Everything
whose cost grows with the history of the account lives here: one call
per page of posts, one call per publication to check it is still there,
one call per comment thread. An installation that only writes and
publishes does not have to pay for any of it.

*Social Media Base* never depends on this module nor calls into it.
Where base needs something only the synchronization knows how to do, it
declares an empty hook and carries on, so base works installed alone.

Main features:

- Import of the posts an account already published, and of the figures
  each of them collected however old it is. Base reads back the
  publications of the last 30 days on its own and draws them in the
  *Statistics* dialog; what this module adds are the views that span the
  whole history — the list of publications, its search filters and the
  ordinary form — and the likes and the comments the footer of a
  dashboard card draws for the social media whose connector reports
  them. A publication older than that window only carries figures once
  this import has run.
- Initial synchronization right after an account is linked, which
  imports what the account already published. A monthly cron picks up
  the accounts still waiting for it, and that same pass asks for the
  daily statistics series again for an account whose history could not
  be read when *Social Media Base* filled it at association.
- A weekly full resynchronization, the only pass that notices a post
  deleted on the social media side.
- Verification that a publication still exists remotely, for the ones
  nobody opens: the weekly pass asks the social media about them, and a
  reaction or a comment answered with a *not found* has the publication
  itself asked about before the line is marked. Asking about the one
  publication a user opens is *Social Media Base*'s and costs the same
  call with or without this module.
- Comment thread of a publication, read from the dashboard: a comment is
  written under the account of the publication, a comment is answered
  where the social media serves the replies, a comment of the thread is
  deleted after a confirmation, and *Recommend* toggles the reaction of
  the account on the publication and on each of its comments. Which of
  them a social media really serves is declared by its connector, and
  only then is the entry offered.

This module implements the API of no social media in particular: it
brings the scheduled actions, the frontend and the common interface, and
a synchronization connector is what implements the calls for one social
media. The only thing it fetches on its own is the media of an imported
publication, from the URL the connector hands it.

**Table of contents**

.. contents::
   :local:

Installation
============

This module depends on *Social Media Base* and is not installed with it:
an Odoo that only writes and publishes does not need it, and the whole
point of having it apart is not paying for what it does.

It brings nothing of its own for a social media in particular. A
synchronization connector is what implements the calls for one of them,
the same way *Social Media Linkedin* or *Social Media X* implement
publishing. Installed without one, the scheduled actions run, find
nothing to ask anybody, and leave everything as it is.

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

Nothing has to be configured for this module to work: it uses the
accounts, the credentials and the groups of *Social Media Base*.

What is worth reviewing is the two scheduled actions it adds, in
*Settings / Technical / Automation / Scheduled Actions*:

- *Social: Initial sync of the new accounts* runs monthly, and is also
  triggered on the spot every time an account is linked. It only picks
  up the accounts still waiting for that first import, so the monthly
  run is a safety net rather than the normal path.
- *Social: Full resync of the accounts* runs weekly. It is the only pass
  that notices a publication deleted on the social media, and the most
  expensive one: it reads every publication of every account, one call
  per page. Making it run more often is what turns a deletion noticed a
  few days late into a quota problem.

Both intervals are the ones to move if the social media of an account is
strict about quotas.

The retention of the downloaded medias is one system parameter, in
*Settings / Technical / Parameters / System Parameters*, which only an
administrator reaches: ``social_media_sync.media_max_age_days``. The
module installs it at ``0``, so it is there to be found, and zero is no
policy at all: no media is ever released.

A system parameter holds text, and this one is read as a number of days.
A value that cannot be read as one — a word — is taken as no policy and
leaves a warning in the log; an empty value, zero and any negative
number are no policy too and are not worth a warning. No media is
released in any of those cases.

Written as a positive number of days, the daily vacuum releases the
images and videos this module downloaded for the imported publications
older than that, and the files are deleted a day later. One run reaches
a thousand of the aged publications that still hold medias. A
publication already released leaves the next run instead of taking the
place of another one, so a database holding more aged publications than
that is released in full over the following runs. Two things to weigh
before writing a number:

- *What is lost* is the media itself. The card of an aged publication is
  drawn with no image and no placeholder in its place. The link to the
  social media, where the media still is, stays.
- *What is kept* is what each social media made of that media. The next
  synchronization pass knows the publication already had it and does not
  ask for it again, so the policy frees the disk once instead of paying
  for the same bytes every week.

Only the publications imported from a social media are reached. The
medias of a post published from Odoo are editorial content and are never
aged out, whatever the age of the post.

The size of each downloaded media is capped by a second system
parameter, in the same place: ``social_media_sync.media_max_size_mb``.
The module installs it at ``100``, in megabytes, and an update of the
module does not overwrite what an administrator wrote in it. A media is
held whole in memory while the import runs, and the cap is what keeps
one large video from exhausting it.

A media larger than the cap is not downloaded. When the social media
announces the size of the file, the download stops before reading it;
when it does not, it stops as soon as what was read goes past the cap.
The publication is imported without that media, and a warning in the log
names the media, its size and this parameter. As the publication does
not hold that media, every synchronization pass asks for it again and
stops at the cap again, until the parameter is raised above its size. A
long video can well be larger than ``100`` megabytes, so an account
publishing them is the one to raise it for.

Zero is no cap at all, and so are an empty value, any negative number
and a parameter that was deleted: every media is downloaded whatever its
size, and nothing is logged. A value that cannot be read as a whole
number of megabytes — a word, a decimal — does not remove the cap: it is
taken as ``100`` and leaves a warning in the log.

Usage
=====

Importing what an account already published.
--------------------------------------------

- Right after an account is linked, its publications and their
  statistics are imported: the scheduled action *Social: Initial sync of
  the new accounts* is triggered on the spot and the dashboard shows the
  account as syncing until it is over.
- If that first import fails, the account stops waiting for it and is
  **not** retried on its own: press the *Update* button of the dashboard
  to import it again. The reason is left in the chatter of the account,
  because the scheduled action runs with nobody connected to be
  notified.
- If the import loses a race against another update of the same account,
  it keeps the account waiting and asks the scheduled action to come
  back a few minutes later. Unlike a web request, a scheduled action
  gets no retry of its own.
- If the social media does not let the import run —the requests of the
  plan are spent, and the account gets a notice saying so— nothing is
  brought in and nothing is recorded as a failure: the account keeps
  waiting and the scheduled action is asked to come back a few minutes
  later. The *Update* button of the dashboard behaves the same way, and
  only stops announcing the first import once the social media was
  really read.
- Publications created outside of Odoo are the only ones whose medias
  the import has to download, and so the only ones whose images appear
  on the dashboard after it rather than before: a publication sent from
  Odoo already shares the medias of its post.
- The time series of the account is filled backwards when the account is
  linked, by *Social Media Base*, as far back as the social media
  answers by day; the first import only asks for it again when that fill
  could not be read then. How far back that is belongs to the social
  media, not to Odoo, so two accounts may well start with a different
  depth of history.
- Afterwards, the *Update* button of the dashboard imports again on
  demand. Without this module that button refreshes the daily series of
  the account and the figures of the publications of the last 30 days;
  with it, the same press also imports the publications.
- Pressed with no account picked, the button imports only the accounts
  known to be behind: the ones announcing publications to import and the
  ones whose first import never ran. Pressed on a single account, it
  imports that account whatever it announces. The figures of every
  account are refreshed either way, because they cost a fixed number of
  calls and they move without anything being published; it is the import
  whose cost grows with the history of the account.
- When nothing needed importing, the button says so —*The data was
  updated. No new publications.*— instead of announcing publications it
  did not bring in.
- The figures imported for a publication — impressions, social media
  clicks, shares, likes, comments, interactions and engagement — are
  added by this module to the list of publications and to their form,
  which span the whole history. The *Statistics* dialog of a card
  belongs to *Social Media Base*, which reads those figures back for the
  publications of the last 30 days on its own; without this module the
  list and the form show only the tracked clicks, counted by the link
  tracker.
- The totals a post adds up from its publications — likes, comments,
  clicks, shares, interactions and engagement — are drawn by this module
  on the list of posts, and Clicks, Interactions and Engagement on the
  kanban card of a post; without it those columns and that card carry a
  zero nothing can turn into a number.

What each notice on a card announces.
-------------------------------------

Three different things can be pending on an account, and each one has
its own notice and its own way out. They are independent: an account may
be carrying one, two or the three of them at once, and none of them says
anything about the others.

- **The credentials expired.** The warning of *Social Media Base*. Only
  a new authorization takes it down, and the *Update* button does not.
- **The first import is running.** Drawn by this module while the
  scheduled action brings in what the account had already published.
- **There are publications to import.** Added by this module when the
  check for updates finds that the account moved on the social media.
  The *Update* button is what resolves it, and the notice goes away on
  its own once the import is over, without the page being reloaded.

Only a social media whose API can tell that an account moved without
reading its publications raises the third one. Where it cannot —reading
the timeline *is* the import— nothing is announced between two runs, and
those accounts are imported on every pass instead.

Noticing what was deleted on the social media.
----------------------------------------------

- The ordinary import asks the social media only about what it needs,
  which is what keeps it affordable on an account with thousands of
  publications. What it cannot notice that way is that a publication was
  **deleted** on the social media: nothing is left to ask about.
- The scheduled action *Social: Full resync of the accounts* reads
  everything again once a week and reconciles it, and each connector may
  also offer to run it on demand from the account form. A publication
  deleted on the social media may therefore take up to a week to be
  reported as such.
- A publication found gone is marked as *Deleted* and keeps its
  reference on the social media: detection is not infallible, so a line
  wrongly marked can be recognised and restored by the next full pass.
- Opening a publication already asks the social media without this
  module, so a deletion the user runs into is marked right away either
  way. What this module adds is noticing the ones nobody opens.

Comments and reactions.
-----------------------

- Commenting a publication from the dashboard publishes the comment on
  the social media, under the account of the publication, which is what
  the composer announces.
- Answering a comment moves the composer under it, so the reply is
  written where it will be read. Once the reply is published the
  composer returns to the head of the dialog; only a reply the social
  media rejected keeps the aim, so the retry starts under the same
  comment. Pressing the entry again hands the composer back to the head
  of the dialog, which also holds it while the answered comment is not
  on the list.
- A comment is answered where the social media serves the replies. Where
  the whole thread already arrives with the comments, the replies are
  nested from what is already on screen and nothing else is asked for.
- *Recommend* is offered both on the publication and on each of its
  comments, and is sent under the same account. It is only shown where
  the social media supports it on comments: a connector that recommends
  them declares itself, and the entry is not rendered for the
  publications of the ones that do not.
- The entry is a toggle: a social media holds one reaction per account,
  so it draws what the account already recommended and pressing it there
  withdraws the reaction. A social media that could not be read leaves
  the entry as it was drawn instead of claiming one thing or the other.
- A reaction or a comment that fails with a *not found* does not mark
  the publication as deleted on its own: the publication is asked about
  first, because a social media answers the same for a reference it does
  not recognise and for a lost permission.

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

``actor_urn``
-------------

``social.post.account.actor_urn`` holds who the social media says
published a publication: the organization page on LinkedIn, the author
of the tweet on X. The import is what fills it, and no view shows it, so
nothing reads it back yet. It looks like it belongs to the reactions,
which take the actor performing them as an argument, and it is the kind
of field the family either starts using or drops.

Storage of the imported medias
------------------------------

The import downloads the medias of every publication it brings in and
stores them as ordinary ``ir.attachment`` records, so the filestore
grows with the history of the accounts and not with what is published
from Odoo: an account importing years of publications brings in years of
images.

An imported publication has no post to share attachments with, so
nothing is shared here: one image of one publication is one attachment,
and the same image published on two accounts is imported twice.
``media_refs`` does not change that, and is not there for it: it is what
tells the next synchronization which references this publication already
holds, so that a media is downloaded once and not on every pass. The
bytes of two identical images still land on the same file, because the
filestore keys its files by the hash of their content; what multiplies
is the rows.

What ages them out is one number for the whole database.
``social_media_sync.media_max_age_days`` reaches every imported
publication older than it, whatever its account, so there is no way to
keep the medias of one account and age out those of another, and a
publication kept only for its figures still costs its images until that
age is reached. The deletion itself belongs to the vacuum:
``_gc_aged_post_medias`` releases what the policy reaches, the next
synchronization releases what the social media no longer serves, and
``_gc_lost_media_attachments`` deletes both a day later. Serving those
bytes from somewhere else is configured at the level of Odoo, through
``ir_attachment.location``, not from here.

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_sync%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

- `Tecnativa <https://www.tecnativa.com>`__:

  - Pedro M. Baeza pedro.baeza@tecnativa.com

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_sync>`_ project on GitHub.

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