Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 72 additions & 10 deletions dapp/src/telegramService.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
async function sendTelegram({

Check warning on line 1 in dapp/src/telegramService.js

View workflow job for this annotation

GitHub Actions / check-code

Refactor this function to reduce its Cognitive Complexity from 20 to the 15 allowed
chatId,
message,
botToken,
senderName = 'Web3Telegram Dapp',
maxRetries = 10,
initialDelay = 1000,
}) {
const messageToSend = `Message from: ${senderName}\n${message}`;
const response = await fetch(
`https://api.telegram.org/bot${botToken}/sendMessage`,
{

const sendMessage = async () => {
// wait 1 second before each call to avoid rate limit
await new Promise((resolve) => {
setTimeout(resolve, 1000);
});

return fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -17,14 +24,69 @@
text: messageToSend,
parse_mode: 'HTML',
}),
});
};

// retry logic with exponential backoff for handling rate limits (429) and network errors
// eslint-disable-next-line no-plusplus
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
try {
// eslint-disable-next-line no-await-in-loop
const response = await sendMessage();

if (response.ok) {
return;
}

if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter, 10) * 1000
: initialDelay * 2 ** attempt;

if (attempt < maxRetries) {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, delay);
});
// eslint-disable-next-line no-continue
continue;
}

throw new Error(
`Failed to send Telegram message: Rate limit exceeded after ${
maxRetries + 1
} attempts`
);
}

// other HTTP errors - throw directly, no retry
throw new Error(
`Failed to send Telegram message, bot API answered with status: ${response.status}`
);
} catch (error) {
// if it's an HTTP error (404, 400, etc.) or rate limit error, re-throw immediately
if (
error.message.includes('Rate limit') ||
error.message.includes('Failed to send')
) {
throw error;
}

// network errors - retry with exponential backoff
if (attempt < maxRetries) {
const delay = initialDelay * 2 ** attempt;
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, delay);
});
// eslint-disable-next-line no-continue
continue;
}

// max retries reached for network errors
throw new Error('Failed to reach Telegram bot API');
}
).catch(() => {
throw new Error('Failed to reach Telegram bot API');
});
if (!response.ok) {
throw new Error(
`Failed to send Telegram message, bot API answered with status: ${response.status}`
);
}
}

Expand Down
54 changes: 46 additions & 8 deletions dapp/tests/_test_outputs_/iexec_out/result.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
{
"success": false,
"error": "Partial failure",
"totalCount": 2,
"successCount": 1,
"errorCount": 1,
"success": true,
"totalCount": 10,
"successCount": 10,
"errorCount": 0,
"results": [
{
"index": 1,
Expand All @@ -12,9 +11,48 @@
},
{
"index": 2,
"protectedData": "invalid-data.zip",
"success": false,
"error": "Failed to parse ProtectedData 2: Failed to load protected data"
"protectedData": "data-chatId.zip",
"success": true
},
{
"index": 3,
"protectedData": "data-chatId.zip",
"success": true
},
{
"index": 4,
"protectedData": "data-chatId.zip",
"success": true
},
{
"index": 5,
"protectedData": "data-chatId.zip",
"success": true
},
{
"index": 6,
"protectedData": "data-chatId.zip",
"success": true
},
{
"index": 7,
"protectedData": "data-chatId.zip",
"success": true
},
{
"index": 8,
"protectedData": "data-chatId.zip",
"success": true
},
{
"index": 9,
"protectedData": "data-chatId.zip",
"success": true
},
{
"index": 10,
"protectedData": "data-chatId.zip",
"success": true
}
]
}
66 changes: 65 additions & 1 deletion dapp/tests/e2e/app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ describe('sendTelegram', () => {
expect(computed).toStrictEqual({
'deterministic-output-path': `${IEXEC_OUT}/result.json`,
});
});
}, 10000);

it('should send a telegram message successfully', async () => {
await expect(start()).resolves.toBeUndefined();
Expand All @@ -220,6 +220,36 @@ describe('sendTelegram', () => {
});
});

describe('Rate Limiting', () => {
it('should handle multiple messages with 1 second delay between calls', async () => {
// Setup bulk processing with 5 messages to test rate limiting
process.env.IEXEC_BULK_SLICE_SIZE = '5';
// eslint-disable-next-line no-plusplus
for (let i = 1; i <= 5; i += 1) {
process.env[`IEXEC_DATASET_${i}_FILENAME`] = 'data-chatId.zip';
}

await expect(start()).resolves.toBeUndefined();

const { IEXEC_OUT } = process.env;
const { result, computed } = await readOutputs(IEXEC_OUT);

expect(result.success).toBe(true);
expect(result.totalCount).toBe(5);
expect(result.successCount).toBe(5);
expect(result.errorCount).toBe(0);

// Verify all messages were sent successfully
result.results.forEach((r) => {
expect(r.success).toBe(true);
});

expect(computed).toStrictEqual({
'deterministic-output-path': `${IEXEC_OUT}/result.json`,
});
}, 30000); // 30 seconds timeout for 5 messages with delays
});

describe('Bulk Processing', () => {
beforeEach(() => {
// Setup bulk processing environment
Expand Down Expand Up @@ -294,4 +324,38 @@ describe('sendTelegram', () => {
});
});
});

describe('Retry mechanism with rate limiting', () => {
it('should successfully send 10 Telegram messages with 1 second delay and handle retries', async () => {
// Setup bulk processing with 10 protected data to test rate limiting and retries
process.env.IEXEC_BULK_SLICE_SIZE = '10';

// eslint-disable-next-line no-plusplus
for (let i = 1; i <= 10; i += 1) {
process.env[`IEXEC_DATASET_${i}_FILENAME`] = 'data-chatId.zip';
}

await expect(start()).resolves.toBeUndefined();

const { IEXEC_OUT } = process.env;
const { result, computed } = await readOutputs(IEXEC_OUT);

expect(result.success).toBe(true);
expect(result.totalCount).toBe(10);
expect(result.successCount).toBe(10);
expect(result.errorCount).toBe(0);
expect(result.results).toHaveLength(10);

// Verify all messages were sent successfully
result.results.forEach((r, index) => {
expect(r.success).toBe(true);
expect(r.index).toBe(index + 1);
expect(r.protectedData).toBe('data-chatId.zip');
});

expect(computed).toStrictEqual({
'deterministic-output-path': `${IEXEC_OUT}/result.json`,
});
}, 60000); // 60 seconds timeout for 10 messages with delays and potential retries
});
});
Loading
Loading