[ENHANCEMENT] [ACTIVITY-SUMMARY] Improvisations post-hacktoberfest (#15882)

## Background
Team Comfortably Summed had submitted Activity Summary for Twenty's
Hacktoberfest. This is a follow-up PR post-Hacktoberfest.

## Changes
### Purpose
>_What is the objective?_

To improve the code based on human and bot comments from the initial
Hacktoberfest PR, and to improve the robustness of task completion
calculation.
>_Are we solving a problem or making necessary changes to support an
existing and / or a new feature?_

Making changes to improve existing features.
### Summary
> _Are there files we can ignore?_

None.
> _Please provide a high-level summary of the changes._

- Corrects `README.md` by removing irrelevant commands and unnecessary
configuration notes
- Updates `people-creation-summariser.ts`
- Replaces requesting of company for each person by including `depth=1`
as query parameter in GET /people request as suggested by Martin Muller
– link to comment:
[link](https://github.com/twentyhq/twenty/pull/15510#discussion_r2486019035)
- Updates `senders.ts`
  - Renames `formattedMesage` to `formattedMessage`
- Updates `task-creation-summariser.ts`
- Improves calculation of completed task percentage by relying only on
list of completed tasks and total list of tasks
- The initial implementation includes relying on pending tasks and
pending overdue tasks which can result in undesired calculation outcome
This commit is contained in:
Ali Ilman
2025-11-19 00:23:11 +08:00
committed by GitHub
parent e02c24bd3a
commit be7c2ad40a
4 changed files with 29 additions and 68 deletions
@@ -20,29 +20,10 @@ A TypeScript-based reporting bot that summarizes activity from your Twenty CRM w
- A [Twenty CRM](https://twenty.com) account with API access
- Optional: Slack webhook, Discord webhook, and/or WhatsApp Business API access
## Installation
## Installing dependencies
```bash
# Clone the repository
git clone <your-repo-url>
cd <project-directory>
# Install dependencies
npm install
```
## Configuration
Create a `.env` file in the root directory with the following variables:
```bash
# Required
TWENTY_API_KEY=your_twenty_api_key_here
DAYS_AGO=7 # Number of days to look back
# Optional - Include only the platforms you want to use
SLACK_HOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR/WEBHOOK/URL
FB_GRAPH_TOKEN=your_facebook_graph_api_token
WHATSAPP_RECIPIENT_PHONE_NUMBER=+1234567890
yarn install
```
### Environment Variables
@@ -56,17 +37,6 @@ WHATSAPP_RECIPIENT_PHONE_NUMBER=+1234567890
| `FB_GRAPH_TOKEN` | ❌ No | Facebook Graph API token for WhatsApp |
| `WHATSAPP_RECIPIENT_PHONE_NUMBER` | ❌ No | WhatsApp recipient phone number (with country code) |
## Usage
```bash
# Run the application
npm start
# or
node dist/index.js # if compiled
# For development
npm run dev # if you have ts-node configured
```
## Project Structure
```
.
@@ -112,23 +82,10 @@ Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last X days:
This bot uses the [Twenty CRM REST API](https://api.twenty.com/rest/). The following endpoints are used:
- `GET /people` - Fetch people data
- `GET /companies` - Fetch company data
- `GET /opportunities` - Fetch opportunity data
- `GET /tasks` - Fetch task data
- `GET /workspaceMembers/{id}` - Fetch workspace member details
## Development
```bash
# Install dependencies
npm install
# Compile TypeScript
npx tsc
# Run in development mode
npx ts-node index.ts
```
## Notes
- The "slacker" detection is lighthearted and identifies team members with the most overdue tasks
@@ -2,6 +2,7 @@ import { request } from "./utils"
type Person = {
companyId: string
company: Company
}
type Company = {
@@ -11,7 +12,7 @@ type Company = {
export const summarisePeopleCreation = async (date: string) => {
const { people }: { people: Person[] } = await request(
`people?filter=createdAt[gte]:${date}`,
`people?depth=1&filter=createdAt[gte]:${date}`,
)
if (people.length === 0) {
@@ -24,12 +25,9 @@ export const summarisePeopleCreation = async (date: string) => {
for (const person of people) {
const isCompanyTracked = createdForCompanies[person.companyId]
if (person.companyId && !isCompanyTracked) {
const { company }: { company: Company } = await request(
`companies/${person.companyId}`,
)
createdForCompanies[company.id] = company
createdForCompanies[person.company.id] = person.company
if (!company.accountOwnerId) {
if (!person?.company?.accountOwnerId) {
numberOfAccountOwnerlessCompanies += 1
}
}
@@ -77,7 +77,7 @@ export const sendToSlack = async (params: {
});
return {
formattedMesage: slackMessage,
formattedMessage: slackMessage,
webhookStatus: response.status,
};
};
@@ -92,7 +92,7 @@ export const sendToDiscord = async (params: {
opportunityCreationSummary,
taskCreationSummary,
} = params;
const formattedMesage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
const formattedMessage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
**🧑‍💻 People & Companies**
${peopleCreationSummary}
@@ -105,7 +105,7 @@ ${taskCreationSummary}`;
const body = {
username: 'Twenty Bot',
content: formattedMesage,
content: formattedMessage,
};
const response = await fetch(process.env.DISCORD_WEBHOOK_URL ?? '', {
@@ -115,7 +115,7 @@ ${taskCreationSummary}`;
});
return {
formattedMesage,
formattedMessage,
webhookStatus: response.status,
};
};
@@ -130,7 +130,7 @@ export const sendToWhatsApp = async (params: {
opportunityCreationSummary,
taskCreationSummary,
} = params;
const formattedMesage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
const formattedMessage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
*🧑‍💻 People & Companies*
${peopleCreationSummary}
@@ -156,7 +156,7 @@ ${taskCreationSummary}`;
type: 'text',
text: {
preview_url: true,
body: formattedMesage,
body: formattedMessage,
},
}),
},
@@ -165,7 +165,7 @@ ${taskCreationSummary}`;
const responseBody = await response.json();
return {
formattedMesage,
formattedMessage,
webhookStatus: response.status,
webhookResponse: responseBody,
};
@@ -16,26 +16,23 @@ export const summariseTaskCreation = async (date: string) => {
return '- No Tasks were added'
}
const presentDateISOString = new Date().toISOString()
const workspaceMemberIdsDueDateCounter: Record<string, number> = {}
let workspaceMembers = []
const completedTasks: Task[] = []
const pendingTasksWithPastDueDates = tasks.reduce((collection: Task[], task) => {
tasks.forEach((task) => {
if (task.status === 'DONE') {
completedTasks.push(task)
return collection
return
}
if (new Date().toISOString() > task.dueAt) {
if (presentDateISOString >= task.dueAt) {
if (!workspaceMemberIdsDueDateCounter[task.createdBy.workspaceMemberId]) {
workspaceMemberIdsDueDateCounter[task.createdBy.workspaceMemberId] = 0
}
workspaceMemberIdsDueDateCounter[task.createdBy.workspaceMemberId] += 1
collection.push(task)
}
return collection
}, [])
})
for (const { userId, count } of findMaxIncompleteKeys(workspaceMemberIdsDueDateCounter)) {
const data = await request(`workspaceMembers/${userId}`)
@@ -61,8 +58,17 @@ export const summariseTaskCreation = async (date: string) => {
}
}, '') : 'No one was caught slacking!'
const taskCompletionPercentage = pendingTasksWithPastDueDates.length > 0 ? 100 - (100 - ((pendingTasksWithPastDueDates.length / (tasks.length - completedTasks.length)) * 100)) : NaN
const taskCompletionMessage = isNaN(taskCompletionPercentage) ? 'No Tasks to be completed' : `${taskCompletionPercentage.toFixed(2)}% Tasks were completed on time`
const tasksCompletedOnTime = tasks.filter(
task => task.status === 'DONE' && task.dueAt >= presentDateISOString
)
const taskCompletionPercentage = tasksCompletedOnTime.length > 0
? (tasksCompletedOnTime.length / tasks.length) * 100
: NaN
const taskCompletionMessage = isNaN(taskCompletionPercentage)
? 'No completed Tasks yet'
: `${taskCompletionPercentage.toFixed(2)}% of Tasks were completed on time`
return `- ${tasks.length} Tasks were created
- ${taskCompletionMessage}