Metadata-Version: 2.1
Name: odoo-addon-social_media_x
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
Requires-Dist: requests_oauthlib
Requires-Dist: tweepy
Summary: Publish on X, with the figures of the account and of the posts sent
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
==============

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

.. |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
    :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
    :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 provides the necessary functionality for basic interaction
with the X social media.

Main features:

- User account integration.
- Post creation.
- Reports with agnostic metrics, on the account card of the Dashboard.
  The figures are read back from X by the connector itself: the
  publications of the last 30 days are refreshed once a day, and the
  card of the account adds up the counters its publications hold. A
  synchronization module widens what is known — the publications Odoo
  did not send, and the history older than that window — but the card is
  not empty without one.
- Reposts and quotes, the two figures only X reports. They are shown as
  *Reposts* and *Quotes* in the *Statistics* dialog of a publication,
  opened from the menu of its card on the Dashboard, and they count
  towards the *Interactions* total and the *Engagement* rate of the
  publication and of the account.
- What X will not publish, shown on the post while it is written. The
  message is checked against the characters the plan of the account
  allows, **280** without X Premium and **25 000** with it, and the
  medias against **4 images** of at most **5 MB** each — **15 MB** for a
  GIF — in JPG, PNG, WEBP or GIF, and **one video** of at most **512
  MB** in MP4. X takes images or a video, never both in the same post,
  so a post mixing them is refused instead of warned about. The same
  checks refuse the publication if the post reaches it anyway, through
  an import or an RPC call.
- The credit balance of the developer App that pays the calls to the X
  API, on the account form. It is read from X every 2 hours and from the
  buttons of the account, and the responsible of the account is warned
  when it drops under a threshold set on the account or is used up.

**An X account will never draw a time series in Social Media >
Statistics.** That screen reads a history per day, and the
`metrics <https://docs.x.com/x-api/fundamentals/metrics>`__ the API v2
answers are the current lifetime counters of each post, with no way of
asking for them grouped by day. Giving X a series means either repeating
the lifetime counter on every day, which draws a cumulative curve of a
different shape than the other social media on the same graph, or
storing the delta between two readings, which is false for any day the
scheduled action did not run. Neither is a measurement, so nothing is
written: an account of X shows the standard empty view there, and its
figures live on its card of the Dashboard, which is where they mean
something.

**Table of contents**

.. contents::
   :local:

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

This module requires the following Python libraries:

- ``tweepy`` 4.0 or above, because the module uses the client of the `X
  API v2 <https://docs.x.com/x-api/getting-started/about-x-api>`__
  (``tweepy.Client``) besides the v1.1 client for the media upload.
- ``requests_oauthlib``

You can install them with:

::

   pip install tweepy requests_oauthlib

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

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

Please note that you must have a developer account.

**Important:** the X API v2 endpoints used by this module require a
developer App **connected to a Pay Per Use project** and an account with
credit on the **pay-per-use** pricing of X, see `about the X
API <https://docs.x.com/x-api/getting-started/about-x-api>`__. There is
no Free access tier: an App left on the **Default project**, whose plan
is **Standard Basic**, cannot spend against the API and is refused, so
the account cannot be associated with it. Pay per use is not a
subscription — there is no monthly fee and no minimum purchase, the App
only needs a positive credit balance — but the balance has to be there
before the first call. The association wizard shows this warning, and
when X rejects the request for this reason the module replaces the raw
error with a message that asks to connect the App to a Pay Per Use
project in *Project Access* > *Manage*, not the Default project on
Standard Basic, to make sure the account has credit in the Developer
Console, and that points to the pricing page. Note that such an App can
be refused at two different moments: when the request token is asked,
before any authorization screen is shown, and the wizard answers with
that message; or later, when the module reads the authorized user, and
the message is then delivered on the Dashboard.

