Merge remote-tracking branch 'origin/main'

This commit is contained in:
Nevo David 2025-04-14 10:33:57 +07:00
commit de14aead77
20 changed files with 2277 additions and 233 deletions

View File

@ -1,23 +0,0 @@
name: Make sure new PRs are sent to development
on:
pull_request_target:
types: [opened, edited]
jobs:
check-branch:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
# Do not checkout the repository here. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ for more information
- uses: Vankka/pr-target-branch-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
target: main
exclude: dev # Don't prevent going from development -> main
change-to: dev
comment: |
Your PR's base branch was set to `main`, PRs should be set to target `dev`.
The base branch of this PR has been automatically changed to `development`, please check that there are no merge conflicts

32
.github/workflows/pr-docker-build vendored Normal file
View File

@ -0,0 +1,32 @@
name: Build and Publish PR Docker Image
on:
pull_request:
types: [opened, synchronize]
permissions: write-all
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Set image tag
id: vars
run: echo "IMAGE_TAG=ghcr.io/gitroomhq/postiz-app-pr:${{ github.event.pull_request.number }}" >> $GITHUB_ENV
- name: Build Docker image from Dockerfile.dev
run: docker build -f Dockerfile.dev -t $IMAGE_TAG .
- name: Push Docker image to GHCR
run: docker push $IMAGE_TAG

View File

@ -44,7 +44,7 @@ This project follows a Fork/Feature Branch/Pull Request model. If you're not fam
```bash
git push -u origin feature/your-feature-name
```
9. **Create a pull request**: Propose your changes **to the dev branch**.
9. **Create a pull request**: Propose your changes **to the main branch**.
# Need Help?

63
Jenkinsfile vendored
View File

