-
-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ADD] webhook_outgoing: send outgoing webhook requests
- Loading branch information
1 parent
848c21f
commit 59c9c13
Showing
17 changed files
with
919 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../../../webhook_outgoing |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import setuptools | ||
|
||
setuptools.setup( | ||
setup_requires=['setuptools-odoo'], | ||
odoo_addon=True, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
================ | ||
Outgoing Webhook | ||
================ | ||
|
||
.. | ||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
!! This file is generated by oca-gen-addon-readme !! | ||
!! changes will be overwritten. !! | ||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
!! source digest: sha256:8bdaa6bf8f8de957410bd7bde59aa2ef3a84eaecce4da8d7d349b040e297dd77 | ||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
.. |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/16.0/webhook_outgoing | ||
:alt: OCA/webhook | ||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png | ||
:target: https://translation.odoo-community.org/projects/webhook-16-0/webhook-16-0-webhook_outgoing | ||
: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=16.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 <https://github.com/OCA/webhook/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/webhook/issues/new?body=module:%20webhook_outgoing%0Aversion:%2016.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 | ||
~~~~~~~ | ||
|
||
* Hoang Tran | ||
|
||
Contributors | ||
~~~~~~~~~~~~ | ||
|
||
* Hoang Tran <[email protected]> | ||
|
||
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 <https://github.com/OCA/webhook/tree/16.0/webhook_outgoing>`_ project on GitHub. | ||
|
||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# Copyright 2024 Hoang Tran <[email protected]>. | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). | ||
|
||
{ | ||
"name": "Outgoing Webhook", | ||
"summary": "Webhook to publish events based on automated triggers", | ||
"version": "16.0.0.0.1", | ||
"author": "Hoang Tran,Odoo Community Association (OCA)", | ||
"license": "LGPL-3", | ||
"website": "https://github.com/OCA/webhook", | ||
"depends": [ | ||
"base_automation", | ||
"queue_job", | ||
], | ||
"data": [ | ||
"security/ir.model.access.csv", | ||
"data/queue_data.xml", | ||
"views/webhook_logging_views.xml", | ||
"views/ir_action_server_views.xml", | ||
"views/menus.xml", | ||
], | ||
"installable": True, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?xml version='1.0' encoding='utf-8' ?> | ||
<odoo> | ||
<record id="webhook_channel" model="queue.job.channel"> | ||
<field name="name">webhook</field> | ||
<field name="parent_id" ref="queue_job.channel_root" /> | ||
</record> | ||
</odoo> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from . import ir_action_server | ||
from . import webhook_logging |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
# Copyright 2024 Hoang Tran <[email protected]>. | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). | ||
import json | ||
import logging | ||
from contextlib import closing | ||
|
||
import requests | ||
from jinja2 import BaseLoader, Environment | ||
|
||
from odoo import SUPERUSER_ID, api, fields, models, registry | ||
from odoo.tools import ustr | ||
from odoo.tools.safe_eval import safe_eval | ||
|
||
from odoo.addons.queue_job.exception import RetryableJobError | ||
|
||
_logger = logging.getLevelName(__name__) | ||
|
||
|
||
ESCAPE_CHARS = ['"', "\n", "\r", "\t", "\b", "\f"] | ||
REPLACE_CHARS = ['\\"', "\\n", "\\r", "\\t", "\\b", "\\f"] | ||
|
||
DEFAULT_GET_TIMEOUT = 5 | ||
DEFAULT_POST_TIMEOUT = 5 | ||
|
||
|
||
class ServerAction(models.Model): | ||
_inherit = "ir.actions.server" | ||
|
||
state = fields.Selection( | ||
selection_add=[("custom_webhook", "Custom Webhook")], | ||
ondelete={"custom_webhook": "cascade"}, | ||
) | ||
endpoint = fields.Char() | ||
headers = fields.Text(default="{}") | ||
body_template = fields.Text(default="{}") | ||
request_method = fields.Selection( | ||
[ | ||
("get", "GET"), | ||
("post", "POST"), | ||
], | ||
default="post", | ||
) | ||
request_type = fields.Selection( | ||
[ | ||
("request", "HTTP Request"), | ||
("graphql", "GraphQL"), | ||
("slack", "Slack"), | ||
], | ||
default="request", | ||
) | ||
log_webhook_calls = fields.Boolean(string="Log Calls", default=False) | ||
delay_execution = fields.Boolean() | ||
delay = fields.Integer("Delay ETA (s)", default=0) | ||
|
||
def _run_action_custom_webhook_multi(self, eval_context): | ||
""" | ||
Execute to send webhook requests to triggered records. Note that execution | ||
is done on each record and not in batch. | ||
""" | ||
records = eval_context.get("records", self.model_id.browse()) | ||
|
||
for record in records: | ||
if self.delay_execution: | ||
self.with_delay(eta=self.delay)._execute_webhook(record, None) | ||
else: | ||
self._execute_webhook(record, eval_context) | ||
|
||
return eval_context.get("action") | ||
|
||
def _execute_webhook(self, record, eval_context): | ||
""" | ||
Prepare request params/body by rendering template and send requests. | ||
""" | ||
self.ensure_one() | ||
if eval_context is None: | ||
eval_context = dict( | ||
self._get_eval_context(action=self), record=record, records=record | ||
) | ||
|
||
try: | ||
response, body = getattr( | ||
self, "_execute_webhook_%s_request" % self.request_method | ||
)(record, eval_context) | ||
response.raise_for_status() | ||
except Exception as e: | ||
self._handle_exception(response, e, body) | ||
else: | ||
status_code = self._get_success_request_status_code(response) | ||
if status_code != 200: | ||
raise RetryableJobError | ||
self._webhook_logging(body, response) | ||
|
||
def _get_webhook_headers(self): | ||
self.ensure_one() | ||
headers = json.loads(self.headers.strip()).copy() if self.headers else {} | ||
return str(headers) | ||
|
||
def _prepare_data_for_post_graphql(self, template, record): | ||
def get_escaped_field(record, field_name): | ||
str_field = getattr(record, str(field_name), False) | ||
if str_field and isinstance(str_field, str): | ||
str_field = str_field.strip() | ||
for esc_char, rep_char in zip(ESCAPE_CHARS, REPLACE_CHARS): | ||
str_field = str_field.replace(esc_char, rep_char) | ||
return str_field | ||
|
||
query = template.render(record=record, escape=get_escaped_field) | ||
payload = json.dumps({"query": query, "variables": {}}) | ||
return payload | ||
|
||
def _prepare_data_for_post_request(self, template, record, eval_context): | ||
data = template.render(**dict(eval_context, record=record)) | ||
return data.encode(encoding="utf-8") | ||
|
||
def _prepare_data_for_post_slack(self, template, record, eval_context): | ||
data = template.render(**dict(eval_context, record=record)) | ||
return data.encode(encoding="utf-8") | ||
|
||
def _prepare_data_for_get(self, template, record, eval_context): | ||
data = template.render(**dict(eval_context, record=record)) | ||
return data.encode(encoding="utf-8") | ||
|
||
def _execute_webhook_get_request(self, record, eval_context): | ||
self.ensure_one() | ||
|
||
endpoint = self.endpoint | ||
headers = safe_eval(self._get_webhook_headers()) | ||
template = Environment(loader=BaseLoader()).from_string(self.body_template) | ||
params = self._prepare_data_for_get(template, record, eval_context) | ||
r = requests.get( | ||
endpoint, | ||
params=(params or {}), | ||
headers=headers, | ||
timeout=DEFAULT_GET_TIMEOUT, | ||
) | ||
|
||
return r, params | ||
|
||
def _execute_webhook_post_request(self, record, eval_context): | ||
endpoint = self.endpoint | ||
headers = safe_eval(self._get_webhook_headers()) | ||
template = Environment(loader=BaseLoader()).from_string(self.body_template) | ||
payload = {} | ||
|
||
prepare_method = "_prepare_data_for_post_%s" % self.request_type | ||
payload = getattr(self, prepare_method)(template, record, eval_context) | ||
|
||
r = requests.post( | ||
endpoint, data=payload, headers=headers, timeout=DEFAULT_POST_TIMEOUT | ||
) | ||
|
||
return r, payload | ||
|
||
def _get_success_request_status_code(self, response): | ||
""" | ||
Sometimes `200` success code is just weirdly return, so we explicitly check if | ||
a request is success or not based on request type. | ||
""" | ||
status_code = 200 | ||
|
||
if self.type == "graphql": | ||
response_data = json.loads(response.text) if response.text else False | ||
if ( | ||
response_data | ||
and response_data.get("data") | ||
and isinstance(response_data.get("data"), dict) | ||
): | ||
for __, value in response_data["data"].items(): | ||
if isinstance(value, dict): | ||
for k, v in value.items(): | ||
if k == "statusCode": | ||
status_code = v | ||
|
||
elif self.type == "slack": | ||
status_code = response.status_code | ||
|
||
return status_code | ||
|
||
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": "outgoing", | ||
"webhook": "%s (%s)" % (self.name, self), | ||
"endpoint": self.endpoint, | ||
"headers": self.headers, | ||
"request": 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 _handle_exception(self, response, exception, body): | ||
try: | ||
raise exception | ||
except requests.exceptions.HTTPError: | ||
_logger.error("HTTPError during request", exc_info=True) | ||
except requests.exceptions.ConnectionError: | ||
_logger.error("Error Connecting during request", exc_info=True) | ||
except requests.exceptions.Timeout: | ||
_logger.error("Connection Timeout", exc_info=True) | ||
except requests.exceptions.RequestException: | ||
_logger.error("Something wrong happened during request", exc_info=True) | ||
except Exception: | ||
# Final exception if none above catched | ||
_logger.error( | ||
"Internal exception happened during sending webhook request", | ||
exc_info=True, | ||
) | ||
finally: | ||
self._webhook_logging(body, exception) | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# Copyright 2024 Hoang Tran <[email protected]>. | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). | ||
|
||
import uuid | ||
|
||
from odoo import fields, models | ||
|
||
|
||
class WebhookLog(models.Model): | ||
_name = "webhook.logging" | ||
_description = "Webhook Logging" | ||
_order = "id DESC" | ||
|
||
name = fields.Char(string="Reference", default=lambda self: str(uuid.uuid4())) | ||
webhook_type = fields.Selection( | ||
selection=[ | ||
("incoming", "Incoming"), | ||
("outgoing", "Outgoing"), | ||
], | ||
string="Type", | ||
) | ||
webhook = fields.Char() | ||
endpoint = fields.Char() | ||
headers = fields.Char() | ||
status = fields.Char() | ||
body = fields.Text() | ||
request = fields.Text() | ||
response = fields.Text() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* Hoang Tran <[email protected]> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink | ||
access_webhook_logging_base_user,access.webhook.logging.base.user,model_webhook_logging,base.group_system,1,1,1,1 |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.