A call made with an App that cannot spend answers ``403 Forbidden``,
either with ``"reason": "client-not-enrolled"`` or asking for an App
attached to a Project, since only an enrolled App can belong to one.
Every endpoint this module uses is affected: linking the account,
refreshing the data of the account, publishing (message and media
upload), deleting, checking whether a publication is still online and
reading the figures of the recent ones.

What each call of the module costs, according to the `pricing of
X <https://docs.x.com/x-api/getting-started/pricing>`__:

+---------------------------+------------------------+-----------------+
| What the module does      | Endpoint               | Price           |
+===========================+========================+=================+
| Publish a post            | Post creation          | $0.015          |
+---------------------------+------------------------+-----------------+
| Publish a post carrying a | Post creation with URL | $0.200          |
| link                      |                        |                 |
+---------------------------+------------------------+-----------------+
| Read the figures of its   | Owned read             | $0.001 per post |
| own publications          |                        |                 |
+---------------------------+------------------------+-----------------+
| Read the authorized user  | Users                  | $0.010          |
+---------------------------+------------------------+-----------------+

**A post that carries a link costs more than ten times a post that does
not.** X bills the link itself, whether it is tracked or not:
``social_media_base`` only routes the links of a publication through
``link_tracker`` when it promotes a marketing campaign, and a tracked
link is still a link. Whether a post carries one is the single decision
that moves the bill of a database that only publishes.

The same resource asked for twice within 24 hours is charged once, which
is what keeps the scheduled actions of the module cheap: the check of
network updates every two hours and the daily refresh of the figures
read the same publications over and over.

So a database that only publishes spends cents a month: this module
reads nothing but its own publications, at the price of an owned read,
and the credit balance of the App, once per API Key, which is not among
the calls the pricing of X lists. What grows with the account is the
history, and the history is read by *Social Media X Sync*, which is a
separate module nobody has to install. A deployment that publishes and
watches its own figures does not need it.

There is no way around the API for what this module does. Publishing on
X through anything else means either a reseller that pays the same API
and charges for it on top, or driving the web with the session of the
user, which its terms of service forbid and this module does not
implement.

- X API pricing and plans:
  https://docs.x.com/x-api/getting-started/pricing
- Rate limits according to plan:
  https://docs.x.com/x-api/fundamentals/rate-limits
- How to get access to the API:
  https://docs.x.com/x-api/getting-started/getting-access

Creating the developer account
------------------------------

The developer account is not the X account: it is the enrolment of that
X account in the API, and it is asked for once per organization, not per
social account published from Odoo.

- Sign in at the `Developer Console <https://console.x.com>`__ with the
  X account that owns the App and press *Start Building*. It does not
  have to be the account whose posts Odoo publishes — the App belongs to
  whoever administers it, and each account authorizes it afterwards from
  the wizard of *Social Media X*. Accept the Developer Agreement and
  Policy if X asks for it.

  |START_BUILDING|

- Create a project. The use case declared here is what X reads if it
  ever reviews the App. A project starts on the default plan of its
  product, so it can be born on **Standard Basic**, the plan of the
  *Default project* that the console may already show with *Plan: Free*.
  Only a project on **Pay Per Use** spends against the credit balance,
  so that is the one the App has to be connected to, see *Project
  Access* below.

  |CREATE_PROJECT|

- Create the App. The App is the set of keys, and the project is what
  those keys are allowed to call, so an App connected to no project
  answers ``403`` to everything, and so does an App connected only to
  the Default project on Standard Basic. Do not take for granted which
  project a new App is connected to: check what its *Project Access*
  shows, as described below.

  |CREATE_APP|

- Add a payment method in *Billing* > *Billing information* with **Add
  Payment Method**, following the steps X asks for.

  |BILLING_INFORMATION|

- Buy credits from the Dashboard or from *Billing* > *Credits*, before
  associating any account in Odoo: reading the authorized user is
  already a billed call, so an App with no balance fails at the
  association itself and never reaches the Dashboard of Odoo. A spend
  cap per billing cycle is set on that same screen, and it is what keeps
  a mistake from spending more than intended — when the cap is reached X
  blocks the requests until the next cycle.

  |BUY_CREDITS|

  |CREDITS|

