Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,28 @@ EXCLUDE_QUEUE=""
# Concurrency divider for worker task allocation (default: 1)
WORKER_CONCURRENCY_DIVIDER="1"

## Remember to set your public internet IP address in the allow-list for the API token.
##
## Cloudflare is currently required to save things like social media avatars for accounts.
CLOUDFLARE_ACCOUNT_ID="your-account-id"
CLOUDFLARE_ACCESS_KEY="your-access-key"
CLOUDFLARE_SECRET_ACCESS_KEY="your-secret-access-key"
CLOUDFLARE_BUCKETNAME="your-bucket-name"
CLOUDFLARE_BUCKET_URL="https://your-bucket-url.r2.cloudflarestorage.com/"
CLOUDFLARE_REGION="auto"
## Optional media normalization (postiz-uploader on RunPod Serverless). Requires STORAGE_PROVIDER="cloudflare".
## When set, web uploads are transcoded to 1080p h264 mp4 / downsized images in the background;
## the media record reports status "processing" until the normalized file replaces the original.
#RUNPOD_API_KEY=""
#RUNPOD_ENDPOINT_ID=""
## Optional video clipping (YouTube video -> captioned vertical clips -> draft posts). Requires STORAGE_PROVIDER="cloudflare",
## RUNPOD_API_KEY (with access to both endpoints below) and OPENAI_API_KEY. Ingest jobs go to the CPU endpoint, clip jobs to the
## GPU endpoint of postiz-uploader (which needs its own Oxylabs account to fetch from YouTube); Deepgram transcribes videos
## that have no usable captions.
#RUNPOD_INGEST_ENDPOINT_ID=""
#RUNPOD_CLIPPER_ENDPOINT_ID=""
#DEEPGRAM_API_KEY=""

# ==============================================================================
# 4. Storage Providers (Cloudflare R2 or Local Disk)
# ==============================================================================
Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ We consider an issue a vulnerability when it is a weakness in an in-scope, suppo

## Contacting Us

