Metadata-Version: 2.1
Name: odoo-addon-social_media_x_sync
Version: 17.0.1.0.0.1
Requires-Python: >=3.10
Requires-Dist: odoo-addon-social_media_sync>=17.0dev,<17.1dev
Requires-Dist: odoo-addon-social_media_x>=17.0dev,<17.1dev
Requires-Dist: odoo>=17.0a,<17.1dev
Summary: Import the X timeline, its figures and the conversation of each post
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 X Sync
===================

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

.. |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_x_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_x_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 X account already published:
the posts of its timeline, the figures each of them collected, and their
comments.

It is the X half of *Social Media Sync*, split from *Social Media X*
along the same line: what a call costs. The connector asks X for a fixed
number of things per account — link it, refresh its card, publish,
delete, read the figures of the publications of the last 30 days, read
the single publication that is being opened — 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:
the timeline that answers up to a hundred publications and one download
per media not stored yet, and up to five calls per publication whose
comments are read, one per page of the conversation.

An Odoo that only publishes on X installs *Social Media X* alone and
pays for none of it.

Main features:

- Import of the publications of the timeline, with their photos, videos
  and animated GIFs, and of the figures each of them collected, on
  demand, through the scheduled actions of *Social Media Sync* and
  through the check for updates of *Social Media Base*, which on X
  imports instead of flagging the account.
- Comments of a publication, read from the dashboard: the conversation
  is walked page by page up to a ceiling, and the replies of a comment
  are nested from what was read instead of being asked for apart. A
  comment answers the publication, and answering a comment answers that
  comment, which on X is a post like any other.
- *Recommend* on a publication and on each comment and reply of its
  thread, which gives or withdraws the like of the account on X. Which
  ones the account already liked is read from its latest likes on X,
  when the account is imported and when the thread is opened.
- *Delete* on the comments the account wrote, which deletes them on X: X
  only deletes the posts of the account that asks, so the entry is not
  offered on the comments of other people.
- Reading only what was published after a chosen publication, with
  *Enable since*, for an account that does not need its whole history
  read again.

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

The figures of the account are the sum of the publications Odoo holds
for it. A publication deleted on X is marked *Deleted* and keeps the
figures of the last read, so it goes on counting; the figures only fall
when the record itself is removed in Odoo.

The figures of the card are the sum of every publication of the account
stored in Odoo, with no window and no ceiling. What the hundred
publications of one timeline page limit is how far back an import
reaches, and what the 30-day window of *Social Media X* limits is which
publications get their figures read back from X; anything older keeps
the last figures that were read for it and keeps being counted. Only
original publications are stored and counted: retweets, replies and
quotes are discarded on import.

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

2. The hand icon: the interactions (likes, comments, retweets and
   quotes) of every publication stored for the account, a publication
   deleted on X included, which keeps the figures of the last read. They
   are the `metrics <https://docs.x.com/x-api/fundamentals/metrics>`__ X
   returns for a post.

3. The star icon: the engagement of the account, the interactions of its
   publications over their impressions. X reports no rate of its own, so
   it is derived from those counters, and it counts the retweets and the
   quotes as interactions like any other.

   |STATISTICS_ACCOUNT|

If the X API `rate
limit <https://docs.x.com/x-api/fundamentals/rate-limits>`__ is reached
while refreshing the statistics, the last metrics stored on the account
are kept instead of failing.

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

**Table of contents**

.. contents::
   :local:

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

This module is installed on its own as soon as *Social Media Sync* and
*Social Media X* are both present, which is what a bridge module is for:
the choice to synchronize was already made by whoever installed *Social
Media Sync*, and this only keeps an X account from being left without
the half that belongs to it.

Installing *Social Media X* alone is a valid installation: the account
is linked, publishes and deletes, reads back the figures of the
publications of the last 30 days, and marks as *Deleted* the publication
that is opened after being deleted on X.

It adds no Python dependency of its own: it asks the connector for the
tweepy client and never builds one.

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

Nothing has to be configured for this module: it reads X with the
credentials the account already holds, which are configured in *Social
Media X*.

What those credentials need is an App able to spend against the API, the
same one the connector requires: the timeline of the user, the recent
search, the likes of the account and the deletion of its comments, which
this module spends, are X API v2 endpoints, so a developer App outside a
Project answers ``403`` to them as it does to the rest. See the
configuration of *Social Media X* and `the pricing of the X
API <https://docs.x.com/x-api/getting-started/pricing>`__.

