Skip to content

Commit 1b95f7d

Browse files
chore: wip
1 parent 5681edd commit 1b95f7d

File tree

7 files changed

+428
-27
lines changed

7 files changed

+428
-27
lines changed

config/services.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default {
3535
},
3636

3737
mailtrap: {
38+
host: env.MAILTRAP_HOST,
3839
token: env.MAILTRAP_TOKEN,
3940
inboxId: env.MAILTRAP_INBOX_ID,
4041
maxRetries: env.MAILTRAP_MAX_RETRIES ? Number.parseInt(env.MAILTRAP_MAX_RETRIES) : 3,
@@ -70,6 +71,13 @@ export default {
7071
// apiKey: '',
7172
// },
7273

74+
slack: {
75+
appId: env.SLACK_APP_ID,
76+
clientId: env.SLACK_CLIENT_ID,
77+
secretKey: env.SLACK_SECRET_KEY,
78+
maxRetries: env.SLACK_MAX_RETRIES ? Number.parseInt(env.SLACK_MAX_RETRIES) : 3,
79+
retryTimeout: env.SENDGRID_RETRY_TIMEOUT ? Number.parseInt(env.SENDGRID_RETRY_TIMEOUT) : 1000,
80+
},
7381
stripe: {
7482
appId: '',
7583
apiKey: '',

storage/framework/actions/src/UserShowOrmAction.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Action } from '@stacksjs/actions'
22
import { mail } from '@stacksjs/email'
3-
import { response } from '@stacksjs/router'
43

54
export default new Action({
65
name: 'User Show',
@@ -14,7 +13,7 @@ export default new Action({
1413
},
1514
to: 'chrisbreuer93@gmail.com',
1615
subject: 'Test Email',
17-
text: 'test',
16+
template: 'WelcomeTest',
1817
})
1918

2019
// async handle(request: UserRequestType) {
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import type { ChatDriver, ChatDriverConfig, ChatMessage, ChatResult, RenderOptions } from '@stacksjs/types'
2+
import { log } from '@stacksjs/logging'
3+
4+
export abstract class BaseChatDriver implements ChatDriver {
5+
public abstract name: string
6+
protected config: Required<ChatDriverConfig>
7+
8+
constructor(config?: ChatDriverConfig) {
9+
this.config = {
10+
maxRetries: config?.maxRetries || 3,
11+
retryTimeout: config?.retryTimeout || 1000,
12+
...config,
13+
}
14+
}
15+
16+
public configure(config: ChatDriverConfig): void {
17+
this.config = { ...this.config, ...config }
18+
}
19+
20+
public abstract send(message: ChatMessage, options?: RenderOptions): Promise<ChatResult>
21+
22+
/**
23+
* Validates chat message fields
24+
*/
25+
protected validateMessage(message: ChatMessage): boolean {
26+
if (!message.to) {
27+
throw new Error('Message recipient is required')
28+
}
29+
30+
if (!message.content && !message.template) {
31+
throw new Error('Either message content or template is required')
32+
}
33+
34+
return true
35+
}
36+
37+
/**
38+
* Error handler with standard formatting
39+
*/
40+
protected async handleError(error: unknown, message: ChatMessage): Promise<ChatResult> {
41+
const err = error instanceof Error ? error : new Error(String(error))
42+
43+
log.error(`[${this.name}] Message sending failed`, {
44+
error: err.message,
45+
stack: err.stack,
46+
to: message.to,
47+
subject: message.subject,
48+
})
49+
50+
let result: ChatResult = {
51+
message: `Message sending failed: ${err.message}`,
52+
success: false,
53+
provider: this.name,
54+
}
55+
56+
if (message.onError) {
57+
const customResult = message.onError(err)
58+
const handlerResult = customResult instanceof Promise
59+
? await customResult
60+
: customResult
61+
62+
result = {
63+
...result,
64+
...handlerResult,
65+
success: false,
66+
provider: this.name,
67+
}
68+
}
69+
70+
return result
71+
}
72+
73+
/**
74+
* Success handler with standard formatting
75+
*/
76+
protected async handleSuccess(message: ChatMessage, messageId?: string): Promise<ChatResult> {
77+
let result: ChatResult = {
78+
message: 'Message sent successfully',
79+
success: true,
80+
provider: this.name,
81+
messageId,
82+
}
83+
84+
try {
85+
if (message.handle) {
86+
const customResult = message.handle()
87+
const handlerResult = customResult instanceof Promise
88+
? await customResult
89+
: customResult
90+
91+
result = {
92+
...result,
93+
...handlerResult,
94+
success: true,
95+
provider: this.name,
96+
messageId,
97+
}
98+
}
99+
100+
if (message.onSuccess) {
101+
const successResult = message.onSuccess()
102+
const handlerResult = successResult instanceof Promise
103+
? await successResult
104+
: successResult
105+
106+
result = {
107+
...result,
108+
...handlerResult,
109+
success: true,
110+
provider: this.name,
111+
messageId,
112+
}
113+
}
114+
}
115+
catch (error) {
116+
return this.handleError(error, message)
117+
}
118+
119+
return result
120+
}
121+
}
Lines changed: 77 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,77 @@
1-
// import { SlackProvider } from '@novu/slack'
2-
// import { italic } from '@stacksjs/cli'
3-
// import type { ChatOptions } from '@stacksjs/types'
4-
// import { ResultAsync } from '@stacksjs/error-handling'
5-
// import { notification } from '@stacksjs/config'
6-
7-
// const env = notification.chat.slack
8-
9-
// const provider = new SlackProvider({
10-
// applicationId: env.appId,
11-
// clientId: env.clientId,
12-
// secretKey: env.secret,
13-
// })
14-
15-
// function send(options: ChatOptions) {
16-
// return ResultAsync.fromPromise(
17-
// provider.sendMessage(options),
18-
// () => new Error(`Failed to send message using provider: ${italic('Slack')}`),
19-
// )
20-
// }
21-
22-
// export { send as Send, send }
23-
24-
export type {}
1+
import type { ChatMessage, ChatOptions, ChatResult } from '@stacksjs/types'
2+
import { SlackProvider } from '@novu/slack'
3+
import { config } from '@stacksjs/config'
4+
import { log } from '@stacksjs/logging'
5+
import { BaseChatDriver } from './base'
6+
7+
export class SlackDriver extends BaseChatDriver {
8+
public name = 'slack'
9+
private provider: SlackProvider
10+
11+
constructor() {
12+
super()
13+
const env = config.services.slack
14+
15+
this.provider = new SlackProvider({
16+
applicationId: env?.appId ?? '',
17+
clientId: env?.clientId ?? '',
18+
secretKey: env?.secret ?? '',
19+
})
20+
}
21+
22+
public async send(message: ChatMessage, options?: ChatOptions): Promise<ChatResult> {
23+
const logContext = {
24+
provider: this.name,
25+
to: message.to,
26+
subject: message.subject || 'No subject',
27+
}
28+
29+
log.info('Sending message via Slack...', logContext)
30+
31+
try {
32+
this.validateMessage(message)
33+
34+
const payload = {
35+
...message,
36+
...options,
37+
}
38+
39+
const response = await this.sendWithRetry(payload)
40+
return this.handleSuccess(message, response.id)
41+
}
42+
catch (error) {
43+
return this.handleError(error, message)
44+
}
45+
}
46+
47+
private async sendWithRetry(payload: any, attempt = 1): Promise<any> {
48+
try {
49+
const response = await this.provider.sendMessage(payload)
50+
51+
log.info(`[${this.name}] Message sent successfully`, {
52+
attempt,
53+
messageId: response.id,
54+
})
55+
56+
return response
57+
}
58+
catch (error) {
59+
if (attempt < (config.services.slack?.maxRetries ?? 3)) {
60+
const retryTimeout = config.services.slack?.retryTimeout ?? 1000
61+
log.warn(`[${this.name}] Message send failed, retrying (${attempt}/${config.services.slack?.maxRetries ?? 3})`)
62+
await new Promise(resolve => setTimeout(resolve, retryTimeout))
63+
return this.sendWithRetry(payload, attempt + 1)
64+
}
65+
throw error
66+
}
67+
}
68+
}
69+
70+
export default SlackDriver
71+
72+
export { SlackDriver as Driver }
73+
export const driver = new SlackDriver()
74+
75+
export async function send(message: ChatMessage, options?: ChatOptions): Promise<ChatResult> {
76+
return driver.send(message, options)
77+
}

0 commit comments

Comments
 (0)