feat(gateway): Discord channel auto-creation on project bootstrap (#200)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #200.
This commit is contained in:
@@ -2,4 +2,10 @@ export interface IChannelPlugin {
|
|||||||
readonly name: string;
|
readonly name: string;
|
||||||
start(): Promise<void>;
|
start(): Promise<void>;
|
||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
|
/** Called when a new project is bootstrapped. Return channelId if a channel was created. */
|
||||||
|
onProjectCreated?(project: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}): Promise<{ channelId: string } | null>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ class DiscordChannelPluginAdapter implements IChannelPlugin {
|
|||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
await this.plugin.stop();
|
await this.plugin.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onProjectCreated(project: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}): Promise<{ channelId: string } | null> {
|
||||||
|
return this.plugin.createProjectChannel(project);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TelegramChannelPluginAdapter implements IChannelPlugin {
|
class TelegramChannelPluginAdapter implements IChannelPlugin {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import type { Brain } from '@mosaic/brain';
|
import type { Brain } from '@mosaic/brain';
|
||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
|
import { PluginService } from '../plugin/plugin.service.js';
|
||||||
import { WorkspaceService } from './workspace.service.js';
|
import { WorkspaceService } from './workspace.service.js';
|
||||||
|
|
||||||
export interface BootstrapProjectParams {
|
export interface BootstrapProjectParams {
|
||||||
@@ -23,6 +24,7 @@ export class ProjectBootstrapService {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(BRAIN) private readonly brain: Brain,
|
@Inject(BRAIN) private readonly brain: Brain,
|
||||||
private readonly workspace: WorkspaceService,
|
private readonly workspace: WorkspaceService,
|
||||||
|
private readonly pluginService: PluginService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,6 +69,28 @@ export class ProjectBootstrapService {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 4. Notify plugins so they can set up project-specific resources (e.g. Discord channel)
|
||||||
|
try {
|
||||||
|
for (const plugin of this.pluginService.getPlugins()) {
|
||||||
|
if (plugin.onProjectCreated) {
|
||||||
|
const result = await plugin.onProjectCreated({
|
||||||
|
id: project.id,
|
||||||
|
name: params.name,
|
||||||
|
description: params.description,
|
||||||
|
});
|
||||||
|
if (result?.channelId) {
|
||||||
|
await this.brain.projects.update(project.id, {
|
||||||
|
metadata: { discordChannelId: result.channelId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Plugin project notification failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.log(`Project ${project.id} bootstrapped at ${workspacePath}`);
|
this.logger.log(`Project ${project.id} bootstrapped at ${workspacePath}`);
|
||||||
|
|
||||||
return { projectId: project.id, workspacePath };
|
return { projectId: project.id, workspacePath };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Client, GatewayIntentBits, type Message as DiscordMessage } from 'discord.js';
|
import { ChannelType, Client, GatewayIntentBits, type Message as DiscordMessage } from 'discord.js';
|
||||||
import { io, type Socket } from 'socket.io-client';
|
import { io, type Socket } from 'socket.io-client';
|
||||||
|
|
||||||
export interface DiscordPluginConfig {
|
export interface DiscordPluginConfig {
|
||||||
@@ -86,6 +86,34 @@ export class DiscordPlugin {
|
|||||||
await this.client.destroy();
|
await this.client.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createProjectChannel(project: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}): Promise<{ channelId: string } | null> {
|
||||||
|
if (!this.config.guildId) return null;
|
||||||
|
|
||||||
|
const guild = this.client.guilds.cache.get(this.config.guildId);
|
||||||
|
if (!guild) return null;
|
||||||
|
|
||||||
|
// Slugify project name for channel: lowercase, replace spaces/special chars with hyphens
|
||||||
|
const channelName = `mosaic-${project.name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-|-$/g, '')}`;
|
||||||
|
|
||||||
|
const channel = await guild.channels.create({
|
||||||
|
name: channelName,
|
||||||
|
type: ChannelType.GuildText,
|
||||||
|
topic: project.description ?? `Mosaic project: ${project.name}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register the channel mapping so messages route correctly
|
||||||
|
this.channelConversations.set(channel.id, `discord-${channel.id}`);
|
||||||
|
|
||||||
|
return { channelId: channel.id };
|
||||||
|
}
|
||||||
|
|
||||||
private handleDiscordMessage(message: DiscordMessage): void {
|
private handleDiscordMessage(message: DiscordMessage): void {
|
||||||
// Ignore bot messages
|
// Ignore bot messages
|
||||||
if (message.author.bot) return;
|
if (message.author.bot) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user