This module is where the pay-per-use bill of an X account is decided.
Reading a publication of the account costs $0.001, but the replies the
comments dialog reads are posts of other people, at $0.005 each, and it
asks for up to five pages of a hundred while it stays open, refreshing
itself every two minutes. The 24-hour deduplication of X charges the
same reply once however many times it is read, so what is paid is the
replies that are new, not the dialog being open; even so, a busy thread
is the most expensive thing the modules do against X. The *Enable since*
date below is the lever: nothing before it is ever read.

Opening the dialog also reads one page of the latest likes of the
account, up to a hundred posts charged one by one, and so does every
import of the account; the refreshes of the dialog do not read it again.
The usage explains what that page shows and what it does not.

Each comment or reply sent from the dashboard, whether X accepts it or
not, each *Recommend* pressed on the card of a publication, and each
comment deleted from the dashboard once X has deleted it, reads the
account again: the dashboard then runs for the account of the
publication what its *Update* button runs, the read of the figures of
the recent publications of *Social Media X* and another read of the
timeline by this module, with its page of likes. Those reads are charged
like any other.

The videos and animated GIFs of the timeline are downloaded whole, and
each file is held to the size cap of *Social Media Sync*, the system
parameter ``social_media_sync.media_max_size_mb``. A long video can be
larger than what it allows, so an account publishing them is the one to
raise it for. Leaving the videos and the animated GIFs out of Odoo
altogether, while the photos are still downloaded, is what
``social_media_sync.download_videos`` written as ``False`` does. See the
configuration of *Social Media Sync* for both.

Enable since
------------

- Go to *Social Media* > Configuration > Accounts

- Select the account

- Select *Enable since*

- The *Post since* field is then enabled, allowing you to select the
  post to start the search for in the next post retrieval. The import no
  longer reads what is older than that publication, but the figures of
  the publications of the last 30 days are read back all the same by
  *Social Media X*; older than that, each publication keeps the last
  figures that were read for it.

  |ENABLE_SINCE|

Scheduled actions
-----------------

The passes over an X account are the ones *Social Media Sync* declares,
plus two of *Social Media Base*: the check for updates and the daily
refresh of the figures of the recent publications.

- *Social: Checking social media updates*, every 2 hours, reads the
  timeline of every X account. X has no cheap answer to whether anything
  moved — the only endpoint that knows is the timeline, and reading it
  is already the import — so this pass imports instead of flagging the
  account.
- *Initial sync of the new accounts*, monthly, imports the timeline of
  an account that was just linked. Linking one triggers this action
  immediately as well, so its card is filled from the first moment.
- *Full resync*, weekly, reads the timeline of an X account exactly as
  the ordinary import does, so it notices nothing that was deleted
  there. A publication deleted on X is marked *Deleted* when someone
  opens it from the dashboard or from its form, which is a check of
  *Social Media X*.
- *Social: Refresh the statistics of the recent publications*, daily,
  reads the publications of the last 30 days by identifier, which is a
  call of *Social Media X* and not of this module.

An account whose first import has not run yet is left out of the
bihourly check, because that check and the initial import write the same
row from two threads.

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

Usage
=====

Importing the publications
--------------------------

- The timeline of the account is read with `the posts of a
  user <https://docs.x.com/x-api/posts/user-posts-timeline>`__, which
  answers up to **100 publications in a single request** and is not
  paginated: what is older than those hundred is not imported, in order
  to spare the requests of the plan. Retweets and replies are excluded,
  so only the original publications of the account are stored.

- Each media of a publication is downloaded once and stored as an
  attachment, and a media already stored is not asked for again. A photo
  is downloaded from the address X answers with it, and one answered
  without an address is not stored. A video or an animated GIF has no
  such address: X lists the files it serves for it, and the mp4 of the
  highest quality is the one downloaded, which for an animated GIF is
  the single mp4 X makes of it. The card of the publication counts its
  videos, and its form plays them.

  |VIDEO_POST|

- No file above the size cap of *Social Media Sync* is downloaded. The
  cap applies to every file on its own, not to the publication as a
  whole.

- A video that is not downloaded — X lists no mp4 for it, the download
  fails or the file is above the cap — still leaves the publication
  marked as having a video, and its form says that the video was not
  downloaded. The next pass that reads the publication tries the
  download again.

- While the system parameter ``social_media_sync.download_videos`` is
  ``False``, no video and no animated GIF is downloaded, and the photos
  still are. The publication is marked as having a video all the same,
  and its form says that the video was not downloaded. The videos
  already downloaded stay, and once the parameter is back to ``True``
  the next import downloads the ones missing from the publications it
  reads.

- The files come from the servers X delivers its media from, not from
  its API, so downloading them spends no credits of the plan.

