Merge branch 'main' into sentry-masking
This commit is contained in:
commit
345c3b6237
142 changed files with 18433 additions and 14248 deletions
|
|
@ -13,6 +13,9 @@ jobs:
|
|||
node-version: ['20.17.0']
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
environment:
|
||||
name: build-pr
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
|
@ -60,9 +63,6 @@ jobs:
|
|||
|
||||
- name: SonarQube Analysis (Pull Request)
|
||||
uses: SonarSource/sonarqube-scan-action@v6
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.projectVersion=${{ steps.get_version.outputs.tag }}
|
||||
75
.github/workflows/build-pr.yml
vendored
75
.github/workflows/build-pr.yml
vendored
|
|
@ -1,75 +0,0 @@
|
|||
---
|
||||
name: Build
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
environment:
|
||||
name: build-pr
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: ['20.17.0']
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ env.STORE_PATH }}
|
||||
${{ github.workspace }}/.next/cache
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Get Commit SHA (short)
|
||||
id: get_version
|
||||
run: |
|
||||
# Get the short 8-character commit SHA
|
||||
VERSION=$(git rev-parse --short=8 HEAD)
|
||||
echo "Commit SHA is $VERSION"
|
||||
echo "tag=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: SonarQube Analysis (Pull Request)
|
||||
uses: SonarSource/sonarqube-scan-action@v6
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.projectVersion=${{ steps.get_version.outputs.tag }}
|
||||
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
|
||||
-Dsonar.pullrequest.branch=${{ github.event.pull_request.head.ref }}
|
||||
-Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:20-alpine3.19
|
||||
FROM node:22.20-alpine
|
||||
ARG NEXT_PUBLIC_VERSION
|
||||
ENV NEXT_PUBLIC_VERSION=$NEXT_PUBLIC_VERSION
|
||||
RUN apk add --no-cache g++ make py3-pip bash nginx
|
||||
|
|
|
|||
96
Jenkins/Build.Jenkinsfile
Normal file
96
Jenkins/Build.Jenkinsfile
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// Declarative Pipeline for building Node.js application and running SonarQube analysis triggered by a push event.
|
||||
pipeline {
|
||||
// Defines the execution environment. Using 'agent any' to ensure an agent is available.
|
||||
agent any
|
||||
|
||||
// Global environment block removed to prevent Groovy scoping issues with manual path calculation.
|
||||
|
||||
stages {
|
||||
// Stage 1: Checkout the code (Relies on the initial SCM checkout done by Jenkins)
|
||||
stage('Source Checkout') {
|
||||
steps {
|
||||
echo "Workspace already populated by the initial SCM checkout. Proceeding."
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 2: Setup Node.js v20 and install pnpm
|
||||
stage('Setup Environment and Tools') {
|
||||
steps {
|
||||
sh '''
|
||||
echo "Ensuring required utilities and Node.js are installed..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl unzip nodejs
|
||||
|
||||
# 1. Install Node.js v20
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
echo "Node.js version: \$(node -v)"
|
||||
|
||||
# 2. Install pnpm globally (version 8)
|
||||
npm install -g pnpm@8
|
||||
echo "pnpm version: \$(pnpm -v)"
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 3: Install dependencies and build the application
|
||||
stage('Install and Build') {
|
||||
steps {
|
||||
sh 'pnpm install'
|
||||
sh 'pnpm run build'
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 4: Run SonarQube analysis: Install scanner, get version, and execute.
|
||||
stage('SonarQube Analysis') {
|
||||
steps {
|
||||
script {
|
||||
// 1. Get the short 8-character commit SHA for project versioning
|
||||
def commitShaShort = sh(returnStdout: true, script: 'git rev-parse --short=8 HEAD').trim()
|
||||
echo "Commit SHA (short) is: ${commitShaShort}"
|
||||
|
||||
// --- 2. MANUALLY INSTALL THE SONAR SCANNER CLI LOCALLY IN THIS STAGE ---
|
||||
sh """
|
||||
echo "Manually downloading and installing Sonar Scanner CLI..."
|
||||
|
||||
# Download the stable scanner CLI package
|
||||
curl -sS -o sonar-scanner.zip \
|
||||
"https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.7.0.2747.zip"
|
||||
|
||||
# Added -o flag to force overwrite and prevent interactive prompt failure
|
||||
unzip -o -q sonar-scanner.zip -d .
|
||||
"""
|
||||
|
||||
// 3. Find the extracted directory name and capture the full absolute bin path in Groovy
|
||||
// This is defined locally and used directly, avoiding environment variable issues.
|
||||
def scannerBinPath = sh(
|
||||
returnStdout: true,
|
||||
script: '''
|
||||
SCANNER_DIR=$(find . -maxdepth 1 -type d -name "sonar-scanner*" | head -n 1)
|
||||
# Get the full absolute path to the executable file
|
||||
echo \$(pwd)/\${SCANNER_DIR}/bin/sonar-scanner
|
||||
'''
|
||||
).trim()
|
||||
|
||||
echo "Scanner executable path captured: ${scannerBinPath}"
|
||||
|
||||
// 4. Use withSonarQubeEnv to set up the secure variables (HOST and TOKEN)
|
||||
withSonarQubeEnv(installationName: 'SonarQube-Server') {
|
||||
// 5. Execute the scanner using the Groovy variable directly.
|
||||
sh """
|
||||
echo "Starting SonarQube Analysis for project version: ${commitShaShort}"
|
||||
|
||||
# Execute the full, absolute path captured in the Groovy variable.
|
||||
'${scannerBinPath}' \\
|
||||
-Dsonar.projectVersion=${commitShaShort} \\
|
||||
-Dsonar.sources=.
|
||||
|
||||
# SONAR_HOST_URL and SONAR_TOKEN are automatically passed as environment variables
|
||||
# by the withSonarQubeEnv block.
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
100
Jenkins/BuildPR.Jenkinsfile
Normal file
100
Jenkins/BuildPR.Jenkinsfile
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Declarative Pipeline for building Node.js application and running SonarQube analysis for a Pull Request.
|
||||
pipeline {
|
||||
// Defines the execution environment. Using 'agent any' to ensure an agent is available.
|
||||
agent any
|
||||
|
||||
// Environment variables that hold PR details, provided by Jenkins Multibranch setup.
|
||||
environment {
|
||||
// FIX: Environment variables must be quoted or wrapped in a function call.
|
||||
// We quote the 'env.CHANGE_ID' reference to fix the compilation error.
|
||||
PR_KEY = "${env.CHANGE_ID}"
|
||||
PR_BRANCH = "${env.CHANGE_BRANCH}"
|
||||
PR_BASE = "${env.CHANGE_TARGET}"
|
||||
}
|
||||
|
||||
stages {
|
||||
// Stage 1: Checkout the code (Relies on the initial SCM checkout done by Jenkins)
|
||||
stage('Source Checkout') {
|
||||
steps {
|
||||
echo "Workspace already populated by the initial SCM checkout. Proceeding."
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 2: Setup Node.js v20, install pnpm, and install required tools (curl, unzip)
|
||||
stage('Setup Environment and Tools') {
|
||||
steps {
|
||||
sh '''
|
||||
echo "Ensuring required utilities and Node.js are installed..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl unzip nodejs
|
||||
|
||||
# 1. Install Node.js v20 (closest matching the specified version '20.17.0')
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
echo "Node.js version: \$(node -v)"
|
||||
|
||||
# 2. Install pnpm globally (version 8)
|
||||
npm install -g pnpm@8
|
||||
echo "pnpm version: \$(pnpm -v)"
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 3: Install dependencies and build the application
|
||||
stage('Install and Build') {
|
||||
steps {
|
||||
sh 'pnpm install'
|
||||
sh 'pnpm run build'
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 4: Run SonarQube PR analysis: Install scanner locally, get version, and execute.
|
||||
stage('SonarQube Pull Request Analysis') {
|
||||
steps {
|
||||
script {
|
||||
// 1. Get the short 8-character commit SHA for project versioning
|
||||
def commitShaShort = sh(returnStdout: true, script: 'git rev-parse --short=8 HEAD').trim()
|
||||
echo "Commit SHA (short) is: ${commitShaShort}"
|
||||
|
||||
// --- 2. MANUALLY INSTALL THE SONAR SCANNER CLI LOCALLY ---
|
||||
sh """
|
||||
echo "Manually downloading and installing Sonar Scanner CLI..."
|
||||
curl -sS -o sonar-scanner.zip \
|
||||
"https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.7.0.2747.zip"
|
||||
unzip -o -q sonar-scanner.zip -d .
|
||||
"""
|
||||
|
||||
// 3. Find the extracted directory name and capture the full absolute executable path.
|
||||
def scannerBinPath = sh(
|
||||
returnStdout: true,
|
||||
script: '''
|
||||
SCANNER_DIR=$(find . -maxdepth 1 -type d -name "sonar-scanner*" | head -n 1)
|
||||
# Get the full absolute path to the executable file
|
||||
echo \$(pwd)/\${SCANNER_DIR}/bin/sonar-scanner
|
||||
'''
|
||||
).trim()
|
||||
|
||||
echo "Scanner executable path captured: ${scannerBinPath}"
|
||||
|
||||
// 4. Use withSonarQubeEnv to set up the secure variables (HOST and TOKEN)
|
||||
withSonarQubeEnv(installationName: 'SonarQube-Server') {
|
||||
// 5. Execute the scanner using the Groovy variable directly with PR parameters.
|
||||
sh """
|
||||
echo "Starting SonarQube Pull Request Analysis for PR #${PR_KEY}"
|
||||
|
||||
'${scannerBinPath}' \\
|
||||
-Dsonar.projectVersion=${commitShaShort} \\
|
||||
-Dsonar.sources=. \\
|
||||
-Dsonar.pullrequest.key=${PR_KEY} \\
|
||||
-Dsonar.pullrequest.branch=${PR_BRANCH} \\
|
||||
-Dsonar.pullrequest.base=${PR_BASE}
|
||||
|
||||
# SONAR_HOST_URL and SONAR_TOKEN are automatically passed as environment variables
|
||||
# by the withSonarQubeEnv block.
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
Jenkinsfile
vendored
70
Jenkinsfile
vendored
|
|
@ -1,70 +0,0 @@
|
|||
pipeline {
|
||||
agent any
|
||||
|
||||
environment {
|
||||
NODE_VERSION = '20.17.0'
|
||||
PR_NUMBER = "${env.CHANGE_ID}" // PR number comes from webhook payload
|
||||
IMAGE_TAG="ghcr.io/gitroomhq/postiz-app-pr:${env.CHANGE_ID}"
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout Repository') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
|
||||
stage('Check Node.js and npm') {
|
||||
steps {
|
||||
script {
|
||||
sh "node -v"
|
||||
sh "npm -v"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Install Dependencies') {
|
||||
steps {
|
||||
sh 'npm ci'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build Project') {
|
||||
steps {
|
||||
sh 'npm run build'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build and Push Docker Image') {
|
||||
when {
|
||||
expression { return env.CHANGE_ID != null } // Only run if it's a PR
|
||||
}
|
||||
steps {
|
||||
withCredentials([string(credentialsId: 'gh-pat', variable: 'GITHUB_PASS')]) {
|
||||
// Docker login step
|
||||
sh '''
|
||||
echo "$GITHUB_PASS" | docker login ghcr.io -u "egelhaus" --password-stdin
|
||||
'''
|
||||
// Build Docker image
|
||||
sh '''
|
||||
docker build -f Dockerfile.dev -t $IMAGE_TAG .
|
||||
'''
|
||||
// Push Docker image to GitHub Container Registry
|
||||
sh '''
|
||||
docker push $IMAGE_TAG
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
success {
|
||||
echo 'Build completed successfully!'
|
||||
|
||||
}
|
||||
failure {
|
||||
echo 'Build failed!'
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
15
README.md
15
README.md
|
|
@ -19,21 +19,6 @@
|
|||
</a>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://sonarqube.ennogelhaus.de/dashboard?id=gitroomhq_postiz-app_bd4cd369-af44-4d19-903b-c4bdb0b66022">
|
||||
<img src="https://sonarqube.ennogelhaus.de/api/project_badges/measure?project=gitroomhq_postiz-app_bd4cd369-af44-4d19-903b-c4bdb0b66022&metric=alert_status&token=sqb_4ef7409d8d3bb84ff51d945f5a62bc0df93895a9" alt="Quality Gate Status" />
|
||||
</a>
|
||||
<a href="https://sonarqube.ennogelhaus.de/dashboard?id=gitroomhq_postiz-app_bd4cd369-af44-4d19-903b-c4bdb0b66022">
|
||||
<img src="https://sonarqube.ennogelhaus.de/api/project_badges/measure?project=gitroomhq_postiz-app_bd4cd369-af44-4d19-903b-c4bdb0b66022&metric=software_quality_maintainability_rating&token=sqb_4ef7409d8d3bb84ff51d945f5a62bc0df93895a9" alt="Maintainability Rating" />
|
||||
</a>
|
||||
<a href="https://sonarqube.ennogelhaus.de/dashboard?id=gitroomhq_postiz-app_bd4cd369-af44-4d19-903b-c4bdb0b66022">
|
||||
<img src="https://sonarqube.ennogelhaus.de/api/project_badges/measure?project=gitroomhq_postiz-app_bd4cd369-af44-4d19-903b-c4bdb0b66022&metric=software_quality_reliability_rating&token=sqb_4ef7409d8d3bb84ff51d945f5a62bc0df93895a9" alt="Reliability Rating" />
|
||||
</a>
|
||||
<a href="https://sonarqube.ennogelhaus.de/dashboard?id=gitroomhq_postiz-app_bd4cd369-af44-4d19-903b-c4bdb0b66022">
|
||||
<img src="https://sonarqube.ennogelhaus.de/api/project_badges/measure?project=gitroomhq_postiz-app_bd4cd369-af44-4d19-903b-c4bdb0b66022&metric=software_quality_security_rating&token=sqb_4ef7409d8d3bb84ff51d945f5a62bc0df93895a9" alt="Security Rating" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<strong>
|
||||
<h2>Your ultimate AI social media scheduling tool</h2><br />
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"scripts": {
|
||||
"dev": "dotenv -e ../../.env -- nest start --watch --entryFile=./apps/backend/src/main",
|
||||
"build": "cross-env NODE_ENV=production nest build",
|
||||
"start": "dotenv -e ../../.env -- node ./dist/apps/backend/src/main.js",
|
||||
"start": "dotenv -e ../../.env -- node --experimental-require-module ./dist/apps/backend/src/main.js",
|
||||
"pm2": "pm2 start pnpm --name backend -- start"
|
||||
},
|
||||
"keywords": [],
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ 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 { SetsController } from '@gitroom/backend/api/routes/sets.controller';
|
||||
import { ThirdPartyController } from '@gitroom/backend/api/routes/third-party.controller';
|
||||
import { MonitorController } from '@gitroom/backend/api/routes/monitor.controller';
|
||||
|
|
@ -63,7 +61,6 @@ const authenticatedController = [
|
|||
StripeController,
|
||||
AuthController,
|
||||
PublicController,
|
||||
McpController,
|
||||
MonitorController,
|
||||
...authenticatedController,
|
||||
],
|
||||
|
|
@ -80,7 +77,6 @@ const authenticatedController = [
|
|||
TrackService,
|
||||
ShortLinkService,
|
||||
Nowpayments,
|
||||
McpService,
|
||||
],
|
||||
get exports() {
|
||||
return [...this.imports, ...this.providers];
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { CreateAgencyDto } from '@gitroom/nestjs-libraries/dtos/agencies/create.
|
|||
export class AgenciesController {
|
||||
constructor(private _agenciesService: AgenciesService) {}
|
||||
@Get('/')
|
||||
async getAgencyByUser(@GetUserFromRequest() user: User) {
|
||||
async getAgencyByUsers(@GetUserFromRequest() user: User) {
|
||||
return (await this._agenciesService.getAgencyByUser(user)) || {};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
|
|||
import { RealIP } from 'nestjs-real-ip';
|
||||
import { UserAgent } from '@gitroom/nestjs-libraries/user/user.agent';
|
||||
import { Provider } from '@prisma/client';
|
||||
import * as Sentry from '@sentry/nestjs';
|
||||
|
||||
@ApiTags('Auth')
|
||||
@Controller('/auth')
|
||||
|
|
@ -101,6 +102,7 @@ export class AuthController {
|
|||
}
|
||||
}
|
||||
|
||||
Sentry.metrics.count("new_user", 1);
|
||||
response.header('onboarding', 'true');
|
||||
response.status(200).json({
|
||||
register: true,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,43 @@
|
|||
import { Logger, Controller, Get, Post, Req, Res, Query } from '@nestjs/common';
|
||||
import {
|
||||
Logger,
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Req,
|
||||
Res,
|
||||
Query,
|
||||
Param,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
CopilotRuntime,
|
||||
OpenAIAdapter,
|
||||
copilotRuntimeNestEndpoint,
|
||||
copilotRuntimeNodeHttpEndpoint,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from '@copilotkit/runtime';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization } from '@prisma/client';
|
||||
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
|
||||
import { MastraAgent } from '@ag-ui/mastra';
|
||||
import { MastraService } from '@gitroom/nestjs-libraries/chat/mastra.service';
|
||||
import { Request, Response } from 'express';
|
||||
import { RuntimeContext } from '@mastra/core/di';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
export type ChannelsContext = {
|
||||
integrations: string;
|
||||
organization: string;
|
||||
ui: string;
|
||||
};
|
||||
|
||||
@Controller('/copilot')
|
||||
export class CopilotController {
|
||||
constructor(private _subscriptionService: SubscriptionService) {}
|
||||
constructor(
|
||||
private _subscriptionService: SubscriptionService,
|
||||
private _mastraService: MastraService
|
||||
) {}
|
||||
@Post('/chat')
|
||||
chat(@Req() req: Request, @Res() res: Response) {
|
||||
chatAgent(@Req() req: Request, @Res() res: Response) {
|
||||
if (
|
||||
process.env.OPENAI_API_KEY === undefined ||
|
||||
process.env.OPENAI_API_KEY === ''
|
||||
|
|
@ -21,28 +46,112 @@ export class CopilotController {
|
|||
return;
|
||||
}
|
||||
|
||||
const copilotRuntimeHandler = copilotRuntimeNestEndpoint({
|
||||
const copilotRuntimeHandler = copilotRuntimeNodeHttpEndpoint({
|
||||
endpoint: '/copilot/chat',
|
||||
runtime: new CopilotRuntime(),
|
||||
serviceAdapter: new OpenAIAdapter({
|
||||
model:
|
||||
// @ts-ignore
|
||||
req?.body?.variables?.data?.metadata?.requestType ===
|
||||
'TextareaCompletion'
|
||||
? 'gpt-4o-mini'
|
||||
: 'gpt-4.1',
|
||||
model: 'gpt-4.1',
|
||||
}),
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return copilotRuntimeHandler(req, res);
|
||||
}
|
||||
|
||||
@Post('/agent')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.AI])
|
||||
async agent(
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
@GetOrgFromRequest() organization: Organization
|
||||
) {
|
||||
if (
|
||||
process.env.OPENAI_API_KEY === undefined ||
|
||||
process.env.OPENAI_API_KEY === ''
|
||||
) {
|
||||
Logger.warn('OpenAI API key not set, chat functionality will not work');
|
||||
return;
|
||||
}
|
||||
const mastra = await this._mastraService.mastra();
|
||||
const runtimeContext = new RuntimeContext<ChannelsContext>();
|
||||
runtimeContext.set(
|
||||
'integrations',
|
||||
req?.body?.variables?.properties?.integrations || []
|
||||
);
|
||||
|
||||
runtimeContext.set('organization', JSON.stringify(organization));
|
||||
runtimeContext.set('ui', 'true');
|
||||
|
||||
const agents = MastraAgent.getLocalAgents({
|
||||
resourceId: organization.id,
|
||||
mastra,
|
||||
// @ts-ignore
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
agents,
|
||||
});
|
||||
|
||||
const copilotRuntimeHandler = copilotRuntimeNextJSAppRouterEndpoint({
|
||||
endpoint: '/copilot/agent',
|
||||
runtime,
|
||||
// properties: req.body.variables.properties,
|
||||
serviceAdapter: new OpenAIAdapter({
|
||||
model: 'gpt-4.1',
|
||||
}),
|
||||
});
|
||||
|
||||
return copilotRuntimeHandler.handleRequest(req, res);
|
||||
}
|
||||
|
||||
@Get('/credits')
|
||||
calculateCredits(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Query('type') type: 'ai_images' | 'ai_videos',
|
||||
@Query('type') type: 'ai_images' | 'ai_videos'
|
||||
) {
|
||||
return this._subscriptionService.checkCredits(organization, type || 'ai_images');
|
||||
return this._subscriptionService.checkCredits(
|
||||
organization,
|
||||
type || 'ai_images'
|
||||
);
|
||||
}
|
||||
|
||||
@Get('/:thread/list')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.AI])
|
||||
async getMessagesList(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('thread') threadId: string
|
||||
): Promise<any> {
|
||||
const mastra = await this._mastraService.mastra();
|
||||
const memory = await mastra.getAgent('postiz').getMemory();
|
||||
try {
|
||||
return await memory.query({
|
||||
resourceId: organization.id,
|
||||
threadId,
|
||||
});
|
||||
} catch (err) {
|
||||
return { messages: [] };
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/list')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.AI])
|
||||
async getList(@GetOrgFromRequest() organization: Organization) {
|
||||
const mastra = await this._mastraService.mastra();
|
||||
// @ts-ignore
|
||||
const memory = await mastra.getAgent('postiz').getMemory();
|
||||
const list = await memory.getThreadsByResourceIdPaginated({
|
||||
resourceId: organization.id,
|
||||
perPage: 100000,
|
||||
page: 0,
|
||||
orderBy: 'createdAt',
|
||||
sortDirection: 'DESC',
|
||||
});
|
||||
|
||||
return {
|
||||
threads: list.threads.map((p) => ({
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integ
|
|||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization, User } from '@prisma/client';
|
||||
import { ApiKeyDto } from '@gitroom/nestjs-libraries/dtos/integrations/api.key.dto';
|
||||
import { IntegrationFunctionDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.function.dto';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -199,6 +199,7 @@ export class UsersController {
|
|||
|
||||
@Post('/logout')
|
||||
logout(@Res({ passthrough: true }) response: Response) {
|
||||
response.header('logout', 'true');
|
||||
response.cookie('auth', '', {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
...(!process.env.NOT_SECURED
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ 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';
|
||||
import { ThirdPartyModule } from '@gitroom/nestjs-libraries/3rdparties/thirdparty.module';
|
||||
import { VideoModule } from '@gitroom/nestjs-libraries/videos/video.module';
|
||||
import { SentryModule } from "@sentry/nestjs/setup";
|
||||
import { SentryModule } from '@sentry/nestjs/setup';
|
||||
import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception';
|
||||
import { ChatModule } from '@gitroom/nestjs-libraries/chat/chat.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
|
|
@ -23,9 +23,9 @@ import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception';
|
|||
ApiModule,
|
||||
PublicApiModule,
|
||||
AgentModule,
|
||||
McpModule,
|
||||
ThirdPartyModule,
|
||||
VideoModule,
|
||||
ChatModule,
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 3600000,
|
||||
|
|
@ -43,7 +43,7 @@ import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception';
|
|||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: PoliciesGuard,
|
||||
}
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
BullMqModule,
|
||||
|
|
@ -51,8 +51,8 @@ import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception';
|
|||
ApiModule,
|
||||
PublicApiModule,
|
||||
AgentModule,
|
||||
McpModule,
|
||||
ThrottlerModule,
|
||||
ChatModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import { initializeSentry } from '@gitroom/nestjs-libraries/sentry/initialize.sentry';
|
||||
initializeSentry('backend', true);
|
||||
|
||||
import { loadSwagger } from '@gitroom/helpers/swagger/load.swagger';
|
||||
import { json } from 'express';
|
||||
|
||||
process.env.TZ = 'UTC';
|
||||
|
||||
|
|
@ -7,37 +11,44 @@ import { Logger, ValidationPipe } from '@nestjs/common';
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
import { initializeSentry } from '@gitroom/nestjs-libraries/sentry/initialize.sentry';
|
||||
initializeSentry('backend', true);
|
||||
|
||||
import { SubscriptionExceptionFilter } from '@gitroom/backend/services/auth/permissions/subscription.exception';
|
||||
import { HttpExceptionFilter } from '@gitroom/nestjs-libraries/services/exception.filter';
|
||||
import { ConfigurationChecker } from '@gitroom/helpers/configuration/configuration.checker';
|
||||
import { startMcp } from '@gitroom/nestjs-libraries/chat/start.mcp';
|
||||
|
||||
async function bootstrap() {
|
||||
async function start() {
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
rawBody: true,
|
||||
cors: {
|
||||
...(!process.env.NOT_SECURED ? { credentials: true } : {}),
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'x-copilotkit-runtime-client-gql-version'],
|
||||
exposedHeaders: [
|
||||
'reload',
|
||||
'onboarding',
|
||||
'activate',
|
||||
'x-copilotkit-runtime-client-gql-version',
|
||||
...(process.env.NOT_SECURED ? ['auth', 'showorg', 'impersonate'] : []),
|
||||
],
|
||||
origin: [
|
||||
process.env.FRONTEND_URL,
|
||||
'http://localhost:6274',
|
||||
...(process.env.MAIN_URL ? [process.env.MAIN_URL] : []),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await startMcp(app);
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
})
|
||||
);
|
||||
|
||||
app.use('/copilot/*', (req: any, res: any, next: any) => {
|
||||
json({ limit: '50mb' })(req, res, next);
|
||||
});
|
||||
|
||||
app.use(cookieParser());
|
||||
app.useGlobalFilters(new SubscriptionExceptionFilter());
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
|
|
@ -69,8 +80,8 @@ function checkConfiguration() {
|
|||
|
||||
Logger.warn('Configuration issues found: ' + checker.getIssuesCount());
|
||||
} else {
|
||||
Logger.log('Configuration check completed without any issues.');
|
||||
Logger.log('Configuration check completed without any issues');
|
||||
}
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
start();
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
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, boolean } 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';
|
||||
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
|
||||
|
||||
@Injectable()
|
||||
export class MainMcp {
|
||||
constructor(
|
||||
private _integrationService: IntegrationService,
|
||||
private _postsService: PostsService,
|
||||
private _openAiService: OpenaiService
|
||||
) {}
|
||||
|
||||
@McpTool({ toolName: 'POSTIZ_GET_CONFIG_ID' })
|
||||
async preRun() {
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text: `id: ${makeId(10)} 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', 'schedule']),
|
||||
configId: string(),
|
||||
generatePictures: boolean(),
|
||||
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';
|
||||
generatePictures: boolean;
|
||||
date: string;
|
||||
providerId: string;
|
||||
posts: { text: string }[];
|
||||
}
|
||||
) {
|
||||
const create = await this._postsService.createPost(organization, {
|
||||
date: obj.date,
|
||||
type: obj.type,
|
||||
tags: [],
|
||||
shortLink: false,
|
||||
posts: [
|
||||
{
|
||||
group: makeId(10),
|
||||
value: await Promise.all(
|
||||
obj.posts.map(async (post) => ({
|
||||
content: post.text,
|
||||
id: makeId(10),
|
||||
image: !obj.generatePictures
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: makeId(10),
|
||||
path: await this._openAiService.generateImage(
|
||||
post.text,
|
||||
true
|
||||
),
|
||||
},
|
||||
],
|
||||
}))
|
||||
),
|
||||
settings: {
|
||||
__type: 'any' as any,
|
||||
},
|
||||
integration: {
|
||||
id: obj.providerId,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Post created successfully, check it here: ${process.env.FRONTEND_URL}/p/${create[0].postId}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
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 {}
|
||||
|
|
@ -26,12 +26,17 @@ import {
|
|||
} from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
import { VideoDto } from '@gitroom/nestjs-libraries/dtos/videos/video.dto';
|
||||
import { VideoFunctionDto } from '@gitroom/nestjs-libraries/dtos/videos/video.function.dto';
|
||||
import { UploadDto } from '@gitroom/nestjs-libraries/dtos/media/upload.dto';
|
||||
import axios from 'axios';
|
||||
import { Readable } from 'stream';
|
||||
import { lookup } from 'mime-types';
|
||||
import * as Sentry from '@sentry/nestjs';
|
||||
|
||||
@ApiTags('Public API')
|
||||
@Controller('/public/v1')
|
||||
export class PublicIntegrationsController {
|
||||
private storage = UploadFactory.createStorage();
|
||||
|
||||
|
||||
constructor(
|
||||
private _integrationService: IntegrationService,
|
||||
private _postsService: PostsService,
|
||||
|
|
@ -44,6 +49,7 @@ export class PublicIntegrationsController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@UploadedFile('file') file: Express.Multer.File
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
if (!file) {
|
||||
throw new HttpException({ msg: 'No file provided' }, 400);
|
||||
}
|
||||
|
|
@ -56,11 +62,44 @@ export class PublicIntegrationsController {
|
|||
);
|
||||
}
|
||||
|
||||
@Post('/upload-from-url')
|
||||
async uploadsFromUrl(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: UploadDto
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
const response = await axios.get(body.url, {
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
|
||||
const buffer = Buffer.from(response.data);
|
||||
|
||||
const getFile = await this.storage.uploadFile({
|
||||
buffer,
|
||||
mimetype: lookup(body?.url?.split?.('?')?.[0]) || 'image/jpeg',
|
||||
size: buffer.length,
|
||||
path: '',
|
||||
fieldname: '',
|
||||
destination: '',
|
||||
stream: new Readable(),
|
||||
filename: '',
|
||||
originalname: '',
|
||||
encoding: '',
|
||||
});
|
||||
|
||||
return this._mediaService.saveFile(
|
||||
org.id,
|
||||
getFile.originalname,
|
||||
getFile.path
|
||||
);
|
||||
}
|
||||
|
||||
@Get('/find-slot/:id')
|
||||
async findSlotIntegration(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id?: string
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
return { date: await this._postsService.findFreeDateTime(org.id, id) };
|
||||
}
|
||||
|
||||
|
|
@ -69,6 +108,7 @@ export class PublicIntegrationsController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@Query() query: GetPostsDto
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
const posts = await this._postsService.getPosts(org.id, query);
|
||||
return {
|
||||
posts,
|
||||
|
|
@ -82,6 +122,7 @@ export class PublicIntegrationsController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() rawBody: any
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
const body = await this._postsService.mapTypeToPost(
|
||||
rawBody,
|
||||
org.id,
|
||||
|
|
@ -98,17 +139,20 @@ export class PublicIntegrationsController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@Param() body: { id: string }
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
const getPostById = await this._postsService.getPost(org.id, body.id);
|
||||
return this._postsService.deletePost(org.id, getPostById.group);
|
||||
}
|
||||
|
||||
@Get('/is-connected')
|
||||
async getActiveIntegrations(@GetOrgFromRequest() org: Organization) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
return { connected: true };
|
||||
}
|
||||
|
||||
@Get('/integrations')
|
||||
async listIntegration(@GetOrgFromRequest() org: Organization) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
return (await this._integrationService.getIntegrationsList(org.id)).map(
|
||||
(org) => ({
|
||||
id: org.id,
|
||||
|
|
@ -132,11 +176,13 @@ export class PublicIntegrationsController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: VideoDto
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
return this._mediaService.generateVideo(org, body);
|
||||
}
|
||||
|
||||
@Post('/video/function')
|
||||
videoFunction(@Body() body: VideoFunctionDto) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
return this._mediaService.videoFunction(
|
||||
body.identifier,
|
||||
body.functionName,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/o
|
|||
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
|
||||
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
|
||||
import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter';
|
||||
import { MastraService } from '@gitroom/nestjs-libraries/chat/mastra.service';
|
||||
|
||||
export const removeAuth = (res: Response) => {
|
||||
res.cookie('auth', '', {
|
||||
|
|
@ -20,7 +21,6 @@ export const removeAuth = (res: Response) => {
|
|||
expires: new Date(0),
|
||||
maxAge: -1,
|
||||
});
|
||||
|
||||
res.header('logout', 'true');
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export class OauthProvider implements ProvidersInterface {
|
|||
redirect_uri: `${this.frontendUrl}/settings`,
|
||||
});
|
||||
|
||||
return `${this.authUrl}/?${params.toString()}`;
|
||||
return `${this.authUrl}?${params.toString()}`;
|
||||
}
|
||||
|
||||
async getToken(code: string): Promise<string> {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"scripts": {
|
||||
"dev": "dotenv -e ../../.env -- nest start --watch --entryFile=./apps/cron/src/main",
|
||||
"build": "cross-env NODE_ENV=production nest build",
|
||||
"start": "dotenv -e ../../.env -- node ./dist/apps/cron/src/main.js",
|
||||
"start": "dotenv -e ../../.env -- node --experimental-require-module ./dist/apps/cron/src/main.js",
|
||||
"pm2": "pm2 start pnpm --name cron -- start"
|
||||
},
|
||||
"keywords": [],
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { CronModule } from './cron.module';
|
||||
|
||||
import { initializeSentry } from '@gitroom/nestjs-libraries/sentry/initialize.sentry';
|
||||
initializeSentry('cron');
|
||||
|
||||
async function bootstrap() {
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { CronModule } from './cron.module';
|
||||
|
||||
async function start() {
|
||||
// some comment again
|
||||
await NestFactory.createApplicationContext(CronModule);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
start();
|
||||
|
|
|
|||
12
apps/frontend/src/app/(app)/(site)/agents/[id]/page.tsx
Normal file
12
apps/frontend/src/app/(app)/(site)/agents/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Metadata } from 'next';
|
||||
import { Agent } from '@gitroom/frontend/components/agents/agent';
|
||||
import { AgentChat } from '@gitroom/frontend/components/agents/agent.chat';
|
||||
export const metadata: Metadata = {
|
||||
title: 'Postiz - Agent',
|
||||
description: '',
|
||||
};
|
||||
export default async function Page() {
|
||||
return (
|
||||
<AgentChat />
|
||||
);
|
||||
}
|
||||
13
apps/frontend/src/app/(app)/(site)/agents/layout.tsx
Normal file
13
apps/frontend/src/app/(app)/(site)/agents/layout.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Metadata } from 'next';
|
||||
import { Agent } from '@gitroom/frontend/components/agents/agent';
|
||||
export const metadata: Metadata = {
|
||||
title: 'Postiz - Agent',
|
||||
description: '',
|
||||
};
|
||||
export default async function Layout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <Agent>{children}</Agent>;
|
||||
}
|
||||
11
apps/frontend/src/app/(app)/(site)/agents/page.tsx
Normal file
11
apps/frontend/src/app/(app)/(site)/agents/page.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Metadata } from 'next';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Postiz - Agent',
|
||||
description: '',
|
||||
};
|
||||
|
||||
export default async function Page() {
|
||||
return redirect('/agents/new');
|
||||
}
|
||||
|
|
@ -678,4 +678,29 @@ html[dir='rtl'] [dir='ltr'] {
|
|||
}
|
||||
.blur-xs {
|
||||
filter: blur(4px);
|
||||
}
|
||||
|
||||
.agent {
|
||||
.copilotKitInputContainer {
|
||||
padding: 0 24px !important;
|
||||
}
|
||||
|
||||
.copilotKitInput {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
.rm-bg .b2 {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
.rm-bg .b1 {
|
||||
background: transparent !important;
|
||||
gap: 0 !important;
|
||||
}
|
||||
|
||||
.copilotKitMessage img {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.copilotKitMessage a {
|
||||
color: var(--new-btn-text) !important;
|
||||
}
|
||||
359
apps/frontend/src/components/agents/agent.chat.tsx
Normal file
359
apps/frontend/src/components/agents/agent.chat.tsx
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
'use client';
|
||||
|
||||
import React, {
|
||||
FC,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { CopilotChat, CopilotKitCSSProperties } from '@copilotkit/react-ui';
|
||||
import {
|
||||
InputProps,
|
||||
UserMessageProps,
|
||||
} from '@copilotkit/react-ui/dist/components/chat/props';
|
||||
import { Input } from '@gitroom/frontend/components/agents/agent.input';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import {
|
||||
CopilotKit,
|
||||
useCopilotAction,
|
||||
useCopilotMessagesContext,
|
||||
} from '@copilotkit/react-core';
|
||||
import {
|
||||
MediaPortal,
|
||||
PropertiesContext,
|
||||
} from '@gitroom/frontend/components/agents/agent';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { TextMessage } from '@copilotkit/runtime-client-gql';
|
||||
import { AddEditModal } from '@gitroom/frontend/components/new-launch/add.edit.modal';
|
||||
import dayjs from 'dayjs';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { ExistingDataContextProvider } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
|
||||
|
||||
export const AgentChat: FC = () => {
|
||||
const { backendUrl } = useVariables();
|
||||
const params = useParams<{ id: string }>();
|
||||
const { properties } = useContext(PropertiesContext);
|
||||
|
||||
return (
|
||||
<CopilotKit
|
||||
{...(params.id === 'new' ? {} : { threadId: params.id })}
|
||||
credentials="include"
|
||||
runtimeUrl={backendUrl + '/copilot/agent'}
|
||||
showDevConsole={false}
|
||||
agent="postiz"
|
||||
properties={{
|
||||
integrations: properties,
|
||||
}}
|
||||
>
|
||||
<Hooks />
|
||||
<LoadMessages id={params.id} />
|
||||
<div
|
||||
style={
|
||||
{
|
||||
'--copilot-kit-primary-color': 'var(--new-btn-text)',
|
||||
'--copilot-kit-background-color': 'var(--new-bg-color)',
|
||||
} as CopilotKitCSSProperties
|
||||
}
|
||||
className="trz agent bg-newBgColorInner flex flex-col gap-[15px] transition-all flex-1 items-center relative"
|
||||
>
|
||||
<div className="absolute left-0 w-full h-full pb-[20px]">
|
||||
<CopilotChat
|
||||
className="w-full h-full"
|
||||
labels={{
|
||||
title: 'Your Assistant',
|
||||
initial: `Hello, I am your Postiz agent 🙌🏻.
|
||||
|
||||
I can schedule a post or multiple posts to multiple channels and generate pictures and videos.
|
||||
|
||||
You can select the channels you want to use from the left menu.
|
||||
|
||||
You can see your previous conversations from the right menu.
|
||||
|
||||
You can also use me as an MCP Server, check Settings >> Public API
|
||||
`,
|
||||
}}
|
||||
UserMessage={Message}
|
||||
Input={NewInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CopilotKit>
|
||||
);
|
||||
};
|
||||
|
||||
const LoadMessages: FC<{ id: string }> = ({ id }) => {
|
||||
const { setMessages } = useCopilotMessagesContext();
|
||||
const fetch = useFetch();
|
||||
|
||||
const loadMessages = useCallback(async (idToSet: string) => {
|
||||
const data = await (await fetch(`/copilot/${idToSet}/list`)).json();
|
||||
setMessages(
|
||||
data.uiMessages.map((p: any) => {
|
||||
return new TextMessage({
|
||||
content: p.content,
|
||||
role: p.role,
|
||||
});
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (id === 'new') {
|
||||
setMessages([]);
|
||||
return;
|
||||
}
|
||||
loadMessages(id);
|
||||
}, [id]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const Message: FC<UserMessageProps> = (props) => {
|
||||
const convertContentToImagesAndVideo = useMemo(() => {
|
||||
return (props.message?.content || '')
|
||||
.replace(/Video: (http.*mp4\n)/g, (match, p1) => {
|
||||
return `<video controls class="h-[150px] w-[150px] rounded-[8px] mb-[10px]"><source src="${p1.trim()}" type="video/mp4">Your browser does not support the video tag.</video>`;
|
||||
})
|
||||
.replace(/Image: (http.*\n)/g, (match, p1) => {
|
||||
return `<img src="${p1.trim()}" class="h-[150px] w-[150px] max-w-full border border-newBgColorInner" />`;
|
||||
})
|
||||
.replace(/\[\-\-Media\-\-\](.*)\[\-\-Media\-\-\]/g, (match, p1) => {
|
||||
return `<div class="flex justify-center mt-[20px]">${p1}</div>`;
|
||||
})
|
||||
.replace(
|
||||
/(\[--integrations--\][\s\S]*?\[--integrations--\])/g,
|
||||
(match, p1) => {
|
||||
return ``;
|
||||
}
|
||||
);
|
||||
}, [props.message?.content]);
|
||||
return (
|
||||
<div
|
||||
className="copilotKitMessage copilotKitUserMessage min-w-[300px]"
|
||||
dangerouslySetInnerHTML={{ __html: convertContentToImagesAndVideo }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const NewInput: FC<InputProps> = (props) => {
|
||||
const [media, setMedia] = useState([] as { path: string; id: string }[]);
|
||||
const [value, setValue] = useState('');
|
||||
const { properties } = useContext(PropertiesContext);
|
||||
return (
|
||||
<>
|
||||
<MediaPortal
|
||||
value={value}
|
||||
media={media}
|
||||
setMedia={(e) => setMedia(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
{...props}
|
||||
onChange={setValue}
|
||||
onSend={(text) => {
|
||||
const send = props.onSend(
|
||||
text +
|
||||
(media.length > 0
|
||||
? '\n[--Media--]' +
|
||||
media
|
||||
.map((m) =>
|
||||
m.path.indexOf('mp4') > -1
|
||||
? `Video: ${m.path}`
|
||||
: `Image: ${m.path}`
|
||||
)
|
||||
.join('\n') +
|
||||
'\n[--Media--]'
|
||||
: '') +
|
||||
`
|
||||
${
|
||||
properties.length
|
||||
? `[--integrations--]
|
||||
Use the following social media platforms: ${JSON.stringify(
|
||||
properties.map((p) => ({
|
||||
id: p.id,
|
||||
platform: p.identifier,
|
||||
profilePicture: p.picture,
|
||||
additionalSettings: p.additionalSettings,
|
||||
}))
|
||||
)}
|
||||
[--integrations--]`
|
||||
: ``
|
||||
}`
|
||||
);
|
||||
setValue('');
|
||||
setMedia([]);
|
||||
return send;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Hooks: FC = () => {
|
||||
const modals = useModals();
|
||||
|
||||
useCopilotAction({
|
||||
name: 'manualPosting',
|
||||
description:
|
||||
'This tool should be triggered when the user wants to manually add the generated post',
|
||||
parameters: [
|
||||
{
|
||||
name: 'list',
|
||||
type: 'object[]',
|
||||
description:
|
||||
'list of posts to schedule to different social media (integration ids)',
|
||||
attributes: [
|
||||
{
|
||||
name: 'integrationId',
|
||||
type: 'string',
|
||||
description: 'The integration id',
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
type: 'string',
|
||||
description: 'UTC date of the scheduled post',
|
||||
},
|
||||
{
|
||||
name: 'settings',
|
||||
type: 'object',
|
||||
description: 'Settings for the integration [input:settings]',
|
||||
},
|
||||
{
|
||||
name: 'posts',
|
||||
type: 'object[]',
|
||||
description: 'list of posts / comments (one under another)',
|
||||
attributes: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'string',
|
||||
description: 'the content of the post',
|
||||
},
|
||||
{
|
||||
name: 'attachments',
|
||||
type: 'object[]',
|
||||
description: 'list of attachments',
|
||||
attributes: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
description: 'id of the attachment',
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
description: 'url of the attachment',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
renderAndWaitForResponse: ({ args, status, respond }) => {
|
||||
if (status === 'executing') {
|
||||
return <OpenModal args={args} respond={respond} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
return null;
|
||||
};
|
||||
|
||||
const OpenModal: FC<{
|
||||
respond: (value: any) => void;
|
||||
args: {
|
||||
list: {
|
||||
integrationId: string;
|
||||
date: string;
|
||||
settings?: Record<string, any>;
|
||||
posts: { content: string; attachments: { id: string; path: string }[] }[];
|
||||
}[];
|
||||
};
|
||||
}> = ({ args, respond }) => {
|
||||
const modals = useModals();
|
||||
const { properties } = useContext(PropertiesContext);
|
||||
const startModal = useCallback(async () => {
|
||||
for (const integration of args.list) {
|
||||
await new Promise((res) => {
|
||||
const group = makeId(10);
|
||||
modals.openModal({
|
||||
id: 'add-edit-modal',
|
||||
closeOnClickOutside: false,
|
||||
removeLayout: true,
|
||||
closeOnEscape: false,
|
||||
withCloseButton: false,
|
||||
askClose: true,
|
||||
size: '80%',
|
||||
title: ``,
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[1400px] text-textColor',
|
||||
},
|
||||
children: (
|
||||
<ExistingDataContextProvider
|
||||
value={{
|
||||
group,
|
||||
integration: integration.integrationId,
|
||||
integrationPicture:
|
||||
properties.find((p) => p.id === integration.integrationId)
|
||||
.picture || '',
|
||||
settings: integration.settings || {},
|
||||
posts: integration.posts.map((p) => ({
|
||||
approvedSubmitForOrder: 'NO',
|
||||
content: p.content,
|
||||
createdAt: new Date().toISOString(),
|
||||
state: 'DRAFT',
|
||||
id: makeId(10),
|
||||
settings: JSON.stringify(integration.settings || {}),
|
||||
group,
|
||||
integrationId: integration.integrationId,
|
||||
integration: properties.find(
|
||||
(p) => p.id === integration.integrationId
|
||||
),
|
||||
publishDate: dayjs.utc(integration.date).toISOString(),
|
||||
image: p.attachments.map((a) => ({
|
||||
id: a.id,
|
||||
path: a.path,
|
||||
})),
|
||||
})),
|
||||
}}
|
||||
>
|
||||
<AddEditModal
|
||||
date={dayjs.utc(integration.date)}
|
||||
allIntegrations={properties}
|
||||
integrations={properties.filter(
|
||||
(p) => p.id === integration.integrationId
|
||||
)}
|
||||
onlyValues={integration.posts.map((p) => ({
|
||||
content: p.content,
|
||||
id: makeId(10),
|
||||
settings: integration.settings || {},
|
||||
image: p.attachments.map((a) => ({
|
||||
id: a.id,
|
||||
path: a.path,
|
||||
})),
|
||||
}))}
|
||||
reopenModal={() => {}}
|
||||
mutate={() => res(true)}
|
||||
/>
|
||||
</ExistingDataContextProvider>
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
respond('User scheduled all the posts');
|
||||
}, [args, respond, properties]);
|
||||
|
||||
useEffect(() => {
|
||||
startModal();
|
||||
}, []);
|
||||
return (
|
||||
<div onClick={() => respond('continue')}>
|
||||
Opening manually ${JSON.stringify(args)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
120
apps/frontend/src/components/agents/agent.input.tsx
Normal file
120
apps/frontend/src/components/agents/agent.input.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import { useCopilotContext, useCopilotReadable } from '@copilotkit/react-core';
|
||||
import AutoResizingTextarea from '@gitroom/frontend/components/agents/agent.textarea';
|
||||
import { useChatContext } from '@copilotkit/react-ui';
|
||||
import { InputProps } from '@copilotkit/react-ui/dist/components/chat/props';
|
||||
const MAX_NEWLINES = 6;
|
||||
|
||||
export const Input = ({
|
||||
inProgress,
|
||||
onSend,
|
||||
isVisible = false,
|
||||
onStop,
|
||||
onUpload,
|
||||
hideStopButton = false,
|
||||
onChange,
|
||||
}: InputProps & { onChange: (value: string) => void }) => {
|
||||
const context = useChatContext();
|
||||
const copilotContext = useCopilotContext();
|
||||
const showPoweredBy = !copilotContext.copilotApiConfig?.publicApiKey;
|
||||
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [isComposing, setIsComposing] = useState(false);
|
||||
|
||||
const handleDivClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// If the user clicked a button or inside a button, don't focus the textarea
|
||||
if (target.closest('button')) return;
|
||||
|
||||
// If the user clicked the textarea, do nothing (it's already focused)
|
||||
if (target.tagName === 'TEXTAREA') return;
|
||||
|
||||
// Otherwise, focus the textarea
|
||||
textareaRef.current?.focus();
|
||||
};
|
||||
|
||||
const [text, setText] = useState('');
|
||||
const send = () => {
|
||||
if (inProgress) return;
|
||||
onSend(text);
|
||||
setText('');
|
||||
|
||||
textareaRef.current?.focus();
|
||||
};
|
||||
|
||||
const isInProgress = inProgress;
|
||||
const buttonIcon =
|
||||
isInProgress && !hideStopButton
|
||||
? context.icons.stopIcon
|
||||
: context.icons.sendIcon;
|
||||
|
||||
const canSend = useMemo(() => {
|
||||
const interruptEvent = copilotContext.langGraphInterruptAction?.event;
|
||||
const interruptInProgress =
|
||||
interruptEvent?.name === 'LangGraphInterruptEvent' &&
|
||||
!interruptEvent?.response;
|
||||
|
||||
return !isInProgress && text.trim().length > 0 && !interruptInProgress;
|
||||
}, [copilotContext.langGraphInterruptAction?.event, isInProgress, text]);
|
||||
|
||||
const canStop = useMemo(() => {
|
||||
return isInProgress && !hideStopButton;
|
||||
}, [isInProgress, hideStopButton]);
|
||||
|
||||
const sendDisabled = !canSend && !canStop;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`copilotKitInputContainer ${
|
||||
showPoweredBy ? 'poweredByContainer' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="copilotKitInput" onClick={handleDivClick}>
|
||||
<AutoResizingTextarea
|
||||
ref={textareaRef}
|
||||
placeholder={context.labels.placeholder}
|
||||
autoFocus={false}
|
||||
maxRows={MAX_NEWLINES}
|
||||
value={text}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
setText(event.target.value);
|
||||
}}
|
||||
onCompositionStart={() => setIsComposing(true)}
|
||||
onCompositionEnd={() => setIsComposing(false)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey && !isComposing) {
|
||||
event.preventDefault();
|
||||
if (canSend) {
|
||||
send();
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="copilotKitInputControls">
|
||||
{onUpload && (
|
||||
<button onClick={onUpload} className="copilotKitInputControlButton">
|
||||
{context.icons.uploadIcon}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
<button
|
||||
disabled={sendDisabled}
|
||||
onClick={isInProgress && !hideStopButton ? onStop : send}
|
||||
data-copilotkit-in-progress={inProgress}
|
||||
data-test-id={
|
||||
inProgress
|
||||
? 'copilot-chat-request-in-progress'
|
||||
: 'copilot-chat-ready'
|
||||
}
|
||||
className="copilotKitInputControlButton"
|
||||
>
|
||||
{buttonIcon}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
77
apps/frontend/src/components/agents/agent.textarea.tsx
Normal file
77
apps/frontend/src/components/agents/agent.textarea.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react";
|
||||
|
||||
interface AutoResizingTextareaProps {
|
||||
maxRows?: number;
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onCompositionStart?: () => void;
|
||||
onCompositionEnd?: () => void;
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
const AutoResizingTextarea = forwardRef<HTMLTextAreaElement, AutoResizingTextareaProps>(
|
||||
(
|
||||
{
|
||||
maxRows = 1,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
onCompositionStart,
|
||||
onCompositionEnd,
|
||||
autoFocus,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const internalTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [maxHeight, setMaxHeight] = useState<number>(0);
|
||||
|
||||
useImperativeHandle(ref, () => internalTextareaRef.current as HTMLTextAreaElement);
|
||||
|
||||
useEffect(() => {
|
||||
const calculateMaxHeight = () => {
|
||||
const textarea = internalTextareaRef.current;
|
||||
if (textarea) {
|
||||
textarea.style.height = "auto";
|
||||
const singleRowHeight = textarea.scrollHeight;
|
||||
setMaxHeight(singleRowHeight * maxRows);
|
||||
if (autoFocus) {
|
||||
textarea.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
calculateMaxHeight();
|
||||
}, [maxRows]);
|
||||
|
||||
useEffect(() => {
|
||||
const textarea = internalTextareaRef.current;
|
||||
if (textarea) {
|
||||
textarea.style.height = "auto";
|
||||
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`;
|
||||
}
|
||||
}, [value, maxHeight]);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={internalTextareaRef}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
onCompositionStart={onCompositionStart}
|
||||
onCompositionEnd={onCompositionEnd}
|
||||
placeholder={placeholder}
|
||||
style={{
|
||||
overflow: "auto",
|
||||
resize: "none",
|
||||
maxHeight: `${maxHeight}px`,
|
||||
}}
|
||||
rows={1}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default AutoResizingTextarea;
|
||||
271
apps/frontend/src/components/agents/agent.tsx
Normal file
271
apps/frontend/src/components/agents/agent.tsx
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
'use client';
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
FC,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import useCookie from 'react-use-cookie';
|
||||
import useSWR from 'swr';
|
||||
import { orderBy } from 'lodash';
|
||||
import { SVGLine } from '@gitroom/frontend/components/launches/launches.component';
|
||||
import ImageWithFallback from '@gitroom/react/helpers/image.with.fallback';
|
||||
import Image from 'next/image';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useWaitForClass } from '@gitroom/helpers/utils/use.wait.for.class';
|
||||
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
|
||||
import { Integration } from '@prisma/client';
|
||||
import Link from 'next/link';
|
||||
import { useParams, usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
export const MediaPortal: FC<{
|
||||
media: { path: string; id: string }[];
|
||||
value: string;
|
||||
setMedia: (event: {
|
||||
target: {
|
||||
name: string;
|
||||
value?: {
|
||||
id: string;
|
||||
path: string;
|
||||
alt?: string;
|
||||
thumbnail?: string;
|
||||
thumbnailTimestamp?: number;
|
||||
}[];
|
||||
};
|
||||
}) => void;
|
||||
}> = ({ media, setMedia, value }) => {
|
||||
const waitForClass = useWaitForClass('copilotKitMessages');
|
||||
if (!waitForClass) return null;
|
||||
return (
|
||||
<div className="pl-[14px] pr-[24px] whitespace-nowrap editor rm-bg">
|
||||
<MultiMediaComponent
|
||||
allData={[{ content: value }]}
|
||||
text={value}
|
||||
label="Attachments"
|
||||
description=""
|
||||
value={media}
|
||||
dummy={false}
|
||||
name="image"
|
||||
onChange={setMedia}
|
||||
onOpen={() => {}}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AgentList: FC<{ onChange: (arr: any[]) => void }> = ({
|
||||
onChange,
|
||||
}) => {
|
||||
const fetch = useFetch();
|
||||
const [selected, setSelected] = useState([]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
return (await (await fetch('/integrations/list')).json()).integrations;
|
||||
}, []);
|
||||
|
||||
const [collapseMenu, setCollapseMenu] = useCookie('collapseMenu', '0');
|
||||
|
||||
const { data } = useSWR('integrations', load, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnMount: true,
|
||||
refreshWhenHidden: false,
|
||||
refreshWhenOffline: false,
|
||||
fallbackData: [],
|
||||
});
|
||||
|
||||
const setIntegration = useCallback(
|
||||
(integration: Integration) => () => {
|
||||
if (selected.some((p) => p.id === integration.id)) {
|
||||
onChange(selected.filter((p) => p.id !== integration.id));
|
||||
setSelected(selected.filter((p) => p.id !== integration.id));
|
||||
} else {
|
||||
onChange([...selected, integration]);
|
||||
setSelected([...selected, integration]);
|
||||
}
|
||||
},
|
||||
[selected]
|
||||
);
|
||||
|
||||
const sortedIntegrations = useMemo(() => {
|
||||
return orderBy(
|
||||
data || [],
|
||||
['type', 'disabled', 'identifier'],
|
||||
['desc', 'asc', 'asc']
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'trz bg-newBgColorInner flex flex-col gap-[15px] transition-all relative',
|
||||
collapseMenu === '1' ? 'group sidebar w-[100px]' : 'w-[260px]'
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-0 start-0 w-full h-full p-[20px] overflow-auto scrollbar scrollbar-thumb-fifth scrollbar-track-newBgColor">
|
||||
<div className="flex items-center">
|
||||
<h2 className="group-[.sidebar]:hidden flex-1 text-[20px] font-[500] mb-[15px]">
|
||||
Select Channels
|
||||
</h2>
|
||||
<div
|
||||
onClick={() => setCollapseMenu(collapseMenu === '1' ? '0' : '1')}
|
||||
className="-mt-3 group-[.sidebar]:rotate-[180deg] group-[.sidebar]:mx-auto text-btnText bg-btnSimple rounded-[6px] w-[24px] h-[24px] flex items-center justify-center cursor-pointer select-none"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="7"
|
||||
height="13"
|
||||
viewBox="0 0 7 13"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M6 11.5L1 6.5L6 1.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx('flex flex-col gap-[15px]')}>
|
||||
{sortedIntegrations.map((integration, index) => (
|
||||
<div
|
||||
onClick={setIntegration(integration)}
|
||||
key={integration.id}
|
||||
className={clsx(
|
||||
'flex gap-[12px] items-center group/profile justify-center hover:bg-boxHover rounded-e-[8px] hover:opacity-100 cursor-pointer',
|
||||
!selected.some((p) => p.id === integration.id) && 'opacity-20'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative rounded-full flex justify-center items-center gap-[6px]',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{(integration.inBetweenSteps || integration.refreshNeeded) && (
|
||||
<div className="absolute start-0 top-0 w-[39px] h-[46px] cursor-pointer">
|
||||
<div className="bg-red-500 w-[15px] h-[15px] rounded-full start-0 -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center">
|
||||
!
|
||||
</div>
|
||||
<div className="bg-primary/60 w-[39px] h-[46px] start-0 top-0 absolute rounded-full z-[199]" />
|
||||
</div>
|
||||
)}
|
||||
<div className="h-full w-[4px] -ms-[12px] rounded-s-[3px] opacity-0 group-hover/profile:opacity-100 transition-opacity">
|
||||
<SVGLine />
|
||||
</div>
|
||||
<ImageWithFallback
|
||||
fallbackSrc={`/icons/platforms/${integration.identifier}.png`}
|
||||
src={integration.picture}
|
||||
className="rounded-[8px]"
|
||||
alt={integration.identifier}
|
||||
width={36}
|
||||
height={36}
|
||||
/>
|
||||
<Image
|
||||
src={`/icons/platforms/${integration.identifier}.png`}
|
||||
className="rounded-[8px] absolute z-10 bottom-[5px] -end-[5px] border border-fifth"
|
||||
alt={integration.identifier}
|
||||
width={18.41}
|
||||
height={18.41}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex-1 whitespace-nowrap text-ellipsis overflow-hidden group-[.sidebar]:hidden',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{integration.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PropertiesContext = createContext({ properties: [] });
|
||||
export const Agent: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [properties, setProperties] = useState([]);
|
||||
|
||||
return (
|
||||
<PropertiesContext.Provider value={{ properties }}>
|
||||
<AgentList onChange={setProperties} />
|
||||
<div className="bg-newBgColorInner flex flex-1">{children}</div>
|
||||
<Threads />
|
||||
</PropertiesContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const Threads: FC = () => {
|
||||
const fetch = useFetch();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const threads = useCallback(async () => {
|
||||
return (await fetch('/copilot/list')).json();
|
||||
}, []);
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const { data } = useSWR('threads', threads);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'trz bg-newBgColorInner flex flex-col gap-[15px] transition-all relative',
|
||||
'w-[260px]'
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-0 start-0 w-full h-full p-[20px] overflow-auto scrollbar scrollbar-thumb-fifth scrollbar-track-newBgColor">
|
||||
<div className="mb-[15px] justify-center flex group-[.sidebar]:pb-[15px]">
|
||||
<Link
|
||||
href={`/agents`}
|
||||
className="text-white whitespace-nowrap flex-1 pt-[12px] pb-[14px] ps-[16px] pe-[20px] group-[.sidebar]:p-0 min-h-[44px] max-h-[44px] rounded-md bg-btnPrimary flex justify-center items-center gap-[5px] outline-none"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="21"
|
||||
height="20"
|
||||
viewBox="0 0 21 20"
|
||||
fill="none"
|
||||
className="min-w-[21px] min-h-[20px]"
|
||||
>
|
||||
<path
|
||||
d="M10.5001 4.16699V15.8337M4.66675 10.0003H16.3334"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="flex-1 text-start text-[16px] group-[.sidebar]:hidden">
|
||||
Start a new chat
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[1px]">
|
||||
{data?.threads?.map((p: any) => (
|
||||
<Link
|
||||
className={clsx(
|
||||
'overflow-ellipsis overflow-hidden whitespace-nowrap hover:bg-newBgColor px-[10px] py-[6px] rounded-[10px] cursor-pointer',
|
||||
p.id === id && 'bg-newBgColor'
|
||||
)}
|
||||
href={`/agents/${p.id}`}
|
||||
key={p.id}
|
||||
>
|
||||
{p.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -271,7 +271,7 @@ export const AddOrEditWebhook: FC<{
|
|||
await fetch(`/autopost/send?url=${encodeURIComponent(url)}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
contentType: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
|
|
|||
|
|
@ -150,31 +150,8 @@ const Info: FC<{
|
|||
const t = useT();
|
||||
|
||||
return (
|
||||
<div className="relative flex gap-[20px] flex-col flex-1 rounded-[4px] border border-customColor6 bg-sixth p-[16px] pt-0 w-[500px]">
|
||||
<TopTitle title="Oh no" />
|
||||
<button
|
||||
className="outline-none absolute end-[20px] top-[15px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="relative flex gap-[20px] flex-col flex-1 rounded-[4px]">
|
||||
<div>
|
||||
{t('we_are_sorry_to_see_you_go', 'We are sorry to see you go :(')}
|
||||
<br />
|
||||
{t(
|
||||
'would_you_mind_shortly_tell_us_what_we_could_have_done_better',
|
||||
'Would you mind shortly tell us what we could have done better?'
|
||||
|
|
@ -192,7 +169,9 @@ const Info: FC<{
|
|||
</div>
|
||||
<div>
|
||||
<Button disabled={feedback.length < 20} onClick={cancel}>
|
||||
{feedback.length < 20 ? t('please_add_at_least', 'Please add at least 20 chars') : t('cancel_subscription', 'Cancel Subscription')}
|
||||
{feedback.length < 20
|
||||
? t('please_add_at_least', 'Please add at least 20 chars')
|
||||
: t('cancel_subscription', 'Cancel Subscription')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -283,13 +262,15 @@ export const MainBillingComponent: FC<{
|
|||
) {
|
||||
const info = await new Promise((res) => {
|
||||
modal.openModal({
|
||||
title: '',
|
||||
withCloseButton: false,
|
||||
title: t(
|
||||
'we_are_sorry_to_see_you_go',
|
||||
'We are sorry to see you go :('
|
||||
),
|
||||
withCloseButton: true,
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
},
|
||||
children: <Info proceed={(e) => res(e)} />,
|
||||
size: 'auto',
|
||||
});
|
||||
});
|
||||
setLoading(true);
|
||||
|
|
|
|||
|
|
@ -73,11 +73,13 @@ function LayoutContextInner(params: { children: ReactNode }) {
|
|||
: '/analytics?onboarding=true';
|
||||
return true;
|
||||
}
|
||||
|
||||
if (response?.headers?.get('reload')) {
|
||||
window.location.reload();
|
||||
return true;
|
||||
}
|
||||
if (response.status === 401) {
|
||||
|
||||
if (response.status === 401 || response?.headers?.get('logout')) {
|
||||
if (!isSecured) {
|
||||
setCookie('auth', '', -10);
|
||||
setCookie('showorg', '', -10);
|
||||
|
|
|
|||
|
|
@ -41,6 +41,24 @@ export const useMenuItem = () => {
|
|||
),
|
||||
path: '/launches',
|
||||
},
|
||||
{
|
||||
name: 'Agent',
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="23"
|
||||
height="23"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M21.1963 9.07375C20.2913 6.95494 18.6824 5.21364 16.6416 4.14422C14.6009 3.0748 12.2534 2.74287 9.99616 3.20455C7.73891 3.66623 5.71031 4.8932 4.25334 6.67802C2.79637 8.46284 2.0004 10.696 2 13V21.25C2 21.7141 2.18437 22.1592 2.51256 22.4874C2.84075 22.8156 3.28587 23 3.75 23H10.8337C11.6141 24.7821 12.8964 26.2984 14.5241 27.3638C16.1519 28.4293 18.0546 28.9978 20 29H28.25C28.7141 29 29.1592 28.8156 29.4874 28.4874C29.8156 28.1592 30 27.7141 30 27.25V19C29.9995 16.5553 29.1036 14.1955 27.4814 12.3666C25.8593 10.5376 23.6234 9.36619 21.1963 9.07375ZM4 13C4 11.4177 4.46919 9.87103 5.34824 8.55544C6.22729 7.23984 7.47672 6.21446 8.93853 5.60896C10.4003 5.00346 12.0089 4.84504 13.5607 5.15372C15.1126 5.4624 16.538 6.22432 17.6569 7.34314C18.7757 8.46197 19.5376 9.88743 19.8463 11.4393C20.155 12.9911 19.9965 14.5997 19.391 16.0615C18.7855 17.5233 17.7602 18.7727 16.4446 19.6518C15.129 20.5308 13.5823 21 12 21H4V13ZM28 27H20C18.5854 26.9984 17.1964 26.6225 15.974 25.9106C14.7516 25.1986 13.7394 24.1759 13.04 22.9463C14.4096 22.8041 15.7351 22.3804 16.9333 21.7017C18.1314 21.023 19.1763 20.104 20.0024 19.0023C20.8284 17.9006 21.4179 16.6401 21.7337 15.2998C22.0495 13.9595 22.0848 12.5684 21.8375 11.2137C23.5916 11.6277 25.1545 12.6218 26.273 14.035C27.3915 15.4482 28 17.1977 28 19V27Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
path: '/agents',
|
||||
},
|
||||
{
|
||||
name: t('analytics', 'Analytics'),
|
||||
icon: (
|
||||
|
|
@ -128,39 +146,6 @@ export const useMenuItem = () => {
|
|||
] satisfies MenuItemInterface[] as MenuItemInterface[];
|
||||
|
||||
const secondMenu = [
|
||||
// {
|
||||
// name: 'GrowChief',
|
||||
// icon: (
|
||||
// <svg
|
||||
// width="20"
|
||||
// height="21"
|
||||
// viewBox="0 0 50 28"
|
||||
// fill="none"
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// data-tooltip-id="tooltip"
|
||||
// data-tooltip-content="New! Automate your X and LinkedIn outreach with GrowChief"
|
||||
// >
|
||||
// <path
|
||||
// d="M24.8789 0.191772C39.9967 0.198463 49.621 14.0845 49.6514 14.1283C49.6514 14.1283 40.0206 27.8931 24.8789 27.8998C9.73703 27.9062 0.189453 14.1283 0.189453 14.1283C0.235704 14.0609 9.77381 0.185332 24.8789 0.191772Z"
|
||||
// fill="none"
|
||||
// stroke="currentColor"
|
||||
// strokeWidth="3"
|
||||
// />
|
||||
//
|
||||
// <circle
|
||||
// cx="24.9189"
|
||||
// cy="14.2621"
|
||||
// r="9.1328"
|
||||
// fill="none"
|
||||
// stroke="currentColor"
|
||||
// strokeWidth="3"
|
||||
// />
|
||||
// </svg>
|
||||
// ),
|
||||
// path: 'https://growchief.com',
|
||||
// role: ['ADMIN', 'SUPERADMIN', 'USER'],
|
||||
// requireBilling: true,
|
||||
// },
|
||||
{
|
||||
name: t('affiliate', 'Affiliate'),
|
||||
icon: (
|
||||
|
|
|
|||
|
|
@ -643,7 +643,7 @@ export const MultiMediaComponent: FC<{
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-[8px] bg-bigStrip rounded-bl-[8px] select-none w-full">
|
||||
<div className="b1 flex flex-col gap-[8px] bg-bigStrip rounded-bl-[8px] select-none w-full">
|
||||
<div className="flex gap-[10px]">
|
||||
<Button
|
||||
onClick={showModal}
|
||||
|
|
@ -757,8 +757,8 @@ export const MultiMediaComponent: FC<{
|
|||
)}
|
||||
</div>
|
||||
{!dummy && (
|
||||
<div className="flex gap-[10px] bg-newBgLineColor w-full">
|
||||
<div className="flex py-[10px]">
|
||||
<div className="flex gap-[10px] bg-newBgLineColor w-full b1">
|
||||
<div className="flex py-[10px] b2">
|
||||
<Button
|
||||
onClick={designMedia}
|
||||
className="ms-[10px] rounded-[4px] gap-[8px] !text-primary justify-center items-center w-[127px] flex border border-dashed border-newBgLineColor bg-newColColor"
|
||||
|
|
|
|||
|
|
@ -160,7 +160,6 @@ export const ManageModal: FC<AddEditModalProps> = (props) => {
|
|||
return;
|
||||
}
|
||||
|
||||
console.log(checkAllValid);
|
||||
for (const item of checkAllValid) {
|
||||
if (item.valid === false) {
|
||||
toaster.show('Some fields are not valid', 'warning');
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ export const suggestion = (
|
|||
let component: any;
|
||||
|
||||
return {
|
||||
allowSpaces: true,
|
||||
items: async ({ query }: { query: string }) => {
|
||||
if (!query || query.length < 2) {
|
||||
component.updateProps({ loading: true, stop: true });
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export const HashnodePublications: FC<{
|
|||
value={currentMedia}
|
||||
>
|
||||
<option value="">{t('select_1', '--Select--')}</option>
|
||||
{publications.map((publication: any) => (
|
||||
{(publications || []).map((publication: any) => (
|
||||
<option key={publication.id} value={publication.id}>
|
||||
{publication.name}
|
||||
</option>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export const HashnodeTags: FC<{
|
|||
);
|
||||
useEffect(() => {
|
||||
customFunc.get('tags').then((data) => setTags(data));
|
||||
const settings = getValues()[props.name];
|
||||
const settings = getValues()[props.name] || [];
|
||||
if (settings) {
|
||||
setTagValue(settings);
|
||||
}
|
||||
|
|
@ -63,12 +63,13 @@ export const HashnodeTags: FC<{
|
|||
if (!tags.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={`text-[14px] mb-[6px]`}>{label}</div>
|
||||
<ReactTags
|
||||
suggestions={tags}
|
||||
selected={tagValue}
|
||||
suggestions={tags || []}
|
||||
selected={tagValue || []}
|
||||
onAdd={onAddition}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const SettingsComponent = () => {
|
|||
<Input label="Subject" {...form.register('subject')} />
|
||||
<Input label="Preview" {...form.register('preview')} />
|
||||
<SelectList {...form.register('list')} />
|
||||
<SelectTemplates {...form.register('templates')} />
|
||||
<SelectTemplates {...form.register('template')} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -29,19 +29,6 @@ export default withProvider({
|
|||
SettingsComponent: SettingsComponent,
|
||||
CustomPreviewComponent: undefined,
|
||||
dto: ListmonkDto,
|
||||
checkValidity: async (posts) => {
|
||||
if (
|
||||
posts.some(
|
||||
(p) => p.some((a) => a.path.indexOf('mp4') > -1) && p.length > 1
|
||||
)
|
||||
) {
|
||||
return 'You can only upload one video per post.';
|
||||
}
|
||||
|
||||
if (posts.some((p) => p.length > 4)) {
|
||||
return 'There can be maximum 4 pictures in a post.';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
checkValidity: undefined,
|
||||
maximumCharacters: 300000,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
'use client';
|
||||
'use client';
|
||||
|
||||
import {
|
||||
PostComment,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,17 @@ const type = [
|
|||
value: 'unlisted',
|
||||
},
|
||||
];
|
||||
|
||||
const madeForKids = [
|
||||
{
|
||||
label: 'No',
|
||||
value: 'no',
|
||||
},
|
||||
{
|
||||
label: 'Yes',
|
||||
value: 'yes',
|
||||
},
|
||||
];
|
||||
const YoutubeSettings: FC = () => {
|
||||
const { register, control } = useSettings();
|
||||
return (
|
||||
|
|
@ -42,6 +53,18 @@ const YoutubeSettings: FC = () => {
|
|||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
label="Made for kids"
|
||||
{...register('selfDeclaredMadeForKids', {
|
||||
value: 'no',
|
||||
})}
|
||||
>
|
||||
{madeForKids.map((t) => (
|
||||
<option key={t.value} value={t.value}>
|
||||
{t.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<MediumTags label="Tags" {...register('tags')} />
|
||||
<div className="mt-[20px]">
|
||||
<MediaComponent
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export const MenuItem: FC<{ label: string; icon: ReactNode; path: string }> = ({
|
|||
path,
|
||||
}) => {
|
||||
const currentPath = usePathname();
|
||||
const isActive = path.indexOf(currentPath) === 0;
|
||||
const isActive = currentPath.indexOf(path) === 0;
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export const PublicComponent = () => {
|
|||
}, [user]);
|
||||
const copyToClipboard2 = useCallback(() => {
|
||||
toaster.show('MCP copied to clipboard', 'success');
|
||||
copy(`${backendUrl}/mcp/` + user?.publicApi + '/sse');
|
||||
copy(`${backendUrl}/mcp/` + user?.publicApi);
|
||||
}, [user]);
|
||||
|
||||
const t = useT();
|
||||
|
|
@ -85,20 +85,20 @@ export const PublicComponent = () => {
|
|||
<div className="text-customColor18 mt-[4px]">
|
||||
{t(
|
||||
'connect_your_mcp_client_to_postiz_to_schedule_your_posts_faster',
|
||||
'Connect your MCP client to Postiz to schedule your posts faster!'
|
||||
'Connect Postiz MCP server to your client (Http streaming) 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" data-sentry-mask>
|
||||
{reveal2 ? (
|
||||
`${backendUrl}/mcp/` + user.publicApi + '/sse'
|
||||
`${backendUrl}/mcp/` + user.publicApi
|
||||
) : (
|
||||
<>
|
||||
<div className="blur-sm">
|
||||
{(`${backendUrl}/mcp/` + user.publicApi + '/sse').slice(0, -5)}
|
||||
{(`${backendUrl}/mcp/` + user.publicApi).slice(0, -5)}
|
||||
</div>
|
||||
<div>
|
||||
{(`${backendUrl}/mcp/` + user.publicApi + '/sse').slice(-5)}
|
||||
{(`${backendUrl}/mcp/` + user.publicApi).slice(-5)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"scripts": {
|
||||
"dev": "dotenv -e ../../.env -- nest start --watch --entryFile=./apps/workers/src/main",
|
||||
"build": "cross-env NODE_ENV=production nest build",
|
||||
"start": "dotenv -e ../../.env -- node ./dist/apps/workers/src/main.js",
|
||||
"start": "dotenv -e ../../.env -- node --experimental-require-module ./dist/apps/workers/src/main.js",
|
||||
"pm2": "pm2 start pnpm --name workers -- start"
|
||||
},
|
||||
"keywords": [],
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { initializeSentry } from '@gitroom/nestjs-libraries/sentry/initialize.sentry';
|
||||
initializeSentry('workers');
|
||||
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
|
||||
import { MicroserviceOptions } from '@nestjs/microservices';
|
||||
import { BullMqServer } from '@gitroom/nestjs-libraries/bull-mq-transport-new/strategy';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { initializeSentry } from '@gitroom/nestjs-libraries/sentry/initialize.sentry';
|
||||
initializeSentry('workers');
|
||||
|
||||
async function bootstrap() {
|
||||
async function start() {
|
||||
process.env.IS_WORKER = 'true';
|
||||
|
||||
// some comment again
|
||||
|
|
@ -21,4 +22,4 @@ async function bootstrap() {
|
|||
await app.listen();
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
start();
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ checksums:
|
|||
video_made_with_ai: c37747aaf8107d339d6238a0463f7096
|
||||
please_add_at_least: 90d3c0237b56e57c7a58d5decf6e9d3c
|
||||
send_invitation_via_email: 9275e0b85147a931421b3bf6c3083cb4
|
||||
global_settings: ba55734261d6bc26e792fda32de3e7ec
|
||||
copy_id: 831147124db35832872f8470c577e440
|
||||
team_members: 61333c4a765e10b2ad46774951725233
|
||||
invite_your_assistant_or_team_member_to_manage_your_account: dadd50655759ac32b9ed62e60f8acb1d
|
||||
|
|
@ -45,7 +46,7 @@ checksums:
|
|||
reveal: 3f7302cc2e097266e447b3e18f1b9ab7
|
||||
copy_key: 8f4f13acec7abf7c3aa6e680b6da99f8
|
||||
mcp: 62c2c8e6703c9e5224e98294a53e1884
|
||||
connect_your_mcp_client_to_postiz_to_schedule_your_posts_faster: 55b92dd743d32bfe04b69c6ec9406e1f
|
||||
connect_your_mcp_client_to_postiz_to_schedule_your_posts_faster: bb53d1093a6ab977f2eee0bbc7e0248c
|
||||
share_with_a_client: 8e27f69c841a9dda635f34fb1b38ad99
|
||||
post: 0beafa0cd18d45690b9b07104eb2c1b8
|
||||
comments: 502237cd1044491c48494aa60967a985
|
||||
|
|
|
|||
|
|
@ -1,8 +1,37 @@
|
|||
import { sign, verify } from 'jsonwebtoken';
|
||||
import { hashSync, compareSync } from 'bcrypt';
|
||||
import bcrypt from 'bcrypt';
|
||||
import crypto from 'crypto';
|
||||
// @ts-ignore
|
||||
import EVP_BytesToKey from 'evp_bytestokey';
|
||||
const algorithm = 'aes-256-cbc';
|
||||
const { keyLength, ivLength } = crypto.getCipherInfo(algorithm);
|
||||
|
||||
function deriveLegacyKeyIv(secret: string) {
|
||||
const { keyLength, ivLength } = crypto.getCipherInfo(algorithm); // 32, 16
|
||||
const pass = Buffer.isBuffer(secret) ? secret : Buffer.from(secret ?? '', 'utf8');
|
||||
|
||||
// evp_bytestokey: key length in **bits**, IV length in **bytes**
|
||||
const { key, iv } = EVP_BytesToKey(pass, null, keyLength * 8, ivLength, 'md5');
|
||||
|
||||
if (key.length !== keyLength || iv.length !== ivLength) {
|
||||
throw new Error(`Derived wrong sizes (key=${key.length}, iv=${iv.length})`);
|
||||
}
|
||||
return { key, iv };
|
||||
}
|
||||
|
||||
export function decrypt_legacy_using_IV(hexCiphertext: string) {
|
||||
const { key, iv } = deriveLegacyKeyIv(process.env.JWT_SECRET);
|
||||
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
||||
const out = Buffer.concat([decipher.update(hexCiphertext, 'hex'), decipher.final()]);
|
||||
return out.toString('utf8');
|
||||
}
|
||||
|
||||
export function encrypt_legacy_using_IV(utf8Plaintext: string) {
|
||||
const { key, iv } = deriveLegacyKeyIv(process.env.JWT_SECRET);
|
||||
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
||||
const out = Buffer.concat([cipher.update(utf8Plaintext, 'utf8'), cipher.final()]);
|
||||
return out.toString('hex');
|
||||
}
|
||||
export class AuthService {
|
||||
static hashPassword(password: string) {
|
||||
return hashSync(password, 10);
|
||||
|
|
@ -18,27 +47,10 @@ export class AuthService {
|
|||
}
|
||||
|
||||
static fixedEncryption(value: string) {
|
||||
// encryption algorithm
|
||||
const algorithm = 'aes-256-cbc';
|
||||
|
||||
// create a cipher object
|
||||
const cipher = crypto.createCipher(algorithm, process.env.JWT_SECRET);
|
||||
|
||||
// encrypt the plain text
|
||||
let encrypted = cipher.update(value, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
|
||||
return encrypted;
|
||||
return encrypt_legacy_using_IV(value);
|
||||
}
|
||||
|
||||
static fixedDecryption(hash: string) {
|
||||
const algorithm = 'aes-256-cbc';
|
||||
const decipher = crypto.createDecipher(algorithm, process.env.JWT_SECRET);
|
||||
|
||||
// decrypt the encrypted text
|
||||
let decrypted = decipher.update(hash, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
|
||||
return decrypted;
|
||||
return decrypt_legacy_using_IV(hash);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
libraries/helpers/src/utils/use.wait.for.class.tsx
Normal file
43
libraries/helpers/src/utils/use.wait.for.class.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* useWaitForClass
|
||||
*
|
||||
* Watches the DOM for the presence of a CSS class and resolves when found.
|
||||
*
|
||||
* @param className - The class to wait for (without the dot, e.g. "my-element")
|
||||
* @param root - The root node to observe (defaults to document.body)
|
||||
* @returns A boolean indicating if the class is currently present
|
||||
*/
|
||||
export function useWaitForClass(className: string, root: HTMLElement | null = null): boolean {
|
||||
const [found, setFound] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const target = root ?? document.body;
|
||||
|
||||
if (!target) return;
|
||||
|
||||
// Check immediately in case the element is already present
|
||||
if (target.querySelector(`.${className}`)) {
|
||||
setFound(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if (target.querySelector(`.${className}`)) {
|
||||
setFound(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(target, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [className, root]);
|
||||
|
||||
return found;
|
||||
}
|
||||
|
|
@ -8,11 +8,11 @@ import {
|
|||
export class ValidUrlExtension implements ValidatorConstraintInterface {
|
||||
validate(text: string, args: ValidationArguments) {
|
||||
return (
|
||||
text?.endsWith('.png') ||
|
||||
text?.endsWith('.jpg') ||
|
||||
text?.endsWith('.jpeg') ||
|
||||
text?.endsWith('.gif') ||
|
||||
text?.endsWith('.mp4')
|
||||
!!text?.split?.('?')?.[0].endsWith('.png') ||
|
||||
!!text?.split?.('?')?.[0].endsWith('.jpg') ||
|
||||
!!text?.split?.('?')?.[0].endsWith('.jpeg') ||
|
||||
!!text?.split?.('?')?.[0].endsWith('.gif') ||
|
||||
!!text?.split?.('?')?.[0].endsWith('.mp4')
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
18
libraries/nestjs-libraries/src/chat/agent.tool.interface.ts
Normal file
18
libraries/nestjs-libraries/src/chat/agent.tool.interface.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import type { ZodLikeSchema } from '@mastra/core/dist/types/zod-compat';
|
||||
import type {
|
||||
ToolExecutionContext,
|
||||
} from '@mastra/core/dist/tools/types';
|
||||
import { Tool } from '@mastra/core/dist/tools/tool';
|
||||
|
||||
export type ToolReturn = Tool<
|
||||
ZodLikeSchema,
|
||||
ZodLikeSchema,
|
||||
ZodLikeSchema,
|
||||
ZodLikeSchema,
|
||||
ToolExecutionContext<ZodLikeSchema, ZodLikeSchema, ZodLikeSchema>
|
||||
>;
|
||||
|
||||
export interface AgentToolInterface {
|
||||
name: string;
|
||||
run(): ToolReturn;
|
||||
}
|
||||
25
libraries/nestjs-libraries/src/chat/async.storage.ts
Normal file
25
libraries/nestjs-libraries/src/chat/async.storage.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// context.ts
|
||||
import { AsyncLocalStorage } from 'node:async_hooks';
|
||||
|
||||
type Ctx = {
|
||||
requestId: string;
|
||||
auth: any; // replace with your org type if you have it, e.g. Organization
|
||||
};
|
||||
|
||||
const als = new AsyncLocalStorage<Ctx>();
|
||||
|
||||
export function runWithContext<T>(ctx: Ctx, fn: () => Promise<T> | T) {
|
||||
return als.run(ctx, fn);
|
||||
}
|
||||
|
||||
export function getContext(): Ctx | undefined {
|
||||
return als.getStore();
|
||||
}
|
||||
|
||||
export function getAuth<T = any>(): T | undefined {
|
||||
return als.getStore()?.auth as T | undefined;
|
||||
}
|
||||
|
||||
export function getRequestId(): string | undefined {
|
||||
return als.getStore()?.requestId;
|
||||
}
|
||||
20
libraries/nestjs-libraries/src/chat/auth.context.ts
Normal file
20
libraries/nestjs-libraries/src/chat/auth.context.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { ToolAction } from '@mastra/core/dist/tools/types';
|
||||
import { getAuth } from '@gitroom/nestjs-libraries/chat/async.storage';
|
||||
|
||||
export const checkAuth: ToolAction['execute'] = async (
|
||||
{ runtimeContext },
|
||||
options
|
||||
) => {
|
||||
const auth = getAuth();
|
||||
// @ts-ignore
|
||||
if (options?.extra?.authInfo || auth) {
|
||||
runtimeContext.set(
|
||||
// @ts-ignore
|
||||
'organization',
|
||||
// @ts-ignore
|
||||
JSON.stringify(options?.extra?.authInfo || auth)
|
||||
);
|
||||
// @ts-ignore
|
||||
runtimeContext.set('ui', 'false');
|
||||
}
|
||||
};
|
||||
13
libraries/nestjs-libraries/src/chat/chat.module.ts
Normal file
13
libraries/nestjs-libraries/src/chat/chat.module.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Global, Module } from '@nestjs/common';
|
||||
import { LoadToolsService } from '@gitroom/nestjs-libraries/chat/load.tools.service';
|
||||
import { MastraService } from '@gitroom/nestjs-libraries/chat/mastra.service';
|
||||
import { toolList } from '@gitroom/nestjs-libraries/chat/tools/tool.list';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [MastraService, LoadToolsService, ...toolList],
|
||||
get exports() {
|
||||
return this.providers;
|
||||
},
|
||||
})
|
||||
export class ChatModule {}
|
||||
104
libraries/nestjs-libraries/src/chat/load.tools.service.ts
Normal file
104
libraries/nestjs-libraries/src/chat/load.tools.service.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Agent } from '@mastra/core/agent';
|
||||
import { openai } from '@ai-sdk/openai';
|
||||
import { Memory } from '@mastra/memory';
|
||||
import { pStore } from '@gitroom/nestjs-libraries/chat/mastra.store';
|
||||
import { array, object, string } from 'zod';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { toolList } from '@gitroom/nestjs-libraries/chat/tools/tool.list';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export const AgentState = object({
|
||||
proverbs: array(string()).default([]),
|
||||
});
|
||||
|
||||
const renderArray = (list: string[], show: boolean) => {
|
||||
if (!show) return '';
|
||||
return list.map((p) => `- ${p}`).join('\n');
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class LoadToolsService {
|
||||
constructor(private _moduleRef: ModuleRef) {}
|
||||
|
||||
async loadTools() {
|
||||
return (
|
||||
await Promise.all<{ name: string; tool: any }>(
|
||||
toolList
|
||||
.map((p) => this._moduleRef.get(p, { strict: false }))
|
||||
.map(async (p) => ({
|
||||
name: p.name as string,
|
||||
tool: await p.run(),
|
||||
}))
|
||||
)
|
||||
).reduce(
|
||||
(all, current) => ({
|
||||
...all,
|
||||
[current.name]: current.tool,
|
||||
}),
|
||||
{} as Record<string, any>
|
||||
);
|
||||
}
|
||||
|
||||
async agent() {
|
||||
const tools = await this.loadTools();
|
||||
return new Agent({
|
||||
name: 'postiz',
|
||||
description: 'Agent that helps manage and schedule social media posts for users',
|
||||
instructions: ({ runtimeContext }) => {
|
||||
const ui: string = runtimeContext.get('ui' as never);
|
||||
return `
|
||||
Global information:
|
||||
- Date (UTC): ${dayjs().format('YYYY-MM-DD HH:mm:ss')}
|
||||
|
||||
You are an agent that helps manage and schedule social media posts for users, you can:
|
||||
- Schedule posts into the future, or now, adding texts, images and videos
|
||||
- Generate pictures for posts
|
||||
- Generate videos for posts
|
||||
- Generate text for posts
|
||||
- Show global analytics about socials
|
||||
- List integrations (channels)
|
||||
|
||||
- We schedule posts to different integration like facebook, instagram, etc. but to the user we don't say integrations we say channels as integration is the technical name
|
||||
- When scheduling a post, you must follow the social media rules and best practices.
|
||||
- When scheduling a post, you can pass an array for list of posts for a social media platform, But it has different behavior depending on the platform.
|
||||
- For platforms like Threads, Bluesky and X (Twitter), each post in the array will be a separate post in the thread.
|
||||
- For platforms like LinkedIn and Facebook, second part of the array will be added as "comments" to the first post.
|
||||
- If the social media platform has the concept of "threads", we need to ask the user if they want to create a thread or one long post.
|
||||
- For X, if you don't have Premium, don't suggest a long post because it won't work.
|
||||
- Platform format will also be passed can be "normal", "markdown", "html", make sure you use the correct format for each platform.
|
||||
|
||||
- Sometimes 'integrationSchema' will return rules, make sure you follow them (these rules are set in stone, even if the user asks to ignore them)
|
||||
- Each socials media platform has different settings and rules, you can get them by using the integrationSchema tool.
|
||||
- Always make sure you use this tool before you schedule any post.
|
||||
- In every message I will send you the list of needed social medias (id and platform), if you already have the information use it, if not, use the integrationSchema tool to get it.
|
||||
- Make sure you always take the last information I give you about the socials, it might have changed.
|
||||
- Before scheduling a post, always make sure you ask the user confirmation by providing all the details of the post (text, images, videos, date, time, social media platform, account).
|
||||
- Between tools, we will reference things like: [output:name] and [input:name] to set the information right.
|
||||
- When outputting a date for the user, make sure it's human readable with time
|
||||
- The content of the post, HTML, Each line must be wrapped in <p> here is the possible tags: h1, h2, h3, u, strong, li, ul, p (you can\'t have u and strong together), don't use a "code" box
|
||||
${renderArray(
|
||||
[
|
||||
'If the user confirm, ask if they would like to get a modal with populated content without scheduling the post yet or if they want to schedule it right away.',
|
||||
],
|
||||
!!ui
|
||||
)}
|
||||
`;
|
||||
},
|
||||
model: openai('gpt-4.1'),
|
||||
tools,
|
||||
memory: new Memory({
|
||||
storage: pStore,
|
||||
options: {
|
||||
threads: {
|
||||
generateTitle: true,
|
||||
},
|
||||
workingMemory: {
|
||||
enabled: true,
|
||||
schema: AgentState,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
26
libraries/nestjs-libraries/src/chat/mastra.service.ts
Normal file
26
libraries/nestjs-libraries/src/chat/mastra.service.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { Mastra } from '@mastra/core/mastra';
|
||||
import { ConsoleLogger } from '@mastra/core/logger';
|
||||
import { pStore } from '@gitroom/nestjs-libraries/chat/mastra.store';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { LoadToolsService } from '@gitroom/nestjs-libraries/chat/load.tools.service';
|
||||
|
||||
@Injectable()
|
||||
export class MastraService {
|
||||
static mastra: Mastra;
|
||||
constructor(private _loadToolsService: LoadToolsService) {}
|
||||
async mastra() {
|
||||
MastraService.mastra =
|
||||
MastraService.mastra ||
|
||||
new Mastra({
|
||||
storage: pStore,
|
||||
agents: {
|
||||
postiz: await this._loadToolsService.agent(),
|
||||
},
|
||||
logger: new ConsoleLogger({
|
||||
level: 'info',
|
||||
}),
|
||||
});
|
||||
|
||||
return MastraService.mastra;
|
||||
}
|
||||
}
|
||||
5
libraries/nestjs-libraries/src/chat/mastra.store.ts
Normal file
5
libraries/nestjs-libraries/src/chat/mastra.store.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { PostgresStore, PgVector } from '@mastra/pg';
|
||||
|
||||
export const pStore = new PostgresStore({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import 'reflect-metadata';
|
||||
|
||||
export function Rules(description: string) {
|
||||
return function (target: any) {
|
||||
// Define metadata on the class prototype (so it can be retrieved from the class)
|
||||
Reflect.defineMetadata(
|
||||
'custom:rules:description',
|
||||
description,
|
||||
target
|
||||
);
|
||||
};
|
||||
}
|
||||
66
libraries/nestjs-libraries/src/chat/start.mcp.ts
Normal file
66
libraries/nestjs-libraries/src/chat/start.mcp.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { INestApplication } from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { MastraService } from '@gitroom/nestjs-libraries/chat/mastra.service';
|
||||
import { MCPServer } from '@mastra/mcp';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
import { runWithContext } from './async.storage';
|
||||
export const startMcp = async (app: INestApplication) => {
|
||||
const mastraService = app.get(MastraService, { strict: false });
|
||||
const organizationService = app.get(OrganizationService, { strict: false });
|
||||
|
||||
const mastra = await mastraService.mastra();
|
||||
const agent = mastra.getAgent('postiz');
|
||||
const tools = await agent.getTools();
|
||||
|
||||
const server = new MCPServer({
|
||||
name: 'Postiz MCP',
|
||||
version: '1.0.0',
|
||||
tools,
|
||||
agents: { postiz: agent },
|
||||
});
|
||||
|
||||
app.use(
|
||||
'/mcp/:id',
|
||||
async (req: Request, res: Response) => {
|
||||
// @ts-ignore
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', '*');
|
||||
res.setHeader('Access-Control-Allow-Headers', '*');
|
||||
res.setHeader('Access-Control-Expose-Headers', '*');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
req.auth = await organizationService.getOrgByApiKey(req.params.id);
|
||||
// @ts-ignore
|
||||
if (!req.auth) {
|
||||
res.status(400).send('Invalid API Key');
|
||||
return ;
|
||||
}
|
||||
|
||||
const url = new URL(
|
||||
`/mcp/${req.params.id}`,
|
||||
process.env.NEXT_PUBLIC_BACKEND_URL
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
await runWithContext({ requestId: req.params.id, auth: req.auth }, async () => {
|
||||
await server.startHTTP({
|
||||
url,
|
||||
httpPath: url.pathname,
|
||||
options: {
|
||||
sessionIdGenerator: () => {
|
||||
return randomUUID();
|
||||
},
|
||||
},
|
||||
req,
|
||||
res,
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { AgentToolInterface } from '@gitroom/nestjs-libraries/chat/agent.tool.interface';
|
||||
import { createTool } from '@mastra/core/tools';
|
||||
import { z } from 'zod';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { checkAuth } from '@gitroom/nestjs-libraries/chat/auth.context';
|
||||
|
||||
@Injectable()
|
||||
export class GenerateImageTool implements AgentToolInterface {
|
||||
private storage = UploadFactory.createStorage();
|
||||
|
||||
constructor(private _mediaService: MediaService) {}
|
||||
name = 'generateImageTool';
|
||||
|
||||
run() {
|
||||
return createTool({
|
||||
id: 'generateImageTool',
|
||||
description: `Generate image to use in a post,
|
||||
in case the user specified a platform that requires attachment and attachment was not provided,
|
||||
ask if they want to generate a picture of a video.
|
||||
`,
|
||||
inputSchema: z.object({
|
||||
prompt: z.string(),
|
||||
}),
|
||||
outputSchema: z.object({
|
||||
id: z.string(),
|
||||
path: z.string(),
|
||||
}),
|
||||
execute: async (args, options) => {
|
||||
const { context, runtimeContext } = args;
|
||||
checkAuth(args, options);
|
||||
// @ts-ignore
|
||||
const org = JSON.parse(runtimeContext.get('organization') as string);
|
||||
const image = await this._mediaService.generateImage(
|
||||
context.prompt,
|
||||
org
|
||||
);
|
||||
|
||||
const file = await this.storage.uploadSimple(
|
||||
'data:image/png;base64,' + image
|
||||
);
|
||||
|
||||
return this._mediaService.saveFile(org.id, file.split('/').pop(), file);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { AgentToolInterface, ToolReturn } from '@gitroom/nestjs-libraries/chat/agent.tool.interface';
|
||||
import { createTool } from '@mastra/core/tools';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { validationMetadatasToSchemas } from 'class-validator-jsonschema';
|
||||
import { VideoManager } from '@gitroom/nestjs-libraries/videos/video.manager';
|
||||
import z from 'zod';
|
||||
import { checkAuth } from '@gitroom/nestjs-libraries/chat/auth.context';
|
||||
|
||||
@Injectable()
|
||||
export class GenerateVideoOptionsTool implements AgentToolInterface {
|
||||
constructor(private _videoManagerService: VideoManager) {}
|
||||
name = 'generateVideoOptions';
|
||||
|
||||
run() {
|
||||
return createTool({
|
||||
id: 'generateVideoOptions',
|
||||
description: `All the options to generate videos, some tools might require another call to generateVideoFunction`,
|
||||
outputSchema: z.object({
|
||||
video: z.array(
|
||||
z.object({
|
||||
type: z.string(),
|
||||
output: z.string(),
|
||||
tools: z.array(
|
||||
z.object({
|
||||
functionName: z.string(),
|
||||
output: z.string(),
|
||||
})
|
||||
),
|
||||
customParams: z.any(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
execute: async (args, options) => {
|
||||
const { context, runtimeContext } = args;
|
||||
checkAuth(args, options);
|
||||
const videos = this._videoManagerService.getAllVideos();
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
video: videos.map((p) => {
|
||||
return {
|
||||
type: p.identifier,
|
||||
output: 'vertical|horizontal',
|
||||
tools: p.tools,
|
||||
customParams: validationMetadatasToSchemas()[p.dto.name],
|
||||
};
|
||||
}),
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
return {
|
||||
video: videos.map((p) => {
|
||||
return {
|
||||
type: p.identifier,
|
||||
output: 'vertical|horizontal',
|
||||
tools: p.tools,
|
||||
customParams: validationMetadatasToSchemas()[p.dto.name],
|
||||
};
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import { AgentToolInterface } from '@gitroom/nestjs-libraries/chat/agent.tool.interface';
|
||||
import { createTool } from '@mastra/core/tools';
|
||||
import { z } from 'zod';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
IntegrationManager,
|
||||
socialIntegrationList,
|
||||
} from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { validationMetadatasToSchemas } from 'class-validator-jsonschema';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
|
||||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
import { VideoManager } from '@gitroom/nestjs-libraries/videos/video.manager';
|
||||
import { checkAuth } from '@gitroom/nestjs-libraries/chat/auth.context';
|
||||
|
||||
@Injectable()
|
||||
export class GenerateVideoTool implements AgentToolInterface {
|
||||
constructor(
|
||||
private _mediaService: MediaService,
|
||||
private _videoManager: VideoManager
|
||||
) {}
|
||||
name = 'generateVideoTool';
|
||||
|
||||
run() {
|
||||
return createTool({
|
||||
id: 'generateVideoTool',
|
||||
description: `Generate video to use in a post,
|
||||
in case the user specified a platform that requires attachment and attachment was not provided,
|
||||
ask if they want to generate a picture of a video.
|
||||
In many cases 'videoFunctionTool' will need to be called first, to get things like voice id
|
||||
Here are the type of video that can be generated:
|
||||
${this._videoManager
|
||||
.getAllVideos()
|
||||
.map((p) => "-" + p.title)
|
||||
.join('\n')}
|
||||
`,
|
||||
inputSchema: z.object({
|
||||
identifier: z.string(),
|
||||
output: z.enum(['vertical', 'horizontal']),
|
||||
customParams: z.array(
|
||||
z.object({
|
||||
key: z.string().describe('Name of the settings key to pass'),
|
||||
value: z.any().describe('Value of the key'),
|
||||
})
|
||||
),
|
||||
}),
|
||||
outputSchema: z.object({
|
||||
url: z.string(),
|
||||
}),
|
||||
execute: async (args, options) => {
|
||||
const { context, runtimeContext } = args;
|
||||
checkAuth(args, options);
|
||||
// @ts-ignore
|
||||
const org = JSON.parse(runtimeContext.get('organization') as string);
|
||||
const value = await this._mediaService.generateVideo(org, {
|
||||
type: context.identifier,
|
||||
output: context.output,
|
||||
customParams: context.customParams.reduce(
|
||||
(all, current) => ({
|
||||
...all,
|
||||
[current.key]: current.value,
|
||||
}),
|
||||
{}
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
url: value.path,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import {
|
||||
AgentToolInterface,
|
||||
ToolReturn,
|
||||
} from '@gitroom/nestjs-libraries/chat/agent.tool.interface';
|
||||
import { createTool } from '@mastra/core/tools';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import z from 'zod';
|
||||
import { checkAuth } from '@gitroom/nestjs-libraries/chat/auth.context';
|
||||
import { getAuth } from '@gitroom/nestjs-libraries/chat/async.storage';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationListTool implements AgentToolInterface {
|
||||
constructor(private _integrationService: IntegrationService) {}
|
||||
name = 'integrationList';
|
||||
|
||||
run() {
|
||||
return createTool({
|
||||
id: 'integrationList',
|
||||
description: `This tool list available integrations to schedule posts to`,
|
||||
outputSchema: z.object({
|
||||
output: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
picture: z.string(),
|
||||
platform: z.string(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
execute: async (args, options) => {
|
||||
console.log(getAuth());
|
||||
console.log(options);
|
||||
const { context, runtimeContext } = args;
|
||||
checkAuth(args, options);
|
||||
const organizationId = JSON.parse(
|
||||
// @ts-ignore
|
||||
runtimeContext.get('organization') as string
|
||||
).id;
|
||||
|
||||
return {
|
||||
output: (
|
||||
await this._integrationService.getIntegrationsList(organizationId)
|
||||
).map((p) => ({
|
||||
name: p.name,
|
||||
id: p.id,
|
||||
disabled: p.disabled,
|
||||
picture: p.picture || '/no-picture.jpg',
|
||||
platform: p.providerIdentifier,
|
||||
display: p.profile,
|
||||
type: p.type,
|
||||
})),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
import { AgentToolInterface } from '@gitroom/nestjs-libraries/chat/agent.tool.interface';
|
||||
import { createTool } from '@mastra/core/tools';
|
||||
import { z } from 'zod';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { socialIntegrationList } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { AllProvidersSettings } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/all.providers.settings';
|
||||
import { validate } from 'class-validator';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { checkAuth } from '@gitroom/nestjs-libraries/chat/auth.context';
|
||||
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
|
||||
import { weightedLength } from '@gitroom/helpers/utils/count.length';
|
||||
|
||||
function countCharacters(text: string, type: string): number {
|
||||
if (type !== 'x') {
|
||||
return text.length;
|
||||
}
|
||||
return weightedLength(text);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationSchedulePostTool implements AgentToolInterface {
|
||||
constructor(
|
||||
private _postsService: PostsService,
|
||||
private _integrationService: IntegrationService
|
||||
) {}
|
||||
name = 'integrationSchedulePostTool';
|
||||
|
||||
run() {
|
||||
return createTool({
|
||||
id: 'schedulePostTool',
|
||||
description: `
|
||||
This tool allows you to schedule a post to a social media platform, based on integrationSchema tool.
|
||||
So for example:
|
||||
|
||||
If the user want to post a post to LinkedIn with one comment
|
||||
- socialPost array length will be one
|
||||
- postsAndComments array length will be two (one for the post, one for the comment)
|
||||
|
||||
If the user want to post 20 posts for facebook each in individual days without comments
|
||||
- socialPost array length will be 20
|
||||
- postsAndComments array length will be one
|
||||
|
||||
If the tools return errors, you would need to rerun it with the right parameters, don't ask again, just run it
|
||||
`,
|
||||
inputSchema: z.object({
|
||||
socialPost: z
|
||||
.array(
|
||||
z.object({
|
||||
integrationId: z
|
||||
.string()
|
||||
.describe('The id of the integration (not internal id)'),
|
||||
isPremium: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"If the integration is X, return if it's premium or not"
|
||||
),
|
||||
date: z.string().describe('The date of the post in UTC time'),
|
||||
shortLink: z
|
||||
.boolean()
|
||||
.describe(
|
||||
'If the post has a link inside, we can ask the user if they want to add a short link'
|
||||
),
|
||||
type: z
|
||||
.enum(['draft', 'schedule', 'now'])
|
||||
.describe(
|
||||
'The type of the post, if we pass now, we should pass the current date also'
|
||||
),
|
||||
postsAndComments: z
|
||||
.array(
|
||||
z.object({
|
||||
content: z
|
||||
.string()
|
||||
.describe(
|
||||
"The content of the post, HTML, Each line must be wrapped in <p> here is the possible tags: h1, h2, h3, u, strong, li, ul, p (you can't have u and strong together)"
|
||||
),
|
||||
attachments: z
|
||||
.array(z.string())
|
||||
.describe('The image of the post (URLS)'),
|
||||
})
|
||||
)
|
||||
.describe(
|
||||
'first item is the post, every other item is the comments'
|
||||
),
|
||||
settings: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z
|
||||
.string()
|
||||
.describe('Name of the settings key to pass'),
|
||||
value: z
|
||||
.any()
|
||||
.describe(
|
||||
'Value of the key, always prefer the id then label if possible'
|
||||
),
|
||||
})
|
||||
)
|
||||
.describe(
|
||||
'This relies on the integrationSchema tool to get the settings [input:settings]'
|
||||
),
|
||||
})
|
||||
)
|
||||
.describe('Individual post'),
|
||||
}),
|
||||
outputSchema: z.object({
|
||||
output: z
|
||||
.array(
|
||||
z.object({
|
||||
postId: z.string(),
|
||||
integration: z.string(),
|
||||
})
|
||||
)
|
||||
.or(z.object({ errors: z.string() })),
|
||||
}),
|
||||
execute: async (args, options) => {
|
||||
const { context, runtimeContext } = args;
|
||||
checkAuth(args, options);
|
||||
const organizationId = JSON.parse(
|
||||
// @ts-ignore
|
||||
runtimeContext.get('organization') as string
|
||||
).id;
|
||||
const finalOutput = [];
|
||||
|
||||
const integrations = {} as Record<string, Integration>;
|
||||
for (const platform of context.socialPost) {
|
||||
integrations[platform.integrationId] =
|
||||
await this._integrationService.getIntegrationById(
|
||||
organizationId,
|
||||
platform.integrationId
|
||||
);
|
||||
|
||||
const { dto, maxLength, identifier } = socialIntegrationList.find(
|
||||
(p) =>
|
||||
p.identifier ===
|
||||
integrations[platform.integrationId].providerIdentifier
|
||||
)!;
|
||||
|
||||
if (dto) {
|
||||
const newDTO = new dto();
|
||||
const obj = Object.assign(
|
||||
newDTO,
|
||||
platform.settings.reduce(
|
||||
(acc, s) => ({
|
||||
...acc,
|
||||
[s.key]: s.value,
|
||||
}),
|
||||
{} as AllProvidersSettings
|
||||
)
|
||||
);
|
||||
const errors = await validate(obj);
|
||||
if (errors.length) {
|
||||
return {
|
||||
errors: JSON.stringify(errors),
|
||||
};
|
||||
}
|
||||
|
||||
const errorsLength = [];
|
||||
for (const post of platform.postsAndComments) {
|
||||
const maximumCharacters = maxLength(platform.isPremium);
|
||||
const strip = stripHtmlValidation('normal', post.content, true);
|
||||
const weightedLength = countCharacters(strip, identifier || '');
|
||||
const totalCharacters =
|
||||
weightedLength > strip.length ? weightedLength : strip.length;
|
||||
|
||||
if (totalCharacters > (maximumCharacters || 1000000)) {
|
||||
errorsLength.push({
|
||||
value: post.content,
|
||||
error: `The maximum characters is ${maximumCharacters}, we got ${totalCharacters}, please fix it, and try integrationSchedulePostTool again.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (errorsLength.length) {
|
||||
return {
|
||||
errors: JSON.stringify(errorsLength),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const post of context.socialPost) {
|
||||
const integration = integrations[post.integrationId];
|
||||
|
||||
if (!integration) {
|
||||
throw new Error('Integration not found');
|
||||
}
|
||||
|
||||
const output = await this._postsService.createPost(organizationId, {
|
||||
date: post.date,
|
||||
type: post.type as 'draft' | 'schedule' | 'now',
|
||||
shortLink: post.shortLink,
|
||||
tags: [],
|
||||
posts: [
|
||||
{
|
||||
integration,
|
||||
group: makeId(10),
|
||||
settings: post.settings.reduce(
|
||||
(acc, s) => ({
|
||||
...acc,
|
||||
[s.key]: s.value,
|
||||
}),
|
||||
{} as AllProvidersSettings
|
||||
),
|
||||
value: post.postsAndComments.map((p) => ({
|
||||
content: p.content,
|
||||
id: makeId(10),
|
||||
image: p.attachments.map((p) => ({
|
||||
id: makeId(10),
|
||||
path: p,
|
||||
})),
|
||||
})),
|
||||
},
|
||||
],
|
||||
});
|
||||
finalOutput.push(...output);
|
||||
}
|
||||
|
||||
return {
|
||||
output: finalOutput,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
import { AgentToolInterface } from '@gitroom/nestjs-libraries/chat/agent.tool.interface';
|
||||
import { createTool } from '@mastra/core/tools';
|
||||
import { z } from 'zod';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
IntegrationManager,
|
||||
socialIntegrationList,
|
||||
} from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { validationMetadatasToSchemas } from 'class-validator-jsonschema';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { checkAuth } from '@gitroom/nestjs-libraries/chat/auth.context';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationTriggerTool implements AgentToolInterface {
|
||||
constructor(
|
||||
private _integrationManager: IntegrationManager,
|
||||
private _integrationService: IntegrationService
|
||||
) {}
|
||||
name = 'triggerTool';
|
||||
|
||||
run() {
|
||||
return createTool({
|
||||
id: 'triggerTool',
|
||||
description: `After using the integrationSchema, we sometimes miss details we can\'t ask from the user, like ids.
|
||||
Sometimes this tool requires to user prompt for some settings, like a word to search for. methodName is required [input:callable-tools]`,
|
||||
inputSchema: z.object({
|
||||
integrationId: z.string().describe('The id of the integration'),
|
||||
methodName: z
|
||||
.string()
|
||||
.describe(
|
||||
'The methodName from the `integrationSchema` functions in the tools array, required'
|
||||
),
|
||||
dataSchema: z.array(
|
||||
z.object({
|
||||
key: z.string().describe('Name of the settings key to pass'),
|
||||
value: z.string().describe('Value of the key'),
|
||||
})
|
||||
),
|
||||
}),
|
||||
outputSchema: z.object({
|
||||
output: z.array(z.record(z.string(), z.any())),
|
||||
}),
|
||||
execute: async (args, options) => {
|
||||
const { context, runtimeContext } = args;
|
||||
checkAuth(args, options);
|
||||
console.log('triggerTool', context);
|
||||
const organizationId = JSON.parse(
|
||||
// @ts-ignore
|
||||
runtimeContext.get('organization') as string
|
||||
).id;
|
||||
|
||||
const getIntegration =
|
||||
await this._integrationService.getIntegrationById(
|
||||
organizationId,
|
||||
context.integrationId
|
||||
);
|
||||
|
||||
if (!getIntegration) {
|
||||
return {
|
||||
output: 'Integration not found',
|
||||
};
|
||||
}
|
||||
|
||||
const integrationProvider = socialIntegrationList.find(
|
||||
(p) => p.identifier === getIntegration.providerIdentifier
|
||||
)!;
|
||||
|
||||
if (!integrationProvider) {
|
||||
return {
|
||||
output: 'Integration not found',
|
||||
};
|
||||
}
|
||||
|
||||
const tools = this._integrationManager.getAllTools();
|
||||
if (
|
||||
// @ts-ignore
|
||||
!tools[integrationProvider.identifier].some(
|
||||
(p) => p.methodName === context.methodName
|
||||
) ||
|
||||
// @ts-ignore
|
||||
!integrationProvider[context.methodName]
|
||||
) {
|
||||
return { output: 'tool not found' };
|
||||
}
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const load = await integrationProvider[context.methodName](
|
||||
getIntegration.token,
|
||||
context.dataSchema.reduce(
|
||||
(all, current) => ({
|
||||
...all,
|
||||
[current.key]: current.value,
|
||||
}),
|
||||
{}
|
||||
),
|
||||
getIntegration.internalId,
|
||||
getIntegration
|
||||
);
|
||||
|
||||
return { output: load };
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
if (err instanceof RefreshToken) {
|
||||
const {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
additionalSettings,
|
||||
} = await integrationProvider.refreshToken(
|
||||
getIntegration.refreshToken
|
||||
);
|
||||
|
||||
if (accessToken) {
|
||||
await this._integrationService.createOrUpdateIntegration(
|
||||
additionalSettings,
|
||||
!!integrationProvider.oneTimeToken,
|
||||
getIntegration.organizationId,
|
||||
getIntegration.name,
|
||||
getIntegration.picture!,
|
||||
'social',
|
||||
getIntegration.internalId,
|
||||
getIntegration.providerIdentifier,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn
|
||||
);
|
||||
|
||||
getIntegration.token = accessToken;
|
||||
|
||||
if (integrationProvider.refreshWait) {
|
||||
await timer(10000);
|
||||
}
|
||||
|
||||
continue;
|
||||
} else {
|
||||
await this._integrationService.disconnectChannel(
|
||||
organizationId,
|
||||
getIntegration
|
||||
);
|
||||
return {
|
||||
output:
|
||||
'We had to disconnect the channel as the token expired',
|
||||
};
|
||||
}
|
||||
}
|
||||
return { output: 'Unexpected error' };
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import { AgentToolInterface } from '@gitroom/nestjs-libraries/chat/agent.tool.interface';
|
||||
import { createTool } from '@mastra/core/tools';
|
||||
import { z } from 'zod';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
IntegrationManager,
|
||||
socialIntegrationList,
|
||||
} from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { validationMetadatasToSchemas } from 'class-validator-jsonschema';
|
||||
import { checkAuth } from '@gitroom/nestjs-libraries/chat/auth.context';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationValidationTool implements AgentToolInterface {
|
||||
constructor(private _integrationManager: IntegrationManager) {}
|
||||
name = 'integrationSchema';
|
||||
|
||||
run() {
|
||||
return createTool({
|
||||
id: 'integrationSchema',
|
||||
description: `Everytime we want to schedule a social media post, we need to understand the schema of the integration.
|
||||
This tool helps us get the schema of the integration.
|
||||
Sometimes we might get a schema back the requires some id, for that, you can get information from 'tools'
|
||||
And use the triggerTool function.
|
||||
`,
|
||||
inputSchema: z.object({
|
||||
isPremium: z
|
||||
.boolean()
|
||||
.describe('is this the user premium? if not, set to false'),
|
||||
platform: z
|
||||
.string()
|
||||
.describe(
|
||||
`platform identifier (${socialIntegrationList
|
||||
.map((p) => p.identifier)
|
||||
.join(', ')})`
|
||||
),
|
||||
}),
|
||||
outputSchema: z.object({
|
||||
output: z.object({
|
||||
rules: z.string(),
|
||||
maxLength: z
|
||||
.number()
|
||||
.describe('The maximum length of a post / comment'),
|
||||
settings: z
|
||||
.any()
|
||||
.describe('List of settings need to be passed to schedule a post'),
|
||||
tools: z
|
||||
.array(
|
||||
z.object({
|
||||
description: z.string().describe('Description of the tool'),
|
||||
methodName: z
|
||||
.string()
|
||||
.describe('Method to call to get the information'),
|
||||
dataSchema: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z
|
||||
.string()
|
||||
.describe('Name of the settings key to pass'),
|
||||
description: z
|
||||
.string()
|
||||
.describe('Description of the setting key'),
|
||||
type: z.string(),
|
||||
})
|
||||
)
|
||||
.describe(
|
||||
'This will be passed to schedulePostTool [output:settings]'
|
||||
),
|
||||
})
|
||||
)
|
||||
.describe(
|
||||
"Sometimes settings require some id, tags and stuff, if you don't have, trigger the `triggerTool` function from the tools list [output:callable-tools]"
|
||||
),
|
||||
}),
|
||||
}),
|
||||
execute: async (args, options) => {
|
||||
const { context, runtimeContext } = args;
|
||||
checkAuth(args, options);
|
||||
const integration = socialIntegrationList.find(
|
||||
(p) => p.identifier === context.platform
|
||||
)!;
|
||||
|
||||
if (!integration) {
|
||||
return {
|
||||
output: { rules: '', maxLength: 0, settings: {}, tools: [] },
|
||||
};
|
||||
}
|
||||
|
||||
const maxLength = integration.maxLength(context.isPremium);
|
||||
const schemas = !integration.dto
|
||||
? false
|
||||
: validationMetadatasToSchemas()[integration.dto.name];
|
||||
const tools = this._integrationManager.getAllTools();
|
||||
const rules = this._integrationManager.getAllRulesDescription();
|
||||
|
||||
return {
|
||||
output: {
|
||||
rules: rules[integration.identifier],
|
||||
maxLength,
|
||||
settings: !schemas ? 'No additional settings required' : schemas,
|
||||
tools: tools[integration.identifier],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
19
libraries/nestjs-libraries/src/chat/tools/tool.list.ts
Normal file
19
libraries/nestjs-libraries/src/chat/tools/tool.list.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { IntegrationValidationTool } from '@gitroom/nestjs-libraries/chat/tools/integration.validation.tool';
|
||||
import { IntegrationTriggerTool } from '@gitroom/nestjs-libraries/chat/tools/integration.trigger.tool';
|
||||
import { IntegrationSchedulePostTool } from './integration.schedule.post';
|
||||
import { GenerateVideoOptionsTool } from '@gitroom/nestjs-libraries/chat/tools/generate.video.options.tool';
|
||||
import { VideoFunctionTool } from '@gitroom/nestjs-libraries/chat/tools/video.function.tool';
|
||||
import { GenerateVideoTool } from '@gitroom/nestjs-libraries/chat/tools/generate.video.tool';
|
||||
import { GenerateImageTool } from '@gitroom/nestjs-libraries/chat/tools/generate.image.tool';
|
||||
import { IntegrationListTool } from '@gitroom/nestjs-libraries/chat/tools/integration.list.tool';
|
||||
|
||||
export const toolList = [
|
||||
IntegrationListTool,
|
||||
IntegrationValidationTool,
|
||||
IntegrationTriggerTool,
|
||||
IntegrationSchedulePostTool,
|
||||
GenerateVideoOptionsTool,
|
||||
VideoFunctionTool,
|
||||
GenerateVideoTool,
|
||||
GenerateImageTool,
|
||||
];
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { AgentToolInterface } from '@gitroom/nestjs-libraries/chat/agent.tool.interface';
|
||||
import { createTool } from '@mastra/core/tools';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { VideoManager } from '@gitroom/nestjs-libraries/videos/video.manager';
|
||||
import z from 'zod';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { checkAuth } from '@gitroom/nestjs-libraries/chat/auth.context';
|
||||
|
||||
@Injectable()
|
||||
export class VideoFunctionTool implements AgentToolInterface {
|
||||
constructor(
|
||||
private _videoManagerService: VideoManager,
|
||||
private _moduleRef: ModuleRef
|
||||
) {}
|
||||
name = 'videoFunctionTool';
|
||||
|
||||
run() {
|
||||
return createTool({
|
||||
id: 'videoFunctionTool',
|
||||
description: `Sometimes when we want to generate videos we might need to get some additional information like voice_id, etc`,
|
||||
inputSchema: z.object({
|
||||
identifier: z.string(),
|
||||
functionName: z.string(),
|
||||
}),
|
||||
execute: async (args, options) => {
|
||||
const { context, runtimeContext } = args;
|
||||
checkAuth(args, options);
|
||||
const videos = this._videoManagerService.getAllVideos();
|
||||
const findVideo = videos.find(
|
||||
(p) =>
|
||||
p.identifier === context.identifier &&
|
||||
p.tools.some((p) => p.functionName === context.functionName)
|
||||
);
|
||||
|
||||
if (!findVideo) {
|
||||
return { error: 'Function not found' };
|
||||
}
|
||||
|
||||
const func = await this._moduleRef
|
||||
// @ts-ignore
|
||||
.get(findVideo.target, { strict: false })
|
||||
[context.functionName]();
|
||||
return func;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ export class MediaService {
|
|||
org: Organization,
|
||||
generatePromptFirst?: boolean
|
||||
) {
|
||||
return await this._subscriptionService.useCredit(
|
||||
const generating = await this._subscriptionService.useCredit(
|
||||
org,
|
||||
'ai_images',
|
||||
async () => {
|
||||
|
|
@ -48,6 +48,8 @@ export class MediaService {
|
|||
return this._openAi.generateImage(prompt, !!generatePromptFirst);
|
||||
}
|
||||
);
|
||||
|
||||
return generating;
|
||||
}
|
||||
|
||||
saveFile(org: string, fileName: string, filePath: string) {
|
||||
|
|
@ -84,6 +86,7 @@ export class MediaService {
|
|||
org,
|
||||
'ai_videos'
|
||||
);
|
||||
|
||||
if (totalCredits.credits <= 0) {
|
||||
throw new SubscriptionException({
|
||||
action: AuthorizationActions.Create,
|
||||
|
|
@ -100,7 +103,9 @@ export class MediaService {
|
|||
throw new HttpException('This video is not available in trial mode', 406);
|
||||
}
|
||||
|
||||
console.log(body.customParams);
|
||||
await video.instance.processAndValidate(body.customParams);
|
||||
console.log('no err');
|
||||
|
||||
return await this._subscriptionService.useCredit(
|
||||
org,
|
||||
|
|
@ -125,8 +130,14 @@ export class MediaService {
|
|||
|
||||
// @ts-ignore
|
||||
const functionToCall = video.instance[functionName];
|
||||
if (typeof functionToCall !== 'function' || this._videoManager.checkAvailableVideoFunction(functionToCall)) {
|
||||
throw new HttpException(`Function ${functionName} not found on video instance`, 400);
|
||||
if (
|
||||
typeof functionToCall !== 'function' ||
|
||||
this._videoManager.checkAvailableVideoFunction(functionToCall)
|
||||
) {
|
||||
throw new HttpException(
|
||||
`Function ${functionName} not found on video instance`,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
return functionToCall(body);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import { plainToInstance } from 'class-transformer';
|
|||
import { validate } from 'class-validator';
|
||||
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
|
||||
dayjs.extend(utc);
|
||||
import * as Sentry from '@sentry/nestjs';
|
||||
|
||||
type PostWithConditionals = Post & {
|
||||
integration?: Integration;
|
||||
|
|
@ -717,6 +718,7 @@ export class PostsService {
|
|||
});
|
||||
}
|
||||
|
||||
Sentry.metrics.count("post_created", 1);
|
||||
postList.push({
|
||||
postId: posts[0].id,
|
||||
integration: post.integration.id,
|
||||
|
|
|
|||
9
libraries/nestjs-libraries/src/dtos/media/upload.dto.ts
Normal file
9
libraries/nestjs-libraries/src/dtos/media/upload.dto.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { IsDefined, IsString, Validate } from 'class-validator';
|
||||
import { ValidUrlExtension } from '@gitroom/helpers/utils/valid.url.path';
|
||||
|
||||
export class UploadDto {
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
@Validate(ValidUrlExtension)
|
||||
url: string;
|
||||
}
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
import { IsDefined, IsString, MinLength } from 'class-validator';
|
||||
import { JSONSchema } from 'class-validator-jsonschema';
|
||||
|
||||
export class DiscordDto {
|
||||
@MinLength(1)
|
||||
@IsDefined()
|
||||
@IsString()
|
||||
@JSONSchema({
|
||||
description: 'Channel must be an id',
|
||||
})
|
||||
channel: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import { Type } from 'class-transformer';
|
||||
import { IsString, ValidateNested } from 'class-validator';
|
||||
|
||||
export class FarcasterId {
|
||||
@IsString()
|
||||
id: string;
|
||||
}
|
||||
export class FarcasterValue {
|
||||
@ValidateNested()
|
||||
@Type(() => FarcasterId)
|
||||
value: FarcasterId;
|
||||
}
|
||||
export class FarcasterDto {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => FarcasterValue)
|
||||
subreddit: FarcasterValue[];
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { IsOptional, IsString, MinLength } from 'class-validator';
|
||||
import { JSONSchema } from 'class-validator-jsonschema';
|
||||
|
||||
export class ListmonkDto {
|
||||
@IsString()
|
||||
|
|
@ -9,9 +10,15 @@ export class ListmonkDto {
|
|||
preview: string;
|
||||
|
||||
@IsString()
|
||||
@JSONSchema({
|
||||
description: 'List must be an id',
|
||||
})
|
||||
list: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@JSONSchema({
|
||||
description: 'Template must be an id',
|
||||
})
|
||||
template: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
IsDefined, IsOptional, IsString, IsUrl, MaxLength, MinLength, ValidateIf
|
||||
} from 'class-validator';
|
||||
import { JSONSchema } from 'class-validator-jsonschema';
|
||||
|
||||
export class PinterestSettingsDto {
|
||||
@IsString()
|
||||
|
|
@ -25,6 +26,9 @@ export class PinterestSettingsDto {
|
|||
})
|
||||
@MinLength(1, {
|
||||
message: 'Board is required',
|
||||
})
|
||||
@JSONSchema({
|
||||
description: 'board must be an id',
|
||||
})
|
||||
board: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import { IsDefined, IsString, MinLength } from 'class-validator';
|
||||
import { JSONSchema } from 'class-validator-jsonschema';
|
||||
|
||||
export class SlackDto {
|
||||
@MinLength(1)
|
||||
@IsDefined()
|
||||
@IsString()
|
||||
@JSONSchema({
|
||||
description: 'Channel must be an id',
|
||||
})
|
||||
channel: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
import { IsOptional, Matches } from 'class-validator';
|
||||
import { IsIn, IsOptional, Matches } from 'class-validator';
|
||||
|
||||
export class XDto {
|
||||
@IsOptional()
|
||||
@Matches(/^(https:\/\/x\.com\/i\/communities\/\d+)?$/, {
|
||||
message: 'Invalid X community URL. It should be in the format: https://x.com/i/communities/1493446837214187523',
|
||||
message:
|
||||
'Invalid X community URL. It should be in the format: https://x.com/i/communities/1493446837214187523',
|
||||
})
|
||||
community?: string;
|
||||
|
||||
@IsIn(['everyone', 'following', 'mentionedUsers', 'subscribers', 'verified'])
|
||||
who_can_reply_post:
|
||||
| 'everyone'
|
||||
| 'following'
|
||||
| 'mentionedUsers'
|
||||
| 'subscribers'
|
||||
| 'verified';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ export class YoutubeSettingsDto {
|
|||
@IsDefined()
|
||||
type: string;
|
||||
|
||||
@IsIn(['yes', 'no'])
|
||||
@IsOptional()
|
||||
selfDeclaredMadeForKids: 'no' | 'yes';
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => MediaDto)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,40 @@ export class IntegrationManager {
|
|||
};
|
||||
}
|
||||
|
||||
getAllTools(): {
|
||||
[key: string]: {
|
||||
description: string;
|
||||
dataSchema: any;
|
||||
methodName: string;
|
||||
}[];
|
||||
} {
|
||||
return socialIntegrationList.reduce(
|
||||
(all, current) => ({
|
||||
...all,
|
||||
[current.identifier]:
|
||||
Reflect.getMetadata('custom:tool', current.constructor.prototype) ||
|
||||
[],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
getAllRulesDescription(): {
|
||||
[key: string]: string;
|
||||
} {
|
||||
return socialIntegrationList.reduce(
|
||||
(all, current) => ({
|
||||
...all,
|
||||
[current.identifier]:
|
||||
Reflect.getMetadata(
|
||||
'custom:rules:description',
|
||||
current.constructor
|
||||
) || '',
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
getAllPlugs() {
|
||||
return socialIntegrationList
|
||||
.map((p) => {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
|||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import axios from 'axios';
|
||||
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
async function reduceImageBySize(url: string, maxSizeKB = 976) {
|
||||
try {
|
||||
|
|
@ -141,6 +142,9 @@ async function uploadVideo(
|
|||
} satisfies AppBskyEmbedVideo.Main;
|
||||
}
|
||||
|
||||
@Rules(
|
||||
'Bluesky can have maximum 1 video or 4 pictures in one post, it can also be without attachments'
|
||||
)
|
||||
export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
||||
override maxConcurrentJob = 2; // Bluesky has moderate rate limits
|
||||
identifier = 'bluesky';
|
||||
|
|
@ -148,6 +152,9 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
isBetweenSteps = false;
|
||||
scopes = ['write:statuses', 'profile', 'write:media'];
|
||||
editor = 'normal' as const;
|
||||
maxLength() {
|
||||
return 300;
|
||||
}
|
||||
|
||||
async customFields() {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.ab
|
|||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
|
||||
export class DevToProvider extends SocialAbstract implements SocialProvider {
|
||||
override maxConcurrentJob = 3; // Dev.to has moderate publishing limits
|
||||
|
|
@ -16,6 +18,10 @@ export class DevToProvider extends SocialAbstract implements SocialProvider {
|
|||
isBetweenSteps = false;
|
||||
editor = 'markdown' as const;
|
||||
scopes = [] as string[];
|
||||
maxLength() {
|
||||
return 100000;
|
||||
}
|
||||
dto = DevToSettingsDto;
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(6);
|
||||
|
|
@ -30,7 +36,7 @@ export class DevToProvider extends SocialAbstract implements SocialProvider {
|
|||
if (body.indexOf('Canonical url has already been taken') > -1) {
|
||||
return {
|
||||
type: 'bad-body' as const,
|
||||
value: 'Canonical URL already exists'
|
||||
value: 'Canonical URL already exists',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +95,7 @@ export class DevToProvider extends SocialAbstract implements SocialProvider {
|
|||
}
|
||||
}
|
||||
|
||||
@Tool({ description: 'Tag list', dataSchema: [] })
|
||||
async tags(token: string) {
|
||||
const tags = await (
|
||||
await fetch('https://dev.to/api/tags?per_page=1000&page=1', {
|
||||
|
|
@ -101,6 +108,7 @@ export class DevToProvider extends SocialAbstract implements SocialProvider {
|
|||
return tags.map((p: any) => ({ value: p.id, label: p.name }));
|
||||
}
|
||||
|
||||
@Tool({ description: 'Organization list', dataSchema: [] })
|
||||
async organizations(token: string) {
|
||||
const orgs = await (
|
||||
await fetch('https://dev.to/api/articles/me/all?per_page=1000', {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import {
|
|||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { DiscordDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/discord.dto';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
|
||||
export class DiscordProvider extends SocialAbstract implements SocialProvider {
|
||||
override maxConcurrentJob = 5; // Discord has generous rate limits for webhook posting
|
||||
|
|
@ -15,6 +17,11 @@ export class DiscordProvider extends SocialAbstract implements SocialProvider {
|
|||
isBetweenSteps = false;
|
||||
editor = 'markdown' as const;
|
||||
scopes = ['identify', 'guilds'];
|
||||
maxLength() {
|
||||
return 1980;
|
||||
}
|
||||
dto = DiscordDto;
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
const { access_token, expires_in, refresh_token } = await (
|
||||
await this.fetch('https://discord.com/api/oauth2/token', {
|
||||
|
|
@ -110,6 +117,7 @@ export class DiscordProvider extends SocialAbstract implements SocialProvider {
|
|||
};
|
||||
}
|
||||
|
||||
@Tool({ description: 'Channels', dataSchema: [] })
|
||||
async channels(accessToken: string, params: any, id: string) {
|
||||
const list = await (
|
||||
await fetch(`https://discord.com/api/guilds/${id}/channels`, {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import FormData from 'form-data';
|
|||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { DribbbleDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dribbble.dto';
|
||||
import mime from 'mime-types';
|
||||
import { DiscordDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/discord.dto';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
|
||||
export class DribbbleProvider extends SocialAbstract implements SocialProvider {
|
||||
override maxConcurrentJob = 3; // Dribbble has moderate API limits
|
||||
|
|
@ -19,6 +21,10 @@ export class DribbbleProvider extends SocialAbstract implements SocialProvider {
|
|||
isBetweenSteps = false;
|
||||
scopes = ['public', 'upload'];
|
||||
editor = 'normal' as const;
|
||||
maxLength() {
|
||||
return 40000;
|
||||
}
|
||||
dto = DribbbleDto;
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
const { access_token, expires_in } = await (
|
||||
|
|
@ -59,6 +65,7 @@ export class DribbbleProvider extends SocialAbstract implements SocialProvider {
|
|||
};
|
||||
}
|
||||
|
||||
@Tool({ description: 'Teams list', dataSchema: [] })
|
||||
async teams(accessToken: string) {
|
||||
const { teams } = await (
|
||||
await this.fetch('https://api.dribbble.com/v2/user', {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
|||
import dayjs from 'dayjs';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { FacebookDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/facebook.dto';
|
||||
import { DribbbleDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dribbble.dto';
|
||||
|
||||
export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'facebook';
|
||||
|
|
@ -24,6 +25,10 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
|||
];
|
||||
override maxConcurrentJob = 3; // Facebook has reasonable rate limits
|
||||
editor = 'normal' as const;
|
||||
maxLength() {
|
||||
return 63206;
|
||||
}
|
||||
dto = FacebookDto;
|
||||
|
||||
override handleErrors(body: string):
|
||||
| {
|
||||
|
|
@ -78,7 +83,8 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
|||
if (body.indexOf('1404006') > -1) {
|
||||
return {
|
||||
type: 'bad-body' as const,
|
||||
value: "We couldn't post your comment, A security check in facebook required to proceed.",
|
||||
value:
|
||||
"We couldn't post your comment, A security check in facebook required to proceed.",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -233,11 +239,7 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
|||
.map((p: any) => p.permission);
|
||||
this.checkScopes(this.scopes, permissions);
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
picture
|
||||
} = await (
|
||||
const { id, name, picture } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/me?fields=id,name,picture&access_token=${access_token}`
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,11 +11,17 @@ import { NeynarAPIClient } from '@neynar/nodejs-sdk';
|
|||
import { Integration } from '@prisma/client';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import { groupBy } from 'lodash';
|
||||
import { FarcasterDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/farcaster.dto';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
const client = new NeynarAPIClient({
|
||||
apiKey: process.env.NEYNAR_SECRET_KEY || '00000000-000-0000-000-000000000000',
|
||||
});
|
||||
|
||||
@Rules(
|
||||
'Farcaster/Warpcast can only accept pictures'
|
||||
)
|
||||
export class FarcasterProvider
|
||||
extends SocialAbstract
|
||||
implements SocialProvider
|
||||
|
|
@ -27,6 +33,10 @@ export class FarcasterProvider
|
|||
scopes = [] as string[];
|
||||
override maxConcurrentJob = 3; // Farcaster has moderate limits
|
||||
editor = 'normal' as const;
|
||||
maxLength() {
|
||||
return 800;
|
||||
}
|
||||
dto = FarcasterDto;
|
||||
|
||||
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
|
||||
return {
|
||||
|
|
@ -69,7 +79,7 @@ export class FarcasterProvider
|
|||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[]
|
||||
postDetails: PostDetails<FarcasterDto>[]
|
||||
): Promise<PostResponse[]> {
|
||||
const ids = [];
|
||||
const subreddit =
|
||||
|
|
@ -115,6 +125,10 @@ export class FarcasterProvider
|
|||
return list;
|
||||
}
|
||||
|
||||
@Tool({
|
||||
description: 'Search channels',
|
||||
dataSchema: [{ key: 'word', type: 'string', description: 'Search word' }],
|
||||
})
|
||||
async subreddits(
|
||||
accessToken: string,
|
||||
data: any,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { HashnodeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/provid
|
|||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
|
||||
export class HashnodeProvider extends SocialAbstract implements SocialProvider {
|
||||
override maxConcurrentJob = 3; // Hashnode has lenient publishing limits
|
||||
|
|
@ -19,6 +20,10 @@ export class HashnodeProvider extends SocialAbstract implements SocialProvider {
|
|||
isBetweenSteps = false;
|
||||
scopes = [] as string[];
|
||||
editor = 'markdown' as const;
|
||||
maxLength() {
|
||||
return 10000;
|
||||
}
|
||||
dto = HashnodeSettingsDto;
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(6);
|
||||
|
|
@ -103,6 +108,12 @@ export class HashnodeProvider extends SocialAbstract implements SocialProvider {
|
|||
return tags.map((tag) => ({ value: tag.objectID, label: tag.name }));
|
||||
}
|
||||
|
||||
@Tool({ description: 'Tags', dataSchema: [] })
|
||||
tagsList() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
@Tool({ description: 'Publications', dataSchema: [] })
|
||||
async publications(accessToken: string) {
|
||||
const {
|
||||
data: {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import dayjs from 'dayjs';
|
|||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
@Rules(
|
||||
"Instagram should have at least one attachment, if it's a story, it can have only one picture"
|
||||
)
|
||||
export class InstagramProvider
|
||||
extends SocialAbstract
|
||||
implements SocialProvider
|
||||
|
|
@ -31,6 +35,10 @@ export class InstagramProvider
|
|||
];
|
||||
override maxConcurrentJob = 10;
|
||||
editor = 'normal' as const;
|
||||
dto = InstagramDto;
|
||||
maxLength() {
|
||||
return 2200;
|
||||
}
|
||||
|
||||
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
|
||||
return {
|
||||
|
|
@ -50,7 +58,6 @@ export class InstagramProvider
|
|||
value: string;
|
||||
}
|
||||
| undefined {
|
||||
|
||||
if (body.indexOf('An unknown error occurred') > -1) {
|
||||
return {
|
||||
type: 'retry' as const,
|
||||
|
|
@ -66,7 +73,9 @@ export class InstagramProvider
|
|||
};
|
||||
}
|
||||
|
||||
if (body.toLowerCase().indexOf('the user is not an instagram business') > -1) {
|
||||
if (
|
||||
body.toLowerCase().indexOf('the user is not an instagram business') > -1
|
||||
) {
|
||||
return {
|
||||
type: 'refresh-token' as const,
|
||||
value:
|
||||
|
|
@ -368,11 +377,7 @@ export class InstagramProvider
|
|||
.map((p: any) => p.permission);
|
||||
this.checkScopes(this.scopes, permissions);
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
picture
|
||||
} = await (
|
||||
const { id, name, picture } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/me?fields=id,name,picture&access_token=${access_token}`
|
||||
)
|
||||
|
|
@ -507,7 +512,7 @@ export class InstagramProvider
|
|||
undefined,
|
||||
'',
|
||||
0,
|
||||
true,
|
||||
true
|
||||
)
|
||||
).json();
|
||||
await timer(30000);
|
||||
|
|
@ -630,44 +635,44 @@ export class InstagramProvider
|
|||
|
||||
private setTitle(name: string) {
|
||||
switch (name) {
|
||||
case "likes": {
|
||||
case 'likes': {
|
||||
return 'Likes';
|
||||
}
|
||||
|
||||
case "followers": {
|
||||
case 'followers': {
|
||||
return 'Followers';
|
||||
}
|
||||
|
||||
case "reach": {
|
||||
case 'reach': {
|
||||
return 'Reach';
|
||||
}
|
||||
|
||||
case "follower_count": {
|
||||
case 'follower_count': {
|
||||
return 'Follower Count';
|
||||
}
|
||||
|
||||
case "views": {
|
||||
case 'views': {
|
||||
return 'Views';
|
||||
}
|
||||
|
||||
case "comments": {
|
||||
case 'comments': {
|
||||
return 'Comments';
|
||||
}
|
||||
|
||||
case "shares": {
|
||||
case 'shares': {
|
||||
return 'Shares';
|
||||
}
|
||||
|
||||
case "saves": {
|
||||
case 'saves': {
|
||||
return 'Saves';
|
||||
}
|
||||
|
||||
case "replies": {
|
||||
case 'replies': {
|
||||
return 'Replies';
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
|
||||
async analytics(
|
||||
|
|
|
|||
|
|
@ -10,9 +10,13 @@ import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.ab
|
|||
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
|
||||
import { InstagramProvider } from '@gitroom/nestjs-libraries/integrations/social/instagram.provider';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
const instagramProvider = new InstagramProvider();
|
||||
|
||||
@Rules(
|
||||
"Instagram should have at least one attachment, if it's a story, it can have only one picture"
|
||||
)
|
||||
export class InstagramStandaloneProvider
|
||||
extends SocialAbstract
|
||||
implements SocialProvider
|
||||
|
|
@ -27,10 +31,18 @@ export class InstagramStandaloneProvider
|
|||
'instagram_business_manage_insights',
|
||||
];
|
||||
override maxConcurrentJob = 10; // Instagram standalone has stricter limits
|
||||
dto = InstagramDto;
|
||||
|
||||
editor = 'normal' as const;
|
||||
maxLength() {
|
||||
return 2200;
|
||||
}
|
||||
|
||||
public override handleErrors(body: string): { type: "refresh-token" | "bad-body" | "retry"; value: string } | undefined {
|
||||
public override handleErrors(
|
||||
body: string
|
||||
):
|
||||
| { type: 'refresh-token' | 'bad-body' | 'retry'; value: string }
|
||||
| undefined {
|
||||
return instagramProvider.handleErrors(body);
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +53,12 @@ export class InstagramStandaloneProvider
|
|||
)
|
||||
).json();
|
||||
|
||||
const { user_id, name, username, profile_picture_url = '' } = await (
|
||||
const {
|
||||
user_id,
|
||||
name,
|
||||
username,
|
||||
profile_picture_url = '',
|
||||
} = await (
|
||||
await fetch(
|
||||
`https://graph.instagram.com/v21.0/me?fields=user_id,username,name,profile_picture_url&access_token=${access_token}`
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { Integration } from '@prisma/client';
|
|||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import { LemmySettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/lemmy.dto';
|
||||
import { groupBy } from 'lodash';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
|
||||
export class LemmyProvider extends SocialAbstract implements SocialProvider {
|
||||
override maxConcurrentJob = 3; // Lemmy instances typically have moderate limits
|
||||
|
|
@ -19,6 +20,10 @@ export class LemmyProvider extends SocialAbstract implements SocialProvider {
|
|||
isBetweenSteps = false;
|
||||
scopes = [] as string[];
|
||||
editor = 'normal' as const;
|
||||
maxLength() {
|
||||
return 10000;
|
||||
}
|
||||
dto = LemmySettingsDto;
|
||||
|
||||
async customFields() {
|
||||
return [
|
||||
|
|
@ -144,13 +149,30 @@ export class LemmyProvider extends SocialAbstract implements SocialProvider {
|
|||
const valueArray: PostResponse[] = [];
|
||||
|
||||
for (const lemmy of firstPost.settings.subreddit) {
|
||||
console.log({
|
||||
community_id: +lemmy.value.id,
|
||||
name: lemmy.value.title,
|
||||
body: firstPost.message,
|
||||
...(lemmy.value.url ? { url: lemmy.value.url } : {}),
|
||||
...(firstPost.media?.length
|
||||
? { custom_thumbnail: firstPost.media[0].path }
|
||||
: {}),
|
||||
nsfw: false,
|
||||
});
|
||||
const { post_view, ...all } = await (
|
||||
await fetch(body.service + '/api/v3/post', {
|
||||
body: JSON.stringify({
|
||||
community_id: +lemmy.value.id,
|
||||
name: lemmy.value.title,
|
||||
body: firstPost.message,
|
||||
...(lemmy.value.url ? { url: lemmy.value.url } : {}),
|
||||
...(lemmy.value.url
|
||||
? {
|
||||
url:
|
||||
lemmy.value.url.indexOf('http') === -1
|
||||
? `https://${lemmy.value.url}`
|
||||
: lemmy.value.url,
|
||||
}
|
||||
: {}),
|
||||
...(firstPost.media?.length
|
||||
? { custom_thumbnail: firstPost.media[0].path }
|
||||
: {}),
|
||||
|
|
@ -203,6 +225,16 @@ export class LemmyProvider extends SocialAbstract implements SocialProvider {
|
|||
}));
|
||||
}
|
||||
|
||||
@Tool({
|
||||
description: 'Search for Lemmy communities by keyword',
|
||||
dataSchema: [
|
||||
{
|
||||
key: 'word',
|
||||
type: 'string',
|
||||
description: 'Keyword to search for',
|
||||
},
|
||||
],
|
||||
})
|
||||
async subreddits(
|
||||
accessToken: string,
|
||||
data: any,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import dayjs from 'dayjs';
|
|||
import { Integration } from '@prisma/client';
|
||||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
@Rules(
|
||||
'LinkedIn can have maximum one attachment when selecting video, when choosing a carousel on LinkedIn minimum amount of attachment must be two, and only pictures, if uploading a video, LinkedIn can have only one attachment'
|
||||
)
|
||||
export class LinkedinPageProvider
|
||||
extends LinkedinProvider
|
||||
implements SocialProvider
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ import { PostPlug } from '@gitroom/helpers/decorators/post.plug';
|
|||
import { LinkedinDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/linkedin.dto';
|
||||
import imageToPDF from 'image-to-pdf';
|
||||
import { Readable } from 'stream';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
@Rules(
|
||||
'LinkedIn can have maximum one attachment when selecting video, when choosing a carousel on LinkedIn minimum amount of attachment must be two, and only pictures, if uploading a video, LinkedIn can have only one attachment'
|
||||
)
|
||||
export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'linkedin';
|
||||
name = 'LinkedIn';
|
||||
|
|
@ -33,7 +37,9 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
override maxConcurrentJob = 2; // LinkedIn has professional posting limits
|
||||
refreshWait = true;
|
||||
editor = 'normal' as const;
|
||||
|
||||
maxLength() {
|
||||
return 3000;
|
||||
}
|
||||
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
|
||||
const {
|
||||
access_token: accessToken,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { Integration } from '@prisma/client';
|
|||
import { ListmonkDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/listmonk.dto';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import slugify from 'slugify';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
|
||||
export class ListmonkProvider extends SocialAbstract implements SocialProvider {
|
||||
override maxConcurrentJob = 100; // Bluesky has moderate rate limits
|
||||
|
|
@ -19,6 +20,11 @@ export class ListmonkProvider extends SocialAbstract implements SocialProvider {
|
|||
isBetweenSteps = false;
|
||||
scopes = [] as string[];
|
||||
editor = 'html' as const;
|
||||
dto = ListmonkDto;
|
||||
|
||||
maxLength() {
|
||||
return 100000000;
|
||||
}
|
||||
|
||||
async customFields() {
|
||||
return [
|
||||
|
|
@ -104,6 +110,7 @@ export class ListmonkProvider extends SocialAbstract implements SocialProvider {
|
|||
}
|
||||
}
|
||||
|
||||
@Tool({ description: 'List of available lists', dataSchema: [] })
|
||||
async list(
|
||||
token: string,
|
||||
data: any,
|
||||
|
|
@ -130,6 +137,7 @@ export class ListmonkProvider extends SocialAbstract implements SocialProvider {
|
|||
return postTypes.data.results.map((p: any) => ({ id: p.id, name: p.name }));
|
||||
}
|
||||
|
||||
@Tool({ description: 'List of available templates', dataSchema: [] })
|
||||
async templates(
|
||||
token: string,
|
||||
data: any,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ export class MastodonProvider extends SocialAbstract implements SocialProvider {
|
|||
isBetweenSteps = false;
|
||||
scopes = ['write:statuses', 'profile', 'write:media'];
|
||||
editor = 'normal' as const;
|
||||
maxLength() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.ab
|
|||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { MediumSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
|
||||
export class MediumProvider extends SocialAbstract implements SocialProvider {
|
||||
override maxConcurrentJob = 3; // Medium has lenient publishing limits
|
||||
|
|
@ -16,6 +18,10 @@ export class MediumProvider extends SocialAbstract implements SocialProvider {
|
|||
isBetweenSteps = false;
|
||||
scopes = [] as string[];
|
||||
editor = 'markdown' as const;
|
||||
dto = MediumSettingsDto;
|
||||
maxLength() {
|
||||
return 100000;
|
||||
}
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(6);
|
||||
|
|
@ -80,6 +86,7 @@ export class MediumProvider extends SocialAbstract implements SocialProvider {
|
|||
}
|
||||
}
|
||||
|
||||
@Tool({ description: 'List of publications', dataSchema: [] })
|
||||
async publications(accessToken: string, _: any, id: string) {
|
||||
const { data } = await (
|
||||
await fetch(`https://api.medium.com/v1/users/${id}/publications`, {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ export class NostrProvider extends SocialAbstract implements SocialProvider {
|
|||
scopes = [] as string[];
|
||||
editor = 'normal' as const;
|
||||
|
||||
maxLength() {
|
||||
return 100000;
|
||||
}
|
||||
|
||||
async customFields() {
|
||||
return [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,7 +12,12 @@ import FormData from 'form-data';
|
|||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import dayjs from 'dayjs';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
@Rules(
|
||||
'Pinterest requires at least one media, if posting a video, you must have two attachment, one for video, one for the cover picture, When posting a video, there can be only one'
|
||||
)
|
||||
export class PinterestProvider
|
||||
extends SocialAbstract
|
||||
implements SocialProvider
|
||||
|
|
@ -28,6 +33,11 @@ export class PinterestProvider
|
|||
'user_accounts:read',
|
||||
];
|
||||
override maxConcurrentJob = 3; // Pinterest has more lenient rate limits
|
||||
maxLength() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
dto = PinterestSettingsDto;
|
||||
|
||||
editor = 'normal' as const;
|
||||
|
||||
|
|
@ -146,6 +156,7 @@ export class PinterestProvider
|
|||
};
|
||||
}
|
||||
|
||||
@Tool({ description: 'List of boards', dataSchema: [] })
|
||||
async boards(accessToken: string) {
|
||||
const { items } = await (
|
||||
await fetch('https://api.pinterest.com/v5/boards', {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.ab
|
|||
import { lookup } from 'mime-types';
|
||||
import axios from 'axios';
|
||||
import WebSocket from 'ws';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
|
||||
// @ts-ignore
|
||||
global.WebSocket = WebSocket;
|
||||
|
|
@ -23,6 +24,11 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
|
|||
isBetweenSteps = false;
|
||||
scopes = ['read', 'identity', 'submit', 'flair'];
|
||||
editor = 'normal' as const;
|
||||
dto = RedditSettingsDto;
|
||||
|
||||
maxLength() {
|
||||
return 10000;
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
const {
|
||||
|
|
@ -319,6 +325,16 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
|
|||
}));
|
||||
}
|
||||
|
||||
@Tool({
|
||||
description: 'Get list of subreddits with information',
|
||||
dataSchema: [
|
||||
{
|
||||
key: 'word',
|
||||
type: 'string',
|
||||
description: 'Search subreddit by string',
|
||||
},
|
||||
],
|
||||
})
|
||||
async subreddits(accessToken: string, data: any) {
|
||||
const {
|
||||
data: { children },
|
||||
|
|
@ -367,6 +383,16 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
|
|||
return permissions;
|
||||
}
|
||||
|
||||
@Tool({
|
||||
description: 'Get list of flairs and restrictions for a subreddit',
|
||||
dataSchema: [
|
||||
{
|
||||
key: 'subreddit',
|
||||
type: 'string',
|
||||
description: 'Search flairs and restrictions by subreddit key should be "/r/[name]"',
|
||||
},
|
||||
],
|
||||
})
|
||||
async restrictions(accessToken: string, data: { subreddit: string }) {
|
||||
const {
|
||||
data: { submission_type, allow_images, ...all2 },
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue