From 719208ec425c3afc6b7f30e24b03d01f62eee16f Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Wed, 18 Sep 2024 01:53:00 +0700 Subject: [PATCH] [ADD] webhook_incoming: trigger actions upon incoming webhook requests --- test-requirements.txt | 2 + webhook_incoming/README.rst | 86 ++++ webhook_incoming/__init__.py | 1 + webhook_incoming/__manifest__.py | 16 + webhook_incoming/models/__init__.py | 2 + webhook_incoming/models/base_automation.py | 224 +++++++++ webhook_incoming/models/ir_actions_server.py | 21 + webhook_incoming/pyproject.toml | 3 + webhook_incoming/readme/CONTRIBUTORS.md | 1 + webhook_incoming/readme/DESCRIPTION.md | 11 + .../static/description/index.html | 432 ++++++++++++++++++ .../views/base_automation_views.xml | 26 ++ 12 files changed, 825 insertions(+) create mode 100644 test-requirements.txt create mode 100644 webhook_incoming/README.rst create mode 100644 webhook_incoming/__init__.py create mode 100644 webhook_incoming/__manifest__.py create mode 100644 webhook_incoming/models/__init__.py create mode 100644 webhook_incoming/models/base_automation.py create mode 100644 webhook_incoming/models/ir_actions_server.py create mode 100644 webhook_incoming/pyproject.toml create mode 100644 webhook_incoming/readme/CONTRIBUTORS.md create mode 100644 webhook_incoming/readme/DESCRIPTION.md create mode 100644 webhook_incoming/static/description/index.html create mode 100644 webhook_incoming/views/base_automation_views.xml diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..f41ffae --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,2 @@ +odoo-addon-webhook_outgoing @ git+https://github.com/OCA/webhook@refs/pull/15/head#subdirectory=webhook_outgoing +odoo-addon-queue_job @ git+https://github.com/OCA/queue@17.0#subdirectory=queue_job diff --git a/webhook_incoming/README.rst b/webhook_incoming/README.rst new file mode 100644 index 0000000..8890871 --- /dev/null +++ b/webhook_incoming/README.rst @@ -0,0 +1,86 @@ +================ +Incoming Webhook +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:801eb9069d1b38681a4da1eec8219ce59055f9cea46af867d49a7be05b955dc4 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwebhook-lightgray.png?logo=github + :target: https://github.com/OCA/webhook/tree/17.0/webhook_incoming + :alt: OCA/webhook +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/webhook-17-0/webhook-17-0-webhook_incoming + :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/webhook&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allow creating an automation that send webhook/requests to +another systems via HTTP. + +To create a new automation to send webhook requests, go to Settings > +Automated Actions: + +- When add an automation, choose Custom Webhook as action to perform. +- Config Endpoint, Headers and Body Template accordingly. + +This webhook action use Jinja and rendering engine, you can draft body +template using Jinja syntax. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Hoang Tran + +Contributors +------------ + +- Hoang Tran + +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. + +This module is part of the `OCA/webhook `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/webhook_incoming/__init__.py b/webhook_incoming/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/webhook_incoming/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/webhook_incoming/__manifest__.py b/webhook_incoming/__manifest__.py new file mode 100644 index 0000000..da30c7d --- /dev/null +++ b/webhook_incoming/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2024 Hoang Tran . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "Incoming Webhook", + "summary": "Receive incoming webhook requests as trigger to execute tasks.", + "version": "17.0.0.0.1", + "author": "Hoang Tran,Odoo Community Association (OCA)", + "license": "LGPL-3", + "website": "https://github.com/OCA/webhook", + "depends": ["base_automation", "webhook_outgoing", "queue_job"], + "data": [ + "views/base_automation_views.xml", + ], + "auto_install": True, +} diff --git a/webhook_incoming/models/__init__.py b/webhook_incoming/models/__init__.py new file mode 100644 index 0000000..be53e2b --- /dev/null +++ b/webhook_incoming/models/__init__.py @@ -0,0 +1,2 @@ +from . import base_automation +from . import ir_actions_server diff --git a/webhook_incoming/models/base_automation.py b/webhook_incoming/models/base_automation.py new file mode 100644 index 0000000..737054f --- /dev/null +++ b/webhook_incoming/models/base_automation.py @@ -0,0 +1,224 @@ +# Copyright 2024 Hoang Tran . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import base64 +import json +import logging +import traceback +from contextlib import closing + +from pytz import timezone + +from odoo import ( + SUPERUSER_ID, + Command, + _, + api, + exceptions, + fields, + models, + registry, + tools, +) +from odoo.tools import ustr +from odoo.tools.float_utils import float_compare +from odoo.tools.safe_eval import safe_eval + +_logger = logging.getLogger(__name__) + + +class InheritedBaseAutomation(models.Model): + _inherit = "base.automation" + + allow_creation = fields.Boolean( + string="Allow creation?", + help="Allow executing webhook to maybe create record if a record is not " + "found using record getter", + ) + create_record_code = fields.Text( + "Record Creation Code", + default="""# Available variables: +# - env: Odoo Environment on which the action is triggered +# - model: Odoo Model of the record on which the action is triggered; +# is a void recordset +# - record: record on which the action is triggered; may be void +# - records: recordset of all records on which the action is triggered +# in multi-mode; may be void +# - payload: input payload from webhook request +# - time, datetime, dateutil, timezone: useful Python libraries +# - float_compare: Odoo function to compare floats based on specific precisions +# - log: log(message, level='info'): logging function to record debug information +# in ir.logging table +# - UserError: Warning Exception to use with raise +# - Command: x2Many commands namespace +# You must return the created record by assign it to `record` variable: +# - record = res.partner(1,) +""", + help="Create record if Record Getter couldn't find a matching one.", + ) + delay_execution = fields.Boolean( + help="Queue actions to perform to delay execution." + ) + + def _get_eval_context(self, payload=None): + """ + Override to add payload to context + """ + eval_context = super()._get_eval_context() + eval_context["model"] = self.env[self.model_name] + eval_context["payload"] = payload if payload is not None else {} + return eval_context + + def _execute_webhook(self, payload): + """Execute the webhook for the given payload. + The payload is a dictionnary that can be used by the `record_getter` to + identify the record on which the automation should be run. + """ + self.ensure_one() + + # info logging is done by the ir.http logger + msg = "Webhook #%s triggered with payload %s" + msg_args = (self.id, payload) + _logger.debug(msg, *msg_args) + + record = self.env[self.model_name] + eval_context = self._get_eval_context(payload=payload) + + if self.record_getter: + try: + record = safe_eval(self.record_getter, eval_context) + except Exception as e: # noqa: BLE001 + msg = ( + "Webhook #%s couldn't be triggered because record_getter failed:" + "\n%s" + ) + msg_args = (self.id, traceback.format_exc()) + _logger.warning(msg, *msg_args) + self._webhook_logging(payload, self._add_postmortem(e)) + raise e + + if not record.exists() and self.allow_creation: + try: + create_eval_context = self._get_create_eval_context(payload=payload) + safe_eval( + self.create_record_code, + create_eval_context, + mode="exec", + nocopy=True, + ) # nocopy allows to return 'action' + record = create_eval_context.get("record", self.model_id.browse()) + except Exception as e: # noqa: BLE001 + msg = "Webhook #%s failed with error:\n%s" + msg_args = (self.id, traceback.format_exc()) + _logger.warning(msg, *msg_args) + self._webhook_logging(payload, self._add_postmortem(e)) + + elif not record.exists(): + msg = ( + "Webhook #%s could not be triggered because " + "no record to run it on was found." + ) + msg_args = (self.id,) + _logger.warning(msg, *msg_args) + self._webhook_logging(payload, msg) + raise exceptions.ValidationError( + _("No record to run the automation on was found.") + ) + + try: + res = self._process(record) + self._webhook_logging(payload, None) + return res + except Exception as e: # noqa: BLE001 + msg = "Webhook #%s failed with error:\n%s" + msg_args = (self.id, traceback.format_exc()) + _logger.warning(msg, *msg_args) + self._webhook_logging(payload, self._add_postmortem(e)) + raise e + + def _get_create_eval_context(self, payload=None): + def log(message, level="info"): + with self.pool.cursor() as cr: + cr.execute( + """ + INSERT INTO ir_logging( + create_date, create_uid, type, dbname, name, + level, message, path, line, func + ) + VALUES ( + NOW() at time zone 'UTC', %s, + %s, %s, %s, %s, + %s, %s, %s, %s, + ) + """, + ( + self.env.uid, + "server", + self._cr.dbname, + __name__, + level, + message, + "action", + self.id, + self.name, + ), + ) + + eval_context = dict(self.env.context) + model_name = self.model_id.sudo().model + model = self.env[model_name] + eval_context.update( + { + "uid": self._uid, + "user": self.env.user, + "time": tools.safe_eval.time, + "datetime": tools.safe_eval.datetime, + "dateutil": tools.safe_eval.dateutil, + "timezone": timezone, + "float_compare": float_compare, + "b64encode": base64.b64encode, + "b64decode": base64.b64decode, + "Command": Command, + "env": self.env, + "model": model, + "log": log, + "payload": payload, + } + ) + return eval_context + + def _webhook_logging(self, body, response): + if self.log_webhook_calls: + with closing(registry(self.env.cr.dbname).cursor()) as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + + def create_log(env, response): + vals = { + "webhook_type": "incoming", + "webhook": f"{self.name} ({self})", + "endpoint": f"/web/hook/{self.webhook_uuid}", + "headers": {}, + "request": json.dumps(ustr(body), indent=4), + "body": json.dumps(ustr(body), indent=4), + "response": ustr(response), + "status": getattr(response, "status_code", None), + } + env["webhook.logging"].create(vals) + env.cr.commit() + + create_log(env, response) + + def _process(self, records, domain_post=None): + """ + Override to allow delay execution + """ + to_delay = self.filtered(lambda a: a.delay_execution) + execute_now = self - to_delay + + super( + InheritedBaseAutomation, + to_delay.with_context(delay_execution=True), + )._process(records, domain_post=domain_post) + + return super(InheritedBaseAutomation, execute_now)._process( + records, domain_post=domain_post + ) diff --git a/webhook_incoming/models/ir_actions_server.py b/webhook_incoming/models/ir_actions_server.py new file mode 100644 index 0000000..153b899 --- /dev/null +++ b/webhook_incoming/models/ir_actions_server.py @@ -0,0 +1,21 @@ +# Copyright 2024 Hoang Tran . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api, models + + +class IrActionsServer(models.Model): + _inherit = "ir.actions.server" + + def run(self): + if self.env.context.get("delay_execution"): + return self.with_delay().run() + return super().run() + + @api.model + def _job_prepare_context_before_enqueue_keys(self): + return ( + "active_model", + "active_ids", + "active_id", + "domain_post", + ) diff --git a/webhook_incoming/pyproject.toml b/webhook_incoming/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/webhook_incoming/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/webhook_incoming/readme/CONTRIBUTORS.md b/webhook_incoming/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..c522a80 --- /dev/null +++ b/webhook_incoming/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Hoang Tran \<\> diff --git a/webhook_incoming/readme/DESCRIPTION.md b/webhook_incoming/readme/DESCRIPTION.md new file mode 100644 index 0000000..ba791af --- /dev/null +++ b/webhook_incoming/readme/DESCRIPTION.md @@ -0,0 +1,11 @@ +This module allow creating an automation that send webhook/requests to +another systems via HTTP. + +To create a new automation to send webhook requests, go to Settings \> +Automated Actions: + +- When add an automation, choose Custom Webhook as action to perform. +- Config Endpoint, Headers and Body Template accordingly. + +This webhook action use Jinja and rendering engine, you can draft body +template using Jinja syntax. diff --git a/webhook_incoming/static/description/index.html b/webhook_incoming/static/description/index.html new file mode 100644 index 0000000..6e9ed3b --- /dev/null +++ b/webhook_incoming/static/description/index.html @@ -0,0 +1,432 @@ + + + + + +Incoming Webhook + + + +
+

Incoming Webhook

+ + +

Beta License: LGPL-3 OCA/webhook Translate me on Weblate Try me on Runboat

+

This module allow creating an automation that send webhook/requests to +another systems via HTTP.

+

To create a new automation to send webhook requests, go to Settings > +Automated Actions:

+
    +
  • When add an automation, choose Custom Webhook as action to perform.
  • +
  • Config Endpoint, Headers and Body Template accordingly.
  • +
+

This webhook action use Jinja and rendering engine, you can draft body +template using Jinja syntax.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Hoang Tran
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

This module is part of the OCA/webhook project on GitHub.

+

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

+
+
+
+ + diff --git a/webhook_incoming/views/base_automation_views.xml b/webhook_incoming/views/base_automation_views.xml new file mode 100644 index 0000000..041cde7 --- /dev/null +++ b/webhook_incoming/views/base_automation_views.xml @@ -0,0 +1,26 @@ + + + + webhook.incoming.view.automation.form + base.automation + + + + + + + + + + + + + + + +