- The *Update* button of the dashboard runs the same import on demand,
  over every account shown.

- The text of a publication is stored as X wrote it, without the
  ``https://t.co/…`` link X appends to it for each of its photos, videos
  or animated GIFs, which are stored apart as attachments. The links the
  author wrote, and the one a quote points at, stay in the text.

- The module adds the *With Reposts* and *With Quotes* filters to the
  search panel of the dashboard, next to the filters of impressions and
  interactions of *Social Media Sync*.

- The list of publications of the dashboard offers the *Reposts* and
  *Quotes* columns, right after *Shares*. The list is shared by every
  social media, so both are hidden until they are chosen from the
  optional columns of the list.

- With *Enable since* the import only asks for what was published after
  the stored publication, so the older publications are no longer read
  by this import. The ones published in the last 30 days are refreshed
  all the same by *Social Media X*, which reads their figures by
  identifier. The card of the account is unaffected either way: the
  figures are written on each publication, and the card adds up every
  publication stored.

Comments
--------

- A comment or a reply written on an X thread may carry images. The
  composer offers the upload, the files travel to X with the reply and
  are shown on the published comment.

- The video or the animated GIF of a reply is shown by its cover, as one
  more image of the comment. It is not played in Odoo: it is watched on
  X.

  |COMMENT_VIDEO_COVER|

- The text of a comment read from X is shown the same way as the text of
  an imported publication: without the links X appends for its media,
  and with the links its author wrote.

- The comments of a publication are read with the `recent
  search <https://docs.x.com/x-api/posts/recent-search>`__ endpoint of
  X, which only covers the **last 7 days**: the replies to older
  publications are not shown on the dashboard even though the post has
  them. Retweets and quotes are excluded as well, only the replies are
  listed.

- The conversation is read a hundred replies at a time, following the
  token X answers with until it runs out or five pages have been read.
  Read to the end, how many replies each comment has is counted in Odoo
  and never asked to X again; cut short by that ceiling or by the limit
  of requests of the plan, no number is stated, and the dashboard offers
  to unfold the replies of every comment.

- Answering a publication and answering one of its comments are the same
  call to X: on X a comment is a post like any other, and what changes
  is the post being replied to.

Recommending and deleting
-------------------------

- *Recommend* is offered on the card of every publication of X and on
  each comment and reply of its thread. Pressing it gives the like of
  the account on X, and pressing it on what is already *Recommended*
  withdraws it. The like is always given as the account the publication
  belongs to.

  |RECOMMEND_POST|

  |RECOMMEND_COMMENT|

- *Delete*, in the menu of a comment, is offered only on the comments
  the account wrote that answer the publication. X only deletes the
  posts of the account that asks, so the comments of other people do not
  offer it, and a reply inside a thread does not offer it either. After
  a confirmation the post is deleted on X and the thread is read again
  without it. A comment already gone from X counts as deleted.

  |DELETE_COMMENT|

- When X refuses the call, its reason is shown instead. An App that
  cannot spend against the API is explained as such, with the link to
  the pricing of X, and credentials X no longer accepts mark the account
  as needing to be authorized again. An error X does not explain on a
  *Recommend* makes Odoo ask about the publication itself, which is
  marked *Deleted* if it is gone from X.

What Odoo sees of the likes
---------------------------

- X cannot be asked whether the account liked a given post. What it
  offers is the list of the latest posts the account liked, and Odoo
  reads a single page of it, the **hundred latest**, at two moments: on
  every import of the account, once its timeline has answered, to mark
  the publications the account recommended; and when the dialog of the
  comments opens, to mark the comments and replies.
- A like older than those hundred is not seen: the publication or the
  comment is drawn as not recommended. Pressing *Recommend* there gives
  a like X already holds; X answers that it is given, and the entry is
  drawn right from then on.
- The refresh of the dialog every two minutes, and the reload after a
  comment is sent or deleted, do not read the likes again. The comments
  already on screen keep the like they had, and the new ones arrive not
  recommended. A like given or withdrawn on x.com while the dialog is
  open is seen the next time the dialog opens; on a publication, on the
  next import.
- The like of a publication is stored on it, and every import writes it
  again on the publications its page of the timeline answers. The like
  of a comment is not stored in Odoo: the dialog reads it again each
  time it opens.
- A page of likes that cannot be read never stops the import or the
  comments. The import leaves the publications as they were marked, and
  the dialog draws its comments as not recommended.

What it costs
-------------

- The page of likes is charged by X for every post it answers, up to a
  hundred, and a post read twice in the same day is charged once. It is
  read once per account on every import — the *Update* button, the check
  for updates every two hours, the initial sync and the full resync —
  and once each time the dialog of the comments of a publication opens.
