-
-
Notifications
You must be signed in to change notification settings - Fork 101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
create resend.com plugin template #852
base: 0.8.2-dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from pydantic import BaseModel | ||
|
||
|
||
class ResendResource(BaseModel): | ||
api_key: str |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
## send email api | ||
|
||
url: POST https://api.resend.com/emails | ||
|
||
```shell | ||
curl -X POST 'https://api.resend.com/emails' \ | ||
-H 'Authorization: Bearer re_123456789' \ | ||
-H 'Content-Type: application/json' \ | ||
-d $'{ | ||
"from": "Acme <[email protected]>", | ||
"to": ["[email protected]"], | ||
"subject": "hello world", | ||
"text": "it works!", | ||
"headers": { | ||
"X-Entity-Ref-ID": "123" | ||
}, | ||
"attachments": [ | ||
{ | ||
"filename": 'invoice.pdf', | ||
"content": invoiceBuffer, | ||
}, | ||
] | ||
}' | ||
``` | ||
|
||
| params | type | required | | ||
|---------------------|------------------|----------| | ||
| from | str | True | | ||
| to | str or str[] | True | | ||
| subject | str | True | | ||
| bcc | str or str[] | False | | ||
| cc | str or str[] | False | | ||
| reply_to | str or str[] | False | | ||
| html | str | False | | ||
| text | str | False | | ||
| react | str | False | | ||
| headers | dict | False | | ||
| attachments | list[attachment] | False | | ||
| attachment.content | buffer or str | False | | ||
| attachment.filename | str | False | | ||
| attachment.path | str | False | | ||
| tags | list[tag] | False | | ||
| tag.name | str | True | | ||
| value | str | False | | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import aiohttp | ||
from aiohttp import ContentTypeError | ||
from json import JSONDecodeError | ||
from tracardi.domain.named_entity import NamedEntity | ||
from tracardi.domain.resources.resend import ResendResource | ||
from tracardi.service.tracardi_http_client import HttpClient | ||
from tracardi.service.storage.driver.elastic import resource as resource_db | ||
from tracardi.service.plugin.domain.config import PluginConfig | ||
from tracardi.service.plugin.domain.result import Result | ||
from tracardi.service.plugin.runner import ActionRunner | ||
from typing import Any, Union, Optional | ||
from pydantic import BaseModel | ||
|
||
|
||
class SendEmailParams(BaseModel): | ||
sender: str | ||
to: Union[str, list[str]] | ||
subject: str | ||
bcc: Optional[Union[str, list[str]]] = None | ||
cc: Optional[Union[str, list[str]]] = None | ||
reply_to: Optional[Union[str, list[str]]] = None | ||
message: dict | ||
headers: Optional[dict[str, Any]] = None | ||
attachments: Optional[Any] = None | ||
tags: Optional[Any] = None | ||
|
||
|
||
class Config(PluginConfig): | ||
resource: NamedEntity | ||
params: SendEmailParams | ||
|
||
|
||
def validate(config: dict) -> Config: | ||
return Config(**config) | ||
|
||
|
||
class ResendSendEmailAction(ActionRunner): | ||
credentials: ResendResource | ||
config: SendEmailParams | ||
|
||
async def set_up(self, init): | ||
config = validate(init) | ||
resource = await resource_db.load(config.resource.id) | ||
|
||
self.config = config.params | ||
self.credentials: "ResendResource" = resource.credentials.get_credentials(self, output=ResendResource) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please reuse: tracardi.resource.api_key instead of ResendResource. |
||
|
||
async def run(self, payload: dict, in_edge=None) -> Result: | ||
url = f"https://api.resend.com/emails" | ||
params = { | ||
"from": self.config.sender, | ||
"to": self.config.to, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You use dotPath for sender, to, etc which is good but the params.sender are not dun through dotAccessor to get the value. to get the right value when the user use dotPath like [email protected] do the following:
this will convert the dot notation if found to the actual value. If not found it will leave the value as it is. THis should be done for all dotPaths. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be also good to check if after converting the sender, to, etc. if the values are real emails. |
||
"subject": self.config.subject, | ||
"bcc": self.config.bcc, | ||
"cc": self.config.cc, | ||
"reply_to": self.config.reply_to, | ||
"headers": self.config.headers | ||
} | ||
if self.config.message.get("type", None) == "text/html": | ||
params["html"] = self.config.message.get("content", "") | ||
else: | ||
params["text"] = self.config.message.get("content", "") | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there is no content we'd rather not send anything and return a message on error port. |
||
timeout = aiohttp.ClientTimeout(total=2) | ||
async with HttpClient(0, [200, 201, 202, 203], timeout=timeout) as client: | ||
async with client.post( | ||
url=url, | ||
json=params, | ||
headers={"Authorization": f"Bearer {self.credentials.api_key}"}, | ||
) as response: | ||
try: | ||
content = await response.json() | ||
except ContentTypeError: | ||
content = await response.text() | ||
except JSONDecodeError: | ||
content = await response.text() | ||
|
||
result = { | ||
"status": response.status, | ||
"content": content | ||
} | ||
|
||
if response.status in [200, 201, 202, 203]: | ||
return Result(port="response", value=result) | ||
else: | ||
return Result(port="error", value=result) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
from tracardi.process_engine.action.v1.connectors.resend.send_email.plugin import ResendSendEmailAction | ||
from tracardi.service.plugin.domain.register import Plugin, Spec, Form, FormGroup, FormField, FormComponent, MetaData, \ | ||
Documentation, PortDoc | ||
|
||
|
||
def register() -> Plugin: | ||
return Plugin( | ||
start=False, | ||
spec=Spec( | ||
module='tracardi.process_engine.action.v1.connectors.resend.send_email.plugin', | ||
className=ResendSendEmailAction.__name__, | ||
inputs=['payload'], | ||
outputs=['response', 'error'], | ||
version="0.8.2", | ||
license="MIT", | ||
author="RyomaHan([email protected])", | ||
init={ | ||
"resource": { | ||
"id": "", | ||
"name": "" | ||
}, | ||
"params": {}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a good practive to set the default values for params:
|
||
}, | ||
form=Form( | ||
groups=[ | ||
FormGroup( | ||
fields=[ | ||
FormField( | ||
id="resource", | ||
name="Resend Resource", | ||
required=True, | ||
description="Select Resend Resource.", | ||
component=FormComponent(type="resource", props={"label": "Resend Resource", "tag": "resend"}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When yo reuse existing ApiKeythen set tag to: api_key |
||
), | ||
] | ||
), | ||
FormGroup( | ||
name="Resend Send Email API Params", | ||
fields=[ | ||
FormField( | ||
id="params.sender", | ||
name="Sender Email Address", | ||
required=True, | ||
description="To include a friendly name, use the format \"Your Name <[email protected]>\".", | ||
component=FormComponent(type="dotPath", props={"label": "Resend"}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You use dotPath for sender which is good but the params.sender are not dun through dotAccessor to get the value. |
||
), | ||
FormField( | ||
id="params.to", | ||
name="Recipient Email Address(es)", | ||
required=True, | ||
description="Recipient email address. For multiple addresses, send as an array of strings, such as: [[email protected],...]. Max 50.", | ||
component=FormComponent(type="dotPath", props={"label": "Resend"}) | ||
), | ||
FormField( | ||
id="params.subject", | ||
name="Email Subject", | ||
required=True, | ||
component=FormComponent(type="dotPath", props={"label": "Resend"}) | ||
), | ||
FormField( | ||
id="params.bcc", | ||
name="Bcc Recipient Email Address", | ||
description="Bcc recipient email address. For multiple addresses, send as an array of strings, such as: [[email protected],...].", | ||
component=FormComponent(type="dotPath", props={"label": "Resend"}) | ||
), | ||
FormField( | ||
id="params.cc", | ||
name="Cc Recipient Email Address", | ||
description="Cc recipient email address. For multiple addresses, send as an array of strings, such as: [[email protected],...].", | ||
component=FormComponent(type="dotPath", props={"label": "Resend"}) | ||
), | ||
FormField( | ||
id="params.reply_to", | ||
name="Reply-to Email Address", | ||
description="Reply-to email address. For multiple addresses, send as an array of strings, such as: [[email protected],...].", | ||
component=FormComponent(type="dotPath", props={"label": "Resend"}) | ||
), | ||
FormField( | ||
id="params.message", | ||
name="Message", | ||
description="The HTML version of the message or the plain text version of the message.", | ||
component=FormComponent( | ||
type="contentInput", | ||
props={ | ||
"rows": 13, | ||
"label": "Message body", | ||
"allowedTypes": ["text/plain", "text/html"] | ||
}) | ||
), | ||
FormField( | ||
id="params.headers", | ||
name="Custom Headers", | ||
description="Custom headers to add to the email.", | ||
component=FormComponent(type="dotPath", props={"label": "Resend"}) | ||
), | ||
] | ||
), | ||
] | ||
) | ||
), | ||
metadata=MetaData( | ||
name="Resend: Send Email", | ||
desc="Send email(s) by Resend.", | ||
brand="Resend", | ||
icon="resend", | ||
group=["Resend"], | ||
documentation=Documentation( | ||
inputs={ | ||
"payload": PortDoc(desc="This port takes payload object.") | ||
}, | ||
outputs={ | ||
"response": PortDoc(desc="This port returns response status and content."), | ||
"error": PortDoc(desc="This port returns error if request will fail ")} | ||
) | ||
) | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,6 +74,14 @@ def get_resource_types() -> List[ResourceSettings]: | |
"password": "<password>" | ||
} | ||
), | ||
ResourceSettings( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. YOu could reuse the existing api_key resource then there is no need for new ResourceSetting. this is the one:
It should be first on the list. |
||
id="resend", | ||
name="Resend", | ||
tags=["resend"], | ||
config={ | ||
"api_key": "<api-key>", | ||
}, | ||
), | ||
ResourceSettings( | ||
id="telegram", | ||
name="Telegram", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is better to reuse existing model for ApiKey. It is at: tracardi.resources.api_key