diff --git a/.github/workflows/projects.json b/.github/workflows/projects.json index d5fe1a49..a89579fa 100644 --- a/.github/workflows/projects.json +++ b/.github/workflows/projects.json @@ -34,5 +34,8 @@ }, { "path": "integrator-default-profile/samples/github-issue-to-google-chat" + }, + { + "path": "integrator-default-profile/samples/trello-summary-email" } ] diff --git a/integrator-default-profile/samples/trello-summary-email/.choreo/config-schema.json b/integrator-default-profile/samples/trello-summary-email/.choreo/config-schema.json new file mode 100644 index 00000000..7d6f19a4 --- /dev/null +++ b/integrator-default-profile/samples/trello-summary-email/.choreo/config-schema.json @@ -0,0 +1,673 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "wso2": { + "type": "object", + "properties": { + "trello_summary_email": { + "type": "object", + "properties": { + "trelloConfig": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "token": { + "type": "string" + }, + "boardIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "listIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "key", + "token", + "boardIds", + "listIds" + ], + "description": "" + }, + "mailchimpConfig": { + "type": "object", + "properties": { + "apiKey": { + "type": "string" + }, + "serverPrefix": { + "type": "string" + }, + "listId": { + "type": "string" + }, + "fromName": { + "type": "string" + }, + "fromAddress": { + "type": "string" + }, + "subjectPrefix": { + "type": "string" + }, + "includeDateInSubject": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "apiKey", + "serverPrefix", + "listId", + "fromName", + "fromAddress", + "subjectPrefix", + "includeDateInSubject" + ], + "description": "" + }, + "filterConfig": { + "type": "object", + "properties": { + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "members": { + "type": "array", + "items": { + "type": "string" + } + }, + "includeDueDateFilter": { + "type": "boolean" + }, + "dueDateDaysAhead": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "labels", + "members", + "includeDueDateFilter", + "dueDateDaysAhead" + ], + "description": "" + }, + "summaryConfig": { + "type": "object", + "properties": { + "grouping": { + "enum": [ + "LABEL", + "MEMBER", + "LIST" + ] + }, + "highlightOverdueCards": { + "type": "boolean" + }, + "showCardAge": { + "type": "boolean" + }, + "staleCardDays": { + "type": "integer" + }, + "showAttachmentCount": { + "type": "boolean" + }, + "showChecklistProgress": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "grouping", + "highlightOverdueCards", + "showCardAge", + "staleCardDays", + "showAttachmentCount", + "showChecklistProgress" + ], + "description": "" + } + }, + "additionalProperties": false, + "required": [ + "trelloConfig", + "mailchimpConfig" + ] + } + }, + "additionalProperties": false + }, + "ballerina": { + "type": "object", + "properties": { + "http": { + "type": "object", + "properties": { + "maxActiveConnections": { + "type": "integer", + "description": "" + }, + "maxIdleConnections": { + "type": "integer", + "description": "" + }, + "waitTime": { + "type": "number", + "description": "" + }, + "maxActiveStreamsPerConnection": { + "type": "integer", + "description": "" + }, + "minEvictableIdleTime": { + "type": "number", + "description": "" + }, + "timeBetweenEvictionRuns": { + "type": "number", + "description": "" + }, + "minIdleTimeInStaleState": { + "type": "number", + "description": "" + }, + "timeBetweenStaleEviction": { + "type": "number", + "description": "" + }, + "defaultListenerPort": { + "type": "integer", + "description": "" + }, + "defaultListenerConfig": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "http1Settings": { + "type": "object", + "properties": { + "keepAlive": { + "enum": [ + "AUTO", + "ALWAYS", + "NEVER" + ] + }, + "maxPipelinedRequests": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "keepAlive", + "maxPipelinedRequests" + ], + "name": "ballerina/http:2.15.0:ListenerHttp1Settings" + }, + "secureSocket": { + "anyOf": [ + { + "type": "object", + "properties": { + "key": { + "anyOf": [ + { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "path", + "password" + ], + "name": "ballerina/crypto:2.9.2:KeyStore" + }, + { + "type": "object", + "properties": { + "certFile": { + "type": "string" + }, + "keyFile": { + "type": "string" + }, + "keyPassword": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "certFile", + "keyFile" + ], + "name": "ballerina/http:2.15.0:CertKey" + } + ] + }, + "mutualSsl": { + "type": "object", + "properties": { + "verifyClient": { + "enum": [ + "OPTIONAL", + "REQUIRE" + ] + }, + "cert": { + "anyOf": [ + { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "path", + "password" + ], + "name": "ballerina/crypto:2.9.2:TrustStore" + }, + { + "type": "string" + } + ] + } + }, + "additionalProperties": false, + "required": [ + "verifyClient", + "cert" + ] + }, + "protocol": { + "type": "object", + "properties": { + "name": { + "enum": [ + "DTLS", + "TLS", + "SSL" + ] + }, + "versions": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "name", + "versions" + ] + }, + "certValidation": { + "type": "object", + "properties": { + "type": { + "enum": [ + "OCSP_STAPLING", + "OCSP_CRL" + ] + }, + "cacheSize": { + "type": "integer" + }, + "cacheValidityPeriod": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "type", + "cacheSize", + "cacheValidityPeriod" + ] + }, + "ciphers": { + "type": "array", + "items": { + "type": "string" + } + }, + "shareSession": { + "type": "boolean" + }, + "handshakeTimeout": { + "type": "number" + }, + "sessionTimeout": { + "type": "number" + } + }, + "additionalProperties": false, + "required": [ + "key", + "ciphers", + "shareSession" + ], + "name": "ballerina/http:2.15.0:ListenerSecureSocket" + }, + { + "type": "" + } + ] + }, + "httpVersion": { + "enum": [ + "2.0", + "1.1", + "1.0" + ] + }, + "timeout": { + "type": "number" + }, + "server": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "" + } + ] + }, + "requestLimits": { + "type": "object", + "properties": { + "maxUriLength": { + "type": "integer" + }, + "maxHeaderSize": { + "type": "integer" + }, + "maxEntityBodySize": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "maxUriLength", + "maxHeaderSize", + "maxEntityBodySize" + ], + "name": "ballerina/http:2.15.0:RequestLimitConfigs" + }, + "gracefulStopTimeout": { + "type": "number" + }, + "socketConfig": { + "type": "object", + "properties": { + "soBackLog": { + "type": "integer" + }, + "connectTimeOut": { + "type": "number" + }, + "receiveBufferSize": { + "type": "integer" + }, + "sendBufferSize": { + "type": "integer" + }, + "tcpNoDelay": { + "type": "boolean" + }, + "socketReuse": { + "type": "boolean" + }, + "keepAlive": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "soBackLog", + "connectTimeOut", + "receiveBufferSize", + "sendBufferSize", + "tcpNoDelay", + "socketReuse", + "keepAlive" + ], + "name": "ballerina/http:2.15.0:ServerSocketConfig" + }, + "http2InitialWindowSize": { + "type": "integer" + }, + "minIdleTimeInStaleState": { + "type": "number" + }, + "timeBetweenStaleEviction": { + "type": "number" + } + }, + "additionalProperties": false, + "required": [ + "host", + "http1Settings", + "secureSocket", + "httpVersion", + "timeout", + "server", + "requestLimits", + "gracefulStopTimeout", + "socketConfig", + "http2InitialWindowSize", + "minIdleTimeInStaleState", + "timeBetweenStaleEviction" + ], + "name": "ballerina/http:2.15.0:ListenerConfiguration", + "description": "" + }, + "traceLogConsole": { + "type": "boolean", + "description": "" + }, + "traceLogAdvancedConfig": { + "type": "object", + "properties": { + "console": { + "type": "boolean" + }, + "path": { + "type": "string" + }, + "host": { + "type": "string" + }, + "port": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "console" + ], + "name": "ballerina/http:2.15.0:TraceLogAdvancedConfiguration", + "description": "" + }, + "accessLogConfig": { + "type": "object", + "properties": { + "console": { + "type": "boolean" + }, + "format": { + "type": "string" + }, + "attributes": { + "type": "array", + "items": { + "type": "string" + } + }, + "path": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "console", + "format" + ], + "name": "ballerina/http:2.15.0:AccessLogConfiguration", + "description": "" + } + }, + "additionalProperties": false + }, + "log": { + "type": "object", + "properties": { + "destinations": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "enum": [ + "stderr", + "stdout" + ] + } + }, + "additionalProperties": false, + "required": [ + "type" + ], + "name": "ballerina/log:2.14.0:StandardDestination" + }, + { + "type": "object", + "properties": { + "type": { + "enum": [ + "file" + ] + }, + "path": { + "type": "string" + }, + "mode": { + "enum": [ + "APPEND", + "TRUNCATE" + ] + } + }, + "additionalProperties": false, + "required": [ + "type", + "path", + "mode" + ], + "name": "ballerina/log:2.14.0:FileOutputDestination" + } + ] + }, + "description": "" + }, + "modules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "level": { + "enum": [ + "WARN", + "INFO", + "ERROR", + "DEBUG" + ] + } + }, + "additionalProperties": false, + "required": [ + "name", + "level" + ], + "name": "ballerina/log:2.14.0:Module" + }, + "description": "" + }, + "keyValues": { + "type": "object", + "properties": { + "msg": {}, + "error": {}, + "stackTrace": {}, + "module": {} + }, + "additionalProperties": false, + "name": "ballerina/log:2.14.0:AnydataKeyValues", + "description": "" + }, + "format": { + "enum": [ + "logfmt", + "json" + ], + "description": "" + }, + "level": { + "enum": [ + "WARN", + "INFO", + "ERROR", + "DEBUG" + ], + "description": "" + }, + "enableSensitiveDataMasking": { + "type": "boolean", + "description": "" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "requiredLevel": 3 +} diff --git a/integrator-default-profile/samples/trello-summary-email/.choreo/diagram.md b/integrator-default-profile/samples/trello-summary-email/.choreo/diagram.md new file mode 100644 index 00000000..b9ef7060 --- /dev/null +++ b/integrator-default-profile/samples/trello-summary-email/.choreo/diagram.md @@ -0,0 +1,10 @@ +A(["Begin"]):::startNode +B["Fetch & Filter
Trello Cards"]:::processNode +C{"Matching
Cards Found?"}:::decisionNode +D["Group Cards &
Generate HTML Email"]:::processNode +E["Create & Send
Mailchimp Campaign"]:::processNode +F(["End"]):::endNode + +A --> B --> C +C -- Yes --> D --> E --> F +C -- No --> F \ No newline at end of file diff --git a/integrator-default-profile/samples/trello-summary-email/.choreo/instructions.md b/integrator-default-profile/samples/trello-summary-email/.choreo/instructions.md new file mode 100644 index 00000000..fe042080 --- /dev/null +++ b/integrator-default-profile/samples/trello-summary-email/.choreo/instructions.md @@ -0,0 +1,56 @@ +## What It Does + +- Fetches cards from specified Trello boards and lists +- Applies optional filters by label, member, or due date range +- Groups cards by **List**, **Member**, or **Label** +- Generates a formatted email summarising all cards, including overdue status, card age, attachments, and checklist progress +- Creates and sends a Mailchimp email campaign to a configured audience list when triggered by Devant automation + +
+ +Trello Setup Guide + +1. A Trello account with at least one board containing cards +2. Trello API credentials: + - API Key - available from [https://trello.com/app-key](https://trello.com/app-key) + - Token - generate a token from the same page +3. The **Board IDs** of the boards to include + - Open a board in Trello, click **Share**, and copy the short link. The ID is the alphanumeric segment: `https://trello.com/b//...` +4. (Optional) **List IDs** to filter specific lists - leave empty to include all lists on the board + +
+ +
+ +Mailchimp Setup Guide + +1. A Mailchimp account with a configured audience (list) +2. Mailchimp API credentials: + - API Key - found under **Profile → Extras → API Keys** + - Server Prefix - the prefix shown in your Mailchimp URL (e.g., `us21`) + - List ID - found under **Audience → Settings → Audience name and defaults** +3. A configured sender name and reply-to email address + +
+ +
+ +Additional Configurations + +1. `filterConfig.labels` + - Filter cards by label name. Leave empty to include all labels. +2. `filterConfig.members` + - Filter cards by member full name. Leave empty to include all members. +3. `filterConfig.includeDueDateFilter` + - Set to `true` to only include cards due within the next `dueDateDaysAhead` days. +4. `mailchimpConfig.includeDateInSubject` + - If `true` (default), today's date is included in the email subject (e.g., `Trello Cards Summary - 2026-03-12`). +5. `summaryConfig.grouping` + - How to group cards in the email. Possible values: + - `LIST` (default) + - `MEMBER` + - `LABEL` +6. `summaryConfig.staleCardDays` + - Cards with no activity for this many days are considered stale (default: `30`). + +
diff --git a/integrator-default-profile/samples/trello-summary-email/.gitignore b/integrator-default-profile/samples/trello-summary-email/.gitignore new file mode 100644 index 00000000..fa4d3d61 --- /dev/null +++ b/integrator-default-profile/samples/trello-summary-email/.gitignore @@ -0,0 +1,12 @@ +# Ballerina generates this directory during the compilation of a package. +# It contains compiler-generated artifacts and the final executable if this is an application package. +target/ + +# Ballerina maintains the compiler-generated source code here. +# Remove this if you want to commit generated sources. +generated/ + +# Contains configuration values used during development time. +# See https://ballerina.io/learn/provide-values-to-configurable-variables/ for more details. +Config.toml +Dependencies.toml diff --git a/integrator-default-profile/samples/trello-summary-email/Ballerina.toml b/integrator-default-profile/samples/trello-summary-email/Ballerina.toml new file mode 100644 index 00000000..6bd22aa9 --- /dev/null +++ b/integrator-default-profile/samples/trello-summary-email/Ballerina.toml @@ -0,0 +1,9 @@ +[package] +org = "wso2" +name = "trello_summary_email" +version = "0.1.0" +distribution = "2201.13.1" +title = "trello-summary-email" + +[build-options] +sticky = true diff --git a/integrator-default-profile/samples/trello-summary-email/README.md b/integrator-default-profile/samples/trello-summary-email/README.md new file mode 100644 index 00000000..4f4be617 --- /dev/null +++ b/integrator-default-profile/samples/trello-summary-email/README.md @@ -0,0 +1,128 @@ +# Trello Summary Email Integration + +## Description + +This integration fetches cards from one or more Trello boards and lists, generates a grouped summary, and sends it as an email campaign through Mailchimp on a configurable schedule. It is designed to give teams a regular digest of active Trello cards, highlighting overdue items, card ages, attachment counts, and checklist progress. + +### What It Does + +- Fetches cards from specified Trello boards and lists +- Applies optional filters by label, member, or due date range +- Groups cards by **List**, **Member**, or **Label** +- Generates a formatted email with: + - Total card count and overdue card count + - Per-card details: board, list, due date, labels, members, description, card age, attachments, and checklist progress +- Creates and sends a Mailchimp email campaign to a configured audience list +- Executes when triggered by Devant automation (scheduling is handled by Devant) + +## Prerequisites + +Before running this integration, you need: + +### Trello Setup + +1. A Trello account with at least one board containing cards +2. Trello API credentials: + - **API Key** – available from [https://trello.com/app-key](https://trello.com/app-key) + - **Token** – generate a token from the same page +3. The **Board IDs** of the boards you want to include + - Open a board in Trello, click **Share**, and copy the short link. The ID is the alphanumeric part (e.g., `https://trello.com/b//...`) +4. (Optional) The **List IDs** of specific lists to filter - leave empty to include all lists on the board + +### Mailchimp Setup + +1. A Mailchimp account with a configured audience (list) +2. Mailchimp API credentials: + - **API Key** – found under **Profile → Extras → API Keys** + - **Server Prefix** – the prefix shown in your Mailchimp URL (e.g., `us21`) + - **List ID** – the audience to send the campaign to (found under **Audience → Settings → Audience name and defaults**) +3. A configured sender name and email address for the campaign + +## Configuration + +Create a `Config.toml` file in the project root with the following values: + +```toml +[trelloConfig] +key = "" +token = "" +boardIds = ["", ""] +listIds = [] # Leave empty to include all lists + +[mailchimpConfig] +apiKey = "" +serverPrefix = "" # e.g., "us21" +listId = "" +fromName = "" +fromAddress = "" +subjectPrefix = "Trello Cards Summary" # Optional, has default +includeDateInSubject = true # If true, today's date is shown in the subject + +[filterConfig] +labels = [] # Filter by label names; empty means no filter +members = [] # Filter by member full names; empty means no filter +includeDueDateFilter = false # Set to true to only include cards due within dueDateDaysAhead +dueDateDaysAhead = 7 + +[summaryConfig] +grouping = "LIST" # Group by: "LIST", "MEMBER", or "LABEL" +highlightOverdueCards = true +showCardAge = true +staleCardDays = 30 # Cards inactive for this many days are considered stale +showAttachmentCount = true +showChecklistProgress = true +``` + +### Configuration Reference + +#### `trelloConfig` + +| Field | Description | +|---|---| +| `key` | Your Trello API key | +| `token` | Your Trello API token | +| `boardIds` | List of Trello board IDs to fetch cards from | +| `listIds` | List of specific list IDs to include; leave empty for all lists | + +#### `mailchimpConfig` + +| Field | Description | +|---|---| +| `apiKey` | Your Mailchimp API key | +| `serverPrefix` | Mailchimp data center prefix (e.g., `us21`) | +| `listId` | Mailchimp audience list ID to send the campaign to | +| `fromName` | Sender display name for the email campaign | +| `fromAddress` | Sender reply-to email address | +| `subjectPrefix` | Prefix for the email subject line (default: `Trello Cards Summary`) | +| `includeDateInSubject` | If `true` (default), today's date is included in the email subject (e.g., `Trello Cards Summary - 2026-03-12`) | + +#### `filterConfig` + +| Field | Description | +|---|---| +| `labels` | Only include cards matching these label names; empty means all labels | +| `members` | Only include cards assigned to these members (by full name); empty means all members | +| `includeDueDateFilter` | If `true`, only include cards due within the next `dueDateDaysAhead` days | +| `dueDateDaysAhead` | Number of days ahead to use for the due date filter (default: `7`) | + +#### `summaryConfig` + +| Field | Description | +|---|---| +| `grouping` | How to group cards in the email: `LIST`, `MEMBER`, or `LABEL` | +| `highlightOverdueCards` | If `true`, overdue cards are flagged in the email | +| `showCardAge` | If `true`, each card shows the number of days since last activity | +| `staleCardDays` | Cards with no activity for this many days are considered stale (default: `30`) | +| `showAttachmentCount` | If `true`, shows the attachment count on each card | +| `showChecklistProgress` | If `true`, shows checklist completion progress on each card | + +## Deploying on Devant + +1. Sign in to your Devant account. +2. Create a new Integration and follow the instructions in the [Devant Documentation](https://wso2.com/devant/docs/references/import-a-repository/) to import this repository. +3. Select the **Technology** as `WSO2 Integrator: BI`. +4. Choose the **Integration** type as `Automation` and click **Create**. +5. Once the build is successful, click **Configure to Continue** and set up the required environment variables for Trello and Mailchimp credentials. +6. Configure the schedule in Devant's automation scheduler (e.g., weekly on Monday mornings). +7. Devant will trigger this automation on the configured schedule and handle all scheduling. +8. Once tested, you may promote the integration to production. Make sure to set the relevant environment variables in the production environment as well. diff --git a/integrator-default-profile/samples/trello-summary-email/agents.bal b/integrator-default-profile/samples/trello-summary-email/agents.bal new file mode 100644 index 00000000..e69de29b diff --git a/integrator-default-profile/samples/trello-summary-email/automation.bal b/integrator-default-profile/samples/trello-summary-email/automation.bal new file mode 100644 index 00000000..ca06faf7 --- /dev/null +++ b/integrator-default-profile/samples/trello-summary-email/automation.bal @@ -0,0 +1,25 @@ +import ballerina/log; + +function sendTrelloSummary() returns error? { + log:printInfo("Starting Trello summary generation..."); + + //Fetch cards + CardSummary[] cards = check fetchTrelloCards(); + log:printInfo(string `Fetched ${cards.length().toString()} cards`); + + if cards.length() == 0 { + log:printInfo("No cards found matching the criteria. Skipping email."); + return; + } + + int overdueCount = countOverdueCards(cards); + int staleCount = countStaleCards(cards); + + GroupedSummary[] groupedSummaries = groupCards(cards); + log:printInfo(string `Grouped cards into ${groupedSummaries.length().toString()} groups`); + + string emailContent = generateEmailContent(groupedSummaries, cards.length(), overdueCount, staleCount); + + check sendEmailSummary(emailContent); + log:printInfo("Email sent successfully"); +} diff --git a/integrator-default-profile/samples/trello-summary-email/config.bal b/integrator-default-profile/samples/trello-summary-email/config.bal new file mode 100644 index 00000000..66397ebe --- /dev/null +++ b/integrator-default-profile/samples/trello-summary-email/config.bal @@ -0,0 +1,32 @@ +configurable record { + string key; + string token; + string[] boardIds; + string[] listIds; +} trelloConfig = ?; + +configurable record { + string apiKey; + string serverPrefix; + string listId; + string fromName; + string fromAddress; + string subjectPrefix = "Trello Cards Summary"; + boolean includeDateInSubject = true; +} mailchimpConfig = ?; + +configurable record { + string[] labels = []; + string[] members = []; + boolean includeDueDateFilter = false; + int dueDateDaysAhead = 7; +} filterConfig = {}; + +configurable record { + SummaryGrouping grouping = LIST; + boolean highlightOverdueCards = true; + boolean showCardAge = true; + int staleCardDays = 30; + boolean showAttachmentCount = true; + boolean showChecklistProgress = true; +} summaryConfig = {}; diff --git a/integrator-default-profile/samples/trello-summary-email/connections.bal b/integrator-default-profile/samples/trello-summary-email/connections.bal new file mode 100644 index 00000000..7a4e846c --- /dev/null +++ b/integrator-default-profile/samples/trello-summary-email/connections.bal @@ -0,0 +1,21 @@ +import ballerina/http; +import ballerinax/mailchimp; +import ballerinax/trello; + +final trello:Client trelloClient = check new ({ + 'key: trelloConfig.key, + token: trelloConfig.token +}); + +final http:Client trelloHttpClient = check new ("https://api.trello.com/1"); +//Had to use a separate client for Trello API calls as the trello:Client does not support all endpoints needed for fetching card details and attachments. + +final mailchimp:Client mailchimpClient = check new ( + config = { + auth: { + username: "anystring", + password: mailchimpConfig.apiKey + } + }, + serviceUrl = string `https://${mailchimpConfig.serverPrefix}.api.mailchimp.com/3.0` +); diff --git a/integrator-default-profile/samples/trello-summary-email/data_mappings.bal b/integrator-default-profile/samples/trello-summary-email/data_mappings.bal new file mode 100644 index 00000000..e69de29b diff --git a/integrator-default-profile/samples/trello-summary-email/functions.bal b/integrator-default-profile/samples/trello-summary-email/functions.bal new file mode 100644 index 00000000..a35a0f19 --- /dev/null +++ b/integrator-default-profile/samples/trello-summary-email/functions.bal @@ -0,0 +1,535 @@ +import ballerina/time; +import ballerinax/mailchimp; +import ballerinax/trello; + +function fetchListCardsAsJson(string listId) returns json[]|error { + json cardsJson = check trelloHttpClient->/lists/[listId]/cards.get( + 'key = trelloConfig.key, + token = trelloConfig.token, + fields = "id,name,url,desc,dateLastActivity,due,idMembers,labels,badges" + ); + + return check cardsJson.ensureType(); +} + +// Fetches all members of a board upfront and builds a memberId -> fullName lookup map. +// Should switch back to trello:Client once issues fixed. +function fetchBoardMembersMap(string boardId) returns map|error { + json membersJson = check trelloHttpClient->/boards/[boardId]/members.get( + 'key = trelloConfig.key, + token = trelloConfig.token, + fields = "id,fullName" + ); + json[] membersArray = check membersJson.ensureType(); + map memberMap = {}; + foreach json memberJson in membersArray { + string? memberId = check memberJson.id; + string? fullName = check memberJson.fullName; + if memberId is string && fullName is string { + memberMap[memberId] = fullName; + } + } + return memberMap; +} + +function fetchTrelloCards() returns CardSummary[]|error { + CardSummary[] allCards = []; + + foreach string boardId in trelloConfig.boardIds { + trello:Board board = check trelloClient->/boards/[boardId].get( + checklists = "none", + cards = "none", + customFields = false, + cardPluginData = false, + memberships = "none", + labels = "none", + tags = false, + boardStars = "none", + lists = "all", + members = "none", + organization = false, + organizationPluginData = false, + pluginData = false, + myPrefs = false, + fields = "name", + actions = "none" + ); + string boardName = board.name ?: "Unknown Board"; + + map memberMap = check fetchBoardMembersMap(boardId); + + json boardJson = board.toJson(); + json listsJson = check boardJson.lists; + json[] listsArray = check listsJson.ensureType(); + + foreach json listJson in listsArray { + string listId = check listJson.id; + string listName = check listJson.name; + + if (trelloConfig.listIds.length() > 0 && trelloConfig.listIds.indexOf(listId) is ()) { + continue; + } + + json[] cardsArray = check fetchListCardsAsJson(listId); + + foreach json cardJson in cardsArray { + CardSummary? cardSummary = check processCardFromJson(cardJson, listName, boardName, memberMap); + if cardSummary is CardSummary { + allCards.push(cardSummary); + } + } + } + } + + return allCards; +} + +function processCardFromJson(json cardJson, string listName, string boardName, map memberMap) returns CardSummary?|error { + string cardId = check cardJson.id; + string cardName = check cardJson.name; + string cardUrl = check cardJson.url; + string? cardDesc = check cardJson.desc; + string description = cardDesc is string ? cardDesc : ""; + + int cardAgeDays = 0; + boolean isStale = false; + string? dateLastActivity = check cardJson.dateLastActivity; + if dateLastActivity is string { + time:Utc lastActivityUtc = check time:utcFromString(dateLastActivity); + time:Utc currentTime = time:utcNow(); + decimal secondsDiff = time:utcDiffSeconds(currentTime, lastActivityUtc); + cardAgeDays = (secondsDiff / 86400); + isStale = cardAgeDays >= summaryConfig.staleCardDays; + } + + int attachmentCount = 0; + int checklistItemsTotal = 0; + int checklistItemsCompleted = 0; + decimal checklistCompletionPercentage = 0.0; + + json? badgesJson = check cardJson.badges; + if badgesJson is json { + int? attachments = check badgesJson.attachments; + int? checkItems = check badgesJson.checkItems; + int? checkItemsChecked = check badgesJson.checkItemsChecked; + + attachmentCount = attachments ?: 0; + checklistItemsTotal = checkItems ?: 0; + checklistItemsCompleted = checkItemsChecked ?: 0; + + if checklistItemsTotal > 0 { + checklistCompletionPercentage = (checklistItemsCompleted / checklistItemsTotal) * 100.0; + } + } + + string[] labelNames = []; + json? labelsJson = check cardJson.labels; + if labelsJson is json[] { + foreach json labelJson in labelsJson { + string? labelName = check labelJson.name; + if labelName is string && labelName.trim().length() > 0 { + labelNames.push(labelName); + } + } + } + + if filterConfig.labels.length() > 0 { + boolean hasMatchingLabel = false; + foreach string filterLabel in filterConfig.labels { + if labelNames.indexOf(filterLabel) !is () { + hasMatchingLabel = true; + break; + } + } + if !hasMatchingLabel { + return (); + } + } + + string[] memberNames = []; + json? membersJson = check cardJson.idMembers; + if membersJson is json[] { + foreach json memberIdJson in membersJson { + string? memberId = check memberIdJson; + if memberId is string && memberMap.hasKey(memberId) { + memberNames.push(memberMap.get(memberId)); + } + } + } + + if filterConfig.members.length() > 0 { + boolean hasMatchingMember = false; + foreach string filterMember in filterConfig.members { + if memberNames.indexOf(filterMember) !is () { + hasMatchingMember = true; + break; + } + } + if !hasMatchingMember { + return (); + } + } + + time:Civil? dueDate = (); + boolean isOverdue = false; + string? dueDateStr = check cardJson.due; + + if dueDateStr is string { + time:Utc dueDateUtc = check time:utcFromString(dueDateStr); + dueDate = time:utcToCivil(dueDateUtc); + + time:Utc currentTime = time:utcNow(); + if dueDateUtc < currentTime { + isOverdue = true; + } + + if filterConfig.includeDueDateFilter { + time:Utc futureTime = time:utcAddSeconds(currentTime, filterConfig.dueDateDaysAhead * 24 * 60 * 60); + if dueDateUtc > futureTime { + return (); + } + } + } + + return { + id: cardId, + name: cardName, + listName: listName, + boardName: boardName, + url: cardUrl, + labels: labelNames, + members: memberNames, + dueDate: dueDate, + isOverdue: isOverdue, + description: description, + cardAgeDays: cardAgeDays, + isStale: isStale, + attachmentCount: attachmentCount, + checklistItemsTotal: checklistItemsTotal, + checklistItemsCompleted: checklistItemsCompleted, + checklistCompletionPercentage: checklistCompletionPercentage + }; +} + +function groupCards(CardSummary[] cards) returns GroupedSummary[] { + map groupMap = {}; + + foreach CardSummary card in cards { + string[] groupKeys = []; + + match summaryConfig.grouping { + LIST => { + groupKeys.push(card.listName); + } + MEMBER => { + if card.members.length() > 0 { + groupKeys = card.members; + } else { + groupKeys.push("Unassigned"); + } + } + LABEL => { + if card.labels.length() > 0 { + groupKeys = card.labels; + } else { + groupKeys.push("No Labels"); + } + } + } + + foreach string groupKey in groupKeys { + if !groupMap.hasKey(groupKey) { + groupMap[groupKey] = []; + } + CardSummary[] existingCards = groupMap.get(groupKey); + existingCards.push(card); + groupMap[groupKey] = existingCards; + } + } + + GroupedSummary[] groupedSummaries = []; + foreach string groupName in groupMap.keys() { + CardSummary[] groupCardsList = groupMap.get(groupName); + groupedSummaries.push({ + groupName: groupName, + cards: groupCardsList + }); + } + + return groupedSummaries; +} + +function getFormattedTimestamp(time:Utc utcTime) returns string { + time:Civil civil = time:utcToCivil(utcTime); + return string `${civil.year}-${civil.month.toString().padZero(2)}-${civil.day.toString().padZero(2)} ${civil.hour.toString().padZero(2)}:${civil.minute.toString().padZero(2)} UTC`; +} + +function escapeHtml(string value) returns string { + string[] parts = []; + foreach int i in 0 ..< value.length() { + string ch = value.substring(i, i + 1); + match ch { + "&" => { + parts.push("&"); + } + "<" => { + parts.push("<"); + } + ">" => { + parts.push(">"); + } + "\"" => { + parts.push("""); + } + "'" => { + parts.push("'"); + } + _ => { + parts.push(ch); + } + } + } + return string:'join("", ...parts); +} + +function getHtmlFormattedGroup(GroupedSummary group) returns string { + string cardsHtml = ""; + foreach CardSummary card in group.cards { + string cardNameEscaped = escapeHtml(card.name); + string cardUrlEscaped = escapeHtml(card.url); + string leftBorder = card.isOverdue + ? " style=\"border-left: 3px solid #de350b; padding: 16px 30px; border-bottom: 1px solid #e1e4e8;\"" + : (card.isStale + ? " style=\"border-left: 3px solid #ff8b00; padding: 16px 30px; border-bottom: 1px solid #e1e4e8;\"" + : " style=\"padding: 16px 30px; border-bottom: 1px solid #e1e4e8;\""); + + string dueDateHtml = ""; + time:Civil? dueDate = card.dueDate; + if dueDate is time:Civil { + string dueDateStr = string `${dueDate.year}-${dueDate.month.toString().padZero(2)}-${dueDate.day.toString().padZero(2)}`; + string dueDateColor = card.isOverdue ? "#de350b" : "#586069"; + dueDateHtml = string `Due: ${dueDateStr}  `; + } + + string labelsHtml = ""; + foreach string label in card.labels { + labelsHtml += string `${escapeHtml(label)}`; + } + + string membersHtml = ""; + foreach string member in card.members { + membersHtml += escapeHtml(member) + " "; + } + + string cardAgeHtml = summaryConfig.showCardAge + ? string `Age: ${card.cardAgeDays.toString()} days  ` : ""; + + string attachmentsHtml = summaryConfig.showAttachmentCount && card.attachmentCount > 0 + ? string `Attachments: ${card.attachmentCount.toString()}  ` : ""; + + string checklistHtml = ""; + if summaryConfig.showChecklistProgress && card.checklistItemsTotal > 0 { + string pct = formatPercentageTwoDecimals(card.checklistCompletionPercentage); + checklistHtml = string `Checklist: ${card.checklistItemsCompleted.toString()}/${card.checklistItemsTotal.toString()} (${pct}%)`; + } + + string descHtml = ""; + if card.description.trim().length() > 0 { + string truncatedDesc = card.description.length() > 200 + ? card.description.substring(0, 200) + "..." : card.description; + descHtml = string `
${escapeHtml(truncatedDesc)}
`; + } + + string overdueTag = summaryConfig.highlightOverdueCards && card.isOverdue + ? "OVERDUE" + : ""; + + string metaHtml = dueDateHtml + cardAgeHtml + attachmentsHtml + checklistHtml; + string labelsRow = labelsHtml.length() > 0 ? string `
${labelsHtml}
` : ""; + string membersRow = membersHtml.trim().length() > 0 + ? string `
Members: ${membersHtml.trim()}
` : ""; + + cardsHtml += string ` + + + ${cardNameEscaped}${overdueTag} +
${metaHtml}
+ ${labelsRow} + ${membersRow} + ${descHtml} + + `; + } + + return string ` + + + ${escapeHtml(group.groupName)} + (${group.cards.length().toString()} cards) + + + + + + ${cardsHtml} +
+ + `; +} + +function formatPercentageTwoDecimals(decimal value) returns string { + decimal rounded = value.round(2); + int wholePart = rounded; + int decimalPart = ((rounded - wholePart) * 100); + + return string `${wholePart.toString()}.${decimalPart.toString().padZero(2)}`; +} + +function generateEmailContent(GroupedSummary[] groupedSummaries, int totalCards, int overdueCount, int staleCount) returns string { + string groupedByStr = summaryConfig.grouping.toString(); + string generatedAt = getFormattedTimestamp(time:utcNow()); + + string groupsHtml = ""; + foreach GroupedSummary group in groupedSummaries { + groupsHtml += getHtmlFormattedGroup(group); + } + + string overdueCountDisplay = summaryConfig.highlightOverdueCards ? overdueCount.toString() : "-"; + + return string ` + + + + + + Trello Cards Summary + + + +
+ +
+ Trello Cards Summary: ${totalCards.toString()} total, ${overdueCount.toString()} overdue, grouped by ${groupedByStr} +
+ + + + + + + + + + + + + + ${groupsHtml} + + + + + + + + +
+ +`; +} + +function sendEmailSummary(string htmlContent) returns error? { + string subject = mailchimpConfig.subjectPrefix; + if mailchimpConfig.includeDateInSubject { + time:Civil currentDate = time:utcToCivil(time:utcNow()); + string dateStr = string `${currentDate.year}-${currentDate.month.toString().padZero(2)}-${currentDate.day.toString().padZero(2)}`; + subject = string `${mailchimpConfig.subjectPrefix} - ${dateStr}`; + } + + mailchimp:Campaign1 campaign = check mailchimpClient->postCampaigns({ + 'type: "regular", + recipients: { + list_id: mailchimpConfig.listId + }, + settings: { + subject_line: subject, + from_name: mailchimpConfig.fromName, + reply_to: mailchimpConfig.fromAddress, + title: subject + } + }); + + string? campaignId = campaign?.id; + if campaignId is () { + return error("Failed to create campaign: Campaign ID is null"); + } + + _ = check mailchimpClient->putCampaignsIdContent( + campaignId = campaignId, + payload = { + html: htmlContent + } + ); + + _ = check mailchimpClient->postCampaignsIdActionsSend(campaignId = campaignId); +} + +function countOverdueCards(CardSummary[] cards) returns int { + int count = 0; + foreach CardSummary card in cards { + if card.isOverdue { + count += 1; + } + } + return count; +} + +function countStaleCards(CardSummary[] cards) returns int { + int count = 0; + foreach CardSummary card in cards { + if card.isStale { + count += 1; + } + } + return count; +} diff --git a/integrator-default-profile/samples/trello-summary-email/main.bal b/integrator-default-profile/samples/trello-summary-email/main.bal new file mode 100644 index 00000000..fca4a603 --- /dev/null +++ b/integrator-default-profile/samples/trello-summary-email/main.bal @@ -0,0 +1,9 @@ +import ballerina/log; + +public function main() returns error? { + log:printInfo("Starting Trello Card Summary Automation"); + log:printInfo(string `Grouping: ${summaryConfig.grouping.toString()}`); + log:printInfo(string `Mailchimp List: ${mailchimpConfig.listId}`); + + check sendTrelloSummary(); +} diff --git a/integrator-default-profile/samples/trello-summary-email/types.bal b/integrator-default-profile/samples/trello-summary-email/types.bal new file mode 100644 index 00000000..2a2c6f56 --- /dev/null +++ b/integrator-default-profile/samples/trello-summary-email/types.bal @@ -0,0 +1,31 @@ +import ballerina/time; + +public enum SummaryGrouping { + LIST, + MEMBER, + LABEL +} + +public type CardSummary record {| + string id; + string name; + string listName; + string boardName; + string url; + string[] labels; + string[] members; + time:Civil? dueDate; + boolean isOverdue; + string description; + int cardAgeDays; + boolean isStale; + int attachmentCount; + int checklistItemsTotal; + int checklistItemsCompleted; + decimal checklistCompletionPercentage; +|}; + +public type GroupedSummary record {| + string groupName; + CardSummary[] cards; +|};