- *Recommend* on a comment is one call to X, and the card is not
  refreshed after it.
- *Recommend* on the card of a publication is one call to X, and then,
  whether X accepted it or not, the card is refreshed the way its
  *Update* button refreshes the account: the figures of the recent
  publications, the timeline and the page of likes.
- *Delete* is one call to X. The thread is then read again, and a
  deletion X accepted refreshes the card the same way *Recommend* does.
- What X charges for each of those calls is on `the pricing of the X
  API <https://docs.x.com/x-api/getting-started/pricing>`__.

What this module does not detect
--------------------------------

- Its own passes do not detect a publication deleted on X: a publication
  deleted there is marked *Deleted* only by the check *Social Media X*
  makes when the publication is opened.

Rate limits
-----------

The endpoints this module spends from the plan of the account are the
timeline of the user, the recent search of the comments, the publication
of a reply, the like and its withdrawal, which share one window, the
list of the latest likes of the account and the deletion of a comment.
Each of them has its own window on the account, the same record where
*Social Media X* stores the windows of the endpoints it spends —the read
of a single post and the read of the figures by identifier—, and when X
answers that one is exhausted Odoo stops calling that endpoint until the
window expires.

While the window of the comments lasts, opening the conversation of a
publication shows the notice *The comments could not be read from X. The
account may have reached the limit of requests of its plan.* instead of
an empty thread, and a refresh in that state leaves the comments already
on screen untouched.

While the window of the replies lasts, publishing a comment answers *The
comment could not be published on X. The account may have reached the
limit of requests of its plan.* instead of reporting a reply X never
received. The text of the composer is not kept, but the comment being
answered stays aimed at, so the retry starts under the same comment.

While the window of the likes lasts, *Recommend* answers *The
recommendation could not be sent to X. The account may have reached the
limit of requests of its plan.* and the entry stays as it was drawn.
While the window of the deletion lasts, *Delete* answers *The comment
could not be deleted on X. The account may have reached the limit of
requests of its plan.* and the comment stays. While the window of the
list of likes lasts, the import and the dialog go on without it, as when
the page cannot be read.

Uninstalling
------------

Uninstalling this module leaves the account linked and able to publish.
What goes with it is the checkpoint of the import: *Enable since*, *Post
since* and the reference of the last publication read.

.. |VIDEO_POST| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x_sync/static/img/readme/VIDEO_POST.png
.. |COMMENT_VIDEO_COVER| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x_sync/static/img/readme/COMMENT_VIDEO_COVER.png
.. |RECOMMEND_POST| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x_sync/static/img/readme/RECOMMEND_POST.png
.. |RECOMMEND_COMMENT| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x_sync/static/img/readme/RECOMMEND_COMMENT.png
.. |DELETE_COMMENT| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x_sync/static/img/readme/DELETE_COMMENT.png

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

- Only the hundred latest likes of the account are read, in a single
  page, so a publication or a comment the account liked before them is
  drawn as not recommended. Pressing *Recommend* there does no harm — X
  answers that the like is already given, and the entry is drawn right —
  but reading further back costs one page per hundred likes, every post
  of it charged, and grows with the history of the account.

- *Delete* is offered on the comments of the account that answer the
  publication, not on its replies inside a thread: *Social Media Sync*
  hides the entry on every reply, whatever the social media. Offering it
  there is a change in *Social Media Sync*; until then, a reply of the
  account is deleted on X itself.

- The card of an imported publication does not play its video: it counts
  the videos, and the file is played in the form of the publication.

- The video or the animated GIF of a comment is not played in Odoo. The
  dialog of the comments shows its cover as one more image of the
  comment, and the video itself is watched on X.

- Only the mp4 files X lists for a video are downloaded. The HLS
  playlist (``application/x-mpegURL``) X lists next to them is not used,
  because Odoo cannot play it without a player of its own.

- The timeline is read once and ``next_token`` is never followed, so an
  account with more than 100 publications is only imported up to that
  page. Paginating it costs one request per page against the plan of the
  account.

- The conversation of a publication is read up to 5 pages of 100
  replies, so a thread longer than 500 replies is read truncated; what
  is missing is said instead of guessed, because no comment of a
  truncated read states how many replies it has. Raising the ceiling
  costs one request per page against the plan of the account, and it is
  paid again on every refresh: the dialog of the comments rereads the
  conversation whole every two minutes while it stays open. Reading only
  the first page on those refreshes, and the whole conversation only
  when the dialog opens, is what would make a higher ceiling affordable.

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_x_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

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

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