All security correspondence goes through [GAdvisory](https://postiz.gadvisory.org/request), our security advisory platform. The form routes four types of request:
All security reports and related correspondence must be submitted through [GAdvisory](https://postiz.gadvisory.org/request), our security advisory platform. The form routes four types of request:

- **Report a vulnerability.** See [Reporting Security Vulnerabilities](#reporting-security-vulnerabilities).
- **Dispute a CVE or Advisory.** Challenge the validity of a record we published. Disputes and their resolution are public and permanent. Our CVE Record Dispute Policy is linked from this flow.
Expand All @@ -64,7 +64,7 @@ If you discover a security vulnerability in the Postiz app, report it through [G
- Steps to reproduce the vulnerability
- Any relevant code or configuration files

If the report has immediate urgency, please also contact one (or more) of the maintainers via email:
Email the maintainers only when the report requires immediate, time-critical attention. Email does not replace submitting the report through GAdvisory.

- @egelhaus ([E-Mail](mailto:egelhaus@ennogelhaus.de))

Expand Down
15 changes: 14 additions & 1 deletion apps/backend/src/api/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integ
import { SettingsController } from '@gitroom/backend/api/routes/settings.controller';
import { PostsController } from '@gitroom/backend/api/routes/posts.controller';
import { MediaController } from '@gitroom/backend/api/routes/media.controller';
import { ClippingController } from '@gitroom/backend/api/routes/clipping.controller';
import { MediaWidgetController } from '@gitroom/backend/api/routes/media.widget.controller';
import { UploadWidgetAuthMiddleware } from '@gitroom/backend/services/auth/upload.widget.auth.middleware';
import { ClippingWidgetController } from '@gitroom/backend/api/routes/clipping.widget.controller';
import { ClippingWidgetAuthMiddleware } from '@gitroom/backend/services/auth/clipping.widget.auth.middleware';
import { UploadModule } from '@gitroom/nestjs-libraries/upload/upload.module';
import { BillingController } from '@gitroom/backend/api/routes/billing.controller';
import { NotificationsController } from '@gitroom/backend/api/routes/notifications.controller';
Expand Down Expand Up @@ -64,6 +67,7 @@ const authenticatedController = [
SettingsController,
PostsController,
MediaController,
ClippingController,
BillingController,
NotificationsController,
CopilotController,
Expand All @@ -81,7 +85,12 @@ const authenticatedController = [
@Module({
imports: [UploadModule, EcosystemModule],
controllers: process.env.MCP_ONLY
? [RootController, OAuthController, MediaWidgetController]
? [
RootController,
OAuthController,
MediaWidgetController,
ClippingWidgetController,
]
: [
RootController,
PaymentController,
Expand All @@ -93,6 +102,7 @@ const authenticatedController = [
NoAuthIntegrationsController,
OAuthController,
MediaWidgetController,
ClippingWidgetController,
...authenticatedController,
],
providers: [
Expand Down Expand Up @@ -129,5 +139,8 @@ export class ApiModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(AuthMiddleware).forRoutes(...authenticatedController);
consumer.apply(UploadWidgetAuthMiddleware).forRoutes(MediaWidgetController);
consumer
.apply(ClippingWidgetAuthMiddleware)
.forRoutes(ClippingWidgetController);
}
}
33 changes: 33 additions & 0 deletions apps/backend/src/api/routes/clipping.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Organization } from '@prisma/client';
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
import { ClippingService } from '@gitroom/nestjs-libraries/database/prisma/clipping/clipping.service';
import { ClippingDto } from '@gitroom/nestjs-libraries/dtos/clipping/clipping.dto';

@ApiTags('Clipping')
@Controller('/clipping')
export class ClippingController {
constructor(private _clippingService: ClippingService) {}

@Post('/')
startClipping(
@GetOrgFromRequest() org: Organization,
@Body() body: ClippingDto
) {
return this._clippingService.startClipping(org, body);
}

@Get('/')
getClippings(
@GetOrgFromRequest() org: Organization,
@Query('page') page: number
) {
return this._clippingService.getClippings(org.id, page);
}

@Get('/:id')
getClipping(@GetOrgFromRequest() org: Organization, @Param('id') id: string) {
return this._clippingService.getClipping(org.id, id);
}
}
27 changes: 27 additions & 0 deletions apps/backend/src/api/routes/clipping.widget.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Controller, Get, Header, Query, Req } from '@nestjs/common';
import { Request } from 'express';
import { ApiTags } from '@nestjs/swagger';
import { Organization } from '@prisma/client';
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
import { ClippingService } from '@gitroom/nestjs-libraries/database/prisma/clipping/clipping.service';

@ApiTags('Clipping')
@Controller('/clipping-widget')
export class ClippingWidgetController {
constructor(private _clippingService: ClippingService) {}

@Get('/status')
@Header('Cache-Control', 'no-store')
status(
@GetOrgFromRequest() org: Organization,
@Req() req: Request,
@Query('seen') seen?: string
) {
return this._clippingService.getWidgetProgress(
org.id,
// @ts-ignore
req.clippingId,
!!seen
);
}
}
34 changes: 34 additions & 0 deletions apps/backend/src/services/auth/clipping.widget.auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { HttpStatus, Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { ClippingService } from '@gitroom/nestjs-libraries/database/prisma/clipping/clipping.service';

// The MCP clipping widget runs in the host's sandboxed iframe (a foreign origin
// without our cookies), so it authenticates with a short-lived ticket that only
// opens the clipping it was made for
@Injectable()
export class ClippingWidgetAuthMiddleware implements NestMiddleware {
constructor(private _clippingService: ClippingService) {}
async use(req: Request, res: Response, next: NextFunction) {
// Not part of the global cors() allowlist on purpose: that one allows
// credentials, and the sandbox origins are shared with every other connector.
// The global cors() answers every preflight itself, so the widget has to stay
// on "simple" requests (GET, no custom headers)
res.setHeader('Access-Control-Allow-Origin', '*');

const ticket =
typeof req.query.ticket === 'string' &&
(await this._clippingService.getWidgetTicket(req.query.ticket));
if (!ticket) {
res
.status(HttpStatus.UNAUTHORIZED)
.json({ msg: 'Clipping ticket not found or expired' });
return;
}

// @ts-ignore
req.org = { id: ticket.org };
// @ts-ignore
req.clippingId = ticket.id;
next();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export enum Sections {
CHANNEL = 'channel',
POSTS_PER_MONTH = 'posts_per_month',
VIDEOS_PER_MONTH = 'videos_per_month',
CLIPPING_MINUTES = 'clipping_minutes',
TEAM_MEMBERS = 'team_members',
COMMUNITY_FEATURES = 'community_features',
FEATURED_BY_GITROOM = 'featured_by_gitroom',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,10 @@ const getErrorMessage = (error: {
default:
return 'You have reached the maximum number of generated videos for your subscription. Please upgrade your subscription to generate more videos.';
}
case Sections.CLIPPING_MINUTES:
switch (error.action) {
default:
return 'You have used all the clipping minutes of your subscription for this month. Please upgrade your subscription to clip more videos.';
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,13 @@ export const BillingFeatures: FC<{ tier: string }> = ({ tier }) => {
prefix: currentPricing?.generate_videos,
});
}
if (currentPricing?.clipping_minutes) {
list.push({
key: 'billing_clipping_minutes_per_month',
defaultValue: 'minutes of AI video clipping per month',
prefix: currentPricing?.clipping_minutes,
});
}
return list;
}, [tier]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ export const Features: FC<{
if (currentPricing?.generate_videos) {
list.push(`${currentPricing?.generate_videos} AI Videos per month`);
}
if (currentPricing?.clipping_minutes) {
list.push(
`${currentPricing?.clipping_minutes} minutes of AI video clipping per month`
);
}
return list;
}, [pack]);
return (
Expand Down
100 changes: 100 additions & 0 deletions apps/orchestrator/src/activities/clipping.activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Injectable } from '@nestjs/common';
import { Activity, ActivityMethod } from 'nestjs-temporal-core';
import { ClippingService } from '@gitroom/nestjs-libraries/database/prisma/clipping/clipping.service';

// An activity on main can never change its parameters, so every one takes a
// single object of ids: the state lives on the clipping records, and a field
// can be added to the object without a new activity
@Injectable()
@Activity()
export class ClippingActivity {
constructor(private _clippingService: ClippingService) {}

@ActivityMethod()
async submitClippingAnalyse({ clippingId }: { clippingId: string }) {
return this._clippingService.submitAnalyse(clippingId);
}

@ActivityMethod()
async checkClippingAnalyse({
clippingId,
jobId,
}: {
clippingId: string;
jobId: string;
}) {
return this._clippingService.checkAnalyse(clippingId, jobId);
}

@ActivityMethod()
async transcribeClipping({ clippingId }: { clippingId: string }) {
return this._clippingService.transcribe(clippingId);
}

@ActivityMethod()
async pickClippingClips({ clippingId }: { clippingId: string }) {
return this._clippingService.pickClips(clippingId);
}

@ActivityMethod()
async submitClipFetch({ clipId }: { clipId: string }) {
return this._clippingService.submitClipFetch(clipId);
}

@ActivityMethod()
async checkClipFetch({ clipId, jobId }: { clipId: string; jobId: string }) {
return this._clippingService.checkClipFetch(clipId, jobId);
}

@ActivityMethod()
async captionClip({ clipId }: { clipId: string }) {
return this._clippingService.captionClip(clipId);
}

@ActivityMethod()
async submitClipRender({ clipId }: { clipId: string }) {
return this._clippingService.submitClipRender(clipId);
}

@ActivityMethod()
async checkClipRender({ clipId, jobId }: { clipId: string; jobId: string }) {
return this._clippingService.checkClipRender(clipId, jobId);
}

// "customer" says the error was written for the customer to read
@ActivityMethod()
async failClip({
clipId,
error,
customer,
}: {
clipId: string;
error: string;
customer?: boolean;
}) {
return this._clippingService.failClip(clipId, error, customer);
}

@ActivityMethod()
async createClippingDrafts({ clippingId }: { clippingId: string }) {
return this._clippingService.createDrafts(clippingId);
}

@ActivityMethod()
async finishClipping({ clippingId }: { clippingId: string }) {
return this._clippingService.finishClipping(clippingId);
}

@ActivityMethod()
async failClipping({
clippingId,
error,
customer,
}: {
clippingId: string;
error: string;
customer?: boolean;
}) {
return this._clippingService.failClipping(clippingId, error, customer);
}
}
2 changes: 2 additions & 0 deletions apps/orchestrator/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { EmailActivity } from '@gitroom/orchestrator/activities/email.activity';
import { IntegrationsActivity } from '@gitroom/orchestrator/activities/integrations.activity';
import { VideoActivity } from '@gitroom/orchestrator/activities/video.activity';
import { MediaActivity } from '@gitroom/orchestrator/activities/media.activity';
import { ClippingActivity } from '@gitroom/orchestrator/activities/clipping.activity';
import { VideoModule } from '@gitroom/nestjs-libraries/videos/video.module';
import { HealthController } from '@gitroom/orchestrator/health.controller';

Expand All @@ -17,6 +18,7 @@ const activities = [
IntegrationsActivity,
VideoActivity,
MediaActivity,
ClippingActivity,
];
@Module({
imports: [
Expand Down
Loading
Loading