The screens below are the ones of the developer portal. X moves them
from time to time; the names of the settings are what matters, not where
the picture puts them.

The steps required for using it are defined below:

- Go to the `Developer Console <https://console.x.com>`__

- Create a developer account as described above.

- Once the account is created, go to *Access* > *Apps* and select the
  App. What it shows as *Project Access* is the project its keys call
  through, with its plan in brackets. An App on *Default project-…
  (Standard Basic)*, as in the picture below, cannot spend against the
  API and X refuses it.

  |PROJECT_DEFAULT|

- In that case press *Manage* on *Project Access*. The dialog lists the
  projects of the account, each with its plan and a *Connect* or
  *Disconnect* button: press *Connect* on the **Pay Per Use** one. The
  keys work on every project the App is connected to, and the limits and
  endpoints follow the plan of that project. The keys do not have to be
  regenerated: an App connected this way associates with the Consumer
  Key and Secret it already had. *Start a new project*, at the bottom of
  the same dialog, offers the Ads, Enterprise and Community Notes
  products, none of which is the Pay Per Use project this module needs.

  |PROJECT_ACCESS_MANAGE|

  Once connected, the page of the App shows the Pay Per Use project
  under *Project Access*.

  |PROJECT_PAY_PER_USE|

- Open *Settings* to reach the authentication settings of the App.

  |CONFIGURATION_ACCOUNT|

- Once on the page, in the App Permissions section, select *Read and
  write*, which is what the endpoints this module calls need.

  |APP_PERMISIONS|

- Go to the App Type section and select Web App, Automated App or Bot.

  |TYPE_APP|

- Then, in the Callback URI / Redirect URL section, add a new address.
  Here are the steps to get that URL in Odoo:

  - Go to *Settings* > *Technical* > *Parameters* > *System Parameters*.
  - Search for web.base.url
  - Copy the base URL and concatenate it with the endpoint. Example:
    web.base.url: http://192.168.1.7:8017 endpoint: /social_x/callback
    (this value is fixed) callback_url:
    http://192.168.1.7:8017/social_x/callback

- Then go to the Website URL section and add your X profile address.

  Example: https://x.com/AccountTest

  |CALLBACK_URI_WEBSITE_URL|

- Finally, go to the *Keys & Tokens* tab of the App, press the
  *Regenerate* button of the *Consumer Key*, and then in the window that
  appears, confirm the generation of the API Key and API Key Secret
  values. The Console lists that pair under *OAuth 1.0 Keys* as the
  **Consumer Key**, which is the same thing this module and X itself
  call API Key and API Key Secret. X shows the pair once, and
  regenerating it again invalidates the one already stored in Odoo: an
  App whose keys were regenerated after being registered in Odoo answers
  ``{"code": 32, "message": "Could not authenticate you."}`` to the
  association.

  |KEYS_AND_TOKENS|

  |GENERATE_NEW_KEY_SECRET|

Learn more at the `X Developer Console <https://console.x.com>`__

Registering the API Key and API Key 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 API Key and API Key Secret
  obtained from your developer account.

  |WIZARD_ASSOCIATE_ACCOUNT|

- By clicking the *Associate* button here, you'll be taken to a X
  authentication page, which lists what the App is allowed to do with
  the account. Once you authorize it, you'll be taken back to the system
  and the Dashboard view, where you'll see your posts.

  |AUTHORIZE_ACCOUNT|

- An App that cannot spend against the API is refused here, after the
  authorization: X answers the reading of the authorized user with
  ``403``, the account is not created and the Dashboard explains it. The
  authorization itself is granted, so nothing has to be undone on the X
  side. The cause is either the project or the balance, and the
  Developer Console tells them apart: if the *Project Access* of the App
  shows the Default project on Standard Basic, connect the App to the
  Pay Per Use project as described above; if it already shows a Pay Per
  Use project, the account has no credit left, so buy credits in
  *Billing* > *Credits*. Then associate again.

  |NO_CREDITS|

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

