-
Notifications
You must be signed in to change notification settings - Fork 215
feat: mattermost connector bot #1142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ALTER TABLE "chat" ADD COLUMN "mattermost_thread_id" text;--> statement-breakpoint | ||
| ALTER TABLE "project" ADD COLUMN "mattermost_settings" jsonb;--> statement-breakpoint | ||
| CREATE INDEX "chat_mattermost_thread_idx" ON "chat" USING btree ("mattermost_thread_id"); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ALTER TABLE `chat` ADD `mattermost_thread_id` text;--> statement-breakpoint | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: SQLite migration checks will fail because the new 0056 migration has no matching Prompt for AI agents |
||
| CREATE INDEX `chat_mattermost_thread_idx` ON `chat` (`mattermost_thread_id`);--> statement-breakpoint | ||
| ALTER TABLE `project` ADD `mattermost_settings` text; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -81,6 +81,7 @@ | |
| "better-auth": "^1.6.3", | ||
| "better-sqlite3": "^12.5.0", | ||
| "chat": "^4.15.0", | ||
| "chat-adapter-mattermost": "^1.1.3", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: The Mattermost adapter requires chat@^4.29.0 as a peer dependency, but the project currently specifies chat@^4.15.0. If the resolved chat version is older than 4.29.0 the adapter may encounter missing APIs or behavioral changes at runtime. Consider bumping the project's chat dependency to ^4.29.0 to ensure compatibility with chat-adapter-mattermost@^1.1.3. Prompt for AI agents |
||
| "cheerio": "^1.2.0", | ||
| "cron-parser": "^5.5.0", | ||
| "dotenv": "^17.2.3", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| import type { LlmProvider, LlmSelectedModel } from '@nao/shared/types'; | ||
| import { eq } from 'drizzle-orm'; | ||
|
|
||
| import s from '../db/abstractSchema'; | ||
| import { db } from '../db/db'; | ||
| import { env } from '../env'; | ||
| import { llmProviderSchema } from '../types/llm'; | ||
| import { MatterMostReplyMode } from '../types/messaging-provider'; | ||
| import { takeFirstOrThrow } from '../utils/queries'; | ||
|
|
||
| function toLlmSelectedModel( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: This adds another copy of the same LLM model-selection parser used by other messaging config queries. Consider moving Prompt for AI agents |
||
| provider: string | null | undefined, | ||
| modelId: string | null | undefined, | ||
| ): LlmSelectedModel | undefined { | ||
| if (!provider || !modelId) { | ||
| return undefined; | ||
| } | ||
| const parsed = llmProviderSchema.safeParse(provider); | ||
| return parsed.success ? { provider: parsed.data, modelId } : undefined; | ||
| } | ||
|
|
||
| export const listMattermostConfigs = async (): Promise<MatterMostConfig[]> => { | ||
| const projects = await db.select().from(s.project).execute(); | ||
| const configs: MatterMostConfig[] = []; | ||
| for (const project of projects) { | ||
| const settings = project.mattermostSettings; | ||
| if (!settings?.mattermostBaseUrl || !settings?.mattermostBotToken) { | ||
| continue; | ||
| } | ||
| configs.push({ | ||
| projectId: project.id, | ||
| baseUrl: settings.mattermostBaseUrl, | ||
| botToken: settings.mattermostBotToken, | ||
| redirectUrl: env.BETTER_AUTH_URL || 'http://localhost:3000/', | ||
| replyMode: 'thread', | ||
| modelSelection: toLlmSelectedModel(settings.mattermostLlmProvider, settings.mattermostLlmModelId), | ||
| }); | ||
| } | ||
| return configs; | ||
| }; | ||
|
|
||
| export const getProjectMattermostConfig = async (projectId: string): Promise<MatterMostConfig | null> => { | ||
| const [project] = await db.select().from(s.project).where(eq(s.project.id, projectId)).execute(); | ||
| const settings = project?.mattermostSettings; | ||
|
|
||
| if (!settings?.mattermostBaseUrl || !settings?.mattermostBotToken) { | ||
| return null; | ||
| } | ||
|
|
||
| return { | ||
| projectId, | ||
| baseUrl: settings.mattermostBaseUrl, | ||
| botToken: settings.mattermostBotToken, | ||
| redirectUrl: env.BETTER_AUTH_URL || 'http://localhost:3000/', | ||
| replyMode: 'thread', | ||
| modelSelection: toLlmSelectedModel(settings.mattermostLlmProvider, settings.mattermostLlmModelId), | ||
| }; | ||
| }; | ||
|
|
||
| export const upsertProjectMattermostConfig = async (data: { | ||
| projectId: string; | ||
| baseUrl: string; | ||
| botToken: string; | ||
| modelProvider?: LlmProvider; | ||
| modelId?: string; | ||
| }): Promise<{ | ||
| baseUrl: string; | ||
| botToken: string; | ||
| modelSelection?: LlmSelectedModel; | ||
| }> => { | ||
| const updated = await takeFirstOrThrow( | ||
| db | ||
| .update(s.project) | ||
| .set({ | ||
| mattermostSettings: { | ||
| mattermostBaseUrl: data.baseUrl, | ||
| mattermostBotToken: data.botToken, | ||
| mattermostLlmProvider: data.modelProvider ?? '', | ||
| mattermostLlmModelId: data.modelId ?? '', | ||
| }, | ||
| }) | ||
| .where(eq(s.project.id, data.projectId)) | ||
| .returning() | ||
| .execute(), | ||
| `Project not found: ${data.projectId}`, | ||
| ); | ||
|
|
||
| const settings = updated.mattermostSettings; | ||
| return { | ||
| baseUrl: settings?.mattermostBaseUrl || '', | ||
| botToken: settings?.mattermostBotToken || '', | ||
| modelSelection: toLlmSelectedModel(settings?.mattermostLlmProvider, settings?.mattermostLlmModelId), | ||
| }; | ||
| }; | ||
|
|
||
| export const updateProjectMattermostModel = async ( | ||
| projectId: string, | ||
| modelProvider: LlmProvider | null, | ||
| modelId: string | null, | ||
| ): Promise<void> => { | ||
| await db.transaction(async (tx) => { | ||
| const project = await takeFirstOrThrow( | ||
| tx.select().from(s.project).where(eq(s.project.id, projectId)).execute(), | ||
| `Project not found: ${projectId}`, | ||
| ); | ||
| const existing = project.mattermostSettings; | ||
|
|
||
| await tx | ||
| .update(s.project) | ||
| .set({ | ||
| mattermostSettings: { | ||
| mattermostBaseUrl: existing?.mattermostBaseUrl ?? '', | ||
| mattermostBotToken: existing?.mattermostBotToken ?? '', | ||
| mattermostLlmProvider: modelProvider ?? '', | ||
| mattermostLlmModelId: modelId ?? '', | ||
| }, | ||
| }) | ||
| .where(eq(s.project.id, projectId)) | ||
| .execute(); | ||
| }); | ||
| }; | ||
|
|
||
| export const deleteProjectMattermostConfig = async (projectId: string): Promise<void> => { | ||
| await db.update(s.project).set({ mattermostSettings: null }).where(eq(s.project.id, projectId)).execute(); | ||
| }; | ||
|
|
||
| export interface MatterMostConfig { | ||
| projectId: string; | ||
| botToken: string; | ||
| baseUrl: string; | ||
| redirectUrl: string; | ||
| replyMode: MatterMostReplyMode; | ||
| modelSelection?: LlmSelectedModel; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import type { App } from '../app'; | ||
| import { getProjectMattermostConfig } from '../queries/project-mattermost-config.queries'; | ||
| import { mattermostService } from '../services/mattermost'; | ||
| import { convertHeaders } from '../utils/utils'; | ||
|
|
||
| export const mattermostRoutes = async (app: App) => { | ||
| app.post('/:projectId', async (request, reply) => { | ||
| const { projectId } = request.params as { projectId: string }; | ||
| const webRequest = new Request(`http://localhost${request.url}`, { | ||
| method: request.method, | ||
| headers: convertHeaders(request.headers), | ||
| body: JSON.stringify(request.body), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Mattermost outgoing webhooks configured as Prompt for AI agents |
||
| }); | ||
|
|
||
| const mattermostConfig = await getProjectMattermostConfig(projectId); | ||
| if (!mattermostConfig) { | ||
| reply.status(200).send('OK'); | ||
| return; | ||
| } | ||
|
|
||
| const webhooks = await mattermostService.getWebhooks(mattermostConfig); | ||
| if (!webhooks) { | ||
| reply.status(200).send('OK'); | ||
| return; | ||
| } | ||
|
|
||
| const response = await webhooks.mattermost(webRequest, { | ||
| waitUntil: (task: Promise<unknown>) => task, | ||
| }); | ||
|
|
||
| reply.status(response.status); | ||
| response.headers.forEach((value, key) => reply.header(key, value)); | ||
| return reply.send(await response.text()); | ||
| }); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Postgres migration checks will fail for this new migration because the matching Drizzle meta snapshot/journal entry was not added. Regenerate or commit the 0056 snapshot and _journal.json update with this SQL migration.
Prompt for AI agents