diff --git a/package-lock.json b/package-lock.json index a4636c95..04f70a5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1963,22 +1963,22 @@ } }, "node_modules/@slack/logger": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-3.0.0.tgz", - "integrity": "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.1.tgz", + "integrity": "sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==", "license": "MIT", "dependencies": { - "@types/node": ">=12.0.0" + "@types/node": ">=18" }, "engines": { - "node": ">= 12.13.0", - "npm": ">= 6.12.0" + "node": ">= 18", + "npm": ">= 8.6.0" } }, "node_modules/@slack/types": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.19.0.tgz", - "integrity": "sha512-7+QZ38HGcNh/b/7MpvPG6jnw7mliV6UmrquJLqgdxkzJgQEYUcEztvFWRU49z0x4vthF0ixL5lTK601AXrS8IA==", + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.20.1.tgz", + "integrity": "sha512-eWX2mdt1ktpn8+40iiMc404uGrih+2fxiky3zBcPjtXKj6HLRdYlmhrPkJi7JTJm8dpXR6BWVWEDBXtaWMKD6A==", "license": "MIT", "engines": { "node": ">= 12.13.0", @@ -1986,55 +1986,40 @@ } }, "node_modules/@slack/web-api": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-6.13.0.tgz", - "integrity": "sha512-dv65crIgdh9ZYHrevLU6XFHTQwTyDmNqEqzuIrV+Vqe/vgiG6w37oex5ePDU1RGm2IJ90H8iOvHFvzdEO/vB+g==", - "license": "MIT", - "dependencies": { - "@slack/logger": "^3.0.0", - "@slack/types": "^2.11.0", - "@types/is-stream": "^1.1.0", - "@types/node": ">=12.0.0", - "axios": "^1.7.4", - "eventemitter3": "^3.1.0", - "form-data": "^2.5.0", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.15.0.tgz", + "integrity": "sha512-va7zYIt3QHG1x9M/jqXXRPFMoOVlVSSRHC5YH+DzKYsrz5xUKOA3lR4THsu/Zxha9N1jOndbKFKLtr0WOPW1Vw==", + "license": "MIT", + "dependencies": { + "@slack/logger": "^4.0.1", + "@slack/types": "^2.20.1", + "@types/node": ">=18", + "@types/retry": "0.12.0", + "axios": "^1.13.5", + "eventemitter3": "^5.0.1", + "form-data": "^4.0.4", "is-electron": "2.2.2", - "is-stream": "^1.1.0", - "p-queue": "^6.6.1", - "p-retry": "^4.0.0" + "is-stream": "^2", + "p-queue": "^6", + "p-retry": "^4", + "retry": "^0.13.1" }, "engines": { - "node": ">= 12.13.0", - "npm": ">= 6.12.0" + "node": ">= 18", + "npm": ">= 8.6.0" } }, "node_modules/@slack/web-api/node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, - "node_modules/@slack/web-api/node_modules/axios/node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/@slack/web-api/node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -2055,6 +2040,18 @@ } } }, + "node_modules/@slack/web-api/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@so-ric/colorspace": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", @@ -2221,15 +2218,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@types/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2299,10 +2287,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "license": "MIT" + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/node-fetch": { "version": "2.6.13", @@ -2314,22 +2305,6 @@ "form-data": "^4.0.4" } }, - "node_modules/@types/node-fetch/node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -5395,9 +5370,9 @@ } }, "node_modules/eventemitter3": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", - "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, "node_modules/execa": { @@ -6068,20 +6043,19 @@ } }, "node_modules/form-data": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", - "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", - "mime-types": "^2.1.35", - "safe-buffer": "^5.2.1" + "mime-types": "^2.1.12" }, "engines": { - "node": ">= 0.12" + "node": ">= 6" } }, "node_modules/form-data-encoder": { @@ -7463,6 +7437,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10852,15 +10827,6 @@ } } }, - "node_modules/openai/node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/openai/node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -14708,7 +14674,7 @@ "license": "ISC", "dependencies": { "@google/genai": "^1.30.0", - "@slack/web-api": "^6.8.0", + "@slack/web-api": "^7.15.0", "axios": "^0.18.1", "body-parser": "^1.20.2", "decimal.js": "^10.6.0", @@ -14731,7 +14697,7 @@ "@types/jest": "^24.0.15", "@types/jest-when": "^2.7.0", "@types/lolex": "^3.1.1", - "@types/node": "^12.12.56", + "@types/node": "^18.0.0", "@types/sentiment": "^5.0.1", "@types/uuid": "^9.0.6", "dotenv": "^16.3.1", diff --git a/packages/backend/package.json b/packages/backend/package.json index 6bce7dac..4471f0fd 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -19,7 +19,7 @@ "license": "ISC", "dependencies": { "@google/genai": "^1.30.0", - "@slack/web-api": "^6.8.0", + "@slack/web-api": "^7.15.0", "axios": "^0.18.1", "body-parser": "^1.20.2", "decimal.js": "^10.6.0", @@ -42,7 +42,7 @@ "@types/jest": "^24.0.15", "@types/jest-when": "^2.7.0", "@types/lolex": "^3.1.1", - "@types/node": "^12.12.56", + "@types/node": "^18.0.0", "@types/sentiment": "^5.0.1", "@types/uuid": "^9.0.6", "dotenv": "^16.3.1", diff --git a/packages/backend/src/ai/ai.service.spec.ts b/packages/backend/src/ai/ai.service.spec.ts index 3cac77bd..c0b0992e 100644 --- a/packages/backend/src/ai/ai.service.spec.ts +++ b/packages/backend/src/ai/ai.service.spec.ts @@ -11,7 +11,6 @@ jest.mock('./openai/openai.service', () => ({ OpenAIService: jest.fn().mockImplementation(() => ({ generateText: jest.fn(), generateImage: jest.fn(), - convertAsterisks: jest.fn(), })), })); jest.mock('./ai.persistence', () => mockAiPersistenceService); @@ -234,7 +233,7 @@ describe('AIService', () => { true, ); expect(sendMessageMock).toHaveBeenCalledWith('channel123', 'Prompt', [ - { text: { text: 'Response with context', type: 'mrkdwn' }, type: 'section' }, + { type: 'markdown', text: 'Response with context' }, { type: 'divider' }, { elements: [ @@ -334,7 +333,7 @@ describe('AIService', () => { expect.arrayContaining([ expect.objectContaining({ type: 'image' }), expect.objectContaining({ type: 'header' }), - expect.objectContaining({ type: 'section' }), + expect.objectContaining({ type: 'markdown' }), ]), ); }); diff --git a/packages/backend/src/ai/ai.service.ts b/packages/backend/src/ai/ai.service.ts index c4f85801..bcefae3e 100644 --- a/packages/backend/src/ai/ai.service.ts +++ b/packages/backend/src/ai/ai.service.ts @@ -7,7 +7,6 @@ import { EventRequest, SlashCommandRequest } from '../shared/models/slack/slack- import { AIPersistenceService } from './ai.persistence'; import { KnownBlock } from '@slack/web-api'; import { WebService } from '../shared/services/web/web.service'; -import { getChunks } from '../shared/util/getChunks'; import { CORPO_SPEAK_INSTRUCTIONS, GENERAL_TEXT_INSTRUCTIONS, @@ -123,11 +122,8 @@ export class AIService { }, }, { - type: 'section', - text: { - type: 'mrkdwn', - text: `"${quote}"`, - }, + type: 'markdown', + text: `"${quote}"`, }, ]; this.webService.sendMessage('#muzzlefeedback', 'Moonbeam has been deployed.', blocks); @@ -215,35 +211,24 @@ export class AIService { return; } - const blocks: KnownBlock[] = []; - - const chunks = getChunks(result); - - if (chunks) { - chunks.forEach((chunk) => { - blocks.push({ - type: 'section', - text: { + const blocks: KnownBlock[] = [ + { + type: 'markdown', + text: result, + }, + { + type: 'divider', + }, + { + type: 'context', + elements: [ + { type: 'mrkdwn', - text: `${chunk}`, + text: `:technologist: _Context-aware prompt generated by <@${request.user_id}> | "${request.text}"_ :technologist:`, }, - }); - }); - } - - blocks.push({ - type: 'divider', - }); - - blocks.push({ - type: 'context', - elements: [ - { - type: 'mrkdwn', - text: `:technologist: _Context-aware prompt generated by <@${request.user_id}> | "${request.text}"_ :technologist:`, - }, - ], - }); + ], + }, + ]; this.webService.sendMessage(request.channel_id, request.text, blocks).catch((e) => { this.aiServiceLogger.error(e); @@ -283,8 +268,14 @@ export class AIService { .generateText(input, 'Moonbeam', MOONBEAM_SYSTEM_INSTRUCTIONS) .then((result) => { if (result) { + const blocks: KnownBlock[] = [ + { + type: 'markdown', + text: result, + }, + ]; this.webService - .sendMessage(channelId, result) + .sendMessage(channelId, result, blocks) .then(() => this.redis.setHasParticipated(teamId, channelId)) .catch((e) => this.aiServiceLogger.error('Error sending AI Participation message:', e)); } @@ -329,35 +320,24 @@ export class AIService { sendGptText(text: string | undefined, userId: string, teamId: string, channelId: string, query: string): void { if (text) { - const blocks: KnownBlock[] = []; - - const chunks = getChunks(text); - - if (chunks) { - chunks.forEach((chunk) => { - blocks.push({ - type: 'section', - text: { + const blocks: KnownBlock[] = [ + { + type: 'markdown', + text: text, + }, + { + type: 'divider', + }, + { + type: 'context', + elements: [ + { type: 'mrkdwn', - text: `${chunk}`, + text: `:altman: _Generated by <@${userId}> | "${query}"_ :altman:`, }, - }); - }); - } - - blocks.push({ - type: 'divider', - }); - - blocks.push({ - type: 'context', - elements: [ - { - type: 'mrkdwn', - text: `:altman: _Generated by <@${userId}> | "${query}"_ :altman:`, - }, - ], - }); + ], + }, + ]; this.webService.sendMessage(channelId, text, blocks).catch((e) => { this.aiServiceLogger.error(e); diff --git a/packages/backend/src/ai/openai/openai.service.spec.ts b/packages/backend/src/ai/openai/openai.service.spec.ts index c065f9bb..ae0e0c8a 100644 --- a/packages/backend/src/ai/openai/openai.service.spec.ts +++ b/packages/backend/src/ai/openai/openai.service.spec.ts @@ -62,7 +62,7 @@ describe('OpenAIService', () => { const result = await service.generateText('Say hello', 'user123'); - expect(result).toBe('Hello *world*! This is _italic_.'); + expect(result).toBe('Hello **world**! This is *italic*.'); }); it('should return undefined when no output_text is found', async () => { @@ -156,164 +156,6 @@ describe('OpenAIService', () => { }); }); - describe('markdownToSlackMrkdwn', () => { - it('should convert bold markdown to Slack format', () => { - const input = 'This is **bold** text'; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe('This is *bold* text'); - }); - - it('should convert italic markdown to Slack format', () => { - const input = 'This is *italic* text'; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe('This is _italic_ text'); - }); - - it('should preserve code blocks', () => { - const input = 'This is `code` text'; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe('This is `code` text'); - }); - - it('should convert links to Slack format', () => { - const input = 'Check out [Google](https://google.com)'; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe('Check out '); - }); - - it('should convert images to Slack format', () => { - const input = 'Look at this ![cat](https://example.com/cat.jpg)'; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe('Look at this '); - }); - - it('should handle multiple formatting types in one string', () => { - const input = 'This is **bold** and *italic* with `code` and [link](https://test.com)'; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe('This is *bold* and _italic_ with `code` and '); - }); - - it('should return undefined for undefined input', () => { - const result = service.markdownToSlackMrkdwn(undefined); - expect(result).toBeUndefined(); - }); - - it('should return empty string for empty input', () => { - const result = service.markdownToSlackMrkdwn(''); - expect(result).toBe(''); - }); - - it('should handle text with no markdown', () => { - const input = 'Just plain text'; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe('Just plain text'); - }); - - it('should handle nested formatting correctly', () => { - const input = 'This is **bold with *italic* inside**'; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe('This is *bold with _italic_ inside*'); - }); - - it('should convert H1 headings to bold format', () => { - const input = '# Main Heading'; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe('*Main Heading*'); - }); - - it('should convert H2 headings to bold format', () => { - const input = '## Secondary Heading'; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe('*Secondary Heading*'); - }); - - it('should convert H3 headings to italic format', () => { - const input = '### Tertiary Heading'; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe('_Tertiary Heading_'); - }); - - it('should handle multiple headings in text', () => { - const input = `# Main Title - ## Subtitle - ### Section - Some content here`; - const expected = `*Main Title* - *Subtitle* - _Section_ - Some content here`; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe(expected); - }); - - it('should handle multiple new lines with different formatting', () => { - const input = `# Heading 1 - ## Heading 2 - ### Heading 3`; - const expected = `*Heading 1* - *Heading 2* - _Heading 3_`; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe(expected); - }); - - it('should handle multiple new lines with the same formatting (bold)', () => { - const input = `**This should - All Be - Bold**`; - const expected = `*This should - All Be - Bold*`; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe(expected); - }); - - it('shold handle multiple new lines with the same formatting (italic)', () => { - const input = `*This should - All Be - Italic*`; - const expected = `_This should - All Be - Italic_`; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe(expected); - }); - - it('should handle multiple lines with the same formatting (bold and italic)', () => { - const input = `**This should - All Be - Bold** and *Italic*`; - const expected = `*This should - All Be - Bold* and _Italic_`; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe(expected); - }); - - it('should handle headings with other formatting', () => { - const input = '# **Bold** heading with *italic*'; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe('**Bold* heading with _italic_*'); - }); - - it('should not convert headings without space after hash', () => { - const input = '#NotAHeading'; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe('#NotAHeading'); - }); - - it('should handle headings at different positions in text', () => { - const input = `Some text - ## Middle Heading - More text`; - const expected = `Some text - *Middle Heading* - More text`; - const result = service.markdownToSlackMrkdwn(input); - expect(result).toBe(expected); - }); - }); - describe('error handling', () => { it('should handle OpenAI API errors in generateText', async () => { mockOpenAI.responses.create.mockRejectedValue(new Error('API Error')); diff --git a/packages/backend/src/ai/openai/openai.service.ts b/packages/backend/src/ai/openai/openai.service.ts index 9a73d443..3cdd1112 100644 --- a/packages/backend/src/ai/openai/openai.service.ts +++ b/packages/backend/src/ai/openai/openai.service.ts @@ -30,7 +30,7 @@ export class OpenAIService { (block: ResponseOutputText | ResponseOutputRefusal) => block.type === 'output_text', ) as ResponseOutputText )?.text; - return this.markdownToSlackMrkdwn(outputText?.trim()); + return outputText?.trim(); }); }; @@ -49,55 +49,4 @@ export class OpenAIService { }); }; - markdownToSlackMrkdwn = (text?: string) => { - if (!text) { - return text; - } - - // Convert ![alt text](image url) to (do this first to avoid conflicts with links) - text = text.replace(/!\[(.*?)\]\((.*?)\)/g, '<$2|$1>'); - - // Convert [link](url) to - text = text.replace(/\[(.*?)\]\((.*?)\)/g, '<$2|$1>'); - - // Convert `code` to `code` (no change needed, but process before bold/italic to avoid conflicts) - text = text.replace(/`(.*?)`/g, '`$1`'); - - // Use a more robust approach for bold and italic - // First, temporarily replace **bold** with a placeholder to avoid conflicts - const boldPlaceholder = 'BOLD_PLACEHOLDER'; - const boldMatches: string[] = []; - - // Extract bold text and replace with placeholders, but first process italic within bold - text = text.replace(/\*\*([\s\S]*?)\*\*/g, (_, content) => { - // Process italic formatting within the bold content - const processedContent = content.replace(/\*([^*]+?)\*/g, '_$1_'); - boldMatches.push(processedContent); - return `${boldPlaceholder}${boldMatches.length - 1}${boldPlaceholder}`; - }); - - // Now convert remaining single asterisks to italic (underscores) - text = text.replace(/\*([^*]+?)\*/g, '_$1_'); - - // Restore bold text with Slack formatting - text = text.replace(new RegExp(`${boldPlaceholder}(\\d+)${boldPlaceholder}`, 'g'), (_, index) => { - return `*${boldMatches[parseInt(index)]}*`; - }); - - // Convert #, ## and ### headings to Slack's mrkdwn format - text = text.replace(/^(\s*)(#{1,3})\s+(.*)$/gm, (_, leadingSpace, hashes, content) => { - const level = hashes.length; - switch (level) { - case 1: - case 2: - return `${leadingSpace}*${content}*`; // Preserve leading whitespace - case 3: - return `${leadingSpace}_${content}_`; // Preserve leading whitespace - default: - return `${leadingSpace}${content}`; - } - }); - - return text; - }; } diff --git a/packages/backend/src/shared/services/web/web.service.ts b/packages/backend/src/shared/services/web/web.service.ts index 41821b13..a630c74e 100644 --- a/packages/backend/src/shared/services/web/web.service.ts +++ b/packages/backend/src/shared/services/web/web.service.ts @@ -1,7 +1,6 @@ import { ChatDeleteArguments, ChatPostMessageArguments, - FilesUploadArguments, WebAPICallResult, WebClient, ChatPostEphemeralArguments, @@ -30,7 +29,6 @@ export class WebService { token: muzzleToken, channel, ts, - as_user: true, }; this.web.chat @@ -77,16 +75,13 @@ export class WebService { */ public sendMessage(channel: string, text: string, blocks?: Block[] | KnownBlock[]): Promise { const token: string | undefined = process.env.MUZZLE_BOT_USER_TOKEN; - const postRequest: ChatPostMessageArguments = { + const postRequest = { token, channel, text, unfurl_links: false, - }; - - if (blocks) { - postRequest.blocks = blocks; - } + ...(blocks && { blocks }), + } as ChatPostMessageArguments; return this.web.chat .postMessage(postRequest) @@ -111,7 +106,7 @@ export class WebService { } public getAllUsers(): Promise { - return this.web.users.list(); + return this.web.users.list({}); } public getAllChannels(): Promise { @@ -119,22 +114,18 @@ export class WebService { } public uploadFile(channel: string, content: string, title: string, userId: string): void { - const muzzleToken: string | undefined = process.env.MUZZLE_BOT_USER_TOKEN; - const uploadRequest: FilesUploadArguments = { - channels: channel, + this.web.filesUploadV2({ + channel_id: channel, content, - filetype: 'auto', title, + filename: `${title}.txt`, initial_comment: title, - token: muzzleToken, - }; - - this.web.files.upload(uploadRequest).catch((e: unknown) => { + }).catch((e: unknown) => { this.logger.error(e); const options: ChatPostEphemeralArguments = { channel, text: - (e as Record>).data.error === 'not_in_channel' + (e as Record>).data?.error === 'not_in_channel' ? `Oops! I tried to post the stats you requested but it looks like I haven't been added to that channel yet. Can you please add me? Just type \`@muzzle\` in the channel!` : `Oops! I tried to post the stats you requested but it looks like something went wrong. Please try again later.`, user: userId, diff --git a/packages/backend/src/shared/util/getChunks.spec.ts b/packages/backend/src/shared/util/getChunks.spec.ts deleted file mode 100644 index 7b11e48b..00000000 --- a/packages/backend/src/shared/util/getChunks.spec.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { getChunks, MAX_CHUNK_SIZE, splitByWords, splitLongWord, splitSentenceByWords } from './getChunks'; - -describe('getChunks', () => { - it('should just return text if length is <= to 2920', () => { - const text = 'a '.repeat(1460); - const result = getChunks(text); - expect(result).toEqual([text]); - }); - - it('returns the whole text if under the limit', () => { - const text = 'Short text.'; - expect(getChunks(text)).toEqual([text]); - }); - - it('splits text at sentence boundaries when possible', () => { - const sentence = 'A sentence. '; - const text = sentence.repeat(Math.ceil(MAX_CHUNK_SIZE / sentence.length) + 2); - const chunks = getChunks(text); - expect(chunks.every((chunk) => chunk.length <= MAX_CHUNK_SIZE)).toBe(true); - // Should split at sentence boundaries - expect(chunks.length).toBeGreaterThan(1); - expect(chunks.join(' ')).toContain('A sentence.'); - }); - - it('splits a single long sentence by words if needed', () => { - const longWord = 'a'.repeat(MAX_CHUNK_SIZE - 10); - const text = `${longWord} ${longWord} ${longWord}`; - const chunks = getChunks(text); - expect(chunks.every((chunk) => chunk.length <= MAX_CHUNK_SIZE)).toBe(true); - expect(chunks.length).toBeGreaterThan(1); - }); - - it('handles a sentence longer than the chunk size', () => { - const longSentence = 'a'.repeat(MAX_CHUNK_SIZE + 100) + '.'; - const text = longSentence + ' Short sentence.'; - const chunks = getChunks(text); - expect( - chunks.every((chunk) => { - return chunk.length <= MAX_CHUNK_SIZE; - }), - ).toBe(true); - expect(chunks.length).toBeGreaterThan(1); - expect(chunks[chunks.length - 1]).toContain('Short sentence.'); - }); - - it('handles text with no punctuation', () => { - const text = 'a '.repeat(MAX_CHUNK_SIZE + 100); - const chunks = getChunks(text); - expect(chunks.every((chunk) => chunk.length <= MAX_CHUNK_SIZE)).toBe(true); - expect(chunks.length).toBeGreaterThan(1); - }); - - it('handles empty string', () => { - expect(getChunks('')).toEqual(['']); - }); - - it('handles text with various punctuation', () => { - const text = 'Hello! How are you? I am fine. Thanks for asking!'; - expect(getChunks(text)).toEqual([text]); - }); - - it('handles words longer than chunk size', () => { - const longWord = 'a'.repeat(MAX_CHUNK_SIZE + 10); - const text = `${longWord} end.`; - const chunks = getChunks(text); - expect(chunks[0].length).toBeLessThanOrEqual(MAX_CHUNK_SIZE); - expect(chunks.join(' ')).toContain('end.'); - }); -}); - -describe('splitByWords', () => { - it('splits text into chunks by words', () => { - const word = 'a'.repeat(100); - const text = Array(40).fill(word).join(' '); - const chunks = splitByWords(text); - expect(chunks.every((chunk) => chunk.length <= MAX_CHUNK_SIZE)).toBe(true); - expect(chunks.join(' ')).toBe(text); - }); - - it('handles a single word longer than MAX_CHUNK_SIZE', () => { - const longWord = 'x'.repeat(MAX_CHUNK_SIZE + 10); - const chunks = splitByWords(longWord); - expect(chunks.length).toBe(2); - expect(chunks[0].length).toBe(MAX_CHUNK_SIZE); - expect(chunks[1].length).toBe(10); - }); - - it('returns empty array for empty string', () => { - expect(splitByWords('')).toEqual([]); - }); -}); - -describe('splitSentenceByWords', () => { - it('splits a long sentence by words into chunks', () => { - const word = 'b'.repeat(200); - const sentence = Array(20).fill(word).join(' '); - const chunks: string[] = []; - splitSentenceByWords(sentence, chunks); - expect(chunks.every((chunk) => chunk.length <= MAX_CHUNK_SIZE)).toBe(true); - expect(chunks.join(' ')).toBe(sentence); - }); - - it('handles a word longer than MAX_CHUNK_SIZE in a sentence', () => { - const longWord = 'y'.repeat(MAX_CHUNK_SIZE + 5); - const sentence = `short ${longWord} end`; - const chunks: string[] = []; - splitSentenceByWords(sentence, chunks); - expect(chunks.some((chunk) => chunk.includes('short'))).toBe(true); - expect(chunks.some((chunk) => chunk.includes('end'))).toBe(true); - expect(chunks.some((chunk) => chunk.length === MAX_CHUNK_SIZE)).toBe(true); - }); - - it('handles empty sentence', () => { - const chunks: string[] = []; - splitSentenceByWords('', chunks); - expect(chunks).toEqual([]); - }); -}); - -describe('splitLongWord', () => { - it('splits a long word into fixed-size chunks', () => { - const longWord = 'z'.repeat(MAX_CHUNK_SIZE * 2 + 5); - const chunks: string[] = []; - splitLongWord(longWord, chunks); - expect(chunks.length).toBe(3); - expect(chunks[0].length).toBe(MAX_CHUNK_SIZE); - expect(chunks[1].length).toBe(MAX_CHUNK_SIZE); - expect(chunks[2].length).toBe(5); - expect(chunks.join('')).toBe(longWord); - }); - - it('handles word shorter than MAX_CHUNK_SIZE', () => { - const word = 'shortword'; - const chunks: string[] = []; - splitLongWord(word, chunks); - expect(chunks).toEqual([word]); - }); - - it('handles empty word', () => { - const chunks: string[] = []; - splitLongWord('', chunks); - expect(chunks).toEqual([]); - }); -}); diff --git a/packages/backend/src/shared/util/getChunks.ts b/packages/backend/src/shared/util/getChunks.ts deleted file mode 100644 index e880b8ce..00000000 --- a/packages/backend/src/shared/util/getChunks.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { split as splitSentences } from 'sentence-splitter'; - -export const MAX_CHUNK_SIZE = 2920; - -export function getChunks(text: string): string[] { - if (text.length <= MAX_CHUNK_SIZE) return [text]; - - // Try to split at sentence boundaries - const sentences = splitSentences(text) - .filter((token) => token.type === 'Sentence') - .map((token) => token.raw); - - if (!sentences) return splitByWords(text); - - const chunks: string[] = []; - let currentChunk = ''; - - for (const sentence of sentences) { - const trimmedSentence = sentence.trim(); - // If adding this sentence would exceed the chunk size, flush currentChunk - if ((currentChunk + trimmedSentence).length > MAX_CHUNK_SIZE) { - if (currentChunk.trim()) { - chunks.push(currentChunk.trim()); - currentChunk = ''; - } - - // If the sentence itself is too long, split by words - if (trimmedSentence.length > MAX_CHUNK_SIZE) { - splitSentenceByWords(trimmedSentence, chunks); - continue; // currentChunk is already flushed - } else { - currentChunk = trimmedSentence.trim(); - } - } else { - currentChunk += ` ${trimmedSentence.trim()}`; - } - } - - if (currentChunk.trim()) { - chunks.push(currentChunk.trim()); - } - - return chunks.map((chunk) => chunk.trim()); -} - -export function splitByWords(text: string): string[] { - const words = text.split(' '); - const chunks: string[] = []; - let currentChunk = ''; - - for (const word of words) { - // If adding this word would exceed the chunk size, flush currentChunk - if ((currentChunk ? currentChunk.length + 1 : 0) + word.length > MAX_CHUNK_SIZE) { - if (currentChunk.trim()) { - chunks.push(currentChunk.trim()); - currentChunk = ''; - } - if (word.length > MAX_CHUNK_SIZE) { - splitLongWord(word, chunks); - } else { - currentChunk = word; - } - } else { - currentChunk += (currentChunk ? ' ' : '') + word; - } - } - - if (currentChunk.trim()) { - chunks.push(currentChunk.trim()); - } - - return chunks; -} - -export function splitSentenceByWords(sentence: string, chunks: string[]) { - let wordChunk = ''; - for (const word of sentence.split(' ')) { - if ((wordChunk ? wordChunk.length + 1 : 0) + word.length > MAX_CHUNK_SIZE) { - if (wordChunk.trim()) { - chunks.push(wordChunk.trim()); - wordChunk = ''; - } - if (word.length > MAX_CHUNK_SIZE) { - splitLongWord(word, chunks); - } else { - wordChunk = word; - } - } else { - wordChunk += (wordChunk ? ' ' : '') + word; - } - } - if (wordChunk.trim()) { - chunks.push(wordChunk.trim()); - } -} - -export function splitLongWord(word: string, chunks: string[]) { - let start = 0; - while (start < word.length) { - chunks.push(word.slice(start, start + MAX_CHUNK_SIZE)); - start += MAX_CHUNK_SIZE; - } -}