- Note that the account credentials (API Key, API Secret and tokens) are
  only visible to users with administration rights (*Settings*). The
  account form shows the API Key in read-only mode and the API Secret
  masked; the tokens are never displayed.

- The authorization flow is bound to the session that started it: the
  request token returned by X only works for the user who opened the
  wizard. A callback without a request token, or with the request token
  of another user, is refused without creating or modifying anything;
  the user gets the generic notice *The account could not be associated.
  Check the server log for details.* and the exact reason is kept in the
  server log, so the raw answer of the provider is never exposed.

- Each X account has to be associated with its own pair of credentials:
  if an account already exists (even archived) with the same *API Key*
  and *API Secret*, the association is refused with *An account with
  that information already exists.* Create a different developer App for
  that account.

- Besides the OAuth 1.0a authorization of the user, the module obtains
  an application-only *bearer token* (OAuth 2.0 *client credentials*)
  from the same API Key and API Secret; that is the one the reads about
  the posts answer to — the figures of the recent publications and the
  check that one of them is still online — while reading the authorized
  user, which is what the association and the update of the data of the
  account do, travels with the OAuth 1.0a credentials of the user, see
  `about the X
  API <https://docs.x.com/x-api/getting-started/about-x-api>`__. If X
  does not deliver it, the account is not created and the notice *The
  account was not created: the OAuth2 access token could not be
  obtained.* is shown. In the same way, if X does not answer the access
  token correctly when closing the authorization, the account is not
  created and the user gets the same generic notice, the reason returned
  by X being kept in the server log.

- Publishing with images or video uses the `media
  upload <https://docs.x.com/x-api/media/upload-media>`__ endpoint of
  the X API v1.1 besides the v2 endpoints. The App has to have access to
  both: on a plan without that access, the publications with attachments
  fail even though the text-only ones work.

- The X account being authorized has to be active and not protected, and
  the App needs read and write permission. Odoo does not check it
  beforehand: if X refuses, the error returned by X is displayed on the
  Dashboard and the account is not associated.

- If the X account 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.

.. |START_BUILDING| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/START_BUILDING.png
.. |CREATE_PROJECT| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/CREATE_PROJECT.png
.. |CREATE_APP| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/CREATE_APP.png
.. |BILLING_INFORMATION| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/BILLING_INFORMATION.png
.. |BUY_CREDITS| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/BUY_CREDITS.png
.. |CREDITS| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/CREDITS.png
.. |PROJECT_DEFAULT| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/PROJECT_DEFAULT.png
.. |PROJECT_ACCESS_MANAGE| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/PROJECT_ACCESS_MANAGE.png
.. |PROJECT_PAY_PER_USE| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/PROJECT_PAY_PER_USE.png
.. |CONFIGURATION_ACCOUNT| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/CONFIGURATION_ACCOUNT.png
.. |APP_PERMISIONS| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/APP_PERMISIONS.png
.. |TYPE_APP| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/TYPE_APP.png
.. |CALLBACK_URI_WEBSITE_URL| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/CALLBACK_URI_WEBSITE_URL.png
.. |KEYS_AND_TOKENS| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/KEYS_AND_TOKENS.png
.. |GENERATE_NEW_KEY_SECRET| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/GENERATE_NEW_KEY_SECRET.png
.. |ASSOCIATE_ACCOUNT| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/ASSOCIATE_ACCOUNT.png
.. |WIZARD_ASSOCIATE_ACCOUNT| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/WIZARD_ASSOCIATE_ACCOUNT.png
.. |AUTHORIZE_ACCOUNT| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/AUTHORIZE_ACCOUNT.png
.. |NO_CREDITS| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/NO_CREDITS.png