@ -6,30 +6,6 @@ pipeline {
}
stages {
stage('Fetch Cache') {
options {
cache(caches: [
arbitraryFileCache(
cacheName: 'Next',
cacheValidityDecidingFile: '',
excludes: '',
includes: '**/*',
path: "./.nx/cache"
),
arbitraryFileCache(
cacheName: 'NodeJS', // Added a cache name for better clarity
cacheValidityDecidingFile: '',
excludes: '',
includes: '**/*',
path: "./node_modules" // Use the HOME environment variable for home directory
)
], defaultBranch: 'dev', maxCacheSize: 256000, skipSave: true)
}
steps {
echo 'Start fetching Cache.'
}
}
stage('Checkout Repository') {
steps {
checkout scm
@ -51,50 +27,13 @@ pipeline {
}
}
stage('Run Unit Tests') {
steps {
sh 'npm test'
}
}
stage('Build Project') {
steps {
sh 'npm run build 2>&1 | tee build_report.log' // Captures build output
}
}
stage('Save Cache') {
options {
cache(caches: [
arbitraryFileCache(
cacheName: 'Next',
cacheValidityDecidingFile: '',
excludes: '',
includes: '**/*',
path: "./.nx/cache"
),
arbitraryFileCache(
cacheName: 'NodeJS', // Added a cache name for better clarity
cacheValidityDecidingFile: '',
excludes: '',
includes: '**/*',
path: "./node_modules" // Use the HOME environment variable for home directory
)
], defaultBranch: 'dev', maxCacheSize: 256000, skipRestore: true)
}
steps {
echo 'Start saving Cache.'
sh 'npm run build'
}
}
}
post {
always {
junit '**/reports/junit.xml'
archiveArtifacts artifacts: 'reports/**', fingerprint: true
archiveArtifacts artifacts: 'build_report.log', fingerprint: true
cleanWs(cleanWhenNotBuilt: false, notFailBuild: true)
}
success {
echo 'Build completed successfully!'

View File

@ -31,6 +31,9 @@ import { Nowpayments } from '@gitroom/nestjs-libraries/crypto/nowpayments';
import { WebhookController } from '@gitroom/backend/api/routes/webhooks.controller';
import { SignatureController } from '@gitroom/backend/api/routes/signature.controller';
import { AutopostController } from '@gitroom/backend/api/routes/autopost.controller';
import { McpService } from '@gitroom/nestjs-libraries/mcp/mcp.service';
import { McpController } from '@gitroom/backend/api/routes/mcp.controller';
import { McpSettings } from '@gitroom/nestjs-libraries/mcp/mcp.settings';
const authenticatedController = [
UsersController,
@ -56,6 +59,7 @@ const authenticatedController = [
StripeController,
AuthController,
PublicController,
McpController,
...authenticatedController,
],
providers: [
@ -71,6 +75,7 @@ const authenticatedController = [
TrackService,
ShortLinkService,
Nowpayments,
McpService
],
get exports() {
return [...this.imports, ...this.providers];

View File

@ -0,0 +1,33 @@
import { Body, Controller, HttpException, Param, Post, Sse } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { McpService } from '@gitroom/nestjs-libraries/mcp/mcp.service';
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
@ApiTags('Mcp')
@Controller('/mcp')
export class McpController {
constructor(
private _mcpService: McpService,
private _organizationService: OrganizationService
) {}
@Sse('/:api/sse')
async sse(@Param('api') api: string) {
const apiModel = await this._organizationService.getOrgByApiKey(api);
if (!apiModel) {
throw new HttpException('Invalid url', 400);
}
return await this._mcpService.runServer(api, apiModel.id);
}
@Post('/:api/messages')
async post(@Param('api') api: string, @Body() body: any) {
const apiModel = await this._organizationService.getOrgByApiKey(api);
if (!apiModel) {
throw new HttpException('Invalid url', 400);
}
return this._mcpService.processPostBody(apiModel.id, body);
}
}

View File

@ -10,6 +10,7 @@ import { PublicApiModule } from '@gitroom/backend/public-api/public.api.module';
import { ThrottlerBehindProxyGuard } from '@gitroom/nestjs-libraries/throttler/throttler.provider';
import { ThrottlerModule } from '@nestjs/throttler';
import { AgentModule } from '@gitroom/nestjs-libraries/agent/agent.module';
import { McpModule } from '@gitroom/backend/mcp/mcp.module';
@Global()
@Module({
@ -20,6 +21,7 @@ import { AgentModule } from '@gitroom/nestjs-libraries/agent/agent.module';
PluginModule,
PublicApiModule,
AgentModule,
McpModule,
ThrottlerModule.forRoot([
{
ttl: 3600000,

View File

@ -0,0 +1,92 @@
import { Injectable } from '@nestjs/common';
import { McpTool } from '@gitroom/nestjs-libraries/mcp/mcp.tool';
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
import { string, array, enum as eenum, object } from 'zod';
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import dayjs from 'dayjs';
@Injectable()
export class MainMcp {
constructor(
private _integrationService: IntegrationService,
private _postsService: PostsService
) {}
@McpTool({ toolName: 'POSTIZ_CONFIGURATION_PRERUN' })
async preRun() {
return [{type: 'text', text: `Today date is ${dayjs.utc().format()}`}];
}
@McpTool({ toolName: 'POSTIZ_PROVIDERS_LIST' })
async listOfProviders(organization: string) {
const list = (
await this._integrationService.getIntegrationsList(organization)
).map((org) => ({
id: org.id,
name: org.name,
identifier: org.providerIdentifier,
picture: org.picture,
disabled: org.disabled,
profile: org.profile,
customer: org.customer
? {
id: org.customer.id,
name: org.customer.name,
}
: undefined,
}));
return [{ type: 'text', text: JSON.stringify(list) }];
}
@McpTool({
toolName: 'POSTIZ_SCHEDULE_POST',
zod: {
type: eenum(['draft', 'scheduled']),
date: string().describe('UTC TIME'),
providerId: string().describe('Use POSTIZ_PROVIDERS_LIST to get the id'),
posts: array(object({ text: string(), images: array(string()) })),
},
})
async schedulePost(
organization: string,
obj: {
type: 'draft' | 'schedule';
date: string;
providerId: string;
posts: { text: string; images: string[] }[];
}
) {
const create = await this._postsService.createPost(organization, {
date: obj.date,
type: obj.type,
tags: [],
posts: [
{
group: makeId(10),
value: obj.posts.map((post) => ({
content: post.text,
id: makeId(10),
image: post.images.map((image) => ({
id: image,
path: image,
})),
})),
// @ts-ignore
settings: {},
integration: {
id: obj.providerId,
},
},
],
});
return [
{
type: 'text',
text: `Post created successfully, check it here: ${process.env.FRONTEND_URL}/p/${create[0].postId}`,
},
];
}
}

View File

@ -0,0 +1,13 @@
import { Global, Module } from '@nestjs/common';
import { MainMcp } from '@gitroom/backend/mcp/main.mcp';
@Global()
@Module({
imports: [],
controllers: [],
providers: [MainMcp],
get exports() {
return [...this.providers];
},
})
export class McpModule {}

View File

@ -2,5 +2,9 @@ import { ReactNode } from 'react';
import { PreviewWrapper } from '@gitroom/frontend/components/preview/preview.wrapper';
export default async function AppLayout({ children }: { children: ReactNode }) {
return <PreviewWrapper>{children}</PreviewWrapper>;
return (
<div className="bg-[#000000] min-h-screen">
<PreviewWrapper>{children}</PreviewWrapper>
</div>
);
}

View File

@ -5,11 +5,14 @@ import { useUser } from '../layout/user.context';
import { Button } from '@gitroom/react/form/button';
import copy from 'copy-to-clipboard';
import { useToaster } from '@gitroom/react/toaster/toaster';
import { useVariables } from '@gitroom/react/helpers/variable.context';
export const PublicComponent = () => {
const user = useUser();
const {frontEndUrl} = useVariables();
const toaster = useToaster();
const [reveal, setReveal] = useState(false);
const [reveal2, setReveal2] = useState(false);
const copyToClipboard = useCallback(() => {
toaster.show('API Key copied to clipboard', 'success');
@ -53,6 +56,30 @@ export const PublicComponent = () => {
)}
</div>
</div>
<h3 className="text-[20px]">MCP</h3>
<div className="text-customColor18 mt-[4px]">
Connect your MCP client to Postiz to schedule your posts faster!
</div>
<div className="my-[16px] mt-[16px] bg-sixth border-fifth items-center border rounded-[4px] p-[24px] flex gap-[24px]">
<div className="flex items-center">
{reveal2 ? (
`${frontEndUrl}/mcp/` + user.publicApi + '/sse'
) : (
<>
<div className="blur-sm">{(`${frontEndUrl}/mcp/` + user.publicApi + '/sse').slice(0, -5)}</div>
<div>{(`${frontEndUrl}/mcp/` + user.publicApi + '/sse').slice(-5)}</div>
</>
)}
</div>
<div>
{!reveal2 ? (
<Button onClick={() => setReveal2(true)}>Reveal</Button>
) : (
<Button onClick={copyToClipboard}>Copy Key</Button>
)}
</div>
</div>
</div>
);
};

View File

@ -27,7 +27,7 @@ import { FarcasterProvider } from '@gitroom/nestjs-libraries/integrations/social
import { TelegramProvider } from '@gitroom/nestjs-libraries/integrations/social/telegram.provider';
import { NostrProvider } from '@gitroom/nestjs-libraries/integrations/social/nostr.provider';
const socialIntegrationList: SocialProvider[] = [
export const socialIntegrationList: SocialProvider[] = [
new XProvider(),
new LinkedinProvider(),
new LinkedinPageProvider(),

View File

@ -0,0 +1,48 @@
import { Injectable } from '@nestjs/common';
import EventEmitter from 'events';
import { finalize, fromEvent, startWith } from 'rxjs';
import { McpTransport } from '@gitroom/nestjs-libraries/mcp/mcp.transport';
import { JSONRPCMessageSchema } from '@gitroom/nestjs-libraries/mcp/mcp.types';
import { McpSettings } from '@gitroom/nestjs-libraries/mcp/mcp.settings';
import { MainMcp } from '@gitroom/backend/mcp/main.mcp';
@Injectable()
export class McpService {
static event = new EventEmitter();
constructor(
private _mainMcp: MainMcp
) {
}
async runServer(apiKey: string, organization: string) {
const server = McpSettings.load(organization, this._mainMcp).server();
const transport = new McpTransport(organization);
const observer = fromEvent(
McpService.event,
`organization-${organization}`
).pipe(
startWith({
type: 'endpoint',
data: process.env.NEXT_PUBLIC_BACKEND_URL + '/mcp/' + apiKey + '/messages',
}),
finalize(() => {
transport.close();
})
);
console.log('MCP transport started');
await server.connect(transport);
return observer;
}
async processPostBody(organization: string, body: object) {
const server = McpSettings.load(organization, this._mainMcp).server();
const message = JSONRPCMessageSchema.parse(body);
const transport = new McpTransport(organization);
await server.connect(transport);
transport.handlePostMessage(message);
return {};
}
}

View File

@ -0,0 +1,80 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { MainMcp } from '@gitroom/backend/mcp/main.mcp';
import { socialIntegrationList } from '@gitroom/nestjs-libraries/integrations/integration.manager';
export class McpSettings {
static singleton: McpSettings;
private _server: McpServer;
createServer(organization: string, service: MainMcp) {
this._server = new McpServer(
{
name: 'Postiz',
version: '2.0.0',
},
{
instructions: `Postiz is a service to schedule social media posts for ${socialIntegrationList
.map((p) => p.name)
.join(
', '
)} to schedule you need to have the providerId (you can get it from POSTIZ_PROVIDERS_LIST), user need to specify the schedule date (or now), text, you also can send base64 images and text for the comments. When you get POSTIZ_PROVIDERS_LIST, always display all the options to the user`,
}
);
for (const usePrompt of Reflect.getMetadata(
'MCP_PROMPT',
MainMcp.prototype
) || []) {
const list = [
usePrompt.data.promptName,
usePrompt.data.zod,
async (...args: any[]) => {
return {
// @ts-ignore
messages: await service[usePrompt.func as string](
organization,
...args
),
};
},
].filter((f) => f);
this._server.prompt(...(list as [any, any, any]));
}
for (const usePrompt of Reflect.getMetadata(
'MCP_TOOL',
MainMcp.prototype
) || []) {
const list: any[] = [
usePrompt.data.toolName,
usePrompt.data.zod,
async (...args: any[]) => {
return {
// @ts-ignore
content: await service[usePrompt.func as string](
organization,
...args
),
};
},
].filter((f) => f);
this._server.tool(...(list as [any, any, any]));
}
return this;
}
server() {
return this._server;
}
static load(organization: string, service: MainMcp): McpSettings {
if (!McpSettings.singleton) {
McpSettings.singleton = new McpSettings().createServer(
organization,
service
);
}
return McpSettings.singleton;
}
}

View File

@ -0,0 +1,25 @@
import { ZodRawShape } from 'zod';
export function McpTool (params: {toolName: string, zod?: ZodRawShape}) {
return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
const existingMetadata = Reflect.getMetadata('MCP_TOOL', target) || [];
// Add the metadata information for this method
existingMetadata.push({ data: params, func: propertyKey });
// Define metadata on the class prototype (so it can be retrieved from the class)
Reflect.defineMetadata('MCP_TOOL', existingMetadata, target);
}
}
export function McpPrompt (params: {promptName: string, zod?: ZodRawShape}) {
return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
const existingMetadata = Reflect.getMetadata('MCP_PROMPT', target) || [];
// Add the metadata information for this method
existingMetadata.push({ data: params, func: propertyKey });
// Define metadata on the class prototype (so it can be retrieved from the class)
Reflect.defineMetadata('MCP_PROMPT', existingMetadata, target);
}
}

View File

@ -0,0 +1,43 @@
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import { McpService } from '@gitroom/nestjs-libraries/mcp/mcp.service';
import { JSONRPCMessage, JSONRPCMessageSchema } from '@gitroom/nestjs-libraries/mcp/mcp.types';
export class McpTransport implements Transport {
constructor(private _organization: string) {}
onclose?: () => void;
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage) => void;
async start() {
}
async send(message: JSONRPCMessage): Promise<void> {
McpService.event.emit(`organization-${this._organization}`, {
type: 'message',
data: JSON.stringify(message),
});
}
async close() {
console.log('MCP transport closed');
McpService.event.removeAllListeners(`organization-${this._organization}`);
}
handlePostMessage(message: any) {
let parsedMessage: JSONRPCMessage;
try {
parsedMessage = JSONRPCMessageSchema.parse(message);
} catch (error) {
this.onerror?.(error as Error);
throw error;
}
this.onmessage?.(parsedMessage);
}
get sessionId() {
return this._organization;
}
}

File diff suppressed because it is too large Load Diff

725
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -38,10 +38,10 @@
"@aws-sdk/client-s3": "^3.787.0",
"@aws-sdk/s3-request-presigner": "^3.787.0",
"@casl/ability": "^6.5.0",
"@copilotkit/react-core": "^1.8.4",
"@copilotkit/react-textarea": "^1.8.4",
"@copilotkit/react-ui": "^1.8.4",
"@copilotkit/runtime": "^1.8.4",
"@copilotkit/react-core": "^1.8.5",
"@copilotkit/react-textarea": "^1.8.5",
"@copilotkit/react-ui": "^1.8.5",
"@copilotkit/runtime": "^1.8.5",
"@hookform/resolvers": "^3.3.4",
"@langchain/community": "^0.3.40",
"@langchain/core": "^0.3.44",
@ -51,6 +51,7 @@
"@mantine/dates": "^5.10.5",
"@mantine/hooks": "^5.10.5",
"@mantine/modals": "^5.10.5",
"@modelcontextprotocol/sdk": "^1.9.0",
"@nestjs/common": "^10.0.2",
"@nestjs/core": "^10.0.2",
"@nestjs/microservices": "^10.3.1",