Usage
=====

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

Only posts generated using Odoo are displayed.

- Go to *Social Media* > *Posts*

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 X account of the active company is
  selected by default in *Accounts*; review the list and remove the ones
  you do not want to use before saving.
- Save
- Click on the *Post* button
- A publication shares the images and the video of its post, so the
  dashboard card shows them, and its video indicator, as soon as X
  accepts the message. Nothing waits for an import.

Update token, API Key, API Secret and account 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
  account's data, its X Premium plan included, see *X Premium plan of
  the account* below, and read the credit balance of the App again, see
  *Credit balance of the App* below.

- If the *Update keys* checkbox is selected, the current API Key and API
  Secret values will be displayed by default. Modify any of these values
  and authentication will be performed again through X to update these
  values and the token.

  |UPDATE_KEYS|

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

  |UPDATE_TOKEN|

- Associating the account and updating it end with a success notice and
  stamp the date of *Last Update Account* on the account form.
  Associating it, associating it again, *Update token* and *Update keys*
  show *The account was associated successfully* on the Dashboard once X
  sends the user back; *Update* with no checkbox selected shows *The
  account was updated successfully*. If X refuses the figures of the
  account right after associating it, its error is shown after the
  success notice.

X Premium plan of the account
-----------------------------

How long the message of a post may be depends on the plan of the X user
who authorized the account: **280** characters without X Premium and
**25 000** with it. Odoo reads that plan from X, it is not set by hand.

- Go to *Social Media* > Configuration > Accounts, select the X account
  and open the *Configuration* tab. The *X Premium* switch shows the
  plan X reported for the account and cannot be changed from the form.
  It is not carried over when an account is duplicated.

  |X_PREMIUM|

- The plan is read from X when the account is associated, when it is
  associated again from *Update account* with *Update keys* or *Update
  token*, and by *Update account* with no checkbox selected. The switch
  is on when X reports any plan of X Premium, whatever its level (Basic,
  Premium or Premium+), and off when X reports that the user has no
  subscription. If X does not report the plan, the switch keeps the
  value it had.

- The automatic check for updates does not read it. A subscription
  bought or cancelled on X is seen in Odoo after pressing *Update
  account*.

- X Premium is a subscription of the X user, bought on
  `x.com <https://x.com>`__ while signed in with that user, from the
  *Premium* entry of its menu. It is not bought in the Developer Console
  (`console.x.com <https://console.x.com>`__), which belongs to the
  developer App: its project, its keys, the pay-per-use plan and its
  credits, see *Configuration*. The credits of the App do not grant X
  Premium: an App with credit publishes on an account without the
  subscription, and that account is still limited to 280 characters.

  |X_PREMIUM_MENU|

Credit balance of the App
-------------------------

The calls to the X API are paid from the credits bought for the
developer App, see *Configuration*. The account form shows what is left
of them, so the balance can be followed from Odoo and not only from the
Developer Console.

- Go to *Social Media* > Configuration > Accounts, select the X account
  and open the *Configuration* tab.

- *Credit Balance (USD)* is the total balance X reports for the App,
  never negative, with the date it was read next to it. The row stays
  hidden until the balance has been read once. The accounts of other
  social media do not show it.

  |X_CREDIT_BALANCE|

- The balance belongs to the App that pays the calls, not to the X
  account: the accounts registered with the same API Key show the same
  figure, and X is asked once per API Key for all of them.

- It is read again by the automatic check for updates that runs every 2
  hours, when the account is associated, by *Update account* with no
  checkbox selected and by *Update statistics* of the account form.

- If X or the network does not answer, the balance and its date are left
  as they were and the reason is only written in the server log: the
  date tells how old the figure is. The user is not warned and the
  account is not marked as needing an update. When X answers that the
  requests for the balance are exhausted, the readings wait for the
  window to end, without any notice.

- *Credit Warning (USD)*, on the same tab, is the amount under which the
  responsible of the account is warned, **1.00** by default. At 0, the
  warning is only given when the balance is used up.

- The warning is given when a reading finds the balance under the
  threshold, once per drop: the following readings under it do not
  repeat it, and a reading at or over the threshold arms it again.
  Raising the threshold over the current balance counts as a drop, so
  the next reading warns.

- When the balance reaches 0 the warning says it is used up: X blocks
  every call until credits are bought in the Developer Console.

- The warning is posted as a note on the chatter of the account that
  notifies its responsible, which keeps it when nobody is connected
  while the check runs, and it is shown as a notice to the responsible
  who is connected, blue when the balance is low and red when it is used
  up. When the balance is read while the account is being associated,
  the notice of the user who associates it is shown once X sends the
  user back to Odoo.

- Every account holding the same API Key warns its own responsible.

Archive Account X
-----------------

- Go to *Social Media* > Configuration > Accounts
- Select the account and use the standard *Actions* > *Archive* entry of
  the account form.
- Please note that all data associated with this account will be
  archived.
- To use an archived account again, open it in *Social Media* >
  *Configuration* > *Accounts* (*Archived* filter) and use the standard
  *Actions* > *Unarchive* entry: the account and its data are restored
  instead of creating a duplicate. *Update account* only reactivates it
  when *Update keys* or *Update token* is ticked, because those are the
  options that send the user back to X to authorize again. The
  *Associate Account* wizard is not the way to do it when the same API
  Key and API Secret are reused, because it refuses the keys already
  registered on another account, archived ones included.
- An archived account can be deleted permanently with the *Delete
  permanently* button, only available to a social media administrator.
  The X publications stay online, only the Odoo history is removed.

X limits and validations
------------------------

What X 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 X. 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 the characters the plan of the account
  allows: **280** without X Premium and **25 000** with it, see the
  `creation of a
  post <https://docs.x.com/x-api/posts/creation-of-a-post>`__. The plan
  is the **X Premium** switch of the account form, read from X, see *X
  Premium plan of the account* above. A longer message is reported on
  the post and the publication is not sent. A post selecting several X
  accounts is measured against the strictest of them while it is
  written, and every publication against its own account when it is
  sent, so only the line that cannot publish is refused. An account
  whose subscription was cancelled after the plan was last read still
  shows X Premium, sends the post and X refuses it: that line is left as
  *Failed* with the reason and a hint to read the plan again with
  *Update account*, see *Posts refused by X* below.
- X publishes **4 images or 1 video** per publication and never both
  kinds in the same message, see the `media
  upload <https://docs.x.com/x-api/media/upload-media>`__ documentation.
  A post mixing them is refused instead of being warned about, and so is
  one carrying more than four images or more than one video. When the
  post carries a video, the preview of X shows the video alone, without
  its images, while the form keeps warning that X refuses the post; the
  preview of any other social media of the same post is not affected.
- The images are checked against the formats X publishes, **JPG, PNG,
  WEBP and GIF**, and against **5 MB** each, **15 MB** for a GIF. The
  video is checked against **MP4** and **512 MB**. The message names the
  files that cannot be published.
- The file picker is not filtered: it accepts any file of type
  ``image/*`` or ``video/*``, and the check is what refuses the ones X
  does not take.
- The duration of the video is not checked in Odoo: a video X considers
  too long is refused by X and the line is left as *Failed* with the
  error it returned.
- A post is refused when it selects **two X accounts with the same
  username**: *There are X accounts with the same username (...), please
  check to avoid spam errors.* X rejects the same content sent twice
  from the same account for spam reasons, see the `creation of a
  post <https://docs.x.com/x-api/posts/creation-of-a-post>`__, so the
  post is stopped in Odoo instead of failing halfway through.

Posts refused by X
------------------

When X refuses a publication, its line is left as *Failed* with the
reason, which is also posted on the chatter of the post. The other
accounts of the post are published as usual, and if every account was
refused the post goes back to *Draft*. What the reason says depends on
the answer of X:

- **The developer App cannot spend against the API.** X answers
  ``403 Forbidden``, either with ``client-not-enrolled`` or asking for
  an App attached to a Project. The reason asks to connect the App to a
  Pay Per Use project in *Project Access* > *Manage*, not the Default
  project on Standard Basic, and to make sure the account has credit in
  the Developer Console, see *Configuration*. It gives the address of
  the `X API
  pricing <https://docs.x.com/x-api/getting-started/pricing>`__ page,
  written as plain text because the reason of a failed publication is
  kept as text.
- **A message longer than 280 characters, sent by an account X reported
  as X Premium.** X answers ``400 Bad Request`` or ``403 Forbidden`` and
  the reason is *X refused the post of {account}: {answer of X}. What an
  account may publish depends on its plan, and X reported X Premium for
  this account when it was last read. Press Update account to read the
  plan again before trying again.* This hint is only given when both
  conditions hold: it is how a subscription cancelled after the plan was
  last read shows up.
- **Any other refusal**, a ``400 Bad Request`` or a ``403 Forbidden``
  for the permissions of the App or the content of the post: *X refused
  the post of {account}: {answer of X}.*

The images and the video are uploaded as part of the same publication,
so an upload X refuses is explained the same way. None of these refusals
marks the account as needing an update: the plan, the permissions of the
App or the content of the post are not fixed by authorizing the account
again. Only a ``401 Unauthorized`` is taken as a problem with the
credentials, see *X credentials* below.

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

- The `rate limit <https://docs.x.com/x-api/fundamentals/rate-limits>`__
  is tracked per endpoint. The ones this module spends are linking the
  account, refreshing the data of the account, publishing (message and
  media upload), deleting, reading a single post, reading the figures of
  the recent ones (``get_posts``) and reading the credit balance of the
  App; a synchronization module adds its own to the same record. When X
  answers that it is exhausted, Odoo stores the window it returns and a
  notice is shown with the limit of the plan, the remaining requests and
  the time of the next attempt. That stored window is what stops a
  deletion, a check of a single post and a refresh of the figures before
  they are attempted; linking the account and publishing are tried all
  the same and report the limit only once X has refused them, and the
  refresh of the data of the account does not track its limit at all. If
  X does not say when the window resets, 60 seconds are assumed. The
  credit balance is the exception to the notice: its window is stored on
  every account of the same API Key and waited for in silence, see
  *Credit balance of the App*.
- If X refuses a publication because the requests of the plan are
  exhausted, the line is left as *Failed* with the message *X did not
  accept the post. The account may have reached the limit of requests of
  its plan: check the account and try again later.* It is not a content
  error: wait for the window to end and press *Post* again, which only
  sends the failed accounts.
- A deletion the window does not let happen stops with the message *The
  post could not be deleted on X. The account may have reached the limit
  of requests of its plan: check the account and try again later.*, and
  the publication stays in Odoo. Deleting the line while the tweet is
  still on X would leave the publication with nothing pointing at it, so
  the deletion waits for the window to end.
- Opening a publication, from its form or from its card on the
  dashboard, reads the tweet on X first. One deleted there answers *Not
  Found* and the publication is marked as *Deleted* on the spot, keeping
  its reference. The read spends one request of the plan and respects
  the same window as the rest: while the limit of the ``get_post``
  endpoint is exhausted the publication is left untouched rather than
  asked about.
- The X accounts are walked by the automatic check for updates that runs
  every 2 hours. The only thing it asks X by itself is the credit
  balance of the App, one request per API Key: the token of X does not
  expire and its API reports no figures by day, so the rest of the pass
  only hands the accounts over to whoever synchronizes them. Without a
  synchronization module installed, reading the balance is all that
  happens on that pass.

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

- The figures of the posts published in the last 30 days — likes,
  replies, impressions, retweets and quotes — are read back from X 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.
- The *Statistics* dialog of a publication, opened from the menu of its
  card on the Dashboard, also shows its *Engagement*: its likes,
  replies, retweets and quotes over its impressions, as a ratio from 0
  to 1, and 0 while it has no impressions. X reports no rate of its own,
  so it is derived from those figures every time they are read, by this
  refresh or by the import of a synchronization module.
- Nothing walks the timeline to do it: Odoo already knows the identifier
  of every post it asks about, so they are read by batches of **100 ids
  per request** on its own ``get_posts`` endpoint. Reading the timeline
  is what imports what Odoo did not publish, and that is a
  synchronization module.
- Daily and not every two hours because the figures of a publication
  move slowly while the quota of the plan is counted per day: the window
  is what bounds what a run spends and the interval is what bounds how
  many runs there are. For the same reason the buttons read the same
  bounded window and never the whole account.
- While the window of ``get_posts`` is exhausted the pass asks X for
  nothing and the publications keep the figures they have, with the date
  of the reading they come from. What was read before the limit was
  reached is kept: those requests are spent either way.
- A post missing from the answer is left untouched. X does not report a
  post that was deleted or hidden, which is not the same as a post whose
  figures are zero, so its line keeps its last figures and its date.

Dashboard actions
-----------------

- Deleting a publication from the dashboard deletes the post on X first.
  If X refuses the deletion, the operation is cancelled with the error
  returned by X and the publication keeps existing both on X and in
  Odoo.
- The direct calls to X made while linking the account (token requests
  and download of the profile image) have a timeout of 10 seconds. If
  the connection or X take longer, the association fails and has to be
  repeated.

X credentials
-------------

The `OAuth
1.0a <https://docs.x.com/resources/fundamentals/authentication/oauth-1-0a/api-key-and-secret>`__
tokens of X do not expire, so there is nothing to renew: they only stop
working when the access is revoked from the X application or the keys
are changed. When that happens X answers ``401 Unauthorized`` to the
publication, the reason is kept on the failed publication and the
account is marked as needing an update. Odoo cannot renew it by itself:
associate the account again from *Update account*. Deleting a
publication from Odoo with such a token does not mark the account: the
deletion is cancelled and only the error returned by X is shown.

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

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

- The access tokens are cleared, so no credential outlives the module.
- The X accounts are archived, together with their posts.
- The X specific data of this module is lost, because Odoo drops the
  columns of an uninstalled module: the API Key, the API Secret, the
  OAuth 1 tokens, the app-only bearer token, the rate limit window of
  each endpoint, the credit balance with its date, the warning
  threshold, which goes back to its default, and the *X Premium* switch,
  which is read from X again when the account is associated after
  reinstalling. The fields of a synchronization module go with that
  module, not with this one.
- The identifier of each account and publication on X 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.
- The server log of the uninstallation shows two ``ERROR`` lines of
  ``bad query: DELETE FROM "social_media"``, which violates the foreign
  key ``social_account_media_id_fkey``. They are expected: Odoo tries to
  delete the *X* record of *Social Media*, the archived accounts still
  point at it, so the deletion is refused and the uninstallation goes
  on. That record, its identifier and the *X* value of the media type
  are kept, and that is what lets the reinstallation reuse the same
  record instead of creating a second one, and the new association
  recover the history.

.. |BUTTON_UPDATE_ACCOUNT| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/BUTTON_UPDATE_ACCOUNT.png
.. |UPDATE_KEYS| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/UPDATE_KEYS.png
.. |UPDATE_TOKEN| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/UPDATE_TOKEN.png
.. |X_PREMIUM| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/X_PREMIUM.png
.. |X_PREMIUM_MENU| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/X_PREMIUM_MENU.png
.. |X_CREDIT_BALANCE| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_x/static/img/readme/X_CREDIT_BALANCE.png

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

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